import SetScriptValueCommand from '../command/SetScriptValueCommand';

/**
 * @author mrdoob / http://mrdoob.com/
 */

function Script(editor) {

    var signals = editor.signals;

    var container = new UI.Panel();
    container.setId('script');
    container.setPosition('absolute');
    container.setBackgroundColor('#272822');
    container.setDisplay('none');

    var header = new UI.Panel();
    header.setPadding('10px');
    container.add(header);

    var title = new UI.Text().setColor('#fff');
    header.add(title);

    var buttonSVG = (function () {
        var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('width', 32);
        svg.setAttribute('height', 32);
        var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        path.setAttribute('d', 'M 12,12 L 22,22 M 22,12 12,22');
        path.setAttribute('stroke', '#fff');
        svg.appendChild(path);
        return svg;
    })();

    var close = new UI.Element(buttonSVG);
    close.setPosition('absolute');
    close.setTop('3px');
    close.setRight('1px');
    close.setCursor('pointer');
    close.onClick(function () {

        container.setDisplay('none');

    });
    header.add(close);


    var renderer;

    signals.rendererChanged.add(function (newRenderer) {

        renderer = newRenderer;

    });


    var delay;
    var currentMode;
    var currentScript;
    var currentObject;

    var codemirror = CodeMirror(container.dom, {
        value: '',
        lineNumbers: true,
        matchBrackets: true,
        indentWithTabs: true,
        tabSize: 4,
        indentUnit: 4,
        hintOptions: {
            completeSingle: false
        }
    });
    codemirror.setOption('theme', 'monokai');
    codemirror.on('change', function () {

        if (codemirror.state.focused === false) return;

        clearTimeout(delay);
        delay = setTimeout(function () {

            var value = codemirror.getValue();

            if (!validate(value)) return;

            if (typeof (currentScript) === 'object') {

                if (value !== currentScript.source) {

                    editor.execute(new SetScriptValueCommand(currentObject, currentScript, 'source', value, codemirror.getCursor(), codemirror.getScrollInfo()));

                }
                return;
            }

            if (currentScript !== 'programInfo') return;

            var json = JSON.parse(value);

            if (JSON.stringify(currentObject.material.defines) !== JSON.stringify(json.defines)) {

                var cmd = new SetMaterialValueCommand(currentObject, 'defines', json.defines);
                cmd.updatable = false;
                editor.execute(cmd);

            }
            if (JSON.stringify(currentObject.material.uniforms) !== JSON.stringify(json.uniforms)) {

                var cmd = new SetMaterialValueCommand(currentObject, 'uniforms', json.uniforms);
                cmd.updatable = false;
                editor.execute(cmd);

            }
            if (JSON.stringify(currentObject.material.attributes) !== JSON.stringify(json.attributes)) {

                var cmd = new SetMaterialValueCommand(currentObject, 'attributes', json.attributes);
                cmd.updatable = false;
                editor.execute(cmd);

            }

        }, 300);

    });

    // prevent backspace from deleting objects
    var wrapper = codemirror.getWrapperElement();
    wrapper.addEventListener('keydown', function (event) {

        event.stopPropagation();

    });

    // validate

    var errorLines = [];
    var widgets = [];

    var validate = function (string) {

        var valid;
        var errors = [];

        return codemirror.operation(function () {

            while (errorLines.length > 0) {

                codemirror.removeLineClass(errorLines.shift(), 'background', 'errorLine');

            }

            while (widgets.length > 0) {

                codemirror.removeLineWidget(widgets.shift());

            }

            //

            switch (currentMode) {

                case 'javascript':

                    try {

                        var syntax = esprima.parse(string, { tolerant: true });
                        errors = syntax.errors;

                    } catch (error) {

                        errors.push({

                            lineNumber: error.lineNumber - 1,
                            message: error.message

                        });

                    }

                    for (var i = 0; i < errors.length; i++) {

                        var error = errors[i];
                        error.message = error.message.replace(/Line [0-9]+: /, '');

                    }

                    break;

                case 'json':

                    errors = [];

                    jsonlint.parseError = function (message, info) {

                        message = message.split('\n')[3];

                        errors.push({

                            lineNumber: info.loc.first_line - 1,
                            message: message

                        });

                    };

                    try {

                        jsonlint.parse(string);

                    } catch (error) {

                        // ignore failed error recovery

                    }

                    break;

                case 'glsl':

                    try {

                        var shaderType = currentScript === 'vertexShader' ?
                            glslprep.Shader.VERTEX : glslprep.Shader.FRAGMENT;

                        glslprep.parseGlsl(string, shaderType);

                    } catch (error) {

                        if (error instanceof glslprep.SyntaxError) {

                            errors.push({

                                lineNumber: error.line,
                                message: "Syntax Error: " + error.message

                            });

                        } else {

                            console.error(error.stack || error);

                        }

                    }

                    if (errors.length !== 0) break;
                    if (renderer instanceof THREE.WebGLRenderer === false) break;

                    currentObject.material[currentScript] = string;
                    currentObject.material.needsUpdate = true;
                    signals.materialChanged.dispatch(currentObject.material);

                    var programs = renderer.info.programs;

                    valid = true;
                    var parseMessage = /^(?:ERROR|WARNING): \d+:(\d+): (.*)/g;

                    for (var i = 0, n = programs.length; i !== n; ++i) {

                        var diagnostics = programs[i].diagnostics;

                        if (diagnostics === undefined ||
                            diagnostics.material !== currentObject.material) continue;

                        if (!diagnostics.runnable) valid = false;

                        var shaderInfo = diagnostics[currentScript];
                        var lineOffset = shaderInfo.prefix.split(/\r\n|\r|\n/).length;

                        while (true) {

                            var parseResult = parseMessage.exec(shaderInfo.log);
                            if (parseResult === null) break;

                            errors.push({

                                lineNumber: parseResult[1] - lineOffset,
                                message: parseResult[2]

                            });

                        } // messages

                        break;

                    } // programs

            } // mode switch

            for (var i = 0; i < errors.length; i++) {

                var error = errors[i];

                var message = document.createElement('div');
                message.className = 'esprima-error';
                message.textContent = error.message;

                var lineNumber = Math.max(error.lineNumber, 0);
                errorLines.push(lineNumber);

                codemirror.addLineClass(lineNumber, 'background', 'errorLine');

                var widget = codemirror.addLineWidget(lineNumber, message);

                widgets.push(widget);

            }

            return valid !== undefined ? valid : errors.length === 0;

        });

    };

    // tern js autocomplete

    var server = new CodeMirror.TernServer({
        caseInsensitive: true,
        plugins: { threejs: null }
    });

    codemirror.setOption('extraKeys', {
        'Ctrl-Space': function (cm) { server.complete(cm); },
        'Ctrl-I': function (cm) { server.showType(cm); },
        'Ctrl-O': function (cm) { server.showDocs(cm); },
        'Alt-.': function (cm) { server.jumpToDef(cm); },
        'Alt-,': function (cm) { server.jumpBack(cm); },
        'Ctrl-Q': function (cm) { server.rename(cm); },
        'Ctrl-.': function (cm) { server.selectName(cm); }
    });

    codemirror.on('cursorActivity', function (cm) {

        if (currentMode !== 'javascript') return;
        server.updateArgHints(cm);

    });

    codemirror.on('keypress', function (cm, kb) {

        if (currentMode !== 'javascript') return;
        var typed = String.fromCharCode(kb.which || kb.keyCode);
        if (/[\w\.]/.exec(typed)) {

            server.complete(cm);

        }

    });


    //

    signals.editorCleared.add(function () {

        container.setDisplay('none');

    });

    signals.editScript.add(function (object, script) {

        var mode, name, source;

        if (typeof (script) === 'object') {

            mode = 'javascript';
            name = script.name;
            source = script.source;
            title.setValue(object.name + ' / ' + name);

        } else {

            switch (script) {

                case 'vertexShader':

                    mode = 'glsl';
                    name = 'Vertex Shader';
                    source = object.material.vertexShader || "";

                    break;

                case 'fragmentShader':

                    mode = 'glsl';
                    name = 'Fragment Shader';
                    source = object.material.fragmentShader || "";

                    break;

                case 'programInfo':

                    mode = 'json';
                    name = 'Program Properties';
                    var json = {
                        defines: object.material.defines,
                        uniforms: object.material.uniforms,
                        attributes: object.material.attributes
                    };
                    source = JSON.stringify(json, null, '\t');

            }
            title.setValue(object.material.name + ' / ' + name);

        }

        currentMode = mode;
        currentScript = script;
        currentObject = object;

        container.setDisplay('');
        codemirror.setValue(source);
        if (mode === 'json') mode = { name: 'javascript', json: true };
        codemirror.setOption('mode', mode);

    });

    signals.scriptRemoved.add(function (script) {

        if (currentScript === script) {

            container.setDisplay('none');

        }

    });

    signals.refreshScriptEditor.add(function (object, script, cursorPosition, scrollInfo) {

        if (currentScript !== script) return;

        // copying the codemirror history because "codemirror.setValue(...)" alters its history

        var history = codemirror.getHistory();
        title.setValue(object.name + ' / ' + script.name);
        codemirror.setValue(script.source);

        if (cursorPosition !== undefined) {

            codemirror.setCursor(cursorPosition);
            codemirror.scrollTo(scrollInfo.left, scrollInfo.top);

        }
        codemirror.setHistory(history); // setting the history to previous state

    });

    return container;

};

export default Script;