import path from "node:path";
import { parse } from "@babel/parser";
import { NodePath } from "@babel/traverse";
import * as t from "@babel/types";
import { normalizePath } from "vite";
import { ComponentsManager } from "../components-manager.js";
import { generate } from "../generate.js";
import { traverse } from "../traverse.js";
import { getLogger } from "../util/logger.js";
export function objectToObjectExpression(obj) {
    const properties = Object.entries(obj).map(([key, value]) => {
        const keyNode = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)
            ? t.identifier(key)
            : t.stringLiteral(key);
        const valueNode = t.valueToNode(value);
        return t.objectProperty(keyNode, valueNode);
    });
    return t.objectExpression(properties);
}
export function objectExpressionToObject(obj, context) {
    return obj.properties.reduce((acc, property) => {
        if (property.type === "ObjectProperty") {
            const key = getIdentiferName(property.key);
            if (key) {
                acc[key] = nodeToValue(property.value, context);
            }
        }
        return acc;
    }, {});
}
export function createObjectKey(key) {
    return t.isValidIdentifier(key) ? t.identifier(key) : t.stringLiteral(key);
}
export function stringToTaggedTemplate(str) {
    return t.taggedTemplateExpression(t.identifier("SB"), t.templateLiteral([t.templateElement({ raw: str, cooked: str }, true)], []));
}
export function nodeToValue(node, context) {
    switch (node.type) {
        case "StringLiteral":
            return node.value;
        case "NumericLiteral":
            return node.value;
        case "BooleanLiteral":
            return node.value;
        case "NullLiteral":
            return null;
        case "UnaryExpression":
            return node.operator;
        case "ArrayExpression":
            return node.elements.map((element) => {
                if (element) {
                    return nodeToValue(element);
                }
                return t.nullLiteral();
            });
        case "ObjectExpression":
            return objectExpressionToObject(node, context);
        case "TaggedTemplateExpression":
            return getTemplateString(node.quasi);
        case "Identifier":
            return node.name === "undefined" ? undefined : node.name;
        case "MemberExpression": {
            const contextName = nodeToValue(node.object, context);
            const value = nodeToValue(node.property, context);
            if (!contextName || !value) {
                return null;
            }
            if (context &&
                context[contextName] &&
                typeof context[contextName] === "object") {
                // handleEnums
                return context[contextName][value];
            }
            else if (context === undefined) {
                // handle other member expressions. e.g: App.variable1
                const parts = memberExpressionToParts(node);
                const expression = parts.join(".");
                return expression;
            }
            return null;
        }
        case "CallExpression": {
            const callee = node.callee;
            if (callee.type === "MemberExpression") {
                return nodeToValue(callee);
            }
            return null;
        }
        case "BinaryExpression":
        case "ConditionalExpression":
            // dont log, its annoying now that clark is doing whatever it wants
            return null;
        default:
            return null;
    }
}
export function memberExpressionToParts(node) {
    const parts = [];
    let current = node;
    // MemberExpressions need to be unwound backwards, because of the way they are
    // nested by babel
    while (current.type === "MemberExpression" ||
        current.type === "OptionalMemberExpression") {
        const property = current.property;
        // this shouldn't happen, the babel type is weird
        if (Array.isArray(property)) {
            break;
        }
        if (property.type === "Identifier") {
            parts.unshift(property.name);
        }
        const object = current.object;
        // again, this shouldn't happen, the babel type is weird
        if (Array.isArray(object)) {
            break;
        }
        current = object;
    }
    if (current.type === "Identifier") {
        parts.unshift(current.name);
    }
    return parts;
}
export function getIdentiferName(attr) {
    switch (attr.type) {
        case "JSXIdentifier":
        case "Identifier":
            return attr.name;
        case "JSXNamespacedName":
            return attr.namespace.name;
        case "PrivateName":
            return attr.id.name;
        case "CallExpression":
            if (attr.callee.type === "MemberExpression") {
                return getIdentiferName(attr.callee);
            }
            return null;
        case "MemberExpression":
            return getIdentiferName(attr.object);
        case "StringLiteral":
            return attr.value;
        default:
            return null;
    }
}
export function getTemplateString(node) {
    if (node.quasis.length !== 1) {
        getLogger().warn(`Unexpected number of quasis in template literal: ${node.quasis.length}
      Line: ${node.loc?.start.line}
      Column: ${node.loc?.start.column}`);
        return null;
    }
    return node.quasis[0]?.value.raw ?? null;
}
export function isSbElement(node, libraryComponents) {
    let element;
    if (node.type === "JSXElement") {
        element = node;
    }
    else if (node.type === "JSXOpeningElement") {
        element = node.name;
    }
    else {
        return false;
    }
    const tagName = getTagName(element);
    return !!tagName && libraryComponents.includes(tagName);
}
export function isCustomComponent(tagName) {
    return ComponentsManager.getInstance().isCustomComponent(tagName);
}
export function getTagName(node) {
    if (node.type === "JSXElement") {
        if (node.openingElement.name.type === "JSXIdentifier") {
            return getTagName(node.openingElement.name);
        }
        else if (node.openingElement.name.type === "JSXMemberExpression") {
            return getTagName(node.openingElement.name);
        }
        else if (node.openingElement.name.type === "JSXNamespacedName") {
            return getTagName(node.openingElement.name);
        }
    }
    if (node.type === "JSXMemberExpression") {
        // For member expressions like MyNamespace.Component, return the full name
        const object = node.object;
        const property = node.property.name;
        if (object.type === "JSXIdentifier") {
            return `${object.name}.${property}`;
        }
        else if (object.type === "JSXMemberExpression") {
            // Handle nested member expressions like A.B.C
            const objectName = getTagName(object);
            return objectName ? `${objectName}.${property}` : property;
        }
        return property;
    }
    // A namespaced name is technically part of the spec, but React does not support it
    if (node.type === "JSXNamespacedName") {
        return node.namespace.name;
    }
    if (node.type === "JSXIdentifier") {
        return node.name;
    }
    return null;
}
export function getImportPath(fromFile, toFile) {
    const fromDir = path.dirname(fromFile);
    const relativePath = path.relative(fromDir, toFile);
    // Normalize to forward slashes (cross-platform compatibility)
    // path.relative() returns backslashes on Windows, but import statements require forward slashes
    const normalizedPath = normalizePath(relativePath);
    // Remove file extension
    let importPath = normalizedPath.replace(/\.(ts|tsx|js|jsx)$/, "");
    // Add ./ prefix for relative imports if needed
    if (!importPath.startsWith(".") && !importPath.startsWith("/")) {
        importPath = "./" + importPath;
    }
    return importPath;
}
export function addImport({ path, importName, importPath, isDefaultImport, onAdd, }) {
    const logger = getLogger();
    const programPath = path.scope.getProgramParent()
        ?.path;
    if (!programPath) {
        logger.warn("No program found");
        return;
    }
    // Check if the name is already used anywhere (across all packages)
    if (isDefaultImport) {
        const existingImportSpecifier = programPath
            .get("body")
            .find((p) => p.isImportDeclaration() &&
            p.node.specifiers.some((s) => s.local.name === importName));
        if (existingImportSpecifier) {
            logger.warn(`Cannot add another default import with an existing name. Import name: ${importName}`);
            return;
        }
    }
    const allImportDeclarationsFromPath = programPath
        .get("body")
        .filter((p) => p.isImportDeclaration() && p.node.source.value === importPath);
    // Check if the import name already exists across ANY import declaration from the same path
    if (!isDefaultImport) {
        const existingImportAcrossDeclarations = allImportDeclarationsFromPath.some((importDeclaration) => importDeclaration.node.specifiers.some((specifier) => specifier.local.name === importName));
        if (existingImportAcrossDeclarations) {
            return;
        }
    }
    const existingImportDeclaration = allImportDeclarationsFromPath[0];
    const importSpecifier = isDefaultImport
        ? t.importDefaultSpecifier(t.identifier(importName))
        : t.importSpecifier(t.identifier(importName), t.identifier(importName));
    if (!existingImportDeclaration) {
        programPath.unshiftContainer("body", t.importDeclaration([importSpecifier], t.stringLiteral(importPath)));
    }
    else {
        if (isDefaultImport) {
            existingImportDeclaration.node.specifiers.unshift(importSpecifier);
        }
        else {
            existingImportDeclaration.node.specifiers.push(importSpecifier);
        }
    }
    onAdd();
}
export function getObjectPropertyByKey(expression, key) {
    if (!expression.isObjectExpression()) {
        return null;
    }
    return (expression
        .get("properties")
        .find((property) => property.isObjectProperty() &&
        nodeToValue(property.get("key").node) === key) ?? null);
}
export function extractImportsFromAst(ast) {
    const imports = {};
    const traverse = (node) => {
        if (t.isNode(node) && node.type === "ImportDeclaration") {
            node.specifiers.forEach((specifier) => {
                const importName = specifier.local.name;
                imports[importName] = node.source.value;
            });
        }
        if (typeof node !== "object" || node === null) {
            return;
        }
        for (const key in node) {
            const possibleNode = node[key];
            traverse(possibleNode);
        }
    };
    traverse(ast.program);
    return imports;
}
export function isScopeFunction(name) {
    return name === "registerScope" || name === "registerPage";
}
export function isSbScopeFunction(name) {
    return name === "createScope";
}
export function extractNameFromBindAttribute(bindAttribute, identifierOnly = false) {
    const bindValue = bindAttribute.value;
    if (!bindValue || !t.isJSXExpressionContainer(bindValue)) {
        return null;
    }
    const expression = bindValue.expression;
    // Case 1: Simple identifier (bind={x})
    if (t.isIdentifier(expression)) {
        return expression.name;
    }
    if (identifierOnly) {
        return null;
    }
    // Case 2: Member expression (bind={x.y} or bind={x.y.z})
    if (t.isMemberExpression(expression)) {
        // Get the rightmost property name
        if (t.isIdentifier(expression.property)) {
            return expression.property.name;
        }
    }
    return null;
}
export function getFunctionExpressionBody(node) {
    if (!node) {
        return "";
    }
    if (node.type !== "ArrowFunctionExpression" &&
        node.type !== "FunctionExpression") {
        return "";
    }
    const isNodePath = node instanceof NodePath;
    const body = isNodePath ? node.get("body").node : node.body;
    const isBlockStatement = body.type === "BlockStatement";
    if (isBlockStatement) {
        const generated = generate(body).code;
        // its safer to remove the surrounding braces than to try and parse each line of the body on its own, because
        // you can run into edge cases (duplicate comments, for example) due to how babel tracks comments on
        // the trailing AND leading edges of statements.
        // See: https://astexplorer.net/#/gist/63fab4f401f0b70618deece3fa6ca08c/ec9eab83b94cca79566b07d38ce1f720d0a17918
        const content = generated.slice(1, -1).trim();
        const lines = content.split("\n");
        // we do this because `generate`, when not called with any minification, seems to prefer 2-space indentation for block statements,
        // which we do not want to preserve.
        return lines
            .map((line) => {
            if (line.trim().length === 0)
                return line;
            if (line.startsWith("  ")) {
                return line.slice(2);
            }
            return line;
        })
            .join("\n");
    }
    const result = generate(body);
    return result?.code || "";
}
export function parseExpression(value) {
    const compiled = parse(value, {
        sourceType: "module",
        plugins: ["typescript", "jsx"],
    });
    const exprStatement = compiled.program.body.find((node) => t.isExpressionStatement(node));
    if (exprStatement) {
        return exprStatement.expression;
    }
    else {
        getLogger().error(`No expression statement found for value: ${value}`);
        return null;
    }
}
export function getFunctionBodyThisExpressionData(code) {
    try {
        // have to handle objects correctly
        if (code.trim().startsWith("{")) {
            code = `(${code})`;
        }
        const wrappingFunction = `const a = () => {
  ${code}
};`;
        const compiled = parse(wrappingFunction, {
            sourceType: "module",
            plugins: ["typescript", "jsx"],
        });
        let hasThisReference = false;
        let hasReturnStatement = false;
        traverse(compiled, {
            ThisExpression() {
                hasThisReference = true;
            },
            ReturnStatement() {
                hasReturnStatement = true;
            },
        });
        return {
            hasThisReference,
            hasReturnStatement,
        };
    }
    catch {
        return {
            hasThisReference: false,
            hasReturnStatement: false,
        };
    }
}
export function canBeAStringLiteral(value) {
    try {
        new Function(`return "${value}"`);
        return true;
    }
    catch {
        return false;
    }
}
//# sourceMappingURL=util.js.map