/*-
 *  Hanban - Chinese character trainer
 */

var settings_nr_questions = 30 // 30 as requested by Giso 2006-09-14
var settings_nr_choices = 7
var settings_use_trick_questions = true
var settings_quiz_level = 500 // @@@TODO should become rating/advice level
var settings_current_level // as selected in drop-down
var settings_source_deck
var settings_wordset
// extra punishment factor for wrong answers (neutral is 1.0)
// @@@ TODO: make this dependent on nr_questions by math reasoning?
var settings_punishment = 2.0

var empty = '(empty)' // because nodeValue=='' doesn't work in Safari

var node_status   = document.createTextNode(empty)
var node_answer   = document.createTextNode(empty)
var node_question = document.createTextNode(empty)
var node_choices
var node_wordset_info = document.createTextNode(empty)
var node_level_info = document.createTextNode(empty)

var quiz
var mistakes
var words_by_length
var words_by_parts

var start_time // @@@ profiling

function pinyin_base(inp)
{
        var map = {
                "\u0101": "a1", "\u00e1": "a2", "\u01ce": "a3", "\u00e0": "a4",
                "\u0113": "e1", "\u00e9": "e2", "\u011b": "e3", "\u00e8": "e4",
                "\u012b": "i1", "\u00ed": "i2", "\u01d0": "i3", "\u00ec": "i4",
                "\u014d": "o1", "\u00f3": "o2", "\u01d2": "o3", "\u00f2": "o4",
                "\u016b": "u1",                 "\u01d4": "u3",
                                                "\u01da": "u:3"
// @@@TODO: complete this table
        }

        var out = ""

        for (var i=0; i<inp.length; i++) {
                var cin = inp.charAt(i)
                var cmap = map[cin]
                if (cmap) {
                        out += cmap
                } else {
                        out += cin
                }
        }

        return out
}

function word_count(word)
{
        var count = 0
        if (word.length > 0) {
                var ix = -1
                do {
                        count++
                        ix = word.indexOf(' ', ix+1)
                } while (ix >= 0)
        }
        return count
}

function init()
{
        document.getElementById("status").appendChild(node_status)
        document.getElementById("answer").appendChild(node_answer)
        document.getElementById("question").appendChild(node_question)
        document.getElementById("wordset_info").appendChild(node_wordset_info)
        document.getElementById("level_info").appendChild(node_level_info)
        node_choices = document.getElementById("choices")

        words_by_length = [ null ]

        init_wordsets()
        var ws = document.startup_form.wordset
        while (ws.hasChildNodes()) ws.removeChild(ws.firstChild)

        words_by_parts = []

        for (var i=0; i<Deck.length; i++) {
                var w = Deck[i][2] // .pinyin

                var wc = word_count(w)

                if (wc == 0) continue //@@@ to skip labels

                if (words_by_length[wc] == null) {
                        words_by_length[wc] = []
                }
                words_by_length[wc].push(i)

                var parts = decompose(w)
                for (var pix in parts) {
                        if (words_by_parts[parts[pix]] == null) {
                                words_by_parts[parts[pix]] = []
                        }
                        words_by_parts[parts[pix]].push(i)
                }
        }

        for (var part in words_by_parts) words_by_parts[part].sort()

        mistakes = [ ]

        for (var i=0; i<wordsets.length; i++) {
                var option = document.createElement('option')
                option.value = wordsets[i].id
                option.appendChild(document.createTextNode(wordsets[i].name))

                document.startup_form.wordset.appendChild(option)
        }

        document.startup_form.wordset.value = 'T-PAVC'
        on_select_wordset()
}

function random(n)
{
        return Math.floor(Math.random() * n)
}

function create_quiz(source_deck, level_lo, level_hi)
{
        var level_size = level_hi - level_lo

        // split words 0..level in 4 groups

        //             hard              easy
        //             40%   30%   20%   10%
        var groups = [ [],   [],   [],   []   ]
        var ratios = [ 0.40, 0.70, 0.90, 1.00 ]

        var deck = []
        for (var ix=level_lo; ix<level_hi; ix++) {
                deck.push(source_deck[level_lo + level_hi - ix - 1])
        }

        // @@@ TODO: sort deck for difficulty
        // spread deck evenly over four groups
        var ix = 0
        for (var gix=0; gix<groups.length; gix++)
        {
                num = Math.floor(level_size * (gix+1) / groups.length) - ix

                for (var i=0; i<num; i++) {
                        groups[gix].push(deck[ix])
                        ix++
                }
        }

        var qdeck = []
        var tix = settings_nr_questions
        if (level_size < tix) tix = level_size

        var ix = 0
        for (var gix=0; gix<groups.length; gix++)
        {
                var num = Math.floor(tix * ratios[gix]) - ix
                var group_len = groups[gix].length
                if (num > group_len) num = group_len
                var weight = group_len / num
                for (var i=0; i<num; i++) {
                        var len = groups[gix].length
                        var jx = random(len)
                        var tmp = groups[gix][len-1]
                        groups[gix][len-1] = groups[gix][jx]
                        groups[gix][jx] = tmp

                        var question = new Object()
                        question.id = groups[gix].pop()
                        question.weight = weight
                        question.group = 'ABCD'.charAt(gix)
                        qdeck.push(question)
                        ix++
                }
        }

        // @@@ shuffle here

        return qdeck
}

function get_settings()
{
        settings_nr_questions = parseInt(document.startup_form.quiz_length.value)
}

function on_start()
{
        node_answer.nodeValue = "New quiz started. Good luck!"

        get_settings()

        quiz = new Object()

        quiz.questions = 0
        quiz.nr_correct = 0
        quiz.nr_correct_old = 0 // nr correct answers from previous run
        quiz.rating = 0
        quiz.level_max = settings_source_deck.length
        quiz.level = settings_quiz_level // @@@ obsolete???
        if (quiz.level > quiz.level_max) quiz.level = quiz.level_max
        quiz.level_lo = 0
        quiz.level_hi = settings_current_level.hi
        quiz.is_rating_test = true
        quiz.nr_repeats = 0 // practice round counter

        // create list of questions
        quiz.deck = create_quiz(settings_source_deck, 0, quiz.level)

        // practice will store all bad answers, and allow the player
        // to rehearse them after the quiz
        // @@@TODO: this smells like we really need to make a Quiz class
        quiz.practice = new Object() // new quiz with all wrong answers
        quiz.practice.questions = 0
        quiz.practice.nr_correct = 0
        quiz.practice.nr_correct_old = 0
        quiz.practice.rating = 0
        quiz.practice.level = quiz.level
        quiz.practice.level_lo = quiz.level_lo
        quiz.practice.level_hi = quiz.level_hi
        quiz.practice.level_max = quiz.level_max
        quiz.practice.deck = []

        // shuffle quiz
        var ix
        for (ix=0; ix<quiz.deck.length; ix++) {
                var jx = ix + random(quiz.deck.length - ix)
                var tmp = quiz.deck[ix]
                quiz.deck[ix] = quiz.deck[jx]
                quiz.deck[jx] = tmp
        }

        document.getElementById("answer").className = 'answer'

        show_status()
        create_question()
}

function on_practice()
{
        node_answer.nodeValue = "Practice round started. Good luck!"

        quiz.practice.nr_correct_old = quiz.nr_correct_old + quiz.nr_correct
        quiz.practice.nr_repeats = quiz.nr_repeats + 1

        quiz = quiz.practice
        quiz.is_rating_test = false

        // @@@TODO: this smells like we really need to make a Quiz class
        quiz.practice = new Object() // new quiz with all wrong answers
        quiz.practice.questions = 0
        quiz.practice.nr_correct = 0
        quiz.practice.rating = 0
        quiz.practice.level = quiz.level
        quiz.practice.level_lo = quiz.level_lo
        quiz.practice.level_hi = quiz.level_hi
        quiz.practice.level_max = quiz.level_max
        quiz.practice.deck = []

        // shuffle
        var ix
        for (ix=0; ix<quiz.deck.length; ix++) {
                var jx = ix + random(quiz.deck.length - ix)
                var tmp = quiz.deck[ix]
                quiz.deck[ix] = quiz.deck[jx]
                quiz.deck[jx] = tmp
        }

        document.getElementById("answer").className = 'answer'

        show_status()
        create_question()
}

function on_practice_level()
{
        get_settings()

        node_answer.nodeValue = "Practice level started. Good luck!"

        quiz = new Object()

        quiz.questions = 0
        quiz.nr_correct = 0
        quiz.nr_correct_old = 0
        quiz.rating = 0
        quiz.level_max = settings_source_deck.length
        quiz.level = settings_quiz_level
        if (quiz.level > quiz.level_max) quiz.level = quiz.level_max
        quiz.level_lo = settings_current_level.lo
        quiz.level_hi = settings_current_level.hi
        quiz.is_rating_test = false
        quiz.nr_repeats = 1

        // create list of questions
        quiz.deck = create_quiz(settings_source_deck,
                settings_current_level.lo,
                settings_current_level.hi)
 
        // @@@TODO: this smells like we really need to make a Quiz class
        quiz.practice = new Object() // new quiz with all wrong answers
        quiz.practice.questions = 0
        quiz.practice.nr_correct = 0
        quiz.practice.rating = 0
        quiz.practice.level = quiz.level
        quiz.practice.level_lo = quiz.level_lo
        quiz.practice.level_hi = quiz.level_hi
        quiz.practice.level_max = quiz.level_max
        quiz.practice.deck = []

        // shuffle
        var ix
        for (ix=0; ix<quiz.deck.length; ix++) {
                var jx = ix + random(quiz.deck.length - ix)
                var tmp = quiz.deck[ix]
                quiz.deck[ix] = quiz.deck[jx]
                quiz.deck[jx] = tmp
        }

        document.getElementById("answer").className = 'answer'

        show_status()
        create_question()
}

function check_answer(answer)
{
        var it_is =
                Deck[quiz.qix][1] + " is " +
                Deck[quiz.qix][2] + " (English: " +
                Deck[quiz.qix][3] + ")"

        if (answer.is_correct) {
                document.getElementById("answer").className = 'goodanswer'
                if (answer.none_of_these) {
                       node_answer.nodeValue = "Right! " + it_is
                } else {
                       node_answer.nodeValue = "Correct! " + it_is
                }
                quiz.nr_correct++
        } else {
                document.getElementById("answer").className = 'badanswer'
                if (answer.dont_know) {
                        node_answer.nodeValue = "Too bad! " + it_is
                } else {
                        if (answer.none_of_these) {
                                node_answer.nodeValue = "Wrong! " + it_is
                        } else {
                                if (!mistakes[quiz.qix]) mistakes[quiz.qix] = new Object()
                                mistakes[quiz.qix].id = answer.id
                                
                                node_answer.nodeValue = "Wrong! " + it_is + ", not " + answer.text
                        }
                }
                var question = new Object()
                question.id = answer.qix
                question.weight = 0
                quiz.practice.deck.push(question)
        }
        quiz.rating += answer.points
}

function on_mousedown(event)
{
        if (!event) event = window.event
        var answer = (event.target || event.srcElement).answer

        if (!quiz.answer) {
                (event.target || event.srcElement).className = 'button_pressed'
                check_answer(answer)
                quiz.answer = answer
        }
}

function on_answer(event)
{
        if (!event) event = window.event
        var answer = (event.target || event.srcElement).answer

        if (quiz.answer) {
                answer = quiz.answer
        } else {
                check_answer(answer)
        }
        quiz.answer = null

        show_status()
        if (quiz.questions < quiz.deck.length) {
                // continue with next
                create_question()
        } else {
                clear_question()
                clear_choices()
                show_end_result()
                // finished
        }
}

function calc_rating()
{
        var rating = 0
        if (quiz.questions > 0) {
                if (quiz.rating > 0) rating = quiz.rating
                //rating = (rating * quiz.deck.length) / quiz.questions
                rating = Math.floor(rating + 0.5)
        }
        return rating
}

function show_end_result()
{
        var score = Math.floor(100.0 *
                (quiz.nr_correct + quiz.nr_correct_old) /
                (quiz.questions  + quiz.nr_correct_old))
        node_question.nodeValue = score + "%"

        var rating = calc_rating()

        if (quiz.is_rating_test) {
                var msg = "Your rating is: " + rating + " words!"
                node_choices.appendChild(document.createTextNode(msg))
        }

        if (quiz.practice.deck.length > 0) {
                var button = document.createElement("button")
                button.className = "choice_button" // @@@ not a quiz choice...
                button.appendChild(document.createTextNode(
                        'Practice ' + quiz.practice.deck.length + ' missed words'))
                button.onclick = on_practice
                node_choices.appendChild(button)
        }
}

function show_status()
{
        var status = 
                "Quiz level: " + quiz.level_hi + "/" + quiz.level_max +
                ", Score: " +
                quiz.nr_correct + "/" +
                quiz.questions + "/" +
                quiz.deck.length
        if (quiz.is_rating_test) {
                status += ", Rating: " + calc_rating()
        } else {
                if (quiz.nr_repeats == 0) {
                        status += " (practice)"
                } else {
                        status += " (practice round " + quiz.nr_repeats + ")"
                }
        }
        node_status.nodeValue = status
}

function clear_question()
{
        node_question.nodeValue = empty
}

function clear_choices()
{
        while (node_choices.hasChildNodes()) {
                node_choices.removeChild(node_choices.firstChild)
        }
}

function is_unique(arr, key)
{
        for (var i=0; i<arr.length; i++) {
                if (key == arr[i].text) return false
        }
        return true
}

function decompose(text)
{
        var result = new Array()
        text = pinyin_base(text)

        var word_nr = 1
        var head = ""
        var fragment = ""
        var tone = ""
        var in_word = false

        for (var ix=0; ix<text.length; ix++) {
                var ch = text.charAt(ix)
                if (ch == ' ') {
                        if (fragment.length > 0) {
                                result.push(word_nr + fragment + tone)
                        }
                        if (head.length > 0) {
                                result.push(word_nr + head + fragment)
                        }
                        head = ""
                        fragment = ""
                        tone = ""
                        word_nr++
                        in_word = false
                        continue
                }
                if (ch == '1' || ch == '2' || ch == '3' || ch == '4') {
                        tone = ch
                        continue
                }
                if (!in_word && (ch == 'a' || ch == 'e' || ch == 'i' || ch == 'o' || ch == 'u')) {
                        head = fragment
                        fragment = ""
                        in_word = true
                }
                fragment += ch
        }

        if (fragment.length > 0) {
                result.push(word_nr + fragment + tone)
        }
        if (head.length > 0) {
                result.push(word_nr + head + fragment)
        }

        result.sort()

        return result
}

function dump(a)
{
        var str = "{"
        for (var ix=0; ix<a.length; ix++) {
                str += "#" + a[ix]
        }
        str += "}"
        return str
}

function search_lookalike(qix)
{
        var result = -1

        var w = Deck[qix][2] // pinyin
        var wc = word_count(w)

        var ww = w
        var a = decompose(ww)

        // slightly stupid version: concat and use sort
        // can be improved by O(N) algo
        var list = []
        for (var aix in a) {
                list = list.concat(words_by_parts[a[aix]])
        }
        list.sort()

        // create a list of lookalikes

        candidates = []
        var min_match = 1

        var current = -1
        var match = 0

        for (var ix in list) {
                if (list[ix] == current) {
                        match++
                        continue
                }
                if (match >= min_match) {
                        var u = Deck[current][2]
                        var uc = word_count(u)
                        if ((u != w) && (uc == wc)) {
                                if (match > min_match) {
                                        min_match = match
                                        candidates.length = 0
                                }
                                candidates.push(current)
                        }
                }
                current = list[ix]
                match = 1
        }
        if (match >= min_match) {
                var u = Deck[current][2]
                var uc = word_count(u)
                if ((u != w) && (uc == wc)) {
                        if (match > min_match) {
                                min_match = match
                                candidates.length = 0
                        }
                        candidates.push(current)
                }
        }

        if (candidates.length > 0) {
                result = candidates[random(candidates.length)]
        }
        return result
}


function create_question()
{
        clear_choices()

        var question = quiz.deck[quiz.questions]
        var qix = question.id
        node_question.nodeValue = Deck[qix][1]

        quiz.qix = qix

        var answers = new Array()

        // answers[].text         -- text to show to player
        // answers[].is_correct   -- right or wrong answer

        var aix = 0

        // algorithm

        // pass 1:
        //   0 is correct answer
        //   >0 some random wrong answer
        //   ensure all are unique

        var wc = word_count(Deck[qix][2])

        answers[aix] = new Object()
        answers[aix].text = Deck[qix][2]
        answers[aix].is_correct = true
        answers[aix].points = question.weight
        answers[aix].id = qix
        aix++

        var max_nr_choices = settings_nr_choices
        // number of choices can be limited by the number of available
        // words with the same length
        if (max_nr_choices > words_by_length[wc].length) {
                // @@@ TODO: this should effect rating calculation also
                max_nr_choices = words_by_length[wc].length
        }

        for (; aix < max_nr_choices; aix++) {
                do {
                        var wix = random(words_by_length[wc].length)
                        rix = words_by_length[wc][wix]
                } while (!is_unique(answers, Deck[rix][2]))

                answers[aix] = new Object()
                answers[aix].text = Deck[rix][2]
                answers[aix].is_correct = false
                answers[aix].points = settings_punishment *
                        -(question.weight / (max_nr_choices-1))
                answers[aix].id = rix
        }

        // pass 2:
        //   replace 1 by some random lookalike word, if any
        var alt = search_lookalike(qix)
        if ((alt>=0) && is_unique(answers, Deck[alt][2])) {
                answers[1].text = Deck[alt][2]
                answers[1].id = alt
        }

        // pass 2.5:
        //   insert the most recent wrong anwer

        var m = mistakes[qix]
        if (m && is_unique(answers, Deck[m.id][2])) {
                answers[2].text = Deck[m.id][2]
                answers[2].id = m.id
        }

        // pass 3:
        //   shuffle

        for (aix=0; aix<max_nr_choices; aix++) {
                ajx = aix + random(max_nr_choices - aix)

                var tmp = answers[aix]
                answers[aix] = answers[ajx]
                answers[ajx] = tmp
        }

        // pass 4:
        //   cover up last answer with 'None of these...'

        if (settings_use_trick_questions && max_nr_choices > 1) {
                answers[aix-1].text = (max_nr_choices == 2) ?
                        "Something else..." :
                        "None of these..."
                answers[aix-1].none_of_these = true
        }

        answers[aix] = new Object()
        answers[aix].text = "I don't know..."
        answers[aix].dont_know = true
        answers[aix].is_correct = false
        answers[aix].points = 0
        aix++

        // output to document
        for (aix=0; aix<answers.length; aix++) {
                var button = document.createElement("button")
                button.className = "choice_button"
                button.answer = answers[aix]
                button.answer.qix = qix
                button.appendChild(document.createTextNode(answers[aix].text))
                button.onclick = on_answer
                button.onmousedown = on_mousedown

                node_choices.appendChild(button)
        }

        quiz.questions++
}

function flip_settings()
{
        var style = document.getElementById('settings').style
        var button = document.getElementById('settings_button').attributes.getNamedItem('value')
        if (style.visibility == "hidden") {
                style.visibility = "visible"
                button.nodeValue = "Hide settings..."
                document.startup_form.settings_button.className = "pressed"
        } else {
                style.visibility = "hidden"
                button.nodeValue = "Show settings..."
                document.startup_form.settings_button.className = null
        }
}

var done_settings_mousedown = false

function on_settings_mousedown()
{
        flip_settings()
        done_settings_mousedown = true
}

function on_settings()
{
        if (!done_settings_mousedown) {
                flip_settings()
        }
        done_settings_mousedown = false
}

function on_select_wordset()
{
        var id = document.startup_form.wordset.value
        var wordset = wordsets[0]

        for (var i=0; i<wordsets.length; i++) {
                if (wordsets[i].id == id) {
                        wordset = wordsets[i]
                        settings_source_deck = wordset.deck
                        settings_wordset = wordset
                        node_wordset_info.nodeValue = wordset.deck.length + " words"
                        break
                }
        }

        var node_level = document.startup_form.level
        while (node_level.hasChildNodes()) {
                node_level.removeChild(node_level.firstChild)
        }

        var level = wordset.levels[0] // determine preselect level

        for (var i=0; i<wordset.levels.length; i++) {
                var option = document.createElement('option')
                option.value = wordset.levels[i].tag
                option.appendChild(document.createTextNode(wordset.levels[i].name))
                node_level.appendChild(option)

                if (wordset.levels[i].lo < settings_quiz_level &&
                    wordset.levels[i].lo < wordset.levels[i].hi) {
                        level = wordset.levels[i]
                }
        }
        document.startup_form.level.value = level.tag
        settings_current_level = level

        node_level_info.nodeValue = level.hi + " (" + (level.hi - level.lo) + " words)"
}

function on_select_level()
{
        var id = document.startup_form.level.value
        var level = settings_wordset.levels_by_tag[id]
        settings_quiz_level = level.hi
        settings_current_level = level

        node_level_info.nodeValue = level.hi + " (" + (level.hi - level.lo) + " words)"
}

