/* Dali Clock - a melting digital clock for PalmOS.
 * Copyright (c) 1991-2009 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.
 */


function ClockAssistant() {
  /* this is the creator function for your scene assistant object. It
     will be passed all the additional parameters (after the scene
     name) that were passed to pushScene. The reference to the scene
     controller (this.controller) has not be established yet, so any
     initialization that needs the scene controller should be done in
     the setup function below. */
}

// For setup tasks that have to happen when the scene is first created.
//
ClockAssistant.prototype.setup = function() {

  // For some reason I can't get these numbers out of div.style.width.
  // So I have to hardcode it here.
  //
  this.screen_width  = 318;
  this.screen_height = 420;

  // Default settings
  //
  this.model = {
    time_mode:    'HHMMSS',
    date_mode:    'DDMMYY',
    twelve_hour_p: true,
    fps:           12,
    cps:           8,
  };

  // Load the Preferences cookie
  //
  
  /*
  this.prefs = new Mojo.Model.Cookie("Preferences"); 
  var op = this.prefs.get();
  if (op) {
    if (op.time_mode)     { this.model.time_mode     = op.time_mode;     }
    if (op.date_mode)     { this.model.date_mode     = op.date_mode;     }
    if (op.twelve_hour_p) { this.model.twelve_hour_p = op.twelve_hour_p; }
    if (op.fps)           { this.model.fps           = op.fps;           }
    if (op.cps)           { this.model.cps           = op.cps;           }
  }
  */

  this.vp_scaling_p = 0;  // works on Palm Host, not on device.

  // Initialize the clock.
  //
  this.orientation   = 'up';
  this.load_fonts();
  this.clock_reset();
  this.init_colors();

  // Bind taps to show the date.
  //
  var bdiv   = document.getElementById ("clockbg");
  var canvas = document.getElementById ("canvas");
  //Mojo.Event.listen (bdiv,   Mojo.Event.tap, this.date_tap.bind(this));
  //Mojo.Event.listen (canvas, Mojo.Event.tap, this.date_tap.bind(this));

  // Set up the App menu.
  //

  /*
  this.controller.setupWidget(Mojo.Menu.appMenu,
    { omitDefaultItems: true },
    { visible: true,
      items: [ {label: "About Dali Clock...", command: 'doAbout' },
               //Mojo.Menu.editItem,
               {label: "Preferences...", command: 'doPrefs' },
               {label: "Help...", command: 'doHelp', disabled: true }
             ]});
    */
}



// Tasks that have to happen each time the scene is deactivated.
//
// #### This is supposed to be called any time some other app
//      becomes the frontmost app, with this app being buried,
//      but as far as I can tell, it is never called!  This
//      means that this app continues running timers and using
//      CPU when it is hidden but un-quit!
//
//      https://prerelease.palm.com/confluence/display/sdk/Launch+Cycle
//
ClockAssistant.prototype.deactivate = function() {
  if (this.clock_id) {
    this.controller.window.clearTimeout (this.clock_id);
    this.clock_id = undefined;
  }
  if (this.color_id) {
    this.controller.window.clearTimeout (this.color_id);
    this.color_id = undefined;
  }
// document.getElementById("log").firstChild.nodeValue += "DE ";
}


// About to exit.
//
ClockAssistant.prototype.cleanup = function() {
  if (this.clock_id) {
    this.controller.window.clearTimeout (this.clock_id);
    this.clock_id = undefined;
  }
  if (this.date_timer_id) {
    this.controller.window.clearTimeout (this.date_timer_id);
    this.date_timer_id = undefined;
  }
};


ClockAssistant.prototype.orientationChanged = function(orient) {
  if (orient != this.orientation) {
    this.orientation = orient;
    this.clock_reset();
  }
}


// Called when something is selected from the App menu.
//
ClockAssistant.prototype.handleCommand = function(event) {
  if (event.type == Mojo.Event.command) {	
    switch (event.command) {
      case 'doAbout':
        var title   = Mojo.Controller.appInfo.title;
        var version = Mojo.Controller.appInfo.version;
        var vendor  = Mojo.Controller.appInfo.vendor;
        var email   = Mojo.Controller.appInfo.vendor_email;
        var url     = Mojo.Controller.appInfo.vendor_url;
        var date    = Mojo.Controller.appInfo.release_date;
        var year    = date.replace (new RegExp('^.*[- ]([0-9]{4})$'), '$1');
        year = '1991-' + year;
        var body    = ('<div align=center>' +
                       'Copyright &copy; ' + year + '<BR>' +
                       vendor + ' &lt;' + 
                       '<A HREF="mailto:' + email + '">' + email + 
                       '</A>&gt;<BR><BR>' +
                       '<A HREF="' + url + '">' + url + '</A>' +
                       '</div>'
                       );
        title = '<div align=center>' + title + ' ' + version + '</div>';

        this.controller.showAlertDialog({
          onChoose: function(value) {},
              title:title,
              message:body,
              choices:[ {label:'OK', value:'OK', type:'color'} ]});
        break;
      case 'doPrefs':
      Mojo.Controller.stageController.pushScene ("prefs", 
                                                 this.prefs, this.model);
      break;
    }
  }
}


// Reset the animation when the settings (number of digits, orientation)
// has changed.  We have to start over since the resolution is different.
//
ClockAssistant.prototype.clock_reset = function() {

  this.pick_font_size();

  this.display_date_p = false;         // the request
  this.display_state  = 'time';        // the reality

  this.current_digits = new Array(6);  // what was there
  this.target_digits  = new Array(6);  // where we are going

  this.orig_frames    = new Array(6);  // what was there
  this.current_frames = new Array(6);  // current intermediate animation
  this.target_frames  = new Array(6);  // where we are going

  for (var i = 0; i < 6; i++) {
    this.current_frames[i] = this.copy_frame (this.font.empty_frame);
  }


  // Set the CSS orientation of the canvas based on the current orientation.
  //
  var canvas = document.getElementById ("canvas");

  switch (this.orientation) {
    case 'left':  canvas.style.webkitTransform = 'rotate(90deg)';  break;
    case 'right': canvas.style.webkitTransform = 'rotate(-90deg)'; break;
    case 'down':  canvas.style.webkitTransform = 'rotate(180deg)'; break;
    default:      canvas.style.webkitTransform = ''; break;
  }

  // And now set the CSS position and size of the canvas
  // (not the same thing as size of the canvas's frame buffer).
  //
  var width  = canvas.width;   // size of the framebuffer
  var height = canvas.height;
  var nn, cc;

  switch (this.model.time_mode) {
    case 'SS':   nn = 2; cc = 0; break;
    case 'HHMM': nn = 4; cc = 1; break;
    default:     nn = 6; cc = 2; break;
  }

  if (this.vp_scaling_p) {   // was doubled, for anti-aliasing
    width  /= 2;
    height /= 2;
  }

  x = (this.screen_width  - width)  / 2;
  y = (this.screen_height - height) / 2;

  canvas.style.left   = x + 'px';
  canvas.style.top    = y + 'px';
  canvas.style.width  = width  + 'px';
  canvas.style.height = height + 'px';

}


ClockAssistant.prototype.fill_target_digits = function(date) {

  var h = date.getHours();
  var m = date.getMinutes();
  var s = date.getSeconds();
  var D = date.getDate();
  var M = date.getMonth() + 1;
  var Y = date.getFullYear() % 100;

  if (this.model.twelve_hour_p) {
    if (h > 12) { h -= 12; }
    else if (h == 0) { h = 12; }
  }

  for (var i = 0; i < 6; i++) {
    this.target_digits[i] = undefined;
  }

  if (this.display_state == 'time' ||
      this.display_state == 'dash' ||
      this.display_state == 'dash2') {

    switch (this.display_state) {
      case 'dash':  this.display_state = 'dash2'; break;
      case 'dash2': this.display_state = 'time';  break;
    }

    switch (this.model.time_mode) {
      case 'SS':
        this.target_digits[0] = Math.floor(s / 10);
        this.target_digits[1] =           (s % 10);
        break;
      case 'HHMM':
        this.target_digits[0] = Math.floor(h / 10);
        this.target_digits[1] =           (h % 10);
        this.target_digits[2] = Math.floor(m / 10);
        this.target_digits[3] =           (m % 10);
        if (this.model.twelve_hour_p && this.target_digits[0] == 0) {
          this.target_digits[0] = -1;
        }
        break;
      default:
        this.target_digits[0] = Math.floor(h / 10);
        this.target_digits[1] =           (h % 10);
        this.target_digits[2] = Math.floor(m / 10);
        this.target_digits[3] =           (m % 10);
        this.target_digits[4] = Math.floor(s / 10);
        this.target_digits[5] =           (s % 10);
        if (this.model.twelve_hour_p && this.target_digits[0] == 0) {
          this.target_digits[0] = -1;
        }
        break;
    }
  } else {

    switch (this.display_state) {
      case 'date_in':   this.display_state = 'date';      break;
      case 'date_out':  this.display_state = 'date_out2'; break;
      case 'date_out2': this.display_state = 'dash';      break;
      case'dash':       this.display_state = 'dash2';     break;
    }

    switch (this.model.date_mode) {
      case 'MMDDYY':
        switch (this.model.time_mode) {
          case 'SS':
            this.target_digits[0] = Math.floor(D / 10);
            this.target_digits[1] =           (D % 10);
            break;
          case 'HHMM':
            this.target_digits[0] = Math.floor(M / 10);
            this.target_digits[1] =           (M % 10);
            this.target_digits[2] = Math.floor(D / 10);
            this.target_digits[3] =           (D % 10);
            break;
          default:  // HHMMSS
            this.target_digits[0] = Math.floor(M / 10);
            this.target_digits[1] =           (M % 10);
            this.target_digits[2] = Math.floor(D / 10);
            this.target_digits[3] =           (D % 10);
            this.target_digits[4] = Math.floor(Y / 10);
            this.target_digits[5] =           (Y % 10);
            break;
        }
        break;
      case 'DDMMYY':
        switch (this.model.time_mode) {
          case 'SS':
            this.target_digits[0] = Math.floor(D / 10);
            this.target_digits[1] =           (D % 10);
            break;
          case 'HHMM':
            this.target_digits[0] = Math.floor(D / 10);
            this.target_digits[1] =           (D % 10);
            this.target_digits[2] = Math.floor(M / 10);
            this.target_digits[3] =           (M % 10);
            break;
          default:  // HHMMSS
            this.target_digits[0] = Math.floor(D / 10);
            this.target_digits[1] =           (D % 10);
            this.target_digits[2] = Math.floor(M / 10);
            this.target_digits[3] =           (M % 10);
            this.target_digits[4] = Math.floor(Y / 10);
            this.target_digits[5] =           (Y % 10);
            break;
        }
        break;
      default:
        switch (this.model.time_mode) {
          case 'SS':
            this.target_digits[0] = Math.floor(D / 10);
            this.target_digits[1] =           (D % 10);
            break;
          case 'HHMM':
            this.target_digits[0] = Math.floor(M / 10);
            this.target_digits[1] =           (M % 10);
            this.target_digits[2] = Math.floor(D / 10);
            this.target_digits[3] =           (D % 10);
            break;
          default:  // HHMMSS
            this.target_digits[0] = Math.floor(Y / 10);
            this.target_digits[1] =           (Y % 10);
            this.target_digits[2] = Math.floor(M / 10);
            this.target_digits[3] =           (M % 10);
            this.target_digits[4] = Math.floor(D / 10);
            this.target_digits[5] =           (D % 10);
            break;
        }
        break;
    }
  }
}


ClockAssistant.prototype.load_fonts = function() {

  var nfonts = 6;
  this.fonts = new Array(nfonts);
  for (var i = 0; i < nfonts; i++) {
    //var path = Mojo.appPath + "images/font" + i + ".json";
    //this.fonts[i] = Mojo.loadJSONFile (path);
    this.fonts[i] = FontList[i];
    this.fonts[i].empty_frame = this.make_empty_frame(this.fonts[i]);
  }
}


// Find the largest font that fits in the canvas given the current settings
// (number of digits and orientation).
//
ClockAssistant.prototype.pick_font_size = function() {

  var nn, cc;

  switch (this.model.time_mode) {
    case 'SS':   nn = 2; cc = 0; break;
    case 'HHMM': nn = 4; cc = 1; break;
    default:     nn = 6; cc = 2; break;
  }

  var canvas = document.getElementById ("canvas");

  var width  = this.screen_width;
  var height = this.screen_height;

  if (this.vp_scaling_p) {   // double it, for anti-aliasing
    width  *= 2;
    height *= 2;
  }

  if (this.orientation == 'left' || this.orientation == 'right') {
    var swap = width; width = height; height = swap;
  }

  for (var i = this.fonts.length-1; i >= 0; i--) {
    var font = this.fonts[i];
    var w = (font.char_width * nn) + (font.colon_width * cc);
    var h = font.char_height;

    if ((w <= width && h <= height) ||
        i == 0) {
      this.font     = font;
      canvas.width  = w;
      canvas.height = h;
      return;
    }
  }
}


ClockAssistant.prototype.make_empty_frame = function(font) {
  var cw = font.char_width;
  var ch = font.char_height;
  var mid = Math.round(cw / 2);
  var frame = new Array(ch);
  for (var y = 0; y < ch; y++) {
    var line, seg;
    line = frame[y] = new Array(1);
    seg = line[0] = new Array(2);
    seg[0] = seg[1] = mid;
  }
  return frame;
}


ClockAssistant.prototype.copy_frame = function(oframe) {

  if (oframe == undefined) { return oframe; }
  var nframe = oframe.slice();   // copy array of lines
  var ch = nframe.length;
  for (var y = 0; y < ch; y++) {
    if (nframe[y]) { nframe[y] = nframe[y].slice(); }  // copy array of segs
    var segs = nframe[y].length;
    for (var x = 0; x < segs; x++) {
      if (nframe[y][x]) { nframe[y][x] = nframe[y][x].slice(); }  // copy segs
    }
    segs = nframe[y].length;
  }
  return nframe;
}


ClockAssistant.prototype.draw_frame = function(ctx, font, frame,
                                               x, y, colonic_p) {

  if (! frame) return;

  var cw = (colonic_p ? font.colon_width : font.char_width);
  var ch = font.char_height;

  for (var py = 0; py < ch; py++)
    {
      var line = frame[py];
      var nsegs = line.length;

      for (var px = 0; px < nsegs; px++)
        {
          var seg = line[px];
          ctx.fillRect (x + seg[0], y + py,
                        seg[1] - seg[0], 1);
        }
    }
  return cw;
}


ClockAssistant.prototype.start_sequence = function(font, date) {

  // Copy the (old) current_frames into the (new) orig_frames,
  // since that's what's on the screen now.
  //
  for (var i = 0; i < 6; i++) {
    this.orig_frames[i] = this.copy_frame (this.current_frames[i]);
    this.current_digits[i] = this.target_digits[i];
  }

  // generate new target_digits
  this.fill_target_digits(date);

  // Fill the (new) target_frames from the (new) target_digits.
  //
  for (var i = 0; i < 6; i++) {
    this.target_frames[i] = (this.current_digits[i] < 0 ? undefined : 
                             font.segments[this.current_digits[i]]);
  }

  this.draw_clock (font);
}


ClockAssistant.prototype.one_step = function(font, orig, curr, target, msecs) {
  var ch = font.char_height;
  var frac = msecs / 1000.0;

  if (! orig)   { orig   = font.empty_frame; }
  if (! target) { target = font.empty_frame; }

  for (var i = 0; i < ch; i++) {
    var oline = orig[i];
    var cline = curr[i];
    var tline = target[i];
    var osegs = oline.length;
    var tsegs = tline.length;
    var segs = (osegs > tsegs ? osegs : tsegs);

    // orig and target might have different numbers of segments.
    // current ends up with the maximal number.

    for (var j = 0; j < segs; j++) {
      var oseg = oline[j] || oline[0];
      var cseg = cline[j];
      var tseg = tline[j] || tline[0];

      if (! cseg) { cseg = cline[j] = new Array(2); }

      cseg[0] = oseg[0] + Math.round (frac * (tseg[0] - oseg[0]));
      cseg[1] = oseg[1] + Math.round (frac * (tseg[1] - oseg[1]));
    }
  }
}


ClockAssistant.prototype.tick_sequence = function(font, date) {

  var ctime = date.getTime();
  var secs = Math.floor(ctime/1000);
  var msecs = ctime - (secs*1000);    // msec position within this second

  if (secs != this.last_secs) {
    // End of the animation sequence; fill target_frames with the
    // digits of the current time.
    this.start_sequence (font, date);
    this.last_secs = secs;
  }

  // Linger for about 1/10th second at the end of each cycle.
  msecs *= 1.2;
  if (msecs > 1000) msecs = 1000;

  // Construct current_frames by interpolating between
  // orig_frames and target_frames.
  //
  for (var i = 0; i < 6; i++) {
    this.one_step (font,
                   this.orig_frames[i],
                   this.current_frames[i],
                   this.target_frames[i],
                   msecs);
  }
}



ClockAssistant.prototype.draw_clock = function(font) {

  var canvas = document.getElementById ("canvas");
  var ctx = canvas.getContext("2d");

  var x = 0;
  var y = 0;

  ctx.clearRect (0, 0, canvas.width, canvas.height);

  var date_p = (this.display_state != 'time');

  switch (this.model.time_mode) {
    case 'SS':
      x += this.draw_frame (ctx, font, this.current_frames[0], x, y, false);
           this.draw_frame (ctx, font, this.current_frames[1], x, y, false);
      break;
    case 'HHMM':
      x += this.draw_frame (ctx, font, this.current_frames[0], x, y, false);
      x += this.draw_frame (ctx, font, this.current_frames[1], x, y, false);
      x += this.draw_frame (ctx, font, font.segments[(date_p ? 11 : 10)],
                            x, y, true);
      x += this.draw_frame (ctx, font, this.current_frames[2], x, y, false);
           this.draw_frame (ctx, font, this.current_frames[3], x, y, false);
      break;
    default:    // HHMMSS
      x += this.draw_frame (ctx, font, this.current_frames[0], x, y, false);
      x += this.draw_frame (ctx, font, this.current_frames[1], x, y, false);
      x += this.draw_frame (ctx, font, font.segments[(date_p ? 11 : 10)],
                            x, y, true);
      x += this.draw_frame (ctx, font, this.current_frames[2], x, y, false);
      x += this.draw_frame (ctx, font, this.current_frames[3], x, y, false);
      x += this.draw_frame (ctx, font, font.segments[(date_p ? 11 : 10)],
                            x, y, true);
      x += this.draw_frame (ctx, font, this.current_frames[4], x, y, false);
           this.draw_frame (ctx, font, this.current_frames[5], x, y, false);
      break;
  }
}


ClockAssistant.prototype.clock_timer = function() {

  if (this.display_date_p && this.display_state == 'time') {
    this.display_state = 'date_in';
  } else if (!this.display_date_p && this.display_state == 'date') {
    this.display_state = 'date_out';
  }

  var now = new Date();
  this.tick_sequence (this.font, now);
  this.draw_clock (this.font);

  // Re-trigger our timer.
  this.clock_timer_id = window.setTimeout(this.timer_fn, this.timer_freq);
};


ClockAssistant.prototype.date_tap = function(event) {

  this.display_date_p = true;   // turn on date display at next second-tick.
  var when = 1000;		// set a timer to turn it back off.
  this.date_off_fn = this.date_off.bind(this);
  this.date_timer_id = window.setTimeout(this.date_off_fn, when);
};


ClockAssistant.prototype.date_off = function() {
  this.display_date_p = false;
};


ClockAssistant.prototype.init_colors = function() {
  this.fg_hsv = [200, 0.4, 1.0];
  this.bg_hsv = [128, 1.0, 0.4];
  this.tick_colors();
}

ClockAssistant.prototype.tick_colors = function() {

  var bdiv   = document.getElementById ("clockbg");
  var canvas = document.getElementById ("canvas");
  var ctx    = canvas.getContext("2d");

  ctx.fillStyle = this.hsv_to_rgb(this.fg_hsv[0],
                                  this.fg_hsv[1],
                                  this.fg_hsv[2]);
  bdiv.style.backgroundColor = this.hsv_to_rgb(this.bg_hsv[0],
                                               this.bg_hsv[1],
                                               this.bg_hsv[2]);

  this.fg_hsv[0] += 1;
  if (this.fg_hsv[0] >= 360) { this.fg_hsv[0] -= 360; }

  this.bg_hsv[0] += 0.91;
  if (this.bg_hsv[0] >= 360) { this.bg_hsv[0] -= 360; }
}

ClockAssistant.prototype.color_timer = function() {
  this.tick_colors();
  this.color_id = window.setTimeout(this.color_fn, this.color_freq);
}


// H is in the range 0 - 360;
// S and V are in the range 0.0 - 1.0.
// Returns string "#RRGGBB".
//
ClockAssistant.prototype.hsv_to_rgb = function(h,s,v) {

  if (s < 0) s = 0;
  if (v < 0) v = 0;
  if (s > 1) s = 1;
  if (v > 1) v = 1;

  var S = s; 
  var V = v;
  var H = (h % 360) / 60.0;
  var i = Math.floor(H);
  var f = H - i;
  var p1 = V * (1 - S);
  var p2 = V * (1 - (S * f));
  var p3 = V * (1 - (S * (1 - f)));
  if	  (i == 0) { R = V;  G = p3; B = p1; }
  else if (i == 1) { R = p2; G = V;  B = p1; }
  else if (i == 2) { R = p1; G = V;  B = p3; }
  else if (i == 3) { R = p1; G = p2; B = V;  }
  else if (i == 4) { R = p3; G = p1; B = V;  }
  else		   { R = V;  G = p1; B = p2; }
  R = Math.floor(R * 255);
  G = Math.floor(G * 255);
  B = Math.floor(B * 255);
  return ('#' +
          (R >> 4).toString(16) +
          (R & 15).toString(16) +
          (G >> 4).toString(16) +
          (G & 15).toString(16) +
          (B >> 4).toString(16) +
          (B & 15).toString(16));
}
// For setup tasks that have to happen each time the scene is activated.
//
ClockAssistant.prototype.activate = function() {

  // Start the clock timer.
  this.timer_freq = Math.round (1000 / this.model.fps);
  this.timer_fn = this.clock_timer.bind(this);
  this.clock_id = this.controller.window.setTimeout(this.timer_fn, 0);

  // Start the color timer.
  this.color_freq = Math.round (1000 / this.model.cps);
  this.color_fn = this.color_timer.bind(this);
  this.color_id = this.controller.window.setTimeout(this.color_fn, 0);

  // Damn, we have to do this in case the prefs changed.
  this.clock_reset();

// document.getElementById("log").firstChild.nodeValue += "AC ";
}
