/*
 * Copyright (C) 2023  Yomitan Authors
 * 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/>.
 */

import fs from 'fs';
import {JSDOM} from 'jsdom';
import {fileURLToPath} from 'node:url';
import path from 'path';
import {expect, test} from 'vitest';
import {DocumentUtil} from '../ext/js/dom/document-util.js';
import {DOMTextScanner} from '../ext/js/dom/dom-text-scanner.js';
import {TextSourceElement} from '../ext/js/dom/text-source-element.js';
import {TextSourceRange} from '../ext/js/dom/text-source-range.js';

const dirname = path.dirname(fileURLToPath(import.meta.url));

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

    /** @type {number} */
    get x() { return this._x; }
    /** @type {number} */
    get y() { return this._y; }
    /** @type {number} */
    get width() { return this._width; }
    /** @type {number} */
    get height() { return this._height; }
    /** @type {number} */
    get left() { return this._x + Math.min(0, this._width); }
    /** @type {number} */
    get right() { return this._x + Math.max(0, this._width); }
    /** @type {number} */
    get top() { return this._y + Math.min(0, this._height); }
    /** @type {number} */
    get bottom() { return this._y + Math.max(0, this._height); }
    /** @returns {string} */
    toJSON() { return '<not implemented>'; }
}


/**
 * @param {string} fileName
 * @returns {JSDOM}
 */
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;
}

/**
 * @param {Element} element
 * @param {string|undefined} selector
 * @returns {?Element}
 */
function querySelectorChildOrSelf(element, selector) {
    return selector ? element.querySelector(selector) : element;
}

/**
 * @param {JSDOM} dom
 * @param {?Node} node
 * @returns {?Text|Node}
 */
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);
}

/**
 * @param {unknown} value
 * @returns {unknown}
 */
function getPrototypeOfOrNull(value) {
    try {
        return Object.getPrototypeOf(value);
    } catch (e) {
        return null;
    }
}

/**
 * @param {Document} document
 * @returns {?Element}
 */
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;

    try {
        await testDocumentTextScanningFunctions(dom);
        await testTextSourceRangeSeekFunctions(dom);
    } finally {
        window.close();
    }
}

/**
 * @param {JSDOM} dom
 */
async function testDocumentTextScanningFunctions(dom) {
    const document = dom.window.document;

    test('DocumentTextScanningFunctions', () => {
        for (const testElement of /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.test[data-test-type=scan]'))) {
            // Get test parameters
            const {
                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));

            const startOffset2 = parseInt(/** @type {string} */ (startOffset), 10);
            const endOffset2 = parseInt(/** @type {string} */ (endOffset), 10);
            const sentenceScanExtent2 = parseInt(/** @type {string} */ (sentenceScanExtent), 10);
            const terminateAtNewlines2 = (terminateAtNewlines !== 'false');

            expect(elementFromPointValue).not.toStrictEqual(null);
            expect(caretRangeFromPointValue).not.toStrictEqual(null);
            expect(startNode).not.toStrictEqual(null);
            expect(endNode).not.toStrictEqual(null);

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

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

                const range = document.createRange();
                range.setStart(/** @type {Node} */ (imposter ? imposter : startNode), startOffset2);
                range.setEnd(/** @type {Node} */ (imposter ? imposter : startNode), endOffset2);

                // Override getClientRects to return a rect guaranteed to contain (x, y)
                range.getClientRects = () => {
                    /** @type {import('test/document-types').PseudoDOMRectList} */
                    const domRectList = Object.assign(
                        [new DOMRect(x - 1, y - 1, 2, 2)],
                        {
                            /**
                             * @this {DOMRect[]}
                             * @param {number} index
                             * @returns {DOMRect}
                             */
                            item: function item(index) { return this[index]; }
                        }
                    );
                    return domRectList;
                };
                return range;
            };

            // Test docRangeFromPoint
            const source = DocumentUtil.getRangeFromPoint(0, 0, {
                deepContentScan: false,
                normalizeCssZoom: true
            });
            switch (resultType) {
                case 'TextSourceRange':
                    expect(getPrototypeOfOrNull(source)).toStrictEqual(TextSourceRange.prototype);
                    break;
                case 'TextSourceElement':
                    expect(getPrototypeOfOrNull(source)).toStrictEqual(TextSourceElement.prototype);
                    break;
                case 'null':
                    expect(source).toStrictEqual(null);
                    break;
                default:
                    expect.unreachable();
                    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,
                sentenceScanExtent2,
                terminateAtNewlines2,
                terminatorMap,
                forwardQuoteMap,
                backwardQuoteMap
            ).text;
            expect(sentenceActual).toStrictEqual(sentence);

            // Clean
            source.cleanup();
        }
    });
}

/**
 * @param {JSDOM} dom
 */
async function testTextSourceRangeSeekFunctions(dom) {
    const document = dom.window.document;

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

            const seekOffset2 = parseInt(/** @type {string} */ (seekOffset), 10);
            const seekLength2 = parseInt(/** @type {string} */ (seekLength), 10);
            const expectedResultOffset2 = parseInt(/** @type {string} */ (expectedResultOffset), 10);

            /** @type {?Node} */
            let seekNode = testElement.querySelector(/** @type {string} */ (seekNodeSelector));
            if (seekNodeIsText === 'true' && seekNode !== null) {
                seekNode = seekNode.firstChild;
            }

            /** @type {?Node} */
            let expectedResultNode = testElement.querySelector(/** @type {string} */ (expectedResultNodeSelector));
            if (expectedResultNodeIsText === 'true' && expectedResultNode !== null) {
                expectedResultNode = expectedResultNode.firstChild;
            }

            const {node, offset, content} = (
                seekDirection === 'forward' ?
                new DOMTextScanner(/** @type {Node} */ (seekNode), seekOffset2, true, false).seek(seekLength2) :
                new DOMTextScanner(/** @type {Node} */ (seekNode), seekOffset2, true, false).seek(-seekLength2)
            );

            expect(node).toStrictEqual(expectedResultNode);
            expect(offset).toStrictEqual(expectedResultOffset2);
            expect(content).toStrictEqual(expectedResultContent);
        }
    });
}


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

await main();