WaniKani Lattice Extension

Extends the WaniKani lattice and allows to choose what data to display

// ==UserScript==
// @name			WaniKani Lattice Extension
// @description		Extends the WaniKani lattice and allows to choose what data to display
// @namespace		irx.wanikani.lattice_extension
// @include			https://www.wanikani.com/
// @include			https://www.wanikani.com/dashboard
// @include			https://www.wanikani.com/review/session*
// @include			https://www.wanikani.com/lattice/radicals/meaning
// @include			https://www.wanikani.com/lattice/kanji/combined
// @include			https://www.wanikani.com/lattice/kanji/meaning
// @include			https://www.wanikani.com/lattice/kanji/reading
// @include			https://www.wanikani.com/lattice/vocabulary/combined
// @include			https://www.wanikani.com/lattice/vocabulary/meaning
// @include			https://www.wanikani.com/lattice/vocabulary/reading
// @version			3.0
// @copyright		2016, Ingo Radax
// @license			MIT; http://opensource.org/licenses/MIT
// @grant			none
// ==/UserScript==

// LZString library from pieroxy
// http://pieroxy.net/blog/pages/lz-string/index.html
// Licence: WTFPL (http://www.wtfpl.net/)
var LZString = (function() {

// private property
var f = String.fromCharCode;
var keyStrBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$";
var baseReverseDic = {};

function getBaseValue(alphabet, character) {
  if (!baseReverseDic[alphabet]) {
    baseReverseDic[alphabet] = {};
    for (var i=0 ; i<alphabet.length ; i++) {
      baseReverseDic[alphabet][alphabet.charAt(i)] = i;
    }
  }
  return baseReverseDic[alphabet][character];
}

var LZString = {
  compressToBase64 : function (input) {
    if (input == null) return "";
    var res = LZString._compress(input, 6, function(a){return keyStrBase64.charAt(a);});
    switch (res.length % 4) { // To produce valid Base64
    default: // When could this happen ?
    case 0 : return res;
    case 1 : return res+"===";
    case 2 : return res+"==";
    case 3 : return res+"=";
    }
  },

  decompressFromBase64 : function (input) {
    if (input == null) return "";
    if (input == "") return null;
    return LZString._decompress(input.length, 32, function(index) { return getBaseValue(keyStrBase64, input.charAt(index)); });
  },

  compressToUTF16 : function (input) {
    if (input == null) return "";
    return LZString._compress(input, 15, function(a){return f(a+32);}) + " ";
  },

  decompressFromUTF16: function (compressed) {
    if (compressed == null) return "";
    if (compressed == "") return null;
    return LZString._decompress(compressed.length, 16384, function(index) { return compressed.charCodeAt(index) - 32; });
  },

  //compress into uint8array (UCS-2 big endian format)
  compressToUint8Array: function (uncompressed) {
    var compressed = LZString.compress(uncompressed);
    var buf=new Uint8Array(compressed.length*2); // 2 bytes per character

    for (var i=0, TotalLen=compressed.length; i<TotalLen; i++) {
      var current_value = compressed.charCodeAt(i);
      buf[i*2] = current_value >>> 8;
      buf[i*2+1] = current_value % 256;
    }
    return buf;
  },

  //decompress from uint8array (UCS-2 big endian format)
  decompressFromUint8Array:function (compressed) {
    if (compressed===null || compressed===undefined){
        return LZString.decompress(compressed);
    } else {
        var buf=new Array(compressed.length/2); // 2 bytes per character
        for (var i=0, TotalLen=buf.length; i<TotalLen; i++) {
          buf[i]=compressed[i*2]*256+compressed[i*2+1];
        }

        var result = [];
        buf.forEach(function (c) {
          result.push(f(c));
        });
        return LZString.decompress(result.join(''));

    }

  },


  //compress into a string that is already URI encoded
  compressToEncodedURIComponent: function (input) {
    if (input == null) return "";
    return LZString._compress(input, 6, function(a){return keyStrUriSafe.charAt(a);});
  },

  //decompress from an output of compressToEncodedURIComponent
  decompressFromEncodedURIComponent:function (input) {
    if (input == null) return "";
    if (input == "") return null;
    input = input.replace(/ /g, "+");
    return LZString._decompress(input.length, 32, function(index) { return getBaseValue(keyStrUriSafe, input.charAt(index)); });
  },

  compress: function (uncompressed) {
    return LZString._compress(uncompressed, 16, function(a){return f(a);});
  },
  _compress: function (uncompressed, bitsPerChar, getCharFromInt) {
    if (uncompressed == null) return "";
    var i, value,
        context_dictionary= {},
        context_dictionaryToCreate= {},
        context_c="",
        context_wc="",
        context_w="",
        context_enlargeIn= 2, // Compensate for the first entry which should not count
        context_dictSize= 3,
        context_numBits= 2,
        context_data=[],
        context_data_val=0,
        context_data_position=0,
        ii;

    for (ii = 0; ii < uncompressed.length; ii += 1) {
      context_c = uncompressed.charAt(ii);
      if (!Object.prototype.hasOwnProperty.call(context_dictionary,context_c)) {
        context_dictionary[context_c] = context_dictSize++;
        context_dictionaryToCreate[context_c] = true;
      }

      context_wc = context_w + context_c;
      if (Object.prototype.hasOwnProperty.call(context_dictionary,context_wc)) {
        context_w = context_wc;
      } else {
        if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate,context_w)) {
          if (context_w.charCodeAt(0)<256) {
            for (i=0 ; i<context_numBits ; i++) {
              context_data_val = (context_data_val << 1);
              if (context_data_position == bitsPerChar-1) {
                context_data_position = 0;
                context_data.push(getCharFromInt(context_data_val));
                context_data_val = 0;
              } else {
                context_data_position++;
              }
            }
            value = context_w.charCodeAt(0);
            for (i=0 ; i<8 ; i++) {
              context_data_val = (context_data_val << 1) | (value&1);
              if (context_data_position == bitsPerChar-1) {
                context_data_position = 0;
                context_data.push(getCharFromInt(context_data_val));
                context_data_val = 0;
              } else {
                context_data_position++;
              }
              value = value >> 1;
            }
          } else {
            value = 1;
            for (i=0 ; i<context_numBits ; i++) {
              context_data_val = (context_data_val << 1) | value;
              if (context_data_position ==bitsPerChar-1) {
                context_data_position = 0;
                context_data.push(getCharFromInt(context_data_val));
                context_data_val = 0;
              } else {
                context_data_position++;
              }
              value = 0;
            }
            value = context_w.charCodeAt(0);
            for (i=0 ; i<16 ; i++) {
              context_data_val = (context_data_val << 1) | (value&1);
              if (context_data_position == bitsPerChar-1) {
                context_data_position = 0;
                context_data.push(getCharFromInt(context_data_val));
                context_data_val = 0;
              } else {
                context_data_position++;
              }
              value = value >> 1;
            }
          }
          context_enlargeIn--;
          if (context_enlargeIn == 0) {
            context_enlargeIn = Math.pow(2, context_numBits);
            context_numBits++;
          }
          delete context_dictionaryToCreate[context_w];
        } else {
          value = context_dictionary[context_w];
          for (i=0 ; i<context_numBits ; i++) {
            context_data_val = (context_data_val << 1) | (value&1);
            if (context_data_position == bitsPerChar-1) {
              context_data_position = 0;
              context_data.push(getCharFromInt(context_data_val));
              context_data_val = 0;
            } else {
              context_data_position++;
            }
            value = value >> 1;
          }


        }
        context_enlargeIn--;
        if (context_enlargeIn == 0) {
          context_enlargeIn = Math.pow(2, context_numBits);
          context_numBits++;
        }
        // Add wc to the dictionary.
        context_dictionary[context_wc] = context_dictSize++;
        context_w = String(context_c);
      }
    }

    // Output the code for w.
    if (context_w !== "") {
      if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate,context_w)) {
        if (context_w.charCodeAt(0)<256) {
          for (i=0 ; i<context_numBits ; i++) {
            context_data_val = (context_data_val << 1);
            if (context_data_position == bitsPerChar-1) {
              context_data_position = 0;
              context_data.push(getCharFromInt(context_data_val));
              context_data_val = 0;
            } else {
              context_data_position++;
            }
          }
          value = context_w.charCodeAt(0);
          for (i=0 ; i<8 ; i++) {
            context_data_val = (context_data_val << 1) | (value&1);
            if (context_data_position == bitsPerChar-1) {
              context_data_position = 0;
              context_data.push(getCharFromInt(context_data_val));
              context_data_val = 0;
            } else {
              context_data_position++;
            }
            value = value >> 1;
          }
        } else {
          value = 1;
          for (i=0 ; i<context_numBits ; i++) {
            context_data_val = (context_data_val << 1) | value;
            if (context_data_position == bitsPerChar-1) {
              context_data_position = 0;
              context_data.push(getCharFromInt(context_data_val));
              context_data_val = 0;
            } else {
              context_data_position++;
            }
            value = 0;
          }
          value = context_w.charCodeAt(0);
          for (i=0 ; i<16 ; i++) {
            context_data_val = (context_data_val << 1) | (value&1);
            if (context_data_position == bitsPerChar-1) {
              context_data_position = 0;
              context_data.push(getCharFromInt(context_data_val));
              context_data_val = 0;
            } else {
              context_data_position++;
            }
            value = value >> 1;
          }
        }
        context_enlargeIn--;
        if (context_enlargeIn == 0) {
          context_enlargeIn = Math.pow(2, context_numBits);
          context_numBits++;
        }
        delete context_dictionaryToCreate[context_w];
      } else {
        value = context_dictionary[context_w];
        for (i=0 ; i<context_numBits ; i++) {
          context_data_val = (context_data_val << 1) | (value&1);
          if (context_data_position == bitsPerChar-1) {
            context_data_position = 0;
            context_data.push(getCharFromInt(context_data_val));
            context_data_val = 0;
          } else {
            context_data_position++;
          }
          value = value >> 1;
        }


      }
      context_enlargeIn--;
      if (context_enlargeIn == 0) {
        context_enlargeIn = Math.pow(2, context_numBits);
        context_numBits++;
      }
    }

    // Mark the end of the stream
    value = 2;
    for (i=0 ; i<context_numBits ; i++) {
      context_data_val = (context_data_val << 1) | (value&1);
      if (context_data_position == bitsPerChar-1) {
        context_data_position = 0;
        context_data.push(getCharFromInt(context_data_val));
        context_data_val = 0;
      } else {
        context_data_position++;
      }
      value = value >> 1;
    }

    // Flush the last char
    while (true) {
      context_data_val = (context_data_val << 1);
      if (context_data_position == bitsPerChar-1) {
        context_data.push(getCharFromInt(context_data_val));
        break;
      }
      else context_data_position++;
    }
    return context_data.join('');
  },

  decompress: function (compressed) {
    if (compressed == null) return "";
    if (compressed == "") return null;
    return LZString._decompress(compressed.length, 32768, function(index) { return compressed.charCodeAt(index); });
  },

  _decompress: function (length, resetValue, getNextValue) {
    var dictionary = [],
        next,
        enlargeIn = 4,
        dictSize = 4,
        numBits = 3,
        entry = "",
        result = [],
        i,
        w,
        bits, resb, maxpower, power,
        c,
        data = {val:getNextValue(0), position:resetValue, index:1};

    for (i = 0; i < 3; i += 1) {
      dictionary[i] = i;
    }

    bits = 0;
    maxpower = Math.pow(2,2);
    power=1;
    while (power!=maxpower) {
      resb = data.val & data.position;
      data.position >>= 1;
      if (data.position == 0) {
        data.position = resetValue;
        data.val = getNextValue(data.index++);
      }
      bits |= (resb>0 ? 1 : 0) * power;
      power <<= 1;
    }

    switch (next = bits) {
      case 0:
          bits = 0;
          maxpower = Math.pow(2,8);
          power=1;
          while (power!=maxpower) {
            resb = data.val & data.position;
            data.position >>= 1;
            if (data.position == 0) {
              data.position = resetValue;
              data.val = getNextValue(data.index++);
            }
            bits |= (resb>0 ? 1 : 0) * power;
            power <<= 1;
          }
        c = f(bits);
        break;
      case 1:
          bits = 0;
          maxpower = Math.pow(2,16);
          power=1;
          while (power!=maxpower) {
            resb = data.val & data.position;
            data.position >>= 1;
            if (data.position == 0) {
              data.position = resetValue;
              data.val = getNextValue(data.index++);
            }
            bits |= (resb>0 ? 1 : 0) * power;
            power <<= 1;
          }
        c = f(bits);
        break;
      case 2:
        return "";
    }
    dictionary[3] = c;
    w = c;
    result.push(c);
    while (true) {
      if (data.index > length) {
        return "";
      }

      bits = 0;
      maxpower = Math.pow(2,numBits);
      power=1;
      while (power!=maxpower) {
        resb = data.val & data.position;
        data.position >>= 1;
        if (data.position == 0) {
          data.position = resetValue;
          data.val = getNextValue(data.index++);
        }
        bits |= (resb>0 ? 1 : 0) * power;
        power <<= 1;
      }

      switch (c = bits) {
        case 0:
          bits = 0;
          maxpower = Math.pow(2,8);
          power=1;
          while (power!=maxpower) {
            resb = data.val & data.position;
            data.position >>= 1;
            if (data.position == 0) {
              data.position = resetValue;
              data.val = getNextValue(data.index++);
            }
            bits |= (resb>0 ? 1 : 0) * power;
            power <<= 1;
          }

          dictionary[dictSize++] = f(bits);
          c = dictSize-1;
          enlargeIn--;
          break;
        case 1:
          bits = 0;
          maxpower = Math.pow(2,16);
          power=1;
          while (power!=maxpower) {
            resb = data.val & data.position;
            data.position >>= 1;
            if (data.position == 0) {
              data.position = resetValue;
              data.val = getNextValue(data.index++);
            }
            bits |= (resb>0 ? 1 : 0) * power;
            power <<= 1;
          }
          dictionary[dictSize++] = f(bits);
          c = dictSize-1;
          enlargeIn--;
          break;
        case 2:
          return result.join('');
      }

      if (enlargeIn == 0) {
        enlargeIn = Math.pow(2, numBits);
        numBits++;
      }

      if (dictionary[c]) {
        entry = dictionary[c];
      } else {
        if (c === dictSize) {
          entry = w + w.charAt(0);
        } else {
          return null;
        }
      }
      result.push(entry);

      // Add w+entry[0] to the dictionary.
      dictionary[dictSize++] = w + entry.charAt(0);
      enlargeIn--;

      w = entry;

      if (enlargeIn == 0) {
        enlargeIn = Math.pow(2, numBits);
        numBits++;
      }

    }
  }
};
  return LZString;
})();

var WaniKani = (function() {
	var local_storage_prefix = 'wk_toolkit_';
	
	var api_key = null;
	
	var radical_data = null;
	var kanji_data = null;
	var vocabulary_data = null;

	function log(msg) {
		console.log(msg);
	}
	
	function is_on_wanikani() {
		return (window.location.host == 'www.wanikani.com');
	}
	
	function is_on_dashboard() {
		return is_on_wanikani() && ((window.location.pathname == '/dashboard') || (window.location.pathname == '/'));
	}
	
	function is_on_review_session_page() {
		return is_on_wanikani() && (window.location.pathname == '/review/session');
	}
	
	function is_on_review_page() {
		return is_on_wanikani() && (window.location.pathname == '/review');
	}
	
	function is_on_lesson_session_page() {
		return is_on_wanikani() && (window.location.pathname == '/lesson/session');
	}
	
	function is_on_lesson_page() {
		return is_on_wanikani() && (window.location.pathname == '/lesson');
	}
	
	function is_on_lattice_radicals_meaning() {
		return is_on_wanikani() && (window.location.pathname == '/lattice/radicals/meaning');
	}
	
	function is_on_lattice_radicals_progress() {
		return is_on_wanikani() && (window.location.pathname == '/lattice/radicals/progress');
	}
	
	function is_on_lattice_kanji_combined() {
		return is_on_wanikani() && (window.location.pathname == '/lattice/kanji/combined');
	}
	
	function is_on_lattice_kanji_meaning() {
		return is_on_wanikani() && (window.location.pathname == '/lattice/kanji/meaning');
	}
	
	function is_on_lattice_kanji_reading() {
		return is_on_wanikani() && (window.location.pathname == '/lattice/kanji/reading');
	}
	
	function is_on_lattice_kanji_progress() {
		return is_on_wanikani() && (window.location.pathname == '/lattice/kanji/status');
	}
	
	function is_on_lattice_vocabulary_combined() {
		return is_on_wanikani() && (window.location.pathname == '/lattice/vocabulary/combined');
	}
	
	function is_on_lattice_vocabulary_meaning() {
		return is_on_wanikani() && (window.location.pathname == '/lattice/vocabulary/meaning');
	}
	
	function is_on_lattice_vocabulary_reading() {
		return is_on_wanikani() && (window.location.pathname == '/lattice/vocabulary/reading');
	}
	
	function is_on_lattice_vocabulary_progress() {
		return is_on_wanikani() && (window.location.pathname == '/lattice/vocabulary/status');
	}
	
	//-------------------------------------------------------------------
	// Try to parse the url and detect if it belongs to a single item.
	// e.g.	'https://www.wanikani.com/level/1/radicals/construction'
	//		will be parsed as 'radicals' and 'construction'
	//-------------------------------------------------------------------
	function parse_item_url(url) {
		url = decodeURI(url);
		var parsed = /.*\/(radicals|kanji|vocabulary)\/(.+)/.exec(url);
		if (parsed) {
			return {type:parsed[1], name:parsed[2]};
		}
		else {
			return null;
		}
	}
	
	function clear_local_storage() {
		localStorage.removeItem(local_storage_prefix + 'last_review_time');
		localStorage.removeItem(local_storage_prefix + 'next_review_time');
		localStorage.removeItem(local_storage_prefix + 'last_unlock_time');
		localStorage.removeItem(local_storage_prefix + 'api_key');
		localStorage.removeItem(local_storage_prefix + 'api_user_radicals');
		localStorage.removeItem(local_storage_prefix + 'api_user_radicals_fetch_time');
		localStorage.removeItem(local_storage_prefix + 'api_user_kanji');
		localStorage.removeItem(local_storage_prefix + 'api_user_kanji_fetch_time');
		localStorage.removeItem(local_storage_prefix + 'api_user_vocabulary');
		localStorage.removeItem(local_storage_prefix + 'api_user_vocabulary_fetch_time');
	}
		
	function track_times() {
		if (is_on_review_session_page()) {
			localStorage.setItem(local_storage_prefix + 'last_review_time', now());
			
			var lastUnlockTime = new Date($('.recent-unlocks time:nth(0)').attr('datetime'))/1000;
			localStorage.setItem(local_storage_prefix + 'last_unlock_time', now());
		}
		
		if (is_on_dashboard()) {
			var next_review = Number($('.review-status .timeago').attr('datetime'));
			// Workaround for "WaniKani Real Times" script, which deletes the element we were looking for above.
			if (isNaN(next_review)) {
				next_review = Number($('.review-status time1').attr('datetime'));
				// Conditional divide-by-1000, in case someone fixed this error in Real Times script.
				if (next_review > 10000000000) next_review /= 1000;
			}
			localStorage.setItem(local_storage_prefix + 'next_review_time', next_review);
		}
	}
	
	function get_last_review_time() {
		return Number(localStorage.getItem(local_storage_prefix + 'last_review_time') || 0);
	}
	
	function get_next_review_time() {
		return Number(localStorage.getItem(local_storage_prefix + 'next_review_time') || 0);
	}
	
	function get_last_unlock_time() {
		return Number(localStorage.getItem(local_storage_prefix + 'last_unlock_time') || 0);
	}

	function now() {
		return Math.floor(new Date() / 1000);
	}
	
	function ajax_retry(url, retries, timeout) {
		retries = retries || 2;
		timeout = timeout || 3000;
		function action(resolve, reject) {
			$.ajax({
				url: url,
				timeout: timeout
			})
			.done(function(data, status){
				if (status === 'success')
					resolve(data);
				else
					reject();
			})
			.fail(function(xhr, status, error){
				if (status === 'error' && --retries > 0)
					action(resolve, reject);
				else
					reject();
			});
		}
		return new Promise(action);
	}

	function get_api_key() {
		return new Promise(function(resolve, reject) {
			api_key = localStorage.getItem(local_storage_prefix + 'api_key');
			if (typeof api_key === 'string' && api_key.length == 32) {
				log("Already having API key");
				return resolve();	
			}
			
			log("Loading API key");
			
			ajax_retry('/account').then(function(page) {
				
				log("Loading API key ... SUCCESS");
				
				// --[ SUCCESS ]----------------------
				// Make sure what we got is a web page.
				if (typeof page !== 'string') {return reject();}

				// Extract the user name.
				page = $(page);
				
				// Extract the API key.
				api_key = page.find('#api-button').parent().find('input').attr('value');
				if (typeof api_key !== 'string' || api_key.length !== 32)  {return reject();}

				localStorage.setItem(local_storage_prefix + 'api_key', api_key);
				resolve();

			},function(result) {
				
				log("Loading API key ... ERROR");
				
				// --[ FAIL ]-------------------------
				reject(new Error('Failed to fetch API key!'));
				
			});
		});
	}
	
	function call_api_user_radicals() {
		return new Promise(function(resolve, reject) {
			log("Calling API: User radicals");
			$.getJSON('/api/user/' + api_key + '/radicals/', function(json){
				if (json.error && json.error.code === 'user_not_found') {
					log("Calling API: User radicals ... ERROR")
					localStorage.removeItem(local_storage_prefix + 'api_user_radicals');
					localStorage.removeItem(local_storage_prefix + 'api_user_radicals_fetch_time');
					localStorage.removeItem(local_storage_prefix + 'api_key');
					location.reload();
					reject();
					return;
				}

				log("Calling API: User radicals ... SUCCESS");
				
				localStorage.setItem(local_storage_prefix + 'api_user_radicals', JSON.stringify(json));
				localStorage.setItem(local_storage_prefix + 'api_user_radicals_fetch_time', now());
				
				radical_data = json;
				
				resolve();
			});
		});
	}
	
	function call_api_user_kanji() {
		return new Promise(function(resolve, reject) {
			log("Calling API: User kanji");
			$.getJSON('/api/user/' + api_key + '/kanji/', function(json){
				if (json.error && json.error.code === 'user_not_found') {
					log("Calling API: User kanji ... ERROR")
					localStorage.removeItem(local_storage_prefix + 'api_user_kanji');
					localStorage.removeItem(local_storage_prefix + 'api_user_kanji_fetch_time');
					localStorage.removeItem(local_storage_prefix + 'api_key');
					location.reload();
					reject();
					return;
				}

				log("Calling API: User kanji ... SUCCESS");
				
				localStorage.setItem(local_storage_prefix + 'api_user_kanji', JSON.stringify(json));
				localStorage.setItem(local_storage_prefix + 'api_user_kanji_fetch_time', now());
				
				kanji_data = json;
				
				resolve();
			});
		});
	}
	
	function call_api_user_vocabulary() {
		return new Promise(function(resolve, reject) {
			log("Calling API: User vocabulary");
			$.getJSON('/api/user/' + api_key + '/vocabulary/', function(json){
				if (json.error && json.error.code === 'user_not_found') {
					log("Calling API: User vocabulary ... ERROR")
					localStorage.removeItem(local_storage_prefix + 'api_user_vocabulary');
					localStorage.removeItem(local_storage_prefix + 'api_user_vocabulary_fetch_time');
					localStorage.removeItem(local_storage_prefix + 'api_key');
					location.reload();
					reject();
					return;
				}

				log("Calling API: User vocabulary ... SUCCESS");
				
				localStorage.setItem(local_storage_prefix + 'api_user_vocabulary', JSON.stringify(json));
				localStorage.setItem(local_storage_prefix + 'api_user_vocabulary_fetch_time', now());
				
				vocabulary_data = json;
				
				resolve();
			});
		});
	}
	
	function get_last_fetch_time_api_user_radicals() {
		return Number(localStorage.getItem(local_storage_prefix + 'api_user_radicals_fetch_time'));
	}
	
	function get_last_fetch_time_api_user_kanji() {
		return Number(localStorage.getItem(local_storage_prefix + 'api_user_kanji_fetch_time'));
	}
	
	function get_last_fetch_time_api_user_vocabulary() {
		return Number(localStorage.getItem(local_storage_prefix + 'api_user_vocabulary_fetch_time'));
	}
	
	function load_radical_data() {
        var next_review_time = get_next_review_time();
		var last_review_time = get_last_review_time();
		var last_unlock_time = get_last_unlock_time();
		var last_fetch_time = get_last_fetch_time_api_user_radicals();
        if ((last_fetch_time <= last_unlock_time) ||
			(last_fetch_time <= last_review_time) ||
			((next_review_time < now()) && (last_fetch_time <= (now() - 3600)))) {
			log("Clearing previous fetched radical data");
			radical_data = null;
			localStorage.removeItem(local_storage_prefix + 'api_user_radicals');
			localStorage.removeItem(local_storage_prefix + 'api_user_radicals_fetch_time');
		}
		
		if (radical_data == null) {
			var stringified = localStorage.getItem(local_storage_prefix + 'api_user_radicals');
			if (stringified != null) {
				log("Radical data loaded from local storage");
				radical_data = JSON.parse(stringified);
			}
		}
		
		if (radical_data != null) {
			log("Radical data already loaded");
			return Promise.resolve();
		}
		
		return new Promise(function(resolve, reject) {
			get_api_key()
				.then(call_api_user_radicals)
				.then(function() {
					resolve();
				});
		});
	}
	
	function load_kanji_data() {
        var next_review_time = get_next_review_time();
		var last_review_time = get_last_review_time();
		var last_unlock_time = get_last_unlock_time();
		var last_fetch_time = get_last_fetch_time_api_user_kanji();
        if ((last_fetch_time <= last_unlock_time) ||
			(last_fetch_time <= last_review_time) ||
			((next_review_time < now()) && (last_fetch_time <= (now() - 3600)))) {
			log("Clearing previous fetched kanji data");
			kanji_data = null;
			localStorage.removeItem(local_storage_prefix + 'api_user_kanji');
			localStorage.removeItem(local_storage_prefix + 'api_user_kanji_fetch_time');
		}
		
		if (kanji_data == null) {
			var stringified = localStorage.getItem(local_storage_prefix + 'api_user_kanji');
			if (stringified != null) {
				log("Kanji data loaded from local storage");
				kanji_data = JSON.parse(stringified);
			}
		}
		
		if (kanji_data != null) {
			log("Kanji data already loaded");
			return Promise.resolve();
		}
		
		return new Promise(function(resolve, reject) {
			get_api_key()
				.then(call_api_user_kanji)
				.then(function() {
					resolve();
				});
		});
	}
	
	function load_vocabulary_data() {
        var next_review_time = get_next_review_time();
		var last_review_time = get_last_review_time();
		var last_unlock_time = get_last_unlock_time();
		var last_fetch_time = get_last_fetch_time_api_user_vocabulary();
        if ((last_fetch_time <= last_unlock_time) ||
			(last_fetch_time <= last_review_time) ||
			((next_review_time < now()) && (last_fetch_time <= (now() - 3600)))) {
			log("Clearing previous fetched vocabulary data");
			vocabulary_data = null;
			localStorage.removeItem(local_storage_prefix + 'api_user_vocabulary');
			localStorage.removeItem(local_storage_prefix + 'api_user_vocabulary_fetch_time');
		}
		
		if (vocabulary_data == null) {
			var stringified = localStorage.getItem(local_storage_prefix + 'api_user_vocabulary');
			if (stringified != null) {
				log("Vocabulary data loaded from local storage");
				vocabulary_data = JSON.parse(stringified);
			}
		}
		
		if (vocabulary_data != null) {
			log("Vocabulary data already loaded");
			return Promise.resolve();
		}
		
		return new Promise(function(resolve, reject) {
			get_api_key()
				.then(call_api_user_vocabulary)
				.then(function() {
					resolve();
				});
		});
	}
	
	function get_radical_data() {
		return radical_data;
	}
	
	function get_kanji_data() {
		return kanji_data;
	}
	
	function get_vocabulary_data() {
		return vocabulary_data;
	}
	
	function find_radical(meaning) {
		if (radical_data ==  null) {
			return null;
		}
		
		var numRadicals = radical_data.requested_information.length;
		for (var i = 0; i < numRadicals; i++) {
			if (radical_data.requested_information[i].meaning == meaning) {
				return radical_data.requested_information[i];
			}
		}
		
		return null;
	}
	
	function find_kanji(character) {
		if (kanji_data ==  null) {
			return null;
		}
		
		var numKanji = kanji_data.requested_information.length;
		for (var i = 0; i < numKanji; i++) {
			if (kanji_data.requested_information[i].character == character) {
				return kanji_data.requested_information[i];
			}
		}
		
		return null;
	}
	
	function find_vocabulary(character) {
		if (vocabulary_data ==  null) {
			return null;
		}
		
		var numVocabulary = vocabulary_data.requested_information.general.length;
		for (var i = 0; i < numVocabulary; i++) {
			if (vocabulary_data.requested_information.general[i].character == character) {
				return vocabulary_data.requested_information.general[i];
			}
		}
		
		return null;
	}
	
	function find_item(type, name) {
		if (type == 'radicals') {
			return find_radical(name);
		}
		else if(type == 'kanji') {
			return find_kanji(name);
		}
		else if(type == 'vocabulary') {
			return find_vocabulary(name);
		}
		else {
			return null;
		}
	}
	
	return {
		is_on_wanikani: is_on_wanikani,
		is_on_dashboard: is_on_dashboard,
		is_on_review_session_page: is_on_review_session_page,
		is_on_review_page: is_on_review_page,
		is_on_lesson_session_page: is_on_lesson_session_page,
		is_on_lesson_page: is_on_lesson_page,
		is_on_lattice_radicals_meaning: is_on_lattice_radicals_meaning,
		is_on_lattice_radicals_progress: is_on_lattice_radicals_progress,
		is_on_lattice_kanji_combined: is_on_lattice_kanji_combined,
		is_on_lattice_kanji_meaning: is_on_lattice_kanji_meaning,
		is_on_lattice_kanji_reading: is_on_lattice_kanji_reading,
		is_on_lattice_kanji_progress: is_on_lattice_kanji_progress,
		is_on_lattice_vocabulary_combined: is_on_lattice_vocabulary_combined,
		is_on_lattice_vocabulary_meaning: is_on_lattice_vocabulary_meaning,
		is_on_lattice_vocabulary_reading: is_on_lattice_vocabulary_reading,
		is_on_lattice_vocabulary_progress: is_on_lattice_vocabulary_progress,
		parse_item_url: parse_item_url,
		clear_local_storage: clear_local_storage,
		track_times: track_times,
		get_last_review_time: get_last_review_time,
		get_next_review_time: get_next_review_time,
		load_radical_data: load_radical_data,
		get_radical_data: get_radical_data,
		find_radical: find_radical,
		load_kanji_data: load_kanji_data,
		get_kanji_data: get_kanji_data,
		find_kanji: find_kanji,
		load_vocabulary_data: load_vocabulary_data,
		get_vocabulary_data: get_vocabulary_data,
		find_vocabulary: find_vocabulary,
		find_item: find_item,
	};
})();

var UI = (function() {
	function addStyle(aCss) {
		var head, style;
		head = document.getElementsByTagName('head')[0];
		if (head) {
			style = document.createElement('style');
			style.setAttribute('type', 'text/css');
			style.textContent = aCss;
			head.appendChild(style);
			return style;
		}
		return null;
	}
	
	function initCss() {
		var css =
			'#lattice_extension {' +
			'  display:none;' +
			'}' +
			'#lattice_extension h3 {' +
			'  height: 24px;' +
	        '  margin-top: 10px;' +
			'  margin-bottom: 0px;' +
			'  padding: 5px 20px;' +
			'  border-radius: 5px 5px 0 0;' +
	        '  background-color: seagreen;' +
	        '  color: white;' +
	        '  text-shadow: none;' +
			'}' +
			'#lattice_extension section {' +
			'  background-color: lightgrey;' +
			'}' +
			'#lattice_extension table {' +
			'}' +
			'#lattice_extension td {' +
			'  padding: 2px 8px;' +
			'}' + 
			'#lattice_extension .close_button {' +
			'  float: right;' +
			'  height: 24px;' +
			'}'
			;
		
		addStyle(css);
	}
	
	function buildIdAttr(id) {
		if (id && id != '')
			return 'id="' + id + '"';
		return '';
	}
	
	function addOnChangeListener(id, data, listener) {
		$('#' + id).off('change');
		$('#' + id).on('change', data, listener);
	}
	
	function addOnClickListener(id, data, listener) {
		$('#' + id).off('click');
		$('#' + id).on('click', data, listener);
	}
	
	function buildWindow(id, title) {
		var html =
			'<div ' + buildIdAttr(id) + ' class="container">' +
				'<div class="row">' +
					'<div class="span12" >' +
						'<section id="' + id + '_body">' +
							'<h3>' +
								title +
								'<button class="close_button" " ' + buildIdAttr(id + '_close_btn') + '>Close</button>' +
							'</h3>' +
						'</section>' +
					'</div>' +
				'</div>' +
			'</div>'
			;
		return html;
	}
	
	function buildTable(id) {
		var html = 
			'<table ' + buildIdAttr(id) + '>' +
				'<tbody ' + buildIdAttr(id + '_body') + '>' +
				'</tbody>' +
			'</table>';
		return html;
	}
	
	function buildTableRow(id, column1, column2, column3 = null) {
		var html =
			'<tr' + buildIdAttr(id) + '>' +
				'<td>' + column1 + '</td>' +
				'<td>' + column2 + '</td>' +
				(column3 == null ? '' : '<td>' + column3 + '</td>') +
			'</tr>';
		return html;
	}
	
	function buildSelection(id, tooltip) {
		var html =
			'<select ' + buildIdAttr(id) + ' class="input" name="' + id + '" title="' + tooltip + '" />';
		return html;
	}
	
	function addSelectionOption(selectId, value, label, selected) {
		$('#' + selectId).append(
				'<option value="' + value + '" ' + (selected ? 'selected' : '') + '>' +
					label +
				'</option>');
	}
	
	function buildCheckBox(id, checked) {
		var html =
			'<input ' + buildIdAttr(id) + ' type="checkbox" ' + (checked ? 'checked' : '') + '>';
		return html;
	}
	
	function buildButton(id, label) {
		var html =
			'<button ' + buildIdAttr(id) + '>' + label + '</button>';
		return html;
	}
	
	return {
		initCss: initCss,
		buildWindow: buildWindow,
		buildTable: buildTable,
		buildTableRow: buildTableRow,
		buildCheckBox: buildCheckBox,
		buildButton: buildButton,
		buildSelection: buildSelection,
		addSelectionOption: addSelectionOption,
		addOnChangeListener: addOnChangeListener,
		addOnClickListener: addOnClickListener,
	};
})();

(function(gobj) {
	
	var settingsWindowAdded = false;
	var dropDownMenuExtended = false;
	
	var localStoragePrefix = 'LatticeExtension_';
	
	var loaded_snapshot = {id:0};
	
	var snapshot_index = {
		next_id: 1,
		snapshots: []
	};
	
	var max_num_snapshots = 5;
	
	var statistics = {};
	
	var classification_thresholds = {};
	
	var radicals_data_labels = {
		meaning_percent_answered_correct: "Meaning (% correct)",
		meaning_correct: "Meaning (correct)",
		meaning_incorrect: "Meaning (incorrect)",
		meaning_total: "Meaning (correct + incorrect)",
		meaning_current_streak: "Meaning (current streak)",
		meaning_max_streak: "Meaning (max streak)",
	};
	
	var kanji_data_labels = {
		combined_percent_answered_correct: "Combined (% correct)",
		combined_correct: "Combined (correct)",
		combined_incorrect: "Combined (incorrect)",
		combined_total: "Combined (correct + incorrect)",
		meaning_percent_answered_correct: "Meaning (% correct)",
		meaning_correct: "Meaning (correct)",
		meaning_incorrect: "Meaning (incorrect)",
		meaning_total: "Meaning (correct + incorrect)",
		meaning_current_streak: "Meaning (current streak)",
		meaning_max_streak: "Meaning (max streak)",
		reading_percent_answered_correct: "Reading (% correct)",
		reading_correct: "Reading (correct)",
		reading_incorrect: "Reading (incorrect)",
		reading_total: "Reading (correct + incorrect)",
		reading_current_streak: "Reading (current streak)",
		reading_max_streak: "Reading (max streak)",
	};
	
	var vocabulary_data_labels = {
		combined_percent_answered_correct: "Combined (% correct)",
		combined_correct: "Combined (correct)",
		combined_incorrect: "Combined (incorrect)",
		combined_total: "Combined (correct + incorrect)",
		meaning_percent_answered_correct: "Meaning (% correct)",
		meaning_correct: "Meaning (correct)",
		meaning_incorrect: "Meaning (incorrect)",
		meaning_total: "Meaning (correct + incorrect)",
		meaning_current_streak: "Meaning (current streak)",
		meaning_max_streak: "Meaning (max streak)",
		reading_percent_answered_correct: "Reading (% correct)",
		reading_correct: "Reading (correct)",
		reading_incorrect: "Reading (incorrect)",
		reading_total: "Reading (correct + incorrect)",
		reading_current_streak: "Reading (current streak)",
		reading_max_streak: "Reading (max streak)",
	};
	
	var used_labels = null;
	
	function setupStatistics() {
		for(var key in used_labels){
			statistics[key] = {min: 10000, max:-10000};
		}
	}
	
	function removeText(jq_expression) {
		$(jq_expression).contents().filter(function () {
			return this.nodeType === 3; 
		}).remove();
	}
	
	function calculate_classification_thresholds(min, max) {
		if (max - min < 5) {
			if (max >= 4) {
				classification_thresholds = {
					t0: max - 4,
					t1: max - 4,
					t2: max - 3,
					t3: max - 2,
					t4: max - 1,
					t5: max,
				};
			}
			else {
				classification_thresholds = {
					t0: min,
					t1: min,
					t2: min + 1,
					t3: min + 2,
					t4: min + 3,
					t5: min + 4,
				};
			}
		}
		else {
			var len = (max - min) / 5;
			classification_thresholds = {
				t0: min,
				t1: Math.floor(min + len),
				t2: Math.floor(min + 2 * len),
				t3: Math.floor(min + 3 * len),
				t4: Math.floor(min + 4 * len),
				t5: max,
			};
		}
	}
	
	function classify_item(value) {
		var t = classification_thresholds;
		if (value <= t.t1)
			return 'percentage-0-20';
		else if ((value >= t.t1 + 1) && (value <= t.t2))
			return 'percentage-21-40';
		else if ((value >= t.t2 + 1) && (value <= t.t3))
			return 'percentage-41-60';
		else if ((value >= t.t3 + 1) && (value <= t.t4))
			return 'percentage-61-80';
		else
			return 'percentage-81-100';
	}
	
	function updateLegend() {
		var dataToDisplay = $('#data_to_display').val();
		var unit = dataToDisplay.includes('percent') ? '%' : '';;
		var t = classification_thresholds;
		$('aside.additional-info ul li:first div span:nth(0)').attr('data-original-title', t.t0 + unit + ' - ' + t.t1 + unit);
		$('aside.additional-info ul li:first div span:nth(1)').attr('data-original-title', (t.t1 + 1) + unit + ' - ' + t.t2 + unit);
		$('aside.additional-info ul li:first div span:nth(2)').attr('data-original-title', (t.t2 + 1) + unit + ' - ' + t.t3 + unit);
		$('aside.additional-info ul li:first div span:nth(3)').attr('data-original-title', (t.t3 + 1) + unit + ' - ' + t.t4 + unit);
		$('aside.additional-info ul li:first div span:nth(4)').attr('data-original-title', (t.t4 + 1) + unit + ' - ' + t.t5 + unit);
	}
	
	function updateKanji() {
		var dataToDisplay = $('#data_to_display').val();
	
		var zeroAsMinimum = $('#zero_as_minimum').is(':checked');
		if (zeroAsMinimum) {
			var max = statistics[dataToDisplay].max;
			calculate_classification_thresholds(0, max);
		}
		else {
			var min = statistics[dataToDisplay].min;
			var max = statistics[dataToDisplay].max;
			calculate_classification_thresholds(min, max);
		}
		
		updateLegend();
		
		var kanji = $('section.lattice-single-character a');
		kanji.each(function(i, item) {
			var isLocked = $(this).parent().attr('is_locked');
			if (isLocked == 'true') {
				return;
			}

			$(this).removeClass('percentage-0-20 percentage-21-40 percentage-41-60 percentage-61-80 percentage-81-100');
			
			var value = $(this).parent().attr('data_' + dataToDisplay);
			$(this).addClass(classify_item(value));
		});
	}
	
	function updateHtmlItems() {
		var dataToDisplay = $('#data_to_display').val();
	
		var zeroAsMinimum = $('#zero_as_minimum').is(':checked');
		if (zeroAsMinimum) {
			var max = statistics[dataToDisplay].max;
			calculate_classification_thresholds(0, max);
		}
		else {
			var min = statistics[dataToDisplay].min;
			var max = statistics[dataToDisplay].max;
			calculate_classification_thresholds(min, max);
		}
		
		updateLegend();
		
		var htmlItems = $('section.lattice-single-character a, section.lattice-multi-character a');
		htmlItems.each(function(i, item) {
			var isLocked = $(this).parent().attr('is_locked');
			if (isLocked != 'false') {
				return;
			}

			$(this).removeClass('percentage-0-20 percentage-21-40 percentage-41-60 percentage-61-80 percentage-81-100');
			
			var value = $(this).parent().attr('data_' + dataToDisplay);
			$(this).addClass(classify_item(value));
		});
	}
	
	function buildSelectBox() {
		var isOnLatticeCombined = WaniKani.is_on_lattice_kanji_combined() || WaniKani.is_on_lattice_vocabulary_combined();
		var isOnLatticeMeaning = WaniKani.is_on_lattice_kanji_meaning() || WaniKani.is_on_lattice_vocabulary_meaning();
		var isOnLatticeReading = WaniKani.is_on_lattice_kanji_reading() || WaniKani.is_on_lattice_vocabulary_reading();
		
		var selectBox = '<select id="data_to_display" class="input" name="data_to_display" title="Select what data to display.">';
		for(var key in used_labels){
			var selected = '';
			
			if (isOnLatticeCombined) {
				if (key == 'combined_percent_answered_correct') {
					selected = 'selected';
				}
			}
			else if (isOnLatticeMeaning) {
				if (key == 'meaning_percent_answered_correct') {
					selected = 'selected';
				}
			}
			else if (isOnLatticeReading) {
				if (key == 'reading_percent_answered_correct') {
					selected = 'selected';
				}
			}
			
			selectBox = selectBox + '<option value="' + key + '" ' + selected + '>' + used_labels[key] + '</option>';
		}
		selectBox = selectBox + '</select>'
		return selectBox;
	}
	
	function addToStatistics(attribute, value) {
		var currentMin = statistics[attribute].min;
		var currentMax = statistics[attribute].max;
		statistics[attribute].min = (currentMin < value) ? currentMin : value;
		statistics[attribute].max = (currentMax < value) ? value : currentMax;
	}
	
	function cleanUpItem(item) {
		var title = item.attr('data-original-title');
		var index = title.indexOf('<br />');
		if (index != -1) {
			title = title.substr(0, index);
			item.attr('data-original-title', title);
		}
	}
	
	function extendItem(value, item, dataAttr) {
		var newTitle = item.attr('data-original-title');
		
		var valueText;
		
		if (loaded_snapshot.id == 0) {
			valueText = value;
		}
		else {
			if (value < 0) {
				valueText = value;
			}
			else if (value > 0) {
				valueText = '+' + value;
			}
			else {
				valueText = 'no change';
			}
		}
		
		newTitle += '<br />' + used_labels[dataAttr] + ': ' + valueText;
		item.attr('data-original-title', newTitle);
		
		item.parent().attr('data_' + dataAttr, value);
		
		addToStatistics(dataAttr, value);
	}
	
	function calcPercentAnsweredCorrect(correct, incorrect) {
		if (correct + incorrect > 0)
			return Math.round(100 * correct / (correct + incorrect));
		else
			return 0;
	}
	
	function extendLatticeRadicals() {
		used_labels = radicals_data_labels;
		
		removeText('aside.additional-info ul li:first');
		
		$('aside.additional-info ul li:first').append(buildSelectBox());
		$('#data_to_display').on('change', function() { updateHtmlItems(); });
		
		$('aside.additional-info ul li:first').append('<br />');
		
		$('aside.additional-info ul li:first').append(UI.buildSelection('selected_snapshot', 'Snapshot to use'));
		UI.addSelectionOption('selected_snapshot', 'none', 'No comparison', true);
		
		for (var i = 0; i < snapshot_index.snapshots.length; i++) {
			var snapshot = snapshot_index.snapshots[i];
		
			var snapshot_label = 'Compare to snapshot [' + snapshot.id + "] - " + new Date(snapshot.date_taken).toLocaleString();
			UI.addSelectionOption('selected_snapshot', snapshot.id, snapshot_label, false);		
		}
		
		UI.addOnChangeListener('selected_snapshot',
				function()
				{
					var selected = $('#selected_snapshot').val();
					
					if (selected == 'none')
						resetLoadedSnapshot();
					else
						loadSnapshot(selected);
					
					extendRadicalItems();
					updateHtmlItems();
				});
		
		$('aside.additional-info ul li:first').append(
			'<br /><input id="zero_as_minimum" type="checkbox" name="zero_as_minimum" checked>Use 0 as minimum<br>');
		$('#zero_as_minimum').on('change', function() { updateHtmlItems(); });
	
		extendRadicalItems();
	}
	
	function extendRadicalItems() {
		setupStatistics();
		
		var htmlItems = $('section.lattice-single-character a');
		htmlItems.each(function(i, item) {
			var uri = WaniKani.parse_item_url($(this).attr('href'));
			var itemInfo = WaniKani.find_item(uri.type, uri.name);
			if ((itemInfo == null) || (itemInfo.user_specific == null)) {
				$(this).parent().attr('is_locked', 'true');
				return;
			}
			
			var meaning = itemInfo.meaning;
			itemInfo = itemInfo.user_specific;
			
			var snapshotInfo = {
					meaning_correct: 0,
					meaning_incorrect: 0,
					meaning_current_streak: 0,
					meaning_max_streak: 0
				};
			if (loaded_snapshot.id != 0) {
				var numRadicals = loaded_snapshot.radical_data.requested_information.length;
				for (var j = 0; j < numRadicals; j++) {
					if (loaded_snapshot.radical_data.requested_information[j].meaning == meaning) {
						if (loaded_snapshot.radical_data.requested_information[j].user_specific != null) {
							snapshotInfo = loaded_snapshot.radical_data.requested_information[j].user_specific;
						}
						break;
					}
				}
			}
			
			$(this).parent().attr('is_locked', 'false');
			
			cleanUpItem($(this));
			
			{
				var correct = itemInfo.meaning_correct;
				var incorrect = itemInfo.meaning_incorrect;
				var total = correct + incorrect;
				var percentAnsweredCorrect = calcPercentAnsweredCorrect(correct, incorrect);
				
				var snapshotCorrect = snapshotInfo.meaning_correct;
				var snapshotIncorrect = snapshotInfo.meaning_incorrect;
				var snapshotTotal = snapshotCorrect + snapshotIncorrect;
				var snapshotPercentAnsweredCorrect = calcPercentAnsweredCorrect(snapshotCorrect, snapshotIncorrect);
				
				extendItem(percentAnsweredCorrect - snapshotPercentAnsweredCorrect, $(this), 'meaning_percent_answered_correct');
				extendItem(total - snapshotTotal,									$(this), 'meaning_total');
			}
			
			extendItem(itemInfo.meaning_correct - snapshotInfo.meaning_correct,					$(this), 'meaning_correct');
			extendItem(itemInfo.meaning_incorrect - snapshotInfo.meaning_incorrect,				$(this), 'meaning_incorrect');
			extendItem(itemInfo.meaning_current_streak - snapshotInfo.meaning_current_streak,	$(this), 'meaning_current_streak');
			extendItem(itemInfo.meaning_max_streak - snapshotInfo.meaning_max_streak,			$(this), 'meaning_max_streak');
		});
	}
	
	function extendLatticeKanji() {
		used_labels = kanji_data_labels;
		
		removeText('aside.additional-info ul li:first');
		
		$('aside.additional-info ul li:first').append(buildSelectBox());
		$('#data_to_display').on('change', function() { updateHtmlItems(); });
		
		$('aside.additional-info ul li:first').append('<br />');
		
		$('aside.additional-info ul li:first').append(UI.buildSelection('selected_snapshot', 'Snapshot to use'));
		UI.addSelectionOption('selected_snapshot', 'none', 'No comparison', true);
		
		for (var i = 0; i < snapshot_index.snapshots.length; i++) {
			var snapshot = snapshot_index.snapshots[i];
		
			var snapshot_label = 'Compare to snapshot [' + snapshot.id + "] - " + new Date(snapshot.date_taken).toLocaleString();
			UI.addSelectionOption('selected_snapshot', snapshot.id, snapshot_label, false);		
		}
		
		UI.addOnChangeListener('selected_snapshot',
				function()
				{
					var selected = $('#selected_snapshot').val();
					
					if (selected == 'none')
						resetLoadedSnapshot();
					else
						loadSnapshot(selected);
					
					extendKanjiItems();
					updateHtmlItems();
				});
		
		$('aside.additional-info ul li:first').append(
			'<br /><input id="zero_as_minimum" type="checkbox" name="zero_as_minimum" checked>Use 0 as minimum<br>');
		$('#zero_as_minimum').on('change', function() { updateHtmlItems(); });
		
		extendKanjiItems();
	}
	
	function extendKanjiItems() {
		setupStatistics();
		
		var htmlItems = $('section.lattice-single-character a');
		htmlItems.each(function(i, item) {
			var uri = WaniKani.parse_item_url($(this).attr('href'));
			var itemInfo = WaniKani.find_item(uri.type, uri.name);
			if ((itemInfo == null) || (itemInfo.user_specific == null)) {
				$(this).parent().attr('is_locked', 'true');
				return;
			}
			
			var character = itemInfo.character;
			itemInfo = itemInfo.user_specific;
			
			var snapshotInfo = {
					meaning_correct: 0,
					meaning_incorrect: 0,
					meaning_current_streak: 0,
					meaning_max_streak: 0,
					reading_correct: 0,
					reading_incorrect: 0,
					reading_current_streak: 0,
					reading_max_streak: 0
				};
			if (loaded_snapshot.id != 0) {
				var numKanji = loaded_snapshot.kanji_data.requested_information.length;
				for (var j = 0; j < numKanji; j++) {
					if (loaded_snapshot.kanji_data.requested_information[j].character == character) {
						if (loaded_snapshot.kanji_data.requested_information[j].user_specific != null) {
							snapshotInfo = loaded_snapshot.kanji_data.requested_information[j].user_specific;
						}
						break;
					}
				}
			}
			
			$(this).parent().attr('is_locked', 'false');
			
			cleanUpItem($(this));
			
			{
				var correct = itemInfo.meaning_correct + itemInfo.reading_correct;
				var incorrect = itemInfo.meaning_incorrect + itemInfo.reading_incorrect;
				var total = correct + incorrect;
				var percentAnsweredCorrect = calcPercentAnsweredCorrect(correct, incorrect);
				
				var snapshotCorrect = snapshotInfo.meaning_correct + snapshotInfo.reading_correct;
				var snapshotIncorrect = snapshotInfo.meaning_incorrect + snapshotInfo.reading_incorrect;
				var snapshotTotal = snapshotCorrect + snapshotIncorrect;
				var snapshotPercentAnsweredCorrect = calcPercentAnsweredCorrect(snapshotCorrect, snapshotIncorrect);
				
				extendItem(percentAnsweredCorrect - snapshotPercentAnsweredCorrect,	$(this), 'combined_percent_answered_correct');
				extendItem(correct - snapshotCorrect,								$(this), 'combined_correct');
				extendItem(incorrect - snapshotIncorrect,							$(this), 'combined_incorrect');
				extendItem(total - snapshotTotal,									$(this), 'combined_total');
			}
			
			{
				var correct = itemInfo.meaning_correct;
				var incorrect = itemInfo.meaning_incorrect;
				var total = correct + incorrect;
				var percentAnsweredCorrect = calcPercentAnsweredCorrect(correct, incorrect);
				
				var snapshotCorrect = snapshotInfo.meaning_correct;
				var snapshotIncorrect = snapshotInfo.meaning_incorrect;
				var snapshotTotal = snapshotCorrect + snapshotIncorrect;
				var snapshotPercentAnsweredCorrect = calcPercentAnsweredCorrect(snapshotCorrect, snapshotIncorrect);
				
				extendItem(percentAnsweredCorrect - snapshotPercentAnsweredCorrect,	$(this), 'meaning_percent_answered_correct');
				extendItem(correct - snapshotCorrect,								$(this), 'meaning_correct');
				extendItem(incorrect - snapshotIncorrect,							$(this), 'meaning_incorrect');
				extendItem(total - snapshotTotal,									$(this), 'meaning_total');
			}
			
			{
				var correct = itemInfo.reading_correct;
				var incorrect = itemInfo.reading_incorrect;
				var total = correct + incorrect;
				var percentAnsweredCorrect = calcPercentAnsweredCorrect(correct, incorrect);
				
				var snapshotCorrect = snapshotInfo.reading_correct;
				var snapshotIncorrect = snapshotInfo.reading_incorrect;
				var snapshotTotal = snapshotCorrect + snapshotIncorrect;
				var snapshotPercentAnsweredCorrect = calcPercentAnsweredCorrect(snapshotCorrect, snapshotIncorrect);
				
				extendItem(percentAnsweredCorrect - snapshotPercentAnsweredCorrect,	$(this), 'reading_percent_answered_correct');
				extendItem(correct - snapshotCorrect,								$(this), 'reading_correct');
				extendItem(incorrect - snapshotIncorrect,							$(this), 'reading_incorrect');
				extendItem(total - snapshotTotal,									$(this), 'reading_total');
			}
			
			extendItem(itemInfo.meaning_correct - snapshotInfo.meaning_correct,					$(this), 'meaning_correct');
			extendItem(itemInfo.meaning_incorrect - snapshotInfo.meaning_incorrect,				$(this), 'meaning_incorrect');
			extendItem(itemInfo.meaning_current_streak - snapshotInfo.meaning_current_streak,	$(this), 'meaning_current_streak');
			extendItem(itemInfo.meaning_max_streak - snapshotInfo.meaning_max_streak,			$(this), 'meaning_max_streak');
			extendItem(itemInfo.reading_correct - snapshotInfo.reading_correct,					$(this), 'reading_correct');
			extendItem(itemInfo.reading_incorrect - snapshotInfo.reading_incorrect,				$(this), 'reading_incorrect');
			extendItem(itemInfo.reading_max_streak - snapshotInfo.reading_max_streak,			$(this), 'reading_max_streak');
			extendItem(itemInfo.reading_current_streak - snapshotInfo.reading_current_streak,	$(this), 'reading_current_streak');
		});
	}
	
	function extendLatticeVocabulary() {
		used_labels = vocabulary_data_labels;
		
		removeText('aside.additional-info ul li:first');
		
		$('aside.additional-info ul li:first').append(buildSelectBox());
		$('#data_to_display').on('change', function() { updateHtmlItems(); });
		
		$('aside.additional-info ul li:first').append('<br />');
		
		$('aside.additional-info ul li:first').append(UI.buildSelection('selected_snapshot', 'Snapshot to use'));
		UI.addSelectionOption('selected_snapshot', 'none', 'No comparison', true);
		
		for (var i = 0; i < snapshot_index.snapshots.length; i++) {
			var snapshot = snapshot_index.snapshots[i];
		
			var snapshot_label = 'Compare to snapshot [' + snapshot.id + "] - " + new Date(snapshot.date_taken).toLocaleString();
			UI.addSelectionOption('selected_snapshot', snapshot.id, snapshot_label, false);		
		}
		
		UI.addOnChangeListener('selected_snapshot',
				function()
				{
					var selected = $('#selected_snapshot').val();
					
					if (selected == 'none')
						resetLoadedSnapshot();
					else
						loadSnapshot(selected);
					
					extendVocabularyItems();
					updateHtmlItems();
				});
		
		$('aside.additional-info ul li:first').append(
			'<br /><input id="zero_as_minimum" type="checkbox" name="zero_as_minimum" checked>Use 0 as minimum<br>');
		$('#zero_as_minimum').on('change', function() { updateHtmlItems(); });
		
		extendVocabularyItems();
	}
	
	function extendVocabularyItems() {
		setupStatistics();
		
		var vocabulary = $('section.lattice-multi-character a');
		vocabulary.each(function(i, item) {
			var uri = WaniKani.parse_item_url($(this).attr('href'));
			var itemInfo = WaniKani.find_item(uri.type, uri.name);
			if ((itemInfo == null) || (itemInfo.user_specific == null)) {
				$(this).parent().attr('is_locked', 'true');
				return;
			}
			
			var character = itemInfo.character;
			itemInfo = itemInfo.user_specific;
			
			var snapshotInfo = {
					meaning_correct: 0,
					meaning_incorrect: 0,
					meaning_current_streak: 0,
					meaning_max_streak: 0,
					reading_correct: 0,
					reading_incorrect: 0,
					reading_current_streak: 0,
					reading_max_streak: 0
				};
			if (loaded_snapshot.id != 0) {
				var numVocabulary = loaded_snapshot.vocabulary_data.requested_information.general.length;
				for (var j = 0; j < numVocabulary; j++) {
					if (loaded_snapshot.vocabulary_data.requested_information.general[j].character == character) {
						if (loaded_snapshot.vocabulary_data.requested_information.general[j].user_specific != null) {
							snapshotInfo = loaded_snapshot.vocabulary_data.requested_information.general[j].user_specific;
						}
						break;
					}
				}
			}
			
			$(this).parent().attr('is_locked', 'false');
			
			cleanUpItem($(this));
			
			{
				var correct = itemInfo.meaning_correct + itemInfo.reading_correct;
				var incorrect = itemInfo.meaning_incorrect + itemInfo.reading_incorrect;
				var total = correct + incorrect;
				var percentAnsweredCorrect = calcPercentAnsweredCorrect(correct, incorrect);
				
				var snapshotCorrect = snapshotInfo.meaning_correct + snapshotInfo.reading_correct;
				var snapshotIncorrect = snapshotInfo.meaning_incorrect + snapshotInfo.reading_incorrect;
				var snapshotTotal = snapshotCorrect + snapshotIncorrect;
				var snapshotPercentAnsweredCorrect = calcPercentAnsweredCorrect(snapshotCorrect, snapshotIncorrect);
				
				extendItem(percentAnsweredCorrect - snapshotPercentAnsweredCorrect,	$(this), 'combined_percent_answered_correct');
				extendItem(correct - snapshotCorrect,								$(this), 'combined_correct');
				extendItem(incorrect - snapshotIncorrect,							$(this), 'combined_incorrect');
				extendItem(total - snapshotTotal,									$(this), 'combined_total');
			}
			
			{
				var correct = itemInfo.meaning_correct;
				var incorrect = itemInfo.meaning_incorrect;
				var total = correct + incorrect;
				var percentAnsweredCorrect = calcPercentAnsweredCorrect(correct, incorrect);
				
				var snapshotCorrect = snapshotInfo.meaning_correct;
				var snapshotIncorrect = snapshotInfo.meaning_incorrect;
				var snapshotTotal = snapshotCorrect + snapshotIncorrect;
				var snapshotPercentAnsweredCorrect = calcPercentAnsweredCorrect(snapshotCorrect, snapshotIncorrect);
				
				extendItem(percentAnsweredCorrect - snapshotPercentAnsweredCorrect,	$(this), 'meaning_percent_answered_correct');
				extendItem(correct - snapshotCorrect,								$(this), 'meaning_correct');
				extendItem(incorrect - snapshotIncorrect,							$(this), 'meaning_incorrect');
				extendItem(total - snapshotTotal,									$(this), 'meaning_total');
			}
			
			{
				var correct = itemInfo.reading_correct;
				var incorrect = itemInfo.reading_incorrect;
				var total = correct + incorrect;
				var percentAnsweredCorrect = calcPercentAnsweredCorrect(correct, incorrect);
				
				var snapshotCorrect = snapshotInfo.reading_correct;
				var snapshotIncorrect = snapshotInfo.reading_incorrect;
				var snapshotTotal = snapshotCorrect + snapshotIncorrect;
				var snapshotPercentAnsweredCorrect = calcPercentAnsweredCorrect(snapshotCorrect, snapshotIncorrect);
				
				extendItem(percentAnsweredCorrect - snapshotPercentAnsweredCorrect,	$(this), 'reading_percent_answered_correct');
				extendItem(correct - snapshotCorrect,								$(this), 'reading_correct');
				extendItem(incorrect - snapshotIncorrect,							$(this), 'reading_incorrect');
				extendItem(total - snapshotTotal,									$(this), 'reading_total');
			}
			
			extendItem(itemInfo.meaning_correct - snapshotInfo.meaning_correct,					$(this), 'meaning_correct');
			extendItem(itemInfo.meaning_incorrect - snapshotInfo.meaning_incorrect,				$(this), 'meaning_incorrect');
			extendItem(itemInfo.meaning_current_streak - snapshotInfo.meaning_current_streak,	$(this), 'meaning_current_streak');
			extendItem(itemInfo.meaning_max_streak - snapshotInfo.meaning_max_streak,			$(this), 'meaning_max_streak');
			extendItem(itemInfo.reading_correct - snapshotInfo.reading_correct,					$(this), 'reading_correct');
			extendItem(itemInfo.reading_incorrect - snapshotInfo.reading_incorrect,				$(this), 'reading_incorrect');
			extendItem(itemInfo.reading_max_streak - snapshotInfo.reading_max_streak,			$(this), 'reading_max_streak');
			extendItem(itemInfo.reading_current_streak - snapshotInfo.reading_current_streak,	$(this), 'reading_current_streak');
		});		
	}
	
	function extendDropDownMenu() {
		if (dropDownMenuExtended) {
			return;
		}
		
	    $('<li><a href="#lattice_extension">Lattice Extension</a></li>')
	    	.insertBefore($('.account .dropdown-menu .nav-header:eq(1)'))
	    	.on('click', toggleSettingsWindow);
	    
	    dropDownMenuExtended = true;
	}
	
	function buildSettingsWindow() {
		UI.initCss();
		
		var html;
		
		html = UI.buildWindow('lattice_extension', 'Lattice Extension');
		$(html).insertAfter($('.navbar'));
		
		updateSettingsWindow();
		
		UI.addOnClickListener('lattice_extension_close_btn', {},
				function(e)
				{
					toggleSettingsWindow(e);
				});
		
		settingsWindowAdded = true;
	}

	function updateSettingsWindow() {
		var html;
		
		$('#lattice_extension_settings').remove();
		
		html = UI.buildTable('lattice_extension_settings');
		$('#lattice_extension_body').append(html);
		
		if (snapshot_index.snapshots.length >= max_num_snapshots) {
			html = UI.buildTableRow(
					'',
					'',
					'Max. number of snapshots reached. Delete some to create new ones.');
					
			$('#lattice_extension_settings_body').append(html);
		}
		else {
			html = UI.buildTableRow(
					'',
					'',
					UI.buildButton('new_snapshot', 'Create new snapshot'));
					
			$('#lattice_extension_settings_body').append(html);
			
			UI.addOnClickListener('new_snapshot', {},
					function(e)
					{
						createSnapshot();
					});
		}
		
		for (var i = 0; i < snapshot_index.snapshots.length; i++) {
			var snapshot = snapshot_index.snapshots[i];
			
			var info = 'Radicals: ' + snapshot.num_radicals + ', Kanji: ' + snapshot.num_kanji + ', Vocabulary: ' + snapshot.num_vocabulary;
			
			html = UI.buildTableRow(
					'',
					'Snapshot [' + snapshot.id + "] - " + new Date(snapshot.date_taken).toLocaleString(),
					UI.buildButton('delete_snapshot_' + snapshot.id, 'Delete snapshot'),
					info);
			$('#lattice_extension_settings_body').append(html);
			
			UI.addOnClickListener('delete_snapshot_' + snapshot.id, {id: snapshot.id},
					function(e)
					{
						var selection = confirm('Are you sure you want to delete this snapshot?');
						if (selection == true) {
							deleteSnapshot(e.data.id);
						}
					});
		}
	}
	
	function createSnapshot() {
		$('#new_snapshot').remove();
		
		html = UI.buildTableRow(
					'',
					'<b>Please wait! A new snapshot is being created ...</b>',
					'');
		$('#lattice_extension_settings_body').append(html);
		
		alert('A new snapshot is being created!\n' +
			'Click now on OK and wait for the snapshot list to update.');
		
		WaniKani
			.load_radical_data()
			.then(WaniKani.load_kanji_data)
			.then(WaniKani.load_vocabulary_data)
			.then(createSnapshotDataLoaded);
	}
	
	function createSnapshotDataLoaded() {
		var new_id = snapshot_index.next_id;
		snapshot_index.next_id++;

		loaded_snapshot = {};
		loaded_snapshot.id = new_id;
		loaded_snapshot.date_taken = new Date();
		loaded_snapshot.radical_data = WaniKani.get_radical_data();
		loaded_snapshot.kanji_data = WaniKani.get_kanji_data();
		loaded_snapshot.vocabulary_data = WaniKani.get_vocabulary_data();
		
		var index_entry = {};
		index_entry.id = new_id;
		index_entry.date_taken = loaded_snapshot.date_taken;
		index_entry.num_radicals = loaded_snapshot.radical_data.requested_information.length;
		index_entry.num_kanji = loaded_snapshot.kanji_data.requested_information.length;
		index_entry.num_vocabulary = loaded_snapshot.vocabulary_data.requested_information.general.length;
		snapshot_index.snapshots.push(index_entry);
		
		var saved = saveLoadedSnapshot();
		
		if (saved) {
			saved = saveSnapshotIndex();
		}
		
		if (saved) {
			updateSettingsWindow();
		}
	}
	
	function deleteSnapshot(snapshot_id) {
		var index = snapshot_index.snapshots.findIndex(function(item) { return (item.id == snapshot_id); });
		if (index > -1) {
			snapshot_index.snapshots.splice(index, 1);
			
			var saved = saveSnapshotIndex();
			
			if (saved) {
				deleteLoadedSnapshot();
				updateSettingsWindow();
			}
		}
	}
	
	function resetLoadedSnapshot() {
		loaded_snapshot = {id:0};
	}
	
	function loadSnapshot(snapshot_id) {
		var loaded = localStorage.getItem(localStoragePrefix + 'snapshot' + snapshot_id);
		if (loaded != null) {
			var tmp = JSON.parse(LZString.decompressFromUTF16(loaded));
			
			if (tmp == null) {
				alert('Failed to load snapshot ' + snapshot_id);
			}
			else {
				loaded_snapshot = tmp;
			}
		}
	}
	
	function loadSnapshotIndex() {
		var loaded = localStorage.getItem(localStoragePrefix + 'snapshot_index');
		if (loaded != null) {
			snapshot_index = JSON.parse(LZString.decompressFromUTF16(loaded));
			
			if (snapshot_index == null) {
				alert('Failed to load snapshot index');
				return false;
			}
		}
		
		return true;
	}
	
	function saveSnapshotIndex() {
		try {
			var stringified = JSON.stringify(snapshot_index);
			var compressed = LZString.compressToUTF16(stringified);
			
			if (LZString.decompressFromUTF16(compressed) == null) {
				alert('Failed to save snapshot index: Compression error');
			}
			else {
				localStorage.setItem(localStoragePrefix + 'snapshot_index', compressed);
			}
		} catch (err) {
			alert(err.message);
			return false;
		}
		
		return true;
	}
	
	function saveLoadedSnapshot() {
		try {
			var stringified = JSON.stringify(loaded_snapshot);
			var compressed = LZString.compressToUTF16(stringified);
			
			if (LZString.decompressFromUTF16(compressed) == null) {
				alert('Failed to save snapshot ' + loaded_snapshot.id + ': Compression error');
			}
			else {
				localStorage.setItem(localStoragePrefix + 'snapshot' + loaded_snapshot.id, compressed);
			}
		} catch (err) {
			alert(err.message);
			return false;
		}
		
		return true;
	}
	
	function deleteLoadedSnapshot() {
		try {
			localStorage.removeItem(localStoragePrefix + 'snapshot' + loaded_snapshot.id);
			loaded_snapshot = {id: 0};
		} catch (err) {
			alert(err.message);
			return false;
		}
		
		return true;
	}
	
    function toggleSettingsWindow(e) {
        if (e !== undefined) e.preventDefault();

        // Add the manager if not already.
        if (!settingsWindowAdded) buildSettingsWindow();

        $('#lattice_extension').slideToggle();
        $('html, body').animate({scrollTop: 0}, 800);
    }
	
	//-------------------------------------------------------------------
	// Main function
	//-------------------------------------------------------------------
	function main() {
		console.log('START - WaniKani Lattice Extension');
		
		var indexLoaded = loadSnapshotIndex();
		
		if (indexLoaded) {
			extendDropDownMenu();
			
			WaniKani.track_times();
		
			if (WaniKani.is_on_lattice_radicals_meaning()) {
				WaniKani.load_radical_data().then(extendLatticeRadicals);
			}
			else if (WaniKani.is_on_lattice_kanji_combined() || WaniKani.is_on_lattice_kanji_meaning() || WaniKani.is_on_lattice_kanji_reading()) {
				WaniKani.load_kanji_data().then(extendLatticeKanji);
			}
			else if (WaniKani.is_on_lattice_vocabulary_combined() || WaniKani.is_on_lattice_vocabulary_meaning() || WaniKani.is_on_lattice_vocabulary_reading()) {
				WaniKani.load_vocabulary_data().then(extendLatticeVocabulary);
			}
		}
		
		console.log('END - WaniKani Lattice Extension');
	}
	window.addEventListener('load', main, false);

}());