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

define([
    'codemirror/lib/codemirror'
], function(
    CodeMirror
) {
    'use strict';

    var GUTTER_ID = 'CodeMirror-lint-markers';
    var MARKER_CLASS_STEM = 'CodeMirror-lint-marker-';
    var MARK_CLASS_STEM = 'CodeMirror-lint-mark-';
    var MSG_CLASS_STEM = 'CodeMirror-lint-message-';

    function showTooltip (e, content) {
        var tt = document.createElement('div');
        tt.className = 'CodeMirror-lint-tooltip';
        tt.appendChild(content.cloneNode(true));
        document.body.appendChild(tt);

        function position (e) {
            if (!tt.parentNode) {
                return CodeMirror.off(document, 'mousemove', position);
            }
            tt.style.top = Math.max(0, e.clientY - tt.offsetHeight - 5) + 'px';
            tt.style.left = (e.clientX + 5) + 'px';
        }
        CodeMirror.on(document, 'mousemove', position);
        position(e);
        if (tt.style.opacity != null) {
            tt.style.opacity = 1;
        }
        return tt;
    }
    function rm (elt) {
        if (elt.parentNode) {
            elt.parentNode.removeChild(elt);
        }
    }
    function hideTooltip (tt) {
        if (!tt.parentNode) {
            return;
        }
        if (tt.style.opacity == null) {
            rm(tt);
        }
        tt.style.opacity = 0;
        setTimeout(function() { rm(tt); }, 600);
    }

    function showTooltipFor (e, content, node) {
        var tooltip = showTooltip(e, content);
        function hide () {
            CodeMirror.off(node, 'mouseout', hide);
            if (tooltip) { hideTooltip(tooltip); tooltip = null; }
        }
        var poll = setInterval(function() {
            if (tooltip) for (var n = node;; n = n.parentNode) {
                if (n && n.nodeType == 11) n = n.host;
                if (n == document.body) return;
                if (!n) { hide(); break; }
            }
            if (!tooltip) return clearInterval(poll);
        }, 400);
        CodeMirror.on(node, 'mouseout', hide);
    }

    function LintState (cm, options, hasGutter) {
        this.marked = [];
        this.options = options;
        this.timeout = null;
        this.hasGutter = hasGutter;
        this.onMouseOver = function(e) { onMouseOver(cm, e); };
        this.waitingFor = 0;
    }

    function parseOptions (_cm, options) {
        if (options instanceof Function) {
            return {getAnnotations: options};
        }
        if (!options || options === true) {
            options = {};
        }
        return options;
    }

    function clearMarks (cm) {
        var state = cm.state.lynt;
        if (state.hasGutter) cm.clearGutter(GUTTER_ID);
        for (var i = 0; i < state.marked.length; ++i)
            state.marked[i].clear();
        state.marked.length = 0;
    }

    function makeMarker (labels, severity, multiple, tooltips) {
        var marker = document.createElement('div'), inner = marker;
        marker.className = MARKER_CLASS_STEM + severity;
        if (multiple) {
            inner = marker.appendChild(document.createElement('div'));
            inner.className = MARKER_CLASS_STEM + 'multiple';
        }

        if (tooltips != false) CodeMirror.on(inner, 'mouseover', function(e) {
            showTooltipFor(e, labels, inner);
        });

        return marker;
    }

    function getMaxSeverity (a, b) {
        if (a == 'error') return a;
        else return b;
    }

    function groupByLine (annotations) {
        var lines = [];
        for (var i = 0; i < annotations.length; ++i) {
            var ann = annotations[i], line = ann.from.line;
            (lines[line] || (lines[line] = [])).push(ann);
        }
        return lines;
    }

    function annotationTooltip (ann) {
        var severity = ann.severity;
        if (!severity) {
            severity = 'error';
        }
        var tip = document.createElement('div');
        tip.className = MSG_CLASS_STEM + severity;
        tip.appendChild(document.createTextNode(ann.message));
        return tip;
    }

    function lintAsync (cm, getAnnotations, passOptions) {
        var state = cm.state.lynt;
        var id = ++state.waitingFor;
        function abort () {
            id = -1;
            cm.off('change', abort);
        }
        cm.on('change', abort);
        getAnnotations(cm.getValue(), function(annotations, arg2) {
            cm.off('change', abort);
            if (state.waitingFor != id) {
                return;
            }
            if (arg2 && annotations instanceof CodeMirror) {
                annotations = arg2;
            }
            updateLinting(cm, annotations);
        }, passOptions, cm);
    }

    function startLinting (cm) {
        var state = cm.state.lynt, options = state.options;
        var passOptions = options.options || options; // Support deprecated passing of `options` property in options
        var getAnnotations = options.getAnnotations || cm.getHelper(CodeMirror.Pos(0, 0), 'lynt');
        if (!getAnnotations) {
            return;
        }
        if (options.async || getAnnotations.async) {
            lintAsync(cm, getAnnotations, passOptions);
        }
        else {
            updateLinting(cm, getAnnotations(cm.getValue(), passOptions, cm));
        }
    }

    function updateLinting (cm, annotationsNotSorted) {
        clearMarks(cm);
        var state = cm.state.lynt, options = state.options;

        var annotations = groupByLine(annotationsNotSorted);

        for (var line = 0; line < annotations.length; ++line) {
            var anns = annotations[line];
            if (!anns) continue;

            var maxSeverity = null;
            var tipLabel = state.hasGutter && document.createDocumentFragment();

            for (var i = 0; i < anns.length; ++i) {
                var ann = anns[i];
                var severity = ann.severity;
                if (!severity) severity = 'error';
                maxSeverity = getMaxSeverity(maxSeverity, severity);

                if (options.formatAnnotation) ann = options.formatAnnotation(ann);
                if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann));

                if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, {
                    className: MARK_CLASS_STEM + severity,
                    __annotation: ann
                }));
            }

            if (state.hasGutter)
                cm.setGutterMarker(line, GUTTER_ID, makeMarker(tipLabel, maxSeverity, anns.length > 1,
                                                                                                             state.options.tooltips));
        }
        if (options.onUpdateLinting) options.onUpdateLinting(annotationsNotSorted, annotations, cm);
    }

    function onChange (cm) {
        var state = cm.state.lynt;
        if (!state) return;
        clearTimeout(state.timeout);
        state.timeout = setTimeout(function () {startLinting(cm);}, state.options.delay || 500);
    }

    function popupTooltips (annotations, e) {
        var target = e.target || e.srcElement;
        var tooltip = document.createDocumentFragment();
        for (var i = 0; i < annotations.length; i++) {
            var ann = annotations[i];
            tooltip.appendChild(annotationTooltip(ann));
        }
        showTooltipFor(e, tooltip, target);
    }

    function onMouseOver (cm, e) {
        var target = e.target || e.srcElement;
        if (!/\bCodeMirror-lint-mark-/.test(target.className)) {
            return;
        }
        var box = target.getBoundingClientRect();
        var   x = (box.left + box.right) / 2;
        var   y = (box.top + box.bottom) / 2;
        var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, 'client'));

        var annotations = [];
        for (var i = 0; i < spans.length; ++i) {
            var ann = spans[i].__annotation;
            if (ann) {
                annotations.push(ann);
            }
        }
        if (annotations.length) {
            popupTooltips(annotations, e);
        }
    }

    CodeMirror.defineOption('lynt', false, function(cm, val, old) {
        if (old && old != CodeMirror.Init) {
            clearMarks(cm);
            if (cm.state.lynt.options.lintOnChange !== false) {
                cm.off('change', onChange);
            }
            CodeMirror.off(cm.getWrapperElement(), 'mouseover', cm.state.lynt.onMouseOver);
            clearTimeout(cm.state.lynt.timeout);
            delete cm.state.lynt;
        }

        if (val) {
            var gutters = cm.getOption('gutters'), hasLintGutter = false;
            for (var i = 0; i < gutters.length; ++i) {
                if (gutters[i] == GUTTER_ID) {
                    hasLintGutter = true;
                }
            }
            var state = cm.state.lynt = new LintState(cm, parseOptions(cm, val), hasLintGutter);
            if (state.options.lintOnChange !== false) {
                cm.on('change', onChange);
            }
            if (state.options.tooltips != false && state.options.tooltips != 'gutter') {
                CodeMirror.on(cm.getWrapperElement(), 'mouseover', state.onMouseOver);
            }
            startLinting(cm);
        }
    });

    CodeMirror.defineExtension('performLint', function() {
        if (this.state.lynt) startLinting(this);
    });
});
