/*
 *  ---------------  TABLE OF CONTENTS ---------------
 *
 * INITIALIZATION
 *
 * STATUS BAR
 * STATUS BAR - REVIEW AND ADD BARS
 * STATUS BAR - TIMES
 * STATUS BAR - VOLUME
 *
 * PROMPT
 * PROMPT - BUFFER
 * PROMPT - BUILD
 * PROMPT - FRONT SETTERS
 * PROMPT - CLEAR
 * PROMPT - CHAR PROMPT
 * PROMPT - LOOKUP BOXES
 * PROMPT - EXAMPLE SENTENCE
 * PROMPT - DECOMPS AND MNEMONICS
 * PROMPT - MISC INTERNAL
 * PROMPT - ACTIONS
 *
 * LIGHT BOXES
 * LIGHT BOXES - ACTIVE LISTS
 * LIGHT BOXES - SETTINGS
 * LIGHT BOXES - FEEDBACK
 *
 * ERROR MESSAGES
 *
 * UTILITIES
 *
 * NICK'S FLASH STUFF
 *
 * NICK'S - STARTUP
 * NICK'S - KEYBOARD
 * NICK'S - MOUSE
 *
 * CHANNEL API
 */


var BLANKS = false;

// --------~-------- INITIALIZATION

// AJAX data caches
var addable_cache;
var containing_vocab_cache;



$(document).ready(function()
  {
    // init the caches
    addable_cache = new DataCache(20);
    containing_vocab_cache = new DataCache(20);

    changeLang();

    // make the sliders
    new SliderWidget($("#anim_speed_bar"),
		     parseFloat($("#id_anim_speed").val()),
		     0.05,animSpeedCallback);
    new SliderWidget($("#order_weight_bar"),
		     parseFloat($("#id_order_weight").val()),
		     0.05,orderWeightCallback);
    volume_slider = new SliderWidget($("#volume_bar"),
		      init_volume, 0.05,volumeCallback);
    makeAutoManualVisualChanges(); // do css for active lists box
    setVolumeSpriteBackground();   // init volume

    empty_prompt = getPrompt().clone().removeClass('hidden');

    $('span.obscured', $('#prompt_wrapper')).live("click", function(evt)
      {
    	$(this).add($(this).prevAll()).removeClass('obscured');
      });

    // Make sure anything tagged as .jedit'able will be jeditable on click
    // (necessary because the click handlers reference wrong element after copy)
    $('.jedit').live('click', function(evt)
      {
        if(!android)
	  if(!$(this).data('event.editable'))
	    makeEditable($(this)).click();
      });

    if(top.location.hash.slice(1) == "settings")
      toggleSettingsWindow();

    if (all_parts.length == 1) {
      var label = $('label[for="id_temp_parts_on_1"]');
      $('input', label).prop('disabled', true).prop('checked', false);
    }

    updateTempPartVisuals();
    $('#temp_parts_line input').click(updateTempPartVisuals);

    var fadeout_timeout = setTimeout(function()
      {
	$('#part_focus_warning').animate(
	  {'opacity':'0.3'}).css('background','white');
      }, 5000);
    $('#part_focus_warning')
      .mouseover(function()
      {
	clearTimeout(fadeout_timeout);
	$('#part_focus_warning')
	  .css('opacity','1.0')
	  .css('background', '#DBFFC1');
      })
      .mouseout(function()
      {
	$('#part_focus_warning')
	  .css('opacity','0.3')
	  .css('background-color','white');
      });

    $('#part_focus_warning .remove_wrapper').click(
      function () { $('#part_focus_warning').remove(); }
    );
});


// --------~-------- STATUS BAR

// --------~-------- STATUS BAR - REVIEW AND ADD BARS

function adjustBarSize(i) {
  // make it go from 0 -> 0 and 500 -> 100, but not linear and not crazy exponential either
  i = Math.log(i+1) / 6.2166; // now it's on the right range but too exponential
  i = Math.pow(i, 3) // better, 150 -> 52%, rather than 25 -> 52%
  //i = ((100 * i) + '').slice(0, 5); // make it readable
  return i;
}

function setReviewBar(num)
{
  if (BLANKS) return;
  if (fetching_review_num) return;// while refreshing, ignore update from Flash
  num = Math.max(0, num);
  //var fraction = num / 100.0;  // old, linear
  //var fraction = Math.log(num + 20 + 1) / Math.log(2) / 10 - .4392317422778761; // old, exponential
  var fraction = adjustBarSize(num); // just right

  fraction = Math.max(0.0, Math.min(1.0, fraction));
  var full_width = parseInt($('#to_review_bar').width());
  var width = Math.round(fraction * full_width);
  if (width > 10) {
    $('#review_fore_bar').css("width", width - 10);
    $('#review_fore_end').css({width: 10, left: -10});
  }
  else {
    $('#review_fore_bar').css("width", 0);
    $('#review_fore_end').css({width: width, left: 0-width});
  }
  if (num > 9000) {
    $('#to_review_bar').attr("title", "Too many to count!");
    $('#to_review_bar span').text("over 9000!!");
  }
  else {
    $('#to_review_bar').attr("title",
      (num > 0 ? num : "No") + " items due for review." +
      " (A word may contain multiple items.)");
    $('#to_review_bar span').text(num + " to review");
  }

}


function setAddedBar(num)
{
  if (BLANKS) return;
  var fraction = num / (added_div*20.0);

  fraction = Math.max(0.0, Math.min(1.0, fraction));
  var full_width = parseInt($('#added_bar').width());
  var width = Math.round(fraction * 125);
  if (width > 10) {
    $('#added_fore_bar').css("width", width - 10);
    $('#added_fore_end').css({width: 10, right: -10});
  }
  else {
    $('#added_fore_bar').css("width", 0);
    $('#added_fore_end').css({width: width, right: 0-width});
  }
  $('#added_bar').attr("title", num + " new items added today." +
		       " (A word may contain multiple items.)");
  $('#added_bar span').text("added " + num);
}

var fetching_review_num = false;

// getReviewValue uses AJAX to fetch the current review bar value
function getReviewValue(partstyles) {
  fetching_review_num = true;
  var params = { parts:parts, styles:styles, list:solo_list };
  $.retried_get('/getnumtodo', params, function(response)
  {
    fetching_review_num = false;
    var num = parseInt(response);
    if(isNaN(num)) { debug.log('nan response: '+response); return; }
    skritter.setToReview(num);
  });
}

function manuallyAddWord() {
  if (study_mode=="demo") return;
  skritter.addWord();
}


function setAddWordButton(state) {
  if (BLANKS) return;
  $('#add_word_button').toggleClass("enabled", state)
    .toggleClass("disabled", !state)
    .toggleClass("clickable", state);
}



// --------~-------- STATUS BAR - TIMES

function setTimes(sessionTime, promptTime, pageOpenTime)
{
  // up to 2x faster than .text()
  var st = $('#session_time');
  st[0].innerHTML = sessionTime;
  $('#prompt_time')[0].innerHTML = promptTime;
  st.parent().attr('title', 'Studied ' + sessionTime + ' today, ' +
                   (promptTime ? (promptTime + ' on current item, ') : '') +
                   'with page opened ' + pageOpenTime + ' ago.');
}

// --------~-------- STATUS BAR - VOLUME

var volume_prev;
var volume_slider;

function volumeCallback(slider)
{
  saveVolumePref(volume_slider.value);
  setVolumeSpriteBackground();
  if (volume_slider.value == 0.0) {
    setSpeaker('word', 'unavailable');
    setSpeaker('char', 'unavailable');
  }
  skritter.setVolume(volume_slider.value);
  setVolumeTooltip(volume_slider.value);
}

function getIntVolume()
{
  var volume = volume_slider.value;
  var int_volume = parseInt(3*(volume+0.2));
  if (volume > 0.0) int_volume = Math.max(1,int_volume);
  if (volume < 1.0) int_volume = Math.min(2,int_volume);
  return int_volume;
}

function toggleMuteVolume() {
  var volume_prev = volume_slider.prev_value;
  if (volume_slider.value == 0) {
    if (volume_prev > 0) volume_slider.setValue(volume_prev);
    else volume_slider.setValue(0.5);
  }
  else
    volume_slider.setValue(0.0);
  setVolumeSpriteBackground();
  setVolumeTooltip(volume_slider.value);
  skritter.setVolume(volume_slider.value);
  saveVolumePref(volume_slider.value);
}

function muteVolume()
{
  volume_slider.setValue(0.0);
  setVolumeTooltip(0);
  setSpeaker('word', 'unavailable');
  setSpeaker('char', 'unavailable');
  debug.warn("Sound is not functioning, turning volume off.");
}

var volumeHighlight = false;
function highlightVolumeSprite(over)
{
  volumeHighlight = over;
  setVolumeSpriteBackground();
}

function setVolumeSpriteBackground()
{
  var int_vol = getIntVolume();
  var ofs = (volumeHighlight ? "0 " : "-18px ") + "-"+(int_vol*14)+"px";
  $('#volume_sprite').css("backgroundPosition", ofs).removeClass('hidden');;
}

function setVolumeTooltip(vol)
{
  $('#volume_control').attr("title", vol ? parseInt(100*vol) + "%" : "muted");
}

function saying(on)
{
  volumeHighlight = on;
  setVolumeSpriteBackground();
}

function saveVolumePref(pref) { savePref('volume', {volume: pref}); }


// --------~-------- PROMPT - BUFFER

var back_buffer;
var current_prompt;
var current_prompt_part;
var value_dict = {};
var clone_sample;
var buffer_counter = 0;

function getPrompt() {
  //return (back_buffer == undefined) ? $('.prompt') : back_buffer;
  if (current_prompt == undefined) current_prompt = $('.prompt');
  return (back_buffer == undefined) ? current_prompt : back_buffer;
}

function beforePromptSwitch() {  // called from Flash right before a switch
  // click cancel buttons on any jeditables hanging out
  $('.jedit :button', getPrompt()).slice(1).click();
}

function initBackBuffer() {
  if (BLANKS) return;
  index_tracker = {};
  back_buffer = $('.prompt').clone(true);
  back_buffer.removeClass('hidden');
  buffer_counter += 1;
  fillPromptValues();
  clearPromptMostly();
}

function flip(animate, part) {
  if (BLANKS) return;
  animate = false;
  var speed = (animate) ? 300: 0;

  var prompt = $('.prompt');
  if (speed>0) {
    prompt.css('position','absolute');
    back_buffer.css('position','absolute');

    var width = parseInt(prompt.css('width'));

    // It's possible to trigger an infinite loop here
    prompt.after(back_buffer);

    prompt.addClass('to_delete');
    back_buffer.css('left',width);

    prompt.animate({'left':-width},speed);
    back_buffer.animate({'left':0},speed);
    var new_prompt = back_buffer;
    setTimeout(function() { $('.to_delete').remove();
			    new_prompt.css('position','inherit');
			  },speed);
  }
  else {
    prompt.replaceWith(back_buffer);
  }
  current_prompt = back_buffer;
  back_buffer = undefined;
  current_prompt_part = part;
}

function getPromptValue(id) {
  if (value_dict[id]==undefined) {
    value_dict[id] = $('#'+id, getPrompt());
    debug.log('HAD TO FETCH VALUE WITH GET PROMPT VALUE! ' + id);
  }
  return value_dict[id];
}

function fillPromptValues() {
  value_dict = {};
  $('.value, .to_cache',getPrompt()).each( function()
  {
    value_dict[this.id] = $(this);
    //debug.log('filled prompt value: '+this.id);
  });
}



// --------~-------- PROMPT - BUILD

// stores the index number each prompt is at for marked spans
var index_tracker = {};

function addFillerText(text, kind, part)
{
  if (BLANKS) return;
  if (kind=='char') showCharacterPrompt();
  var id = kind + '_' + part + '_value';
  getPromptValue(id).append('<span class="filler">'+text+'</span><wbr />');
}

function addMarkedText(text, kind, part, spansize)
{
  if (BLANKS) return;
  var prompt = getPrompt();
  if (kind=='char') showCharacterPrompt();
  if (typeof(spansize)=='undefined') spansize = 1;
  var class_name = kind + '_' + part;
  if (typeof(index_tracker[class_name])=="undefined")
    index_tracker[class_name] = 0;

  if(part == "defn")
    text = textToHTML(text);
  var markup = $("<span class='outer'><span class='inner'>"
		 +text+"</span></span>");
  for (var i = 0; i<spansize; i++)
    markup.addClass("i"+(i+index_tracker[class_name]));
  if (part == "rdng" && lang == "zh")
    colorizeTones(markup);
  index_tracker[class_name] += spansize;
  getPromptValue(class_name+'_value').append(markup);
  if(part != "defn")
    markup.after($('<wbr />'));
  else if(android) {
    setFontSize(markup, text.length, kind);
  }
}



// --------~-------- PROMPT - FRONT SETTERS

function setStyle(style, kind) {
  if (BLANKS) return;
  var s = '';
  switch(style) {
  case 'simp': s = 'simp'; break;
  case 'trad': s = 'trad'; break;
  case 'rare': s = 'rare'; break;
  default: s = '';
  }
  var rune = getPromptValue(kind + '_rune');
  if (s == '')
    rune.addClass('no_style').removeClass('simp').removeClass('trad');
  else
    rune.removeClass('no_style').addClass(s);
  if (kind == "word")
    getPromptValue('style_value').text(s);
}

function setReadiness(readiness, tooltip) {
  // it's good to be readiness
  if (BLANKS) return;
  getPromptValue('readiness_value')
    .html('<span>'+readiness+'</span>')
    .attr('title',tooltip);
  if (study_mode == "standard")
    trackTimeIfNecessary();
}

var SCORE_MESSAGES = ["don't know","forgot","so-so","got it","too easy"];
var SCORE_TOOLTIPS = [
  "You don't know this word yet.", "You forgot this word.",
  "You mostly remembered this word, but it was too hard.",
  "You remembered this word just fine.",
  "This word was way too easy and will be scheduled much later next time."];
var PART_NAMES = {rune: "writing", tone: "tone", rdng: "reading", defn: "def."};

var last_timeout;
var last_nice;
var last_rdng_or_tone = 'rdng';

function setScore(score, part, nice, from_flash) {
  if (BLANKS) return;
  console.log("setScore:", score, part, nice, from_flash);
  var prompt = getPrompt();
  var class_name = "word_" + (part == "tone" ? "rdng" : part);
  $('.score_msg',prompt).remove();
  var msg_index = nice && score == 1 ? 0 : score;
  var score_msg = $('<span>'+SCORE_MESSAGES[msg_index]+' ('+PART_NAMES[part]+')</span>')
    .addClass('score_msg');
  $('.grader', getPromptValue(class_name))
    .prop('className','grader clickable')
    .addClass('grader'+score)
    .prop('title',SCORE_TOOLTIPS[msg_index] + " (Click to toggle.)")
    .after(score_msg)
    .parent().removeClass('hidden');
  if (last_timeout!=undefined) clearTimeout(last_timeout);
  last_timeout = setTimeout(function()
    { $('.score_msg',prompt).fadeOut(1000); },2000);
  last_nice = nice;
  if(part == 'tone' || part == 'rdng' && from_flash)
    last_rdng_or_tone = part;
}

function setSpeaker(kind, state) {
  if (BLANKS) return;
  if (volume_slider.value == 0.0 && state != "unavailable") return;
  var speaker = getPromptValue(kind+'_play_button');
  if (state=="playing") speaker.addClass('playing');
  else if (state=="stopped") speaker.removeClass('playing');
  if (state=="unavailable") speaker.hide();
  else speaker.show();
}

function setStar(kind,on) {
  var button = getPromptValue(kind+'_star_button');
  if (on) button.removeClass('offstar');
  else button.addClass('offstar');
}

function setVocabKey(key, kind) {
  if (BLANKS) return;
  getPromptValue(kind+'_vocab_key').val(key);
}

function setListSect(list, sect) {
  if (sect) list += ':';
  getPromptValue('list_value').text(list);
  getPromptValue('sect_value').text(sect);
  getPromptValue('list_sect').show().removeClass('hidden');
}

// --------~-------- PROMPT - CLEAR

function clearClasses(elem) {
  for (var i=0; i<P_CLASSES.length; i++)
    $('.outer.'+P_CLASSES[i],elem).removeClass(P_CLASSES[i]);
}

function clearPrompt(kind, part) {
  getPromptValue(kind+'_'+part+'_value').text('');
  index_tracker[kind+'_'+part] = 0;
}

function clearPromptMostly(skip_some) {
  var prompt = getPrompt();
  if (!skip_some) {
    $('.value.toclear',prompt).each(function()
    {
      if (this.tagName=="INPUT") $(this).val(' ');
      else $(this).text(' ');
    });
  }
  setStar('word',false); setStar('char',false);
  closeCharacterBubble();
  hideCharacterPrompt();
  setStyle('', 'word');
  setStyle('', 'char');
  getPromptValue('char_prompt').attr("style"," ");
  $('.score_msg',prompt).remove();
}

var empty_prompt;

function clearEntirePrompt() {
  if (BLANKS) return;
  if (back_buffer==undefined) $('.prompt').replaceWith(empty_prompt.clone());
  else back_buffer = empty_prompt.clone();
  current_prompt = undefined;
  buffer_counter += 1;
  fillPromptValues();
}

function clearMnem(kind) {
  $('#'+kind+'_mnem', getPrompt()).empty();
}

// --------~-------- PROMPT - CHAR PROMPT

function hideCharacterPrompt() {
  getPromptValue('char_prompt').addClass('hidden');
  getPromptValue('word_char_divider').addClass('hidden');
}

function showCharacterPrompt() {
  getPromptValue('char_prompt').removeClass('hidden');
  getPromptValue('word_char_divider').removeClass('hidden');
}

function openCharacterBubble(instant) {
  if (BLANKS) return;
  var prompt = getPrompt();
  var speed = 150;
  var c_b = getPromptValue('char_bubble', prompt);
  if (!c_b.hasClass('clickable')) return;
  c_b.css('min-height',c_b.height())
    .css('min-width',c_b.width())
    .removeClass('clickable');
  var cbt = getPromptValue('char_bubble_text');

  if (false && !instant) {  // let's just not animate this; it's not helpful
    setTimeout(function () { c_b.css('border-color','#f3f3f3'); },speed*.3);
    setTimeout(function () { c_b.css('border-color','#f6f6f6'); },speed*.6);
    setTimeout(function () { c_b.css('border',0).css('width','100%');
			     cbt.css('width','100%');
			   }, speed);
    cbt.show(speed).css('visibility','hidden');
    setTimeout(function () {
      cbt.css('visibility','visible').hide().fadeIn(speed); },speed+10);
  }
  else {
    c_b.css('border',0).css('width','100%');
    cbt.css('width','100%');
    cbt.show();
  }

  getPromptValue('char_bubble_label').hide();
}

function closeCharacterBubble() {
  getPromptValue('char_bubble').addClass('clickable').attr('style',' ');
  getPromptValue('char_bubble_text').attr('style',' ').hide();
  getPromptValue('char_bubble_label',prompt).attr('style',' ').show();
}


function setCharBubbleText(part) {
  if (part=='rdng'||part=='rune'||part=='defn')
    getPromptValue('char_bubble_label')
      .html(getPromptValue('char_'+part+'_value').html());
  else {
    getPromptValue('char_bubble_label').html(part);
  }
}



// --------~-------- PROMPT - LOOKUP BOXES

function practiceWordPopup(kind) {
  var key = getPromptValue(kind + "_vocab_key").val();
  new Popup("/vocab/wordpopup?vocabKey=" + encodeURIComponent(key)).open();
}

// --------~-------- PROMPT - EXAMPLE SENTENCE

function setSentence(sentence, target, delay) {
  //debug.info("setSentence("+sentence+", "+target+", "+delay);
  if(delay) {
    setTimeout(function() { setSentence(sentence, target); }, delay);
    return;
  }
  var me = getPromptValue('word_example_sentence');
  var rune = createSentenceRunes(me.find('.sentence_rune'), sentence.rune,
                                 sentence.vkeys, target);
  rune.toggleClass('defaulted', !!sentence.defaulted);
  createSentenceRdngs(me.find('.sentence_rdng'), rune, sentence.rdng,
                      sentence.vkeys);
  me.find('.sentence_defn').text(sentence.defn);
  var tooltip = me.find('.sentence_tooltip');
  rune.unbind('hover').
    hover(function() { tooltip.css({left: rune.position().left,
                                    top:rune.position().top+rune.height()+2})
                       .show(); },
          function() { tooltip.hide(); });
  if(username && username != "anonymous")
    me.find('.sentence_list_button').unbind('click').
    click(practiceSentencesPopup).removeClass('hidden');
  setTimeout(function() { addExampleSentencePlayButton(me); }, 100);
}

function changeSentence(action) { setTimeout(action + "Sentence();", 1); }
function clozeSentence() { $('.sentence_target').addClass('cloze'); }
function unclozeSentence() { $('.sentence_target').removeClass('cloze'); }
function hideSentence() { getPromptValue('word_example_sentence').hide(); }
function showSentence() { getPromptValue('word_example_sentence').show(); }

function practiceSentencesPopup(evt) {
  var key = getPromptValue("word_vocab_key").val();
  new Popup('/vocab/sentence_list', {vkey: key, num: 10}).open();
  evt.stopImmediatePropagation(); // otherwise it just closes itself right away?
}

function addExampleSentencePlayButton(sentence_container) {
  var text = sentence_container.find('.sentence_rune').text();
  var url = "http://tts-voice.phiware.com/skritt5642809vox?url=" + 'http://www.skritter.com/study/all' /*window.location*/ + "&lang=zh&save=1&voxtext=\\speed=10" + encodeURIComponent(text);
  $('.play_button', sentence_container).show()
    .unbind("click")
    .click(function() { skritter.playSentenceAudio(url, text); });
}

// --------~-------- PROMPT - DECOMPS AND MNEMONICS

function setDecomp(decomp) {
  //debug.info("setDecomp("+decomp+")");
  var d = getPromptValue('decomp').html(decomp).addClass('hide_complete');
  if(lang == "zh")
    $('.comp_rdng span', d).each(function() { colorizeTones($(this)); });
  $('.comp_defn', d).each(function() {
    $(this).html(textToHTML($(this).text()));
  });
}

function showDecomp(decomp) {
  getPromptValue('decomp').removeClass('hide_complete');
}

function showMnem(kind, start_editing) {
  //$('.hide_complete',getPromptValue(kind+'_mnem')).removeClass('hide_complete');
  $('#'+kind+'_mnem .hide_complete', getPrompt()).removeClass('hide_complete');
  removeMnemPlaceholder(kind, start_editing);
  // maybe make another call for this so other words with same vkey aren't shown
  skritter.updateUserVocab({vkey: $('#'+kind+'_vocab_key', getPrompt()).val(),
			    shown: true});
}

function removeMnemPlaceholder(kind, start_editing) {
  var top_wrapper = $('#'+kind+'_mnem', getPrompt());
  $('.mnem_placeholder', top_wrapper).remove();
  $('.hide_complete', top_wrapper).removeClass('hide_complete');
  if(!$('.mnem_widget', top_wrapper).data('event.editable'))
    makeEditable($('.mnem_widget', top_wrapper));
  if(start_editing && !android)
    $('.mnem_widget', top_wrapper).click();
  var textarea = $('textarea', top_wrapper);
  if(textarea.width() < 300)  // for some reason, it's width:0 in IE unless
    textarea.width(300);      // we force it
}

function setMnem(mnem, author, is_shared, shared_mnem_count, kind, show) {
  //debug.info("setMnem("+mnem+","+author+","+is_shared+","+shared_mnem_count+","+kind+","+show+")");
  var top_wrapper = $('#'+kind+'_mnem', getPrompt());
  top_wrapper.toggleClass('no_mnem', !mnem);

  var widget = $('<div class="mnem_widget jedit hide_complete"></div>');
  if(mnem)
    widget.html(textToHTML(mnem, author));
  else if(android)
    widget.html('add mnemonic');
  widget.data('shared_mnem_count', shared_mnem_count);
  var edit_wrapper = $('<div></div>');
  edit_wrapper.append(widget);

  var apparent_kind = kind;
  if(apparent_kind == "word" &&
     $("#word_vocab_key").val().split('-')[1].length == 1)
    apparent_kind = "char";
  var mnem_button = $('<div class="mnem_placeholder"></div>')
    .click(function() { if(!android) showMnem(kind, !mnem); })
    .text(mnem ? "show mnemonic" : "add " + apparent_kind + " mnemonic ("
            + shared_mnem_count + ")");

  var show_others = $('<input type="button"></input>')
    .attr("value", 'see all ('+shared_mnem_count+')')
    .addClass("when_editing").addClass("popup_button").mousedown(function()
      {
	var params = $.param({vkey: $('#'+kind+'_vocab_key').val()});
	new Popup('/get_public_mnemonics?'+params).open();
      });

  // for shared, we override clicks because we need to toggle it with mousedown
  // instead, otherwise blur happens before click finishes
  var init_state = is_shared == null ? mnem_public_default : Boolean(is_shared);
  var shared = $('<div id="'+kind+'_mnem_shared" class="when_editing shared">');
  var shared_label = $('<label for="'+kind+'_mnem_shared_box">shared</label>')
    .click(preventDefault);
  var shared_box = $('<input id="'+kind+'_mnem_shared_box" type="checkbox" />')
    .prop('checked', init_state).click(preventDefault).change(function()
    {
      var vkey = $('#'+kind+'_vocab_key').val();
      var params = {vkey: vkey, mnem_public: $(this).is(':checked')};
      $.retried_post("/save_user_vocab", params, onUserVocabUpdate);
    });
  shared.append(shared_label).append(shared_box).mousedown(function(evt)
    {
      evt.preventDefault();
      var shared_box = $('input', this);
      shared_box.prop('checked', !shared_box.is(':checked')).change();
      mnem_public_default = !mnem_public_default;
    });

  top_wrapper.append(mnem_button).append(edit_wrapper);
  if(shared_mnem_count > 0)  // if there is anything shared (even defaulted)
    // there are some more public mnems; add the button to see them
    top_wrapper.append(show_others);
  top_wrapper.append(shared).append('<div class="clear"></div>');

  if(show)
    showMnem(kind);
}

function setOrigDefn(defn, defn_kind, kind) {
  //debug.info("setting orig defn:", defn, defn_kind, kind);
  if (defn.length == 0)
    getPromptValue(kind+'_defn_value')
      .attr("title","Click to edit this definition for yourself.");
  else
    getPromptValue(kind+'_defn_value')
      .attr("title", defn_kind + ": " + defn +
            " -- Click to edit this definition for yourself.");
}

function setCustomDefn(defn, orig_defn, is_shared, kind) {
  //debug.info("setCustomDefn("+defn+","+is_shared+","+kind+")");
  var top_wrapper = getPromptValue(kind+'_defn_value').addClass('edit_wrapper');

  var widget = top_wrapper.children().children()
    .addClass('c_defn_widget jedit').data('orig_val', orig_defn);

  var shared = $('<div id="'+kind+'_defn_shared" class="when_editing shared">');;

  var shared_box = $('<input id="'+kind+'_defn_shared_box" type="checkbox" />')
    .prop('checked', Boolean(is_shared))
    .click(preventDefault)
    .mousedown(preventDefault)
    .resize(function() {
      // hack: using resize since change and mouse events are uncontrollable
      var vkey = $('#'+kind+'_vocab_key').val();
      var params = {vkey: vkey, c_defn_submit: $(this).is(':checked'),
                    source_lang: source_lang};
      debug.info(params);
      $.retried_post("/save_user_vocab", params, onUserVocabUpdate);
    });

  var shared_label = $('<label for="'+kind+'_defn_shared_box"></label>')
    .text("shared as correction")
    .click(preventDefault)
    .mousedown(preventDefault);

  shared.mousedown(function(evt) {
    shared_box.prop('checked', !shared_box.is(':checked')).resize();
  });

  shared.append(shared_label).append(shared_box);
  top_wrapper.append(shared);
}

function selectOtherMnem(mnem_ref, vkey, mnem, author) {
  debug.info("selectOtherMnem:", mnem_ref, vkey, mnem, author);
  var params = {mnem_ref: mnem_ref, user: username, vkey: vkey};
  $.retried_post('/save_user_vocab', params, onUserVocabUpdate);
  var kind = $('#word_vocab_key').val() == vkey ? "word" : "char";
  var widget = $('#'+kind+'_mnem .mnem_widget');
  $('#'+kind+'_mnem .mnem_widget').html(
    textToHTML(mnem, author != username ? author : ""))
    .data('last_value', mnem);
  widget.parent().parent().toggleClass('no_mnem', !mnem);
}

// gets called when it's clicked on if it's not editable, since the events
// don't point at the right target when the prompt is copied with clone(true)
// can't edit these on Android
function makeEditable(widget) {
  if(!widget.is('*') || android) return widget;
  widget.data('last_value', HTMLToText(widget.html()));
  var kind = widget.parents('#word_prompt').is('*') ? "word" : "char";
  var apparent_kind = kind;
  if(apparent_kind == "word" &&
     $("#word_vocab_key").val().split('-')[1].length == 1)
    apparent_kind = "char";
  var what = widget.hasClass('mnem_widget') ? "mnem" : "c_defn";
  var old_tooltip = "";
  if(what == "c_defn")
    old_tooltip = getPromptValue(kind+"_defn_value").attr("title");
  if(old_tooltip)
    old_tooltip = "Original: " +
      old_tooltip.replace(/ -- .*/, '').replace("Original: ", "") + " -- ";
  widget.editable(function(value, settings)
  {
    value = $.trim(HTMLToText(value));
    if(what == "c_defn" && !value)  // don't let them make empty defns
      value = widget.data('orig_val');
    $.proxy(onEditingOff, this)();
    if(value == widget.data('last_value'))
      return textToHTML(value);  // not updated  // NEEDS: author name somehow
    widget.data('last_value', value);
    var shared_box_id = '#'+kind+'_'+(what=="c_defn"?"defn":what)+'_shared';
    var shared = Boolean($(shared_box_id).children('input').is(':checked') &&
                         value);
    var vkey = $("#"+kind+"_vocab_key").val();
    var params = {user: username, vkey: vkey};
    params[what] = value;
    params[what == "mnem" ? "mnem_public" : "c_defn_submit"] = shared;
    if(what == "c_defn" && value == widget.data('orig_val'))
      params.c_defn = "";
    else if(what == "c_defn")
      params.source_lang = source_lang;
    $.retried_post('/save_user_vocab', params, onUserVocabUpdate);
    debug.info(params);
    return textToHTML(value);  // author is us
  },
  {
    submit: "save",
    cancel: "cancel",  // keeping this button so I can trigger reset, but hiding
    type: "textarea",
    width: 300,  // also force this above for IE
    rows: 3,
    tooltip: old_tooltip + 'Click to edit this '
      + (what == "mnem" ? "mnemonic" : "definition")
      + ' for yourself',
    placeholder: 'add ' + apparent_kind + ' mnemonic ('
      + widget.data('shared_mnem_count') + ')',
    onsubmit: $.proxy(onEditingOff, widget),
    onreset: $.proxy(onEditingOff, widget),
    onblur: 'submit'
  }).click(onEditingOn);
  return widget;
}

function onEditingOn() {
  absorbKeys = true;
  $(document).keydown(trapEnterForSubmit);
  var wrapper = $(this).parent().parent().addClass('editing');
  wrapper.removeClass('no_mnem');
  $('textarea', this).val(HTMLToText($('textarea', this).val()));
}

function onEditingOff() {
  absorbKeys = false;
  $(document).unbind("keydown", trapEnterForSubmit);
  var wrapper = $(this).parent().parent().removeClass('editing');
  if($(this).hasClass('mnem_widget'))
    // figure out whether we have anything entered after this completes
    setTimeout(function() {
      var empty = $('.mnem_widget', wrapper)
        .text().match(/add (word|char) mnemonic/);
      wrapper.toggleClass('no_mnem',empty);
    }, 50);
  else if(android)
    setFontSize($(this), $('textarea', this).val().length,
                $(this).parents().find('#char_prompt').is("*")?"char":"word");
}

function trapEnterForSubmit(evt) {
  if(evt.keyCode != 13 || evt.shiftKey) return true; // allow shift+enter for \n
  // enter pressed -- need to submit jedit textareas if any are active
  if($(".jedit textarea").is("*"))
    $(".jedit form").submit();
  return false;
}

function onUserVocabUpdate(response) {
  skritter.updateUserVocab($.parseJSON(response));
}

function openMnemonicsPopup(kind, mnem_wrapper) {
  // For Android, instead of inline editing
  if($('#popup_wrapper .popup').is('*'))
    return; // trying to open tons of these for some reason
  var params = $.param({vkey: $('#'+kind+'_vocab_key').val(), popup: true});
  new Popup('/get_public_mnemonics?'+params).open();
}

// --------~-------- PROMPT - MISC INTERNAL

var P_CLASSES = [
  'target_hide',     // hides the text and underlines it
  'target_show',     // shows the text and underlines it
  'non_target_hide', // hides the text, underlines it less
  'deemphasize',     // sets color to gray
  'hide_complete'    // hides and keeps from using up space
];

var TONES = ['*āáǎà','*ōóǒò','*ēéěè','*īíǐì','*ūúǔù','*ǖǘǚǜ',
	     '*ĀÁǍÀ','*ŌÓǑÒ','*ĒÉĚÈ','*ĪÍǏÌ','*ŪÚǓÙ','*ǕǗǙǛ'];

function colorizeTones(syllable) {
  for (var i=0; i<TONES.length; i++)
    for (var j=0; j<TONES[i].length; j++)
      if (syllable.text().indexOf(TONES[i][j]) > -1)
	{
	  syllable.addClass('tone'+j);
	  return;
	}
}

function toggleScore(part, grader) {
  var class_name = "word_"+part;
  grader = $(grader);
  var score = (grader.hasClass('grader1')) ? 1 :
    (grader.hasClass('grader2')) ? 2:
    (grader.hasClass('grader3')) ? 3: 4;
  score = Math.max((score + 1) % 5, 1);
  if(part == 'rdng' && last_rdng_or_tone == 'tone')
    part = 'tone';
  setScore(score, part, last_nice);
  skritter.scoreToggled(score, part);
}

function toggleStar(kind, suppress_save) {
  var star = getPromptValue(kind+'_star_button').toggleClass('offstar');
  if(suppress_save) return;
  var vkey = $('#'+kind+'_vocab_key').val();
  var params = {vkey: vkey, starred: !star.hasClass('offstar')};
  $.retried_post("/save_user_vocab", params, onUserVocabUpdate);
}

function banWordPopup() {
  var vkey = $('#word_vocab_key').val();
  new Popup('/vocab/ban_popup', {vocab:vkey}).open();
}


// --------~-------- PROMPT - ACTIONS

/* ZH RUNE */

function zhRuneInit(index, hide_rdng, hide_for_tone) {
  if (BLANKS) return;
  var rune = getPromptValue('word_rune_value');
  clearClasses(rune);
  for (var i = 0; i < index_tracker['word_rune']; i++)
    if (i > index)  $('.i'+i,rune).addClass('non_target_hide');
  $('.i'+index,rune).addClass('target_hide').removeClass('non_target_hide');
  getPromptValue('char_rune_value').addClass('target_hide');

  var rdng = getPromptValue('word_rdng_value');
  clearClasses(rdng);

  for (i = 0; i < index_tracker['word_rdng']; i++) {
    $('.i'+i, rdng).toggleClass('deemphasize', i != index)
      .toggleClass("obscured", hide_rdng && i >= index);
  }
  if(hide_rdng) {
    $('.obscured',rdng).prevAll('.filler:contains("\'")').slice(0,1)
    .addClass('hide_complete');

    if(hide_for_tone)
      // hide char bubble rdng, too
      getPromptValue('char_rdng_value').addClass('obscured');
  }

  var rdng_text = $('.i'+index,rdng).text();
  setCharBubbleText(hide_rdng ? "show" : rdng_text); // maybe hide bubble pinyin
}

function hiddenRdngShow(i, prompt) {
  var limit = i == -1 ? '' : '.i'+i;
  $('#word_rdng_value span.outer'+limit, prompt ? prompt : getPrompt())
    .removeClass('obscured').prevAll('.filler').removeClass('hide_complete');
  if(!limit)
    getPromptValue('char_rdng_value').removeClass('obscured');
}

function zhRuneShow(instant, expose_all_rdngs, hide_for_tone) {
  if (BLANKS) return;
  //report("zhRuneShow:", instant, expose_all_rdngs, hide_for_tone);
  var prompt = getPrompt();
  var targ = $('.target_hide', prompt).removeClass('target_hide');
  openCharacterBubble(instant);
  var targIndex = targ.prevAll('.outer').length;
  if(hide_for_tone)
    {
      targIndex -= 1;
      if(targIndex >= 0)
        hiddenRdngShow(targIndex);
    }
  else if(expose_all_rdngs)
    hiddenRdngShow(-1);
  else
    hiddenRdngShow(targIndex);
  showDecomp();
  showMnem('char');
  if(!targ.nextAll('.outer').is('*'))  // last one
    showMnem('word');
}

function zhRuneHide(index) {
  if (BLANKS) return;
  var rune = getPromptValue('word_rune_value');
  clearClasses(rune);
  for (var i = 0; i < index_tracker['word_rune']; i++)
    if (i > index)  $('.i'+i,rune).addClass('non_target_hide');
}

/* ZH RDNG */

function zhRdngInit() {
  var rdng = getPromptValue('word_rdng_value');
  rdng.addClass('target_hide');

  $('.outer', getPromptValue('word_defn_value')).addClass('hide_complete');
  setCharBubbleText('rune');
}

function zhRdngShow(showDefn) {
  if(showDefn) {
    $('.hide_complete',getPromptValue('word_defn_value'))
      .removeClass('hide_complete');
    showDecomp();
    showMnem('word');
  }
  getPromptValue('word_rdng_value').removeClass('target_hide');
}


/* ZH DEFN */

function zhDefnInit(showRdng) {
  $('.outer',getPromptValue('word_defn_value')).addClass('hide_complete');
  if(!showRdng)
    $('*',getPromptValue('word_rdng_value')).addClass('hide_complete');
  setCharBubbleText('rune');
}

function zhDefnShow() {
  $('.outer',getPromptValue('word_defn_value')).removeClass('hide_complete');
  $('*',getPromptValue('word_rdng_value')).removeClass('hide_complete');
  openCharacterBubble();
  showDecomp();
  showMnem('word');
}


/* ZH TONE */

function zhToneInit(index, hide_rdng_after, single_char, hide_defn) {
  if (BLANKS) return;
  //report("zhToneInit:", index, hide_rdng_after, single_char);
  var rdng = getPromptValue('word_rdng_value');
  clearClasses(rdng);
  getPromptValue('char_rune_value').removeClass('target_hide');

  for (var i = 0; i < index_tracker['word_rdng']; i++) {
    $('.i'+i, rdng).toggleClass('deemphasize', i != index)
      .toggleClass("obscured", i > hide_rdng_after);
  }
  if(hide_rdng_after != 9001 && !single_char)
    $('.obscured',rdng).prevAll('.filler:contains("\'")').slice(0,1)
    .addClass('hide_complete');

  if(hide_defn)
    $('.outer', getPromptValue('word_defn_value')).addClass('obscured');

  setCharBubbleText('rune');
}

// Not used for char level any more, just word level.
function zhToneReplace(index, text, kind) {
  if (BLANKS) return;
  var rdng_value = getPromptValue(kind+'_rdng_value');
  colorizeTones($('.i'+index+' .inner', rdng_value).text(text));
  //openCharacterBubble();
}

function zhToneShow(word_too, expose_all_rdngs) {
  var targ = $('.outer:not(.deemphasize)', getPromptValue('word_rdng_value'));
  hiddenRdngShow(expose_all_rdngs ? -1 : targ.prevAll('.outer').length);
  getPromptValue('char_rdng_value').removeClass('obscured');
  if(word_too)
    $('.obscured', getPromptValue('word_defn_value')).removeClass('obscured');
  showMnem('char');
  if(word_too)
    showMnem('word');
  showDecomp();
  getPromptValue('char_rdng_value').removeClass('obscured');
}

/* JA RUNE */

function jaRuneInit(index, hide_rdng) {
  if (BLANKS) return;
  var rune = getPromptValue('word_rune_value');
  clearClasses(rune);
  for (var i = 0; i < index_tracker['word_rune']; i++)
    if (i > index)  $('.i'+i,rune).addClass('non_target_hide');
  $('.i'+index,rune).addClass('target_hide').removeClass('non_target_hide');
  getPromptValue('char_rune_value').addClass('target_hide');

  var rdng = getPromptValue('word_rdng_value');
  $('.i'+index,rdng).addClass('highlight');
  setCharBubbleText(hide_rdng ? "show" : 'rdng');  // maybe hide bubble rdng

  for (i = 0; i < index_tracker['word_rdng']; i++) {
    $('.i'+i, rdng).toggleClass("obscured", hide_rdng && i >= index);
  }
}

function jaRuneShow(instant, index) {
  if (BLANKS) return;
  var prompt = getPrompt();
  var targ = $('.target_hide', prompt).removeClass('target_hide');
  openCharacterBubble(instant);
  //hiddenRdngShow(-1);
  if(!index)
    index = 0;
  var rdng = $('#word_rdng .outer', prompt);
  if(!rdng.hasClass('i' + (index + 1)))   // last one in kana group; show it
    hiddenRdngShow(index);
  showDecomp();
  showMnem('char');
  if(!targ.nextAll('.outer').is('*'))  // last one
    showMnem('word');
}

/* JA RDNG */

function jaRdngInit(no_kanji, defn_to_rdng) {
  $('span', getPromptValue('word_rdng_value')).addClass('hide_complete');
  if(no_kanji)
    $('span', getPromptValue('word_rune_value')).addClass('hide_complete');
  if(!no_kanji && !defn_to_rdng)
    $('.outer', getPromptValue('word_defn_value')).addClass('obscured');
  setCharBubbleText('rune');
}

function jaRdngShow() {
  var prompt = getPrompt();
  $('span', getPromptValue('word_rdng_value')).removeClass('hide_complete');
  $('span', getPromptValue('word_rune_value')).removeClass('hide_complete');
  $('.outer', getPromptValue('word_defn_value')).removeClass('obscured');
  showDecomp();
  showMnem('word');
}

/* JA DEFN */

function jaDefnInit(hide_rdng, no_kanji) {
  $('.outer', getPromptValue('word_defn_value')).addClass('hide_complete');
  if(no_kanji)
    getPromptValue('word_rdng_value').parent().hide();
  else
    getPromptValue('word_rdng_value').toggleClass("obscured", hide_rdng);
  setCharBubbleText('rune');
}

function jaDefnShow() {
  $('.outer', getPromptValue('word_defn_value')).removeClass('hide_complete');
  getPromptValue('word_rdng_value').removeClass("obcured");
  showDecomp();
  showMnem('word');
}

// --------~-------- LIGHT BOXES

function toggleInback(elem,haveit) {
  if      (haveit==true) elem.addClass('inback');
  else if (haveit==false) elem.removeClass('inback');
  else    elem.toggleClass('inback');

  if ($.browser.msie && parseInt($.browser.version) < 8) {
    var value = (elem.hasClass('inback')) ? 'hidden' : 'visible';
    $('*',elem).each(function ()
    {
      if (!$(this).hasClass('hidden'))
	$(this).css('visibility', value);
    });
  }
}

function toggleActiveListsWindow() {
  if (study_mode=="demo") return;
  if ($('#active_lists_box').hasClass('inback')) {
    closeLightBoxes();
    if(typeof pageTracker != 'undefined')
      pageTracker._trackEvent("Clicks", "Active Lists Open", "Study Page");
  }
  toggleInback($('#active_lists_box'));
  $('#active_lists_button')
    .toggleClass('unactive_button')
    .toggleClass('active_button');
  concatListInfos();
  checkLightboxes();
}

function toggleSettingsWindow() {
  if (study_mode=="demo") return;
  if ($('#settings_box').hasClass('inback')) {
    closeLightBoxes();
    if(typeof pageTracker != 'undefined')
      pageTracker._trackEvent("Clicks", "Settings Open", "Study Page");
  }
  toggleInback($('#settings_box'));
  $('#settings_button')
    .toggleClass('unactive_button')
    .toggleClass('active_button');
  checkLightboxes();
  makeCanvasButton($('#settings_box .canvas_button'));
}

function closeLightBoxes() {
  toggleInback($('.litebox'),true);
  $('.tools_settings_button')
    .addClass('unactive_button')
    .removeClass('active_button');
  closeFeedbackBox();
  checkLightboxes(false);
}

function checkLightboxes(open) {
  if (open == null)
    open = $('#settings_box.inback').add('#active_lists_box.inback').length < 2;
  if (open)
    $('body').click(mouseClickCloseBoxes); // enable click off lightboxes
  else
    $('body').unbind('click', mouseClickCloseBoxes);
  if (skritter) {
    skritter.giveMacMouse(open);
    $(skritter).css("visibility", open ? "hidden" : "visible");
  }
}

function mouseClickCloseBoxes(e) {
  var elem = e.target;
  try {
    while (! $(elem).hasClass('litebox') &&
	   ! $(elem).hasClass('tools_settings_button') &&
	   elem.id!='feedback_tab')
      elem = elem.parentNode;
  }
  catch (e) { closeLightBoxes(); }
  return;
}


// --------~-------- LIGHT BOXES - ACTIVE LISTS


// press the play or pause button
function toggleUVLPMode(button, params)
{
  if (!$(button).is('.clickable')) return;
  $('div', button.parentNode).toggleClass("enabled")
    .toggleClass("disabled").toggleClass("clickable");
  var play = ($(button).hasClass('playbutton')) ? 'true' : 'false';
  params += "&add="+play;
  $.retried_post("/toggleuvlpmode", makeUVLPParams(params),
    function(response) {
      if (response == "Add")
	skritter.setAddable(true);
    });
}

function setUVLPMode(row, enabled) {
  var onbutton = (enabled) ? 'play' : 'pause';
  var offbutton = (enabled) ? 'pause' : 'play';
  $('.'+onbutton+'button',row)
    .addClass('enabled').removeClass('clickable').removeClass('disabled');
  $('.'+offbutton+'button',row)
    .removeClass('enabled').addClass('clickable').addClass('disabled');
}

function concatListInfos()
{
  $('#uvliststable span').each(function () { makeIntoOneLine(this); });
}

function makeAutoManualVisualChanges() {
  var auto = $('#id_add_auto').prop('checked');
  $('#add_words_ul input').prop('disabled',!auto);
}

// for Flash
function hasActiveLists() { return $('.playbutton.enabled').length > 0; }
function hasAnyLists() { return $('.playbutton').length > 0; }

function saveAutoAddWords(pref) {
  savePref('auto_add_words', {auto_add_words: pref});
  makeAutoManualVisualChanges();
  skritter.setAddable(pref);
  skritter.settings({auto_add_words: pref});
}
function saveAutoMoveSect(pref) {
  savePref('auto_move_sect', {auto_move_sect: pref}); }

function updateLists(pcts) {
  for (var i = 0, max = pcts.length; i < max; i++) {
    var id = pcts[i][0];
    var pct = pcts[i][1];
    var add = pcts[i][2];
    var finished = pcts[i][3];

    var list = $('.list_row[id="'+id+'"]');

    // update the progress
    $('.std_progress_bar', list).css('width', pct+'%');

    // move between adding and reviewing if necessary
    if (!add && list.hasClass('adding')) { stopAdding(list); }
    if (add && list.hasClass('reviewing')) { restartAdding(list); }

    // if finished, change the text to say so
    if (finished) $('.toggle_adding_cell', list).text('finished');
  }
}


// --------~-------- LIGHT BOXES - SETTINGS


function setOrAppend(params, name, value) {
  if (params[name]==undefined) params[name] = [value];
  else params[name].push(value);
}

function updateTempPartVisuals() {
  var disabled = $('#id_temp_parts_on_0').prop('checked');
  // disable temp study parts that can't be studied
  $('.temp_parts_select li:visible').each(function()
  {
    var input = $('input', $(this));
    if (all_parts.indexOf(input.val()) == -1) {
      $(this).css('text-decoration','line-through');
      input.prop('disabled', true).prop('checked', false);
    }

    else
      input.prop('disabled', disabled);
  });
}

function submitSettings() {
  if ($('#id_temp_parts_on_1').prop('checked')) {
    var cbs=$('.temp_parts_select input:visible:not(:checked):not(:disabled)');
    var visable_cbs=$('.temp_parts_select input:visible');
    // need to unselect some unselectable parts
    if (cbs.length == 0 && visable_cbs.length > 0) {
      alert("To review a subset of parts, please unselect at least one part.");
      return;
    }
    if (visable_cbs.length > 0 && visable_cbs.length == cbs.length) {
      alert("You must select at least one part to temporarily study.");
      return;
    }
  }

  $('#settings_box input[type="button"]')
    .prop('disabled',true)
    .attr('value','Saving');
  $('#settings_box .canvas_button').addClass('disabled');

  var params = {};
  $("input, select", $('#settings_box')).each(function()
    {
      if (this.id.indexOf("id_") == 0) {
	if ($(this).attr("type")=="checkbox") {
	  if ($(this).is(':checked'))
	    setOrAppend(params,$(this).attr('name'),$(this).val());
	}
	else if ($(this).attr("type")=="radio") {
	  if ($(this).prop("checked"))
	    params[$(this).attr('name')] = $(this).val();
	}
	else
	  params[this.id.replace("id_", "")] = this.value;
      }
    });
  $.retried_post("/submitsettings", params, function(response)
  {
    $('#settings_box input[type="button"]')
      .prop('disabled',false)
      .attr('value',(response == "refresh") ? 'Reloading' : 'Save Changes');

    if ($('#id_color_tones').prop('checked'))
      $('#prompt_wrapper').addClass('tones_on');
    else
      $('#prompt_wrapper').removeClass('tones_on');

    if ($('input[name="canvas_right"]:checked').val()=="True")
      $('#interactive_frame').css('float','right');
    else
      $('#interactive_frame').css('float','left');

    if (response=="refresh")
      {
        if(top.location.hash)
          top.location.href = top.location.href.replace(/#.*/, '');
        else
          location.reload(true);
      }
    else {
      closeLightBoxes();
      skritter.settings($.parseJSON(response));
      $('#settings_box .canvas_button').removeClass('disabled');
      $('#settings_box input[type="button"]').prop('disabled',false);
    }
  });
}

function changeLang() {
  $('#settings_box :not(.hidden).'+lang).show();
  $('#settings_box .'+((lang=='zh')?'ja':'zh')).hide();
}

function animSpeedCallback(slider) {
  $('#id_anim_speed').val(slider.value);
}

function orderWeightCallback(slider) {
  $('#id_order_weight').val(slider.value);
}



// --------~-------- LIGHT BOXES - FEEDBACK

var msgs = {
  initial: "Please select a subject.",
  "Stroke Order": "Thanks; we'll investigate this stroke order.",
  "Correction (Reading)": "Thanks; we'll check this reading out.",
  "Correction (Wrong Character)": "Thanks; we'll get the right character in there.",
  "Correction (Decomp)": "Thanks; we'll fix up this decomposition.",
  "Correction (Audio)": "Thanks; we'll look into this audio issue.",
  "Correction (Sentence)": "Thanks; we'll get this sentence fixed.",
  "Study Bug": "Thanks for the bug report; we'll get back to you soon!",
  "Study Suggestion": "Thanks for the feature suggestion!",
  "Comment": "Thank you for your feedback!"
};
var msgs_reversed = {};
for(var msg_subject in msgs)
  msgs_reversed[msgs[msg_subject]] = msg_subject;

function showFeedbackBox() {
  //toggleInback($('#user_inputs_div').show(),false);
  //if (!$.browser.msie || parseInt($.browser.version)>7) $('#screen').show();
  $('#feedback').closest('.popup')
      .removeClass('hidden')
      .find('.popup_close')
        .unbind('click')
        .click(function(e) {
          $('#feedback').closest('.popup').addClass('hidden');
        });
  if($('#feedback').focus().prop("disabled"))
    $('#id_subject').focus();
  if (skritter)
    skritter.giveMacMouse(true);
  if(typeof professor != "undefined")
    professor.init();
}

function closeFeedbackBox() {
  $('#feedback').closest('.popup').addClass('hidden');
  if (skritter)
    skritter.giveMacMouse(false);
}

function changedFeedbackType() {
  var feedback_type = $('#id_subject').val();
  if(feedback_type) {
    $('#feedback').prop('disabled', '').focus();
    if($('#feedback').val() == msgs.initial)
      $('#feedback').val('');
  }
  else
    $('#feedback').prop('disabled', 'disabled');
}

function sendFeedback(professor_suggestions)
{
  var feedbackBox = $('#feedback');
  var msg = feedbackBox.val();
  if(!skritter) {
    $('#feedback_box p.error').show();
    return;
  }
  if(!msg || msgs_reversed[msg])  // Don't send their confirmation message to us
    {
      feedbackBox.focus();
      return;
    }
  skritter.sendComment(msg, $("#id_subject").val(), professor_suggestions);
  feedbackBox.val(msgs[$("#id_subject").val()]);
  setTimeout(function()
  {
    closeFeedbackBox();
    feedbackBox.val("");
  }, 2000);
}

function clearThanks()
{
  var feedbackBox = $('#feedback');
  var msg = feedbackBox.val();
  if (msg == msgs.start || msg == msgs.thanks || msg == msgs.order)
    feedbackBox.val('');
}


// --------~-------- ERROR MESSAGES

function noWords()
{
  $('#no_words').show();
  $("#skritter_window").css("opacity", .1);
}

function needToResetSections() {
  var popup =new Popup('/vocab/list/activate?list='+solo_list+'&page=practice');
  popup.setCallback(startFlash);
  popup.open();
  popup.lock();
}

function flashVersionWarning(version)
{
  var warning = $('#flash_version_warning');
  $('span',warning.show()).text(version);
  warning.fadeOut(10000).click(function() { $(this).remove(); } );
}

function frameRateWarning(browser)
{
  if(browser == "unknown")
    browser = "firefox 3";  // just guess
  var warning = $('#frame_rate_warning').show().click(function() {
    $(this).hide();
  });
  $('.'+browser.split(" ")[0], warning).show();
}

function safariRefreshWarning()
{
  var warning = $('#safari_refresh_warning');
  warning.show();
  warning.fadeOut(10000);
  warning.click(function() { $(this).remove(); } );
}

function siteDisabled()
{
  top.location.href = '/disabled';
}




// --------~-------- UTILITIES


function savePref(pool, prefs, delay)
{
  if (username == "anonymous") return;
  if (!delay) delay = 2000;
  $.delayed_ajax({type: "POST", url: "/save_pref", data: prefs,
		  request_type: pool, delay: delay});
}


function androidLog() {
  var message = [];
  for(var i = 0; i < arguments.length; ++i)
    message.push(arguments[i].toString());
  $('#original_recipe').html(message.join(' ') + '<br/>\n' +
                             $('#original_recipe').html().slice(0, 2000)
                            .replace(/<b?r?\/?$/, ''));
}

/*
 * Useful little object that can be used to retain a limited amount of
 * data. It will store up to size number of objects then begin deleting
 * them based on when it was last put in or accessed. Both put and get
 * are O(n), so it's not particularly efficient.
 */
function DataCache(size) {
  this.dict = {};
  this.queue = [];

  this.getValue = function(key) {
    if (this.dict[key]!=undefined) this.moveToEndOfLine(key);
    return this.dict[key];
  };

  this.moveToEndOfLine = function(key) {
    // need to make sure it's not in the line first
    if (this.dict[key]!=undefined) {
      for (var i=0; i<this.queue.length; i++)
	if (this.queue[i]==key) {
	  this.queue.splice(i,1);
	  break;
	}
    }
    this.queue.push(key);
  };

  this.setValue = function(key, value) {
    this.moveToEndOfLine(key);
    this.dict[key] = value;
    this.trim();
  };

  this.trim = function() {
    while (this.queue.length > size)
      delete this.dict[this.queue.shift()];
  };
}


/* Sliders */

function SliderWidget(div, value, snap_radius, callback) {
  this.value = value;
  this.snap_radius = snap_radius;

  /* setup */

  this.bar = $('<div class="slider_bar"></div>');
  this.knob = $('<div class="slider_knob"></div>');
  div.replaceWith(this.bar.append(this.knob));

  var width = parseInt(this.bar.width());

  var area = $('<div class="slider_select_area"></div>');
  area.css('width',width);
  this.bar.append(area);
  this.value_max = width - 1;

  this.bar.append($('<div class="slider_marker"></div>').css('left',width/2));


  /* functions */

  this.setValue = function(value) {
    this.prev_value = this.value;
    if (Math.abs(value-0.5)<this.snap_radius) value = 0.5;
    if (Math.abs(value-1.0)<this.snap_radius) value = 1.0;
    if (Math.abs(value-0.0)<this.snap_radius) value = 0.0;
    slider.value = value;
    slider.moveKnob();
  };

  this.moveKnob = function() {
    this.knob.css('left',parseInt((this.value || 0) * width));
  };
  this.moveKnob();

  /* event handlers */

  var slider = this;

  this.bar.mousedown(function(e)
  {
    var elem = $(e.target);
    if (!elem.hasClass('slider_select_area')) return;
    var x = e.pageX-elem.offset().left;
    slider.setValue(x/slider.value_max);
    slider.bar.mousemove(slider.mousemove);
    $("body").mouseup(function(e) {
      callback(slider);
      $("body").unbind("mouseup", arguments.callee);
      slider.bar.unbind("mousemove", slider.mousemove);
    });
  });

  this.mousemove = function(e)
  {
    var elem = $(e.target);
    if (!elem.hasClass('slider_select_area')) return;
    var x = e.pageX-elem.offset().left;
    slider.setValue(x/slider.value_max);
  };

}



// --------~-------- NICK'S FLASH STUFF

var skritter;
var wacom = false;

var skritterFocused = false;
var absorbKeys = false;
var SHIFT = false;
var CTRL = false;
var ALT = false;
// These timeouts are to release the modifier key after 5 seconds, because
// Firefox won't give the key up events if you switch to another tab, for
// example (the other tab gets them).
var SHIFT_RELEASE_TIMEOUT = 0;
var CTRL_RELEASE_TIMEOUT = 0;
var ALT_RELEASE_TIMEOUT = 0;


// --------~-------- NICK'S - STARTUP
function setup()
{
  skritter = document.skritter || window.skritter;

  $(document).keydown(browserKeyDown);
  $(document).keyup(browserKeyUp);
  $(document).mouseup(mouseUp);
  //$(document).mousemove(mouseMove);
  if(window.addEventListener)  // Firefox
    window.addEventListener("mousemove", mouseMove, true);
  else
    window.onmousemove = document.onmousemove = mouseMove;

  $('#interactive_frame :first').focusin(function() {
    skritterFocused = true;
    skritter.focused(1);
    //skritter.say("Focus in", 2);
  });
  $('#interactive_frame :first').focusout(function() {
    skritterFocused = false;
    skritter.focused(0);
    //skritter.say("Focus out", 2);
  });
  $('#signin_menu').focusin(function() { absorbKeys = true;} )
    .focusout(function() { absorbKeys = false; } );
  $(window).blur(function() { if(skritter.pageFocusChange)
    skritter.pageFocusChange(false); })
    .focus(function() { if(skritter.pageFocusChange)
      skritter.pageFocusChange(true); });

  if(use_wacom)
    {
      // ------------ IE --//------------------- FireFox --//
      wacom = window.Wacom || document.embeds["wacom-plugin"];
      if(!wacom || !wacom.isWacom)
	{
	  wacom = false;
	  //$("#wacom_plugin_warning").show();
	  // http://www.skritter.com/forum/topic?id=28100992
	  // this thing misfires too often, so let's not show it
	}
    }

  $("#id_subject").change(changedFeedbackType);
  $("#feedback_send").click(sendFeedback);

  if(browser == "safari" && $.browser.safari && parseFloat($.browser.version) > 530 && parseFloat($.browser.version) < 540)
    // I have no idea how to actually detect Safari 5.1; my version is "534.50".
    safariRefreshWarning();

  if(study_mode == "demo")
    $(skritter).css('visibility', 'hidden');  // hide until demo starts
}

// --------~-------- NICK'S - KEYBOARD

// try to grab focus for typing stuff
function focusFlash()
{
  if(!skritterFocused && $.browser.msie)  // only works in IE
    skritter.focus();
}

var z = false;
var r = false;
var zr = false;

function browserKeyDown(evt)
{
  //debug.log("browserKeyDown", evt.keyCode);
  if(absorbKeys)
    return true;
  switch(evt.keyCode)
  {
  case 8:  // backspace
    if(!skritterFocused)
      return false;  // don't let them accidentally go back (rdng typer)
    break;
  case 16:
    SHIFT = true;
    SHIFT_RELEASE_TIMEOUT = setTimeout("SHIFT = false;", 5000);
    break;
  case 17:
    CTRL = true;
    CTRL_RELEASE_TIMEOUT = setTimeout("CTRL = false;", 5000);
    //debug.log("CTRL pressed");
    break;
  case 18:
    ALT = true;
    ALT_RELEASE_TIMEOUT = setTimeout("ALT = false;", 5000);
    break;
  case 32: // space
  case 37: // left arrow
  case 38: // up arrow
  case 39: // right arrow
  case 40: // down arrow
    if(!skritterFocused)
      preventDefault(evt);
    break;
  case 68: // Keyboard.D
  case 75: // Keyboard.K
    if(!ALT && (current_prompt_part != 'rdng' || lang == "ja"))
      {
        if($('.root_popup').is('*'))
          $('.root_popup').data('j_obj').close();
        else
          practiceWordPopup('word');
      }
    break;
  case 82: //Keyboard.R:
    if(zr && z)
      skritter.roll();
    zr = z || zr;
    r = true;
    setTimeout("z = r = zr = false;", 1500);
    break;
  case 90: //Keyboard.Z:
    if(zr && r)
      skritter.roll();
    zr = r || zr;
    z = true;
    setTimeout("z = r = zr = false;", 1500);
    break;
  }
  // if Flash won't get the keypress, pass it on
  if(!skritterFocused)
    skritter.keypress(evt.keyCode, true, CTRL, ALT, SHIFT);
  return true;
}

function browserKeyUp(evt)
{
  //debug.log("browserKeyUp", evt.keyCode);
  if(absorbKeys)
    return;
  switch(evt.keyCode)
  {
  case 16:
    SHIFT = false;
    clearTimeout(SHIFT_RELEASE_TIMEOUT);
    break;
  case 17:
    CTRL = false;
    clearTimeout(CTRL_RELEASE_TIMEOUT);
    //debug.log("CTRL unpressed");
    break;
  case 18:
    ALT = false;
    clearTimeout(ALT_RELEASE_TIMEOUT);
    break;
  case 32: // space
  // case 45: // ins  (# 0)
  // case 35: // end  (# 1)
  // case 40: // down (# 2)
  // case 34: // pgdn (# 3)
  // case 37: // left (# 4)
  // case 12: // ??   (# 5)
    if(!skritterFocused)
      preventDefault(evt);
    break;
  case 82: //Keyboard.R:
    r = false;
    break;
  case 90: //Keyboard.Z:
    z = false;
    break;
  }
  // if Flash won't get the keypress, pass it on
  if(!skritterFocused)
    skritter.keypress(evt.keyCode, false, CTRL, ALT, SHIFT);
}

function preventDefault(evt)
{
  if(evt.preventDefault)
    evt.preventDefault();
  else
    evt.returnValue = false;
}


// --------~-------- NICK'S - MOUSE

function mouseUp(evt)
{
  //var ofs = $(skritter).offset();
  //console.log("x:", evt.pageX - ofs.left, "y:", evt.pageY - ofs.top);
  if(skritter && evt.button != 2)
    setTimeout("skritter.browserMouseUp()",20); //give real clicks precedence
}

var sk_ofs;
var last_pressure = -1;
var points_tracked = [];
function mouseMove(evt)
{
  // recalculate sk_ofs every once a while, but mostly cache it
  if(Math.random() < .01 || sk_ofs == undefined)
    sk_ofs = $(skritter).offset();

  if(!evt)  // IE, fix evt.pageX and evt.pageY
    {
      evt = window.event;
      var doc = document.documentElement, body = document.body;
      evt.pageX = evt.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0);
      evt.pageY = evt.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0);
    }

  //points_tracked.push((new Date()).getTime());
  //if(points_tracked.length > 100) {
  //  var diff = points_tracked[points_tracked.length-1] - points_tracked[0];
  //  $('#original_recipe').text(points_tracked.length / diff * 1000 + " " + skritter.squigReport());
  //  points_tracked = points_tracked.slice(50);
  //}

  if(skritter && !android)
    {
      skritter.browserMouseMove(evt.pageX - sk_ofs.left, evt.pageY -sk_ofs.top);
      if(wacom)
	{
	  var p = wacom.pressure;
	  if(p != last_pressure)
	    skritter.setPressure(p);
	  last_pressure = p;
	}
    }
}

function updatePing(ping, saving_request_count) {
  // Ping from last minute in ms. If no activity in last minute, -1.
  // Also tracks how many outstanding saving requests there are.
  //debug.log("Measured ping:", ping, "requests:", saving_request_count);

  if(admin)
    $('#latency_ping').text(ping == -1 ? '-' : ping + "ms");

  var color;
  var colors = [[0, 'ffffff'], [1000, '8bec00'], [2000, 'c4ff35'],
                [4000, 'eaff15'], [8000, 'ee8820'], [90019001, 'ff1515']];
  for(var i = 0; i < colors.length; ++i)
    if(ping <= colors[i][0])
      break;
  color = '#' + colors[i][1];
  //$('#latency_ping').css('background-color', color);
  $('#latency_ping').data('col', color)
    .unbind('hover')
    .hover(function() { $(this).css('background-color', $(this).data('col')); },
           function() { $(this).css('background-color', '#fff'); });

  $('#saving_requests').text(saving_request_count || "");

  var tooltip = "";
  if(saving_request_count)
    tooltip += 'Saving ' + saving_request_count + ' request' +
    (saving_request_count > 1 ? 's' : '') +
    ' to the server--don\'t leave this page. ';
  if(ping == -1)
    tooltip += 'Connection idle (no recent communication with server).';
  else
    tooltip += 'Average server response is ' + ping + ' milliseconds.';
  $('#latency_indicator').attr('title', tooltip);
};

function channelOpen() {
  debug.log("channel", channel_token, "opened");
};

function channelMessage(message) {
  debug.log("channel message:", message.data);
  eval(message.data);
};

function channelError(error) {
  debug.log("channel error", error.code, error.description);
};

function channelClose() {
  debug.log("channel", channel_token, "closed");
};

function channelBroadcast(message) {
  $("#channel_broadcast").show().html(message).delay(8000).fadeOut(2000);
}

var TimeTracker = {
  setBuckets: function(b) { this.bucket = b; },
  bucket: [100, 500, 1500, 2500, 5000],
  getTimeDiff: function() { return (this.endTime - this.startTime); },
  recordStartTime: function(opt_time) {
    this.startTime = opt_time || (new Date()).getTime();
  },
  recordEndTime: function(opt_time) {
    this.endTime = opt_time || (new Date()).getTime();
  },
  track: function(tracker, opt_event_obj_name, opt_event_label) {
    var eventTracker = tracker._createEventTracker(
      opt_event_obj_name || "TimeTracker");
    var bucketString;
    var t = this.getTimeDiff();
    for(var i = 0; i < this.bucket.length; i++) {
      if(t < this.bucket[i]) {
	if(i == 0) {
          bucketString = "0-" + (this.bucket[0]);
          break;
	} else {
          bucketString = this.bucket[i - 1] + "-" + (this.bucket[i] - 1);
          break;
	}
      }
    }
    if(!bucketString)
      bucketString = this.bucket[i - 1] + "+";
    eventTracker._trackEvent(bucketString, opt_event_label, t);
  }
};

var pageLoadComplete = false;
function trackTimeIfNecessary()
{
  try {
    if(TimeTracker && !pageLoadComplete) {
      TimeTracker.recordEndTime();
      debug.log("practice page load time: "+TimeTracker.getTimeDiff()+"ms");
      TimeTracker.track(pageTracker, "TimeTracker", "Practice Page Load");
    }
  }
  catch(err) {};
  pageLoadComplete = true;
}

function setFontSize(container, chars, kind) {
  var MAX_CHARS = 80;
  var font_size = 20;
  var min_font_size = 16;
  if(kind == "char") {
    font_size = 16;
    min_font_size = 14;
  }
  if(chars > MAX_CHARS) {
    font_size *= MAX_CHARS / chars;
    font_size = Math.max(font_size, min_font_size);
  }
  container.css('font-size', font_size);
  //debug.info("font size:", font_size);
}


