import Query from "@specs-feup/lara/api/weaver/Query.js";
import { Class, Field, FileJp, Method } from "../../Joinpoints.js";
import { getOrNewClass } from "../Factory.js";

export function CreateClassGenerator(
    adapterMethod: string,
    $interfaceMethod: Method,
    adapterClass: string = ".*",
    $storingClass?: Class
) {
    let $adaptMethod: Method | undefined;
    let generate: ((...args: string[]) => string) | undefined;
    let generateQualified: ((...args: string[]) => string) | undefined;

    for (const $method of Query.search(FileJp)
        .search(Class, (c: Class) => RegExp(adapterClass).exec(c.name) !== null)
        .search(Method, adapterMethod)) {
        if ($adaptMethod != undefined) {
            throw new Error(
                "More than one target method generator was found, please define a finer selection"
            );
        }
        const adapter = FunctionGenerator(
            $method,
            $interfaceMethod,
            $storingClass
        );
        $adaptMethod = adapter.$adaptMethod;
        generate = adapter.generate;
        generateQualified = adapter.generateQualified;
    }

    if ($adaptMethod == undefined) {
        throw new Error(
            "Could not find given method generator: " +
                adapterMethod +
                " in " +
                adapterClass
        );
    }

    return {
        $adaptMethod: $adaptMethod,
        generate: generate,
        generateQualified: generateQualified,
    };
}

export function FunctionGenerator(
    $adapterMethod: Method,
    $interfaceMethod: Method,
    $storingClass: Class = getOrNewClass("kadabra.utils.Adapters")
): {
    $adaptMethod: Method;
    generate: (...args: string[]) => string;
    generateQualified: (...args: string[]) => string;
} {
    console.log(
        "[FunctionGenerator] Creating new functional class with " +
            $interfaceMethod +
            " and " +
            $adapterMethod
    );

    const $adaptMethod = $storingClass.newFunctionalClass(
        $interfaceMethod,
        $adapterMethod
    );
    const generate = function (...args: string[]) {
        let invoke = $adaptMethod + "(";
        const _args = args.slice();
        invoke += _args.join(", ");
        invoke += ")";
        return invoke;
    };

    const generateQualified = function (...args: string[]) {
        let invoke = $storingClass.qualifiedName + "." + $adaptMethod + "(";
        const _args = args.slice();
        invoke += _args.join(", ");
        invoke += ")";
        return invoke;
    };

    return {
        $adaptMethod: $adapterMethod,
        generate: generate,
        generateQualified: generateQualified,
    };
}

/**
 *
 */
export function CreateAdapter(
    target: string,
    adapter: string,
    name: string,
    targetClass: string = ".*",
    adapterClass: string = ".*"
) {
    let $adaptClass: Class | undefined;
    let addField;
    const $methods = Query.search(FileJp)
        .search(Class, (c: Class) => RegExp(targetClass).exec(c.name) !== null)
        .search(Method, { name: target });

    for (const $adaptMethod of Query.search(FileJp)
        .search(Class, (c: Class) => RegExp(adapterClass).exec(c.name) != null)
        .search(Method, { name: adapter })) {
        for (const $method of $methods) {
            if ($method.equals($adaptMethod)) {
                if ($adaptClass != undefined) {
                    throw new Error(
                        "More than one target class/adapter method was found, please define a finer selection"
                    );
                }

                name ??=
                    $method.name.charAt(0).toUpperCase() +
                    $method.name.substring(1) +
                    "_" +
                    adapter +
                    "_" +
                    $adaptMethod.name.charAt(0).toUpperCase() +
                    $adaptMethod.name.substring(1) +
                    "_Adapter";

                const _adapter = TransformMethod($method, $adaptMethod, name); //, $target.name.firstCharToUpper()+"Adapter");
                $adaptClass = _adapter.$adaptClass;
                addField = _adapter.addField;
            }
        }
    }
    return { $adaptClass: $adaptClass, addField: addField };
}

/**
 * Create an adapter based on the target class and the method that transforms the class bytecodes.
 *
 */
export function TransformMethod(
    $target: Method,
    $adaptMethod: Method,
    name: string = defaultTransformMethodName($target, $adaptMethod)
): {
    $adaptClass: Class;
    addField: (
        $class?: Class,
        name?: string,
        init?: boolean
    ) => {
        name: string;
        $field: Field;
        addAdapter: string;
        adapt: (...args: string[]) => string;
    };
} {
    let $adaptClass: Class | undefined;
    for (const $class of Query.search(FileJp).search(Class, { name: name })) {
        $adaptClass = $class;
    }

    if ($adaptClass === undefined) {
        try {
            $adaptClass = $target.createAdapter($adaptMethod, name);
        } catch (e) {
            if (e instanceof Error) console.log(e.message);
        }
    }

    if ($adaptClass === undefined) {
        throw new Error("[TransformMethod] couldn't create $adaptClass");
    }

    //this method returns information regarding the field and class, as well as the methods that can be invoked in the field
    const addField = (
        $class: Class = $adaptClass,
        fieldName: string = $adaptClass.name.charAt(0).toLowerCase() +
            $adaptClass.name.substring(1),
        init: boolean = false
    ) => {
        const $newField = $class.newField(
            ["public", "static"],
            $adaptClass.qualifiedName,
            fieldName,
            "new " + $adaptClass.name + "()"
        );

        const field = {
            name: fieldName,
            $field: $newField,
            addAdapter:
                "weaver.kadabra.agent.AgentUtils.addAdapter(" +
                fieldName +
                ");",
            adapt: (...args: string[]) => {
                let invoke = $newField.staticAccess + ".adapt(";
                const _args = args.slice();
                invoke += _args.join(", ");
                invoke += ");";
                return invoke;
            },
        };

        if (init) {
            $class.insertStatic(`${field.addAdapter}`);
        }

        return field;
    };

    return { $adaptClass: $adaptClass, addField: addField };
}

function defaultTransformMethodName($target: Method, $adaptMethod: Method) {
    return (
        $target.name.charAt(0).toUpperCase() +
        $target.name.substring(1) +
        $adaptMethod.name.charAt(0).toUpperCase +
        $adaptMethod.name.substring(1) +
        "Adapter"
    );
}
