/* Copyright © 2009, 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. */

// History:
//   0.1: functional; storage with SQL
//   0.2: moved to localStorage; querySelector
//   0.2.1: small fix for Chrome (which does not support localStorage)
//   0.3: split words; everything is a magnet; about page (stub)
//   0.3.1: fix to clear in FF; saving sticky magnets across pages
//   0.3.2: moved data_from_storage to utils (used elsewhere)
//   0.3.3: fixed transformation and z-index; added symbols set
//   0.4: drop magnet on clear to remove it (should make a trash magnet)
//   0.4.1: hover over clear magnet (image should change for better effect)
//   0.4.2: removed about page
//   0.5: rotation (pretty rough)
//   0.6: absolute coordinates, magnets initially anywhere, fixed rotation
//   0.6.1: removed title
//   0.6.2: fixed add magnets magnets jumping around
//   0.6.3: fixed blank magnet
//   0.6.4: help magnet
//   0.6.5: pictures for trash
//   0.7: shraring through codes
//   0.8: sound
//   0.9: random set from the DB
//   0.9.1: Opera support (from 10.50)
//   0.9.2: loading

// BUGS:
//   * after "clear", new magnets appear below the sticky magnets.
//   * magnets are sometimes too close to the edges and create scrollbars
//   * blank magnet doesn't work well in Opera.

// TODO:
//   * add picture magnets for main site, secret_lab, help, &c.
//   * more sets, other languages
//   * make a custom set
//   * open the fridge and reveal stuff inside!

var SET = "Shakespeare";   // default value
var TRASH = null;          // the trash magnet
var MAGNET = null;         // magnet currently selected
var HELP = null;           // help magnet after it's been removed
var N = 12;                // number of magnets to add each time
var AS_JSON = true;        // flag for get_from_storage
var Z = 1;                 // increase z-index for each moved magnet

var Θ_RANGE = 20.0;        // range for initial rotation angle
var ROTATE_DISTANCE = .7;  // distance of the center for rotation

var FUNCTIONS = {
  "blank": add_blank,
  "clear": clear_magnets,
  "help": show_help,
  "share": share,
  "random": function() {}
};


// Initialize magnets: restore or generate transformations, add event handlers
// If a code is given, attempt to get the magnets data from the server using
// this code.
function go()
{
  var shared = get_shared_magnets();
  TRASH = $("#trash");
  var set = shared.set || get_from_storage("magnets_set");
  if (set) SET = set;
  // Sticky magnets
  var actions = $("#actions");
  for (var w in WORDS) actions.appendChild(make_add_magnet(w));
  var storage = shared.sticky || get_from_storage("magnets_sticky") || {};
  var elems = document.querySelectorAll(".magnet.sticky");
  for (var i = 0, n = elems.length; i < n; ++i) {
    var magnet = elems[i];
    var data = storage[magnet.id] ? storage[magnet.id] :
      random_transform(magnet);
    make_magnet(magnet, data);
  }
  // Hide the help magnet
  HELP = document.querySelector(".magnet.help");
  HELP.parentNode.removeChild(HELP);
  // Word magnets
  var magnets = shared.magnets || get_from_storage("magnets");
  if (magnets && magnets.length > 0) {
    magnets.forEach(add_magnet);
  } else {
    add_magnets(N, SET);
  }
  document.addEventListener("mousemove", drag_magnet, false);
  document.addEventListener("mouseup", drop_magnet, false);
  $("#fridge").style.visibility = "visible";
  $("#loading").show(false);
}


// Add a blank magnet and make it editable; pressing RETURN will stop the
// editing.
function add_blank()
{
  // Set a z index so that it appears on top at all times
  var magnet = add_magnet({s: SET, w: "", z: ++Z});
  magnet.contentEditable = true;
  magnet.addEventListener("keydown", function(e) {
      // if the magnet changes size, we need to change its position so that the
      // center does not move.
      var rect = magnet.getBoundingClientRect();
      magnet._x = magnet.style.left =
        rect.left + Math.round((rect.right - rect.left) / 2);
      magnet._y = magnet.style.top =
        rect.top + Math.round((rect.bottom - rect.top) / 2);
      magnet.style.removeProperty("-moz-transform");
      magnet.style.removeProperty("-o-transform");
      magnet.style.removeProperty("-webkit-transform");
      rect = magnet.getBoundingClientRect();
      magnet._w = Math.round((rect.right - rect.left) / 2);
      set_transform(magnet);
      if (e.keyCode == 13) {
        magnet.contentEditable = false;
        magnet.blur();
        save_magnets();
      }
    }, false);
  // don't save the magnets yet -- this one will be set when it gets a word
  magnet.focus();
}

// Add a bunch of magnets randomly selected from the givent set.
function add_magnets(how_many, set)
{
  if (set != SET && set in WORDS) {
    SET = set;
    if (window.localStorage) localStorage.setItem("magnets_set", SET);
  }
  var w = WORDS[SET].length - 1;
  for (var i = 0; i < how_many; ++i) {
    add_magnet({s: SET, w: WORDS[SET][Math.round(Math.random() * w)]});
  }
  save_magnets();
}

// Add a new magnet from the given data
function add_magnet(data)
{
  var magnet = create("div");
  magnet.add_class("magnet");
  magnet.add_class(data.s);
  magnet._set = data.s;
  magnet.add_text(data.w);
  document.querySelector("#magnets").appendChild(magnet);
  return make_magnet(magnet, data);
}

// Clear all magnets from the fridge (and from local storage if possible)
function clear_magnets()
{
  Z = 1;
  var magnets = document.querySelectorAll("#magnets .magnet");
  for (var i = 0, n = magnets.length; i < n; ++i) {
    magnets[i].parentNode.removeChild(magnets[i]);
  }
  save_magnets();
}

// When a magnet is being dragged (either rotated or translated)
function drag_magnet(e)
{
  if (MAGNET) {
    if (!MAGNET.__m) {
      MAGNET.style.zIndex = ++Z;
      MAGNET.__m = true;
    }
    if (MAGNET.__θ !== undefined) {
      // rotating
      MAGNET._θ = MAGNET.__θ +
        Math.atan2(e.clientY - MAGNET._y, e.clientX - MAGNET._x) * 180.0 / π;
      set_transform(MAGNET);
    } else {
      // translating
      MAGNET.style.left = (MAGNET.__x + e.clientX) + "px";
      MAGNET.style.top = (MAGNET.__y + e.clientY) + "px";
      MAGNET.style.visibility = "hidden";
      if (document.elementFromPoint(e.clientX, e.clientY) == TRASH) {
        open_trash(TRASH);
      } else {
        close_trash(TRASH);
      }
      MAGNET.style.removeProperty("visibility");
    }
  }
}

function open_trash(trash)
{
  if (!trash.has_class("over")) {
    trash.add_class("over");
    $("#audio_open").play();
  }
}

function close_trash(trash)
{
  if (trash.has_class("over")) {
    trash.remove_class("over");
    $("#audio_close").play();
  }
}

function empty_trash(trash)
{
  trash.remove_class("over");
  $("#audio_trash").play();
}

// When a magnet is dropped (i.e. on mouse down)
function drop_magnet(e)
{
  if (MAGNET) {
    if (!MAGNET.__m && FUNCTIONS[MAGNET.id]) FUNCTIONS[MAGNET.id]();
    if (MAGNET.__θ === undefined) {
      MAGNET._x = e.clientX + MAGNET.__x + MAGNET._w;
      MAGNET._y = e.clientY + MAGNET.__y + MAGNET._h;
    }
    MAGNET.remove_class("moving");
    MAGNET.style.visibility = "hidden";
    if (document.elementFromPoint(e.clientX, e.clientY) == TRASH) {
      MAGNET.parentNode.removeChild(MAGNET);
      empty_trash(TRASH);
    } else {
      MAGNET.style.removeProperty("visibility");
    }
    MAGNET = null;
    save_magnets();
  }
}

// Make a JSON string for storage/export
function json_string_magnets()
{
  var magnets = document.querySelectorAll("#magnets .magnet");
  var strings = [];
  for (var i = 0, n = magnets.length; i < n; ++i) {
    var m = magnets[i];
    strings.push("{θ:{0},s:\"{1}\",w:\"{2}\",x:{3},y:{4}{5}}"
        .fmt(Math.round(100.0 * m._θ) / 100.0, m._set,
          quote_json(m.textContent), m._x, m._y,
          m.style.zIndex ? ",z:{0}".fmt(m.style.zIndex) : ""));
  }
  return "[{0}]".fmt(strings.join(", "));
}

// Make a JSON string for sticky magnets (indexed by id instead of being in a
// list)
function json_string_sticky()
{
  var magnets = document.querySelectorAll(".magnet.sticky");
  var strings = [];
  for (var i = 0, n = magnets.length; i < n; ++i) {
    var m = magnets[i];
    if (m.id) {
      strings.push("{0}:{θ:{1},x:{2},y:{3}{4}}"
          .fmt(m.id, Math.round(100.0 * m._θ) / 100.0, m._x, m._y,
            m.style.zIndex ? ",z:{0}".fmt(m.style.zIndex) : ""));
    }
  }
  return "{{0}}".fmt(strings.join(", "));
}

function get_shared_magnets()
{
  var shared = {};
  try
  {
    shared.magnets = eval("({0})".fmt($("#shared .magnets").textContent));
    shared.set = $("#shared .set").textContent,
    shared.sticky = eval("({0})".fmt($("#shared .sticky").textContent));
  }
  catch (e) {}
  return shared;
}

// Make a magnet to add new words from a set
function make_add_magnet(set)
{
  var div = create("div");
  div.className = "magnet sticky";
  div.id = "add_" + set;
  FUNCTIONS[div.id] = function() { add_magnets(N, set); };
  div.add_text(set);
  return div;
}

// Complete the magnet from an existing element (new magnet or sticky magnet)
// with additional data
function make_magnet(magnet, data)
{
  var t = random_transform(magnet);
  magnet._w = t.w;
  magnet._h = t.h;
  // Position (anywhere within the fridge area)
  var x;
  if (data.x === undefined) {
    x = t.x;
    magnet._x = x + magnet._w;
  } else {
    magnet._x = data.x;
    x = magnet._x - magnet._w;
  }
  var y;
  if (data.y === undefined) {
    y = t.y;
    magnet._y = y + magnet._h;
  } else {
    magnet._y = data.y;
    y = magnet._y - magnet._h;
  }
  magnet.style.position = "absolute";
  magnet.style.left = x + "px";
  magnet.style.top = y + "px";
  // Z index
  if (data.z !== undefined) {
    magnet.style.zIndex = data.z;
    if (data.z > Z) Z = data.z;
  }
  // Rotation
  magnet._θ = data.θ === undefined ? t.θ : data.θ;
  set_transform(magnet);
  var q = [magnet];
  while (q.length > 0) {
    var e = q.shift();
    e.addEventListener("mousedown", function(ev) { select_magnet(ev, magnet); },
      false);
    for (var i = 0, n = e.childNodes.length; i < n; ++i) {
      var e_ = e.childNodes[i];
      if (e_.nodeType == Node.ELEMENT_NODE) q.push(e_);
    }
  }
  return magnet;
}

// Initial transform for sticky magnets (position + rotation)
function random_transform(magnet)
{
  var rect = magnet.getBoundingClientRect();
  var transform = {
    w: Math.round((rect.right - rect.left) / 2),  // half width
    h: Math.round((rect.bottom - rect.top) / 2)   // and height
  };
  transform.x = 2 * transform.w +
    Math.round(Math.random() * (window.innerWidth - 4 * transform.w));
  transform.y = 2 * transform.h +
    Math.round(Math.random() * (window.innerHeight - 4 * transform.h));
  transform.θ = Θ_RANGE - Math.random() * Θ_RANGE * 2.0;
  return transform;
}

// Save magnets to local storage if available; otherwise do nothing.
function save_magnets()
{
  if (window.localStorage) {
    localStorage.setItem("magnets", json_string_magnets());
    localStorage.setItem("magnets_sticky", json_string_sticky());
  }
}

// When a magnet is selected. If we select at the margins of the magnet, we're
// going to rotate it, otherwise we're going to translate it. We set the __θ
// flag if rotating, otherwise it is undefined. __m is also set on the magnet,
// to false initially (so that we can tell if it's a single click.)
function select_magnet(e, magnet)
{
  // Distance of the click from the center of the magnet
  var d = Math.sqrt(Math.pow(e.clientX - magnet._x, 2) +
      Math.pow(e.clientY - magnet._y, 2));
  if (d > (Math.max(magnet._w, magnet._h) * ROTATE_DISTANCE)) {
    // Rotating, __θ is the initial rotation angle computed from the starting
    // angle and the click position
    magnet.__θ = magnet._θ - Math.atan2(e.clientY - magnet._y,
        e.clientX - magnet._x) * 180.0 / π;
  } else {
    // Translation
    magnet.__θ = undefined;
    magnet.__x = magnet._x - e.clientX - magnet._w;
    magnet.__y = magnet._y - e.clientY - magnet._h;
  }
  magnet.__m = false;
  magnet.add_class("moving");
  MAGNET = magnet;
  e.preventDefault();
}

// Set the transform attributs for an element, using position for translation
// and angle for rotation. Set -moz-transform, -o-transform and
// -webkit-transform.
function set_transform(magnet)
{
  magnet.style.MozTransform =
  magnet.style.OTransform =
  magnet.style.WebkitTransform = "rotate({0}deg)".fmt(magnet._θ);
}

// Save the magnets data to the server (will return a sharing code to the user)
function share()
{
  var form = $("form");
  form.elements["magnets"].value = json_string_magnets();
  form.elements["magnets_set"].value = SET;
  form.elements["magnets_sticky"].value = json_string_sticky();
  form.submit();
}

// Show the help magnet
function show_help()
{
  if (!HELP.parentNode) {
    document.body.appendChild(HELP);
    make_magnet(HELP, random_transform(HELP));
    HELP.style.removeProperty("visibility");
    HELP.style.zIndex = ++Z;
  }
}
