unit Myc.Ast.Compiler.Macros;

interface

uses
    System.SysUtils,
    System.Classes,
    System.Generics.Collections,
    Myc.Data.Scalar,
    Myc.Data.Value,
    Myc.Ast.Nodes,
    Myc.Ast.Visitor,
    Myc.Ast.Scope,
    Myc.Ast.Types,
    Myc.Ast;

type
    // Exception specific to macro expansion errors
    EMacroException = class(EAstException);

    IAstMacroExpander = interface(IAstVisitor)
        function Execute(const RootNode: IAstNode): IAstNode;
    end;

    // Callback used by TMacroExpander to request full compilation and execution of macro arguments.
    TMacroEvaluatorProc = reference to function(const Scope: IExecutionScope; const Node: IAstNode): TDataValue;

    // Handles the expansion of the macro body template (Quasiquotes/Unquotes).
    TExpansionVisitor = class(TAstTransformer)
    private
        FMacroEvaluator: TMacroEvaluatorProc;
        FMacroScope: IExecutionScope;
        FRenameMap: TDictionary<string, string>;
        class var
            FGensymCounter: Int64;

        // Helper to handle unquote-splicing (~@) within lists.
        // Takes a source list (e.g., Arguments) and returns a flattened array for the new node.
        function TransformAndSpliceNodes(const ANodes: INodeList<IAstNode>): TArray<IAstNode>;
        function Gensym(const ABaseName: string): string;
    protected
        function VisitUnquote(const Node: IUnquoteNode): IAstNode; override;
        function VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): IAstNode; override;

        // We override these container visits to handle Splicing manually,
        // bypassing the standard 1:1 list visitor.
        function VisitFunctionCall(const Node: IFunctionCallNode): IAstNode; override;
        function VisitBlockExpression(const Node: IBlockExpressionNode): IAstNode; override;

        function VisitRecordLiteral(const Node: IRecordLiteralNode): IAstNode; override;
        function VisitVariableDeclaration(const Node: IVariableDeclarationNode): IAstNode; override;
        function VisitLambdaExpression(const Node: ILambdaExpressionNode): IAstNode; override;
        function VisitIdentifier(const Node: IIdentifierNode): IAstNode; override;
    public
        constructor Create(const AMacroScope: IExecutionScope; const AMacroEvaluator: TMacroEvaluatorProc);
        destructor Destroy; override;

        class function Expand(
            const MacroScope: IExecutionScope;
            const RootNode: IAstNode;
            const MacroEvaluator: TMacroEvaluatorProc
        ): IAstNode;
    end;

    IMacroRegistry = interface
        {$region 'private'}
        function GetParent: IMacroRegistry;
        {$endregion}
        procedure Define(const Node: IMacroDefinitionNode);
        function Find(const Name: string): IMacroDefinitionNode;
        function CreateChildRegistry: IMacroRegistry;
        property Parent: IMacroRegistry read GetParent;
    end;

    // Handles the expansion of macro calls within the AST.
    TMacroExpander = class(TAstTransformer, IAstMacroExpander)
    private
        FInitialScope: IExecutionScope;
        FCurrentMacroRegistry: IMacroRegistry;
        FMacroEvaluator: TMacroEvaluatorProc;

        procedure EnterMacroScope;
        procedure ExitMacroScope;

    protected
        function VisitMacroDefinition(const Node: IMacroDefinitionNode): IAstNode; override;
        function VisitFunctionCall(const Node: IFunctionCallNode): IAstNode; override;

        function VisitQuasiquote(const Node: IQuasiquoteNode): IAstNode; override;
        function VisitUnquote(const Node: IUnquoteNode): IAstNode; override;
        function VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): IAstNode; override;

        function VisitBlockExpression(const Node: IBlockExpressionNode): IAstNode; override;
        function VisitLambdaExpression(const Node: ILambdaExpressionNode): IAstNode; override;

    public
        constructor Create(
            const ARootRegistry: IMacroRegistry;
            const AInitialScope: IExecutionScope;
            const AMacroEvaluator: TMacroEvaluatorProc
        );
        destructor Destroy; override;

        function Execute(const RootNode: IAstNode): IAstNode;

        class function ExpandMacros(
            const ARootRegistry: IMacroRegistry;
            const AInitialScope: IExecutionScope;
            const RootNode: IAstNode;
            const MacroEvaluator: TMacroEvaluatorProc
        ): IAstNode; static;
    end;

    // Implementation of the macro registry interface defined in Myc.Ast.Environment.
    TMacroRegistry = class(TInterfacedObject, IMacroRegistry)
    private
        FParent: IMacroRegistry;
        FMacros: TDictionary<string, IMacroDefinitionNode>;
        function GetParent: IMacroRegistry;
    public
        constructor Create(AParent: IMacroRegistry);
        destructor Destroy; override;
        procedure Define(const Node: IMacroDefinitionNode);
        function Find(const Name: string): IMacroDefinitionNode;
        function CreateChildRegistry: IMacroRegistry;
    end;

implementation

uses
    System.Generics.Defaults,
    System.SyncObjs,
    Myc.Data.Keyword;

{ TExpansionVisitor }

constructor TExpansionVisitor.Create(const AMacroScope: IExecutionScope; const AMacroEvaluator: TMacroEvaluatorProc);
begin
    inherited Create;
    FMacroScope := AMacroScope;
    FMacroEvaluator := AMacroEvaluator;
    FRenameMap := TDictionary<string, string>.Create;
end;

destructor TExpansionVisitor.Destroy;
begin
    FRenameMap.Free;
    inherited;
end;

function TExpansionVisitor.Gensym(const ABaseName: string): string;
begin
    if not FRenameMap.TryGetValue(ABaseName, Result) then
    begin
        var count := TInterlocked.Add(FGensymCounter, 1);
        if count < 0 then
            count := -count;
        Result := ABaseName + '#' + count.ToString;
        FRenameMap.Add(ABaseName, Result);
    end;
end;

class function TExpansionVisitor.Expand(
    const MacroScope: IExecutionScope;
    const RootNode: IAstNode;
    const MacroEvaluator: TMacroEvaluatorProc
): IAstNode;
begin
    var expander := TExpansionVisitor.Create(MacroScope, MacroEvaluator);
    try
        Result := expander.Accept(RootNode);
    finally
        expander.Free;
    end;
end;

function TExpansionVisitor.TransformAndSpliceNodes(const ANodes: INodeList<IAstNode>): TArray<IAstNode>;
var
    newList: TList<IAstNode>;
    nodeToSplice: IAstNode;
    transformedNode: IAstNode;
    node: IAstNode;
begin
    newList := TList<IAstNode>.Create;
    try
        for node in ANodes do
        begin
            if node.Kind = akUnquoteSplicing then
            begin
                var spliceExpr := node.AsUnquoteSplicing.Expression;
                var evaluatedSpliceValue: TDataValue;

                try
                    evaluatedSpliceValue := FMacroEvaluator(FMacroScope, spliceExpr);
                except
                    on E: EAstException do
                        raise;
                    on E: Exception do
                        raise EMacroException.Create('Error during macro splice evaluation: ' + E.Message);
                end;

                if (not evaluatedSpliceValue.IsVoid) and (evaluatedSpliceValue.Kind = vkInterface) then
                begin
                    nodeToSplice := evaluatedSpliceValue.AsIntf<IAstNode>;

                    // Splicing implies the value must be a list-like AST structure (Block or just a List Node)
                    // If the evaluated value is an IArgumentList, IExpressionList etc., we flatten it.
                    if nodeToSplice.Kind = akExpressionList then
                    begin
                        for var subItem in nodeToSplice.AsExpressionList do
                            newList.Add(subItem);
                    end
                    else if nodeToSplice.Kind = akArgumentList then
                    begin
                        for var subItem in nodeToSplice.AsArgumentList do
                            newList.Add(subItem);
                    end
                    else if nodeToSplice.Kind = akBlockExpression then
                    begin
                        // Splice content of block
                        for var subItem in nodeToSplice.AsBlockExpression.Expressions do
                            newList.Add(subItem);
                    end
                    else
                    begin
                        // Treat as single item
                        newList.Add(nodeToSplice);
                    end;
                end
                else if (not evaluatedSpliceValue.IsVoid) then
                begin
                    // Use Splice Node Identity for the new Constant
                    newList.Add(TAst.Constant(evaluatedSpliceValue, node.Identity.Location));
                end;
            end
            else
            begin
                transformedNode := Self.Accept(node);
                if Assigned(transformedNode) then
                    newList.Add(transformedNode);
            end;
        end;
        Result := newList.ToArray;
    finally
        newList.Free;
    end;
end;

function TExpansionVisitor.VisitIdentifier(const Node: IIdentifierNode): IAstNode;
var
    newName: string;
begin
    // Apply hygienic renaming
    if FRenameMap.TryGetValue(Node.Name, newName) then
    begin
        // Create new Identifier with new Name but preserve Location from original Node
        Result := TAst.Identifier(newName, Node.Identity.Location);
        exit;
    end;
    Result := Node;
end;

function TExpansionVisitor.VisitVariableDeclaration(const Node: IVariableDeclarationNode): IAstNode;
var
    newTarget, newInit: IAstNode;
    newName: string;
begin
    newInit := Accept(Node.Initializer);

    if Node.Target.Kind = akIdentifier then
    begin
        newName := Gensym(Node.Target.AsIdentifier.Name);
        // Use location from original target
        newTarget := TAst.Identifier(newName, Node.Target.Identity.Location);
    end
    else
    begin
        newTarget := Accept(Node.Target);
    end;

    Result := TAst.VarDecl(Node.Identity, newTarget, newInit, TTypes.Unknown, False);
end;

function TExpansionVisitor.VisitLambdaExpression(const Node: ILambdaExpressionNode): IAstNode;
var
    newParams: TArray<IIdentifierNode>;
    newBody: IAstNode;
    newName: string;
    i: Integer;
    paramList: IParameterList;
begin
    SetLength(newParams, Node.Parameters.Count);
    for i := 0 to Node.Parameters.Count - 1 do
    begin
        var param := Node.Parameters[i];
        newName := Gensym(param.Name);
        // Use location from original param
        newParams[i] := TAst.Identifier(newName, param.Identity.Location);
    end;

    newBody := Accept(Node.Body);

    // Rebuild: Wrap array in TParameterList with original Identity
    paramList := TParameterList.Create(newParams, Node.Parameters.Identity);

    // Use transformation overload
    Result := TAst.LambdaExpr(Node.Identity, paramList, newBody);
end;

function TExpansionVisitor.VisitUnquote(const Node: IUnquoteNode): IAstNode;
var
    value: TDataValue;
    expr: IAstNode;
    addr: TResolvedAddress;
begin
    expr := Node.Expression;

    // Optimization: If unquote refers to a local macro variable directly, try to get it.
    if expr.Kind = akIdentifier then
    begin
        addr := FMacroScope.Resolve(expr.AsIdentifier.Name);
        if (addr.Kind = akLocalOrParent) and (addr.ScopeDepth = 0) then
        begin
            var argValue := FMacroScope.Values[addr];
            if argValue.Kind = vkInterface then
            begin
                Result := argValue.AsIntf<IAstNode>;
                exit;
            end;
        end;
    end;

    try
        value := FMacroEvaluator(FMacroScope, expr);
    except
        on E: EAstException do
            raise;
        on E: Exception do
            raise EMacroException.Create('Error during macro evaluation: ' + E.Message);
    end;

    if value.Kind = vkInterface then
    begin
        Result := value.AsIntf<IAstNode>;
        exit;
    end;

    if value.Kind in [vkScalar, vkText, vkVoid] then
        // Create Constant node, inheriting location from the Unquote node
        Result := TAst.Constant(value, Node.Identity.Location)
    else
        raise EMacroException.CreateFmt('Cannot unquote complex runtime value of type %s at compile time.', [value.Kind.ToString]);
end;

function TExpansionVisitor.VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): IAstNode;
begin
    raise EMacroException.Create('Unquote-splicing (`~@`) can only appear inside a list form.');
end;

function TExpansionVisitor.VisitFunctionCall(const Node: IFunctionCallNode): IAstNode;
var
    newArgs: TArray<IAstNode>;
    transformedCallee: IAstNode;
    argList: IArgumentList;
begin
    transformedCallee := Self.Accept(Node.Callee);

    // Use splicing aware transformation for arguments (returns TArray)
    newArgs := TransformAndSpliceNodes(Node.Arguments);

    // Wrap in List container to use Transformation overload
    argList := TArgumentList.Create(newArgs, Node.Arguments.Identity);

    Result := TAst.FunctionCall(Node.Identity, transformedCallee, argList);
end;

function TExpansionVisitor.VisitBlockExpression(const Node: IBlockExpressionNode): IAstNode;
var
    newExprs: TArray<IAstNode>;
    exprList: IExpressionList;
begin
    // Use splicing aware transformation for block expressions
    newExprs := TransformAndSpliceNodes(Node.Expressions);

    // Wrap in List
    exprList := TExpressionList.Create(newExprs, Node.Expressions.Identity);

    Result := TAst.Block(Node.Identity, exprList);
end;

function TExpansionVisitor.VisitRecordLiteral(const Node: IRecordLiteralNode): IAstNode;
begin
    // For now, standard transformation for records (no splicing key-values yet)
    Result := inherited VisitRecordLiteral(Node);
end;

{ TMacroExpander }

constructor TMacroExpander.Create(
    const ARootRegistry: IMacroRegistry;
    const AInitialScope: IExecutionScope;
    const AMacroEvaluator: TMacroEvaluatorProc
);
begin
    inherited Create;
    FInitialScope := AInitialScope;
    FMacroEvaluator := AMacroEvaluator;
    FCurrentMacroRegistry := ARootRegistry.CreateChildRegistry;
end;

destructor TMacroExpander.Destroy;
begin
    inherited Destroy;
end;

procedure TMacroExpander.EnterMacroScope;
begin
    FCurrentMacroRegistry := FCurrentMacroRegistry.CreateChildRegistry;
end;

procedure TMacroExpander.ExitMacroScope;
begin
    FCurrentMacroRegistry := FCurrentMacroRegistry.Parent;
end;

class function TMacroExpander.ExpandMacros(
    const ARootRegistry: IMacroRegistry;
    const AInitialScope: IExecutionScope;
    const RootNode: IAstNode;
    const MacroEvaluator: TMacroEvaluatorProc
): IAstNode;
begin
    var expander := TMacroExpander.Create(ARootRegistry, AInitialScope, MacroEvaluator) as IAstMacroExpander;
    Result := expander.Execute(RootNode);
end;

function TMacroExpander.Execute(const RootNode: IAstNode): IAstNode;
begin
    Result := Accept(RootNode);
    if not Assigned(Result) then
        Result := TAst.Block([], nil);
end;

function TMacroExpander.VisitBlockExpression(const Node: IBlockExpressionNode): IAstNode;
begin
    EnterMacroScope;
    try
        Result := inherited VisitBlockExpression(Node);
    finally
        ExitMacroScope;
    end;
end;

function TMacroExpander.VisitLambdaExpression(const Node: ILambdaExpressionNode): IAstNode;
begin
    EnterMacroScope;
    try
        Result := inherited VisitLambdaExpression(Node);
    finally
        ExitMacroScope;
    end;
end;

function TMacroExpander.VisitMacroDefinition(const Node: IMacroDefinitionNode): IAstNode;
begin
    FCurrentMacroRegistry.Define(Node);
    Result := Node;
end;

function TMacroExpander.VisitFunctionCall(const Node: IFunctionCallNode): IAstNode;
var
    calleeIdentifier: IIdentifierNode;
    macroDef: IMacroDefinitionNode;
    i: Integer;
begin
    if Node.Callee.Kind <> akIdentifier then
        exit(inherited VisitFunctionCall(Node));

    calleeIdentifier := Node.Callee.AsIdentifier;
    macroDef := FCurrentMacroRegistry.Find(calleeIdentifier.Name);
    if macroDef = nil then
        exit(inherited VisitFunctionCall(Node));

    var expansionScope := TScope.CreateScope(FInitialScope, nil, nil);

    var params := macroDef.Parameters;
    if Node.Arguments.Count <> params.Count then
        raise EMacroException.CreateFmt('Macro %s expects %d arguments.', [calleeIdentifier.Name, params.Count]);

    for i := 0 to params.Count - 1 do
        expansionScope.Define(params[i].Name, TDataValue.FromIntf<IAstNode>(Node.Arguments[i]));

    var expandedBody := TExpansionVisitor.Expand(expansionScope, macroDef.Body.AsQuasiquote.Expression, FMacroEvaluator);

    // The MacroExpansionNode wraps the expansion, preserving the original call's identity (Source Location)
    // for better error reporting.
    var macroNode := TAst.MacroExpansionNode(Node.Identity, Node, expandedBody);

    Result := Self.Accept(macroNode);
end;

function TMacroExpander.VisitQuasiquote(const Node: IQuasiquoteNode): IAstNode;
begin
    Result := Node;
end;

function TMacroExpander.VisitUnquote(const Node: IUnquoteNode): IAstNode;
begin
    raise EMacroException.Create('Unquote (`~`) can only be used inside a quasiquote.');
end;

function TMacroExpander.VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): IAstNode;
begin
    raise EMacroException.Create('Unquote-splicing (`~@`) can only be used inside a quasiquote.');
end;

{ TMacroRegistry }

constructor TMacroRegistry.Create(AParent: IMacroRegistry);
begin
    inherited Create;
    FParent := AParent;
    FMacros := TDictionary<string, IMacroDefinitionNode>.Create;
end;

destructor TMacroRegistry.Destroy;
begin
    FMacros.Free;
    inherited Destroy;
end;

function TMacroRegistry.GetParent: IMacroRegistry;
begin
    Result := FParent;
end;

procedure TMacroRegistry.Define(const Node: IMacroDefinitionNode);
begin
    FMacros.AddOrSetValue(Node.Name.Name, Node);
end;

function TMacroRegistry.Find(const Name: string): IMacroDefinitionNode;
var
    current: IMacroRegistry;
begin
    current := Self;
    while Assigned(current) do
    begin
        if (current as TMacroRegistry).FMacros.TryGetValue(Name, Result) then
            exit;
        current := current.Parent;
    end;
    Result := nil;
end;

function TMacroRegistry.CreateChildRegistry: IMacroRegistry;
begin
    Result := TMacroRegistry.Create(Self);
end;

end.
