/*
 * =================================================================
 * Gossamer Mail - enhanced email management system
 *
 *   Website  : http://gossamer-threads.com/
 *   Support  : http://gossamer-threads.com/scripts/support/
 *   Revision : $Id: spellcheck.js,v 1.5 2006/11/18 14:22:58 brewt Exp $
 *
 * Copyright (c) 2004 Gossamer Threads Inc.  All Rights Reserved.
 * Redistribution in part or in whole strictly prohibited. Please
 * see LICENSE file for full details.
 * =================================================================
 */

function spellCheck(objName) {
    this.config = {
// FIXME remove this?
        menuWidth       : 110,                        // The width of the misspelled words menu
        menuMaxRows     : 10,                         // The maximum number of suggestions (maximum is 10)
        disableElements : [],                         // An array of html elements which should be disabled in spell check mode
        addPermission   : true,                       // Whether or not words can be added to the dictionary
        textCheckButton : '',                         // Text of the button when in regular mode (uses original value)
        editorObj       : null                        // The advanced HTML editor object
    };

    this.lang = {
        'add'          : 'Add...',
        'edit'         : 'Edit...',
        'delete'       : 'Delete',
        'replaceAll'   : 'Replace All',
        'resume'       : 'Resume Compose',
        'title'        : 'Suggested Spellings',
        'noMisspelled' : 'No words were misspelled.'
    };

    this.id = {
        checkContent : 'compose_body',
        checkFrame   : 'spellcheck_frame',
        checkButton  : 'button_spellcheck',
        checkResult  : 'spellcheck_result',
        menuSuggest  : 'spellcheck_suggestions',
        htmlCompose  : 'compose_is_html',
        extra        : [] // Extra input values to pass through (these elements must exist both on the page and spellcheck-inline form)
    };

    this.name          = objName;
    this.words         = [];
    this.corrections   = {};
    this.selectedIndex = -1;
    this.loaded        = false;
    this.formLoaded    = false;
    this.misspelled    = false;
    this.replaceAll    = false;
    this.loadingInterval;
}

spellCheck.prototype.load = function () {
    if (this.config.editorObj) {
        var self = this;
        this.loadingInterval = setInterval(function () { self.init(); }, 250);
    }
    else
        this.init();
}

spellCheck.prototype.init = function () {
    if (this.config.editorObj) {
        if (this.config.editorObj.loaded)
            clearInterval(this.loadingInterval);
        else
            return;
    }

    this.objects = {
        checkForm    : null,
        checkContent : document.getElementById(this.id.checkContent),
        checkFrame   : document.getElementById(this.id.checkFrame),
        checkButton  : [],
        checkResult  : document.getElementById(this.id.checkResult),
        menuSuggest  : document.getElementById(this.id.menuSuggest)
    };

    if (typeof this.id.checkButton == 'string')
        this.objects.checkButton.push(document.getElementById(this.id.checkButton));
    else
        for (var i = 0; i < this.id.checkButton.length; i++)
            this.objects.checkButton.push(document.getElementById(this.id.checkButton[i]));

    if (!this.objects.menuSuggest) {
        this.objects.menuSuggest = document.createElement('div');
        this.objects.menuSuggest.id = this.id.menuSuggest;
        this.objects.menuSuggest.style.display = 'none';
        document.body.appendChild(this.objects.menuSuggest);
    }
    if (!this.objects.checkResult) {
        this.objects.checkResult = document.createElement('div');
        this.objects.checkResult.id = this.id.checkResult;
        this.objects.checkResult.style.display = 'none';
        document.body.appendChild(this.objects.checkResult);
    }
    if (!this.objects.checkContent) return;

    var self = this;
    for (var i = 0; i < this.objects.checkButton.length; i++)
        registerEvent(this.objects.checkButton[i], 'click', function (e) {
            if (!e) e = window.event;

            if (self.formLoaded) {
                if (self.objects.checkResult.style.display == 'none')
                    self.prepareCheck();
                else
                    self.resumeCompose();
            }

            cancelEvent(e);
            stopPropagation(e);
        });

// This handler should only be called on a click on everything but the misspelled
// words, edit_word input, and the menu.
    registerEvent(document, 'click', function (e) {
        if (!e) e = window.event;

// FIXME this should call dMenuObj.hide();
        if (self.objects.menuSuggest.style.display != 'none')
            self.objects.menuSuggest.style.display = 'none';

        self.checkEditWord();
    });

    registerEvent(window, 'resize', function () {
        if (self.objects.checkResult.style.display != 'none')
            self.showCheckResult();
    });

    this.loaded = true;
};

spellCheck.prototype.prepareCheck = function () {
    this.objects.checkForm.elements.content.value = this.config.editorObj ? this.config.editorObj.getContent(true) : this.objects.checkContent.value;
    if (this.objects.checkForm.elements.content.value == '') {
        alert(this.lang.noMisspelled);
        return;
    }

    var html = document.getElementById(this.id.htmlCompose);
    if (this.objects.checkForm.elements[this.id.htmlCompose])
        this.objects.checkForm.elements[this.id.htmlCompose].value = html ? html.value : 0;

    for (var i = 0; i < this.id.extra.length; i++) {
        var sourceObj = document.getElementById(this.id.extra[i]);
        var destObj = this.objects.checkForm[this.id.extra[i]];
        if (sourceObj && destObj) destObj.value = sourceObj.value;
    }

    this.objects.checkForm.submit();
}

spellCheck.prototype.resumeCompose = function () {
    var content = '';
    for (var i = 0; i < this.words.length; i++)
        content += this.words[i].word;
    if (this.config.editorObj)
        content = content.replace(/<a dhref=/g, '<a href=');

    if (this.config.editorObj)
        this.config.editorObj.setContent(content);
    else
        this.objects.checkContent.value = content;

    this.objects.menuSuggest.style.display = 'none';
    this.hideCheckResult();

    if (this.config.editorObj) {
        this.config.editorObj.enable();
        this.config.editorObj.focus();
    }
    else
        this.objects.checkContent.focus();

    for (var i = 0; i < this.objects.checkButton.length; i++)
        this.objects.checkButton[i].value = this.config.textCheckButton;
    this.disableElementsEnable(true);
}

spellCheck.prototype.processCheck = function () {
    var self = this;
    if (this.words.length == 0 || !this.misspelled) {
        alert(this.lang.noMisspelled);
        return;
    }

    if (this.config.editorObj)
        this.config.editorObj.disable();

    var content = '';
    for (var i = 0; i < this.words.length; i++) {
        if (!this.words[i].misspelled)
            content += this.config.editorObj ? this.words[i].word : htmlEscape(this.words[i].word).replace(/\n/g, '<br>').replace(/  /g, ' &nbsp;');
        else
            content += '<span id="msw_' + i + '">' + this.words[i].word + '</span>';
    }
    if (this.config.editorObj)
        cmntent = content.replace(/<a href=/gi, '<a dhref=');
    else
        content = '<span class="pre">' + content + '</span>';
    this.objects.checkResult.innerHTML = content;
    for (var i = 0; i < this.words.length; i++) {
        if (!this.words[i].misspelled)
            continue;
        var msw = document.getElementById('msw_' + i);
        msw.title = this.lang.title;
        msw.className = 'misspelled';
        registerEvent(msw, 'click', function (e) {
            if (!e) e = window.event;
            var targ = e.target ? e.target : e.srcElement;
            if (targ.nodeType == 3) // defeat Safari bug
                targ = targ.parentNode;

            self.checkEditWord();

            self.selectedIndex = parseInt(targ.id.replace(/^msw_/, ''));
            var suggest = new dMenu(self, self.corrections[self.words[self.selectedIndex].oword.toLowerCase()]);
            var selected = document.getElementById('msw_' + self.selectedIndex);

            var posY = findPosY(selected) + selected.offsetHeight + 1 - self.objects.checkResult.scrollTop;
            var posX = findPosX(selected);
            suggest.show(posX, posY);

            stopPropagation(e);
        });
    }

    this.showCheckResult();

    if (!this.config.textCheckButton)
        this.config.textCheckButton = this.objects.checkButton[0].value;
    for (var i = 0; i < this.objects.checkButton.length; i++)
        this.objects.checkButton[i].value = this.lang.resume;
    this.disableElementsEnable(false);
}

spellCheck.prototype.disableElementsEnable = function (flag) {
    for (var i = 0; i < this.config.disableElements.length; i++) {
        var element = document.getElementById(this.config.disableElements[i]);
        if (!element)
            continue;
        if (element.tagName == 'INPUT')
            element.disabled = !flag;
        else
            element.style.visibility = flag ? 'visible' : 'hidden';
    }
}

spellCheck.prototype.showCheckResult = function () {
    var width, height, left, top;
    if (this.config.editorObj) {
        width = this.config.editorObj.objects.editableFrame.offsetWidth -
            getStyleLength(this.config.editorObj.objects.editableFrame, 'border-left-width') -
            getStyleLength(this.config.editorObj.objects.editableFrame, 'border-right-width');
        height = this.config.editorObj.objects.editableFrame.offsetHeight -
            getStyleLength(this.config.editorObj.objects.editableFrame, 'border-top-width') -
            getStyleLength(this.config.editorObj.objects.editableFrame, 'border-bottom-width');
        left = findPosX(this.config.editorObj.objects.editorFrame) + findPosX(this.config.editorObj.objects.editableFrame) +
            getStyleLength(this.config.editorObj.objects.editorFrame, 'border-left-width');
        top = findPosY(this.config.editorObj.objects.editorFrame) + findPosY(this.config.editorObj.objects.editableFrame) +
            getStyleLength(this.config.editorObj.objects.editorFrame, 'border-top-width');
    }
    else {
        width = this.objects.checkContent.offsetWidth -
            getStyleLength(this.objects.checkContent, 'border-left-width') -
            getStyleLength(this.objects.checkContent, 'border-right-width');
        height = this.objects.checkContent.offsetHeight -
            getStyleLength(this.objects.checkContent, 'border-top-width') -
            getStyleLength(this.objects.checkContent, 'border-bottom-width');
        left = findPosX(this.objects.checkContent) + getStyleLength(this.objects.checkContent, 'border-left-width');
        top = findPosY(this.objects.checkContent) + getStyleLength(this.objects.checkContent, 'border-top-width');
    }

    this.objects.checkResult.style.top = top + 'px';
    this.objects.checkResult.style.left = left + 'px';
// set display: block before width and height because in Safari, you can't get current styles of hidden elements
    this.objects.checkResult.style.display = '';

    this.objects.checkResult.style.width = calcCSSWidth(this.objects.checkResult, width);
    this.objects.checkResult.style.height = calcCSSHeight(this.objects.checkResult, height);
}

spellCheck.prototype.hideCheckResult = function () {
    this.objects.checkResult.style.display = 'none';
}

spellCheck.prototype.checkEditWord = function () {
    var edit_word = document.getElementById('edit_word');
    if (edit_word) {
        var suggest = new dMenu(this, this.corrections[this.words[edit_word.name].oword.toLowerCase()]);
        suggest.updateWord(13);
    }
}

function dMenu(spellcheck, corrections) {
    var oMenu = spellcheck.objects.menuSuggest;
    while (oMenu.hasChildNodes())
        oMenu.removeChild(oMenu.firstChild);

    this.object      = oMenu;
    this.corrections = corrections;
    this.spellcheck  = spellcheck;
    if (corrections.length < 0) return;

    var menu_rows = corrections.length > spellcheck.config.menuMaxRows ? spellcheck.config.menuMaxRows : corrections.length;
    for (var i = 0; i < menu_rows; i++)
        this.addNode('menu_' + i, corrections[i]);
    var actions = [['hr', '', '<hr />'], ['add', 'wordAdd', spellcheck.lang.add], ['edit', 'wordEdit', spellcheck.lang.edit], ['delete', 'wordEdit', spellcheck.lang['delete']], ['option', 'wordOption', '<span id="replace_all">' + spellcheck.lang.replaceAll + '</span>']];
    menu_rows += actions.length;

    var new_word = this.newWord();
    for (var i = 0; i < actions.length; i++) {
        if (actions[i][0] == 'add' && (this.spellcheck.config.addPermission == false || !new_word)) {
            menu_rows--;
            continue;
        }
        this.addNode(actions[i][0], actions[i][2]);
    }

    oMenu.style.width = spellcheck.config.menuWidth + 'px';
}

dMenu.prototype.show = function (posX, posY) {
    this.object.style.left = posX + 'px';
    this.object.style.top  = posY + 'px';

    if (this.spellcheck.replaceAll && document.getElementById('replace_all'))
        document.getElementById('replace_all').className = 'replace_all';
    this.object.style.display = '';
}

dMenu.prototype.hide = function () {
    this.object.style.display = 'none';

    if (document.getElementById('replace_all'))
        document.getElementById('replace_all').className = '';
}

dMenu.prototype.addNode = function (name, text) {
    var oNode = document.createElement('div');
// FIXME text may contain html
    oNode.innerHTML = text;
    if (name != 'hr') {
        oNode.id = name;
        oNode.className = 'menu-item';
        oNode.setAttribute('unselectable', 'on');
        if (oNode.offsetWidth > this.spellcheck.config.menuWidth)
            this.spellcheck.config.menuWidth = oNode.offsetWidth + 'px';

        var self = this;
        registerEvent(oNode, 'mouseover', function (e) {
            if (!e) e = window.event;
            var targ = e.target ? e.target : e.srcElement;
            if (targ.nodeType == 3) // defeat Safari bug
                targ = targ.parentNode;
// The target when clicking on "Replace All" will be the span
            if (targ.tagName == 'SPAN')
                targ = targ.parentNode;
            targ.className = 'menu-item mouseover';
        });
        registerEvent(oNode, 'mouseout', function (e) {
            if (!e) e = window.event;
            var targ = e.target ? e.target : e.srcElement;
            if (targ.nodeType == 3)
                targ = targ.parentNode;
            if (targ.tagName == 'SPAN')
                targ = targ.parentNode;
            targ.className = 'menu-item';
        });
        registerEvent(oNode, 'click', function (e) {
            if (!e) e = window.event;
            var targ = e.target ? e.target : e.srcElement;
            if (targ.nodeType == 3)
                targ = targ.parentNode;
            if (targ.tagName == 'SPAN')
                targ = targ.parentNode;

            if (targ.id == 'add')
                self.addWord();
            else if (targ.id == 'edit')
                self.editWord();
            else if (targ.id == 'delete')
                self.deleteWord();
            else if (targ.id == 'option')
                self.setOption();
            else
                self.replaceWord(targ.firstChild.nodeValue);
            stopPropagation(e);
        });
    }
    this.object.appendChild(oNode);
}

dMenu.prototype.addWord = function () {
    this.hide();
    if (!this.spellcheck.words[this.spellcheck.selectedIndex].misspelled) return;
    if (!this.spellcheck.config.addPermission) {
        alert("You cannot add words to the dictionary.");
        return;
    }

    var selected = document.getElementById('msw_' + this.spellcheck.selectedIndex);
    if (!selected) return;

    var word = this.spellcheck.words[this.spellcheck.selectedIndex].word;
    if (confirm("Are you sure you want to add '" + word + "' to the dictionary?")) {
        this.spellcheck.objects.checkForm.elements['do'].value = 'add_word';
        this.spellcheck.objects.checkForm.elements.content.value = word;
        this.spellcheck.objects.checkForm.submit();
        selected.className = 'misspelled updated';
        for (var i = 0; i < this.spellcheck.words.length; i++) {
            if (i != this.spellcheck.selectedIndex && this.spellcheck.words[i].misspelled && this.spellcheck.words[i].word.toLowerCase() == word.toLowerCase()) {
                var w = document.getElementById('msw_' + i);
                w.className = 'misspelled updated';
            }
        }
        var new_words = [word];
        this.spellcheck.corrections[this.spellcheck.words[this.spellcheck.selectedIndex].oword.toLowerCase()] = new_words.concat(this.spellcheck.corrections[this.spellcheck.words[this.spellcheck.selectedIndex].oword.toLowerCase()]);
    }
}

dMenu.prototype.editWord = function () {
    this.hide();
    var selected = document.getElementById('msw_' + this.spellcheck.selectedIndex);
    if (!selected) return;

    var word = selected.firstChild.nodeValue;

    var edit_word = document.createElement('input');
    edit_word.id = 'edit_word';
    edit_word.name = this.spellcheck.selectedIndex;
    edit_word.value = word;
    edit_word.size = word.length + 5;
    edit_word.className = 'text';

    selected.replaceChild(edit_word, selected.firstChild);

    var self = this;
    registerEvent(edit_word, 'keyup', function (e) {
        if (!e) e = window.e;
        self.updateWord(e.keyCode);
    });
// Prevent the click event from propagating to the msw_* span (which shows the suggestions)
    registerEvent(edit_word, 'click', function (e) {
        if (!e) e = window.e;
        stopPropagation(e);
    });
    edit_word.focus();
}

dMenu.prototype.updateWord = function (code) {
    var selected  = document.getElementById('msw_' + this.spellcheck.selectedIndex);
    var edit_word = document.getElementById('edit_word');
    if (!edit_word) return;

    if (code == 27) // esc
        selected.replaceChild(document.createTextNode(this.spellcheck.words[this.spellcheck.selectedIndex].word), selected.firstChild);
    else if (code == 13) { // enter
        if (this.spellcheck.words[this.spellcheck.selectedIndex].word != edit_word.value) {
            this.spellcheck.words[this.spellcheck.selectedIndex].word = edit_word.value;
            selected.className = 'misspelled updated';
        }
        selected.replaceChild(document.createTextNode(edit_word.value), selected.firstChild);
    }
}

dMenu.prototype.replaceWord = function (new_word) {
    this.hide();

    if (typeof(this.spellcheck.selectedIndex) == 'undefined' || typeof(new_word) == 'undefined') return;
    if (this.corrections.length == 0) return;

    var selected = document.getElementById('msw_' + this.spellcheck.selectedIndex);
    if (!selected) return;

    var format = this.getFormat(this.spellcheck.words[this.spellcheck.selectedIndex].word);
    new_word   = this.setFormat(format, new_word);
    if (new_word) {
        var old_word = this.spellcheck.words[this.spellcheck.selectedIndex].word;
        this.spellcheck.words[this.spellcheck.selectedIndex].word = new_word;
        selected.replaceChild(document.createTextNode(new_word), selected.firstChild);
        selected.className = 'misspelled updated';
        if (this.spellcheck.replaceAll) {
// Update all other words that match with added word
            for (var i = 0; i < this.spellcheck.words.length; i++) {
                if (i != this.spellcheck.selectedIndex && this.spellcheck.words[i].misspelled && this.spellcheck.words[i].word.toLowerCase() == old_word.toLowerCase()) {
                    var w = document.getElementById('msw_' + i);
                    var f = this.getFormat(this.spellcheck.words[i].word);
                    var n = this.setFormat(f, new_word);
                    this.spellcheck.words[i].word = n;
                    w.replaceChild(document.createTextNode(n), w.firstChild);
                    w.className = 'misspelled updated';
                }
            }
        }
    }
}

dMenu.prototype.deleteWord = function () {
    this.hide();
    var selected = document.getElementById('msw_' + this.spellcheck.selectedIndex);
    if (!selected) return;

    if (confirm("Are you sure you want to delete '" + this.spellcheck.words[this.spellcheck.selectedIndex].word + "'?")) {
        this.spellcheck.words[this.spellcheck.selectedIndex].word = '';
        selected.parentNode.removeChild(selected);
// Delete the next 'word' if it's a single space
        var next_word = this.spellcheck.selectedIndex + 1;
        if (this.spellcheck.words.length > next_word && this.spellcheck.words[next_word].word == ' ')
            this.spellcheck.words[next_word].word = '';
    }
}

dMenu.prototype.getFormat = function (word) {
    if (word == '') return 3;

    if (word == word.toUpperCase()) return 1;
    else if (word.substr(0, 1) == word.substr(0, 1).toUpperCase()) return 2;
    else return 3;
}

dMenu.prototype.setFormat = function (format, word) {
    if (format == 1) return word.toUpperCase();
    else if (format == 2) return word.substr(0, 1).toUpperCase() + word.substr(1).toLowerCase();
    else return word.toLowerCase();
}

dMenu.prototype.setOption = function () {
    if (this.spellcheck.replaceAll) {
        this.spellcheck.replaceAll = false;
        document.getElementById('replace_all').className = '';
    }
    else {
        this.spellcheck.replaceAll = true;
        document.getElementById('replace_all').className = 'replace_all';
    }
}

dMenu.prototype.newWord = function () {
    var spellcheck = this.spellcheck;
    if (spellcheck.config.selectedIndex == -1 || spellcheck.config.selectedIndex > spellcheck.words.length || !spellcheck.words[spellcheck.selectedIndex].misspelled) return false;

    var words   = spellcheck.corrections[spellcheck.words[spellcheck.selectedIndex].oword.toLowerCase()];
    var current = document.getElementById('msw_' + spellcheck.selectedIndex);
    if (!current || words.length == 0) return false;

    var word = spellcheck.words[spellcheck.selectedIndex].word;
    if (word == '') return false;

    for (var i = 0; i < words.length; i++)
        if (words[i].toLowerCase() == word.toLowerCase())
            return false;
    return true;
}
