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

/**
 * Prepares a given method call by:
 * - Extracting a functional interface.
 * - Creating a field of that type.
 * - Initializing the field with the called method.
 * - Replacing the call with invocation of the field method.
 *
 * @param call - The method call join point.
 * @param method - The method join point (optional).
 * @param fieldLocation - The location to insert the field (optional).
 * @param newFile - Whether to create the interface in a new file (default: true).
 * @param funcInterface - The functional interface join point (optional).
 * @returns An object containing the extracted field, interface, and related information.
 */
export function extractToField(
    call: Call | undefined | null,
    method?: Method,
    fieldLocation?: Class,
    newFile: boolean = true,
    funcInterface: InterfaceType | undefined | null = null
): {
    $field: Field | undefined | null;
    $interface: InterfaceType | null;
    $interfaceMethod: Method | undefined;
    defaultMethod: string | undefined;
} {
    if (call === undefined || call === null) {
        return {
            $field: null,
            $interface: funcInterface,
            $interfaceMethod: undefined,
            defaultMethod: undefined,
        };
    }

    if (method === undefined) {
        const ancestor = call.getAncestor("method") as Method | undefined;
        if (ancestor == undefined) {
            throw new Error("No method found for the given call.");
        }
        method = ancestor;
    }

    if (funcInterface === undefined || funcInterface === null) {
        const extracted = generateFunctionalInterface(
            call.name,
            call.declarator,
            undefined,
            undefined,
            newFile
        );
        funcInterface = extracted.$interface;

        console.log(
            `[LOG] Extracted a functional interface "${funcInterface.name}" based on method "${call.name}"`
        );
    }

    const defaultMethod = `${call.qualifiedDecl}::${call.name}`;

    fieldLocation ??= Query.search(Class, {
        qualifiedName: method.declarator,
    }).getFirst();

    if (fieldLocation === undefined) {
        throw new Error(
            "Could not get a location to insert new field. Please verify the input arguments of extractToField."
        );
    }

    let field: Field | undefined = undefined;
    let interfaceMethod: Method | undefined = undefined;

    for (const m of Query.searchFrom(funcInterface, Method, call.name)) {
        interfaceMethod = m;
        field = fieldLocation.newField(
            method.isStatic ? ["static"] : [],
            funcInterface.qualifiedName,
            interfaceMethod.name,
            defaultMethod
        );

        console.log(
            `[LOG] Extracted a field "${field.name}", from call "${call.name}", to ${field.declarator}`
        );
        call.setTarget(field.name);
        call.setExecutable(interfaceMethod);
    }

    if (field !== undefined) {
        console.log(
            `[LOG] Call to "${call.name}" (in method "${method.name}") is ready!`
        );
    }

    return {
        $field: field,
        $interface: funcInterface,
        $interfaceMethod: interfaceMethod,
        defaultMethod,
    };
}

const DEFAULT_PACKAGE = "pt.up.fe.specs.lara.kadabra.utils";

/**
 * Generates a new mapping class for functional mapping.
 *
 * @param interfaceJp - The functional interface join point.
 * @param methodName - The name of the method.
 * @param getterType - The type of the getter.
 * @param target - The target join point (optional).
 * @returns An object containing the mapping class and related methods.
 */
export function newMappingClass(
    interfaceJp: InterfaceType,
    methodName: string,
    getterType: string,
    target: Class | App | FileJp = Query.root() as App
): {
    $mapClass: Class;
    put: (key: string, value: string) => string;
    contains: (key: string) => string;
    get: (param: string, defaultMethod?: string) => string;
} {
    const targetMethodFirstCap =
        methodName.charAt(0).toUpperCase() + methodName.slice(1);
    const mapClassName = `${DEFAULT_PACKAGE}.${targetMethodFirstCap}Caller`;

    console.log(`[LOG] Creating new functional mapping class: ${mapClassName}`);

    let mapClass = undefined;
    if (
        target instanceof App ||
        target instanceof FileJp ||
        target instanceof Class
    ) {
        mapClass = target.mapVersions(
            mapClassName,
            getterType,
            interfaceJp,
            methodName
        );
    } else {
        throw new Error(
            "Target join point for new functional method caller has to be: app, file, class, or interface."
        );
    }

    return {
        $mapClass: mapClass,
        put: (key: string, value: string) =>
            `${mapClass.qualifiedName}.put(${key}, ${value})`,
        contains: (key: string) => `${mapClass.qualifiedName}.contains(${key})`,
        get: (param: string, defaultMethod?: string) =>
            defaultMethod
                ? `${mapClass.qualifiedName}.get(${param}, ${defaultMethod})`
                : `${mapClass.qualifiedName}.get(${param})`,
    };
}

/**
 * Generates a new functional method caller.
 *
 * @param interfaceJp - The functional interface join point.
 * @param methodName - The name of the method.
 * @param getterType - The type of the getter.
 * @param defaultMethodStr - The default method string.
 * @returns An object containing the mapping class and related methods.
 */
export function newFunctionalMethodCaller(
    interfaceJp: InterfaceType | null = null,
    methodName: string | null = null,
    getterType: string | null = null,
    defaultMethodStr: string | null = null
): {
    $mapClass: Class | undefined;
    put: string;
    contains: string;
    get: ((param: string) => string) | undefined;
} {
    if (
        interfaceJp === null ||
        methodName === null ||
        getterType === null ||
        defaultMethodStr === null
    ) {
        return {
            $mapClass: undefined,
            put: "put",
            contains: "contains",
            get: undefined,
        };
    }

    const targetMethodFirstCap =
        methodName.charAt(0).toUpperCase() + methodName.slice(1);
    const mapClassName = `${DEFAULT_PACKAGE}.${targetMethodFirstCap}Caller`;

    console.log(`[LOG] Creating new functional mapping class: ${mapClassName}`);

    const mapClass = (Query.root() as App).mapVersions(
        mapClassName,
        getterType,
        interfaceJp,
        methodName
    );

    return {
        $mapClass: mapClass,
        put: "put",
        contains: "contains",
        get: (param: string) =>
            `${mapClass.qualifiedName}.get(${param}, ${defaultMethodStr})`,
    };
}
