// @ts-check
/**
 * An ESLint rule that ensures all multiblock pattern builders use
 * `@` char for the controller, ` ` (space) for air and `#` for the 'any' predicate.
 */

import { ESLintUtils } from "@typescript-eslint/utils"

const builderFactoryArgs = "definition"
const builderName = "FactoryBlockPattern"

/** @type {Record<string, string>} */
const rulePredicateCode = {
    "@": `Predicates.controller(Predicates.blocks(${builderFactoryArgs}.get()))`,
    "#": "Predicates.any()",
    " ": "Predicates.air()",
}

/**
 * Finds the root expression in the call chain
 * @param {import('@typescript-eslint/utils').TSESTree.CallExpression} rootCall
 */
export function isInMultiblockBuilderCallChain(rootCall) {
    let rootCallee
    for(;;) {
        if(rootCall.callee.type === "MemberExpression")
            rootCallee = rootCall.callee
        else break
        if(rootCallee.object.type === "CallExpression")
            rootCall = rootCallee.object
        else break
    }
    if(!rootCallee) return false
    const o = rootCallee.object
    return o && o.type === "Identifier" && o.name === builderName
}

export default ESLintUtils.RuleCreator.withoutDocs({
    meta: {
        type: "problem",
        hasSuggestions: true,
        fixable: "code",
        messages: {
            "char-not-char": "The first argument must be a single character, like '@'.",
            "not-2-args": "A 'where' call should have 2 arguments.",
            ...Object.fromEntries(
                Object.entries(rulePredicateCode).map(([char, code]) => [
                    "predicate-for-" + char, `The predicate for '${char}' should be ${code}`
                ])
            ),

            "factory-no-args": "Multiblock pattern factory must have a single argument.",
            "factory-args": `Multiblock pattern factory arguments should be '${builderFactoryArgs} =>'`
        },
        schema: [],
    },
    defaultOptions: [],
    create(context) {
        return {
            CallExpression(node) {
                if(node.callee.type !== "MemberExpression")
                    return
                if(node.callee.property.type !== "Identifier" || node.callee.property.name !== "where")
                    return
                if(!isInMultiblockBuilderCallChain(node))
                    return
                // Now we are sure we are dealing with a multiblock pattern builder

                if(node.arguments.length !== 2)
                    return context.report({
                        messageId: "not-2-args",
                        node: node
                    })

                const [char, predicate] = node.arguments

                // First argument check
                if(char.type !== "Literal" || typeof char.value !== "string" || char.value.length !== 1)
                    return context.report({
                        messageId: "char-not-char",
                        node: char
                    })

                // Second argument check
                const predicateCodeActual = context.sourceCode.text.slice(...predicate.range)
                const predicateCodeShouldBe = rulePredicateCode[char.value]
                if(predicateCodeShouldBe && predicateCodeActual !== predicateCodeShouldBe)
                    context.report({
                        // @ts-ignore It's not smart enough
                        messageId: "predicate-for-" + char.value,
                        node: predicate,
                        fix: fixer => [
                            fixer.replaceText(predicate, predicateCodeShouldBe)
                        ]
                    })
            },
            ArrowFunctionExpression(node) {
                if(node.body.type !== "CallExpression")
                    return
                if(!isInMultiblockBuilderCallChain(node.body))
                    return
                // Now we are sure we are dealing with a multiblock factory function

                if(node.params.length < 1)
                    return context.report({
                        messageId: "factory-no-args",
                        node: node
                    })

                /** @type {[number, number]} */
                const paramsRange = [
                    node.params[0].range[0],
                    node.params.at(-1).range[1]
                ]
                if(context.sourceCode.text.slice(...paramsRange) !== builderFactoryArgs)
                    return context.report({
                        messageId: "factory-args",
                        node: node.params[0],
                        fix: fixer => [
                            fixer.replaceTextRange(paramsRange, builderFactoryArgs)
                        ]
                    })
            }
        }
    },
})
