
Ext.namespace('Orfo');

Orfo.Checker = Ext.extend(Ext.util.Observable, {
    xml : null,
    results : null,
    resultPos : 0,
    
    constructor: function(config){
        this.addEvents('checkcomplete');
        Orfo.Checker.superclass.constructor.call(this, config);
    },

    check : function(text, lang, opts){
        Ext.Ajax.request({
            method: 'POST',
            url: 'http://www.orfo.ru/webservice/checktexts.aspx',
            params: {
                options: opts,
                lang: lang,
                text: text
            },
            success: this.onCheckSuccess,
            failure: this.onCheckFailure,
            scope: this
        });
    },
    
    onCheckSuccess : function(resp, opts){
        var xml = resp.responseXML;
        if (xml) {
            this.xml = xml;
            this.results = Ext.query('incorrect', xml);
            this.resultPos = 0;
            this.fireEvent('checkcomplete');
        } else {
            this.xml = null;
            this.results = null;
            this.resultPos = 0;
            alert('checktests script returned not XML: '+resp.responseText);            
        }
    },
    
    onCheckFailure : function(){
        alert('checktexts script failed: '+resp.responseText);
    },
    
    getNextResult : function(needSuggestions){
        var res = this.results;
        if (res) {
            var i = this.resultPos;
            if (i < res.length) {
                var ret = {};
                ret.pos = parseInt(res[i].getAttribute('pos'), 10);
                ret.len = parseInt(res[i].getAttribute('len'), 10);
                if (needSuggestions) {
                    var sugg = Ext.query('suggest', res[i]);
                    ret.suggestions = [];
                    for (s = 0; s < sugg.length; s++) {
                        ret.suggestions[s] = {};
                        ret.suggestions[s].hint = sugg[s].firstChild.nodeValue;
                        ret.suggestions[s].lang = sugg[s].getAttribute('lang');
                    }                    
                }
                this.resultPos++;
                return ret;
            } else {
                return null;                
            }
        } else {
            return null;
        }
    },
    
    reqIscorrect : function(word) {
        Ext.Ajax.request({
            method: 'GET',
            url: 'http://www.orfo.ru/webservice/iscorrect.aspx',
            params: {
                word: word
            }
        });        
    },
    
    reqChangelog : function(word, repl) {
        Ext.Ajax.request({
            method: 'GET',
            url: 'http://www.orfo.ru/webservice/changelog.aspx',
            params: {
                word: word,
                change: repl
            }
        });                
    }
});

Orfo.HtmlEditor = Ext.extend(Ext.form.HtmlEditor, {
    // overridden
    createLinkText : 'Введите URL ссылки:',
    
    buttonTips : {
        bold : {
            title: 'Полужирный (Ctrl+B)',
            text: 'Применение полужирного начертания к выделенному тексту.',
            cls: 'x-html-editor-tip'
        },
        italic : {
            title: 'Курсив (Ctrl+I)',
            text: 'Применение курсивного начертания к выделенному тексту.',
            cls: 'x-html-editor-tip'
        },
        underline : {
            title: 'Подчеркнутый (Ctrl+U)',
            text: 'Подчеркивание выделенного текста.',
            cls: 'x-html-editor-tip'
        },
        increasefontsize : {
            title: 'Увеличить размер',
            text: 'Увеличение размера шрифта.',
            cls: 'x-html-editor-tip'
        },
        decreasefontsize : {
            title: 'Уменьшить размер',
            text: 'Уменьшение размера шрифта.',
            cls: 'x-html-editor-tip'
        },
        backcolor : {
            title: 'Цвет выделения текста',
            text: 'Текст будет выглядеть так, как если бы он был закрашен фломастером.',
            cls: 'x-html-editor-tip'
        },
        forecolor : {
            title: 'Цвет текста',
            text: 'Изменение цвета текста.',
            cls: 'x-html-editor-tip'
        },
        justifyleft : {
            title: 'Выровнять текст по левому краю',
            text: 'Выравнивание текста по левому краю.',
            cls: 'x-html-editor-tip'
        },
        justifycenter : {
            title: 'По центру',
            text: 'Выравнивание текста по центру.',
            cls: 'x-html-editor-tip'
        },
        justifyright : {
            title: 'Выровнять текст по правому краю',
            text: 'Выравнивание текста по правому краю.',
            cls: 'x-html-editor-tip'
        },
        insertunorderedlist : {
            title: 'Маркеры',
            text: 'Начало маркированного списка.',
            cls: 'x-html-editor-tip'
        },
        insertorderedlist : {
            title: 'Нумерация',
            text: 'Начало нумерованного списка.',
            cls: 'x-html-editor-tip'
        },
        createlink : {
            title: 'Гиперссылка',
            text: 'Превратить выделенный текст в гиперссылку.',
            cls: 'x-html-editor-tip'
        },
        sourceedit : {
            title: 'Редактирование HTML',
            text: 'Переключиться в режим редактирования исходного кода HTML.',
            cls: 'x-html-editor-tip'
        },
        spellcheck : {
            title: 'Проверка орфографии',
            text: 'Проверить орфографию всего текста.',
            cls: 'x-html-editor-tip'
        }
    },
        

    //new
    editorStyles: 'orfo_htmleditor.css',
    fullChecker : null,
    wordChecker : null,
    word : '',
    wordEl : null,
    
    // overridden
    initComponent : function(){ 
        Orfo.HtmlEditor.superclass.initComponent.call(this);
        this.on('initialize', this.onInitialize, this);
        this.fullChecker = new Orfo.Checker();
        this.fullChecker.on('checkcomplete', this.onSpellCheck, this);
        this.wordChecker = new Orfo.Checker();
        this.wordChecker.on('checkcomplete', this.onSpellWord, this);
    },
    
    getDocMarkup : function(){
        var h = Ext.fly(this.iframe).getHeight() - this.iframePad * 2;
        return String.format(
            '<html>' +
            '<head>' +
            '<link rel="stylesheet" type="text/css" href="{2}">' +
            '<style type="text/css">body{border: 0; margin: 0; padding: {0}px; height: {1}px; cursor: text}</style>' +
            '</head>' +
            '<body spellcheck="false">' +
            '</body>' +
            '</html>',
            this.iframePad,
            h,
            this.editorStyles
        );
    },

    createLink : function(){
        Ext.Msg.prompt(
            this.createLinkText,
            '',
            function(btn, text){
                if (btn == 'ok') {
                    if(text && text != 'http:/'+'/'){
                        this.relayCmd('createlink', text);
                    }                    
                }
            },
            this,
            false,
            this.defaultLinkValue
        );
    },

    // new
    onInitialize : function(editor){
        if (this.el.dom.spellcheck) {
            this.el.dom.spellcheck = false;
        }
        this.on('editmodechange', this.onEditModeChange, this);
        
        var tb = this.getToolbar();
        var tipsEnabled = Ext.QuickTips && Ext.QuickTips.isEnabled();
        tb.insertButton(tb.items.indexOfKey('sourceedit'), [{
                itemId : 'spellcheck',
                iconCls: 'orfo-btn-spellcheck',
                text: '<b>Проверить</b>',
                scope: this,
                handler: this.spellCheck,
                clickEvent: 'mousedown',
                tooltip: tipsEnabled ? this.buttonTips.spellcheck || undefined : undefined,
                overflowText: this.buttonTips.spellcheck.title || undefined,
                tabIndex: -1
            },
            '->'
        ]);
        tb.enableOverflow = true;
        tb.doLayout();
    },
        
    onEditModeChange : function(editor, srcEdit){
        if (! srcEdit) {
            // Borrowed from Ext.form.HtmlEditor.onFirstFocus().
            // Switches to "no styles" mode after returning from source edit.
            // setValue() and getValue() still break "no styles" mode!
            if(Ext.isGecko){
                try{
                    this.execCmd('useCSS', true);
                    this.execCmd('styleWithCSS', false);
                }catch(e){}
            }                                            
            // Restore contextmenu events.
            this.spellSetCm();
        }
    },
    
    collectText: function(topNode) {
        if (! topNode.hasChildNodes()) {
            return '';
        }
        var textArr = [];
        var needSpace = false;
        var node = topNode;
        var newNode;
        var dir;
        
        while(true) {
            if (dir != -1 && (newNode = node.firstChild) != null) {
                dir = 1;
            } else if ((newNode = node.nextSibling) != null) {
                dir = 0;
            } else if ((newNode = node.parentNode) != null) {
                dir = -1;
                if (newNode == topNode) {
                    break;
                }
            }
            node = newNode;
            if (node.nodeType == 1) {  // ELEMENT
                switch (node.tagName) {
                    case 'BR':
                    case 'DIV':
                    case 'P':
                    case 'LI':
                        if (needSpace) {
                            textArr.push(' ');
                            needSpace = false;
                        }
                }
            } else if (node.nodeType == 3) {  // TEXT
                textArr.push(node.nodeValue);
                needSpace = true;
            }
        }
        return textArr.join('');
    },

    modifyDom : function(topNode){
        if (! topNode.hasChildNodes()) {
            return;
        }
        var doc = this.getDoc();
        var res = this.fullChecker.getNextResult();
        if (! res) {
            return;
        }
        var pos = res.pos;
        var len = res.len;
        var insideRes = false;
        var offset = 0;
        
        var path = [topNode];
        var startPath = [];
        var endPath = [];
        var startOffset = 0;
        var endOffset = 0;
        var ca = 0;
        var level = 0;
        var insertedSpan = null;
        var sameText = false;
        
        var text;
        var needSpace = false;
        
        var node;
        var dir;
        
        while(true) {
            if (dir != -1 && (node = path[level].firstChild) != null) {
                path[++level] = node;
                dir = 1;
            } else if ((node = path[level].nextSibling) != null) {
                path[level] = node;
                dir = 0;
            } else if ((node = path[level].parentNode) != null) {
                path[--level] = node;
                dir = -1;
                if (level == 0 || node == topNode) {
                    break;
                }
            }
            //alert('Node: '+node.nodeName+(node.nodeName == '#text' ? ' <'+node.nodeValue+'>' : ''));
            if (node.nodeType == 1) {  // ELEMENT
                switch (node.tagName) {
                    case 'BR':
                    case 'DIV':
                    case 'P':
                    case 'LI':
                        if (needSpace) {
                            offset++;
                            needSpace = false;
                        }
                }
            } else if (node.nodeType == 3) {  // TEXT
                text = node.nodeValue;
                needSpace = true;
                for (var i = 0; i < text.length; i++) {
                    if (res) {
                        if (insideRes) {
                            // check for range END
                            if (offset == pos + len - 1) {
                                endPath = path.slice(0, level+1);
                                endOffset = i + 1;
                                
                                ca = this.findCommonAncestor(startPath, endPath);
                                if (startPath[startPath.length-1] == endPath[endPath.length-1]) {
                                    sameText = true;
                                } else {
                                    sameText = false;
                                }
                                //alert('commonAncestor: '+(ca >= 0 ? startPath[ca].nodeName : 'invalid'));
                                this.splitDom(startPath, ca, startOffset);
                                if (sameText) {
                                    endPath[endPath.length-1] = startPath[startPath.length-1];
                                    endOffset -= startOffset;
                                }
                                this.splitDom(endPath, ca, endOffset);
                                //alert('node: <'+node.nodeValue+'>'+'\nstartPath: <'+startPath[ca+1].nodeValue+'>'+'\nendPath: <'+endPath[ca+1].nodeValue+'>');
                                insertedSpan = this.surroundByElement(startPath, endPath, ca);

                                // search for next result after inserted span
                                res = this.fullChecker.getNextResult();
                                if (res) {
                                    pos = res.pos;
                                    len = res.len;
                                    insideRes = false;
                                }
                                node = insertedSpan;
                                level = ca + 1;
                                path[level] = node;
                                dir = -1;                  
                                offset++;
                                break;
                            }
                        } else {
                            // check for range START 
                            if (offset == pos) {
                                startPath = path.slice(0, level+1);
                                startOffset = i;
                                insideRes = true;
                            }
                        }
                    }
                    offset++;
                }  // for
            }
        }  // while
    },
    
    findCommonAncestor : function(path1, path2) {
        var minLen = path1.length < path2.length ? path1.length : path2.length;
        for (var i = minLen - 1; i >= 0; i--) {
            if (path1[i] == path2[i]) {
                if (path1[i].nodeType == 3 && i > 0) { // TEXT
                    return i - 1;
                } else {
                    return i;
                }
            }
        }
        return -1;
    },

    splitDom : function(path, ca, offset) {
        var doc = this.getDoc();
        var oldNode, newNode;
        var par;
        
        var first = true;
        for (var i = ca + 1; i < path.length; i++) {
            oldNode = path[i];
            par = path[i-1];
            if (oldNode.nodeType == 3) {  // TEXT
                newNode = doc.createTextNode(oldNode.nodeValue.substring(offset));
                oldNode.nodeValue = oldNode.nodeValue.substring(0, offset);
                if (first && oldNode.nextSibling) {
                    par.insertBefore(newNode, oldNode.nextSibling);
                } else {
                    par.appendChild(newNode);
                }
            } else {
                newNode = oldNode.cloneNode(false);
                if (first && oldNode.nextSibling) {
                    par.insertBefore(newNode, oldNode.nextSibling);
                } else {
                    par.appendChild(newNode);                
                }
            }
            if (! first) {
                // switch siblings to new parent
                while (oldNode.nextSibling) {
                    par.appendChild(oldNode.nextSibling);
                }
            }
            path[i] = newNode;
            first = false;
        }
    },

    surroundByElement : function(startPath, endPath, ca){
        var doc = this.getDoc();
        var par = startPath[ca];
        var sur = doc.createElement('span');
        var startNode = startPath[ca+1];
        var endNode = endPath[ca+1];
        sur.className = 'orfo-misspelled';
        par.insertBefore(sur, startNode);
        
        while (startNode.nextSibling && startNode.nextSibling != endNode) {
            sur.appendChild(startNode.nextSibling);
        }
        sur.insertBefore(startNode, sur.firstChild);
        
        return sur;
    },
    
    spellCheck : function(){
        this.spellClear();
        var body = this.getDoc().body;
        var pureText = this.collectText(body);
        if (pureText.length > 5000) {
            Ext.Msg.alert('Предупреждение', 'Ваш техт длиннее 5000 символов. Только первые 5000 будут проверены.');
        }
        //pureText = pureText.substr(0, 2500);
        //alert('pureText: <'+ pureText.replace(/\s/g, '<s>') +'>');
        var opts = this.spellGetOpts();
        opts += 16;     // always turn off suggestions when checking the whole text.
        this.fullChecker.check(pureText, this.spellGetLangs().join(','), opts.toString());
    },
    
    onSpellCheck : function(){
        var body = this.getDoc().body;
        this.modifyDom(body);
        this.spellSetCm();
    },
        
    spellSetCm : function() {
        var els = Ext.query('span[class=orfo-misspelled]', this.getDoc());
        for (var i = 0; i < els.length; i++) {
            Ext.EventManager.on(els[i], 'contextmenu',
                this.onContextMenu, this, {stopEvent: true});
        }
    },

    onContextMenu : function(e, t){
        var span = e.getTarget('span[class=orfo-misspelled]');
        this.spellWord(span);
    },

    spellWord : function(span){
        var pureText = document.all ? span.innerText : span.textContent;
        if (pureText != '') {
            this.wordEl = span;
            this.word = pureText;
            var opts = this.spellGetOpts();
            this.wordChecker.check(pureText, this.spellGetLangs().join(','), opts.toString());
        }
    },

    onSpellWord : function(){
        // only one result is expected
        var res = this.wordChecker.getNextResult(true);  // with suggestions
        var el = this.wordEl;
        var word = this.word;

        var sv = [{
            text: 'Пропустить',
            handler: this.spellWordIgnore.createDelegate(this, [el])
        }];
        
        if (res) {
            var sugg = res.suggestions;
            var variant;
            for (i = 0; i < sugg.length; i++) {
                var hint = sugg[i].hint;
                var lang = sugg[i].lang;
                sv.push({
                    text: lang+': '+'<b>'+hint+'</b>',
                    handler: this.spellWordReplace.createDelegate(this, [el, hint])
                });
            }
        }
        var editorXY = Ext.get(this.iframe).getXY();
        var scroll = Ext.fly(document).getScroll();

        //var elXY = Ext.get(el).getXY();
        //alert(''
        //    + (el.getBoundingClientRect ? 'getBoundingClientRect() exists\n' : 'getBoundingClientRect() does not exist\n')
        //    + 'Word under cursor, getXY(): x: '+ elXY[0] + ', y: ' + elXY[1] + '\n'
        //    + 'Editor iframe, getXY(): x: '+ editorXY[0] + ', y: ' + editorXY[1] + '\n'
        //    + 'Main document, getScroll(): left: ' + scroll.left + ' top: ' + scroll.top + '\n'
        //    + 'To compensate getXY() oddity inside an iframe I had to substract scroll values back. \n'
        //    + 'Only this way a context menu is positioned correctly in all situations \n'
        //);
        
        editorXY[0] -= scroll.left;
        editorXY[1] -= scroll.top;
        
        var cm = new Ext.menu.Menu({
            //maxHeight: 100,
            defaultAlign: 'tl-br?',
            defaultOffsets: editorXY,
            items: sv
        });
        cm.show(el);
    },

    spellWordIgnore : function(el) {
        var word = document.all ? el.innerText : el.textContent;
        this.wordChecker.reqIscorrect(word);
        this.spellWordFree(el);
    },

    spellWordReplace : function(el, repl) {
        var word = document.all ? el.innerText : el.textContent;
        el.innerHTML = repl;
        this.wordChecker.reqChangelog(word, repl);
        this.spellWordFree(el);
    },

    spellWordFree : function(el){
        while (el.firstChild) {
            el.parentNode.insertBefore(el.firstChild, el);
        }
        el.parentNode.removeChild(el);
    },
    
    spellClear : function() {
        var els = Ext.query('span[class=orfo-misspelled]', this.getDoc());
        for (var i = 0; i < els.length; i++) {
            this.spellWordFree(els[i]);
        }
    },
    
    spellGetOpts : function(){
        var opts = Ext.getCmp('orfoopts').getValue();
        var optVal = 0;
        for (var i = 0; i < opts.length; i++) {
            optVal += parseInt(opts[i].getRawValue());
        }
        //alert(optVal);
        return optVal;
    },
    
    spellGetLangs : function(){
        var langs = Ext.getCmp('orfolangs').getValue();
        var langArr = [];
        for (var i = 0; i < langs.length; i++) {
            langArr.push(langs[i].getRawValue());
        }
        //alert(langArr);
        return langArr;
    }    
});
Ext.reg('orfo.htmleditor', Orfo.HtmlEditor);


Ext.onReady(function(){
    Ext.QuickTips.init();  // enable tooltips
    
    var myWidth, myHeight, myApplyTo;
    if (orfoIframed) {
        myWidth = window.innerWidth;
        myHeight = window.innerHeight;
        myApplyTo = 'orfo_spellcheck_form';
    } else {
        // own page
        myWidth = orfoWidth;
        myHeight = orfoHeight;
        myApplyTo = orfoApplyTo;
    }

    new Ext.FormPanel({
        applyTo: myApplyTo,
        width: myWidth,
        height: myHeight,
        title: 'ОРФО: Проверка правописания он-лайн',
        frame: true,
        layout: 'border',
        items: [{
            region: 'center',
            id: 'orfoeditor',
            xtype: 'orfo.htmleditor'
        },{
            region: 'south',
            height: 260,
            collapsible: true,
            cmargins: '0',
            layout: 'form',
            titleCollapse: true,
            floatable: false,
            toolTemplate: new Ext.XTemplate(
                '<tpl if="id==\'expand-south\'">',
                    '<div class="x-tool x-tool-{id}">&#160;</div>',
                    '<div style="float:right; margin:3px; color:#15428b; font:bold 11px tahoma,arial,verdana,sans-serif;">Языки и опции</div>',
                '</tpl>',
                '<tpl if="id!=\'expand-south\'">',
                    '<div class="x-tool x-tool-{id}">&#160;</div>',
                '</tpl>'
            ),
            items: [{
                xtype: 'fieldset',
                title: 'Языки',
                style: 'margin: 5 5 10 5',
                layout: 'form',
                items: [{
                    id: 'orfolangs',
                    xtype: 'checkboxgroup',
                    hideLabel: true,
                    columns: 3,
                    items: [
                        {boxLabel: 'Русский',       name: 'lang', inputValue: 'ru', checked: true},
                        {boxLabel: 'Украинский',    name: 'lang', inputValue: 'uk'},
                        {boxLabel: 'Английский',    name: 'lang', inputValue: 'en'},
                        {boxLabel: 'Испанский',     name: 'lang', inputValue: 'sp'},
                        {boxLabel: 'Немецкий',      name: 'lang', inputValue: 'de'},
                        {boxLabel: 'Французский',   name: 'lang', inputValue: 'fr'},
                        {boxLabel: 'Итальянский',   name: 'lang', inputValue: 'it'},
                        {boxLabel: 'Португальский', name: 'lang', inputValue: 'pt'},
                        {boxLabel: 'Бразильский',   name: 'lang', inputValue: 'br'}
                    ]                    
                }]
            },{
                xtype: 'fieldset',
                title: 'Опции',
                style: 'margin: 0 5 10 5',
                layout: 'form',
                items: [{
                    id: 'orfoopts',
                    xtype: 'checkboxgroup',
                    hideLabel: true,
                    columns: 2,
                    items: [
                        {boxLabel: 'Игнорировать ЗАГЛАВНЫЕ буквы', name: 'opts', inputValue: 1},
                        {boxLabel: 'Игнорировать цифры',           name: 'opts', inputValue: 2},
                        {boxLabel: 'Искать повторяющиеся слова',   name: 'opts', inputValue: 4, checked: true},
                        {boxLabel: 'Игнорировать Латинские буквы', name: 'opts', inputValue: 8},
                        //{boxLabel: 'Без подсказок',              name: 'opts', inputValue: 16},
                        {boxLabel: 'Игнорировать кАПИТАЛИЗАЦИЮ',   name: 'opts', inputValue: 32},
                        {boxLabel: 'Многоязыковая подсказка',      name: 'opts', inputValue: 64}
                    ]                    
                }]
            }]
        }],
        buttons: [{
            itemId : 'spellcheck',
            iconCls: 'orfo-btn-spellcheck',
            //iconAlign: 'top',
            text: '<b>Проверить</b>',
            handler: function(){ Ext.getCmp('orfoeditor').spellCheck() },
            clickEvent: 'mousedown',
            //tooltip: this.buttonTips.spellcheck || undefined,
            //overflowText: this.buttonTips.spellcheck.title || undefined,
            tabIndex: -1
        }]
    });

});

