/* Copyright © 2009-2010, Julien Quint <consulting@romulusetrem.us>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   • Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *   • Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   • Neither the name of consulting.romulusetrem.us nor the names of its
 *     contributors may be used to endorse or promote products derived from
 *     this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE. */

// Localized strings
L10N.LESS = "[-]";
L10N.MORE = "[+]";
L10N.STOPPED = "Stopped.";
L10N.STROKE = "{0} stroke";
L10N.STROKE_DISPLAY = "Stroke #{0}/{1}";
L10N.STROKES = "{0} strokes";

// This value should be available from the kanji data
var DEFAULT_SIZE = 109;
var DEFAULT_LIST = 20;
var DEFAULT_WORDS = 5;

var SPEED_MIN = 1;
var SPEED_MAX = 10;

// Initialize the display view
function display()
{
  test_canvas();
  init_speed();
  var kanji = new Kanji($("#data"));
  // Resize the canvases
  var canvases = $("#canvases");
  var static_canvas = $("#static");
  var anim_canvas = $("#animation");
  var hl_canvas = $("#highlights");
  var size_box = $("#size_box");
  var handle = $("#resize_handle");
  var pause_button = $("#pause_button");
  var paused_display = $("#paused");
  var play_button = $("#play_button");
  var stop_button = $("#stop_button");
  var stroke_display = $("#stroke_display");
  add_event_listener(kanji, "@frozen", function() {
      pause_button.show(false);
      play_button.show(true);
    });
  add_event_listener(kanji, "@paused", function() {
      pause_button.show(false);
      play_button.show(true);
      paused_display.show(true);
    });
  add_event_listener(kanji, "@playing", function() {
      size_box.disabled = true;
      size_box.add_class("disabled");
      pause_button.show(true);
      play_button.show(false);
      paused_display.show(false);
      handle.show(false);
    });
  add_event_listener(kanji, "@stopped", function() {
      play_button.show(true);
      play_button.remove_class("disabled");
      pause_button.show(false);
      size_box.disabled = false;
      size_box.remove_class("disabled");
      paused_display.show(false);
      handle.show(true);
      stroke_display.innerHTML = L10N.STOPPED;
    });
  add_event_listener(kanji, "@stroke", function(_, e) {
      stroke_display.innerHTML = L10N.STROKE_DISPLAY.fmt(e.index + 1, e.total);
    });
  var min_size = canvases.pixel_value("min-width");
  make_number_box(size_box, true, .5, min_size);
  var s = get_from_storage("kanji_size") || canvases.pixel_value("width");
  if (s <= min_size) s = min_size;
  resize_canvases(canvases, s, kanji, static_canvas);
  size_box.update_value(s);
  var resizing = null;
  add_event_listener(size_box, "@num_box_change", function() {
      if (!kanji.playing) {
        resize_canvases(canvases, parseInt(size_box.value), kanji,
          static_canvas);
      }
    });
  add_event_listener(size_box, "@num_box_value", function() {
      if (!resizing && kanji.state == "stopped") {
        resize_canvases(canvases, parseInt(size_box.value), kanji,
          static_canvas);
      }
    });
  $$("#canvases > canvas").for_each(function(x) { x.width = x.height = s; });
  handle.addEventListener("mousedown", function(e) {
      if (kanji.state == "stopped") {
        resizing = { x: e.pageX, y: e.pageY, s: canvases.pixel_value("width") };
        document.body.style.WebkitUserSelect = "none";
      }
      e.stopPropagation();
    }, false);
  document.addEventListener("mousemove", function(e) {
      if (resizing) {
        var diff_x = e.pageX - resizing.x;
        var diff_y = e.pageY - resizing.y;
        var diff = diff_x > 0 ?
          diff_y > 0 ? Math.min(diff_x, diff_y) :
            diff_x > -diff_y ? diff_y : diff_x :
          diff_y < 0 ? Math.max(diff_x, diff_y) :
            diff_x > -diff_y ? diff_y : diff_x;
        size_box.update_value(resize_canvases(canvases, resizing.s + diff,
            kanji, static_canvas));
      }
    }, false);
  document.addEventListener("mouseup", function(e) {
      resizing = null;
      document.body.style.WebkitUserSelect = "inherit";
    }, false);
  canvases.addEventListener("mouseup", function() {
      if (!resizing) kanji.play_pause(anim_canvas);
    }, false);
  make_buttons({
      "#back_button": bind(kanji.prev_stroke, kanji, anim_canvas, -1),
      "#fwd_button": bind(kanji.next_stroke, kanji, anim_canvas, 1),
      "#help_button": help,
      "#pause_button": bind(kanji.pause, kanji, anim_canvas),
      "#play_button": bind(kanji.play, kanji, anim_canvas),
      "#stop_button": bind(kanji.stop, kanji, anim_canvas)
    });
  link_words();
  shorten_lists();
  kanji.stop(static_canvas);
  kanji.draw(static_canvas, kanji.BACKGROUND_COLOR);
  draw_handle($("#resize_handle canvas"));
  document.highlight = bind(highlight_, document, kanji, hl_canvas);
  document.unhighlight = bind(unhighlight_, document, hl_canvas);
}

// Setup the print page instead of the display page
function print()
{
  var canvases = $("#canvases");
  var min_size = canvases.pixel_value("min-width");
  var s = get_from_storage("kanji_size") || canvases.pixel_value("width");
  if (s < min_size) s = min_size;
  canvases.style.width = canvases.style.height = "{0}px".fmt(s);
  $$("#canvases > canvas").for_each(function(x) { x.width = x.height = s; });
  var kanji = new Kanji($("#data"));
  kanji.draw($("#static"), kanji.PRINT_COLOR);
  var strokes = kanji.paths.length;
  $("#strokes").innerHTML = strokes > 1 ?
    L10N.STROKES.fmt(strokes) : L10N.STROKE.fmt(strokes);
}


// Draw the resize handle
function draw_handle(canvas)
{
  var context = canvas.getContext("2d");
  var w = canvas.width;
  var h = canvas.height;
  var n = 4;
  var d = .1;
  for (var i = 0; i < n; ++i) {
    context.moveTo(w * (1 - d), h * (2 * i + 1) * d);
    context.lineTo(w * (2 * i + 1) * d, h * (1 - d));
  }
  context.stroke();
}

// Show/hide the help section
function help()
{
  var help = $("#help");
  if (help.has_class("no_help")) {
    try
    {
      var req = new XMLHttpRequest();
      req.open("GET", "help", false);
      req.send();
      if (req.status == 200) help.innerHTML = req.responseText;
    }
    catch (e) {}
    help.remove_class("no_help");
  }
  help.show();
  $("#help_button").toggle_class("selected");
  window.location.hash = help.has_class("hidden") ? "" : "help";
}

// Init the animation speed box and parameter
function init_speed()
{
  /*make_number_box($("#splits_box"), true, .01, 0);
  make_number_box($("#interval_box"), true, .1, 0);
  make_number_box($("#interval_strokes_box"), true, 1, 0);*/
  var box = $("#speed_box");
  make_number_box(box, true, .05, SPEED_MIN, SPEED_MAX);
  var speed = get_from_storage("kanji_speed") || box.value;
  if (speed >= SPEED_MIN && speed <= SPEED_MAX) box.value = speed;
  if (window.localStorage) {
    add_event_listener(box, "@num_box_value", function() {
        window.localStorage.setItem("kanji_speed", box.value);
      });
  }
}

// Link the characters of each sample word to the corresponding kanji page
// Use code search to find uncommon characters as well as common ones
function link_words()
{
  $$(".example_word").for_each(function(x) {
      re = new RegExp("([{0}])".fmt(x.querySelector(".word.chars").innerHTML),
        "g");
      var k = x.querySelector(".word.k");
      k.innerHTML = k.innerHTML.replace(re, function(_, p) {
          return "<a href=\"?u={0}\">{1}</a>"
            .fmt(p.charCodeAt(0).toString(16), p);
        } );
    });
}

// Set the size of the canvases when the drawing area is resized
function resize_canvases(canvases, size, kanji, static_canvas)
{
  canvases.style.width = canvases.style.height = "{0}px".fmt(size);
  var size = canvases.pixel_value("width");
  $$("#canvases > canvas").for_each(function(x) { x.width = x.height = size; });
  kanji.draw(static_canvas, kanji.BACKGROUND_COLOR);
  if (window.localStorage) {
    window.localStorage.setItem("kanji_size", size);
  }
  return size;
}

function shorten_list(items, threshold)
{
  if (items.length > threshold) {
    var ul = items[0].parentNode;
    var more_items = [];
    var more = create("li", { "class": "more_less" });
    ul.appendChild(more);
    function more_less()
    {
      if (more_items.length > 0) {
        ul.removeChild(more);
        more.innerHTML = L10N.LESS;
        more_items.forEach(function(x) { ul.appendChild(x); });
        more_items = [];
        ul.appendChild(more);
      } else {
        more.innerHTML = L10N.MORE;
        for (var i = threshold - 1, n = items.length - 1; i < n; ++i) {
          more_items.push(items[i]);
          ul.removeChild(items[i]);
        }
      }
    }
    more.addEventListener("click", more_less, false);
    more_less();
  }
}

function shorten_lists()
{
  $$("#info ul:not(.words)").for_each(function(x) {
      shorten_list(x.querySelectorAll("li"), DEFAULT_LIST);
    });
  var words = $$("#info ul.words li");
  if (words) shorten_list(words, DEFAULT_WORDS);
}

// Warn if canvas is not supported
function test_canvas()
{
  $("#canvas_warning").show(!document.createElement("canvas").getContext);
}

// function get_splits() { return $("#splits_box").value; }
// function get_command_rate() { return $("#interval_box").value; }
// function get_stroke_pause() { return $("#interval_strokes_box").value; }

var SPLITS = 3;
var COMMAND_MS = 5;
var PAUSE_MS = 100;

function get_splits() { return SPLITS; }
function get_command_rate() { return COMMAND_MS * (10 - $("#speed_box").value); }
function get_stroke_pause() { return PAUSE_MS * (10 - $("#speed_box").value); }


// Highlight the indexth strokegroup of the given kanji in the canvas.
function highlight_(kanji, canvas, index)
{
  var element = $$("#data strokegr")[index];
  var strokes = 0;
  if (element.hasAttribute("part")) {
    var e = element.getAttribute("element");
    $$(".part_{0}".fmt(e)).for_each(function(x) { x.add_class("hl"); });
    $$("#data strokegr").for_each(function(x) {
        if (x != element &&
          x.hasAttribute("part") && x.getAttribute("element") == e) {
          strokes += kanji.highlight(canvas, x, kanji.HIGHLIGHT_OTHER_COLOR);
        }
      });
  }
  strokes += kanji.highlight(canvas, element, kanji.HIGHLIGHT_COLOR);
  $("#strokes").innerHTML = strokes > 1 ?
    L10N.STROKES.fmt(strokes) : L10N.STROKE.fmt(strokes);
}

// Unhighlight any highlighted element.
function unhighlight_(canvas)
{
  $$(".hl").for_each(function(x) { x.remove_class("hl"); });
  canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height);
  $("#strokes").innerHTML = "";
}


// Create a new kanji from an XML element containing kanji elements and paths.
function Kanji(xml)
{
  this.paths = xml.querySelectorAll("stroke")
    .map(function(x) { return x.getAttribute("path"); });  // all strokes
  this.stroke = -1;              // current stroke
  this.state = "";               // animation state
  this.stepping = false;         // stepping or playing through
  this.timeout = null;           // timeout for the next stroke
  this.timeout_function = null;  // timeout for the next command in a path
}

Kanji.prototype =
{
  ANIMATING_COLOR: "#f80",
  BACKGROUND_COLOR: "#ccc",
  HIGHLIGHT_COLOR: "#4a8",
  HIGHLIGHT_OTHER_COLOR: "#084",
  PRINT_COLOR: "#000",

  // Animation commands

  _animate_strokes: function(context)
  {
    ++this.stroke;
    if (this.stroke >= this.paths.length) {
      this.stop(context.canvas);
    } else {
      send_event(this, "@stroke",
          { index : this.stroke, total: this.paths.length });
      var path = this.paths[this.stroke];
      var commands = parse_path_data(path, get_splits());
      context.beginPath();
      var i = 0;
      var pixels = context.getImageData(0, 0, context.canvas.width,
          context.canvas.height);
      this.timeout_function = bind(function() {
          if (i < commands.length) {
            context.putImageData(pixels, 0, 0);
            for (var j = 0; j <= i; ++j) draw_command(context, commands[j]);
            context.stroke();
            ++i;
            this.timeout = setTimeout(this.timeout_function,
              get_command_rate());
          } else if (!this.stepping) {
            this.timeout_function = bind(this._animate_strokes, this, context);
            this.timeout = setTimeout(this.timeout_function,
              get_stroke_pause());
          } else {
            this._freeze(context);
          }
        }, this);
      this.timeout = setTimeout(this.timeout_function, get_command_rate());
    }
  },

  _freeze: function(context)
  {
    context.restore();
    if (this.timeout != null) {
      clearTimeout(this.timeout);
      this.timeout = null;
    }
    this.timeout_function = null;
    this.state = "frozen";
    send_event(this, "@frozen");
  },

  pause: function()
  {
    if (this.timeout) {
      clearTimeout(this.timeout);
      this.timeout = null;
    }
    this.state = "paused";
    send_event(this, "@paused");
  },

  // Play the whole character stroke by stroke from the current stroke
  play: function(canvas)
  {
    if (this.state == "stopped" || this.state == "frozen") {
      this.state = "playing";
      send_event(this, "@playing");
      var context = this._set_context(canvas.getContext("2d"),
          this.ANIMATING_COLOR);
      this.stepping = false;
      this._animate_strokes(context);
    } else if (this.state == "paused") {
      this.state = "playing";
      send_event(this, "@playing");
      if (this.timeout_function) {
        this.stepping = false;
        this.timeout_function();
      }
    }
  },

  play_pause: function(canvas)
  {
    this[this.state == "playing" ? "pause" : "play"](canvas);
  },

  // Step to the next stroke
  next_stroke: function(canvas)
  {
    this._freeze(canvas.getContext("2d"));
    this.playing = true;
    send_event(this, "@playing");
    this.draw(canvas, this.ANIMATING_COLOR, this.stroke + 1);
    var context = this._set_context(canvas.getContext("2d"),
        this.ANIMATING_COLOR);
    this.stepping = true;
    this._animate_strokes(context);
  },

  prev_stroke: function(canvas)
  {
    if (this.stroke > 0) {
      this._freeze(canvas.getContext("2d"));
      this.stroke = Math.max(-1, this.stroke - 1);
      this.draw(canvas, this.ANIMATING_COLOR, this.stroke + 1);
      send_event(this, "@stroke",
          { index : this.stroke, total: this.paths.length });
    } else {
      this.stop(canvas);
    }
  },

  // Stop playback and clear the animation canvas.
  stop: function(canvas)
  {
    var context = canvas.getContext("2d");
    context.clearRect(0, 0, canvas.width, canvas.height);
    context.restore();
    this.stroke = -1;
    if (this.timeout != null) {
      clearTimeout(this.timeout);
      this.timeout = null;
    }
    this.timeout_function = null;
    this.state = "stopped";
    send_event(this, "@stopped");
  },

  // Set the context for drawing a kanji
  _set_context: function(context, color)
  {
    context.save();
    context.scale(context.canvas.width / DEFAULT_SIZE,
        context.canvas.height / DEFAULT_SIZE);
    context.lineWidth = 3;
    context.lineCap = "round";
    context.lineJoin = "round";
    context.strokeStyle = color;
    return context;
  },


  // Draw the complete kanji instantly (or up to the given number of strokes)
  // in a given color.
  draw: function(canvas, color, strokes)
  {
    if (strokes === undefined) strokes = this.paths.length;
    var context = this._set_context(canvas.getContext("2d"), color);
    context.clearRect(0, 0, canvas.width, canvas.height);
    context.beginPath();
    for (var i = 0; i < strokes; ++i) draw_path(context, this.paths[i]);
    context.stroke();
    context.restore();
  },

  // Highlight the paths of the given strokegr
  // Return the number of strokes in the strokegr
  highlight: function(canvas, strokegr, color)
  {
    var context = this._set_context(canvas.getContext("2d"), color);
    context.beginPath();
    var paths = strokegr.querySelectorAll("stroke")
      .map(function(x) { return x.getAttribute("path"); });
    paths.forEach(bind(draw_path, document, context));
    context.stroke();
    context.restore();
    return paths.length;
  },

}

// SVG stuff

// Draw all commands for a path
function draw_path(context, p)
{
  if (p) parse_path_data(p, 0).forEach(bind(draw_command, document, context));
}

// Draw one command in the given context
function draw_command(context, command)
{
  var cmd = command.shift();
  if (cmd == "C") {
    context.bezierCurveTo(command[0], command[1], command[2], command[3],
      command[4], command[5]);
  } else if (cmd == "M") {
    context.moveTo(command[0], command[1]);
  } else if (cmd == "Z") {
    context.closePath();
  }
}

// Split a C command n times by splitting repeatedly in the middle (this
// produces 2**n commands in the end.)
function split_C(c, x, y, n)
{
  if (n > 0) {
    var x0 = x;
    var y0 = y;
    var x1 = c[1];
    var y1 = c[2];
    var x2 = c[3];
    var y2 = c[4];
    x = c[5];
    y = c[6];
    var x3 = (x1 + x0) / 2;
    var y3 = (y1 + y0) / 2;
    var x4 = (x2 + x1) / 2;
    var y4 = (y2 + y1) / 2;
    var x5 = (x + x2) / 2;
    var y5 = (y + y2) / 2;
    var x6 = (x4 + x3) / 2;
    var y6 = (y4 + y3) / 2;
    var x7 = (x5 + x4) / 2;
    var y7 = (y5 + y4) / 2;
    var x8 = (x7 + x6) / 2;
    var y8 = (y7 + y6) / 2;
    var r1 = split_C(["C", x3, y3, x6, y6, x8, y8], x0, y0, n - 1);
    var r2 = split_C(["C", x7, y7, x5, y5, x, y], x8, y8, n - 1);
    return split_C(["C", x3, y3, x6, y6, x8, y8], x0, y0, n - 1)
      .concat(split_C(["C", x7, y7, x5, y5, x, y], x8, y8, n - 1));
  } else {
    return [c];
  }
}

// Parse the d attribute of an SVG path element and returns a list of commands.
// Always return absolute commands.
// Cf. http://www.w3.org/TR/SVGMobile12/paths.html
function parse_path_data(d, splits)
{
  var tokens = tokenize_path_data(d);
  var commands = [];
  var token;
  var x = 0;
  var y = 0;
  while (token = tokens.shift()) {
    if (token == "z" || token == "Z") {
      // Close path; no parameter
      commands.push(["Z"]);
    } else if (token == "M" || token == "L") {
      x = tokens.next_p();
      y = tokens.next_p();
      commands.push([token, x, y]);
    } else if (token == "m" || token == "l") {
      x += tokens.next_p();
      y += tokens.next_p();
      commands.push([token == "m" ? "M" : "L", x, y]);
    } else if (token == "C") {
      // Cubic curveto (6 params)
      var x1 = tokens.next_p();
      var y1 = tokens.next_p();
      var x2 = tokens.next_p();
      var y2 = tokens.next_p();
      var x3 = tokens.next_p();
      var y3 = tokens.next_p();
      commands = commands
        .concat(split_C(["C", x1, y1, x2, y2, x3, y3], x, y, splits));
      x = x3;
      y = y3;
    } else if (token == "c") {
      var x1 = tokens.next_p() + x;
      var y1 = tokens.next_p() + y;
      var x2 = tokens.next_p() + x;
      var y2 = tokens.next_p() + y;
      var x3 = tokens.next_p() + x;
      var y3 = tokens.next_p() + y;
      commands = commands
        .concat(split_C(["C", x1, y1, x2, y2, x3, y3], x, y, splits));
      x = x3;
      y = y3;
    } else if (token == "S") {
      // Smooth curveto where the two middle control points are the same
      // we expand it to a regular curveto and split it just the same
      var x1 = tokens.next_p();
      var y1 = tokens.next_p();
      var x2 = tokens.next_p();
      var y2 = tokens.next_p();
      commands = commands
        .concat(split_C(["C", x1, y1, x1, y1, x2, y2], x, y, splits));
      x = x2;
      y = y2;
    } else if (token == "s") {
      var x1 = tokens.next_p() + x;
      var y1 = tokens.next_p() + y;
      var x2 = tokens.next_p() + x;
      var y2 = tokens.next_p() + y;
      commands = commands
        .concat(split_C(["C", x1, y1, x1, y1, x2, y2], x, y, splits));
      x = x2;
      y = y2;
    } else {
      // Additional parameters, depending on the previous command
      var prev = commands[commands.length - 1];
      if (prev === undefined || prev == "Z") {
        tokens.unshift("M");
      } else if (prev == "M") {
        tokens.unshift("L");
      } else {
        tokens.unshift(prev);
      }
    }
  }
  return commands;
}

// Return the tokens (commands and parameters) for path data, everything else
// is ignored (no error reporting is done.)
// The token list has a next_p method to get the next parameter, or 0 if there
// is none.
function tokenize_path_data(d)
{
  var tokenizer = /([chlmqstvz]|(?:\-?\d+\.?\d*)|(?:\-?\.\d+))/i;
  var tokens = [];
  tokens.next_p = function() { return parseFloat(this.shift()) || 0; };
  var match;
  while (match = tokenizer.exec(d)) {
    tokens.push(match[1]);
    d = d.substr(match.index + match[0].length);
  }
  return tokens;
}
