/**
 * This spell checker was inspired by work done by Garrison Locke, but
 * was rewritten almost completely by Chuck Hagenbuch to use
 * Prototype/Scriptaculous.
 *
 * Requires: prototype.js (v1.6.1+), KeyNavList.js
 *
 * Custom Events:
 * --------------
 * Custom events are triggered on the target element.
 *
 * 'SpellChecker:after'
 *    Fired when the spellcheck processing ends.
 *
 * 'SpellChecker:before'
 *    Fired before the spellcheck is performed.
 *
 * 'SpellChecker:error'
 *    Fired when at least 1 spellcheck error was found.
 *
 * 'SpellChecker:noerror'
 *    Fired when no spellcheck errors are found.
 *
 * @author     Chuck Hagenbuch <chuck@horde.org>
 * @copyright  2005-2015 Horde LLC
 * @license    LGPL-2.1 (http://www.horde.org/licenses/lgpl21)
 */

var SpellChecker = Class.create({

    // Vars used and defaulting to null:
    //   bad, choices, disabled, htmlAreaParent, lc, locale, reviewDiv,
    //   statusButton, statusClass, suggestions, target, url

    delimiter: "\1\0\1",
    options: {},
    resumeOnDblClick: true,
    state: 'CheckSpelling',

    // Options:
    //   bs = (array) Button states
    //   locales = (array) List of locales. See KeyNavList for structure.
    //   sc = (string) Status class
    //   statusButton = (string/element) DOM ID or element of the status
    //                  button
    //   target = (string|Element) DOM element containing data
    //   url = (string) URL of specllchecker handler
    initialize: function(opts)
    {
        this.url = opts.url;
        this.target = $(opts.target);
        this.statusButton = $(opts.statusButton);
        this.buttonStates = opts.bs;
        this.statusClass = opts.sc || this.statusButton.className;
        this.disabled = false;

        this.options.onComplete = this.onComplete.bind(this);

        document.observe('click', this.onClick.bindAsEventListener(this));

        if (opts.locales) {
            this.lc = new KeyNavList(this.statusButton, {
                list: opts.locales,
                onChoose: this.setLocale.bindAsEventListener(this)
            });

            this.statusButton.insert({ after: new Element('SPAN', { className: 'horde-popdown horde-spellcheck-popdown' }) });
        }

        this.setStatus('CheckSpelling');
    },

    setLocale: function(locale)
    {
        this.locale = locale;
    },

    targetValue: function()
    {
        return Object.isUndefined(this.target.value)
            ? this.target.innerHTML
            : this.target.value;
    },

    spellCheck: function()
    {
        this.target.fire('SpellChecker:before');

        var opts = Object.clone(this.options),
            p = $H();

        this.setStatus('Checking');

        p.set(this.target.identify(), this.targetValue());

        if (this.locale) {
            p.set('locale', this.locale);
        }
        if (this.htmlAreaParent) {
            p.set('html', 1);
        }

        opts.parameters = p.toQueryString();

        new Ajax.Request(this.url, opts);
    },

    onComplete: function(request)
    {
        var bad, content, washidden,
            i = 0,
            result = request.responseJSON;

        if (Object.isUndefined(result)) {
            this.setStatus('Error');
            return;
        }

        this.suggestions = result.suggestions || [];

        if (!this.suggestions.size()) {
            this.setStatus('CheckSpelling');
            this.target.fire('SpellChecker:noerror');
            return;
        }

        bad = result.bad || [];

        content = this.targetValue();
        content = this.htmlAreaParent
            ? content.replace(/\r?\n/g, '')
            : content.replace(/\r?\n/g, this.delimiter).escapeHTML();

        $A(bad).each(function(node) {
            var re_text = '<span index="' + (i++) + '" class="spellcheckIncorrect">' + node + '</span>';
            content = content.replace(new RegExp("(?:^|\\b)" + RegExp.escape(node) + "(?:\\b|$)", 'g'), re_text);

            // Go through and see if we matched anything inside a tag (i.e.
            // class/spellcheckIncorrect is often matched if using a
            // non-English lang).
            content = content.replace(new RegExp("(<[^>]*)" + RegExp.escape(re_text) + "([^>]*>)", 'g'), '$1' + node + '$2');
        }, this);

        if (!this.reviewDiv) {
            this.reviewDiv = new Element('DIV', { className: this.target.readAttribute('class') }).addClassName('spellcheck').setStyle({ overflow: 'auto' });
            if (this.resumeOnDblClick) {
                this.reviewDiv.observe('dblclick', this.resume.bind(this));
            }
        }

        if (!this.target.visible()) {
            this.target.show();
            washidden = true;
        }
        this.reviewDiv.setStyle({ width: this.target.clientWidth + 'px', height: this.target.clientHeight + 'px'});
        if (washidden) {
            this.target.hide();
        }

        if (!this.htmlAreaParent) {
            content = content.replace(new RegExp(this.delimiter, 'g'), '<br />');
        }
        this.reviewDiv.update(content);

        if (this.htmlAreaParent) {
            $(this.htmlAreaParent).insert({ bottom: this.reviewDiv });
        } else {
            this.target.hide().insert({ before: this.reviewDiv });
        }

        this.setStatus('ResumeEdit');

        this.target.fire('SpellChecker:error');
    },

    onClick: function(e)
    {
        var data = [], index, elt = e.element();

        if (this.disabled) {
            return;
        }

        if (elt == this.statusButton) {
            switch (this.state) {
            case 'CheckSpelling':
                this.spellCheck();
                break;

            case 'ResumeEdit':
                this.resume();
                break;
            }

            e.stop();
        } else if (elt.hasClassName('horde-spellcheck-popdown')) {
            this.lc.show();
            this.lc.ignoreClick(e);
            e.stop();
        } else if (elt.hasClassName('spellcheckIncorrect')) {
            index = e.element().readAttribute('index');

            $A(this.suggestions[index]).each(function(node) {
                data.push({ l: node, v: node });
            });

            if (this.choices) {
                this.choices.updateBase(elt);
                this.choices.opts.onChoose = function(val) {elt.update(val).writeAttribute({ className: 'spellcheckCorrected' });};
            } else {
                this.choices = new KeyNavList(elt, {
                    esc: true,
                    onChoose: function(val) {
                        elt.update(val).writeAttribute({ className: 'spellcheckCorrected' });
                    }
                });
            }

            this.choices.show(data);
            this.choices.ignoreClick(e);
            e.stop();
        }
    },

    resume: function()
    {
        if (!this.reviewDiv) {
            return;
        }

        var t;

        [ 'Corrected', 'Incorrect' ].each(function(i) {
            this.reviewDiv.select('span.spellcheck' + i).each(function(n) {
                n.insert({ before: n.innerHTML }).remove();
            });
        }, this);

        t = this.reviewDiv.innerHTML;
        if (!this.htmlAreaParent) {
            t = t.replace(/<br *\/?>/gi, this.delimiter).unescapeHTML().replace(new RegExp(this.delimiter, 'g'), "\n");
        }
        this.target.setValue(t);
        this.target.enable();

        if (this.resumeOnDblClick) {
            this.reviewDiv.stopObserving('dblclick');
        }
        this.reviewDiv.remove();
        this.reviewDiv = null;

        this.setStatus('CheckSpelling');

        if (!this.htmlAreaParent) {
            this.target.show();
        }

        this.target.fire('SpellChecker:after');
    },

    setStatus: function(state)
    {
        if (!this.statusButton) {
            return;
        }

        this.state = state;
        switch (this.statusButton.tagName) {
        case 'INPUT':
            this.statusButton.setValue(this.buttonStates[state]);
            break;

        case 'A':
            this.statusButton.update(this.buttonStates[state]);
            break;
        }
        this.statusButton.className = this.statusClass + ' spellcheck' + state;
    },

    isActive: function()
    {
        return this.reviewDiv;
    },

    disable: function(disable)
    {
        this.disabled = disable;
    }

});
