#import "Tune.h"
#import "AppDelegate.h"

static sqlite3_stmt *insert_statement = nil;
static sqlite3_stmt *init_statement = nil;
static sqlite3_stmt *delete_statement = nil;
static sqlite3_stmt *delete_tunes_statement = nil;
static sqlite3_stmt *hydrate_statement = nil;
static sqlite3_stmt *dehydrate_statement = nil;

@implementation Tune

@synthesize numSharps, timeSigNum, timeSigDen, rating, selected, hydrated, temporary, order;

+ (void)finalizeStatements {
  if (insert_statement) {
    sqlite3_finalize(insert_statement);
    insert_statement = nil;
  }
  if (init_statement) {
    sqlite3_finalize(init_statement);
    init_statement = nil;
  }
  if (delete_statement) {
    sqlite3_finalize(delete_statement);
    delete_statement = nil;
  }
  if (delete_tunes_statement) {
    sqlite3_finalize(delete_tunes_statement);
    delete_tunes_statement = nil;
  }
  if (hydrate_statement) {
    sqlite3_finalize(hydrate_statement);
    hydrate_statement = nil;
  }
  if (dehydrate_statement) {
    sqlite3_finalize(dehydrate_statement);
    dehydrate_statement = nil;
  }
}

- (id)init {
  self = [super init];
  if ( self ) {
    primaryKey = 0;
    database = nil;
    temporary = NO;
  }
  return self;
}

static NSMutableDictionary *tuneIndex;//FIXME

- (id)initWithPrimaryKey:(NSInteger)pk database:(sqlite3 *)db {
  self = [super init];
  if ( self ) {
    primaryKey = pk;
    database = db;

    if (init_statement == nil) {
#if KEYWORDS
      const char *sql = "SELECT title, key, sharps, timesig, rating, keywords FROM tune WHERE pk=?";
#else
      const char *sql = "SELECT title, key, sharps, timesig, rating FROM tune WHERE pk=?";
#endif
      if (sqlite3_prepare_v2(database, sql, -1, &init_statement, NULL) != SQLITE_OK) {
        NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database));
      }
    }

    sqlite3_bind_int(init_statement, 1, primaryKey);
    if (sqlite3_step(init_statement) == SQLITE_ROW) {
      self.title = [NSString stringWithUTF8String:(char *)sqlite3_column_text(init_statement, 0)];
      const char *k = (const char *)sqlite3_column_text(init_statement, 1);
      if ( k )
        self.key = [NSString stringWithUTF8String:k];
      self.numSharps = sqlite3_column_int(init_statement, 2);
      int ts = sqlite3_column_int(init_statement, 3);
      self.timeSigNum = ts >> 8;
      self.timeSigDen = ts & 0xff;
      self.rating = sqlite3_column_int(init_statement, 4);
#if KEYWORDS
      self.keyWords = sqlite3_column_int(init_statement, 5);
#endif
      dirty = NO;
      hydrated = NO;
      if ( tuneIndex == nil )
        tuneIndex = [[NSMutableDictionary alloc] init]; 
      [tuneIndex setObject:self forKey:[NSNumber numberWithInt:primaryKey]];
    
    } else {
      [self dealloc];
      self = NULL;
    }

    sqlite3_reset(init_statement);
  }
  return self;
}

+ (id)tuneWithPrimaryKey:(NSInteger)pk database:(sqlite3 *)database
{
  if ( tuneIndex != nil )
    return [[tuneIndex objectForKey:[NSNumber numberWithInt:pk]] retain];
  return [[Tune alloc] initWithPrimaryKey:pk database:database];
}

- (BOOL)inDatabase
{
  return database != nil;
}

- (void)insertIntoDatabase:(sqlite3 *)db {
  database = db;

  if (insert_statement == nil) {
#if KEYWORDS
    static char *sql = "INSERT INTO tune (title, dots, timesig, key, sharps, rating, keywords) VALUES(?, ?, ?, ?, ?, ?, ?)";
#else
    static char *sql = "INSERT INTO tune (title, dots, timesig, key, sharps, rating) VALUES(?, ?, ?, ?, ?, ?)";
#endif
    if (sqlite3_prepare_v2(database, sql, -1, &insert_statement, NULL) != SQLITE_OK) {
      NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database));
    }
  }
  sqlite3_bind_text(insert_statement, 1, [title UTF8String], -1, SQLITE_TRANSIENT);
  sqlite3_bind_text(insert_statement, 2, [dots UTF8String], -1, SQLITE_TRANSIENT);
  sqlite3_bind_int(insert_statement, 3, (timeSigNum << 8) | timeSigDen);
  sqlite3_bind_text(insert_statement, 4, [key UTF8String], -1, SQLITE_TRANSIENT);
  sqlite3_bind_int(insert_statement, 5, numSharps);
  sqlite3_bind_int(insert_statement, 6, rating);
#if KEYWORDS
  sqlite3_bind_text(insert_statement, 7, [keyWords UTF8String], -1, SQLITE_TRANSIENT);
#endif
  int success = sqlite3_step(insert_statement);

  sqlite3_reset(insert_statement);
  if (success == SQLITE_ERROR) {
    NSAssert1(0, @"Error: failed to insert into the database with message '%s'.", sqlite3_errmsg(database));
  } else {
    primaryKey = sqlite3_last_insert_rowid(database);
    if ( tuneIndex == nil )
      tuneIndex = [[NSMutableDictionary alloc] init]; 
    [tuneIndex setObject:self forKey:[NSNumber numberWithInt:primaryKey]];
  }
  hydrated = YES;
}

- (void)dealloc {
  [title release];
  [key release];
  [dots release];
  [super dealloc];
}

- (void)deleteFromDatabase {
  if (delete_statement == nil) {
    const char *sql = "DELETE FROM tune WHERE pk=?";
    if (sqlite3_prepare_v2(database, sql, -1, &delete_statement, NULL) != SQLITE_OK) {
      NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database));
    }
  }
  sqlite3_bind_int(delete_statement, 1, primaryKey);
  int success = sqlite3_step(delete_statement);
  sqlite3_reset(delete_statement);
  if (success != SQLITE_DONE) {
    NSAssert1(0, @"Error: failed to delete from database with message '%s'.", sqlite3_errmsg(database));
  }
  [tuneIndex removeObjectForKey:[NSNumber numberWithInt:primaryKey]];

#if PLAYLISTS
  if (delete_tunes_statement == nil) {
    const char *sql = "DELETE FROM playlistlink WHERE tune=?";
    if (sqlite3_prepare_v2(database, sql, -1, &delete_tunes_statement, NULL) != SQLITE_OK) {
      NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database));
    }
  }
  sqlite3_bind_int(delete_tunes_statement, 1, primaryKey);
  success = sqlite3_step(delete_tunes_statement);
  sqlite3_reset(delete_tunes_statement);
  if (success != SQLITE_DONE) {
    NSAssert1(0, @"Error: failed to delete tunes from database with message '%s'.", sqlite3_errmsg(database));
  }
#endif
}

- (void)hydrate {
  if (hydrated)
    return;
  if (hydrate_statement == nil) {
    const char *sql = "SELECT dots FROM tune WHERE pk=?";
    if (sqlite3_prepare_v2(database, sql, -1, &hydrate_statement, NULL) != SQLITE_OK) {
      NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database));
    }
  }

  int success = sqlite3_bind_int(hydrate_statement, 1, primaryKey);
  success = sqlite3_step(hydrate_statement);
  if (success == SQLITE_ROW) {
    char *str = (char *)sqlite3_column_text(hydrate_statement, 0);
    if ( str ) {
      self.dots = [NSString stringWithUTF8String:str];
    } else
      self.dots = @"";
  } else {
    self.dots = @"Unknown";
  }
  sqlite3_reset(hydrate_statement);
  hydrated = YES;
  dirty = NO;
}

- (void)dehydrate {
  if (dirty && hydrated && database != nil) {
    if (dehydrate_statement == nil) {
      const char *sql = "UPDATE tune SET title=?, dots=?, timesig=?, key=?, sharps=?, rating=? WHERE pk=?";
      if (sqlite3_prepare_v2(database, sql, -1, &dehydrate_statement, NULL) != SQLITE_OK) {
        NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database));
      }
    }
    sqlite3_bind_text(dehydrate_statement, 1, [title UTF8String], -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(dehydrate_statement, 2, [dots UTF8String], -1, SQLITE_TRANSIENT);
    sqlite3_bind_int(dehydrate_statement, 3, (timeSigNum << 8) | timeSigDen);
    sqlite3_bind_text(dehydrate_statement, 4, [key UTF8String], -1, SQLITE_TRANSIENT);
    sqlite3_bind_int(dehydrate_statement, 5, numSharps);
    sqlite3_bind_int(dehydrate_statement, 6, rating);
    sqlite3_bind_int(dehydrate_statement, 7, primaryKey);

    int success = sqlite3_step(dehydrate_statement);
    sqlite3_reset(dehydrate_statement);
    if (success != SQLITE_DONE) {
      NSAssert1(0, @"Error: failed to dehydrate with message '%s'.", sqlite3_errmsg(database));
    }
    dirty = NO;
  }

  [dots release];
  dots = nil;
  [data release];
  data = nil;
  hydrated = NO;
}

#pragma mark Properties

- (NSInteger)primaryKey {
  return primaryKey;
}

- (NSString *)title {
  return title;
}

- (void)setTitle:(NSString *)aString {
  if ((!title && !aString) || (title && aString && [title isEqualToString:aString]))
    return;
  dirty = YES;
  [title release];
  title = [aString copy];
}

- (NSString *)dots {
  return dots;
}

- (void)setDots:(NSString *)aString {
  if ((!dots && !aString) || (dots && aString && [dots isEqualToString:aString]))
    return;
  dirty = YES;
  NSString* newDots = [aString copy];
  [dots release];
  dots = newDots;
}

- (NSString *)key {
  return key;
}

- (void)setKey:(NSString *)aString {
  if ((!key && !aString) || (key && aString && [key isEqualToString:aString]))
    return;
  dirty = YES;
  [key release];
  key = [aString copy];
}

#if KEYWORDS
- (NSString *)keyWords {
  return keyWords;
}

- (void)setKeyWords:(NSString *)aString {
  if ((!keyWords && !aString) || (keyWords && aString && [keyWords isEqualToString:aString]))
    return;
  dirty = YES;
  [keyWords release];
  keyWords = [aString copy];
}
#endif

- (BOOL)dirty {
  return dirty;
}

- (void)setRating:(char)r {
  dirty = YES;
  rating = r;
}

- (char)rating {
  return rating;
}

- (void)setKeySignature:(char)k sharps:(char)n {
  numSharps = n;
  dirty = YES;
}

- (void)setTimeSignature:(int)num denominator:(int)den {
  timeSigNum = num;
  timeSigDen = den;
  dirty = YES;
}

- (NSData *)midiDataWithGuitarChords:(BOOL)guitarChords;
{
  return [Tune midiDataForTune:dots guitarChords:guitarChords];
}

+ (NSData *)midiDataForTune:(NSString *)tune guitarChords:(BOOL)guitarChords {
  NSString *directory = NSTemporaryDirectory();
  NSString *abcPath = [directory stringByAppendingPathComponent:@"tune.abc"];
  NSString *midiPath = [directory stringByAppendingPathComponent:@"tune.mid"];

  NSError *error = NULL;
  if ( [tune writeToFile:abcPath atomically:false encoding:NSUTF8StringEncoding error:&error] ) {
    const char *argv[] = {
      "abc2midi",
      [abcPath UTF8String],
      guitarChords ? "-GC" : "-NGC",
      "-quiet",
      "-o",
      [midiPath UTF8String],
      NULL
    };
    extern int abc2midi_main(int argc, const char *argv[]);
    abc2midi_main( 6, argv );
    return [NSData dataWithContentsOfFile:midiPath];
  }
  return nil;
}

- (NSString*) meter
{
  if ( timeSigDen != 0 )
    return [NSString stringWithFormat:@"%d/%d", timeSigNum, timeSigDen];
  return @"?";
}

static NSInteger sortByOrder( Tune *tune1, Tune *tune2, void *context )
{
  return tune1.primaryKey-tune2.primaryKey;
}

static NSInteger sortByTitle( Tune *tune1, Tune *tune2, void *context )
{
  return [tune1.title localizedCaseInsensitiveCompare:tune2.title];
}

static NSInteger sortByKey( Tune *tune1, Tune *tune2, void *context )
{
  return [tune1.key caseInsensitiveCompare:tune2.key];
}

static NSInteger sortByMeter( Tune *tune1, Tune *tune2, void *context )
{
  return 32*(tune1.timeSigDen-tune2.timeSigDen) + tune1.timeSigNum-tune2.timeSigNum;
}

+ (void) sortTunes:(NSMutableArray *)tunes by:(SortOrder)sortOrder
{
  switch ( sortOrder ) {
  default:
  case kSortTunesByOrder:
    [tunes sortUsingFunction:sortByOrder context:nil];
    break;
  case kSortTunesByTitle:
    [tunes sortUsingFunction:sortByTitle context:nil];
    break;
  case kSortTunesByKey:
    [tunes sortUsingFunction:sortByKey context:nil];
    break;
  case kSortTunesByMeter:
    [tunes sortUsingFunction:sortByMeter context:nil];
    break;
  }
}

+ (int) drawTune:(NSString *)tune intoContext:(CGContextRef)context bounds:(CGRect)bounds scale:(float)scale
{
  extern CGContextRef gcx;
  extern const char *abcInputString;
  gcx = context;
#if EXPERIMENTAL
  NSMutableString* dots = [[NSMutableString alloc] init];
  extern UIFont* gFonts[4];//FIXME
  if ( gFonts[0] )
    [dots appendFormat:@"%%%%titlefont %@\n", gFonts[0].familyName];
  if ( gFonts[1] )
    [dots appendFormat:@"%%%%subtitlefont %@\n", gFonts[1].familyName];
  if ( gFonts[2] )
    [dots appendFormat:@"%%%%composerfont %@\n", gFonts[2].familyName];
  if ( gFonts[3] )
    [dots appendFormat:@"%%%%wordsfont %@\n", gFonts[3].familyName];
  [dots appendString:tune];
  abcInputString = [dots UTF8String];
  [dots release];
#else
#if 1//FIXME
  AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
  if ( appDelegate.tuneScaling != 1.0f )
    tune = [[NSString stringWithFormat:@"%%%%scale %f\n\n", appDelegate.tuneScaling] stringByAppendingString:tune];
#endif
  abcInputString = [tune UTF8String];
#endif
  const char *argv[32];
  int argc = 0;

  argv[argc++] = "abc2ps";
  argv[argc++] = "-o";
  argv[argc++] = "-maxv";
  argv[argc++] = "8";
  argv[argc++] = "-maxs";
  argv[argc++] = "4800";
  argv[argc++] = "tune.abc";
  argv[argc] = NULL;
  extern int abc2ps_main(int argc, const char *argv[]);
  extern int pagenum;
  abc2ps_main( argc, argv );
  return pagenum;
}

- (NSData *)pdfData
{
  return [Tune pdfDataForTune:dots];
}

+ (NSData *)pdfDataForTune:(NSString *)tune
{
  CGRect bounds = CGRectMake( 0, 0, PaperWidth, PaperHeight );
  CFMutableDataRef theData = CFDataCreateMutable(NULL, 0);
  CGDataConsumerRef theConsumer = CGDataConsumerCreateWithCFData(theData);
  CGContextRef context = CGPDFContextCreate( theConsumer, &bounds, NULL );
  CGDataConsumerRelease( theConsumer );
  if ( context ) {
    CGContextSaveGState(context);
    [Tune drawTune:tune intoContext:context bounds:bounds scale:1];
    CGContextRestoreGState(context);
    CGContextFlush(context);
    CGContextRelease(context);
  }
  NSData* data = (NSData *)theData;
  [data autorelease];
  return data;
}

#if TARGET_OS_IPHONE
+ (UIImage *)thumbnailForTune:(NSString *)tune
{
  float scale = 0.22f;
  CGSize size = CGSizeMake(scale*PaperWidth, scale*PaperHeight);
  CGRect bounds = CGRectMake( 0, 0, size.width, size.height );
	UIGraphicsBeginImageContext( size );
	CGContextRef context = UIGraphicsGetCurrentContext();

  if ( context ) {
    CGContextSaveGState(context);
//    CGContextTranslateCTM( context, 0, PaperHeight );
    CGContextScaleCTM( context, scale, scale );
    [Tune drawTune:tune intoContext:context bounds:bounds scale:1];
    CGContextRestoreGState(context);
    CGContextFlush(context);
  }

	UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
	UIGraphicsEndImageContext();
  return image;
}
#endif

// FIXME: we should at least do a binary search, or compile this table into a state machine
struct Accent {
  const char *text;
  const unichar replacement;
};

// Make sure this table remains sorted!
struct Accent accents[] = {
  { "'A", 0xc1, },
  { "'C", 0x106, },
  { "'E", 0xc9, },
  { "'I", 0xcd, },
  { "'L", 0x139, },
  { "'N", 0x143, },
  { "'O", 0xd3, },
  { "'R", 0x154, },
  { "'S", 0x15a, },
  { "'U", 0xda, },
  { "'Z", 0x179, },
  { "'a", 0xe1, },
  { "'c", 0x107, },
  { "'e", 0xe9, },
  { "'i", 0xed, },
  { "'l", 0x13a, },
  { "'n", 0x144, },
  { "'o", 0xf3, },
  { "'r", 0x155, },
  { "'s", 0x15b, },
  { "'u", 0xfa, },
  { "'z", 0x17a, },
  { ",C", 0xc7, },
  { ",G", 0x122, },
  { ",L", 0x13b, },
  { ",N", 0x145, },
  { ",R", 0x156, },
  { ",S", 0x15e, },
  { ",T", 0x162, },
  { ",c", 0xe7, },
  { ",g", 0x123, },
  { ",l", 0x13c, },
  { ",n", 0x146, },
  { ",r", 0x157, },
  { ",s", 0x15f, },
  { ",t", 0x163, },
  { ".C", 0x10a },
  { ".E", 0x116, },
  { ".G", 0x120, },
  { ".I", 0x130, },
  { ".L", 0x13f, },
  { ".Z", 0x17b, },
  { ".c", 0x10b },
  { ".e", 0x117, },
  { ".g", 0x121, },
  { ".i", 0x131, },
  { ".l", 0x140, },
  { ".z", 0x17c, },
  { "/L", 0x141, },
  { "/O", 0xd8, },
  { "/l", 0x142, },
  { "/o", 0xf8, },
  { ":O", 0x150, },
  { ":U", 0x170, },
  { ":o", 0x151, },
  { ":u", 0x171, },
  { ";A", 0x104, },
  { ";E", 0x118, },
  { ";I", 0x12e, },
  { ";U", 0x172, },
  { ";a", 0x105, },
  { ";e", 0x119, },
  { ";i", 0x12f, },
  { ";u", 0x173, },
  { "=A", 0x100, },
  { "=D", 0x110, },
  { "=E", 0x112, },
  { "=H", 0x126, },
  { "=I", 0x12a, },
  { "=O", 0x14c, },
  { "=T", 0x166, },
  { "=U", 0x16b, },
  { "=a", 0x101, },
  { "=d", 0x111, },
  { "=e", 0x113, },
  { "=h", 0x127, },
  { "=i", 0x12b, },
  { "=o", 0x14d, },
  { "=t", 0x167, },
  { "=u", 0x16b, },
  { "AA", 0xc5, },
  { "AE", 0xc6, },
  { "NG", 0x14a, },
  { "OE", 0x152, },
  { "\"A", 0xc3, },
  { "\"E", 0xcb, },
  { "\"I", 0xcf, },
  { "\"O", 0xd6, },
  { "\"U", 0xdc, },
  { "\"Y", 0x17b, },
  { "\"a", 0xe3, },
  { "\"e", 0xeb, },
  { "\"i", 0xef, },
  { "\"o", 0xf6, },
  { "\"u", 0xfc, },
  { "\"y", 0xff, },
  { "^A", 0xc2, },
  { "^C", 0x108, },
  { "^E", 0xca, },
  { "^G", 0x11c, },
  { "^H", 0x124, },
  { "^I", 0xce, },
  { "^O", 0xd4, },
  { "^S", 0x15c, },
  { "^U", 0xdb, },
  { "^W", 0x174, },
  { "^Y", 0x175, },
  { "^a", 0xe2, },
  { "^c", 0x109, },
  { "^e", 0xea, },
  { "^g", 0x11d, },
  { "^h", 0x125, },
  { "^i", 0xee, },
  { "^o", 0xf4, },
  { "^s", 0x15d, },
  { "^u", 0xfb, },
  { "^w", 0x175, },
  { "^y", 0x176, },
  { "`A", 0xc0, },
  { "`E", 0xc8, },
  { "`I", 0xcc, },
  { "`O", 0xd2, },
  { "`U", 0xd9, },
  { "`a", 0xe0, },
  { "`e", 0xe8, },
  { "`i", 0xec, },
  { "`o", 0xf2, },
  { "`u", 0xf9, },
  { "aa", 0xe5, },
  { "ae", 0xe6, },
  { "cC", 0xc7, },
  { "cc", 0xe7, },
  { "ng", 0x14b, },
  { "oA", 0xc5, },
  { "oU", 0x16e, },
  { "oa", 0xe5, },
  { "oe", 0x153, },
  { "ou", 0x16f, },
  { "ss", 0xdf, },
  { "uA", 0x102, },
  { "uE", 0x114, },
  { "uG", 0x11e, },
  { "uI", 0x12c, },
  { "uO", 0x14e, },
  { "uU", 0x16c, },
  { "ua", 0x103, },
  { "ue", 0x115, },
  { "ug", 0x11f, },
  { "ui", 0x12d, },
  { "uo", 0x14f, },
  { "uu", 0x16d, },
  { "vC", 0x10c, },
  { "vD", 0x10e, },
  { "vL", 0x13d, },
  { "vN", 0x147, },
  { "vR", 0x158, },
  { "vS", 0x160, },
  { "vT", 0x164, },
  { "vZ", 0x17d, },
  { "vc", 0x10d, },
  { "vd", 0x10f, },
  { "vl", 0x13e, },
  { "vn", 0x148, },
  { "vr", 0x159, },
  { "vs", 0x161, },
  { "vt", 0x165, },
  { "vz", 0x17e, },
  { "~A", 0xc3, },
  { "~I", 0x128, },
  { "~K", 0x136, },
  { "~N", 0xd1, },
  { "~O", 0xd5, },
  { "~U", 0x168, },
  { "~a", 0xe3, },
  { "~i", 0x129, },
  { "~k", 0x137, },
  { "~n", 0xf1, },
  { "~o", 0xf5, },
  { "~u", 0x169, },

  { NULL, 0 }
};

+ (NSString *)fixAccents:(NSString *)s
{
  const char *p = [s UTF8String];
  while ( *p ) {
    if ( *p++ == '\\' ) {
      char buf[2048];
      p = [s UTF8String];
      int k = 0;
      while ( *p && k < sizeof(buf)-2 ) {
        char c = *p++;
        if ( c == '{' || c == '}' ) {
          // Skip these
        } else if ( c == '\\' ) {
          bool done = false;
          for ( int a = 0; accents[a].text; a++ ) {
            const char *ap = accents[a].text;
            const char *sp = p;
            while ( *ap && *ap == *sp ) {
              ap++;
              sp++;
            }
            if ( !*ap ) {
              unichar ua = accents[a].replacement;
              buf[k++] = 0xc0 | ua >> 6;
              buf[k++] = 0x80 | (ua & 0x3f);
              p = sp;
              done = true;
              break;
            }
          }
          if ( !done )
            buf[k++] = c;
        } else
          buf[k++] = c;
      }
      buf[k++] = 0;
      return [NSString stringWithCString:buf encoding:NSUTF8StringEncoding];
    }
  }

  return s;
}

void drawUIText( CGContextRef context, float x, float y, const char *text, int alignment, bool serif, int fontSize, bool utf8charset )
{
#if TARGET_OS_IPHONE
  UIGraphicsPushContext( context );
#endif
  NSString *s = [NSString stringWithCString:text encoding:utf8charset ? NSUTF8StringEncoding : NSISOLatin1StringEncoding];
  s = [Tune fixAccents:s];
#if TARGET_OS_IPHONE
  CGContextSaveGState( context );
  CGContextTranslateCTM( context, x, y );
  CGContextScaleCTM( context, 1, -1 );
  UIFont *font = serif ? [UIFont fontWithName:@"Times New Roman" size:fontSize] : [UIFont systemFontOfSize:fontSize];
  CGSize size = [s sizeWithFont:font];
  [s drawAtPoint:CGPointMake( alignment == 1 ? 0 : alignment == -1 ? -size.width : -size.width/2, -font.ascender ) withFont:font];
  CGContextRestoreGState( context );
#else
  CGContextSetTextMatrix(context, CGAffineTransformMakeScale(1.0, 1.0)); 
  CTFontRef font = CTFontCreateWithName( serif ? CFSTR("Times New Roman") : CFSTR("Helvetica"), fontSize, NULL );

  CFStringRef keys[] = { kCTFontAttributeName };
  CFTypeRef values[] = { font };

  CFDictionaryRef attributes =
      CFDictionaryCreate(kCFAllocatorDefault, (const void**)&keys,
          (const void**)&values, sizeof(keys) / sizeof(keys[0]),
          &kCFTypeDictionaryKeyCallBacks,
          &kCFTypeDictionaryValueCallBacks);

  CFAttributedStringRef attrString = CFAttributedStringCreate(kCFAllocatorDefault, (CFStringRef)s, attributes);
  CFRelease(attributes);

  CTLineRef line = CTLineCreateWithAttributedString(attrString);
  double width = CTLineGetTypographicBounds( line, NULL, NULL, NULL );
  CGContextSetTextPosition(context, alignment == 1 ? x : alignment == -1 ? x-width : x-width/2, y);
  CTLineDraw(line, context);
  CFRelease(line);
  CFRelease(attrString);
  CFRelease(font);
#endif
#if TARGET_OS_IPHONE
  UIGraphicsPopContext();
#endif
}
@end

@implementation TuneFormatter
//TODO - this will eventually replace the static methods in Tune
@synthesize oneTunePerPage;
@synthesize showPageNumbers;
@synthesize landscape;
@synthesize transpose;

-(id) init
{
  self = [super init];
  if ( self != nil ) {
    oneTunePerPage = NO;
    showPageNumbers = NO;
    landscape = NO;
    transpose = 0;
  }
  return self;
}

- (int) drawTune:(NSString *)tune intoContext:(CGContextRef)context bounds:(CGRect)bounds scale:(float)scale
{
  extern CGContextRef gcx;
  extern const char *abcInputString;
  gcx = context;
#if EXPERIMENTAL
  NSMutableString* dots = [[NSMutableString alloc] init];
  extern UIFont* gFonts[4];//FIXME
  if ( gFonts[0] )
    [dots appendFormat:@"%%%%titlefont %@\n", gFonts[0].familyName];
  if ( gFonts[1] )
    [dots appendFormat:@"%%%%subtitlefont %@\n", gFonts[1].familyName];
  if ( gFonts[2] )
    [dots appendFormat:@"%%%%composerfont %@\n", gFonts[2].familyName];
  if ( gFonts[3] )
    [dots appendFormat:@"%%%%wordsfont %@\n", gFonts[3].familyName];
  [dots appendString:tune];
  abcInputString = [dots UTF8String];
  [dots release];
#else
#if 0//FIXME
  tune = [@"%%titlecaps\n%%scale 1.2\n%%margins 144x24\n" stringByAppendingString:tune];
#endif
  abcInputString = [tune UTF8String];
#endif
  const char *argv[32];
  int argc = 0;
  
  argv[argc++] = "abc2ps";
  argv[argc++] = "-o";
  argv[argc++] = "-maxv";
  argv[argc++] = "8";
  argv[argc++] = "-maxs";
  argv[argc++] = "4800";
  if ( oneTunePerPage )
    argv[argc++] = "-1";
  if ( landscape )
    argv[argc++] = "-l";
  if ( showPageNumbers )
    argv[argc++] = "-N";
  if ( transpose != 0 ) {
    char t[32];
    sprintf( t, "%d", transpose );
    argv[argc++] = "-t";
    argv[argc++] = t;
  }
  argv[argc++] = "tune.abc";
  argv[argc] = NULL;
  extern int abc2ps_main(int argc, const char *argv[]);
  extern int pagenum;
  abc2ps_main( argc, argv );
  return pagenum;
}

- (NSData *)pdfDataForTune:(NSString *)tune
{
  CGRect bounds = CGRectMake( 0, 0, PaperWidth, PaperHeight );
  CFMutableDataRef theData = CFDataCreateMutable(NULL, 0);
  CGDataConsumerRef theConsumer = CGDataConsumerCreateWithCFData(theData);
  CGContextRef context = CGPDFContextCreate( theConsumer, &bounds, NULL );
  CGDataConsumerRelease( theConsumer );
  if ( context ) {
    CGContextSaveGState(context);
    [Tune drawTune:tune intoContext:context bounds:bounds scale:1];
    CGContextRestoreGState(context);
    CGContextFlush(context);
    CGContextRelease(context);
  }
  NSData* data = (NSData *)theData;
  [data autorelease];
  return data;
}
@end
