// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE

(function (mod) {
    if (typeof exports == "object" && typeof module == "object")
        // CommonJS
        mod(
            require("../../lib/codemirror"),
            require("../xml/xml"),
            require("../javascript/javascript"),
            require("../css/css")
        );
    else if (typeof define == "function" && define.amd)
        // AMD
        define([
            "../../lib/codemirror",
            "../xml/xml",
            "../javascript/javascript",
            "../css/css",
        ], mod);
    // Plain browser env
    else mod(CodeMirror);
})(function (CodeMirror) {
    "use strict";

    var defaultTags = {
        script: [
            ["lang", /(javascript|babel)/i, "javascript"],
            ["type", /^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^$/i, "javascript"],
            ["type", /./, "text/plain"],
            [null, null, "javascript"],
        ],
        style: [
            ["lang", /^css$/i, "css"],
            ["type", /^(text\/)?(x-)?(stylesheet|css)$/i, "css"],
            ["type", /./, "text/plain"],
            [null, null, "css"],
        ],
    };

    function maybeBackup(stream, pat, style) {
        var cur = stream.current(),
            close = cur.search(pat);
        if (close > -1) {
            stream.backUp(cur.length - close);
        } else if (cur.match(/<\/?$/)) {
            stream.backUp(cur.length);
            if (!stream.match(pat, false)) stream.match(cur);
        }
        return style;
    }

    var attrRegexpCache = {};
    function getAttrRegexp(attr) {
        var regexp = attrRegexpCache[attr];
        if (regexp) return regexp;
        return (attrRegexpCache[attr] = new RegExp(
            "\\s+" + attr + "\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*"
        ));
    }

    function getAttrValue(stream, attr) {
        var pos = stream.pos,
            match;
        while (pos >= 0 && stream.string.charAt(pos) !== "<") pos--;
        if (pos < 0) return pos;
        if ((match = stream.string.slice(pos, stream.pos).match(getAttrRegexp(attr))))
            return match[2];
        return "";
    }

    function getTagRegexp(tagName, anchored) {
        return new RegExp((anchored ? "^" : "") + "</s*" + tagName + "s*>", "i");
    }

    function addTags(from, to) {
        for (var tag in from) {
            var dest = to[tag] || (to[tag] = []);
            var source = from[tag];
            for (var i = source.length - 1; i >= 0; i--) dest.unshift(source[i]);
        }
    }

    function findMatchingMode(tagInfo, stream) {
        for (var i = 0; i < tagInfo.length; i++) {
            var spec = tagInfo[i];
            if (!spec[0] || spec[1].test(getAttrValue(stream, spec[0]))) return spec[2];
        }
    }

    CodeMirror.defineMode(
        "htmlmixed",
        function (config, parserConfig) {
            var htmlMode = CodeMirror.getMode(config, {
                name: "xml",
                htmlMode: true,
                multilineTagIndentFactor: parserConfig.multilineTagIndentFactor,
                multilineTagIndentPastTag: parserConfig.multilineTagIndentPastTag,
            });

            var tags = {};
            var configTags = parserConfig && parserConfig.tags,
                configScript = parserConfig && parserConfig.scriptTypes;
            addTags(defaultTags, tags);
            if (configTags) addTags(configTags, tags);
            if (configScript)
                for (var i = configScript.length - 1; i >= 0; i--)
                    tags.script.unshift(["type", configScript[i].matches, configScript[i].mode]);

            function html(stream, state) {
                var tagName = state.htmlState.tagName && state.htmlState.tagName.toLowerCase();
                var tagInfo = tagName && tags.hasOwnProperty(tagName) && tags[tagName];

                var style = htmlMode.token(stream, state.htmlState),
                    modeSpec;

                if (
                    tagInfo &&
                    /\btag\b/.test(style) &&
                    stream.current() === ">" &&
                    (modeSpec = findMatchingMode(tagInfo, stream))
                ) {
                    var mode = CodeMirror.getMode(config, modeSpec);
                    var endTagA = getTagRegexp(tagName, true),
                        endTag = getTagRegexp(tagName, false);
                    state.token = function (stream, state) {
                        if (stream.match(endTagA, false)) {
                            state.token = html;
                            state.localState = state.localMode = null;
                            return null;
                        }
                        return maybeBackup(
                            stream,
                            endTag,
                            state.localMode.token(stream, state.localState)
                        );
                    };
                    state.localMode = mode;
                    state.localState = CodeMirror.startState(
                        mode,
                        htmlMode.indent(state.htmlState, "")
                    );
                }
                return style;
            }

            return {
                startState: function () {
                    var state = htmlMode.startState();
                    return { token: html, localMode: null, localState: null, htmlState: state };
                },

                copyState: function (state) {
                    var local;
                    if (state.localState) {
                        local = CodeMirror.copyState(state.localMode, state.localState);
                    }
                    return {
                        token: state.token,
                        localMode: state.localMode,
                        localState: local,
                        htmlState: CodeMirror.copyState(htmlMode, state.htmlState),
                    };
                },

                token: function (stream, state) {
                    return state.token(stream, state);
                },

                indent: function (state, textAfter) {
                    if (!state.localMode || /^\s*<\//.test(textAfter))
                        return htmlMode.indent(state.htmlState, textAfter);
                    else if (state.localMode.indent)
                        return state.localMode.indent(state.localState, textAfter);
                    else return CodeMirror.Pass;
                },

                innerMode: function (state) {
                    return {
                        state: state.localState || state.htmlState,
                        mode: state.localMode || htmlMode,
                    };
                },
            };
        },
        "xml",
        "javascript",
        "css"
    );

    CodeMirror.defineMIME("text/html", "htmlmixed");
});
