/**
 * Common module!
 *
 * @module common
 */

// debug - v0.4 - 6/22/2010
// http://benalman.com/projects/javascript-debug-console-log/
window.debug=(function(){var i=this,b=Array.prototype.slice,d=i.console,h={},f,g,m=9,c=["error","warn","info","debug","log"],l="assert clear count dir dirxml exception group groupCollapsed groupEnd profile profileEnd table time timeEnd trace".split(" "),j=l.length,a=[];while(--j>=0){(function(n){h[n]=function(){m!==0&&d&&d[n]&&d[n].apply(d,arguments)}})(l[j])}j=c.length;while(--j>=0){(function(n,o){h[o]=function(){var q=b.call(arguments),p=[o].concat(q);a.push(p);e(p);if(!d||!k(n)){return}d.firebug?d[o].apply(i,q):d[o]?d[o](q):d.log(q)}})(j,c[j])}function e(n){if(f&&(g||!d||!d.log)){f.apply(i,n)}}h.setLevel=function(n){m=typeof n==="number"?n:9};function k(n){return m>0?m>n:c.length+m<=n}h.setCallback=function(){var o=b.call(arguments),n=a.length,p=n;f=o.shift()||null;g=typeof o[0]==="boolean"?o.shift():false;p-=typeof o[0]==="number"?o.shift():n;while(p<n){e(a[p++])}};return h})();

var report = debug.log;  // mirrors Flash
var debug_alias = debug;
// Firebug now has debug func on localhost which clobbers ours. Clobber it back!
setTimeout("debug = debug_alias;", 1000);

// cookie plugin - http://plugins.jquery.com/files/jquery.cookie.js.txt
jQuery.cookie=function(d,c,a){if(typeof c!="undefined"){a=a||{};if(c===null){c="";a.expires=-1}var b="";if(a.expires&&(typeof a.expires=="number"||a.expires.toUTCString)){if(typeof a.expires=="number"){b=new Date;b.setTime(b.getTime()+a.expires*24*60*60*1E3)}else b=a.expires;b="; expires="+b.toUTCString()}var e=a.path?"; path="+a.path:"",f=a.domain?"; domain="+a.domain:"";a=a.secure?"; secure":"";document.cookie=[d,"=",encodeURIComponent(c),b,e,f,a].join("")}else{c=null;if(document.cookie&&document.cookie!=
""){a=document.cookie.split(";");for(b=0;b<a.length;b++){e=jQuery.trim(a[b]);if(e.substring(0,d.length+1)==d+"="){c=decodeURIComponent(e.substring(d.length+1));break}}}return c}};

$(document).ready(function() {
  $('#admin_console_border').mousedown(beginDrag);
  $(document).keydown(hideConsole);
  if(!developing && !$('#original_recipe').hasClass('no_recipe'))
    setTimeout(startRecipes, 1000);
});

var recipeCount = 0;
var recipeTimer;
function startRecipes()
{
  originalRecipe();
  recipeTimer = setInterval(originalRecipe, 5*60*1000);
}

function originalRecipe()
{
  if(++recipeCount >= 6)
    clearInterval(recipeTimer);
  else if(!recipeTimer)
    recipeTimer = setInterval(originalRecipe, 5*60*1000);
  $.getJSON("/recipe", function(recipe) {
    if(!recipe) return;
    $('#original_recipe').html(recipe.recipe).attr("title", recipe.source);
  });
}

function makeIntoOneLine(elem)
{
  var height = elem.offsetHeight;
  var text = elem.innerHTML;
  elem.innerHTML = 'test';
  var normheight = elem.offsetHeight;
  elem.innerHTML = text;
  return shortenTextInElem(elem, normheight);
}

function shortenTextInElem(elem, targetheight) {
  var text = elem.innerHTML;
  var height = elem.offsetHeight;
  if (height > targetheight) {
    if (elem.title == '')
      {
    elem.title = text.replace(/(<([^>]+>))/ig, "");
    $(elem).data('full', text);
      }
    var words = text.split(' ');
    words.pop();
    text = words.shift();
    while(words.length != 0) {
      var word = words.shift();
      elem.innerHTML = text+' '+word+' ...';
      if (elem.offsetHeight > targetheight)
    break;
      text += ' '+word;
    }
    text += ' ...';
  }
  var same = elem.innerHTML = text;
  elem.innerHTML = text;
  return (height > targetheight && !same);
}

function makeGroupSingleLined(objects) {
  var has_multilines = true;
  var cycles = 0;
  while (has_multilines) {
    has_multilines = false;
    for (var i=0; i<objects.length; i++)
      if (makeIntoOneLine(objects[i]))
    has_multilines = true;
    cycles += 1;
    if (cycles > objects.length) break;
  }
}


function RGBtoHex(R,G,B) {return toHex(R)+toHex(G)+toHex(B);}
function toHex(N) {
 if (N==null) return "00";
 N=parseInt(N); if (N==0 || isNaN(N)) return "00";
 N=Math.max(0,N); N=Math.min(N,255); N=Math.round(N);
 return "0123456789ABCDEF".charAt((N-N%16)/16)
      + "0123456789ABCDEF".charAt(N%16);
}

function HexToR(h) {return parseInt((cutHex(h)).substring(0,2),16);}
function HexToG(h) {return parseInt((cutHex(h)).substring(2,4),16);}
function HexToB(h) {return parseInt((cutHex(h)).substring(4,6),16);}
function cutHex(h) {return (h.charAt(0)=="#") ? h.substring(1,7) : h;}

function switchToInnerTab(windowid, newtab, windowcontainer) {
  var sclass = "selectedinnertab";
  $('.innertabs div.'+sclass, windowcontainer).removeClass(sclass);
  $(newtab).addClass(sclass);

  $('div.window', windowcontainer).addClass("hidden");
  var towindow = $('#'+windowid).removeClass("hidden");
  $('.canvas_button', towindow).each(function() { makeCanvasButton($(this)); });
}

function makeUVLPParams(q) {
  var params = {};
  if(q.charAt(0) == "?") q = q.substr(1);
  jQuery.each(q.split('&'), function()
    {
      params[this.split('=')[0]] = this.split('=')[1];
    });
  return params;
}

function isAE500(response) {
  return response.responseText.length > 300 && response.responseText.indexOf("The server encountered an error and could not complete your request.") != -1 && response.responseText.indexOf("skritter") == -1;
}

function isnginx500(response) {
  return response.responseText.indexOf("500 Internal Server Error") != -1 && response.responseText.indexOf("nginx/") != -1;
}

$.retried_ajax = function(type, url, data, success, delay) {
  delay = delay || 1000;  // start retries at 1000ms, doubling each time
  if(delay >= 1200000)
    return;  // no response after twenty minutes? quit (show giveup error msg?)
  // if the delay gets too high here, we should show an info bubble notifying
  // the user that there are network issues
  if(!data) { // no extra params passed
    data = {};
    success = function() {};
  }
  else if(!success) { // missing success
    success = data;
    data = {};
  }

  $.ajax({type: type, url: url, data: data, success: function(response) {
    if(response == "timeout" || response == "retry" || response == "")
      setTimeout(function() { $.retried_ajax(type,url,data,success,delay*2); },
         delay);
    else if (response == "disabled")
      window.location.href = '/disabled';
    else
      success(response);
  }, error: function(response, textStatus, errorThrown) {
    if(!response.responseText || isAE500(response) || isnginx500(response))
      setTimeout(function() { $.retried_ajax(type,url,data,success,delay*2); },
                 delay);
    else
      success(response.responseText);  // original caller can handle the error
  }});
};

// If you use two args, the second is treated as the success function instead.
// You can also just pass the URL.
$.retried_get = function(url, data, success) {
  $.retried_ajax("get", url, data, success);
};

$.retried_post = function(url, data, success) {
  $.retried_ajax("post", url, data, success);
};


function inversePct(num, total) {
  return parseInt((100 * (total - num)) / total) + "%";
}

function removeScreen() { $("#screen").fadeOut(500); }
function replaceScreen() { $("#screen").fadeIn(500); }

function logServerError(e) {
  var params = {'message':e.message,
        'fileName':e.fileName,
        'lineNumber':e.lineNumber,
        'stack':e.stack,
        'name':e.name};
  $.retried_get("/j_error", params, function() {});
  debug.error("error!: ", params);
}

function logServerMsg(m) {
  var params = {'msg':m};
  $.retried_get("/j_log", params, function() {});
}


function clickWithin(e) {
  var jelem = $(e.target);
  if (jelem.hasClass("parent_clicker")) $('*',jelem).click();
}



// Admin console controls

var offsetX = 0;
var offsetY = 0;

function beginDrag(e) {
  var console = $('#admin_console');
  offsetX = e.pageX - parseInt(console.css('left'));
  offsetY = e.pageY - parseInt(console.css('top'));
  $('body').mousemove(drag).mouseup(endDrag);
  /*
  var pageCoords = "( " + e.pageX + ", " + e.pageY + " )";
  var clientCoords = "( " + e.clientX + ", " + e.clientY + " )";
  debug.log("( B e.pageX, e.pageY ) - " + pageCoords);
  debug.log("( B e.clientX, e.clientY ) - " + clientCoords);
   */
}

function drag(e) {
  /*
  var pageCoords = "( " + e.pageX + ", " + e.pageY + " )";
  var clientCoords = "( " + e.clientX + ", " + e.clientY + " )";
  debug.log("( D e.pageX, e.pageY ) - " + pageCoords);
  debug.log("( D e.clientX, e.clientY ) - " + clientCoords);
  */
  $('#admin_console')
    .css('left',e.pageX-offsetX+'px')
    .css('top',e.pageY-offsetY+'px');
}

function endDrag(e) {
  saveConsoleSettings();
  $('body').unbind("mousemove", drag).unbind("mouseup", endDrag);
}

function hideConsole(e) {
  if (e.keyCode==33 && e.shiftKey==1) {
    var console = $('#admin_console');
    console.toggleClass('hidden');
    saveConsoleSettings();
  }
}

function saveConsoleSettings() {
  var console = $('#admin_console');
  if (!console.is('*')) return;
  var params = { left:console.css('left'),
         top:console.css('top'),
             hidden:console.hasClass('hidden') };
  $.retried_get("/admin/saveconsoleparams",params,function() { ; });
}


var part_map = {
  W:'Writing', D:'Definition', T:'Tone',
  P:'Pinyin', R:'Reading'
};

function addPartTitles(elems) {
  elems.each(function()
  {
    var cell = $(this);
    var html = cell.html();
    if (html.indexOf('span')>-1) return true;
    cell.attr('title',part_map[$.trim(html)]);
    //for (var i=0; i<to_replace.length; i++)
      //html = html.replace(to_replace[i][0], to_replace[i][1]);
    //cell.html(html);
    return true;
  });
}


function log(s) {
  var t = $('#admin_console textarea');
  if (t.length==0) return;
  if (t.hasClass('hidden')) {
    t.val('');
    t.removeClass('hidden');
  }
  t.val(s+'\n'+t.val());
}


function findParent(elem, key_func) {
  elem = $(elem)[0];
  try { while (!key_func(elem)) elem = elem.parentNode; }
  catch (e) { return null; }
  return elem;
}

function textToHTML(s, author) {
  //var old_s = s;
  s = $('<div>').html(s).text();  // strip the HTML
  s = s.replace(/img:(http:\/\/\S+)/gi, '<img src="$1"/>')  // img:http://...
    .replace(/_([^ _][^_]*)_(?!\S{4})/gi, '<em>$1</em>')    // _italicizes_ it
    .replace(/\n/gi, '<br/>')                               // newlines
    .replace(/\*([^*]+)\*/gi, '<b>$1</b>');                  // *bolds* it
  // don't italicize underscores in links: hack heuristic, require 3 or less
  // non-whitespace chars after trailing _, to allow _hack_ing or _silly_. but
  // not /silly_hacker_party.png
  //debug.info(old_s, "\n->\n", s);
  if(author)
    s = s + '<span class="other_mnem_byline"> ('+author+')</span>';
  return s;
}

function HTMLToText(s) {
  s = s.replace(/<img src="(http:\/\/\S+)"\/?>/gi, 'img:$1')
    .replace(/<br\/?>/gi, '\n')
    .replace(/<b>(.*?)<\/b>/gi, '*$1*')
    .replace(/<em>(.*?)<\/em>/gi, '_$1_')
    .replace(/<span class="other_mnem_byline">.*?<\/span>/gi, '');
  return s;
}


// http://www.vanyli.net/?p=3
// could extend this to retry timeouts with a failure func, too
$.delayed_ajax = function(options) {
  options.tstamp = (new Date().getTime() + new Date().getMilliseconds());
  options.success_original = options.success;
  options.success = function(data, status) {
    var last = $('body').data('ajax_pool' + options.request_type);
    if (last.tstamp == options.tstamp && options.success_original)
       options.success_original(data, status);
  };

  $('body').data('ajax_pool' + options.request_type, options);

  if ($('body').data('timeout_pool' + options.request_type))
    clearTimeout($('body').data('timeout_pool' + options.request_type));

  $('body').data('timeout_pool' + options.request_type,
         setTimeout("do_request('" + options.request_type + "')",
                 options.delay));

  do_request = function(request_type) {
    $.ajax($('body').data('ajax_pool' + request_type));
  };
};

/*
 * POP UPS (to replace dialogs)
 *
 * To create a popup, pass in the url and params for the popup's body,
 * and the parent popup if there is one.
 *
 * Things like locking and callbacks are done with the javascript object.
 *
 */

function Popup(url, params) {

  // PROPERTIES

  this.callback = null;
  this.ret_val = null;
  this.parent = null; // Javascript Object
  this.child = null; // Javascript Object
  this.jqobj = null; // jQuery Object
  this.locked = false;
  this.url = null;
  this.params = null;

  // FUNCTIONS

  this.setCallback = function(callback) {
    this.callback = callback;
    return this;
  };

  this.clearBody = function() {
    $('.popup_body, .popup_loading', this.jqobj).remove();
    return this;
  };

  this.mouseClick = function(js_obj) {
    return function(e) {
      // this function gets called whenever the mouse is clicked when a popup
      // is open. Only one gets bound, when it's the root popup.

      // ignore clicks on popup buttons
      if ($(e.target).closest('.popup_button').length>0) return;

      // find the popup that needs closing.
      var popup = $(e.target).closest('.popup');

      // If no popup was clicked, pick the bottom most
      if (popup.length==0) {

	// handle for clicks that happen outside the general dom
	if ($(e.target).closest('body').length==0) return;

	popup = $('.root_popup');

	// unbind this function if there's no need for it anymore
	if (popup.length==0) {
	  $('body').unbind('click', js_obj.mouseClick);
	  return;
	}
      }

      // if it was in a popup, close it if it clicked the x, otherwise its
      // first child
      else {
	if ($(e.target).closest('.popup_close').length==0) {
	  var j_obj = $(popup).data('j_obj');
	  if (j_obj==null) return;
	  var child = j_obj.child;
	  if (child!=null) popup = child.jqobj;
	  else return; // nothing to close
	}
      }

      // don't close locked popups though, or their parents
      if (popup.hasClass('locked') ||
	  $('.popup.locked', popup).length>0) {
	popup = $('.popup:not(.locked):not(:has(.popup.locked))',popup);
      }

      if (popup.length>0)
	popup.data('j_obj').close();

    };
  }(this);

  this.openOnPage = function() {
    $('#popup_screen').height($(document).height());
    //debug.info('put this as body height: ',$(document).height());
    $(window).resize(function () {
      $('#popup_screen').height($(document).height());
    }); // ensures that the screen covers eeeeeeverything!
    // inserts this popup in the page, so not nested in any other popups
    this.jqobj.addClass('root_popup');
    $('#popup_screen').show();
    $('#popup_wrapper').show().append(this.jqobj);
    $('body').click(this.mouseClick);
    return this;
  };

  this.openInPopup = function(parent_popup) {
    // puts this popup, nested in another popup
    this.jqobj.addClass('child_popup');
    if (!this.jqobj.hasClass('inner_node_popup') &&
    !this.jqobj.hasClass('leaf_node_popup'))
      this.jqobj.addClass('leaf_node_popup');
    $(parent_popup).append(this.jqobj);
    this.parent = $(parent_popup).data('j_obj');
    this.parent.child = this;
    return this;
  };

  this.showNav = function(parent_popup) {
    this.jqobj.find('.popup_nav').removeClass('hidden');
    return this;
  };

  this.open = function(parent_popup) {
    if(typeof skritter != "undefined") {
      skritter.giveMacMouse(true);
      if(android)
        $(skritter).css("visibility", "hidden");
    }
    if (parent_popup==null || parent_popup.length==0) this.openOnPage();
    else this.openInPopup(parent_popup);

    // on reopening, this is lost, so make sure it's put back
    this.jqobj.data('j_obj',this);

    return this;
  };

  this.close = function() {
    //debug.info('close', this);
    // closes this popup, calling any callback with any return value
    // that has been set prior to the closing to the callback and ret_val
    // values
    if (this.callback!=null) {
      try { this.callback(this.ret_val); }
      catch (e) { ; }
    }

    if (this.jqobj.hasClass('root_popup')) {
      $(window).unbind('resize');
	  $('#popup_screen').hide();
      $('#popup_wrapper').hide().html('');
      $('body').unbind('click', this.mouseClick);
      if(typeof skritter != "undefined") {
        skritter.giveMacMouse(false);
        if(android)
          $(skritter).css("visibility", "visible");
      }
    }

    this.jqobj.remove();
    if (this.parent) this.parent.child = null;
    return this;
  };

  this.lock = function() {
    // Makes it so that you can't close this popup with clicks outside
    // the box or with the remove button
    $('.popup_close', this.jqobj).hide();
    this.locked = true;
    this.jqobj.addClass('locked');
    return this;
  };

  this.unlock = function() {
    // Makes it so that you can't close this popup with clicks outside
    // the box or with the remove button
    $('.popup_close', this.jqobj).show();
    this.locked = false;
    this.jqobj.removeClass('locked');
    return this;
  };

  this.positionFromTop = function(pageY) {
    var top = Math.max(pageY-474, 0);
    this.jqobj.css('top',top+'px');
    return this;
  };

  this.loadUrl = function(url, params) {
    if (!params) params = {};
    this.url = url;
    this.params = params;

    $.retried_get(url, params, function(popup, url, params) {
      return function(response)
    {
      if (popup.url!=url || popup.params!=params) return;
      $('.popup_body', popup.jqobj).remove();
      $('.popup_loading', popup.jqobj).remove();
      $('.popup_bar', popup.jqobj)
        .after($(response).addClass('popup_body'));
      //if (popup.locked) $('input[value="Cancel"]', popup.jqobj).hide();
    };
    } (this, url, params)
    );
  };

  this.setAsInnerNode = function() {
    //makeFullWidth
    this.jqobj.removeClass('leaf_popup');
    this.jqobj.addClass('inner_node_popup');
    return this;
  };

  // INITIALIZATION

  this.jqobj = $('#blank_popup .popup').clone();
  this.jqobj.data('j_obj',this);

  if (url)
    this.loadUrl(url, params);
}

function findParentPopup(elem) {
  var key = function(elem) { return $(elem).hasClass('popup'); };
  return $(findParent(elem, key)).data('j_obj');
}

function closeParentPopup(elem) {
  findParentPopup(elem).close();
}

// Info Popups

function infoPopup(object, title, body) {
  var popup = new Popup().clearBody();
  popup.jqobj
    .append($('<h2></h2>').css('text-align','center').html(title))
    .append($('<div></div>').html(body).css('margin-bottom','20px'));
  popup.positionFromTop($(object).offset().top+250).open();
}

// User Profile Popup

function userPopup(object, user) {
  var popup = new Popup('/userprofile?name='+user);
  popup.positionFromTop($(object).offset().top+150).open();
}

/* CANVAS BUTTON! */

/*
 * The canvas button takes a div with text in it and turns it effectively
 * into an input button with a background rendered with canvas. The button
 * has five states: neutral, hover, active, focus, and hover+focus, rendered
 * in that order.
 */

function makeCanvasButton(div) {
  // hidden ones break this algorithm. Skip them for now
  if (!div.is(':visible')) return;

  var SPRITE_HEIGHT = 6;

  // button starts out as a div with text in it. Move the text into a button
  if (div.hasClass('rendered')) return;
  var type = (div.hasClass('submit')) ? "submit" : "button";
  var input = $('<input></input>')
    .attr('type',type)
    .val(div.text());
  div.text('').append(input);

  // figure out the width and height of the div and lock it in place
  div.attr('height',div.css('height')).attr('width',div.css('width'));
  var height = div.outerHeight();
  var width  = div.outerWidth();

  // radius and line_width based on the size of the button
  var radius = 5;
  var line_width = 1;

  // make a canvas element and add it to the div, then get the context
  var canvas = $('<canvas width='+width+' height='+
		 (height*SPRITE_HEIGHT)+'></canvas>');
  canvas.addClass('button');
  var ctx = canvas[0].getContext ? canvas[0].getContext('2d') : null;

  // check if it's a browser that doesn't have a context, handle if it doesn't
  //ctx = null; // for testing normal css buttons
  if (!ctx) {
    if (div.hasClass('hide')) div.hide();
    div.text(input.val());
    div.addClass('alt_button');
    return;
  }


  div.append(canvas);

  // a little geometry, figure out where the linear gradient should go relative
  // to (0,0) so that the end of the gradient hits both the lower left and
  // upper right corners of the button.
  var x = (Math.pow(height,2)) / (width + (height/width));
  var y = (width*x) / height;

  // figure out the colors based on the class. Colors are start and stop
  // gradient, and focus color
  var colors = ['#FD4239','#B60000','#39C3FD']; // red
  //if (div.hasClass('blue')) colors = ['#3B39FD','#2A2664','#39C3FD'];
  if (div.hasClass('blue')) colors = ['#4195D3','#092E60','#39C3FD'];
  if (div.hasClass('green')) colors = ['#7dbc00','#459300','#39C3FD'];
  if (div.hasClass('white')) colors = ['#FFFFFF','#A2A2A2','#39C3FD'];

  var normal = 0,
      hover = 1,
      press = 2,
      focus = 3,
      hover_focus = 4,
      disabled = 5;

  // make the rounded rectangles and borders
  for (var i=0; i<SPRITE_HEIGHT; i++) {
    // move into position
    ctx.save();
    ctx.translate(0,height*i);

    // make the gradients for the background and the stroke style
    var fill_gradient = ctx.createLinearGradient(0,0,0,height);
    if (i==disabled) { // disabled state colored differently
      var grey = '#777777';
      fill_gradient.addColorStop(0, grey);
      fill_gradient.addColorStop(1, grey);
    }
    else {
      fill_gradient.addColorStop(0,colors[0]);
      fill_gradient.addColorStop(1,colors[1]);
    }
    ctx.fillStyle = fill_gradient;

    var border_stroke_style = null;
    if (i==press) {
      border_stroke_style = ctx.createLinearGradient(0,0,x,y);
      border_stroke_style.addColorStop(0,'rgba(0,0,0,0.7)');
      border_stroke_style.addColorStop(1,'rgba(0,0,0,0.4)');
    }
    else if (i==disabled) {
      border_stroke_style = '#222';
    }
    else {
      border_stroke_style = '#777';
    }

    // draw rounded rectangle with borders and gradients
    roundRect(ctx, line_width, line_width,
	      width-line_width*2, height-line_width*2, radius);
    ctx.fill();

    ctx.strokeStyle=border_stroke_style;
    roundRect(ctx, line_width*1.5, line_width*1.5,
	      width-line_width*3, height-line_width*3, radius-line_width);
    ctx.lineWidth = line_width;
    ctx.stroke();

    // if this is a hover state, need to whiten it a bit
    if (i==hover || i==hover_focus) {
      ctx.fillStyle = "rgba(255,255,255,0.15)";
      ctx.fill();
    }
    // if pressed, make it darker
    if (i==press) {
      ctx.fillStyle = "rgba(0,0,0,0.2)";
      ctx.fill();
    }
    // if focused, add a complimentary color glow
    if (i==press || i==focus || i==hover_focus) {
      roundRect(ctx, line_width*1,line_width*1,
		width-(line_width*2), height-(line_width*2),
		radius);
      ctx.lineWidth = line_width*2;
      ctx.strokeStyle = colors[2];
      ctx.globalAlpha = 0.6;
      ctx.stroke();
      ctx.globalAlpha = 0.3;
      ctx.fillStyle = colors[2];
      //ctx.fill();
    }
    // go back to original state
    ctx.restore();
  }
  if (div.hasClass('hide')) div.hide();

  // don't render a canvas button over again
  div.addClass('rendered');

  //debug.info(canvas[0]);
  //debug.info('url("'+canvas[0].toDataURL()+'")');

  /*
   * Note: tried putting the image url in as a background-image so that
   * the canvas built data could be used for input buttons, but the image
   * gets screwed up when Firefox or Chrome changes page zoom. Note sure
   * why. When you zoom, then refresh, it's fine.
   */
  //div.css('background-image','url("'+canvas[0].toDataURL()+'")');
  //canvas.hide();
}


/*
 * http://js-bits.blogspot.com/2010/07/canvas-rounded-corner-rectangles.html
 *
 * Draws a rounded rectangle using the current state of the canvas.
 * If you omit the last three params, it will draw a rectangle
 * outline with a 5 pixel border radius
 *
 * @namespace SKRIT
 * @method roundRect
 * @param {CanvasRenderingContext2D} ctx
 * @param {Number} x The top left x coordinate
 * @param {Number} y The top left y coordinate
 * @param {Number} width The width of the rectangle
 * @param {Number} height The height of the rectangle
 * @param {Number} radius The corner radius. Defaults to 5;
 * @param {Boolean} fill Whether to fill the rectangle. Defaults to false.
 * @param {Boolean} stroke Whether to stroke the rectangle. Defaults to true.
 */
function roundRect(ctx, x, y, width, height, radius, fill, stroke) {
  stroke = stroke === undefined ? false : false;
  radius = radius === undefined ? 5 : radius;
  ctx.beginPath();
  ctx.moveTo(x + radius, y);
  ctx.lineTo(x + width - radius, y);
  ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
  ctx.lineTo(x + width, y + height - radius);
  ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
  ctx.lineTo(x + radius, y + height);
  ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
  ctx.lineTo(x, y + radius);
  ctx.quadraticCurveTo(x, y, x + radius, y);
  ctx.closePath();
  if (stroke) {
    ctx.stroke();
  }
  if (fill) {
    ctx.fill();
  }
}


// Checkboxes in the upper left corner of tables. Set them up like this: //
/*
 * <th><input type="checkbox" onclick="checkAllInTable(this);" /></th>
 * <td><input type="checkbox" onclick="updateAllCheckbox(this);" /></td>
 */

function checkAllInTable(dom) {
  dom = $(dom);
  var table = dom.closest('table');
  var checkboxes = $('td input[type="checkbox"]', table);
  checkboxes.prop('checked',dom.prop('checked'));
}

function updateAllCheckbox(checkbox) {
  if (checkbox.target) checkbox = checkbox.target;
  checkbox = $(checkbox);
  var table = checkbox.closest('table');
  var unchecked = $('td input[type="checkbox"]:not(:checked):visible',
		    table).length;
  var allcheckbox = $('th input[type="checkbox"]', table);
  allcheckbox.prop('checked',unchecked==0);
}





/* info bubbles */

function InfoBubble(parent, message) {

  /*
   * Usage:
   *
   * var ib = new InfoBubble($('#parent_object'), 'Message!');
   * ib.position({'top':'15px', 'left':'30px'});
   * ib.noRepeatID('only_once');
   * ib.open();
   *
   * The Info Bubble is given an object to attach itself to and a message
   * to display; that's the minimum it will need.
   *
   * You can also set the position of the bubble itself or the arrow.
   *
   * It has a built in system to only show itself if it hasn't been shown
   * before for the user. Use noRepeatID to give it a unique ID that it
   * can check via AJAX. If the open function is called, it will first check
   * the server to see if it should show, and then will show. So it will
   * not show up immediately if it needs to check first.
   *
   * Give it a callback function if you want it to unbind after it shows up
   * to be more efficient.
   *
   * These automatically chain, in that if you try to open two of them, the
   * second one will only show when the first is closed.
   *
   */

  // PROPERTIES

  this.jqobj = null; // jQuery Object
  this.parent = parent;
  this.message = message;
  this.id = null;
  this.opened = false;
  this.checking = false;
  this.next_bubble = null;

  // FUNCTIONS

  this.mouseClick = function(ib) {
    return function(e) {
      // this function gets called whenever the mouse is clicked when an info
      // bubble is open. Checks to see if it should be closed.
      if ($(e.target).hasClass('info_bubble_button')) return;
      ib.close();
      e.stopPropagation();
    };
  }(this);

  this.checkAndOpen = function(id, no_chaining) {
    // check to see if this bubble has been seen before, then open it if not
    if (this.checking || this.opened || !this.parent) return this;
    this.id = id;
    this.checking = true;
    $.retried_get('/info_bubble_status',{id:id},function(ib) {
      return function(response) {
	ib.checking = false;
	if (response == 'not seen') ib.open(no_chaining);
	else ib.opened = true;
      };
    }(this));
    return this;
  };

  this.open = function(no_chaining) {
    // opens the bubble, or puts it in the stack of bubbles to open if one
    // is already visible
    if (this.opened || this.checking || !this.parent) return this;

    // check for already open ones
    var open_ib = $('.info_bubble:visible');
    if (open_ib.length>0) {
      open_ib = $(open_ib[0]).data('j_obj');
      if (!no_chaining) open_ib.pushOntoStack(this);
      return this;
    }

    // make sure the parent is still visible
    if (!this.parent.is(':visible')) {
      this.parent = null;
      return this;
    }

    // okay now open it
    this.opened = true;
    this.parent.css('position','relative');
    this.parent.append(this.jqobj);
    $('.info_bubble_content',this.jqobj).html(message);
    this.jqobj.show();
    $('body').click(this.mouseClick);

    // and if there was an ID, notifiy the server so it doesn't open again
    $.retried_post('/info_bubble_status',{id:this.id}, function() { ; });

    return this;
  };

  this.close = function() {
    this.jqobj.remove();
    $('body').unbind('click', this.mouseClick);
    if (this.next_bubble) this.next_bubble.open();
    return this;
  };

  this.pushOntoStack = function(ib) {
    if (this.next_bubble) this.next_bubble.pushOntoStack(ib);
    else this.next_bubble = ib;
  };

  this.position = function(css) {
    for (var key in css) {
      this.jqobj.css(key, css[key]);
    }
    return this;
  };

  this.arrowPosition = function(css) {
    var arrow = $('.info_bubble_arrow',this.jqobj);
    arrow.css('left','auto');
    for (var key in css) {
      arrow.css(key, css[key]);
    }

    return this;
  };

  // INITIALIZATION

  this.jqobj = $('#blank_info_bubble .info_bubble').clone();
  this.jqobj.data('j_obj',this);
}


// This is where we begin to use the module design.

var SKRIT = SKRIT || {};

// function from O'Reilly's Javascript Patterns

SKRIT.namespace = function (ns_string) {
    var parts = ns_string.split('.'),
        parent = SKRIT,
        i;
    // strip redundant leading global
    if (parts[0] === "SKRIT") {
        parts = parts.slice(1);
    }
    for (i = 0; i < parts.length; i += 1) {
        // create a property if it doesn't exist
        if (typeof parent[parts[i]] === "undefined") {
            parent[parts[i]] = {};
        }
       parent = parent[parts[i]];
   }
   return parent;
};



SKRIT.namespace('SKRIT.utils');

/**
 * Lazy List
 *
 * This is used for tables used throughout the site, which fetch a certain
 * number of elements at first and then download more via AJAX as they
 * are needed. First made a more advanced version of this on the My Words
 * page and this is a version for generalized use.
 *
 * To use, make a global list on startup with the URL of where to fetch
 * more. Fetch slices as needed. If you try to get a slice and it returns
 * null, give it a load_callback function so that javascript can try again
 * once more has been fetched.
 *
 * Details:
 *
 * Items can be anything.
 *
 * load_callback is called with the LazyList as its first argument. The
 * load_callback variable is cleared before the function itself is called
 *
 * type is just to be used to differentiate between different types of
 * data with the same URL. It can be ignored
 *
 * The data passed in from ajax needs to be JSON formatted data, with two
 * pieces of information in dictionaries: items (a list of items to be
 * added) and cursor (a string, or null if there are no more to fetch).
 *
 * @namespace SKRIT.utils
 * @class LazyList
 * @param fetch_url {String} url for getting more items
 * @param params {Object} optional additional url params to send to the server
 *
 */
SKRIT.utils.LazyList = function LazyList(fetch_url, params) {

  /*
   * Same basic setup as the my words page. Used for both published and
   * user lists.
   */

  // PROPERTIES

  this.items = [];
  this.cursor = "None";
  this.fetching = false;
  this.load_callback = null;
  this.params = params || {};
  this.lookup_table = {};
  this.lookup_func = null;

  this.BUFFER_SIZE = 40;
  this.PAGE_SIZE = 10;

  // for exporting
  this.forced_minimum_size = 0;
  this.item_filter = null;

  // FUNCTIONS

  this.setLookupFunc = function(func) {
    /**
     * The function should accept one item, and return a key.
     */
    if (this.items.length != 0) throw "TOO LATE!";
    this.lookup_func = func;
  };

  this.fetchMoreIfNeeded = function(offset, page_size) {
    // checks to see if more can be gotten and should be gotten, and gets if so
    // returns true if it does end up getting more, or is already getting more.
    // at the end call and remove the load callback, and call this function
    // once again in case more need to be fetched
    if (this.fetching) return true;

    if (!this.haveEnoughFor(offset, page_size) && this.canLoadMore()) {
      this.fetching = true;
      this.params.cursor = this.cursor;
      var list_obj = this;

      $.retried_post(fetch_url, this.params, function(response)
      {
	var data = $.evalJSON(response);
	list_obj.addItems(data.items);
	list_obj.cursor = data.cursor;
	list_obj.fetching = false;
	if (list_obj.load_callback) {
	  var callback = list_obj.load_callback;
	  list_obj.load_callback = null;
	  callback();
	}
	list_obj.fetchMoreIfNeeded(offset, page_size);
      });
      return true;
    }

    else {
      if (this.load_callback) {
	  var callback = this.load_callback;
	  this.load_callback = null;
	  callback();
      }
    }
    return false;
  };

  this.haveEnoughFor = function(offset, page_size) {
    return (offset + page_size + this.BUFFER_SIZE < this.items.length &&
	    this.items.length > this.forced_minimum_size);
  };

  this.canLoadMore = function() {
    return Boolean(this.cursor);
  };

  this.setItemFilter = function(func) {
    this.item_filter = func;
  };

  this.addItems = function(items) {
    for (var i = 0, max = items.length; i < max; i++) {
      var item = items[i];
      if (this.item_filter != null && !this.item_filter(item))
	continue;

      this.items.push(item);
      if (this.lookup_func)
	this.lookup_table[this.lookup_func(item)] = item;
    }
  };

  this.findItem = function(key) {
    /*
     * Can only be used if a lookup func was submitted.
     */
    return this.lookup_table[key];
  };

  this.getItemSlice = function(offset, number) {
    // fetches vocabs in the given range. Returns null if the loading is not
    // finished, empty list if the list is just not that long
    //
    // also return null if there are enough vocabs, but only just enough,
    // and there are still cursors to go through. This way the loading waits
    // until it knows if there's a next page or not

    if (!number) number = 10;
    var min_length = offset + number;
    if (this.forced_minimum_size != 0) min_length = this.forced_minimum_size;
    if (this.items.length<=min_length && this.canLoadMore()) {
      return null;
    }

    return this.items.slice(offset, offset+number);
  };
};





/**
 * LazyListTable
 *
 * Goes hand in hand with the LazyList. Basically it handles the displaying,
 * navigation, events, and the numbers shown above the table.
 *
 * Augment the functionality with event delegation, by attaching event handlers
 * to the wrapper or the table element, which don't change once initialized.
 *
 * Usage:
 *
 *   var ll = new LazyList('/something/fetch','published');
 *   // configure ll...
 *   var options = {};
 *   // configure options...
 *   var llt = new LazyListTable($('#table_wrapper'), ll, options);
 *   llt.initialize();
 *   llt.render();
 *
 * @namespace SKRIT.utils
 * @class LazyListTable
 * @param wrapper {jQuery} empty div to put the table into
 * @param lazylist {Object} the list, proxying with the server to fetch items
 * @param itemToRow {function} converts item to table row
 * @param options.page_size {Number} table page size
 * @param options.total_size {Number} for display above table
 * @param options.header {jQuery} header to be added to the tbody
 * @param options.empty_msg {jQuery} div to show below the table if empty
 * @param options.renderCallback {function} called after table draw
 *
 */
SKRIT.utils.LazyListTable = function (wrapper, lazylist, itemToRow, options) {
  var page_size = options.page_size || 10,
      total_size = options.total_size || null,
      header = options.header || null,
      empty_msg = options.empty_msg || null,
      renderCallback = options.renderCallback || null,
      offset = 0,
      self = this,
      dimensions_locked = false,
      waiting = false; // waiting for the list to finish fetching

  wrapper.css('position','relative');

  /**
   * Puts the barebones lazy table and associated elements into the dom.
   *
   * @method initialize
   */
  this.initialize = function initialize() {
    var self = this,
        tnav,
	table,
	loading_msg;

    if (empty_msg==null) {
      empty_msg = $('<div>No results</div>');
    }
    empty_msg
      .addClass('hidden')
      .addClass('empty_msg');

    tnav = $('<div></div>')
      .addClass('stdtable_nav')
      .addClass('unselectable')
      .append($('<div></div>')
	.addClass('left_nav')
	.addClass('nav_block')
	.append($('<span></span>')
	  .addClass('hidden')
	  .addClass('clickable')
	  .click(function() { self.render(-page_size); })
	  .text('Last')))
      .append($('<div></div>')
	.addClass('center_nav')
	.addClass('nav_block')
	.append($('<span></span>')
	  .addClass('hidden')))
      .append($('<div></div>')
	.addClass('right_nav')
	.addClass('nav_block')
	.append($('<span></span>')
	  .addClass('hidden')
	  .addClass('clickable')
	  .click(function() { self.render(page_size); })
	  .text('Next')))
      .append($('<div></div>')
	.addClass('clear'));

    table = $('<table></table>')
      .addClass('stdtable')
      .addClass('hidden')
      .append($('<tbody></tbody>'));

    loading_msg = $('<div></div>')
      .addClass('loading_msg')
      .addClass('rounded')
      .append($('<div></div>')
	.text('Loading...'));

    wrapper
      .html('')
      .addClass('lazytable_wrapper')
      .append(tnav)
      .append($('<div></div>')
	.addClass('screen_wrapper')
	.append(table)
	.append(loading_msg)
	.append(empty_msg));
  };

  /**
   * Renders the table rows, taking the table offset change into account if
   * there is one. Handles waiting for the LazyList to load.
   *
   * @method render
   * @param offset_change {Number} How much to change the offset of the table
   */
  this.render = function(self) { return function render(offset_change)
  {
    var items,
        new_tbody,
        start_num,
	end_num,
	nav_string;

    // don't do anything if waiting for more items to load
    if (waiting && lazylist.fetching) return;
    waiting = false;

    // handle offset changes
    if (typeof offset_change == 'number') {
      offset += offset_change;
    }

    // get the items and handle if the lazylist needs to load more
    items = lazylist.getItemSlice(offset, page_size);
    if (items == null) {
      waiting = true;
      $('.loading_msg', wrapper).show();
      lazylist.load_callback = self.render;
      lazylist.fetchMoreIfNeeded(offset, page_size);
      return;
    }

    // build and insert the table body
    new_tbody = $('<tbody></tbody>');
    if (header != null) new_tbody.append(header);
    for (var i = 0, max = items.length; i < max; i++) {
      new_tbody.append(itemToRow(items[i]));
    }
    $('tbody',wrapper).replaceWith(new_tbody);

    // set up the nav
    start_num = offset + 1;
    end_num = offset + items.length;
    nav_string = 'Viewing '+start_num+' - '+end_num;
    if (total_size != null) nav_string += ' of ' + total_size;
    $('.center_nav span', wrapper).text(nav_string).show();
    if (lazylist.items.length > offset + page_size) {
      $('.right_nav span', wrapper).show(); }
    else $('.right_nav span', wrapper).hide();
    if (offset == 0) $('.left_nav span', wrapper).hide();
    else $('.left_nav span', wrapper).show();

    // make sure the right things are showing up
    $('.loading_msg', wrapper).hide();
    $('table', wrapper).removeClass('hidden');
    if (items.length==0) {
      $('.empty_msg',wrapper).show();
      $('.center_nav span',wrapper).text('');
    }
    else {
      $('.empty_msg',wrapper).hide();
    }

    // lock the column widths and table height the first time it's rendered
    if (!dimensions_locked) {
      dimensions_locked = true;
      $('tr:first td, tr:first th',wrapper).each(function()
      {
	$(this).css('width',$(this).outerWidth()+'px');
      });
      var screen_wrapper = $('.screen_wrapper', wrapper);
      screen_wrapper.css('min-height',screen_wrapper.outerHeight()+'px');
    }

    // make sure any canvas buttons that were hidden get rendered
    if (self.renderCallback) {
      self.renderCallback(self);
    }

  }; }(this);
};




SKRIT.namespace('SKRIT.utils.vocab');

/**
 * A set of vocab span related functions
 * @namespace SKRIT.utils
 * @class vocab
 */
SKRIT.utils.vocab = {};

/**
 * Creates a vocab span, commonly used in tables with lists of words.
 *
 * @namespace SKRIT.utils.vocab
 * @method makeSpan
 * @param {String} rune
 * @param {String} rdng
 * @param {String} defn
 * @param {Boolean} options.comp Component flag. Default false.
 * @param {Boolean} options.rare Rare Kanji flag. Default false.
 * @param {Boolean} options.word_popup_link Default false.
 * @param {Boolean} options.no_defn Default false.
 * @return {jQuery} The resulting span to be inserted into a table.
 */
SKRIT.utils.vocab.makeSpan = function(key, rune, rdng, defn, options) {

  var span = $('<span></span>')
    .addClass('vocab_span')
    .attr('id', key)

    .append($('<span></span>')
      .html(rune)
      .addClass('rune')
      .attr('lang',lang))

    .append(' ')

    .append($('<span></span>')
      .addClass('rdng')
      .attr('lang', lang == 'ja' ? 'ja' : 'zh-Latin-pinyin')
      .html((rdng==rune) ? '' : rdng))

    .append((rdng==rune || options.no_defn) ? '' :': ')

    .append($('<span></span>')
      .addClass('defn')
      .html(defn));

  if (options.no_defn) $('.defn', span).remove();

  if (options.word_popup_link != false) {
    $('span.rune', span)
      .addClass('popup_button')
      .addClass('clickable_char')
      .addClass('word_popup_link')
      .attr('onclick','clickedLink=true');
  }

  if (defn=='' && !options.no_defn) {
    $('span.defn',span).append($('<span></span>')
      .addClass('popup_button')
      .addClass('unselectable')
      .addClass('clickable')
      .addClass('spanbutton')
      .addClass('add_defn_link')
      .text('Add definition'));
  }

  if (options.comp && lang=='ja') {
      $('.rdng',span).text('').after(
	$('<span></span>')
	  .addClass('comp')
	  .text('(comp)'));
  }

  if (options.rare && lang=='ja') {
      $('.rune',span).after(
	$('<span></span>')
	  .addClass('rare')
	  .text(' (rare kanji) '));
  }

  return span;
};

/**
 * Sets up common vocab span popup links, adding a definition and opening
 * a word popup. Hand it a callback that takes a LEW formatted vocab row
 * and applies the new definition to any javascript datastore.
 *
 * @namespace SKRIT.utils.vocab
 * @method bindVocabEventSet
 * @param {jQuery} target_parent The HTML element that encompasses the
 * target words
 * @param {func} defn_callback A function that is called when the definition
 * is set. It is passed the vocab row and the target of
 * the original click event. The function may use this information to alter
 * any existing javascript datastore of the definition.
 */
SKRIT.utils.vocab.bindVocabEvents = function(target_parent, defn_callback) {
  target_parent.click(function(e)
  {
    var target = $(e.target),
	key,
	position,
        vocabs = [],
	popup,
	callback;

    if (target.hasClass('word_popup_link')) {
      e.stopPropagation();
      key = target.closest('.vocab_span').attr('id');
      position = $(target.closest('tr:has(.vocab_span)'))
	.prevAll(':visible:has(.vocab_span)').length;
      //$('.vocab_span:visible').each(function() { vocabs.push(this.id); });
      new Popup('/vocab/wordpopup',
		{vocabKey: key,position:position,vocabs:vocabs})
	.showNav()
	.positionFromTop(e.pageY)
	.open();
    }
    else if (target.hasClass('add_defn_link')) {
      e.stopPropagation();

      key = target.closest('.vocab_span').attr('id');

      popup = new Popup('/vocab/lew/set_defn', { vocab:key });

      callback = function(ret_val) {
	var vocab;
	if (ret_val == null || ret_val.vocabs.length == 0) return;
	vocab = ret_val.vocabs[0];
	$('span[id="'+key+'"] .defn', target).html(vocab.defn);
	if (defn_callback) defn_callback(ret_val, target);
      };
      if (defn_callback) popup.setCallback(callback);
      popup.positionFromTop(e.pageY);
      popup.open($('.popup:not(:has(.popup)):visible'));

    }
  });
};





// http://www.tutorialspoint.com/javascript/array_indexof.htm
if (!Array.prototype.indexOf) {
  Array.prototype.indexOf = function(elt /*, from*/)
  {
    var len = this.length;

    var from = Number(arguments[1]) || 0;
    from = (from < 0)
         ? Math.ceil(from)
         : Math.floor(from);
    if (from < 0)
      from += len;

    for (; from < len; from++)
    {
      if (from in this &&
          this[from] === elt)
        return from;
    }
    return -1;
  };
}


// these two functions mess with the tab system so it goes at the end
function createSentenceRunes(elem, rune, vkeys, target) {
  if(!rune) return elem.empty();
  var temp_container = $('<div></div>');
  for(var i = 0, j = 0; i < vkeys.length; ++i) {
    while(rune.charAt(j).search(/[ .,:;!?()[\]0-9。．、，：；！？（）【】《》·…]/)
          != -1)
      temp_container.append($('<span>'+rune.charAt(j++)+'</span>')
                            .addClass('filler'));
    var vkey = vkeys[i];
    var char_span = vkey.length - 5;  // zh-一二三-0 -> 3 chars
    var word = $("<span>" + rune.slice(j, j + char_span) + "</span>")
      .addClass('can_lookup')
      .data('vkey', vkey)
      .click(function(evt) {
        var parent = findParentPopup(this);
        if(parent)
          parent = parent.jqobj;
        new Popup('/vocab/wordpopup',{vocabKey: $(this).data('vkey')})
          .setAsInnerNode().open(parent);
        evt.stopImmediatePropagation();
      })
      .appendTo(temp_container);
    if(target && target.search('['+rune.slice(j, j + char_span) + ']') != -1)
      word.addClass('sentence_target');  // shares some characters with target
    j += char_span;
  }
  if(j < rune.length)
    temp_container.append($('<span>'+rune.slice(j)+'</span>')
                          .addClass('filler'));

  elem.empty().append(temp_container.children());
  return elem;
}


function createSentenceRdngs(rdng_elem, rune_elem, rdng, vkeys) {
  if(!rdng) return rdng_elem.empty();
  var temp_container = $('<div></div>');
  var rdngs = rdng.split(' ');
  for(var i = 0, j = 0; i < vkeys.length; ++i) {
    // spaces in pinyin correspond to words in rune (if you skip punctuation)
    while(rdngs[j].search(/[^.,:;!?()[\]0-9。．、，：；！？（）【】《》·…-－]/)
          == -1)
      temp_container.append($('<span>'+rdngs[j++]+'</span>')
                            .addClass('filler'));
    var word = $("<span>" + rdngs[j++] + " </span>").appendTo(temp_container);
    if($(rune_elem.find('.can_lookup')[i]).hasClass('sentence_target'))
      word.addClass('sentence_target');  // shares some characters with target
  }
  if(j < rdngs.length)
    temp_container.append($('<span>'+rdngs[j]+'</span>')
                          .addClass('filler'));
  rdng_elem.empty().append(temp_container.children());
  return rdng_elem;
}


