/*
 * Copyright (C) 2020-2022  Yomichan Authors
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

const fs = require('fs');
const path = require('path');
const assert = require('assert');
const {JSDOM} = require('jsdom');
const {testMain} = require('../dev/util');
const {VM} = require('../dev/vm');


// DOMRect class definition
class DOMRect {
    constructor(x, y, width, height) {
        this._x = x;
        this._y = y;
        this._width = width;
        this._height = height;
    }

    get x() { return this._x; }
    get y() { return this._y; }
    get width() { return this._width; }
    get height() { return this._height; }
    get left() { return this._x + Math.min(0, this._width); }
    get right() { return this._x + Math.max(0, this._width); }
    get top() { return this._y + Math.min(0, this._height); }
    get bottom() { return this._y + Math.max(0, this._height); }
}


function createJSDOM(fileName) {
    const domSource = fs.readFileSync(fileName, {encoding: 'utf8'});
    const dom = new JSDOM(domSource);
    const document = dom.window.document;
    const window = dom.window;

    // Define innerText setter as an alias for textContent setter
    Object.defineProperty(window.HTMLDivElement.prototype, 'innerText', {
        set(value) { this.textContent = value; }
    });

    // Placeholder for feature detection
    document.caretRangeFromPoint = () => null;

    return dom;
}

function querySelectorChildOrSelf(element, selector) {
    return selector ? element.querySelector(selector) : element;
}

function getChildTextNodeOrSelf(dom, node) {
    if (node === null) { return null; }
    const Node = dom.window.Node;
    const childNode = node.firstChild;
    return (childNode !== null && childNode.nodeType === Node.TEXT_NODE ? childNode : node);
}

function getPrototypeOfOrNull(value) {
    try {
        return Object.getPrototypeOf(value);
    } catch (e) {
        return null;
    }
}

function findImposterElement(document) {
    // Finds the imposter element based on it's z-index style
    return document.querySelector('div[style*="2147483646"]>*');
}


async function testDocument1() {
    const dom = createJSDOM(path.join(__dirname, 'data', 'html', 'test-document1.html'));
    const window = dom.window;
    const document = window.document;
    const Node = window.Node;
    const Range = window.Range;

    const vm = new VM({document, window, Range, Node});
    vm.execute([
        'js/dom/dom-text-scanner.js',
        'js/dom/text-source-range.js',
        'js/dom/text-source-element.js',
        'js/dom/document-util.js'
    ]);
    const [DOMTextScanner, TextSourceRange, TextSourceElement, DocumentUtil] = vm.get([
        'DOMTextScanner',
        'TextSourceRange',
        'TextSourceElement',
        'DocumentUtil'
    ]);

    try {
        await testDocumentTextScanningFunctions(dom, {DocumentUtil, TextSourceRange, TextSourceElement});
        await testTextSourceRangeSeekFunctions(dom, {DOMTextScanner});
    } finally {
        window.close();
    }
}

async function testDocumentTextScanningFunctions(dom, {DocumentUtil, TextSourceRange, TextSourceElement}) {
    const document = dom.window.document;

    for (const testElement of document.querySelectorAll('.test[data-test-type=scan]')) {
        // Get test parameters
        let {
            elementFromPointSelector,
            caretRangeFromPointSelector,
            startNodeSelector,
            startOffset,
            endNodeSelector,
            endOffset,
            resultType,
            sentenceScanExtent,
            sentence,
            hasImposter,
            terminateAtNewlines
        } = testElement.dataset;

        const elementFromPointValue = querySelectorChildOrSelf(testElement, elementFromPointSelector);
        const caretRangeFromPointValue = querySelectorChildOrSelf(testElement, caretRangeFromPointSelector);
        const startNode = getChildTextNodeOrSelf(dom, querySelectorChildOrSelf(testElement, startNodeSelector));
        const endNode = getChildTextNodeOrSelf(dom, querySelectorChildOrSelf(testElement, endNodeSelector));

        startOffset = parseInt(startOffset, 10);
        endOffset = parseInt(endOffset, 10);
        sentenceScanExtent = parseInt(sentenceScanExtent, 10);
        terminateAtNewlines = (terminateAtNewlines !== 'false');

        assert.notStrictEqual(elementFromPointValue, null);
        assert.notStrictEqual(caretRangeFromPointValue, null);
        assert.notStrictEqual(startNode, null);
        assert.notStrictEqual(endNode, null);

        // Setup functions
        document.elementFromPoint = () => elementFromPointValue;

        document.caretRangeFromPoint = (x, y) => {
            const imposter = getChildTextNodeOrSelf(dom, findImposterElement(document));
            assert.strictEqual(!!imposter, hasImposter === 'true');

            const range = document.createRange();
            range.setStart(imposter ? imposter : startNode, startOffset);
            range.setEnd(imposter ? imposter : startNode, endOffset);

            // Override getClientRects to return a rect guaranteed to contain (x, y)
            range.getClientRects = () => [new DOMRect(x - 1, y - 1, 2, 2)];
            return range;
        };

        // Test docRangeFromPoint
        const documentUtil = new DocumentUtil();
        const source = documentUtil.getRangeFromPoint(0, 0, false);
        switch (resultType) {
            case 'TextSourceRange':
                assert.strictEqual(getPrototypeOfOrNull(source), TextSourceRange.prototype);
                break;
            case 'TextSourceElement':
                assert.strictEqual(getPrototypeOfOrNull(source), TextSourceElement.prototype);
                break;
            case 'null':
                assert.strictEqual(source, null);
                break;
            default:
                assert.ok(false);
                break;
        }
        if (source === null) { continue; }

        // Sentence info
        const terminatorString = '…。..??!!';
        const terminatorMap = new Map();
        for (const char of terminatorString) {
            terminatorMap.set(char, [false, true]);
        }
        const quoteArray = [['「', '」'], ['『', '』'], ['\'', '\''], ['"', '"']];
        const forwardQuoteMap = new Map();
        const backwardQuoteMap = new Map();
        for (const [char1, char2] of quoteArray) {
            forwardQuoteMap.set(char1, [char2, false]);
            backwardQuoteMap.set(char2, [char1, false]);
        }

        // Test docSentenceExtract
        const sentenceActual = documentUtil.extractSentence(
            source,
            false,
            sentenceScanExtent,
            terminateAtNewlines,
            terminatorMap,
            forwardQuoteMap,
            backwardQuoteMap
        ).text;
        assert.strictEqual(sentenceActual, sentence);

        // Clean
        source.cleanup();
    }
}

async function testTextSourceRangeSeekFunctions(dom, {DOMTextScanner}) {
    const document = dom.window.document;

    for (const testElement of document.querySelectorAll('.test[data-test-type=text-source-range-seek]')) {
        // Get test parameters
        let {
            seekNodeSelector,
            seekNodeIsText,
            seekOffset,
            seekLength,
            seekDirection,
            expectedResultNodeSelector,
            expectedResultNodeIsText,
            expectedResultOffset,
            expectedResultContent
        } = testElement.dataset;

        seekOffset = parseInt(seekOffset, 10);
        seekLength = parseInt(seekLength, 10);
        expectedResultOffset = parseInt(expectedResultOffset, 10);

        let seekNode = testElement.querySelector(seekNodeSelector);
        if (seekNodeIsText === 'true') {
            seekNode = seekNode.firstChild;
        }

        let expectedResultNode = testElement.querySelector(expectedResultNodeSelector);
        if (expectedResultNodeIsText === 'true') {
            expectedResultNode = expectedResultNode.firstChild;
        }

        const {node, offset, content} = (
            seekDirection === 'forward' ?
            new DOMTextScanner(seekNode, seekOffset, true, false).seek(seekLength) :
            new DOMTextScanner(seekNode, seekOffset, true, false).seek(-seekLength)
        );

        assert.strictEqual(node, expectedResultNode);
        assert.strictEqual(offset, expectedResultOffset);
        assert.strictEqual(content, expectedResultContent);
    }
}


async function main() {
    await testDocument1();
}


if (require.main === module) { testMain(main); }