#import "TuneDatabase.h"
#import "Playlist.h"

@implementation TuneDatabase

@synthesize tunes, playlists, database, tuneSortOrder;

- (void)dealloc
{
  [tunes release];
  [super dealloc];
}

- (void)createEditableCopyOfDatabaseIfNeeded {
  BOOL success;
  NSFileManager *fileManager = [NSFileManager defaultManager];
  NSError *error;
#if TARGET_OS_IPHONE
  //FIXME-duplicated code from below....
  NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
  NSString *documentsDirectory = [paths objectAtIndex:0];
  NSString *writableDBPath = [documentsDirectory stringByAppendingPathComponent:@"tunedb.sql"];
#else
#if (MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_5)
  NSString *documentsDirectory = [NSHomeDirectory() stringByAppendingPathComponent:@"Music"];
#else
  NSArray *paths = NSSearchPathForDirectoriesInDomains(NSMusicDirectory, NSUserDomainMask, YES);
  NSString *documentsDirectory = [paths objectAtIndex:0];
#endif
  NSString *writableDBPath = [documentsDirectory stringByAppendingPathComponent:@"Tunebook Database"];
#endif
  success = [fileManager fileExistsAtPath:writableDBPath];
  if ( success )
    return;

  NSString *defaultDBPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"tunedb.sql"];
  success = [fileManager copyItemAtPath:defaultDBPath toPath:writableDBPath error:&error];
  if (!success) {
    NSAssert1(0, @"Failed to create writable database file with message '%@'.", [error localizedDescription]);
  }
}

- (void)initializeDatabase {
  schemaVersion = 0;
  [self createEditableCopyOfDatabaseIfNeeded];

  NSMutableArray *tuneArray = [[NSMutableArray alloc] init];
  self.tunes = tuneArray;
  [tuneArray release];
  playlists = [[NSMutableArray alloc] init];
#if TARGET_OS_IPHONE
  NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
  NSString *documentsDirectory = [paths objectAtIndex:0];
  NSString *path = [documentsDirectory stringByAppendingPathComponent:@"tunedb.sql"];
#else
#if (MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_5)
  NSString *documentsDirectory = [NSHomeDirectory() stringByAppendingPathComponent:@"Music"];
#else
  NSArray *paths = NSSearchPathForDirectoriesInDomains(NSMusicDirectory, NSUserDomainMask, YES);
  NSString *documentsDirectory = [paths objectAtIndex:0];
#endif
  NSString *path = [documentsDirectory stringByAppendingPathComponent:@"Tunebook Database"];
#endif

  if ( sqlite3_open([path UTF8String], &database) == SQLITE_OK ) {
    const char *sql = "CREATE TABLE IF NOT EXISTS tunebook ('schemaVersion' INTEGER PRIMARY KEY);";
    if (sqlite3_exec(database, sql, NULL, NULL, NULL) != SQLITE_OK) {
      NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database));
    }
    sql = "SELECT schemaVersion from tunebook;";
    sqlite3_stmt *statement;
    if ( sqlite3_prepare_v2(database, sql, -1, &statement, NULL) == SQLITE_OK ) {
      while ( sqlite3_step(statement) == SQLITE_ROW ) {
        schemaVersion = sqlite3_column_int(statement, 0);
      }
    }
    sqlite3_finalize(statement);

    [Playlist initPlaylists:self];

    if ( schemaVersion == 0 ) {
      // Updating from version 0 to 1. Add sort order column for playlists.
      if (sqlite3_exec(database, "ALTER TABLE playlist ADD COLUMN 'sortOrder' INTEGER;", NULL, NULL, NULL) != SQLITE_OK) {
//        NSAssert1(0, @"Error: failed to prepare alter table statement with message '%s'.", sqlite3_errmsg(database));
      }
      if (sqlite3_exec(database, "UPDATE playlist SET sortOrder = pk;", NULL, NULL, NULL) != SQLITE_OK) {
//        NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database));
      }

      if (sqlite3_exec(database, "ALTER TABLE playlistlink ADD COLUMN 'sortOrder' INTEGER;", NULL, NULL, NULL) != SQLITE_OK) {
//        NSAssert1(0, @"Error: failed to prepare alter table statement with message '%s'.", sqlite3_errmsg(database));
      }

      sql = "INSERT INTO tunebook (schemaVersion) VALUES(?)";
      if (sqlite3_prepare_v2(database, sql, -1, &statement, NULL) != SQLITE_OK) {
        NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database));
      }
      sqlite3_bind_int(statement, 1, 1);
      int success = sqlite3_step(statement);
      sqlite3_finalize(statement);

//      if (sqlite3_exec(database, "UPDATE tunebook SET schemaVersion = 1;", NULL, NULL, NULL) != SQLITE_OK) {
//        NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database));
//      }
    }

    sql = "SELECT pk FROM tune";
    if ( sqlite3_prepare_v2(database, sql, -1, &statement, NULL) == SQLITE_OK ) {
      while ( sqlite3_step(statement) == SQLITE_ROW ) {
        int primaryKey = sqlite3_column_int(statement, 0);
        Tune *tune = [[Tune alloc] initWithPrimaryKey:primaryKey database:database];
        [tunes addObject:tune];
        [tune release];
      }
    }
    sqlite3_finalize(statement);

    sql = "SELECT pk FROM playlist ORDER BY sortOrder";
    if ( sqlite3_prepare_v2(database, sql, -1, &statement, NULL) == SQLITE_OK ) {
      while ( sqlite3_step(statement) == SQLITE_ROW ) {
        int primaryKey = sqlite3_column_int(statement, 0);
        Playlist *playlist = [[Playlist alloc] initWithPrimaryKey:primaryKey database:self];
        [playlists addObject:playlist];
        [playlist release];
      }
    }
    sqlite3_finalize(statement);
  } else {
    sqlite3_close(database);
    NSAssert1(0, @"Failed to open database with message '%s'.", sqlite3_errmsg(database));
  }

  [Tune finalizeStatements];
  [Playlist finalizeStatements];
}

- (void)startTransaction
{
  if (sqlite3_exec(database, "BEGIN TRANSACTION", NULL, NULL, NULL) != SQLITE_OK) {
    NSAssert1(0, @"Error: failed to begin transaction with message '%s'.", sqlite3_errmsg(database));
  }
}

- (void)commitTransaction
{
  if (sqlite3_exec(database, "COMMIT TRANSACTION", NULL, NULL, NULL) != SQLITE_OK) {
    NSAssert1(0, @"Error: failed to commit transaction with message '%s'.", sqlite3_errmsg(database));
  }
}

- (id)tuneWithPrimaryKey:(NSInteger)pk
{
  return [Tune tuneWithPrimaryKey:pk database:database];
}

- (void)removeTune:(Tune *)tune {
  [tune deleteFromDatabase];
  [tunes removeObject:tune];
}

- (void)addTune:(Tune *)tune {
  [tune insertIntoDatabase:database];
  [tunes addObject:tune];
}

- (void)deleteAllTunes {
  sqlite3_exec( database, "DELETE FROM tune", NULL, NULL, NULL );
  [tunes removeAllObjects];
  [[NSNotificationCenter defaultCenter] postNotificationName:@"databaseChanged" object:self]; 
}

- (void)sortTunesBy:(SortOrder)sortOrder {
  [Tune sortTunes:tunes by:sortOrder];
  [[NSNotificationCenter defaultCenter] postNotificationName:@"databaseChanged" object:self]; 
}

- (void)addPlaylist:(Playlist *)playlist {
  [playlist insertIntoDatabase:self];
  [playlists addObject:playlist];
  [[NSNotificationCenter defaultCenter] postNotificationName:@"playlistsChanged" object:self];
}

- (void)removePlaylist:(Playlist *)playlist {
  [playlist deleteFromDatabase];
  [playlists removeObject:playlist];
  [[NSNotificationCenter defaultCenter] postNotificationName:@"playlistsChanged" object:self];
}

- (void)searchForTunes:(NSString *)searchText tunes:(NSArray *)tunelist results:(NSMutableArray *)results
{
	if ( searchText.length > 0 ) {
    Tune *tune;
    NSArray *words = [searchText componentsSeparatedByString:@" "];
    int count = [words count];
    if ( count == 0 )
      return;

    int tsi = -1, tsn = 0, tsd = 0;
    for ( int j = 0; j < count; j++ ) {
      NSString *word = [words objectAtIndex:j];
      if ( sscanf( [word UTF8String], "%d/%d", &tsn, &tsd ) == 2 ) {
        tsi = j;
      }
    }
    
    for ( tune in tunelist ) {
      BOOL add = true;
      NSString *title = tune.title;
      NSString *key = tune.key;
#if KEYWORDS
      NSString *keyWords = tune.keyWords;
#endif

      if ( tsi != -1 && (tune.timeSigNum != tsn || tune.timeSigDen != tsd) )
        add = false;

      for ( int j = 0; add && j < count; j++ ) {
        NSString *word = [words objectAtIndex:j];
        if ( j != tsi && word.length != 0 ) {
          NSRange range;
          range.length = 0;
          range = [title rangeOfString:word options:NSCaseInsensitiveSearch|NSDiacriticInsensitiveSearch];
#if KEYWORDS
          if ( range.length == 0 && keyWords != nil )
            range = [keyWords rangeOfString:word options:NSCaseInsensitiveSearch|NSDiacriticInsensitiveSearch];
#endif
          if ( range.length == 0 ) {
            // Exact match
            if ( [key caseInsensitiveCompare:word] != NSOrderedSame )
              add = false;
          }
        }
      }
      if ( add )
        [results addObject:tune];
    }
  } else {
		[results addObjectsFromArray: tunelist];
  }
}

- (void)searchForTunesWithTitle:(NSString *)titleText key:(NSString *)keyText meter:(NSString *)meterText tunes:(NSArray *)tunelist results:(NSMutableArray *)results
{
	if ( titleText.length > 0 || keyText.length > 0 || meterText.length > 0 ) {
    Tune *tune;
    NSArray *titleWords = [titleText componentsSeparatedByString:@" "];
    NSArray *keyWords = [keyText componentsSeparatedByString:@" "];
    NSArray *meterWords = [meterText componentsSeparatedByString:@" "];
    int titleCount = [titleWords count];
    int keyCount = [keyWords count];
    int meterCount = [meterWords count];
    if ( meterCount > 16 )
      meterCount = 16;

    int tsn[16], tsd[16], tsCount = 0;
    for ( int j = 0; j < meterCount; j++ ) {
      NSString *word = [meterWords objectAtIndex:j];
      if ( sscanf( [word UTF8String], "%d/%d", &tsn[tsCount], &tsd[tsCount] ) == 2 ) {
        tsCount++;
      }
    }
    
    for ( tune in tunelist ) {
      BOOL add = false;
      NSString *title = tune.title;
      NSString *key = tune.key;
#if KEYWORDS
      NSString *keyWords = tune.keyWords;
#endif

      if ( meterText.length > 0 ) {
        for ( int j = 0; j < meterCount; j++ ) {
          if ( tune.timeSigNum == tsn[j] && tune.timeSigDen == tsd[j] ) {
            add = true;
            break;
          }
        }
      } else
        add = true;

      if ( add && titleText.length > 0 ) {
        BOOL matchTitle = false;
        for ( int j = 0; add && j < titleCount; j++ ) {
          NSString *word = [titleWords objectAtIndex:j];
          if ( word.length != 0 ) {
            NSRange range;
            range.length = 0;
            range = [title rangeOfString:word options:NSCaseInsensitiveSearch|NSDiacriticInsensitiveSearch];
            if ( range.length != 0 ) {
              matchTitle = true;
              break;
            }
          }
        }
        if ( !matchTitle )
          add = false;
      }

      if ( add && keyText.length > 0 ) {
        BOOL matchKey = false;
        for ( int j = 0; add && j < keyCount; j++ ) {
          NSString *word = [keyWords objectAtIndex:j];
          if ( word.length != 0 ) {
            NSRange range;
            range.length = 0;
            if ( [key caseInsensitiveCompare:word] == NSOrderedSame ) {
              matchKey = true;
              break;
            }
          }
        }
        if ( !matchKey )
          add = false;
      }

      if ( add )
        [results addObject:tune];
    }
  } else {
		[results addObjectsFromArray: tunelist];
  }
}

- (void)searchForDots:(NSString *)searchText tunes:(NSArray *)tunelist results:(NSMutableArray *)results
{
  sqlite3_stmt *search_statement = nil;
  if (search_statement == nil) {
    const char *sql = "SELECT pk FROM tune WHERE dots LIKE ?";
    if (sqlite3_prepare_v2(database, sql, -1, &search_statement, NULL) != SQLITE_OK) {
      NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database));
    }
  }

  searchText = [NSString stringWithFormat:@"%%%@%%", searchText];
  sqlite3_bind_text(search_statement, 1, [searchText UTF8String], -1, SQLITE_TRANSIENT);
  while (sqlite3_step(search_statement) == SQLITE_ROW) {
    int pk = sqlite3_column_int(search_statement, 0);
    Tune *tune = [Tune tuneWithPrimaryKey:pk database:database];
    if ( tune ) {
      [results addObject:tune];
      [tune release];
    }
  }

  sqlite3_reset(search_statement);

  if (search_statement) {
    sqlite3_finalize(search_statement);
    search_statement = nil;
  }
}

- (void)updateTunebookSortOrder
{
  [self startTransaction];
  int sortOrder = 0;
  sqlite3_stmt *statement;
  const char *sql = "UPDATE playlist SET sortOrder=? WHERE pk=?";
  if (sqlite3_prepare_v2(database, sql, -1, &statement, NULL) != SQLITE_OK) {
    NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database));
  }
  for ( Playlist* tunebook in playlists ) {
    sortOrder++;
    sqlite3_bind_int(statement, 1, sortOrder);
    sqlite3_bind_int(statement, 2, tunebook.primaryKey);

    int success = sqlite3_step(statement);
    sqlite3_reset(statement);
    if (success != SQLITE_DONE) {
      NSAssert1(0, @"Error: failed to update playlist with message '%s'.", sqlite3_errmsg(database));
    }
  }
  sqlite3_finalize(statement);
  [self commitTransaction];
}

- (void) close
{
  if (sqlite3_close(database) != SQLITE_OK) {
    NSAssert1(0, @"Error: failed to close database with message '%s'.", sqlite3_errmsg(database));
  }
}

@end

