if (typeof process !== "undefined") {
    require("./test/mockdom");
}

"use strict";

var Range = require("./range").Range;
var Editor = require("./editor").Editor;
var EditSession = require("./edit_session").EditSession;
var VirtualRenderer = require("./virtual_renderer").VirtualRenderer;
var vim = require("./keyboard/vim");
var assert = require("./test/assertions");
var lang = require("./lib/lang");

function setScreenPosition(node, rect) {
    node.style.left = rect[0] + "px";
    node.style.top = rect[1] + "px";
    node.style.width = rect[2] + "px";
    node.style.height = rect[3] + "px";
}

var editor = null;
module.exports = {
    setUp: function() {
        require("./config").setLoader(function(moduleName, cb) {
            if (moduleName == "ace/ext/error_marker")
                return cb(null, require("./ext/error_marker"));
            if (moduleName == "ace/mode/javascript")
                return cb(null, require("./mode/javascript"));
            throw new Error("module not configured " + moduleName);
        });
        
        if (editor)
            editor.destroy();
        var el = document.createElement("div");

        el.style.left = "20px";
        el.style.top = "30px";
        el.style.width = "300px";
        el.style.height = "100px";
        el.style.position = "fixed";
        document.body.appendChild(el);
        var renderer = new VirtualRenderer(el);
        editor = new Editor(renderer);
        editor.on("destroy", function() {
            el.remove();
        });
    },
    tearDown: function() {
        editor && editor.destroy();
        editor = null;
    },
    "test: screen2text the column should be rounded to the next character edge" : function() {
        var renderer = editor.renderer;

        renderer.setPadding(0);
        renderer.setSession(new EditSession("1234"));

        var r = renderer.scroller.getBoundingClientRect();
        function testPixelToText(x, y, row, column) {
            assert.position(renderer.screenToTextCoordinates(x+r.left, y+r.top), row, column);
        }

        testPixelToText(renderer.characterWidth * 0.4, 0, 0, 0);
        testPixelToText(renderer.characterWidth * 0.5, 0, 0, 1);
        testPixelToText(renderer.characterWidth * 0.9, 0, 0, 1);
        testPixelToText(renderer.characterWidth * 1.0, 0, 0, 1);
        testPixelToText(renderer.characterWidth * 1.4, 0, 0, 1);
        testPixelToText(renderer.characterWidth * 1.5, 0, 0, 2);
    },
    "test: handle css transforms" : function() {
        editor.setValue("hello world\nabc -א,ב,ג+ xyz");
        var renderer = editor.renderer;
        var fontMetrics = renderer.$fontMetrics;
        editor.setOption("hasCssTransforms", true);
        setScreenPosition(editor.container, [20, 30, 300, 100]);
        
        renderer.onResize(true);
        
        editor.container.style.transformOrigin = "0 0";
        var H1 = -0.0007, H2 = -0.001;
        var m0 = 0.7, m1 = 0.1, m2 = 0.3, m3 = 0.82;
        var t1 = 100, t2 = 20;
        function testTransform() {
            fontMetrics.config.$transformData = null;
            editor.container.style.transform = `matrix3d(
                ${m0},  ${m2},    0,   ${H1},
                ${m1},  ${m3},    0,   ${H2}, 
                0,      0,        1,    0, 
                ${t1},  ${t2},    0,    1
            )`;
            
            var expected = [
                m0 - H1* t1, m1 - H2* t1, 0,
                m2 - H1* t2, m3 - H2* t2, 0,
                H1,          H2,          1
            ];

            project(expected, [20, 30]); 

            var transform = editor.renderer.$fontMetrics.getTransform();

            for (var i = 0; i < 9; i++) {
                assert.ok(Math.abs(transform.M[i] - expected[i]) < 10e-6, `Expected M[${i}] to be approximately ${expected[i]}, but got ${transform.M[i]}`);
            }

            assert.equal(transform.t + "", [100 + 20, 20 + 30] + "");
            
            var p = project(expected, [
                renderer.gutterWidth + renderer.$padding + renderer.characterWidth * 4,
                renderer.lineHeight / 2
            ]);
            p[0] += transform.t[0];
            p[1] += transform.t[1];

            var pos = renderer.pixelToScreenCoordinates(p[0], p[1]);
            
            var docPos = editor.session.screenToDocumentPosition(pos.row, pos.column);
            assert.position(docPos, 0, 4);

            editor.renderer.$loop._flush();
        }

        testTransform();
        H1 = H2 = 0;
        testTransform();
        m0 = m1 = m3 = 1;
        m2 = -1;
        testTransform();

        function project(M, point) {
            var px = point[0], py = point[1];
            var k = 1 / (M[6] * px + M[7] * py + M[8]);
            return [(M[0] * px + M[1] * py + M[2]) * k, (M[3] * px + M[4] * py + M[5]) * k];
        }
    },
    "test pixelposition in surrogate pairs": function() {
        var renderer = editor.renderer;
        editor.setValue("ab\ud83d\ude02cd");
        renderer.onResize(true);
        var p1 = renderer.textToScreenCoordinates(0, 2);
        var p2 = renderer.textToScreenCoordinates(0, 4);
        for (var i = 0.01; i <= 1.1; i+=0.1) {
            var pos = renderer.pixelToScreenCoordinates((1-i) * p1.pageX+ i * p2.pageX, p1.pageY);
            assert.position(pos, 0, i < 0.5 ? 2 : 4);
        }
    },
    "test scrollmargin + autosize": async function(done) {
        editor.setOptions({
            maxLines: 100,
            wrap: true
        });        
        editor.renderer.setScrollMargin(10, 10);
        editor.setValue("\n\n");
        editor.setValue("\n\n\n\n");
        await editor.renderer.once("afterRender");
        done();
    },

    "test scrollbars after value change": function() {
        editor.container.style.height = "0px";
        editor.setOptions({
            maxLines: 8,
        });
        var renderCount = 0;
        editor.renderer.on("afterRender", function(e) {
            renderCount++;
        });
        // horizontal scroll
        editor.setValue("\n");
        editor.resize(true);
        editor.renderer.$loop._flush(); // 1
        assert.notOk(editor.renderer.scrollBarH.isVisible);
        assert.notOk(editor.renderer.scrollBar.isVisible);
        editor.setValue("\n\n\n\n" + "_".repeat(30));
        editor.resize(true);
        editor.renderer.$loop._flush(); // 2
        assert.notOk(editor.renderer.scrollBarH.isVisible);
        assert.notOk(editor.renderer.scrollBar.isVisible);
        editor.setValue("\n\n\n\n" + "_".repeat(100));
        editor.resize(true);
        editor.renderer.$loop._flush(); // 3
        assert.ok(editor.renderer.scrollBarH.isVisible);
        assert.notOk(editor.renderer.scrollBar.isVisible);
        // vertical scroll
        editor.setValue("\n".repeat(9));
        editor.resize(true);
        editor.renderer.$loop._flush(); // 4
        assert.notOk(editor.renderer.scrollBarH.isVisible);
        assert.ok(editor.renderer.scrollBar.isVisible);
        // vertical and horizontal scroll
        editor.setValue("\n");
        editor.resize(true);
        editor.renderer.$loop._flush(); // 5
        editor.setValue("\n".repeat(9) + "_".repeat(100));
        editor.resize(true);
        editor.renderer.$loop._flush(); // 6
        assert.notOk(editor.renderer.scrollBarH.isVisible);
        assert.ok(editor.renderer.scrollBar.isVisible);
        editor.resize(true);
        editor.renderer.$loop._flush(); // 7
        // autosize changes vscroll value in which case updates longestLine
        // this is why it renders an extra time
        assert.ok(editor.renderer.scrollBarH.isVisible);
        assert.ok(editor.renderer.scrollBar.isVisible);
        assert.equal(renderCount, 7);
    },

    "test autosize from 0 height": function() {
        editor.container.style.height = "0px";
        editor.textInput.getElement().style.position = "fixed";
        editor.container.style.lineHeight = 1;
        editor.setOptions({
            fontSize: 9
        });

        editor.resize(true);
        editor.setOptions({
            maxLines: 100
        });

        editor.resize(true);

        editor.resize(true);
        editor.renderer.$size = {};

        var renderCount = 0;
        editor.renderer.on("afterRender", function(e) {
            renderCount++;
        });
        editor.setValue("1");
        editor.renderer.$loop._flush();
        assert.equal(editor.container.style.height, "9px");

        editor.setValue("\n\n");
        editor.renderer.$loop._flush();
        assert.equal(editor.container.style.height, "27px");
        
        editor.container.remove();
        editor.setValue("\n\n\n");
        editor.resize(true);
        editor.renderer.$loop._flush();
        editor.resize(true);
        assert.equal(renderCount, 2);
    },
    
    "test invalid valus of minLines": function() {
        editor.setOptions({
            maxLines: Infinity,
            minLines: Infinity
        });
        editor.renderer.$loop._flush();
        editor.setOptions({
            maxLines: NaN,
            minLines: NaN
        });
        editor.renderer.$loop._flush();
        editor.setOptions({
            maxLines: Number.MAX_SAFE_INTEGER + 1,
            minLines: Number.MAX_SAFE_INTEGER + 1
        });
        editor.renderer.$loop._flush();
    },
    
    "test line widgets": function() {
        editor.session.setValue("a\nb|c\nd");
        editor.session.setAnnotations([{row: 1, column: 2, type: "error"}]);
        editor.execCommand(editor.commands.byName.goToNextError);
        assert.position(editor.getCursorPosition(), 1, 2);
        editor.renderer.$loop._flush();
        assert.ok(editor.session.lineWidgets[1]);
    },
    
    "test wrapped text rendering": function() {
        editor.setValue("a".repeat(452) + "\n" + "b".repeat(100) + "\nxxxxxx", -1);
        editor.container.style.height = "500px";
        editor.setOption("wrap", 40);
        editor.resize(true);
        editor.renderer.$loop._flush();
        
        assert.equal(editor.renderer.$changedLines, null);
        editor.session.remove(new Range(0, 10, 0, 350));
        editor.session.insert({row: 1, column: 1}, "l".repeat(350));
        editor.renderer.$loop._flush();
        var lines = editor.renderer.$textLayer.element.children;
        assert.notEqual(lines[0].style.height, lines[1].style.height);
        assert.equal(lines[0].style.height, lines[1].style.top);
    },
    
    "test resize": function() {
        editor.setValue("Juhu kinners!");
        editor.resize(true);
    },

    "test placeholder": function() {
        editor.setOption("placeholder", "hello");
        assert.equal(editor.renderer.content.textContent, "hello");

        editor.setOption("placeholder", "new");
        assert.equal(editor.renderer.content.textContent, "new");

        editor.setValue("test");
        editor.renderer.$loop._flush();
        assert.equal(editor.renderer.content.textContent, "test");

        editor.setOption("placeholder", "only visible for empty value");
        assert.equal(editor.renderer.content.textContent, "test");

        editor.setValue("");
        editor.renderer.$loop._flush();
        editor._signal("input", {});
        assert.equal(editor.renderer.content.textContent, "only visible for empty value");
    },
    "test: highlight indent guide": async function (done) {
        editor.session.setValue(
            "function Test() {\n" + "    function Inner() {\n" + "        \n" + "        \n" + "    }\n" + "}");
        editor.setOption("highlightIndentGuides", false);
        editor.setOption("wrap", 10); // to make sure higlight works with wrapped lines
        editor.session.selection.$setSelection(1, 22, 1, 22);
        editor.resize(true);

        function assertIndentGuides(activeIndentGuidesCount) {
            var activeIndentGuides = editor.container.querySelectorAll(".ace_indent-guide-active");
            assert.equal(activeIndentGuides.length, activeIndentGuidesCount);
        }

        assertIndentGuides( 0);

        editor.setOption("highlightIndentGuides", true);
        assertIndentGuides( 2);

        editor.session.selection.clearSelection();
        editor.session.selection.$setSelection(1, 15, 1, 15);
        editor.resize(true);
        assertIndentGuides( 0);

        editor.session.selection.clearSelection();
        editor.session.selection.$setSelection(4, 5, 4, 5);

        await lang.sleep(100);
        assertIndentGuides(2);
        done();
    },
    "test annotation marks": function() {
        function findPointFillStyle(imageData, x, y) {
            var data = imageData.slice(4 * y, 4 * (y + 1));
            var a = Math.round(data[3] / 256 * 100);
            if (a == 100) return "rgb(" + data.slice(0, 3).join(",") + ")";
            return "rgba(" + data.slice(0, 3).join(",") + "," + (a / 100) + ")";
        }

        function assertCoordsColor(expected) {
            var imageData = context.getImageData(0, 0, 1, 100).data;
            for (var el of expected) {
                assert.equal(findPointFillStyle(imageData, el.x, el.y), el.color);
            }
        }

        var renderer = editor.renderer;
        renderer.container.scrollHeight = 100;
        renderer.layerConfig.maxHeight = 200;
        renderer.layerConfig.lineHeight = 14;

        editor.setOptions({
            customScrollbar: true,
            vScrollBarAlwaysVisible: true
        });
        editor.setValue("a" + "\n".repeat(100) + "b" + "\nxxxxxx", -1);
        editor.session.setAnnotations([
            {
                row: 1,
                column: 2,
                type: "error"
            }, {
                row: 4,
                column: 1,
                type: "warning"
            }, {
                row: 20,
                column: 1,
                type: "info"
            }
        ]);
        renderer.$loop._flush();
        var context = renderer.$scrollDecorator.canvas.getContext("2d");
        var scrollDecoratorColors = renderer.$scrollDecorator.colors.light;
        var values = [
            // reflects cursor position on canvas
            {x: 0, y: 0, color: "rgba(0,0,0,0.5)"},
            // reflects error annotation mark on canvas overlapped by cursor
            {x: 0, y: 2, color: scrollDecoratorColors.error},
            // default value
            {x: 0, y: 3, color: "rgba(0,0,0,0)"},
            // reflects warning annotation mark on canvas
            {x: 0, y: 4, color: scrollDecoratorColors.warning},
            {x: 0, y: 5, color: scrollDecoratorColors.warning},
            {x: 0, y: 6, color: "rgba(0,0,0,0)"},
            {x: 0, y: 20, color: scrollDecoratorColors.info},
            {x: 0, y: 21, color: scrollDecoratorColors.info}
        ];
        assertCoordsColor(values);
        editor.moveCursorTo(5, 6);
        renderer.$loop._flush();
        values = [
            {x: 0, y: 0, color: "rgba(0,0,0,0)"},
            {x: 0, y: 1, color: scrollDecoratorColors.error},
            {x: 0, y: 2, color: scrollDecoratorColors.error},
            {x: 0, y: 3, color: "rgba(0,0,0,0)"},
            {x: 0, y: 4, color: scrollDecoratorColors.warning},
            {x: 6, y: 6, color: "rgba(0,0,0,0.5)"}
        ];
        assertCoordsColor(values);
        renderer.session.addFold("...", new Range(0, 0, 3, 2));
        editor.moveCursorTo(10, 0);
        renderer.$loop._flush();
        values = [
            {x: 0, y: 0, color: scrollDecoratorColors.error},
            {x: 0, y: 1, color: scrollDecoratorColors.error},
            {x: 0, y: 2, color: scrollDecoratorColors.warning},
            {x: 0, y: 3, color: "rgba(0,0,0,0)"},
            {x: 0, y: 4, color: "rgba(0,0,0,0)"},
            {x: 0, y: 5, color: "rgba(0,0,0,0)"},
            {x: 0, y: 6, color: "rgba(0,0,0,0)"}
        ];
        assertCoordsColor(values);
    },
    "test ghost text": function() {
        editor.session.setValue("abcdef");
        editor.setGhostText("Ghost");

        editor.renderer.$loop._flush();
        assert.equal(editor.renderer.content.textContent, "Ghostabcdef");

        editor.setGhostText("Ghost", {row: 0, column: 3});

        editor.renderer.$loop._flush();
        assert.equal(editor.renderer.content.textContent, "abcGhostdef");

        editor.setGhostText("Ghost", {row: 0, column: 6});

        editor.renderer.$loop._flush();
        assert.equal(editor.renderer.content.textContent, "abcdefGhost");

        editor.removeGhostText();

        editor.renderer.$loop._flush();
        assert.equal(editor.renderer.content.textContent, "abcdef");
    },

    "test multiline ghost text": function() {
        editor.session.setValue("abcdef");
        editor.renderer.$loop._flush();

        editor.setGhostText("Ghost1\nGhost2\nGhost3", {row: 0, column: 6});

        editor.renderer.$loop._flush();
        assert.equal(editor.renderer.content.textContent, "abcdefGhost1");
        
        assert.equal(editor.session.lineWidgets[0].el.innerHTML, `<div><span class="ace_ghost_text">Ghost2</span></div><div><span class="ace_ghost_text">Ghost3</span><span></span></div>`);

        editor.removeGhostText();

        editor.renderer.$loop._flush();
        assert.equal(editor.renderer.content.textContent, "abcdef");
        
        assert.equal(editor.session.lineWidgets, null);
    },
    "test long multiline ghost text": function() {
        editor.session.setValue("abcdef");
        editor.renderer.$loop._flush();

        editor.setGhostText("This is a long test text that is longer than 30 characters\n\nGhost3", 
            {row: 0, column: 6});

        editor.renderer.$loop._flush();
        assert.equal(editor.renderer.content.textContent, "abcdefThis is a long test text that is longer than ");

        assert.equal(editor.session.lineWidgets[0].el.innerHTML, `<div class="ghost_text_line_wrapped"><span class="ace_ghost_text">30 characters</span></div><div><span class="ace_ghost_text"> </span></div><div><span class="ace_ghost_text">Ghost3</span><span></span></div>`);

        editor.removeGhostText();

        editor.renderer.$loop._flush();
        assert.equal(editor.renderer.content.textContent, "abcdef");

        assert.equal(editor.session.lineWidgets, null);
    },
    "test: brackets highlighting": async function (done) {
        var renderer = editor.renderer;
        editor.session.setValue(
            "function Test() {\n" + "    function Inner(){\n" + "        \n" + "        \n" + "    }\n" + "}");
        editor.session.selection.$setSelection(1, 21, 1, 21);
        renderer.$loop._flush();

        await lang.sleep(60);
        assert.ok(editor.session.$bracketHighlight);
        assert.range(editor.session.$bracketHighlight.ranges[0], 1, 20, 1, 21);
        assert.range(editor.session.$bracketHighlight.ranges[1], 4, 4, 4, 5);

        editor.session.selection.$setSelection(1, 16, 1, 16);

        await lang.sleep(60);
        assert.ok(editor.session.$bracketHighlight == null);
        editor.setKeyboardHandler(vim.handler);
        editor.session.selection.$setSelection(1, 20, 1, 20);

        await lang.sleep(60);
        assert.ok(editor.session.$bracketHighlight);
        assert.range(editor.session.$bracketHighlight.ranges[0], 1, 20, 1, 21);
        assert.range(editor.session.$bracketHighlight.ranges[1], 4, 4, 4, 5);
        done();
    },
    "test: scroll cursor into view": function() {
        editor.renderer.$loop._flush();
        
        function X(n) {
            return "X".repeat(n);
        }
        editor.session.setValue(`${X(10)}\n${X(1000)}}`);

        var initialContentLeft = editor.renderer.content.getBoundingClientRect().left;

        // Scroll so far to the right that the first line is completely hidden
        editor.session.selection.$setSelection(1, 1000, 1, 1000);
        editor.renderer.scrollCursorIntoView();
        editor.renderer.$loop._flush();

        editor.session.selection.$setSelection(0, 10, 0, 10);
        editor.renderer.scrollCursorIntoView();
        editor.renderer.$loop._flush();

        var contentLeft = editor.renderer.content.getBoundingClientRect().left;
        var scrollDelta = initialContentLeft - contentLeft;

        const leftBoundPixelPos = editor.renderer.$cursorLayer.getPixelPosition({row: 0, column: 8}).left;
        const rightBoundPixelPos = editor.renderer.$cursorLayer.getPixelPosition({row: 0, column: 9}).left;
        assert.ok(
            scrollDelta >= leftBoundPixelPos && scrollDelta < rightBoundPixelPos,
            "Expected content to have been scrolled two characters beyond the cursor"
        );
    },
    "test: set gutter class": function(done) {
        editor.session.setMode("ace/mode/javascript", function() {
            editor.session.setValue("x = {\n  foo: 1\n}");
            editor.execCommand("toggleFoldWidget");
            editor.renderer.$loop._flush();
            assert.equal(editor.renderer.$loop.changes, 0);
            var cell = editor.renderer.$gutterLayer.$lines.cells[0];
            assert.equal(cell.element.childNodes[1].className, "ace_fold-widget ace_start ace_closed");
            assert.equal(cell.element.className.trim(), "ace_gutter-cell ace_gutter-active-line");

            editor.session.setBreakpoint(0, "hello");
            assert.notEqual(editor.renderer.$loop.changes, 0);
            editor.renderer.$loop._flush();

            cell = editor.renderer.$gutterLayer.$lines.cells[0];
            assert.equal(editor.renderer.$loop.changes, 0);
            assert.equal(cell.element.className, "ace_gutter-cell ace_gutter-active-line hello");
            done();
        });
    },
    "test: screenToTextCoordinates with line widget offset, issue #5874": function() {
        var renderer = editor.renderer;
        editor.setValue("line0\nline1\nline2\nline3\nline4");
        renderer.setPadding(0);
        renderer.characterWidth = 10;
        renderer.lineHeight = renderer.layerConfig.lineHeight = 13;

        var widgetEl = document.createElement("div");
        widgetEl.style.height = "40px";
        editor.session.widgetManager.addLineWidget({
            el: widgetEl,
            row: 1,
            pixelHeight: 40
        });
        renderer.$loop._flush();

        var r = renderer.scroller.getBoundingClientRect();

        // Line 2 starts at: line0(13) + line1(13) + widget(40) = 66px
        // Clicking in top half of line 2 (y=69) should return row 2
        var pos = renderer.screenToTextCoordinates(r.left + 5, r.top + 69);
        assert.equal(pos.row, 2);

        // Clicking bottom half of line 2 (y=75) should also return row 2
        pos = renderer.screenToTextCoordinates(r.left + 5, r.top + 75);
        assert.equal(pos.row, 2);
    }

    // change tab size after setDocument (for text layer)
};


require("./test/run")(module);