/*
 * Copyright (C) 2024  Yomitan 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 {describe, test} from 'vitest';
import {LanguageTransformer} from '../ext/js/language/language-transformer.js';
import {getAllLanguageTransformDescriptors} from '../ext/js/language/languages.js';

class DeinflectionNode {
    /**
     * @param {string} text
     * @param {string[]} ruleNames
     * @param {?RuleNode} ruleNode
     * @param {?DeinflectionNode} previous
     */
    constructor(text, ruleNames, ruleNode, previous) {
        /** @type {string} */
        this.text = text;
        /** @type {string[]} */
        this.ruleNames = ruleNames;
        /** @type {?RuleNode} */
        this.ruleNode = ruleNode;
        /** @type {?DeinflectionNode} */
        this.previous = previous;
    }

    /**
     * @param {DeinflectionNode} other
     * @returns {boolean}
     */
    historyIncludes(other) {
        /** @type {?DeinflectionNode} */
        // eslint-disable-next-line @typescript-eslint/no-this-alias
        let node = this;
        for (; node !== null; node = node.previous) {
            if (
                node.ruleNode === other.ruleNode &&
                node.text === other.text &&
                arraysAreEqual(node.ruleNames, other.ruleNames)
            ) {
                return true;
            }
        }
        return false;
    }

    /**
     * @returns {DeinflectionNode[]}
     */
    getHistory() {
        /** @type {DeinflectionNode[]} */
        const results = [];
        /** @type {?DeinflectionNode} */
        // eslint-disable-next-line @typescript-eslint/no-this-alias
        let node = this;
        for (; node !== null; node = node.previous) {
            results.unshift(node);
        }
        return results;
    }
}

class RuleNode {
    /**
     * @param {string} groupName
     * @param {import('language-transformer').SuffixRule} rule
     */
    constructor(groupName, rule) {
        /** @type {string} */
        this.groupName = groupName;
        /** @type {import('language-transformer').SuffixRule} */
        this.rule = rule;
    }
}

/**
 * @template [T=unknown]
 * @param {T[]} rules1
 * @param {T[]} rules2
 * @returns {boolean}
 */
function arraysAreEqual(rules1, rules2) {
    if (rules1.length !== rules2.length) { return false; }
    for (const rule1 of rules1) {
        if (!rules2.includes(rule1)) { return false; }
    }
    return true;
}

const languagesWithTransforms = getAllLanguageTransformDescriptors();

describe.each(languagesWithTransforms)('Cycles Test $iso', ({languageTransforms}) => {
    test('Check for cycles', ({expect}) => {
        const languageTransformer = new LanguageTransformer();
        languageTransformer.addDescriptor(languageTransforms);

        /** @type {RuleNode[]} */
        const ruleNodes = [];
        for (const [groupName, reasonInfo] of Object.entries(languageTransforms.transforms)) {
            for (const rule of reasonInfo.rules) {
                if (rule.type === 'suffix') {
                    ruleNodes.push(new RuleNode(groupName, /** @type {import('language-transformer').SuffixRule}*/ (rule)));
                }
            }
        }

        /** @type {DeinflectionNode[]} */
        const deinflectionNodes = [];
        for (const {rule: {isInflected}} of ruleNodes) {
            const suffixIn = isInflected.source.substring(0, isInflected.source.length - 1);
            deinflectionNodes.push(new DeinflectionNode(`?${suffixIn}`, [], null, null));
        }

        for (let i = 0; i < deinflectionNodes.length; ++i) {
            const deinflectionNode = deinflectionNodes[i];
            const {text, ruleNames} = deinflectionNode;
            for (const ruleNode of ruleNodes) {
                const {isInflected, deinflected: suffixOut, conditionsIn, conditionsOut} = ruleNode.rule;
                const suffixIn = isInflected.source.substring(0, isInflected.source.length - 1);
                if (
                    !LanguageTransformer.conditionsMatch(
                        languageTransformer.getConditionFlagsFromConditionTypes(ruleNames),
                        languageTransformer.getConditionFlagsFromConditionTypes(conditionsIn)
                    ) ||
                    !text.endsWith(suffixIn) ||
                    (text.length - suffixIn.length + suffixOut.length) <= 0
                ) {
                    continue;
                }

                const newDeinflectionNode = new DeinflectionNode(
                    text.substring(0, text.length - suffixIn.length) + suffixOut,
                    conditionsOut,
                    ruleNode,
                    deinflectionNode
                );

                // Cycle check
                if (deinflectionNode.historyIncludes(newDeinflectionNode)) {
                    const stack = [];
                    for (const {text: itemText, ruleNode: itemNode} of newDeinflectionNode.getHistory()) {
                        if (itemNode !== null) {
                            const itemSuffixIn = itemNode.rule.isInflected.source.substring(0, itemNode.rule.isInflected.source.length - 1);
                            const itemSuffixOut = itemNode.rule.deinflected;
                            stack.push(`${itemText} (${itemNode.groupName}, ${itemNode.rule.conditionsIn.join(',')}=>${itemNode.rule.conditionsOut.join(',')}, ${itemSuffixIn}=>${itemSuffixOut})`);
                        } else {
                            stack.push(`${itemText} (start)`);
                        }
                    }
                    const message = `Cycle detected:\n  ${stack.join('\n  ')}`;
                    expect.soft(true, message).toEqual(false);
                    continue;
                }

                deinflectionNodes.push(newDeinflectionNode);
            }
        }
    });
});