import * as t from "@babel/types";
import { SOURCE_ID_ATTRIBUTE, NodeType, Property, } from "@superblocksteam/library-shared";
import { generatePredictableId } from "../ids.js";
import { getLogger } from "../util/logger.js";
import { getSbElementId } from "./ids.js";
import { parseNodeProperties } from "./properties.js";
import { isSbElement, getTagName, isCustomComponent, getIdentiferName, extractNameFromBindAttribute, } from "./util.js";
export function makeJSXAttribute(key, value) {
    return t.jsxAttribute(t.jsxIdentifier(key), t.stringLiteral(value));
}
export function getProperties(element, context) {
    const logger = getLogger();
    const properties = {};
    const openingElement = element.get("openingElement");
    if (!openingElement) {
        logger.warn(`No opening element for element: ${element}`);
        return properties;
    }
    openingElement.get("attributes").forEach((attr) => {
        if (attr.isJSXAttribute()) {
            const attrName = getIdentiferName(attr.get("name").node);
            if (!attrName) {
                return;
            }
            const value = attr.get("value");
            if (value.isStringLiteral()) {
                properties[attrName] = Property.Static(value.node.value);
            }
            else if (value.isJSXExpressionContainer()) {
                const parsedValue = parseNodeProperties(value, context);
                if (parsedValue) {
                    properties[attrName] = parsedValue;
                }
            }
        }
    });
    return properties;
}
function processJSXFragment(nodePath, parent, context) {
    nodePath
        .get("children")
        .forEach((child) => buildNodes(child, parent, context));
}
export function processJSXElement(nodePath, parent, context, isChild = true) {
    const element = nodePath.node;
    const name = getDescriptiveName(element);
    const tagName = getTagName(element);
    if (!name || !tagName) {
        return;
    }
    const props = getProperties(nodePath, context);
    const id = getSbElementId(element.openingElement) ??
        props[SOURCE_ID_ATTRIBUTE]?.value;
    let node;
    // We check if a name like <MyComponent /> is defined on the page as `function MyComponent() { ... }`
    // If so, we can use that tie this particular JSXElement to the SB element inside that function component
    const referenceBinding = context.bindings.find(({ binding }) => {
        const bindingName = getIdentiferName(binding.identifier);
        return bindingName === name;
    });
    if (id &&
        (isSbElement(element, context.libraryComponents) ||
            isCustomComponent(tagName))) {
        node = {
            name,
            id,
            type: NodeType.WidgetNode,
            properties: props,
            tagName,
            children: element.children.length > 0 ? [] : undefined,
        };
    }
    else {
        node = {
            name,
            id: id ?? name,
            type: NodeType.HTMLElementNode,
            properties: props,
            tagName,
            children: element.children.length > 0 ? [] : undefined,
            reference: referenceBinding?.sbElementId,
        };
    }
    if (context.conditional) {
        node.isConditional = true;
    }
    if (isChild && parent) {
        parent.children?.push(node);
    }
    if (nodePath.get("children")) {
        nodePath
            .get("children")
            .forEach((child) => buildNodes(child, node, context));
    }
    return node;
}
function handleConditionalExpression(expression, parent, context) {
    const conditionalContext = {
        ...context,
        conditional: true,
    };
    const consequent = expression.get("consequent");
    if (!Array.isArray(consequent)) {
        if (consequent.isJSXElement() || consequent.isJSXFragment()) {
            buildNodes(consequent, parent, conditionalContext);
        }
        else if (consequent.isConditionalExpression()) {
            handleConditionalExpression(consequent, parent, conditionalContext);
        }
        else if (consequent.isCallExpression()) {
            handleCallExpression(consequent, parent, conditionalContext);
        }
    }
    const alternate = expression.get("alternate");
    if (!Array.isArray(alternate)) {
        if (alternate.isJSXElement() || alternate.isJSXFragment()) {
            buildNodes(alternate, parent, conditionalContext);
        }
        else if (alternate.isConditionalExpression()) {
            handleConditionalExpression(alternate, parent, conditionalContext);
        }
        else if (alternate.isCallExpression()) {
            handleCallExpression(alternate, parent, conditionalContext);
        }
    }
}
function handleCallExpression(expression, parent, context) {
    const handleMapExpression = (expression) => {
        const repeaterParentNode = {
            type: NodeType.RepeaterNode,
            name: "repeater",
            id: generatePredictableId(expression.toString()),
            children: [],
            properties: {},
            tagName: "",
        };
        parent.children?.push(repeaterParentNode);
        expression.traverse({
            JSXElement(path) {
                buildNodes(path, repeaterParentNode, context);
            },
        });
    };
    // lets check if we are calling a .map function
    const callee = expression.get("callee");
    if (Array.isArray(callee)) {
        return;
    }
    if (callee.isIdentifier()) {
        const identifierName = getIdentiferName(callee.node);
        if (identifierName === "computed") {
            expression.traverse({
                ArrowFunctionExpression(path) {
                    const body = path.get("body");
                    if (body.isJSXElement()) {
                        buildNodes(body, parent, context);
                    }
                },
                ReturnStatement(path) {
                    const returnStatement = path.get("argument");
                    if (returnStatement.isJSXElement()) {
                        buildNodes(returnStatement, parent, context);
                    }
                },
                CallExpression(path) {
                    const mapCallee = path.get("callee");
                    if (!Array.isArray(mapCallee) &&
                        (mapCallee.isMemberExpression() ||
                            mapCallee.isOptionalMemberExpression())) {
                        const property = mapCallee.get("property");
                        if (!Array.isArray(property) &&
                            property.isIdentifier() &&
                            getIdentiferName(property.node) === "map") {
                            handleMapExpression(path);
                            // skip traversing since we handled it as a map already
                            path.skip();
                        }
                    }
                },
            });
        }
        else if (identifierName) {
            const binding = expression.scope.getBinding(identifierName);
            if (binding && binding.path.isVariableDeclarator()) {
                const init = binding.path.get("init");
                // Handle useCallback(() => <JSX />, []) or () => <JSX />
                if (init.isCallExpression() ||
                    init.isArrowFunctionExpression() ||
                    init.isFunctionExpression()) {
                    init.traverse({
                        JSXElement(jsxPath) {
                            buildNodes(jsxPath, parent, context);
                            jsxPath.skip();
                        },
                    });
                }
            }
        }
    }
    else if (callee.isMemberExpression() ||
        callee.isOptionalMemberExpression()) {
        const property = callee.get("property");
        if (!Array.isArray(property) &&
            property.isIdentifier() &&
            getIdentiferName(property.node) === "map") {
            handleMapExpression(expression);
        }
    }
}
function processJSXExpressionContainer(nodePath, parent, context) {
    const expression = nodePath.get("expression");
    const startingChildrenLen = parent.children?.length ?? 0;
    switch (expression.type) {
        case "ConditionalExpression": {
            if (expression.isConditionalExpression()) {
                handleConditionalExpression(expression, parent, context);
            }
            break;
        }
        case "LogicalExpression": {
            const right = expression.get("right");
            if (!Array.isArray(right) &&
                (right.isJSXElement() || right.isJSXFragment())) {
                buildNodes(right, parent, {
                    conditional: true,
                    bindings: context.bindings,
                    jsxVariables: context.jsxVariables,
                    libraryComponents: context.libraryComponents,
                });
            }
            break;
        }
        case "CallExpression":
        case "OptionalCallExpression": {
            if (expression.isCallExpression() ||
                expression.isOptionalCallExpression()) {
                handleCallExpression(expression, parent, context);
            }
            break;
        }
        case "Identifier": {
            if (expression.isIdentifier()) {
                const identifierName = getIdentiferName(expression.node);
                const jsxVariable = context.jsxVariables.find(({ binding }) => {
                    const bindingName = getIdentiferName(binding.identifier);
                    return bindingName === identifierName;
                });
                if (jsxVariable) {
                    buildNodes(jsxVariable.jsxElement, parent, context);
                }
            }
            break;
        }
        default:
            break;
    }
    if (startingChildrenLen === parent.children?.length) {
        // can be hard to track if any of the above code ends up adding a node or not
        processJSXFallbackExpression(nodePath, parent);
    }
}
// These types of nodes are currently to add clarity to situations
// where JSX children contain a mixture of static text and expressions
// We don't actually bother reading into the internals of these nodes anywhere (yet)
function processJSXFallbackExpression(nodePath, parent) {
    const expression = nodePath.get("expression");
    let name = expression.toString();
    if (name.length > 100) {
        name = name.substring(0, 100) + "...";
    }
    parent.children?.push({
        properties: {},
        id: parent.id + "-" + generatePredictableId(name),
        name,
        type: NodeType.UnknownNode,
        children: [],
    });
}
function processJSXText(nodePath, parent) {
    const text = nodePath.node.value;
    if (text.replace(/\s/g, "").length === 0) {
        return;
    }
    parent.children?.push({
        id: parent.id + "-" + generatePredictableId(text),
        name: text,
        type: NodeType.TextNode,
        tagName: "text",
        text: text,
        isConditional: false,
    });
}
export function buildNodes(node, parent, context) {
    if (node.isJSXFragment()) {
        processJSXFragment(node, parent, context);
    }
    else if (node.isJSXElement()) {
        processJSXElement(node, parent, context);
    }
    else if (node.isJSXExpressionContainer()) {
        processJSXExpressionContainer(node, parent, context);
    }
    else if (node.isJSXText()) {
        processJSXText(node, parent);
    }
}
export function isNamedComponent(element) {
    const bindAttr = element.attributes.find((attr) => attr.type === "JSXAttribute" && attr.name.name === "bind");
    return !!bindAttr;
}
export function getDescriptiveName(element) {
    const bindAttr = element.openingElement.attributes.find((attr) => attr.type === "JSXAttribute" && attr.name.name === "bind");
    if (bindAttr) {
        const extractedName = extractNameFromBindAttribute(bindAttr);
        if (extractedName) {
            return extractedName;
        }
    }
    const openingName = element.openingElement.name;
    if (openingName.type === "JSXIdentifier") {
        const name = openingName.name;
        // Hacky way to get a clean name, we should probably leave the tag name in the future and map on the UI side?
        // Depends on how we want to use this data, if we want to use it outside just UI
        if (name.startsWith("Sb")) {
            return name.substring(2);
        }
        return name;
    }
    return null;
}
export function getScopeFunctionName(path) {
    const scope = path.scope;
    if (scope.path?.isArrowFunctionExpression()) {
        // maybe its assigned to some variable? we gotta do some funky scope stuff
        if (t.isVariableDeclarator(scope.parentBlock)) {
            const id = scope.parentBlock.id;
            if (id && t.isIdentifier(id)) {
                return getIdentiferName(id);
            }
        }
    }
    else if (scope.path?.isFunctionDeclaration()) {
        const id = scope.path.get("id");
        if (id && id.isIdentifier()) {
            return getIdentiferName(id.node);
        }
    }
    return null;
}
export function getJSXAttributeType(attr) {
    const logger = getLogger();
    if (attr.type === "JSXSpreadAttribute") {
        return "string";
    }
    if (attr.value?.type === "StringLiteral") {
        return "string";
    }
    if (attr.value?.type === "JSXExpressionContainer") {
        const expression = attr.value.expression;
        switch (expression.type) {
            case "StringLiteral":
                return "string";
            case "NumericLiteral":
                return "number";
            case "BooleanLiteral":
                return "boolean";
            case "ObjectExpression":
                return "object";
            case "ArrayExpression":
                return "array";
            default:
                logger.warn(`Unknown expression type in JSX attribute: ${expression.type} ${attr.value}`);
                return "string";
        }
    }
    return "string";
}
export function getJSXAttributeName(attr) {
    if (attr.type === "JSXAttribute") {
        const name = attr.name;
        if (name.type === "JSXIdentifier") {
            return name.name;
        }
    }
    return null;
}
export function getVariableReferences(jsxAttribute, variablesToCheck) {
    try {
        const result = new Set();
        // Simple recursive function to visit all nodes
        function visit(node) {
            if (!node || typeof node !== "object")
                return;
            // Handle identifiers
            if (t.isIdentifier(node)) {
                if (variablesToCheck.has(node.name)) {
                    result.add(node.name);
                }
            }
            // Handle member expressions - check the object part
            if (t.isMemberExpression(node)) {
                if (t.isIdentifier(node.object) &&
                    variablesToCheck.has(node.object.name)) {
                    result.add(node.object.name);
                }
            }
            // Handle call expressions - check the callee
            if (t.isCallExpression(node)) {
                if (t.isIdentifier(node.callee) &&
                    variablesToCheck.has(node.callee.name)) {
                    result.add(node.callee.name);
                }
            }
            // Recursively visit all child nodes
            for (const key in node) {
                if (key === "type" ||
                    key === "start" ||
                    key === "end" ||
                    key === "loc") {
                    continue;
                }
                const child = node[key];
                if (Array.isArray(child)) {
                    child.forEach(visit);
                }
                else if (child && typeof child === "object") {
                    visit(child);
                }
            }
        }
        // Start visiting from the JSX attribute
        visit(jsxAttribute);
        return Array.from(result);
    }
    catch (e) {
        console.error("Error analyzing JSX attribute for variable references:", e);
        return [];
    }
}
//# sourceMappingURL=jsx.js.map