var fc;

require(['lexicon', 'html', 'syllables', 'quizzes'],
        // Do sanity checks and fill in default or regular values.
        function flashcards() {
  var REFLEXIVE_PRONOUNS = ['me', 'te', 'se', 'nos', 'os', 'se'];
  var PRESENT_CONJUGATION = [null, ['o', 'as', 'a', 'amos', 'a`is', 'an'],
                             ['o', 'es', 'e', 'emos', 'e`is', 'en'],
                             ['o', 'es', 'e', 'imos', 'i`s', 'en']];
  var PAST_CONJUGATION = [null, ['e`', 'aste', 'o`', 'amos', 'asteis', 'aron'],
                          ['i`', 'iste', 'io`', 'imos', 'isteis', 'ieron'],
                          ['i`', 'iste', 'io`', 'imos', 'isteis', 'ieron']];
  var POS_IMPERATIVE_CONJUGATION = [null, ['a', 'e', 'ad', 'en'],
                                    ['e', 'a', 'ed', 'an'],
                                    ['e', 'a', 'id', 'an']];
  var NEG_IMPERATIVE_CONJUGATION = [null, ['es', 'e', 'e`is', 'en'],
                                    ['as', 'a', 'a`is', 'an'],
                                    ['as', 'a', 'a`is', 'an']];
  var IMPERFECT_CONJUGATION = [
      null, ['aba', 'abas', 'aba', 'a`bamos', 'abais', 'aban'],
      ['i`a', 'i`as', 'i`a', 'i`amos', 'i`ais', 'i`an'],
      ['i`a', 'i`as', 'i`a', 'i`amos', 'i`ais', 'i`an']];
  var FUTURE_SIMPLE_CONJUGATION = ['e`', 'a`s', 'a`', 'emos', 'e`is', 'a`n'];
  var FUTURE_CONDITIONAL_CONJUGATION = [
      'i`a', 'i`as', 'i`a', 'i`amos', 'i`ais', 'i`an'];  
  var AUXILIARY_ESTAR = [
      'estoy', 'esta`s', 'esta`', 'estamos', 'esta`is', 'esta`n'];
  var AUXILIARY_HABER = ['he', 'has', 'ha', 'hemos', 'habe`is', 'han'];
  var seen = {};  // Tracks Spanish form + POS
  // A list of questions and weights.
  // The higher the weight the more likely the question is to be asked.
  // A question is represented as a function that takes a DIV and rewrites
  // the content to display the question.
  var questions = [];
  // A map such that questionsByKey[q.key] === q for all q in questions.
  var questionsByKey = {};

  // Number of bits of history kept for each term.
  // For each term, we keep track of the last HISTORY_LENGTH times a question
  // featuring it was correctly answered, so we can weight more highly
  // questions that 
  var HISTORY_LENGTH = 4;

  // Set up the database of question scores.
  var historyDb;
  (function () {
    if (window.openDatabase) {
      historyDb = openDatabase(
          'FlashCards', '1.0', 'FlashCard Per User Data', 1 << 17);
      historyDb.transaction(function (tx) {
          tx.executeSql(
              'CREATE TABLE IF NOT EXISTS History'
              + ' (questionkey VARCHAR PRIMARY KEY, '
              + 'history INT NOT NULL)');
        });
    }
  })();
  (function () {
    // Fetch answer history from the database.
    var answerHistory = {};
    if (historyDb) {
      historyDb.transaction(function (tx) {
        tx.executeSql('SELECT questionkey, history FROM History', [],
                      function (tx, result) {
                        var rows = result.rows;
                        for (var i = 0, n = rows.length; i < n; ++i) {
                          var row = rows.item(i);
                          answerHistory[row.questionkey] = +row.history;
                        }
                        cleanUpLexicon(answerHistory);
                      });
      });
    } else {
      setTimeout(function () { cleanUpLexicon({}); }, 0);
    }
  })();

  var lexiconReady;
  function cleanUpLexicon(answerHistory) {
    for (var i = 0, n = lexicon.length; i < n; ++i) {
      var term = lexicon[i];
      // Having to keep a comma off the end of the lexicon is a PITA
      if (!term) { continue; }

      function enforce(cond, desc) {
        if (!cond) {
          var format = global.uneval || function (term) { return term.esp; };
          throw new Error('For term ' + i + ':' + format(term) + ' ' + desc);
        }
      }
      // Must have both an English and a Spanish translation.
      enforce('string' === typeof term.eng, 'English form');
      enforce('string' === typeof term.esp, 'Spanish form');
      enforce(!/`/.test(html(term.esp)), 'Spanish Accent');
      enforce(!/`/.test(term.eng), 'English Accent');
      // Can be of exactly one part of speech.
      var isVerb = !!term.present;
      var isNoun = !!term.noun;
      var isAdverb = !!term.adverb;
      var isAdjective = !!term.adj;
      var isConjunction = !!term.conj;
      var isPreposition = !!term.prep;
      var isOther = !!term.other;
      enforce(1 === (isVerb + isNoun + isAdverb + isAdjective + isConjunction
                     + isPreposition + isOther),
              'Term must have one part of speech');
      enforce(!!term.past === isVerb, 'Verbs must have a past tense');
      enforce(!!term.futureRoot === isVerb, 'Verbs must have a future root');
      enforce(term.imperative
              ? isVerb && (term.imperative === NONE
                           || term.imperative === REGULAR
                           || term.imperative.length === 2)
              : !isVerb, 'Verbs must have an imperative');
      enforce(!!term.gerund === isVerb, 'Verbs must have a gerund');
      enforce(!!term.participle === isVerb, 'Verbs must have a participle');
      enforce(!!term.imperfect === isVerb, 'Verbs must have an imperfect');
      var posTag = isVerb ? 'verb' : isNoun ? 'noun' : isAdverb ? 'adv'
          : isAdjective ? 'adj' : isConjunction ? 'conj'
          : isPreposition ? 'prep' : isOther ? 'other'
          : enforce(false, 'pos tag');

      var termKey = posTag + '-' + term.esp;
      enforce(!seen.hasOwnProperty(termKey), 'duplicate term');
      seen[termKey] = term;
      term.key = termKey;

      if (isVerb) {
        var present = term.present === REGULAR ? [] : term.present.split(/\//g);
        enforce(present.length <= 6, 'too many present forms');
        var past = term.past === REGULAR ? [] : term.past.split(/\//g);
        enforce(past.length <= 6, 'too many past forms');
        var root = term.esp.replace(/[aei]`?r(?:se)?/, '');
        var reflexive = /se$/.test(term.esp);
        var conjugation = null;
        switch (term.esp.charAt(root.length)) {
          case 'a': conjugation = 1; break;
          case 'e': conjugation = 2; break;
          case 'i': conjugation = 3; break;
        }
        enforce(root !== term.esp, 'could not determine verb root');
        enforce(conjugation !== null, 'could not determine conjugation');
        for (var person = 0; person < 6; ++person) {
          if (!present[person]) {
            present[person] = (
                (reflexive ? REFLEXIVE_PRONOUNS[person] + ' ' : '')
                + joinWord(term.esp, root.length,
                           PRESENT_CONJUGATION[conjugation][person]));
          }
          if (!past[person]) {
            past[person] = (
                (reflexive ? REFLEXIVE_PRONOUNS[person] + ' ' : '')
                + joinWord(term.esp, root.length,
                           PAST_CONJUGATION[conjugation][person]));
          }
        }
        var imperative = null;
        (function () {
          if (term.imperative === NONE) { return; }
          imperative = [[], []];
          var imperativeVosotrosBase = term.esp;
          var imperativeVosotrosRootLength = root.length;
          // Use the first person singular form as the root for most imperative
          // forms, but without any reflexive pronoun
          var imperativeBase = present[0].replace(/^me /, '');
          var imperativeRootLength = imperativeBase.replace(/oy?$/, '').length;
          if (term.imperative !== REGULAR) {
            enforce(term.imperative instanceof Array
                    && term.imperative.length === 2,
                    'imperative must be an array of 2 "/" separated strings');
            for (var sign = 2; --sign >= 0;) {
              if (term.imperative[sign] === REGULAR) { continue; }
              enforce('string' === typeof term.imperative[sign],
                      'expected string in imperative');
              imperative[sign] = term.imperative[sign].split(/\//g);
              enforce(imperative[sign].length <= 4,
                      'too many imperative forms');
            }
          }
          for (var person = 0; person < 4; ++person) {
            if (imperative[0][person] && imperative[1][person]) { continue; }
            if (person !== 2) {
              enforce(imperativeRootLength > 0
                      && imperativeRootLength < present[0].length,
                      'could not determine imperative root for person');
            }
            var personBase, personRootLength;
            if (person === 2) {
              personBase = imperativeVosotrosBase;
              personRootLength = imperativeVosotrosRootLength;
            } else {
              personBase = imperativeBase;
              personRootLength = imperativeRootLength;
            }
            if (!imperative[0][person]) {
              imperative[0][person] = joinWord(
                  personBase, personRootLength,
                  POS_IMPERATIVE_CONJUGATION[conjugation][person]);
              if (reflexive) {
                imperative[0][person] = joinWord(
                    imperative[0][person],
                    // When the pronoun 'os' follows the positive imperative
                    // form, the 'd' is dropped.
                    imperative[0][person].length - (person === 2 ? 1 : 0),
                    REFLEXIVE_PRONOUNS[((person % 2) + 1) + (person >> 1) * 3],
                    true);
              }
            }
            if (!imperative[1][person]) {
              imperative[1][person] = (
                  reflexive 
                  ? REFLEXIVE_PRONOUNS[
                      ((person % 2) + 1) + (person >> 1) * 3] + ' ' : '')
                  + joinWord(personBase, personRootLength,
                             NEG_IMPERATIVE_CONJUGATION[conjugation][person]);
            }
          }
        })();

        // Future and future conditional are very regular.
        var futureSimple = [];
        var futureConditional = [];
        var futureRoot = term.futureRoot === REGULAR
            ? term.esp.replace(/se$/, '') : term.futureRoot;
        for (var person = 0; person < 6; ++person) {
          var futurePrefix = reflexive ? REFLEXIVE_PRONOUNS[person] + ' ' : '';
          futureSimple[person] = futurePrefix
              + joinWord(futureRoot, futureRoot.length,
                         FUTURE_SIMPLE_CONJUGATION[person]);
          futureConditional[person] = futurePrefix
              + joinWord(futureRoot, futureRoot.length,
                         FUTURE_CONDITIONAL_CONJUGATION[person]);
        }

        var participle = term.participle;
        if (participle === REGULAR) {
          participle = joinWord(term.esp, root.length,
                                (conjugation === 1 ? 'ado' : 'ido'));
        }
        // The present perfect is formed from the present of the verb haber and
        // the participle.
        var presentPerfect = [];
        for (var person = 0; person < 6; ++person) {
          presentPerfect[person] = ((
              reflexive ? REFLEXIVE_PRONOUNS[person] + ' ' : '')
              + AUXILIARY_HABER[person] + ' ' + participle);
        }

        var gerund = term.gerund;
        if (gerund === REGULAR) {
          gerund = joinWord(term.esp, root.length,
                            (conjugation === 1 ? 'ando' : 'iendo'));
        }
        // The present progressive is formed from the present of the verb haber
        // and the participle.
        var presentProgressive = [];
        (function () {
          for (var person = 0; person < 6; ++person) {
            var prog = gerund;
            if (reflexive) {
              // Join the pronoun before the auxiliary so that joinWord isn't
              // confused by any emphasis on the auxiliary.
              prog = joinWord(
                  prog, prog.length, REFLEXIVE_PRONOUNS[person], true);
            }
            presentProgressive[person] = AUXILIARY_ESTAR[person] + ' ' + prog;
          }
        })();

        var imperfect;
        if (term.imperfect === REGULAR) {
          imperfect = [];
        } else {
          imperfect = term.imperfect.split(/\//g);
          enforce(imperfect.length <= 6, 'too many imperfect forms');
        }
        (function () {
          for (var person = 0; person < 6; ++person) {
            if (!imperfect[person]) {
              // TODO: check that reflexive pronoun precedes verb.
              imperfect[person] = (
                  (reflexive ? REFLEXIVE_PRONOUNS[person] +  ' ' : '')
                  + joinWord(term.esp, root.length,
                             IMPERFECT_CONJUGATION[conjugation][person]));
            }
          }
        })();

        term.conjugation = conjugation;
        term.participle = participle;
        term.gerund = gerund;
        term.present = present;
        term.past = past;
        term.futureConditional = futureConditional;
        term.futureSimple = futureSimple;
        term.presentPerfect = presentPerfect;
        term.presentProgressive = presentProgressive;
        term.imperfect = imperfect;
        delete term.imperative;

        questions.push(conjugateQuiz(term, 'present'));
        questions.push(conjugateQuiz(term, 'past'));
        if (imperative) {
          term.posImperative = imperative[0];
          term.negImperative = imperative[1];
          questions.push(conjugateQuiz(term, 'posImperative'));
          questions.push(conjugateQuiz(term, 'negImperative'));
        }
        questions.push(conjugateQuiz(term, 'futureSimple'));
        questions.push(conjugateQuiz(term, 'futureConditional'));
        questions.push(conjugateQuiz(term, 'presentPerfect'));
        questions.push(conjugateQuiz(term, 'presentProgressive'));
        questions.push(conjugateQuiz(term, 'imperfect'));
      }
      questions.push(defineWordQuiz(term, choose));
    }

    (function () {
      for (var i = questions.length; --i >= 0;) {
        var q = questions[i];
        q.index = i;
        q.bits = (answerHistory.hasOwnProperty(q.key)
                  && answerHistory[q.key]) || 0;
        if (questionsByKey.hasOwnProperty(q.key)) { throw new Error(q.key); }
        questionsByKey[q.key] = q;
      }
    })();

    lexiconReady = true;
  }

  var weightSum = NaN;

  function choose() {
    if (weightSum !== weightSum) {
      weightSum = 0;
      for (var i = questions.length; --i >= 0;) {
        weightSum += weightOf(questions[i]);
      }
      if (global.console) { global.console.log('weightSum <= ' + weightSum); }
    }
    var randNum = (Math.random() * weightSum) | 0;
    var questionIndex = 0;
    while (questionIndex + 1 < questions.length && randNum > 0) {
      randNum -= weightOf(questions[questionIndex]);
      ++questionIndex;
    }
    return questions[questionIndex];
  }

  function checkAnswer(termIndex, form, correct, container) {
    var wasCorrect;
    if (typeof correct === 'number') {
      wasCorrect = checkCheckedAnswer(termIndex, form, correct, container);
    } else if (correct instanceof Array) {
      wasCorrect = checkTypedAnswer(termIndex, form, correct, container);
    } else {
      throw new Error(correct);
    }
    updateHistory(termIndex, wasCorrect);

    function showNext() {
      choose().question(container);
      container.getElementsByTagName('INPUT')[0].focus();
    }

    // If they're correct, show the next question.
    // Otherwise, let them submit again to get the next question.
    if (wasCorrect) {
      setTimeout(showNext, 500);
    } else {
      form.onsubmit = function () { setTimeout(showNext, 0); return false; };
    }
    // Disable the inputs since we're not going to read them again.
    for (var i = form.elements.length; --i >= 0;) {
      var input = form.elements[i];
      if (input.type !== 'submit') {
        input.disabled = true;
      } else {
        input.focus();
      }
    }
  }

  function checkCheckedAnswer(termIndex, form, correct, container) {
    var chosen = reduce(form.elements, function (item, old) {
        if (item.name === 'answer' && item.checked) { return +item.value; }
        return old;
      });
    var wasCorrect = chosen === correct;
    var correctItem = form.elements['answer-' + correct];
    correctItem.parentNode.style.backgroundColor = wasCorrect ? '#bfb' : '#fbb';
    return wasCorrect;
  }

  function normalize(userInput) {
    return unaccent(userInput).replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' ');
  }

  function checkTypedAnswer(termIndex, form, correct, container) {
    var answerIndex = 0;
    var els = form.elements;
    var errors = 0;
    for (var i = 0, n = els.length; i < n; ++i) {
      var inp = els[i];
      if (inp.name === 'answer') {
        var answer = normalize(inp.value);
        var golden = normalize(correct[answerIndex]);
        var diffs = diff(golden, answer);
        var isInpCorrect = diffs.match
            || (diffs.diff == '`' && diffs.start === diffs.endB);
        inp.style.backgroundColor = isInpCorrect ? '#bfb' : '#fbb';
        inp.value = accent(golden);
        if (!isInpCorrect) {
          ++errors;
          if (global.console && global.console.warn) {
            global.console.warn(accent(golden) + ' != ' + accent(answer));
          }
        }
        ++answerIndex;
      }
    }
    return errors === 0 && answerIndex === correct.length;
  }

  function updateHistory(questionKey, wasCorrect) {
    try {
      var q = questionsByKey[questionKey];
      var oldWeight = weightOf(q);
      q.bits = (q.bits >>> 1) | (wasCorrect ? (1 << HISTORY_LENGTH) : 0);
      delete q.weight;
      var newWeight = weightOf(q);
      if (oldWeight !== newWeight) {
        weightSum += newWeight - oldWeight;
        if (global.console) { global.console.log('weightSum <= ' + weightSum); }
      }
      if (historyDb) {
        historyDb.transaction(function (tx) {
            tx.executeSql(
                'REPLACE INTO History (questionkey, history) VALUES (?, ?)',
                [q.key, q.bits]);
          });
      }
    } catch (ex) {
      console.error(ex);
    }
  }

  function weightOf(question) {
    if (!question.weight) {
      question.weight = Math.max(
          1, sqr((HISTORY_LENGTH - countBits(question.bits)) + 1));
    }
    return question.weight * (question.term.importance || 1);
  }

  function countBits(n) {
    var bits = 0;
    while (n) { n = n & (n - 1); ++bits; }
    return bits;
  }

  function sqr(n) { n = +n; return n * n; }

  global.fc = {
    ask: function (container, continuation) { 
      runWhen(function () {
                choose().question(container);
                if (continuation) { continuation.call({}); }
              },
              function () { return lexiconReady; });
    },
    checkAnswer: checkAnswer
  };
});

// TODO: Add checkboxes to turn off certain answer types, and to toggle accent
// checking in answers.
