/* xdaliclock - a melting digital clock
 * Copyright (c) 1991-2020 Jamie Zawinski <jwz@jwz.org>
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation.  No representations are made about the suitability of this
 * software for any purpose.  It is provided "as is" without express or
 * implied warranty.
 */

#import "DaliClockView.h"
#import "xdaliclock.h"

#import <sys/time.h>

#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4
  /* In 10.5 and later, NSColor wants CGfloat args, but that type
     does not exist in 10.4. */
  typedef float CGFloat;
#endif


/* Some systems get flicker if we don't turn on OpenGL double-buffering.
   I have reports of flicker on 10.7.5 Macbooks, but I can't reproduce
   it on a 10.8.3 iMac-2010.
 */
#define DO_DOUBLE_BUFFER


/* On a 10.6 iMac-2008, I was seeing the behavior that a transparent
   Dali Clock window on top of iTunes playing videos would flicker.  At
   the time I believed that OpenGL double-buffering didn't fix it, but
   that alternating between two different texture IDs, and only writing
   to the one not currently on screen, did fix it.
*/
#undef DO_DOUBLE_TEXTURE


static void check_gl_error (const char *type);
static void log_gl_error (const char *type, GLenum error);

#ifdef USE_IPHONE
static void rgb_to_hsv (CGFloat r, CGFloat g, CGFloat b, 
                        CGFloat *h, CGFloat *s, CGFloat *v);
#endif /* USE_IPHONE */


@interface DaliClockView (ForwardDeclarations)
- (void)setForeground:(NSColor *)fg background:(NSColor *)bg;
- (void)clockTick;
- (void)colorTick;
- (void)dateTick;
@end


@implementation DaliClockView
{
  GLuint textures[2];

  NSTimer *clockTimer;
  NSTimer *colorTimer;
  NSTimer *dateTimer;
  NSTimer *countdownHomeStretchStartTimer;
  NSTimer *countdownHomeStretchEndTimer;
  float autoDateInterval;

  BOOL ownWindow;
  BOOL constrainSizes;
  BOOL usesCountdownTimer;
  BOOL usesHomeStretchTimer;
  NSDate *countdownDate;

  NSColor *fg;
  NSColor *bg;
  NSColor *initialForegroundColor;
  NSColor *initialBackgroundColor;
}

static NSUserDefaultsController *staticUserDefaultsController = 0;

+ (NSUserDefaultsController *)userDefaultsController
{
  return staticUserDefaultsController;
}

+ (void)setUserDefaultsController:(NSUserDefaultsController *)ctl
{
  if (staticUserDefaultsController &&
      staticUserDefaultsController != ctl)
    [staticUserDefaultsController release];
  staticUserDefaultsController = [ctl retain];
}


+ (void)registerDefaults:(NSUserDefaults *)defs
{
  /* Set the defaults for all preferences handled by DaliClockView.
     (AppController or DaliClockSaverView handles other preferences).
   */
  NSColor *deffg = [NSColor  blueColor];
  NSColor *defbg = [NSColor colorWithCalibratedHue:0.50
                                        saturation:1.00
                                        brightness:0.70   // cyan, but darker
# ifndef USE_IPHONE
                                             alpha:0.30   // and translucent
# else  /* USE_IPHONE */
                                             alpha:1.00
# endif /* USE_IPHONE */
                            ];

  // Default countdown date is "12 hours from now"
  NSDate *defdate = [NSDate dateWithTimeIntervalSinceNow:
                     (NSTimeInterval) 60 * 60 * 12];


  /* Try to determine the current locale's short date format, and set our
     dateStyle based on that.  We do this by creating an NSDate with a
     particular unambiguous date/time in the current time zone, having
     NSDateFormatter convert that to a string in the current locale, and
     parsing that string.  There doesn't seem to be any other way to get
     the answer to the questions, "what order are year, month and day 
     printed?", and "are hours printed mod 12 or 24?"
   */
  struct tm TM = { 0,0,13, 31,11,132, -1,-1,-1, 0, }; // 2032-12-31 13:00:00
  time_t tt = mktime (&TM);
  NSDate *test_date = [NSDate dateWithTimeIntervalSince1970:tt];
  NSDateFormatter *df = [[NSDateFormatter alloc] init];
  [df setFormatterBehavior:NSDateFormatterBehavior10_4];
  [df setDateStyle:NSDateFormatterShortStyle];
  [df setTimeStyle:NSDateFormatterNoStyle];

  NSString *test_str = [df stringFromDate:test_date];
  NSString *ds = ([test_str hasPrefix:@"12/"]   ? @"0" :  // MMDDYY
                  [test_str hasPrefix:@"31/"]   ? @"1" :  // DDMMYY
                  [test_str hasPrefix:@"32/"]   ? @"3" :  // YYMMDD
                  [test_str hasPrefix:@"2032/"] ? @"3" :  // YYMMDD
                  @"0");

  [df setDateStyle:NSDateFormatterNoStyle];
  [df setTimeStyle:NSDateFormatterShortStyle];
  test_str = [df stringFromDate:test_date];
  NSString *hs = ([test_str hasPrefix:@"13"] ? @"1" : @"0"); // 24- or 12 hour
  [df release];

  NSDictionary* extras = [NSDictionary dictionaryWithObjectsAndKeys:
    @"0", @"timeStyle",
    ds,   @"dateStyle",
    hs,   @"hourStyle",
    [NSArchiver archivedDataWithRootObject:deffg], @"initialForegroundColor",
    [NSArchiver archivedDataWithRootObject:defbg], @"initialBackgroundColor",
    @"10.0", @"cycleSpeed",
    @"NO", @"usesCountdownTimer",
    @"YES", @"usesHomeStretchTimer",
    defdate,@"countdownDate",
    nil];

  NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:100];
  [dict addEntriesFromDictionary:[defs dictionaryRepresentation]];
  [dict addEntriesFromDictionary:extras];
  [defs registerDefaults:dict];
}


- (void)bindPreferences
{
# ifndef USE_IPHONE
  NSUserDefaultsController *controller = staticUserDefaultsController;

  [self    bind:@"hourStyle"
       toObject:controller
    withKeyPath:@"values.hourStyle" options:nil];
  [self    bind:@"timeStyle"
       toObject:controller
    withKeyPath:@"values.timeStyle"
        options:nil];
  [self    bind:@"dateStyle"
       toObject:controller
    withKeyPath:@"values.dateStyle"
        options:nil];
  [self    bind:@"cycleSpeed"
       toObject:controller
    withKeyPath:@"values.cycleSpeed"
        options:nil];
  [self    bind:@"usesCountdownTimer"
       toObject:controller
    withKeyPath:@"values.usesCountdownTimer"
        options:nil];
  [self    bind:@"usesHomeStretchTimer"
       toObject:controller
    withKeyPath:@"values.usesHomeStretchTimer"
        options:nil];
  [self    bind:@"countdownDate"
       toObject:controller
    withKeyPath:@"values.countdownDate"
        options:nil];

  NSDictionary *colorBindingOptions =
    [NSDictionary dictionaryWithObject:@"NSUnarchiveFromData"
                                forKey:NSValueTransformerNameBindingOption];
  [self    bind:@"initialForegroundColor"
       toObject:controller
    withKeyPath:@"values.initialForegroundColor"
        options:colorBindingOptions];
  [self    bind:@"initialBackgroundColor"
       toObject:controller
    withKeyPath:@"values.initialBackgroundColor"
        options:colorBindingOptions];

# else  /* USE_IPHONE */

  // WTF, why don't we have bindings for this!!
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

  [self setHourStyle:(int)[defaults integerForKey:@"hourStyle"]];
  [self setTimeStyle:(int)[defaults integerForKey:@"timeStyle"]];
  [self setDateStyle:(int)[defaults integerForKey:@"dateStyle"]];
  [self setCycleSpeed:[defaults floatForKey:@"cycleSpeed"]];
  [self setUsesCountdownTimer:[defaults boolForKey:@"usesCountdownTimer"]];
  [self setUsesHomeStretchTimer:[defaults boolForKey:@"usesHomeStretchTimer"]];
  [self setCountdownDate:(NSDate *)[defaults objectForKey:@"countdownDate"]];

  /* Is it a problem that I'm using NSKeyedUnarchiver on an object
     that was created with NSArchiver instead of NSKeyedArchiver?
     Does the NSUnarchiveFromData NSValueTransformerNameBindingOption
     work with both NSArchiver and NSKeyedArchiver objects?
   */
  NSData *ifgd = [defaults objectForKey:@"initialForegroundColor"];
  NSData *ibgd = [defaults objectForKey:@"initialBackgroundColor"];
  UIColor *ifg = (ifgd ? [NSKeyedUnarchiver unarchiveObjectWithData:ifgd] : 0);
  UIColor *ibg = (ibgd ? [NSKeyedUnarchiver unarchiveObjectWithData:ibgd] : 0);
  if (! ifg) ifg = [UIColor whiteColor];  // This shouldn't happen...
  if (! ibg) ibg = [UIColor blackColor];
  [self setInitialForegroundColor:ifg];
  [self setInitialBackgroundColor:ibg];

  // So we can tell when we're docked.
  [UIDevice currentDevice].batteryMonitoringEnabled = YES;
# endif /* USE_IPHONE */
}


- (id)initWithFrame:(NSRect)rect
{
  // by default, we may change window background
  return [self initWithFrame:rect ownWindow:YES];
}


#ifdef USE_IPHONE
- (void)initFramebuffer
{

  if (gl_framebuffer)  glDeleteFramebuffersOES  (1, &gl_framebuffer);
  if (gl_renderbuffer) glDeleteRenderbuffersOES (1, &gl_renderbuffer);

  glGenFramebuffersOES  (1, &gl_framebuffer);
  glBindFramebufferOES  (GL_FRAMEBUFFER_OES,  gl_framebuffer);

  glGenRenderbuffersOES (1, &gl_renderbuffer);
  glBindRenderbufferOES (GL_RENDERBUFFER_OES, gl_renderbuffer);

// redundant?
//   glRenderbufferStorageOES (GL_RENDERBUFFER_OES, GL_RGBA8_OES, w, h);
  [ogl_ctx renderbufferStorage:GL_RENDERBUFFER_OES
           fromDrawable:(CAEAGLLayer*)self.layer];

  glFramebufferRenderbufferOES (GL_FRAMEBUFFER_OES,  GL_COLOR_ATTACHMENT0_OES,
                                GL_RENDERBUFFER_OES, gl_renderbuffer);

  int err = glCheckFramebufferStatusOES (GL_FRAMEBUFFER_OES);
  switch (err) {
  case GL_FRAMEBUFFER_COMPLETE_OES:
    break;
  case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_OES:
    NSAssert (0, @"framebuffer incomplete attachment");
    break;
  case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_OES:
    NSAssert (0, @"framebuffer incomplete missing attachment");
    break;
  case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_OES:
    NSAssert (0, @"framebuffer incomplete dimensions");
    break;
  case GL_FRAMEBUFFER_INCOMPLETE_FORMATS_OES:
    NSAssert (0, @"framebuffer incomplete formats");
    break;
  case GL_FRAMEBUFFER_UNSUPPORTED_OES:
    NSAssert (0, @"framebuffer unsupported");
    break;
/*
  case GL_FRAMEBUFFER_UNDEFINED:
    NSAssert (0, @"framebuffer undefined");
    break;
  case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER:
    NSAssert (0, @"framebuffer incomplete draw buffer");
    break;
  case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER:
    NSAssert (0, @"framebuffer incomplete read buffer");
    break;
  case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE:
    NSAssert (0, @"framebuffer incomplete multisample");
    break;
  case GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS:
    NSAssert (0, @"framebuffer incomplete layer targets");
    break;
 */
  default:
    NSAssert (0, @"framebuffer incomplete, unknown error 0x%04X", err);
    break;
  }

  check_gl_error ("OES_init");
}
#endif // USE_IPHONE


- (id)initWithFrame:(NSRect)rect ownWindow:(BOOL)own_p
{
  self = [super initWithFrame:rect];
  if (! self) return nil;

  ownWindow = own_p;

  memset (&config, 0, sizeof(config));

  config.max_fps = 30;

# ifndef USE_IPHONE
  // Tell the color selector widget to show the "opacity" slider.
  [[NSColorPanel sharedColorPanel] setShowsAlpha:YES];
# endif /* !USE_IPHONE */

# ifdef USE_IPHONE
  [self initGestures];
# endif /* USE_IPHONE */

  // initialize the fonts and bitmaps
  //
  [self setFrameSize:[self frame].size];
  [self bindPreferences];

  [self clockTick];
  [self colorTick];


  // Initialize the OpenGL context.
  //
# ifndef USE_IPHONE

  NSOpenGLContext *ctx = ogl_ctx;
  if (! ctx) {

    NSOpenGLPixelFormatAttribute attrs[] = {
      NSOpenGLPFAColorSize, 24,
      NSOpenGLPFAAlphaSize, 8,
      NSOpenGLPFADepthSize, 0, //16,  we don't need a depth buffer at all.
#  ifdef DO_DOUBLE_BUFFER
      NSOpenGLPFADoubleBuffer,
#  endif
      0 };

    NSOpenGLPixelFormat *pixfmt = 
      [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs];

    NSAssert (pixfmt, @"unable to create NSOpenGLPixelFormat");

    ctx = [[NSOpenGLContext alloc] 
            initWithFormat:pixfmt
              shareContext:nil];
    ogl_ctx = ctx;
//    [pixfmt release]; // #### ???
  }

  // Sync refreshes to the vertical blanking interval
  GLint r = 1;
  [ctx setValues:&r forParameter:NSOpenGLCPSwapInterval];
//  check_gl_error ("NSOpenGLCPSwapInterval");  // SEGV sometimes. Too early?

  // Make window transparency possible
  r = NO;
  [ogl_ctx setValues:&r forParameter:NSOpenGLCPSurfaceOpacity];
//  check_gl_error ("init opacity");


  [ctx makeCurrentContext];
  check_gl_error ("makeCurrentContext");

  // Get device pixels instead of points, for Retina displays.
  self.wantsBestResolutionOpenGLSurface = YES;

  // Clear frame buffer ASAP, else there are bits left over from other apps.
  // #### On 10.7, this causes GL_INVALID_FRAMEBUFFER_OPERATION.
  // In Dali Clock but not XScreenSaver?
  //  glClearColor (0, 0, 0, 1);
  //  glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  //  check_gl_error ("init clear");


  // Enable multi-threading, if possible.  This runs most OpenGL commands
  // and GPU management on a second CPU.
  {
#   ifndef  kCGLCEMPEngine
#    define kCGLCEMPEngine 313  // Added in MacOS 10.4.8 + XCode 2.4.
#   endif
    CGLContextObj cctx = CGLGetCurrentContext();
    CGLError err = CGLEnable (cctx, kCGLCEMPEngine);
    if (err != kCGLNoError) {
      NSLog (@"enabling multi-threaded OpenGL failed: %d", err);
    }
  }

  {
    GLboolean d = 0;
    glGetBooleanv (GL_DOUBLEBUFFER, &d);
    if (d)
      glDrawBuffer (GL_BACK);
    else
      glDrawBuffer (GL_FRONT);

    /* WTF?  Sometimes we get "invalid operation" when doing GL_FRONT,
       which means the front buffer doesn't exist!  But that's a lie,
       because on ignoring the error, everything works.  So, just flush
       errors here.
     */
    // check_gl_error ("init drawBuffer");
    while (glGetError() != GL_NO_ERROR)
      ;
  }

  check_gl_error ("init");

# else  // USE_IPHONE

  if (!ogl_ctx) {

    CAEAGLLayer *eagl_layer = (CAEAGLLayer *) self.layer;
    eagl_layer.opaque = TRUE;
    eagl_layer.drawableProperties = 
      [NSDictionary dictionaryWithObjectsAndKeys:
       kEAGLColorFormatRGBA8,           kEAGLDrawablePropertyColorFormat,
       [NSNumber numberWithBool:FALSE], kEAGLDrawablePropertyRetainedBacking,
       nil];

    // Without this, the GL frame buffer is half the screen resolution!
    eagl_layer.contentsScale = [UIScreen mainScreen].scale;

    ogl_ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];
  }

  if (!ogl_ctx) {
    [self release];
    return 0;
  }

  [EAGLContext setCurrentContext: ogl_ctx];

  [self initFramebuffer];
# endif // USE_IPHONE

# ifdef DO_DOUBLE_TEXTURE
  glGenTextures (2, textures);
# else
  glGenTextures (1, textures);
# endif

  check_gl_error ("glGenTextures");

  return self;
}


/* Called when the View is resized.
 */
- (void)setFrameSize:(NSSize)newSize
{
# ifndef USE_IPHONE
  [ogl_ctx clearDrawable];  // need this to cause the vp change to stick
  [super setFrameSize:newSize];

  /* If the user is interactively resizing the window, don't regenerate
     the bitmap until we're done.  (We will just scale whatever image is
     already on the window instead, which reduces flicker when the
     target bitmap size shifts over a boundary).
   */
  if ([self inLiveResize]) return;
# endif /* USE_IPHONE */

  int ow = config.width;
  int oh = config.height;

# if defined(USE_IPHONE) && defined(__IPHONE_4_0)
  /* Make sure we are dealing with real pixels, not "virtual" pixels. */
  if ([self respondsToSelector:@selector(contentScaleFactor)]) {
    double s = [self contentScaleFactor];
    newSize.width  *= s;
    newSize.height *= s;
  }
# endif /* __IPHONE_4_0 */

  float sscale = 2.5;   // use the next-larger bitmap

  config.width  = newSize.width  * sscale;
  config.height = newSize.height * sscale;

  render_bitmap_size (&config, 
                      &config.width, &config.height,
                      &config.width2, &config.height2);

  if (config.render_state && (ow == config.width && oh == config.height))
    return;  // nothing to do

  /* When the window is resized, re-create the bitmaps for the largest
     font that will now fit in the window.
   */
  if (config.bitmap) free (config.bitmap);
  config.bitmap = 0;

  if (config.render_state)
    render_free (&config);
  render_init (&config);

# ifdef USE_IPHONE
  // Orientation changes resize the window (swapping W and H) so we need
  // to get a new framebuffer that matches the current shape of the window,
  // or else the aspect ratio goes wonky.
  if (gl_framebuffer)
    [self initFramebuffer];
  if (aboutBox)
    [self aboutOff];

# endif
}


#ifdef USE_IPHONE

- (void)setFrame:(NSRect)r
{
  [super setFrame:r];
  [self setFrameSize:r.size];
}


+ (Class) layerClass
{
    return [CAEAGLLayer class];
}


/* Gestures
 */


- (void)initGestures
{
  UITapGestureRecognizer *dtap = [[UITapGestureRecognizer alloc]
                                   initWithTarget:self
                                   action:@selector(handleDoubleTap)];
  dtap.numberOfTapsRequired = 2;
  [self addGestureRecognizer: dtap];
  [dtap release];

  UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc]
                                   initWithTarget:self
                                   action:@selector(handleTap)];
  stap.numberOfTapsRequired = 1;
  [stap requireGestureRecognizerToFail: dtap];
  [self addGestureRecognizer: stap];
  [stap release];
 
  UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]
                                  initWithTarget:self
                                  action:@selector(handlePan:)];
  [self addGestureRecognizer: pan];
  [pan release];
 
  UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc]
                                      initWithTarget:self
                                      action:@selector(handlePinch:)];
  [self addGestureRecognizer: pinch];
  [pinch release];
}


- (void) handleTap
{
  // Single tap: briefly display date and credits.

  config.display_date_p = 1;
  [NSTimer scheduledTimerWithTimeInterval:2.0
           target:self
           selector:@selector(dateOff)
           userInfo:nil
           repeats:NO];
  [NSTimer scheduledTimerWithTimeInterval:3.5
           target:self
           selector:@selector(aboutOff)
           userInfo:nil
           repeats:NO];
  [self aboutClick:nil];
}


- (void) handleDoubleTap
{
  // Double tap: toggle 24 hour mode.

  config.display_date_p = 0;
  // Change it only if hours currently visible;
  // In seconds-only mode, this is a mis-tap.
  if (config.time_mode != SS)
    [self setHourStyle: ![self hourStyle]];

  // Save prefs
  NSUserDefaults *controller = [NSUserDefaults standardUserDefaults];
  [controller setObject:[NSNumber numberWithInt:[self hourStyle]]
              forKey:@"hourStyle"];
  [controller synchronize];
}


- (void) pinch:(BOOL)in
{
  config.display_date_p = 0;
  if (in) {					// Pinch in: smaller digits
    if ([self timeStyle] == SS)        [self setTimeStyle:HHMM];
    else if ([self timeStyle] == HHMM) [self setTimeStyle:HHMMSS];

  } else {					// Pinch out: larger digits
    if ([self timeStyle] == HHMMSS)    [self setTimeStyle:HHMM];
    else if ([self timeStyle] == HHMM) [self setTimeStyle:SS];
  }

  // Save prefs
  NSUserDefaults *controller = [NSUserDefaults standardUserDefaults];
  [controller setObject:[NSNumber numberWithInt:[self timeStyle]]
              forKey:@"timeStyle"];
  [controller synchronize];
}


- (void) handlePinch:(UIPinchGestureRecognizer *)sender
{
  if (sender.state == UIGestureRecognizerStateEnded)
    [self pinch: ([sender scale] < 1)];
}


// I couldn't get Swipe to work, but Pan works fine.
- (void) handlePan:(UIPanGestureRecognizer *)sender
{
  CGPoint translate = [sender translationInView: self];
  if (sender.state != UIGestureRecognizerStateEnded)
    ;
  else if (fabs (translate.x) > fabs (translate.y))	// Horizontal
    [self pinch: (translate.x > 0)];
  else							// Vertical
    [self pinch: (translate.y > 0)];
}


- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
  if (event.type == UIEventSubtypeMotionShake) {
    config.test_hack = '0' + (random() % 11);  /* 0-9 or SPC */
  }
}


/* We need this to respond to "shake" gestures
 */
- (BOOL)canBecomeFirstResponder {
  return YES;
}
 

#else /* !USE_IPHONE */


/* The top-level window is sometimes transparent.
 */
- (BOOL)isOpaque
{
  return NO;
}


/* Called when the user starts interactively resizing the window.
 */
- (void)viewWillStartLiveResize
{
}


/* Called when the user is done interactively sizing the window.
 */
- (void)viewDidEndLiveResize
{
  // Resize the frame one last time, now that we're finished dragging.
  [self setFrameSize:[self frame].size];
}


/* Announce our willingness to accept keyboard input.
 */
- (BOOL)acceptsFirstResponder
{
  return YES;
}

/* Display date when mouse clicked in window.
 */
- (void)mouseDown:(NSEvent *)ev
{
  config.display_date_p = 1;
}


/* Back to time display shortly after mouse released.
 */
- (void)mouseUp:(NSEvent *)ev
{
  float delay = 2.0;  // seconds
  if (config.display_date_p)
    [NSTimer scheduledTimerWithTimeInterval:delay
                                     target:self
                                   selector:@selector(dateOff)
                                   userInfo:nil
                                    repeats:NO];
}


/* Typing Ctrl-0 through Ctrl-9 and Ctrl-hyphen are a debugging hack.
 */
- (void)keyDown:(NSEvent *)ev
{
  NSString *ns = [ev charactersIgnoringModifiers];
  if (! [ns canBeConvertedToEncoding:NSASCIIStringEncoding])
    goto FAIL;
  const char *s = [ns cStringUsingEncoding:NSASCIIStringEncoding];
  if (! s) goto FAIL;
  if (strlen(s) != 1) goto FAIL;
  if (! ([ev modifierFlags] & NSControlKeyMask)) goto FAIL;
  if (*s == '-' || (*s >= '0' && *s <= '9'))
    config.test_hack = *s;
  else goto FAIL;
  return;
FAIL:
  [super keyDown:ev];
}

#endif /* !USE_IPHONE */


- (void)dateOff
{
  config.display_date_p = 0;
}

# ifdef USE_IPHONE
- (void)aboutOff
{
  if (aboutBox) {
    CABasicAnimation *anim = 
      [CABasicAnimation animationWithKeyPath:@"opacity"];
    anim.duration     = 0.3;
    anim.repeatCount  = 1;
    anim.autoreverses = NO;
    anim.fromValue    = [NSNumber numberWithFloat: 1];
    anim.toValue      = [NSNumber numberWithFloat: 0];
//  anim.delegate     = self;
    aboutBox.layer.opacity = 0;
    [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
  }
}


- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
  if (! aboutBox) return;
  [aboutBox removeFromSuperview];
  aboutBox = 0;
}

# endif /* USE_IPHONE */


/* This is called from the timer (and other places) to change the colors.
 */
- (void)setForeground:(NSColor *)new_fg background:(NSColor *)new_bg
{
  if (fg != new_fg) {
    if (fg) [fg release];
# ifdef USE_IPHONE
    fg = [new_fg retain];
# else  /* !USE_IPHONE */
    fg = [[new_fg colorUsingColorSpaceName:NSCalibratedRGBColorSpace] retain];
# endif /* !USE_IPHONE */
  }
  if (bg != new_bg) {
    if (bg) [bg release];
# ifdef USE_IPHONE
    bg = [new_bg retain];
# else  /* !USE_IPHONE */
    bg = [[new_bg colorUsingColorSpaceName:NSCalibratedRGBColorSpace] retain];
# endif /* !USE_IPHONE */
  }

# ifdef USE_IPHONE
//    [self drawRect:[self frame]];
# else  /* !USE_IPHONE */
  [self setNeedsDisplay:TRUE];
# endif /* !USE_IPHONE */
}


#if 0
#ifndef USE_IPHONE
- (void)viewDidMoveToWindow
{
  // Make window's background transparent so that the GL background works.
  // But only do this if our creator has said that it's ok for us to diddle
  // the background color of the window we are running on.
  //
  // #### For some reason we are now getting "invalid context 0" errors
  //      inside AppController:[window setContentView:view] when doing
  //      this here.  So, this code was moved to inside of colorTick,
  //      which means the window flickers at startup (initially has a
  //      solid background color, then turns transparent.)  Bleh.
  //
  if (ownWindow) {
    [[self window] setBackgroundColor:[NSColor clearColor]];
    [[self window] setOpaque:NO];
    [[NSColor clearColor] set];
    NSRectFill ([self frame]);
  }
}
#endif /* !USE_IPHONE */
#endif /* 0 */



/**************************************************************************
 **************************************************************************

   The big kahuna refresh method.  Draw the current contents of the bitmap
   onto the window.  Sounds easy, doesn't it?

 **************************************************************************
 **************************************************************************/

- (void)drawRect:(NSRect)rect
{
  NSRect framerect = [self frame];
  NSRect torect;

  /**************************************************************************
     Compute the desired size and rotation of the destination rectangle.
   **************************************************************************/

  if (config.width <= 0 || config.height <= 0) abort();

  if (!fg || !bg) return; // Called too early somehow?

# ifdef USE_IPHONE

  double s = 1;  /* Retina scale factor */

#  if defined(__IPHONE_4_0)
  /* Make sure we are dealing with real pixels, not "virtual" pixels. */
  if ([self respondsToSelector:@selector(contentScaleFactor)]) {
    s = [self contentScaleFactor];
    framerect.size.width  *= s;
    framerect.size.height *= s;
  }
#  endif /* __IPHONE_4_0 */

# else // !USE_IPHONE

  CGFloat s = self.window.backingScaleFactor;
  framerect.size.width  *= s;
  framerect.size.height *= s;

# endif // !USE_IPHONE

  float img_aspect = (float) config.width / (float) config.height;
  float win_aspect = framerect.size.width / (float) framerect.size.height;

  // Scale the image to fill the window without changing its aspect ratio.
  //
  if (win_aspect > img_aspect) {
    torect.size.height = framerect.size.height;
    torect.size.width  = framerect.size.height * img_aspect;
  } else {
    torect.size.width  = framerect.size.width;
    torect.size.height = framerect.size.width / img_aspect;
  }


  /**************************************************************************
     Choose the size of the texture
   **************************************************************************/

  char err[100];
  int retries = 0;

  while (1) {	// Loop trying smaller sizes

    strcpy (err, "glTexImage2D");

    // The animation slows down a lot if we use truly gigantic numbers,
    // so limit the number size in screensaver-mode.  Note that this
    // limits the size of the output quad, not the size of the texture.
    //
    if (constrainSizes) {
      int maxh = (config.time_mode == SS ? 512 : /*256*/ 200);
      maxh *= s;
      if (torect.size.height > maxh) {
        torect.size.height = maxh;
        torect.size.width  = maxh * img_aspect;
      }
    }

    // put a 10% margin between the numbers and the edge of the window.
    torect.size.width  *= 0.9;
    torect.size.height *= 0.9;

    // center it in the window
    //
    torect.origin.x = (framerect.size.width  - torect.size.width ) / 2;
    torect.origin.y = (framerect.size.height - torect.size.height) / 2;

    // Don't allow the top of the number to be off screen (iPhone 5, sec only)
    //
    if (torect.origin.x < 0) {
      float r = ((torect.size.width + torect.origin.x) / torect.size.width);
      torect.size.width  *= r;
      torect.size.height *= r;
      torect.origin.x = (framerect.size.width  - torect.size.width ) / 2;
      torect.origin.y = (framerect.size.height - torect.size.height) / 2;
    }


  /**************************************************************************
     Set the current GL context before talking to OpenGL
   **************************************************************************/

# ifdef USE_IPHONE
  [EAGLContext setCurrentContext:ogl_ctx];
  glBindFramebufferOES(GL_FRAMEBUFFER_OES, gl_framebuffer);
# else /* !USE_IPHONE */
  [ogl_ctx makeCurrentContext];
  [ogl_ctx setView:self];
  [ogl_ctx update];
# endif /* !USE_IPHONE */


  /**************************************************************************
     Bind the bitmap to a texture
   **************************************************************************/

    // We alternate between two different texture IDs, only writing to the one
    // that is not currently on screen.  If we don't do this, then the window
    // flickers if it is above another window that has video playing in it.

    // Drawing with GL_LUMINANCE_ALPHA means that only the "ink" pixels of the
    // digits are written into the color buffer, and are multiplied by by the
    // prevailing glColor.

    sprintf (err + strlen(err), " #%d %dx%d", 
             retries, config.width2, config.height2);

    if (! config.bitmap) abort();
    glBindTexture (GL_TEXTURE_2D, textures[0]);
    glTexImage2D (GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA,
                  config.width2, config.height2, 0,
                  GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, config.bitmap);

    GLenum errn = glGetError();
    if (errn == GL_NO_ERROR) {			// Texture succeeded.
      break;
    } else if (errn == GL_INVALID_VALUE) {	// Texture too large. Retry.

      log_gl_error (err, errn);
      retries++;

      // Reduce the target size of the texture.
      // If it's insanely small, abort.

      unsigned int ow = config.width2;
      unsigned int oh = config.height2;
      int toosmall = 20;
      int size_tries = 0;

      while (config.height2 > toosmall &&
             ow == config.width2 &&
             oh == config.height2) {
        config.height -= 4;
        render_bitmap_size (&config, 
                            &config.width, &config.height,
                            &config.width2, &config.height2);
        if (size_tries++ > 2000) abort();  // sanity
      }

      if (config.height2 <= toosmall) abort();

      // Must re-render if size changed.
      if (config.bitmap) free (config.bitmap);
      config.bitmap = 0;
      if (config.render_state) render_free (&config);
      render_init (&config);

    } else if (errn != GL_NO_ERROR) {		// Unknown error.
      log_gl_error (err, errn);
      abort();
    }
  }

  if (retries > 0)
    NSLog (@"%s: Succeded: %s", progname, err);


  /**************************************************************************
     Finish texture initialization
   **************************************************************************/

# ifdef DO_DOUBLE_TEXTURE
  GLuint swap = textures[0];
  textures[0] = textures[1];
  textures[1] = swap;
# endif

  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
  check_gl_error ("glTexParameteri");

  glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  glEnable (GL_BLEND);
  glEnable (GL_TEXTURE_2D);


  /**************************************************************************
     Set the viewport and projection matrix
   **************************************************************************/

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glViewport (0, 0, framerect.size.width, framerect.size.height);
  glOrtho (0, framerect.size.width, 0, framerect.size.height, -1, 1);
  check_gl_error ("gOrtho");

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();


  /**************************************************************************
     Set the foreground and background colors
   **************************************************************************/

  CGFloat fgr, fgg, fgb, fga;
  CGFloat bgr, bgg, bgb, bga;
# ifndef USE_IPHONE
  [fg getRed:&fgr green:&fgg blue:&fgb alpha:&fga];
  [bg getRed:&bgr green:&bgg blue:&bgb alpha:&bga];
# else  /* USE_IPHONE */
  const CGFloat *rgba;
  rgba = CGColorGetComponents ([fg CGColor]);
  fgr = rgba[0]; fgg = rgba[1]; fgb = rgba[2]; fga = rgba[3];
  rgba = CGColorGetComponents ([bg CGColor]);
  bgr = rgba[0]; bgg = rgba[1]; bgb = rgba[2]; bga = 1;  // rgba[3];
# endif /* USE_IPHONE */

  glClearColor (bgr, bgg, bgb, bga);
  glColor4f (fgr, fgg, fgb, fga);
  glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  check_gl_error ("clear");


  /**************************************************************************
     Create the quad
   **************************************************************************/

  GLfloat qx = torect.origin.x;
  GLfloat qy = torect.origin.y;
  GLfloat qw = torect.size.width;
  GLfloat qh = torect.size.height;
  GLfloat tw = (GLfloat) config.width  / config.width2;
  GLfloat th = (GLfloat) config.height / config.height2;

  if (config.left_offset != 0)
    glTranslatef (-qw * ((GLfloat) config.left_offset / config.width),
                  0, 0);

# ifndef USE_IPHONE
  glBegin (GL_QUADS);
  glTexCoord2f (0,  0);  glVertex3f (qx,    qy,    0);
  glTexCoord2f (tw, 0);  glVertex3f (qx+qw, qy,    0);
  glTexCoord2f (tw, th); glVertex3f (qx+qw, qy+qh, 0);
  glTexCoord2f (0,  th); glVertex3f (qx,    qy+qh, 0);
  glEnd();
  check_gl_error ("quad");
# else /* USE_IPHONE */

  GLfloat vertices[] = {
    qx,    qy,    0,
    qx+qw, qy,    0,
    qx,    qy+qh, 0,
    qx+qw, qy+qh, 0
  };
  GLfloat texCoords[] = {
    0,  0,
    tw, 0,
    0,  th,
    tw, th
  };
  glEnableClientState(GL_VERTEX_ARRAY);
  glEnableClientState(GL_TEXTURE_COORD_ARRAY);
  check_gl_error ("client state");

  glVertexPointer(3, GL_FLOAT, 0, vertices);
  glTexCoordPointer(2, GL_FLOAT, 0, texCoords);
  glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
  check_gl_error ("draw arrays");

# endif /* USE_IPHONE */

  /**************************************************************************
     Flush bits to the screen.
   **************************************************************************/

  glFinish();

# ifndef USE_IPHONE
  [ogl_ctx flushBuffer];
  check_gl_error ("finish");
# else /* USE_IPHONE */
  glBindRenderbufferOES (GL_RENDERBUFFER_OES, gl_renderbuffer);
  [ogl_ctx presentRenderbuffer:GL_RENDERBUFFER_OES];
  check_gl_error ("finish");
# endif /* USE_IPHONE */
}


/* When this timer goes off, we re-generate the bitmap
   and mark the display as invalid.
*/
- (void)clockTick
{
  if (clockTimer && [clockTimer isValid]) {
    [clockTimer invalidate];
    clockTimer = 0;
  }

  if (config.max_fps <= 0) abort();

# ifndef USE_IPHONE
  NSWindow *w = [self window];
  if (w && ![w isMiniaturized])   // noop if no window yet, or if iconified.
# else  /* USE_IPHONE */
  if (! screenLocked)
# endif /* USE_IPHONE */
  {
    render_once (&config);
    // [[self window] invalidateShadow]; // windows with shadows flicker...

    [self drawRect:[self frame]];
  }

# ifdef USE_IPHONE
  // Never automatically turn the screen off if we are docked.
  [UIApplication sharedApplication].idleTimerDisabled =
    ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
# endif /* USE_IPHONE */


  // re-schedule the timer according to current fps.
  // Schedule it in two run loop modes so that the timer fires even
  // during liveResize.
  //
  float delay = 0.9 / config.max_fps;

# ifdef USE_IPHONE
  if (screenLocked) delay = 0.25;  // only wake up 4x/second.
# endif /* USE_IPHONE */

  clockTimer = [NSTimer timerWithTimeInterval:delay
                                       target:self
                                     selector:@selector(clockTick)
                                     userInfo:nil
                                      repeats:NO];
  NSRunLoop *L = [NSRunLoop currentRunLoop];
  [L addTimer:clockTimer forMode:NSDefaultRunLoopMode];
# ifndef USE_IPHONE
  [L addTimer:clockTimer forMode:NSEventTrackingRunLoopMode];
# endif /* !USE_IPHONE */
}


/* When this timer goes off, we re-pick the foreground/background colors,
   and mark the display as invalid.
 */
- (void)colorTick
{
  if (colorTimer && [colorTimer isValid]) {
    [colorTimer invalidate];
    colorTimer = 0;
  }

# ifndef USE_IPHONE
  // See comment in viewDidMoveToWindow -- doing this there caused errors.
  // 
  if ([self window] && ownWindow) {
    ownWindow = 0; // only do it once
    [[self window] setBackgroundColor:[NSColor clearColor]];
    [[self window] setOpaque:NO];
    // Not sure why I was doing this, but this causes the System Preferences
    // window to have random black areas drawn on it.
    //  [[NSColor clearColor] set];
    //  NSRectFill ([self frame]);
  }
# endif /* !USE_IPHONE */

  if (config.max_cps <= 0) return;   // cycling is turned off, do nothing

  if ([self window]		     // do nothing if no window yet
# ifdef USE_IPHONE
      && !screenLocked
# endif /* USE_IPHONE */
      ) {
    CGFloat h, s, v, a;
    NSColor *fg2, *bg2;
    float tick = 1.0 / 360.0;   // cycle H by one degree per tick

# ifdef USE_IPHONE
    const CGFloat *rgba = CGColorGetComponents([fg CGColor]);
    rgb_to_hsv (rgba[0], rgba[1], rgba[2], &h, &s, &v);
    h /= 360.0;
    a = rgba[3];
# else  /* !USE_IPHONE */
    [fg getHue:&h saturation:&s brightness:&v alpha:&a];
# endif /* !USE_IPHONE */

    h += tick;
    while (h > 1.0) h -= 1.0;
    fg2 = [NSColor colorWithCalibratedHue:h saturation:s brightness:v alpha:a];

# ifdef USE_IPHONE
    rgba = CGColorGetComponents([bg CGColor]);
    rgb_to_hsv (rgba[0], rgba[1], rgba[2], &h, &s, &v);
    h /= 360.0;
    a = rgba[3];
# else  /* !USE_IPHONE */
    [bg getHue:&h saturation:&s brightness:&v alpha:&a];
# endif /* !USE_IPHONE */

    h += tick * 0.91;   // cycle bg slightly slower than fg, for randomosity.
    while (h > 1.0) h -= 1.0;
    bg2 = [NSColor colorWithCalibratedHue:h saturation:s brightness:v alpha:a];

    [self setForeground:fg2 background:bg2];

# ifdef USE_IPHONE
    /* Every time we tick the color, write the current colors into
       preferences so that the next time the app starts up, the color
       cycle begins where it left off.  (Maybe this is dumb.)
     */
    NSUserDefaults *controller = [NSUserDefaults standardUserDefaults];
    [controller setObject:[NSArchiver archivedDataWithRootObject:fg]
                forKey:@"initialForegroundColor"];
    [controller setObject:[NSArchiver archivedDataWithRootObject:bg]
                forKey:@"initialBackgroundColor"];
# endif /* USE_IPHONE */
  }

  /* re-schedule the timer according to current fps.
     Schedule it in two run loop modes so that the timer fires even
     during liveResize.
   */
  float delay = 1.0 / config.max_cps;

# ifdef USE_IPHONE
  if (screenLocked) delay = 0.25;  // only wake up 4x/second.
# endif /* USE_IPHONE */

  colorTimer = [NSTimer timerWithTimeInterval:delay
                                       target:self
                                     selector:@selector(colorTick)
                                     userInfo:nil
                                      repeats:NO];
  NSRunLoop *L = [NSRunLoop currentRunLoop];
  [L addTimer:colorTimer forMode:NSDefaultRunLoopMode];
# ifndef USE_IPHONE
  [L addTimer:colorTimer forMode:NSEventTrackingRunLoopMode];
# endif /* !USE_IPHONE */
}


/* When this timer goes off, we switch to "show date" mode.
 */
- (void)dateTick
{
  if (dateTimer && [dateTimer isValid]) {
    [dateTimer invalidate];
    dateTimer = 0;
  }

  if (autoDateInterval <= 0) return;

  BOOL was_on = config.display_date_p;

  if (config.time_mode != SS)		// don't auto-date in secs-only mode
    config.display_date_p = !was_on;

  /* re-schedule the timer according to current fps.
   */
  float delay = autoDateInterval;
  if (!was_on) delay = 3.0;
  dateTimer = [NSTimer scheduledTimerWithTimeInterval:delay
                                               target:self
                                             selector:@selector(dateTick)
                                             userInfo:nil
                                              repeats:NO];
}


static NSTimeInterval countdown_homestretch_interval = 30;

- (void)updateCountdown
{
  config.countdown = (usesCountdownTimer
                      ? [countdownDate timeIntervalSince1970]
                      : 0);

  if (countdownHomeStretchStartTimer) {
    if ([countdownHomeStretchStartTimer isValid])
      [countdownHomeStretchStartTimer invalidate];
    [countdownHomeStretchStartTimer release];
    countdownHomeStretchStartTimer = 0;
  }

  if (countdownHomeStretchEndTimer) {
    if ([countdownHomeStretchEndTimer isValid])
      [countdownHomeStretchEndTimer invalidate];
    [countdownHomeStretchEndTimer release];
    countdownHomeStretchEndTimer = 0;
  }

  [self countdownHomeStretch];

  if (usesCountdownTimer && usesHomeStretchTimer) {

    NSDate *now = [NSDate date];
    NSDate *start =
      [NSDate dateWithTimeInterval: -countdown_homestretch_interval
                         sinceDate: countdownDate];
    NSDate *end =
      [NSDate dateWithTimeInterval: countdown_homestretch_interval
                         sinceDate: countdownDate];
    NSTimeInterval delay1 = [start timeIntervalSinceDate: now];
    NSTimeInterval delay2 = [end   timeIntervalSinceDate: now];

    delay1 -= 1.5;  // start a little earlier

    if (delay1 > 0) {
      countdownHomeStretchStartTimer =
        [NSTimer timerWithTimeInterval:delay1
                                target:self
                              selector:@selector(countdownHomeStretch)
                              userInfo:nil
                               repeats:NO];
      [countdownHomeStretchStartTimer retain];
      // Without this, the timer doesn't fire...
      NSRunLoop *L = [NSRunLoop currentRunLoop];
      [L addTimer:countdownHomeStretchStartTimer forMode:NSDefaultRunLoopMode];
    }

    if (delay2 > 0) {
      countdownHomeStretchEndTimer =
        [NSTimer timerWithTimeInterval:delay2
                                target:self
                              selector:@selector(countdownHomeStretch)
                              userInfo:nil
                               repeats:NO];
      [countdownHomeStretchEndTimer retain];
      // Without this, the timer doesn't fire...
      NSRunLoop *L = [NSRunLoop currentRunLoop];
      [L addTimer:countdownHomeStretchEndTimer forMode:NSDefaultRunLoopMode];
    }
  }
}


- (void)countdownHomeStretch
{
  if (!usesCountdownTimer) return;

  NSTimeInterval secs = [countdownDate timeIntervalSinceNow];
  if (usesHomeStretchTimer &&
      secs >= -countdown_homestretch_interval &&
      secs <   countdown_homestretch_interval) {
    [self setTimeStyle:SS];
  } else {
    [self setTimeStyle:HHMMSS];
  }
}


# ifdef USE_IPHONE

- (void)showBlurb:(NSString *)text
{
  CGRect frame = [self frame];
  // CGFloat rot;
  int size = ([self frame].size.width >= 768 ? 24 : 14);
  UIFont *font = [UIFont boldSystemFontOfSize: size];
  // #### sizeWithFont deprecated as of iOS 7; use boundingRectWithSize.
  CGSize tsize = [text sizeWithFont:font
                  constrainedToSize:CGSizeMake(frame.size.width,
                                               frame.size.height)];

  // Don't know how to find inner margin of UITextView.
  CGFloat margin = 10;
  tsize.width  += margin * 4;
  tsize.height += margin * 2;

  frame = CGRectMake (0, 0, tsize.width, tsize.height);
  CGRect frame2 = frame;

  frame.origin.x = ([self frame].size.width  - tsize.width) / 2;
  frame.origin.y =  [self frame].size.height - tsize.height;

  if (aboutBox)
    [aboutBox removeFromSuperview];
  
  /* The aboutBox is just a view that applies rotation.
     Inside that is aboutText, pre-rotated, which draws the text.
     (We used to set the transformation on the UITextView itself,
     but that stopped working in iOS 7.)
     Finally inside that is a CALayer that applies dropshadows to the text.
   */
  aboutBox = [[UIView alloc] init];
  [aboutBox setFrame:frame];
  // aboutBox.transform = CGAffineTransformMakeRotation (rot);
  
  UITextView *aboutText = [[UITextView alloc] initWithFrame:frame2];
 // aboutText.transform = CGAffineTransformMakeRotation (rot);
  aboutText.font = font;
  aboutText.textAlignment = NSTextAlignmentCenter;
  aboutText.showsHorizontalScrollIndicator = NO;
  aboutText.showsVerticalScrollIndicator = NO;
  aboutText.scrollEnabled = NO;
  aboutText.textColor = [UIColor whiteColor];  // fg?
  aboutText.backgroundColor = [UIColor clearColor];
  aboutText.text = text;
  aboutText.editable = NO;

  // Too subtle?
  CALayer *textLayer = (CALayer *)[aboutText.layer.sublayers objectAtIndex:0];
  textLayer.shadowColor   = [UIColor blackColor].CGColor;
  textLayer.shadowOffset  = CGSizeMake(0, 0);
  textLayer.shadowOpacity = 1;
  textLayer.shadowRadius  = 2;
  CABasicAnimation *anim =
    [CABasicAnimation animationWithKeyPath:@"opacity"];
  anim.duration     = 0.3;
  anim.repeatCount  = 1;
  anim.autoreverses = NO;
  anim.fromValue    = [NSNumber numberWithFloat:0.0];
  anim.toValue      = [NSNumber numberWithFloat:1.0];
  [aboutText.layer addAnimation:anim forKey:@"animateOpacity"];

  UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]
                                  initWithTarget:self
                                  action:@selector(handleAboutTap)];
  [aboutBox addGestureRecognizer: tap];
  [tap release];

  [aboutBox addSubview:aboutText];
  [[self superview] addSubview:aboutBox];
}

- (void) handleAboutTap
{
  // Find the URL in the "about" text, and load it in Safari.
  NSDictionary *info = [[NSBundle bundleForClass:[self class]] infoDictionary];
  NSString *s = [info objectForKey:@"CFBundleGetInfoString"];
  NSRange r = [s rangeOfString:@"http://" options:0];
  if (r.location == NSNotFound)
    return;
  s = [s substringFromIndex: r.location];
  r = [s rangeOfCharacterFromSet:
           [NSCharacterSet whitespaceAndNewlineCharacterSet]];
  if (r.location != NSNotFound)
    s = [s substringToIndex: r.location];
  [[UIApplication sharedApplication] openURL:[NSURL URLWithString: s]];
}

#endif /* USE_IPHONE */


/* The About button in the menu bar or preferences sheet.
 */
- (IBAction)aboutClick:(id)sender
{
  NSBundle *nsb = [NSBundle bundleForClass:[self class]];
  NSAssert1 (nsb, @"no bundle for class %@", [self class]);

  NSDictionary *info = [nsb infoDictionary];
# ifndef USE_IPHONE
  NSString *name = @"Dali Clock";
  NSString *vers = [info objectForKey:@"CFBundleVersion"];

  NSString *ver2 = [info objectForKey:@"CFBundleShortVersionString"];
  NSString *icon = [info objectForKey:@"CFBundleIconFile"];
  NSString *cred_file = [nsb pathForResource:@"Credits" ofType:@"html"];
  NSAttributedString *cred = [[[NSAttributedString alloc]
                                initWithPath:cred_file
                                documentAttributes:(NSDictionary **)NULL]
                               autorelease];
  NSString *icon_file = [nsb pathForResource:icon ofType:@"icns"];
  NSImage *iimg = [[NSImage alloc] initWithContentsOfFile:icon_file];

  NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:
    name, @"ApplicationName",
    vers, @"Version",
    ver2, @"ApplicationVersion",
    @"",  @"Copyright",
    cred, @"Credits",
    iimg, @"ApplicationIcon",
    nil];

  [[NSApplication sharedApplication]
    orderFrontStandardAboutPanelWithOptions:dict];

# else  /* USE_IPHONE */
  NSString *vers = [info objectForKey:@"CFBundleGetInfoString"];
  vers = [vers stringByReplacingOccurrencesOfString:@", "  withString:@"\n"];
  vers = [vers stringByReplacingOccurrencesOfString:@".\n" withString:@"\n"];
  [self showBlurb:vers];
# endif /* USE_IPHONE */
}


// hint for XCode popup menu
#pragma mark accessors

/* [ken] if you (1) only wanted these for bindings purposes (true) and
   (2) they were dumb methods that just manipulated ivars of the same
   name (not true, they mostly write into config), then we would not
   have to write them.  I use the freeware Accessorizer to give me
   accessor templates.
 */
- (int)hourStyle { return !config.twelve_hour_p; }
- (void)setHourStyle:(int)aHourStyle
{
  config.twelve_hour_p = !aHourStyle;
}


- (int)timeStyle { return config.time_mode; }
- (void)setTimeStyle:(int)aTimeStyle
{
  if (config.time_mode != aTimeStyle) {
    config.time_mode = aTimeStyle;
    config.width++;  // kludge: force regeneration of bitmap
    [self setFrameSize:[self frame].size];
  }
}


- (int)dateStyle { return config.date_mode; }
- (void)setDateStyle:(int)aDateStyle
{
  config.date_mode = aDateStyle;
}


- (float)cycleSpeed { return (float)config.max_cps; }
- (void)setCycleSpeed:(float)aCycleSpeed
{
  if (config.max_cps != aCycleSpeed) {
    config.max_cps = (int)aCycleSpeed;
    if (config.max_cps < 0.0001) {
      [self setForeground:initialForegroundColor
               background:initialBackgroundColor];
    } else {
      [self colorTick];
    }
  }
}


- (int)usesCountdownTimer { return usesCountdownTimer; }
- (void)setUsesCountdownTimer:(int)flag
{
  if (usesCountdownTimer != flag) {

    // If we're in the home stretch while turning off countdown, pop out.
    NSTimeInterval secs = [countdownDate timeIntervalSinceNow];
    if (!flag &&
        usesHomeStretchTimer &&
        secs >= -countdown_homestretch_interval &&
        secs <   countdown_homestretch_interval) {
      [self setTimeStyle:HHMMSS];
    }

    usesCountdownTimer = flag;
    [self updateCountdown];
  }
}


- (int)usesHomeStretchTimer { return usesHomeStretchTimer; }
- (void)setUsesHomeStretchTimer:(int)flag
{
  if (usesHomeStretchTimer != flag) {
    usesHomeStretchTimer = flag;
    [self updateCountdown];
  }
}


- (NSDate *)countdownDate { return [[countdownDate retain] autorelease]; }
- (void)setCountdownDate:(NSDate *)aCountdownDate
{
  if (countdownDate != aCountdownDate) {
    [countdownDate release];
    countdownDate = [aCountdownDate retain];
    [self updateCountdown];
  }
}


- (NSColor *)initialForegroundColor
{
  return [[initialForegroundColor retain] autorelease];
}


- (void)setInitialForegroundColor:(NSColor *)c
{
  if (initialForegroundColor != c) {
    if (initialForegroundColor)
      [initialForegroundColor release];
    initialForegroundColor = (c ? [c retain] : 0);
    [self setForeground:initialForegroundColor
             background:initialBackgroundColor];
  }
}


- (NSColor *)initialBackgroundColor
{
  return [[initialBackgroundColor retain] autorelease];
}


- (void)setInitialBackgroundColor:(NSColor *)c
{
  if (initialBackgroundColor != c) {
    if (initialBackgroundColor)
      [initialBackgroundColor release];
    initialBackgroundColor = (c ? [c retain] : 0);
    [self setForeground:initialForegroundColor
             background:initialBackgroundColor];
  }
}


- (void)setConstrainSizes:(BOOL)constrain_p;
{
  constrainSizes = constrain_p;
}


- (void)setAutoDate:(float)interval
{
  autoDateInterval = interval;

  config.display_date_p = 1;
  [self dateTick];
  config.display_date_p = 0;
}

#ifdef USE_IPHONE
- (void)setScreenLocked:(BOOL)locked
{
  screenLocked = locked;

  // Reset the auto-date timer every time the screen is locked/unlocked,
  // so that it never happens while the user is active.
  [self setAutoDate:autoDateInterval];
}
#endif /* USE_IPHONE */


@end


#ifdef USE_IPHONE
static void
rgb_to_hsv (CGFloat R, CGFloat G, CGFloat B,
	    CGFloat *h, CGFloat *s, CGFloat *v)
{
  CGFloat H, S, V;
  CGFloat cmax, cmin;
  CGFloat cmm;
  int imax;
  cmax = R; cmin = G; imax = 1;
  if  ( cmax < G ) { cmax = G; cmin = R; imax = 2; }
  if  ( cmax < B ) { cmax = B; imax = 3; }
  if  ( cmin > B ) { cmin = B; }
  cmm = cmax - cmin;
  V = cmax;
  if (cmm == 0)
    S = H = 0;
  else
    {
      S = cmm / cmax;
      if       (imax == 1)    H =       (G - B) / cmm;
      else  if (imax == 2)    H = 2.0 + (B - R) / cmm;
      else /*if (imax == 3)*/ H = 4.0 + (R - G) / cmm;
      if (H < 0) H += 6.0;
    }
  *h = (H * 60.0);
  *s = S;
  *v = V;
}
#endif /* USE_IPHONE */



/* Prints the GL error to the system log.
 */
static void
log_gl_error (const char *type, GLenum err)
{
  char buf[100];
  const char *e;

# ifndef  GL_TABLE_TOO_LARGE_EXT
#  define GL_TABLE_TOO_LARGE_EXT 0x8031
# endif
# ifndef  GL_TEXTURE_TOO_LARGE_EXT
#  define GL_TEXTURE_TOO_LARGE_EXT 0x8065
# endif
# ifndef  GL_INVALID_FRAMEBUFFER_OPERATION
#  define GL_INVALID_FRAMEBUFFER_OPERATION 0x0506
# endif

  switch (err) {
    case GL_NO_ERROR:              return;
    case GL_INVALID_ENUM:          e = "invalid enum";      break;
    case GL_INVALID_VALUE:         e = "invalid value";     break;
    case GL_INVALID_OPERATION:     e = "invalid operation"; break;
    case GL_STACK_OVERFLOW:        e = "stack overflow";    break;
    case GL_STACK_UNDERFLOW:       e = "stack underflow";   break;
    case GL_OUT_OF_MEMORY:         e = "out of memory";     break;
    case GL_TABLE_TOO_LARGE_EXT:   e = "table too large";   break;
    case GL_TEXTURE_TOO_LARGE_EXT: e = "texture too large"; break;
    case GL_INVALID_FRAMEBUFFER_OPERATION: e = "invalid framebuffer op"; break;
    default:
      e = buf; sprintf (buf, "unknown GL error 0x%04x", (int) err); break;
  }

  NSLog (@"%s: GL error in %s: %s", progname, type, e);
}


/* Log a GL error and abort. */
static void
check_gl_error (const char *type)
{
  GLenum err = glGetError();
  if (err == GL_NO_ERROR) return;
  log_gl_error (type, err);
  abort();
}
