'use strict';

var NodeGraph = require('./NodeGraph.js');
var ScriptAST = require('./ScriptAST.js');

/* ────────────────────────────────────────────────────────────────
   Graph to AST Converter
   
   Converts visual scripting NodeGraph to language-agnostic AST
   for export to any target language.
──────────────────────────────────────────────────────────────── */
/* ── Type Mapping ─────────────────────────────────────────────── */
function portTypeToASTType(portType) {
    const mapping = {
        flow: 'void',
        boolean: 'bool',
        number: 'float',
        string: 'string',
        vec2: 'vector2',
        color: 'color',
        entity: 'entity',
        any: 'any',
    };
    return ScriptAST.createType(mapping[portType] || 'any');
}
function variableTypeToASTType(type) {
    return portTypeToASTType(type);
}
const defaultOptions = {
    className: 'GeneratedScript',
    useAsync: false,
    includeNodeIds: false,
    varPrefix: '_v',
};
function createContext(library, graph, options) {
    return {
        library,
        graph,
        options: { ...defaultOptions, ...options },
        nodeVars: new Map(),
        varCounter: 0,
        visited: new Set(),
        statements: [],
    };
}
/* ── Main Converter ───────────────────────────────────────────── */
class GraphToAST {
    constructor(library) {
        this.library = library !== null && library !== void 0 ? library : NodeGraph.createDefaultNodeLibrary();
    }
    /**
     * Convert a visual script graph to AST representation.
     */
    convert(graph, options = {}) {
        const ctx = createContext(this.library, graph, options);
        // Generate class with methods for each entry point
        const cls = {
            kind: 'class',
            name: ctx.options.className,
            baseClass: ctx.options.baseClass,
            fields: this.convertVariables(graph.variables, ctx),
            methods: [],
        };
        // Find all entry point nodes (event_start, event_update, etc.)
        const entryNodes = this.findEntryNodes(graph);
        for (const [entryType, nodes] of entryNodes) {
            const methodName = this.entryTypeToMethodName(entryType);
            const method = this.convertEntryPointsToMethod(methodName, nodes, ctx);
            cls.methods.push(method);
        }
        // If no entry points, create a default execute method
        if (cls.methods.length === 0) {
            cls.methods.push(this.createEmptyMethod('execute'));
        }
        return {
            name: graph.name,
            imports: [],
            classes: [cls],
        };
    }
    /**
     * Convert multiple graphs into a single script module.
     */
    convertMultiple(graphs, moduleName, options = {}) {
        const classes = [];
        for (const graph of graphs) {
            const script = this.convert(graph, { ...options, className: graph.name });
            classes.push(...script.classes);
        }
        return {
            name: moduleName,
            imports: [],
            classes,
        };
    }
    /* ── Entry Point Detection ──────────────────────────────────── */
    findEntryNodes(graph) {
        const entries = new Map();
        const entryTypes = [
            'event_start',
            'event_update',
            'event_fixed_update',
            'event_input',
            'event_collision',
            'event_trigger',
            'event_custom',
        ];
        for (const node of graph.nodes) {
            if (entryTypes.includes(node.type)) {
                const existing = entries.get(node.type) || [];
                existing.push(node);
                entries.set(node.type, existing);
            }
        }
        return entries;
    }
    entryTypeToMethodName(entryType) {
        const mapping = {
            event_start: 'onStart',
            event_update: 'onUpdate',
            event_fixed_update: 'onFixedUpdate',
            event_input: 'onInput',
            event_collision: 'onCollision',
            event_trigger: 'onTrigger',
        };
        return mapping[entryType] || entryType.replace('event_', 'on_');
    }
    /* ── Variable Conversion ────────────────────────────────────── */
    convertVariables(variables, _ctx) {
        return variables.map((v) => ({
            name: v.name,
            fieldType: variableTypeToASTType(v.type),
            initializer: v.value !== undefined ? ScriptAST.createLiteral(v.value, this.valueToASTType(v.value)) : undefined,
            visibility: v.scope === 'global' ? 'public' : 'private',
            isExported: v.scope === 'global',
        }));
    }
    valueToASTType(value) {
        if (typeof value === 'boolean')
            return 'bool';
        if (typeof value === 'number')
            return Number.isInteger(value) ? 'int' : 'float';
        if (typeof value === 'string')
            return 'string';
        return 'any';
    }
    /* ── Method Generation ──────────────────────────────────────── */
    convertEntryPointsToMethod(methodName, entryNodes, ctx) {
        var _a;
        // Reset context for new method
        ctx.nodeVars.clear();
        ctx.varCounter = 0;
        ctx.visited.clear();
        ctx.statements = [];
        // Parameters based on entry type
        const params = this.getEntryParams(((_a = entryNodes[0]) === null || _a === void 0 ? void 0 : _a.type) || '');
        // Process each entry node's flow
        for (const entryNode of entryNodes) {
            this.processNode(entryNode, ctx);
        }
        return {
            kind: 'function',
            name: methodName,
            params,
            returnType: ScriptAST.createType('void'),
            body: ctx.statements,
            visibility: 'public',
        };
    }
    getEntryParams(entryType) {
        switch (entryType) {
            case 'event_update':
            case 'event_fixed_update':
                return [{ name: 'dt', paramType: ScriptAST.createType('float') }];
            case 'event_collision':
                return [{ name: 'other', paramType: ScriptAST.createType('entity') }];
            default:
                return [];
        }
    }
    createEmptyMethod(name) {
        return {
            kind: 'function',
            name,
            params: [],
            returnType: ScriptAST.createType('void'),
            body: [],
            visibility: 'public',
        };
    }
    /* ── Node Processing ────────────────────────────────────────── */
    processNode(node, ctx) {
        if (ctx.visited.has(node.id))
            return;
        ctx.visited.add(node.id);
        const def = ctx.library.get(node.type);
        if (!def) {
            ctx.statements.push({
                kind: 'expressionStatement',
                expression: ScriptAST.createCall('__unknown_node__', [ScriptAST.createLiteral(node.type, 'string')]),
                comment: `Unknown node type: ${node.type}`,
            });
            return;
        }
        // Add debug comment if requested
        if (ctx.options.includeNodeIds) {
            const lastStmt = ctx.statements[ctx.statements.length - 1];
            if (lastStmt) {
                lastStmt.comment = `Node: ${node.id} (${node.type})`;
            }
        }
        // Generate code based on node type
        this.generateNodeCode(node, def, ctx);
        // Follow flow connections
        this.followFlowConnections(node, ctx);
    }
    generateNodeCode(node, def, ctx) {
        switch (def.category) {
            case 'flow':
                this.generateFlowNode(node, def, ctx);
                break;
            case 'math':
                this.generateMathNode(node, def, ctx);
                break;
            case 'logic':
                this.generateLogicNode(node, def, ctx);
                break;
            case 'variable':
                this.generateVariableNode(node, def, ctx);
                break;
            case 'entity':
                this.generateEntityNode(node, def, ctx);
                break;
            case 'input':
                this.generateInputNode(node, def, ctx);
                break;
            case 'audio':
                this.generateAudioNode(node, def, ctx);
                break;
            default:
                this.generateGenericNode(node, def, ctx);
        }
    }
    /* ── Flow Node Generation ───────────────────────────────────── */
    generateFlowNode(node, def, ctx) {
        switch (node.type) {
            case 'event_start':
            case 'event_update':
                // Entry nodes don't generate code themselves
                break;
            case 'if_else':
                this.generateIfElse(node, ctx);
                break;
            case 'delay':
                this.generateDelay(node, ctx);
                break;
            case 'sequence':
                // Sequence just follows connections in order
                break;
            case 'for_loop':
                this.generateForLoop(node, ctx);
                break;
            case 'while_loop':
                this.generateWhileLoop(node, ctx);
                break;
        }
    }
    generateIfElse(node, ctx) {
        const condExpr = this.resolveInputExpression(node, 'cond', ctx);
        // Find true and false branch connections
        const trueBranch = this.getFlowTargetNodes(node, 'true', ctx);
        const falseBranch = this.getFlowTargetNodes(node, 'false', ctx);
        // Generate true branch statements
        const savedStatements = ctx.statements;
        ctx.statements = [];
        for (const target of trueBranch) {
            this.processNode(target, ctx);
        }
        const thenStatements = ctx.statements;
        // Generate false branch statements
        ctx.statements = [];
        for (const target of falseBranch) {
            this.processNode(target, ctx);
        }
        const elseStatements = ctx.statements;
        // Restore and add if statement
        ctx.statements = savedStatements;
        ctx.statements.push(ScriptAST.createIf(condExpr, thenStatements, elseStatements.length > 0 ? elseStatements : undefined));
    }
    generateDelay(node, ctx) {
        const seconds = this.resolveInputExpression(node, 'seconds', ctx);
        // Generate a yield/await based on target
        if (ctx.options.useAsync) {
            ctx.statements.push({
                kind: 'await',
                expression: ScriptAST.createCall('delay', [seconds]),
            });
        }
        else {
            ctx.statements.push({
                kind: 'yield',
                value: ScriptAST.createCall('WaitForSeconds', [seconds]),
            });
        }
    }
    generateForLoop(node, ctx) {
        const startExpr = this.resolveInputExpression(node, 'start', ctx);
        const endExpr = this.resolveInputExpression(node, 'end', ctx);
        const loopVar = `${ctx.options.varPrefix}i${ctx.varCounter++}`;
        const bodyTargets = this.getFlowTargetNodes(node, 'body', ctx);
        const savedStatements = ctx.statements;
        ctx.statements = [];
        for (const target of bodyTargets) {
            this.processNode(target, ctx);
        }
        const bodyStatements = ctx.statements;
        ctx.statements = savedStatements;
        ctx.statements.push({
            kind: 'for',
            init: ScriptAST.createVarDecl(loopVar, ScriptAST.createType('int'), startExpr),
            condition: ScriptAST.createBinaryOp('<', ScriptAST.createIdentifier(loopVar), endExpr),
            update: {
                kind: 'unaryOp',
                operator: '++',
                operand: ScriptAST.createIdentifier(loopVar),
                prefix: false,
            },
            body: bodyStatements,
        });
    }
    generateWhileLoop(node, ctx) {
        const condExpr = this.resolveInputExpression(node, 'condition', ctx);
        const bodyTargets = this.getFlowTargetNodes(node, 'body', ctx);
        const savedStatements = ctx.statements;
        ctx.statements = [];
        for (const target of bodyTargets) {
            this.processNode(target, ctx);
        }
        const bodyStatements = ctx.statements;
        ctx.statements = savedStatements;
        ctx.statements.push({
            kind: 'while',
            condition: condExpr,
            body: bodyStatements,
        });
    }
    /* ── Math Node Generation ───────────────────────────────────── */
    generateMathNode(node, _def, ctx) {
        const varName = this.getOrCreateNodeVar(node, ctx);
        let expr;
        switch (node.type) {
            case 'math_add':
                expr = ScriptAST.createBinaryOp('+', this.resolveInputExpression(node, 'a', ctx), this.resolveInputExpression(node, 'b', ctx));
                break;
            case 'math_sub':
                expr = ScriptAST.createBinaryOp('-', this.resolveInputExpression(node, 'a', ctx), this.resolveInputExpression(node, 'b', ctx));
                break;
            case 'math_mul':
                expr = ScriptAST.createBinaryOp('*', this.resolveInputExpression(node, 'a', ctx), this.resolveInputExpression(node, 'b', ctx));
                break;
            case 'math_div':
                expr = ScriptAST.createBinaryOp('/', this.resolveInputExpression(node, 'a', ctx), this.resolveInputExpression(node, 'b', ctx));
                break;
            case 'math_random':
                expr = ScriptAST.createCall('random', [
                    this.resolveInputExpression(node, 'min', ctx),
                    this.resolveInputExpression(node, 'max', ctx),
                ]);
                break;
            default:
                expr = ScriptAST.createLiteral(0, 'float');
        }
        ctx.statements.push(ScriptAST.createVarDecl(varName, ScriptAST.createType('float'), expr));
    }
    /* ── Logic Node Generation ──────────────────────────────────── */
    generateLogicNode(node, _def, ctx) {
        const varName = this.getOrCreateNodeVar(node, ctx);
        let expr;
        switch (node.type) {
            case 'compare_eq':
                expr = ScriptAST.createBinaryOp('==', this.resolveInputExpression(node, 'a', ctx), this.resolveInputExpression(node, 'b', ctx));
                break;
            case 'compare_gt':
                expr = ScriptAST.createBinaryOp('>', this.resolveInputExpression(node, 'a', ctx), this.resolveInputExpression(node, 'b', ctx));
                break;
            case 'compare_lt':
                expr = ScriptAST.createBinaryOp('<', this.resolveInputExpression(node, 'a', ctx), this.resolveInputExpression(node, 'b', ctx));
                break;
            case 'logic_and':
                expr = ScriptAST.createBinaryOp('&&', this.resolveInputExpression(node, 'a', ctx), this.resolveInputExpression(node, 'b', ctx));
                break;
            case 'logic_or':
                expr = ScriptAST.createBinaryOp('||', this.resolveInputExpression(node, 'a', ctx), this.resolveInputExpression(node, 'b', ctx));
                break;
            case 'logic_not':
                expr = {
                    kind: 'unaryOp',
                    operator: '!',
                    operand: this.resolveInputExpression(node, 'a', ctx),
                    prefix: true,
                };
                break;
            default:
                expr = ScriptAST.createLiteral(false, 'bool');
        }
        ctx.statements.push(ScriptAST.createVarDecl(varName, ScriptAST.createType('bool'), expr));
    }
    /* ── Variable Node Generation ───────────────────────────────── */
    generateVariableNode(node, _def, ctx) {
        const varName = node.inputValues['name'] || 'unnamed';
        switch (node.type) {
            case 'get_variable':
                ctx.nodeVars.set(node.id, varName);
                break;
            case 'set_variable':
                ctx.statements.push(ScriptAST.createAssignment(ScriptAST.createIdentifier(varName), this.resolveInputExpression(node, 'value', ctx)));
                break;
        }
    }
    /* ── Entity Node Generation ─────────────────────────────────── */
    generateEntityNode(node, _def, ctx) {
        switch (node.type) {
            case 'get_position': {
                const entity = this.resolveInputExpression(node, 'entity', ctx);
                const xVar = this.getOrCreateNodeVar(node, ctx, '_x');
                const yVar = this.getOrCreateNodeVar(node, ctx, '_y');
                ctx.statements.push(ScriptAST.createVarDecl(xVar, ScriptAST.createType('float'), ScriptAST.createPropertyAccess(ScriptAST.createPropertyAccess(entity, 'position'), 'x')));
                ctx.statements.push(ScriptAST.createVarDecl(yVar, ScriptAST.createType('float'), ScriptAST.createPropertyAccess(ScriptAST.createPropertyAccess(entity, 'position'), 'y')));
                break;
            }
            case 'set_position': {
                const entity = this.resolveInputExpression(node, 'entity', ctx);
                const x = this.resolveInputExpression(node, 'x', ctx);
                const y = this.resolveInputExpression(node, 'y', ctx);
                ctx.statements.push(ScriptAST.createExprStatement(ScriptAST.createMethodCall(entity, 'setPosition', [x, y])));
                break;
            }
            // Add more entity nodes as needed
        }
    }
    /* ── Input Node Generation ──────────────────────────────────── */
    generateInputNode(node, _def, ctx) {
        switch (node.type) {
            case 'input_action': {
                const actionName = node.inputValues['action'] || 'action';
                const varName = this.getOrCreateNodeVar(node, ctx);
                ctx.statements.push(ScriptAST.createVarDecl(varName, ScriptAST.createType('any'), ScriptAST.createMethodCall(ScriptAST.createIdentifier('input'), 'getAction', [
                    ScriptAST.createLiteral(actionName, 'string'),
                ])));
                break;
            }
        }
    }
    /* ── Audio Node Generation ──────────────────────────────────── */
    generateAudioNode(node, _def, ctx) {
        switch (node.type) {
            case 'play_sound': {
                const soundId = this.resolveInputExpression(node, 'soundId', ctx);
                const volume = this.resolveInputExpression(node, 'volume', ctx);
                ctx.statements.push(ScriptAST.createExprStatement(ScriptAST.createMethodCall(ScriptAST.createIdentifier('audio'), 'play', [soundId, volume])));
                break;
            }
        }
    }
    /* ── Generic Node Generation ────────────────────────────────── */
    generateGenericNode(node, def, ctx) {
        // Generate a function call for unknown/custom nodes
        const args = [];
        for (const port of def.ports) {
            if (port.direction === 'input' && port.dataType !== 'flow') {
                args.push(this.resolveInputExpression(node, port.id, ctx));
            }
        }
        const hasOutput = def.ports.some((p) => p.direction === 'output' && p.dataType !== 'flow');
        if (hasOutput) {
            const varName = this.getOrCreateNodeVar(node, ctx);
            ctx.statements.push(ScriptAST.createVarDecl(varName, ScriptAST.createType('any'), ScriptAST.createCall(node.type, args)));
        }
        else {
            ctx.statements.push(ScriptAST.createExprStatement(ScriptAST.createCall(node.type, args)));
        }
    }
    /* ── Helper Methods ─────────────────────────────────────────── */
    resolveInputExpression(node, portId, ctx) {
        var _a, _b;
        // Check for incoming connection
        const conn = ctx.graph.connections.find((c) => c.toNode === node.id && c.toPort === portId);
        if (conn) {
            // Get value from connected node's output
            const sourceNode = ctx.graph.nodes.find((n) => n.id === conn.fromNode);
            if (sourceNode) {
                // Ensure source node has been processed
                if (!ctx.visited.has(sourceNode.id)) {
                    this.processNode(sourceNode, ctx);
                }
                // Return reference to the generated variable
                const varName = ctx.nodeVars.get(sourceNode.id);
                if (varName) {
                    return ScriptAST.createIdentifier(varName);
                }
            }
        }
        // Fall back to static input value
        const def = ctx.library.get(node.type);
        const portDef = def === null || def === void 0 ? void 0 : def.ports.find((p) => p.id === portId);
        const value = (_b = (_a = node.inputValues[portId]) !== null && _a !== void 0 ? _a : portDef === null || portDef === void 0 ? void 0 : portDef.defaultValue) !== null && _b !== void 0 ? _b : null;
        return this.valueToExpression(value, portDef === null || portDef === void 0 ? void 0 : portDef.dataType);
    }
    valueToExpression(value, portType) {
        if (value === null || value === undefined) {
            return ScriptAST.createLiteral(null, 'any');
        }
        switch (typeof value) {
            case 'boolean':
                return ScriptAST.createLiteral(value, 'bool');
            case 'number':
                return ScriptAST.createLiteral(value, portType === 'number' ? 'float' : 'int');
            case 'string':
                return ScriptAST.createLiteral(value, 'string');
            case 'object':
                if ('x' in value && 'y' in value) {
                    return ScriptAST.createLiteral(value, 'vector2');
                }
                return ScriptAST.createLiteral(value, 'any');
            default:
                return ScriptAST.createLiteral(value, 'any');
        }
    }
    getOrCreateNodeVar(node, ctx, suffix = '') {
        const key = node.id + suffix;
        if (!ctx.nodeVars.has(key)) {
            ctx.nodeVars.set(key, `${ctx.options.varPrefix}${ctx.varCounter++}${suffix}`);
        }
        return ctx.nodeVars.get(key);
    }
    getFlowTargetNodes(node, portId, ctx) {
        const connections = ctx.graph.connections.filter((c) => c.fromNode === node.id && c.fromPort === portId);
        return connections
            .map((c) => ctx.graph.nodes.find((n) => n.id === c.toNode))
            .filter((n) => n !== undefined);
    }
    followFlowConnections(node, ctx) {
        const def = ctx.library.get(node.type);
        if (!def)
            return;
        // Find output flow ports
        const flowPorts = def.ports.filter((p) => p.direction === 'output' && p.dataType === 'flow');
        for (const port of flowPorts) {
            // Skip special ports handled by specific node generators
            if (['true', 'false', 'body'].includes(port.id))
                continue;
            const targets = this.getFlowTargetNodes(node, port.id, ctx);
            for (const target of targets) {
                this.processNode(target, ctx);
            }
        }
    }
}
/* ── Export Convenience Functions ─────────────────────────────── */
function convertGraphToAST(graph, library, options) {
    const converter = new GraphToAST(library);
    return converter.convert(graph, options);
}

exports.GraphToAST = GraphToAST;
exports.convertGraphToAST = convertGraphToAST;
//# sourceMappingURL=GraphToAST.js.map
