unit Myc.Ast.Script;

interface

uses
    System.SysUtils,
    Myc.Data.Value,
    Myc.Ast.Nodes,
    Myc.Ast.Visitor;

type
    // Exception for parsing errors containing location information
    EParserException = class(EAstException)
    strict private
        FLine: Integer;
        FCol: Integer;
    public
        constructor Create(const AMessage: string; ALine, ACol: Integer);
        property Line: Integer read FLine;
        property Col: Integer read FCol;
    end;

var
    // This BNF should always be kept up to date and valid.
    BNF: String =
        '''
        (* ---------------------------------------------------------------------- *)
        (* ---- Main Productions (Start Symbols)                             ---- *)
        (* ---------------------------------------------------------------------- *)
        program         ::= expression
        expression      ::= atom | list | record_literal | reader_macro

        (* ---------------------------------------------------------------------- *)
        (* ---- Atoms (Basic Values)                                         ---- *)
        (* ---------------------------------------------------------------------- *)
        atom            ::= number | string | identifier | keyword

        (* ---------------------------------------------------------------------- *)
        (* ---- Reader-Macros (Syntactic Sugar)                              ---- *)
        (* ---------------------------------------------------------------------- *)
        reader_macro    ::= "'"  expression      (* (quote ...) *)
                          | "`"  expression      (* (quasiquote ...) *)
                          | "~"  expression      (* (unquote ...) *)
                          | "~@" expression      (* (unquote-splicing ...) *)

        (* ---------------------------------------------------------------------- *)
        (* ---- Lists (S-Expressions)                                        ---- *)
        (* ---------------------------------------------------------------------- *)
        list            ::= "(" list_content ")"
        list_content    ::= special_form | function_call

        special_form    ::= "if" expression expression expression?
                          | "?" (expression expression)* expression  (* cond1 branch1 ... else *)
                          | "def" identifier expression?
                          | "defmacro" identifier parameter_list expression
                          | "assign" identifier expression
                          | "fn" parameter_list expression
                          | "do" expression*
                          | "recur" expression*
                          | "get" expression expression
                          | dot_identifier expression

        function_call   ::= expression expression*

        (* ---------------------------------------------------------------------- *)
        (* ---- Parameter Lists and Records                                  ---- *)
        (* ---------------------------------------------------------------------- *)
        parameter_list  ::= "[" identifier* "]"
        record_literal  ::= "{" (keyword expression)* "}"

        (* ---------------------------------------------------------------------- *)
        (* ---- Terminals (Lexer Tokens)                                     ---- *)
        (* ---------------------------------------------------------------------- *)
        number          ::= ["-"] digit+ ["." digit+]
        string          ::= '"' ( ? any char except \ or " ? | '\' ( '"' | '\' | 'n' | 'r' | 't' ) )* '"'
        keyword         ::= ":" identifier
        dot_identifier  ::= "." identifier
        identifier      ::= ? any sequence of chars not containing whitespace, '()[]{}'`~:; ?
        digit           ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
        ''';

type
    // Provides a high-level facade for parsing and printing the AST.
    TAstScript = record
    public
        class function Parse(const ASource: string): IAstNode; static;
        class function Print(const ANode: IAstNode): string; static;
    end;

implementation

uses
    System.Classes,
    System.Generics.Collections,
    System.Character,
    System.Math,
    Myc.Data.Scalar,
    Myc.Data.Keyword,
    Myc.Ast,
    Myc.Ast.Identities;

type
    // --- Internal Parser Implementation ---

    TTokenKind = (
        tkLeftParen, // (
        tkRightParen, // )
        tkLeftBracket, // [
        tkRightBracket, // ]
        tkLeftBrace, // {
        tkRightBrace, // }
        tkQuote, // '
        tkBacktick, // `
        tkTilde, // ~
        tkAt, // @
        tkIdentifier,
        tkKeyword,
        tkNumber,
        tkString,
        tkEOF,
        tkError
    );

    TTokenKindHelper = record helper for TTokenKind
        function ToString: String;
    end;

    TToken = record
        Kind: TTokenKind;
        Text: string;
        Line: Integer;
        Col: Integer;
        function GetLocation: ISourceLocation;
    end;

    TLexer = class
    private
        FSource: string;
        FCurrentPos: Integer;
        FLine: Integer;
        FLineStart: Integer;
        function Peek: Char;
        procedure Advance;
        function ReadNumber: string;
        function ReadString: string;
        function ReadIdentifier: string;
        function GetCurrentLine: Integer;
        function GetCurrentCol: Integer;
        procedure Error(const Msg: string);
    public
        constructor Create(const ASource: string);
        function GetNextToken: TToken;
        property CurrentLine: Integer read GetCurrentLine;
        property CurrentCol: Integer read GetCurrentCol;
    end;

    TExpr = record
        Token: TToken;
        Node: IAstNode;
        Params: TArray<IIdentifierNode>;
    end;

    TParser = class
    private
        FLexer: TLexer;
        FCurrentToken: TToken;
        procedure Error(const Msg: string);
        procedure ErrorFmt(const Msg: string; const Args: array of const);
        procedure Consume(AExpectedKind: TTokenKind);
        procedure NextToken;
        function ParseList: IAstNode;
        function ParseRecordLiteral: IAstNode;
        function ParseParameterList: TArray<IIdentifierNode>;
        function ParseExpression: TExpr;
    public
        constructor Create(const ASource: string);
        destructor Destroy; override;
        function Parse: IAstNode;
    end;

    // --- Internal Printer Implementation ---

    TPrettyPrintVisitor = class(TAstVisitor)
    private
        FBuilder: TStringBuilder;
        FIndentLevel: Integer;
        procedure Indent;
        procedure Unindent;
        procedure Append(const S: string);
        procedure NewLine;
    public
        constructor Create;
        destructor Destroy; override;
        function GetResult: string;
        function Execute(const RootNode: IAstNode): TDataValue;

        function VisitConstant(const Node: IConstantNode): TVoid; override;
        function VisitIdentifier(const Node: IIdentifierNode): TVoid; override;
        function VisitKeyword(const Node: IKeywordNode): TVoid; override;

        // List Visitors
        function VisitParameterList(const Node: IParameterList): TVoid; override;
        function VisitArgumentList(const Node: IArgumentList): TVoid; override;
        function VisitExpressionList(const Node: IExpressionList): TVoid; override;
        function VisitRecordFieldList(const Node: IRecordFieldList): TVoid; override;
        function VisitRecordField(const Node: IRecordFieldNode): TVoid; override;

        function VisitIfExpression(const Node: IIfExpressionNode): TVoid; override;
        function VisitCondExpression(const Node: ICondExpressionNode): TVoid; override;
        function VisitLambdaExpression(const Node: ILambdaExpressionNode): TVoid; override;
        function VisitMacroDefinition(const Node: IMacroDefinitionNode): TVoid; override;
        function VisitQuasiquote(const Node: IQuasiquoteNode): TVoid; override;
        function VisitUnquote(const Node: IUnquoteNode): TVoid; override;
        function VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): TVoid; override;
        function VisitFunctionCall(const Node: IFunctionCallNode): TVoid; override;
        function VisitMacroExpansionNode(const Node: IMacroExpansionNode): TVoid; override;
        function VisitBlockExpression(const Node: IBlockExpressionNode): TVoid; override;
        function VisitVariableDeclaration(const Node: IVariableDeclarationNode): TVoid; override;
        function VisitAssignment(const Node: IAssignmentNode): TVoid; override;
        function VisitIndexer(const Node: IIndexerNode): TVoid; override;
        function VisitMemberAccess(const Node: IMemberAccessNode): TVoid; override;
        function VisitRecordLiteral(const Node: IRecordLiteralNode): TVoid; override;
        function VisitCreateSeries(const Node: ICreateSeriesNode): TVoid; override;
        function VisitAddSeriesItem(const Node: IAddSeriesItemNode): TVoid; override;
        function VisitSeriesLength(const Node: ISeriesLengthNode): TVoid; override;
        function VisitRecurNode(const Node: IRecurNode): TVoid; override;
        function VisitNop(const Node: INopNode): TVoid; override;
    end;

function TTokenKindHelper.ToString: String;
begin
    case Self of
        tkLeftParen: Result := '(';
        tkRightParen: Result := ')';
        tkLeftBracket: Result := '[';
        tkRightBracket: Result := ']';
        tkLeftBrace: Result := '{';
        tkRightBrace: Result := '}';
        tkQuote: Result := '''';
        tkBacktick: Result := '`';
        tkTilde: Result := '~';
        tkAt: Result := '@';
        tkIdentifier: Result := '<identifier>';
        tkKeyword: Result := '<keyword>';
        tkNumber: Result := '<number>';
        tkString: Result := '<string>';
        tkEOF: Result := '<EOF>';
    else
        Result := '<error>';
    end;
end;

function TToken.GetLocation: ISourceLocation;
begin
    Result := TIdentities.Location(Line, Col);
end;

{ EParserException }

constructor EParserException.Create(const AMessage: string; ALine, ACol: Integer);
begin
    inherited CreateFmt('[Line %d, Col %d] %s', [ALine, ACol, AMessage]);
    FLine := ALine;
    FCol := ACol;
end;

{ TLexer }

constructor TLexer.Create(const ASource: string);
begin
    inherited Create;
    FSource := ASource;
    FCurrentPos := 1;
    FLine := 1;
    FLineStart := 1;
end;

function TLexer.GetCurrentLine: Integer;
begin
    Result := FLine;
end;

function TLexer.GetCurrentCol: Integer;
begin
    Result := FCurrentPos - FLineStart + 1;
end;

procedure TLexer.Error(const Msg: string);
begin
    raise EParserException.Create(Msg, FLine, GetCurrentCol);
end;

function TLexer.Peek: Char;
begin
    if FCurrentPos > Length(FSource) then
        Result := #0
    else
        Result := FSource[FCurrentPos];
end;

procedure TLexer.Advance;
var
    c: Char;
begin
    if FCurrentPos <= Length(FSource) then
    begin
        c := FSource[FCurrentPos];
        if c = #10 then
        begin
            Inc(FLine);
            FLineStart := FCurrentPos + 1;
        end;
    end;
    Inc(FCurrentPos);
end;

function TLexer.ReadIdentifier: string;
var
    startPos: Integer;
begin
    startPos := FCurrentPos;
    while (Peek <> #0) and (not (Peek.IsWhiteSpace or CharInSet(Peek, ['(', ')', '[', ']', '{', '}', '''', '`', '~', ';']))) do
        Advance;
    Result := Copy(FSource, startPos, FCurrentPos - startPos);
end;

function TLexer.ReadNumber: string;
var
    startPos: Integer;
begin
    startPos := FCurrentPos;
    if (Peek = '-') then
        Advance;

    while (Peek <> #0) and (Peek.IsDigit or (Peek = '.')) do
        Advance;
    Result := Copy(FSource, startPos, FCurrentPos - startPos);
end;

function TLexer.ReadString: string;
var
    builder: TStringBuilder;
    c: Char;
begin
    Advance; // Skip opening "
    builder := TStringBuilder.Create;
    try
        while (FCurrentPos <= Length(FSource)) do
        begin
            c := Peek;
            if c = '"' then
            begin
                Advance; // Consume closing "
                Result := builder.ToString;
                exit;
            end;

            if c = '\' then
            begin
                Advance; // Consume '\'
                if FCurrentPos > Length(FSource) then
                    Error('Syntax Error: String literal ends with an escape character.');

                c := Peek; // Get escaped char
                case c of
                    '"': builder.Append('"');
                    '\': builder.Append('\');
                    'n': builder.Append(sLineBreak);
                    't': builder.Append(#9);
                    'r': builder.Append(#13);
                else
                    builder.Append(c);
                end;
                Advance; // Consume escaped char
            end
            else
            begin
                if c = #0 then
                    break;
                builder.Append(c);
                Advance;
            end;
        end;
        Error('Syntax Error: Unterminated string literal.');
    finally
        builder.Free;
    end;
end;

function TLexer.GetNextToken: TToken;
begin
    while FCurrentPos <= Length(FSource) do
    begin
        if FSource[FCurrentPos].IsWhiteSpace then
        begin
            Advance;
            continue;
        end;

        if FSource[FCurrentPos] = ';' then
        begin
            while (FCurrentPos <= Length(FSource)) and (not CharInSet(FSource[FCurrentPos], [#10, #13])) do
                Advance;
            continue;
        end;
        break;
    end;

    Result.Line := CurrentLine;
    Result.Col := CurrentCol;

    if FCurrentPos > Length(FSource) then
    begin
        Result.Kind := tkEOF;
        exit;
    end;

    var c := Peek;
    case c of
        '(':
        begin
            Result.Kind := tkLeftParen;
            Advance;
        end;
        ')':
        begin
            Result.Kind := tkRightParen;
            Advance;
        end;
        '[':
        begin
            Result.Kind := tkLeftBracket;
            Advance;
        end;
        ']':
        begin
            Result.Kind := tkRightBracket;
            Advance;
        end;
        '{':
        begin
            Result.Kind := tkLeftBrace;
            Advance;
        end;
        '}':
        begin
            Result.Kind := tkRightBrace;
            Advance;
        end;
        '''':
        begin
            Result.Kind := tkQuote;
            Advance;
        end;
        '`':
        begin
            Result.Kind := tkBacktick;
            Advance;
        end;
        '~':
        begin
            Result.Kind := tkTilde;
            Advance;
        end;
        '@':
        begin
            Result.Kind := tkAt;
            Advance;
        end;
        '"':
        begin
            Result.Kind := tkString;
            Result.Text := ReadString;
        end;
        ':':
        begin
            Advance;
            Result.Kind := tkKeyword;
            Result.Text := ReadIdentifier;
            if Result.Text.IsEmpty then
                Result.Kind := tkError;
        end;
    else
        if c.IsDigit or ((c = '-') and (FCurrentPos < Length(FSource)) and FSource[FCurrentPos + 1].IsDigit) then
        begin
            Result.Kind := tkNumber;
            Result.Text := ReadNumber;
        end
        else
        begin
            Result.Kind := tkIdentifier;
            Result.Text := ReadIdentifier;
        end;
    end;
end;

// -----------------------------------------------------------------------------
// IMPLEMENTATION: Parser
// -----------------------------------------------------------------------------

constructor TParser.Create(const ASource: string);
begin
    inherited Create;
    FLexer := TLexer.Create(ASource);
    NextToken;
end;

destructor TParser.Destroy;
begin
    FLexer.Free;
    inherited;
end;

procedure TParser.Error(const Msg: string);
begin
    raise EParserException.Create(Msg, FCurrentToken.Line, FCurrentToken.Col);
end;

procedure TParser.ErrorFmt(const Msg: string; const Args: array of const);
begin
    raise EParserException.Create(Format(Msg, Args), FCurrentToken.Line, FCurrentToken.Col);
end;

procedure TParser.NextToken;
begin
    FCurrentToken := FLexer.GetNextToken;
end;

procedure TParser.Consume(AExpectedKind: TTokenKind);
begin
    if FCurrentToken.Kind <> AExpectedKind then
        ErrorFmt('Syntax Error: Expected token %s, but found %s', [AExpectedKind.ToString, FCurrentToken.Kind.ToString]);
    NextToken;
end;

function TParser.ParseParameterList: TArray<IIdentifierNode>;
var
    params: TList<IIdentifierNode>;
begin
    Consume(tkLeftBracket);
    params := TList<IIdentifierNode>.Create;
    try
        while FCurrentToken.Kind <> tkRightBracket do
        begin
            if FCurrentToken.Kind = tkEOF then
                Error('Syntax Error: Unexpected end of file.');
            if FCurrentToken.Kind <> tkIdentifier then
                Error('Syntax Error: Expected identifier in parameter list.');

            params.Add(TAst.Identifier(FCurrentToken.Text, FCurrentToken.GetLocation));
            NextToken;
        end;
        Result := params.ToArray;
    finally
        params.Free;
    end;
    Consume(tkRightBracket);
end;

function TParser.ParseRecordLiteral: IAstNode;
var
    fields: TList<IRecordFieldNode>;
    fieldName: string;
    fieldValue: IAstNode;
    keyToken: TToken;
    keyNode: IKeywordNode;
begin
    var startLoc := FCurrentToken.GetLocation;
    Consume(tkLeftBrace);
    fields := TList<IRecordFieldNode>.Create;
    try
        while FCurrentToken.Kind <> tkRightBrace do
        begin
            if FCurrentToken.Kind = tkEOF then
                Error('Syntax Error: Unexpected end of file in record literal.');

            if FCurrentToken.Kind <> tkKeyword then
                Error('Syntax Error: Expected keyword (e.g., :key) as field name in record literal.');

            keyToken := FCurrentToken;
            fieldName := FCurrentToken.Text;
            keyNode := TAst.Keyword(fieldName, keyToken.GetLocation);
            NextToken;

            if FCurrentToken.Kind = tkRightBrace then
                Error('Syntax Error: Missing value for key ' + fieldName + ' in record literal.');

            fieldValue := ParseExpression.Node;

            fields.Add(TAst.RecordField(keyNode, fieldValue, keyToken.GetLocation));
        end;

        Result := TAst.RecordLiteral(fields.ToArray, startLoc);
    finally
        fields.Free;
    end;
    Consume(tkRightBrace);
end;

function TParser.ParseList: IAstNode;
var
    elements: TList<TExpr>;
    head: TExpr;
    tailNodes: TArray<IAstNode>;
    initializer: IAstNode;
    startLoc: ISourceLocation;
begin
    startLoc := FCurrentToken.GetLocation;
    Consume(tkLeftParen);
    if FCurrentToken.Kind = tkRightParen then
        Error('Syntax Error: Empty list () is not a valid expression.');

    elements := TList<TExpr>.Create;
    try
        while FCurrentToken.Kind <> tkRightParen do
        begin
            if FCurrentToken.Kind = tkEOF then
                Error('Syntax Error: Unexpected end of file.');
            elements.Add(ParseExpression);
        end;

        head := elements[0];

        SetLength(tailNodes, elements.Count - 1);
        for var i := 0 to High(tailNodes) do
            tailNodes[i] := elements[i + 1].Node;

        if head.Token.Kind = tkIdentifier then
        begin
            if SameText(head.Token.Text, 'if') then
            begin
                if not (Length(tailNodes) in [2, 3]) then
                    Error('Syntax Error in if statement.');
                var elseBranch: IAstNode := nil;
                if Length(tailNodes) = 3 then
                    elseBranch := tailNodes[2];
                Result := TAst.IfExpr(tailNodes[0], tailNodes[1], elseBranch, startLoc);
            end
            else if SameText(head.Token.Text, '?') then
            begin
                // (? test1 branch1 test2 branch2 ... else)
                // Requires odd number of tailNodes (pairs + 1 else)
                var count := Length(tailNodes);
                if (count < 1) or ((count mod 2) = 0) then
                    Error('Syntax Error: ''?'' (cond) requires an odd number of arguments (pairs of condition/branch and one final else).');

                var pairs: TArray<TCondPair>;
                SetLength(pairs, count div 2);
                for var i := 0 to High(pairs) do
                    pairs[i] := TCondPair.Create(tailNodes[i * 2], tailNodes[i * 2 + 1]);

                var elseNode := tailNodes[High(tailNodes)];
                Result := TAst.CondExpr(pairs, elseNode, startLoc);
            end
            else if SameText(head.Token.Text, 'def') then
            begin
                if not (Length(tailNodes) in [1, 2]) then
                    Error('Syntax Error: ''def'' requires a target and an optional initializer.');
                initializer := nil;
                if Length(tailNodes) = 2 then
                    initializer := tailNodes[1];
                Result := TAst.VarDecl(tailNodes[0], initializer, startLoc);
            end
            else if SameText(head.Token.Text, 'defmacro') then
            begin
                if (Length(tailNodes) <> 3) or (elements[1].Token.Kind <> tkIdentifier) then
                    Error('Syntax Error: ''defmacro'' requires a name, a parameter list, and a body.');

                var macroName := tailNodes[0].AsIdentifier;
                var macroParams := elements[2].Params;

                if tailNodes[2].Kind <> akQuasiquote then
                    Error('Syntax Error: Expected a quasiquote as macro body.');

                Result := TAst.MacroDef(macroName, macroParams, tailNodes[2], startLoc);
            end
            else if SameText(head.Token.Text, 'assign') then
            begin
                if Length(tailNodes) <> 2 then
                    Error('Syntax Error: ''assign'' requires exactly 2 arguments (target and value).');
                Result := TAst.Assign(tailNodes[0], tailNodes[1], startLoc);
            end
            else if SameText(head.Token.Text, 'fn') then
            begin
                if Length(tailNodes) <> 2 then
                    Error('Syntax Error: ''fn'' requires a parameter list and a body.');
                Result := TAst.LambdaExpr(elements[1].Params, tailNodes[1], startLoc);
            end
            else if SameText(head.Token.Text, 'do') then
                Result := TAst.Block(tailNodes, startLoc)
            else if SameText(head.Token.Text, 'recur') then
                Result := TAst.Recur(tailNodes, startLoc)
            else if SameText(head.Token.Text, 'get') then
            begin
                if Length(tailNodes) <> 2 then
                    Error('Syntax Error: ''get'' requires exactly 2 arguments (base and index).');
                Result := TAst.Indexer(tailNodes[0], tailNodes[1], startLoc)
            end
            else if (Length(head.Token.Text) > 1) and (head.Token.Text.StartsWith('.')) then
            begin
                if Length(tailNodes) <> 1 then
                    ErrorFmt(
                        'Syntax Error: Member access (e.g., ''%s'') requires exactly 1 argument (the base object).',
                        [head.Token.Text]
                    );
                Result := TAst.MemberAccess(tailNodes[0], TAst.Keyword(head.Token.Text.Substring(1), head.Token.GetLocation), startLoc);
            end;
        end;

        if Result = nil then
            Result := TAst.FunctionCall(head.Node, tailNodes, startLoc);

    finally
        elements.Free;
    end;

    Consume(tkRightParen);
end;

function TParser.ParseExpression: TExpr;
var
    i64: Int64;
    dbl: Double;
    expr: TExpr;
    startLoc: ISourceLocation;
begin
    Result.Token := FCurrentToken;
    startLoc := FCurrentToken.GetLocation;

    case FCurrentToken.Kind of
        tkBacktick:
        begin
            NextToken;
            expr := ParseExpression;
            Result.Node := TAst.Quasiquote(expr.Node, startLoc);
            exit;
        end;
        tkTilde:
        begin
            NextToken;
            if FCurrentToken.Kind = tkAt then
            begin
                NextToken;
                expr := ParseExpression;
                Result.Node := TAst.UnquoteSplicing(expr.Node.AsQuasiquote, startLoc);
            end
            else
            begin
                expr := ParseExpression;
                Result.Node := TAst.Unquote(expr.Node, startLoc);
            end;
            exit;
        end;
        tkQuote:
        begin
            NextToken;
            expr := ParseExpression;
            Result.Node := TAst.FunctionCall(TAst.Identifier('quote', startLoc), [expr.Node], startLoc);
            exit;
        end;
    end;

    Result.Token := FCurrentToken;
    case FCurrentToken.Kind of
        tkNumber:
        begin
            if TryStrToInt64(FCurrentToken.Text, i64) then
                Result.Node := TAst.Constant(i64, startLoc)
            else if TryStrToFloat(FCurrentToken.Text, dbl, TFormatSettings.Invariant) then
                Result.Node := TAst.Constant(dbl, startLoc)
            else
                ErrorFmt('Syntax Error: Invalid number format "%s"', [FCurrentToken.Text]);
            NextToken;
        end;
        tkString:
        begin
            Result.Node := TAst.Constant(FCurrentToken.Text, startLoc);
            NextToken;
        end;
        tkKeyword:
        begin
            Result.Node := TAst.Keyword(FCurrentToken.Text, startLoc);
            NextToken;
        end;
        tkIdentifier:
        begin
            if FCurrentToken.Text = '...' then
                Result.Node := TAst.Nop(startLoc)
            else
                Result.Node := TAst.Identifier(FCurrentToken.Text, startLoc);
            NextToken;
        end;
        tkLeftParen: Result.Node := ParseList;
        tkLeftBracket: Result.Params := ParseParameterList;
        tkLeftBrace: Result.Node := ParseRecordLiteral;
        tkEOF: {nop};
    else
        ErrorFmt('Syntax Error: Unexpected token %s', [FCurrentToken.Kind.ToString]);
    end;
end;

function TParser.Parse: IAstNode;
begin
    var expr := ParseExpression;
    if FCurrentToken.Kind <> tkEOF then
        Error('Syntax Error: Unexpected characters after end of expression.');
    Result := expr.Node;
end;

{ TPrettyPrintVisitor }

constructor TPrettyPrintVisitor.Create;
begin
    inherited Create;
    FBuilder := TStringBuilder.Create;
    FIndentLevel := 0;
end;

destructor TPrettyPrintVisitor.Destroy;
begin
    FBuilder.Free;
    inherited Destroy;
end;

function TPrettyPrintVisitor.GetResult: string;
begin
    Result := FBuilder.ToString;
end;

procedure TPrettyPrintVisitor.Indent;
begin
    inc(FIndentLevel, 2);
end;

procedure TPrettyPrintVisitor.Unindent;
begin
    dec(FIndentLevel, 2);
end;

procedure TPrettyPrintVisitor.Append(const S: string);
begin
    FBuilder.Append(S);
end;

procedure TPrettyPrintVisitor.NewLine;
begin
    FBuilder.AppendLine;
    FBuilder.Append(''.PadLeft(FIndentLevel));
end;

function TPrettyPrintVisitor.Execute(const RootNode: IAstNode): TDataValue;
begin
    if Assigned(RootNode) then
        RootNode.Accept(Self);
    Result := TDataValue.Void;
end;

function TPrettyPrintVisitor.VisitConstant(const Node: IConstantNode): TVoid;
var
    val: TDataValue;
begin
    val := Node.Value;
    if val.Kind = vkText then
        Append('"' + val.AsText + '"')
    else
        Append(val.ToString);
end;

function TPrettyPrintVisitor.VisitIdentifier(const Node: IIdentifierNode): TVoid;
begin
    Append(Node.Name);
end;

function TPrettyPrintVisitor.VisitKeyword(const Node: IKeywordNode): TVoid;
begin
    Append(':' + Node.Value.Name);
end;

// --- List Visitors Implementation ---

function TPrettyPrintVisitor.VisitParameterList(const Node: IParameterList): TVoid;
begin
    Append('[');
    for var i := 0 to Node.Count - 1 do
    begin
        if i > 0 then
            Append(' ');
        Node[i].Accept(Self);
    end;
    Append(']');
end;

function TPrettyPrintVisitor.VisitArgumentList(const Node: IArgumentList): TVoid;
begin
    // Argument list does not have brackets of its own in Lisp call syntax
    for var i := 0 to Node.Count - 1 do
    begin
        Append(' '); // Space before each argument
        Node[i].Accept(Self);
    end;
end;

function TPrettyPrintVisitor.VisitExpressionList(const Node: IExpressionList): TVoid;
begin
    Indent;
    for var item in Node do
    begin
        NewLine;
        item.Accept(Self);
    end;
    Unindent;
    NewLine;
end;

function TPrettyPrintVisitor.VisitRecordFieldList(const Node: IRecordFieldList): TVoid;
begin
    Indent;
    for var item in Node do
    begin
        NewLine;
        item.Accept(Self);
    end;
    Unindent;
    NewLine;
end;

function TPrettyPrintVisitor.VisitRecordField(const Node: IRecordFieldNode): TVoid;
begin
    Node.Key.Accept(Self);
    Append(' ');
    Node.Value.Accept(Self);
end;

// --- Node Visitors ---

function TPrettyPrintVisitor.VisitIfExpression(const Node: IIfExpressionNode): TVoid;
begin
    Append('(if ');
    Node.Condition.Accept(Self);
    Indent;
    NewLine;
    Node.ThenBranch.Accept(Self);
    if Assigned(Node.ElseBranch) then
    begin
        NewLine;
        Node.ElseBranch.Accept(Self);
    end;
    Unindent;
    NewLine;
    Append(')');
end;

function TPrettyPrintVisitor.VisitCondExpression(const Node: ICondExpressionNode): TVoid;
begin
    Append('(?');
    Indent;
    for var pair in Node.Pairs do
    begin
        NewLine;
        pair.Condition.Accept(Self);
        Append(' ');
        pair.Branch.Accept(Self);
    end;
    // Else
    NewLine;
    Node.ElseBranch.Accept(Self);
    Unindent;
    NewLine;
    Append(')');
end;

function TPrettyPrintVisitor.VisitLambdaExpression(const Node: ILambdaExpressionNode): TVoid;
begin
    Append('(fn ');
    Node.Parameters.Accept(Self);
    Indent;
    NewLine;
    Node.Body.Accept(Self);
    Unindent;
    NewLine;
    Append(')');
end;

function TPrettyPrintVisitor.VisitMacroDefinition(const Node: IMacroDefinitionNode): TVoid;
begin
    Append('(defmacro ');
    Node.Name.Accept(Self);
    Append(' ');
    Node.Parameters.Accept(Self);
    Indent;
    NewLine;
    Node.Body.Accept(Self);
    Unindent;
    NewLine;
    Append(')');
end;

function TPrettyPrintVisitor.VisitQuasiquote(const Node: IQuasiquoteNode): TVoid;
begin
    Append('`');
    Node.Expression.Accept(Self);
end;

function TPrettyPrintVisitor.VisitUnquote(const Node: IUnquoteNode): TVoid;
begin
    Append('~');
    Node.Expression.Accept(Self);
end;

function TPrettyPrintVisitor.VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): TVoid;
begin
    Append('~@');
    Node.Expression.Accept(Self);
end;

function TPrettyPrintVisitor.VisitFunctionCall(const Node: IFunctionCallNode): TVoid;
begin
    if (Node.Callee.Kind = akIdentifier) and (Node.Callee.AsIdentifier.Name = 'quote') and (Node.Arguments.Count = 1) then
    begin
        Append('''');
        Node.Arguments[0].Accept(Self);
        exit;
    end;

    Append('(');
    Node.Callee.Accept(Self);
    Node.Arguments.Accept(Self); // Prints separated by space
    Append(')');
end;

function TPrettyPrintVisitor.VisitMacroExpansionNode(const Node: IMacroExpansionNode): TVoid;
begin
    VisitFunctionCall(Node.CallNode);
end;

function TPrettyPrintVisitor.VisitRecurNode(const Node: IRecurNode): TVoid;
begin
    Append('(recur');
    Node.Arguments.Accept(Self);
    Append(')');
end;

function TPrettyPrintVisitor.VisitBlockExpression(const Node: IBlockExpressionNode): TVoid;
begin
    Append('(do');
    Node.Expressions.Accept(Self);
    Append(')');
end;

function TPrettyPrintVisitor.VisitVariableDeclaration(const Node: IVariableDeclarationNode): TVoid;
begin
    Append('(def ');
    Node.Target.Accept(Self);
    if Assigned(Node.Initializer) then
    begin
        Append(' ');
        Node.Initializer.Accept(Self);
    end;
    Append(')');
end;

function TPrettyPrintVisitor.VisitAssignment(const Node: IAssignmentNode): TVoid;
begin
    Append('(assign ');
    Node.Target.Accept(Self);
    Append(' ');
    Node.Value.Accept(Self);
    Append(')');
end;

function TPrettyPrintVisitor.VisitIndexer(const Node: IIndexerNode): TVoid;
begin
    Append('(get ');
    Node.Base.Accept(Self);
    Append(' ');
    Node.Index.Accept(Self);
    Append(')');
end;

function TPrettyPrintVisitor.VisitMemberAccess(const Node: IMemberAccessNode): TVoid;
begin
    Append('(.');
    Node.Member.Accept(Self);
    Append(' ');
    Node.Base.Accept(Self);
    Append(')');
end;

function TPrettyPrintVisitor.VisitRecordLiteral(const Node: IRecordLiteralNode): TVoid;
begin
    if Node.Fields.Count = 0 then
    begin
        Append('{}');
        exit;
    end;

    Append('{');
    Node.Fields.Accept(Self);
    Append('}');
end;

function TPrettyPrintVisitor.VisitCreateSeries(const Node: ICreateSeriesNode): TVoid;
begin
    Append(Format('(new-series "%s")', [Node.Definition]));
end;

function TPrettyPrintVisitor.VisitAddSeriesItem(const Node: IAddSeriesItemNode): TVoid;
begin
    Append('(add-item ');
    Node.Series.Accept(Self);
    Append(' ');
    Node.Value.Accept(Self);
    if Assigned(Node.Lookback) then
    begin
        Append(' ');
        Node.Lookback.Accept(Self);
    end;
    Append(')');
end;

function TPrettyPrintVisitor.VisitNop(const Node: INopNode): TVoid;
begin
    Append('...');
end;

function TPrettyPrintVisitor.VisitSeriesLength(const Node: ISeriesLengthNode): TVoid;
begin
    Append('(count ');
    Node.Series.Accept(Self);
    Append(')');
end;

{ TAstScript }

class function TAstScript.Parse(const ASource: string): IAstNode;
var
    p: TParser;
begin
    if ASource.Trim.IsEmpty then
        exit(nil);
    p := TParser.Create(ASource);
    try
        Result := p.Parse;
    finally
        p.Free;
    end;
end;

class function TAstScript.Print(const ANode: IAstNode): string;
var
    visitor: TPrettyPrintVisitor;
begin
    if not Assigned(ANode) then
        exit('');
    visitor := TPrettyPrintVisitor.Create;
    try
        visitor.Execute(ANode);
        Result := visitor.GetResult;
    finally
        visitor.Free;
    end;
end;

end.
