package io.github.vmzakharov.ecdataframe.dsl.visitor;

import io.github.vmzakharov.ecdataframe.dsl.*;
import io.github.vmzakharov.ecdataframe.util.CollectingPrinter;
import io.github.vmzakharov.ecdataframe.util.Printer;
import io.github.vmzakharov.ecdataframe.util.PrinterFactory;
import org.eclipse.collections.api.list.ListIterable;
import org.eclipse.collections.impl.utility.StringIterate;

public class PrettyPrintVisitor
implements ExpressionVisitor
{
    final private Printer printer;

    private int offset = 0;
    private boolean startedNewLine = true;

    static public String exprToString(Expression e)
    {
        CollectingPrinter printer = new CollectingPrinter();
        e.accept(new PrettyPrintVisitor(printer));
        return printer.toString();
    }

    public PrettyPrintVisitor()
    {
        this(PrinterFactory.getPrinter());
    }

    public PrettyPrintVisitor(Printer newPrinter)
    {
        this.printer = newPrinter;
    }

    @Override
    public void visitAssignExpr(AssingExpr expr)
    {
        this.print(expr.getVarName()).print(" = ").printExpression(expr.getExpression());
    }

    @Override
    public void visitBinaryExpr(BinaryExpr expr)
    {
        this.print("(").printExpression(expr.getOperand1()).print(" ")
            .print(expr.getOperation().asString())
            .print(" ").printExpression(expr.getOperand2()).print(")");
    }

    @Override
    public void visitUnaryExpr(UnaryExpr expr)
    {
        if (expr.getOperation().isPrefix())
        {
            this.print(expr.getOperation().asString()).print("(").printExpression(expr.getOperand()).print(")");
        }
        else
        {
            this.print("(").printExpression(expr.getOperand()).print(") ").print(expr.getOperation().asString());
        }
    }

    @Override
    public void visitConstExpr(ConstExpr expr)
    {
        this.print(expr.getValue().asStringLiteral());
    }

    @Override
    public void visitFunctionCallExpr(FunctionCallExpr expr)
    {
        this.print(expr.getFunctionName()).print("(").printExpressionList(expr.getParameters()).print(")");
    }

    private PrettyPrintVisitor printExpressionListLn(ListIterable<Expression> expressions)
    {
        int count = expressions.size();
        for (int i = 0; i < count; i++)
        {
            expressions.get(i).accept(this);
            this.newLine();
        }

        return this;
    }

    private PrettyPrintVisitor printExpressionList(ListIterable<Expression> expressions, String separator)
    {
        int count = expressions.size();
        for (int i = 0; i < count; i++)
        {
            expressions.get(i).accept(this);
            if (i < count - 1)
            {
                this.print(separator);
            }
        }

        return this;
    }

    private PrettyPrintVisitor printExpressionList(ListIterable<Expression> expressions)
    {
        return this.printExpressionList(expressions, ", ");
    }

    private PrettyPrintVisitor printExpression(Expression expression)
    {
        expression.accept(this);
        return this;
    }

    @Override
    public void visitPropertyPathExpr(PropertyPathExpr expr)
    {
        this.print(expr.getPathElements().makeString("."));
    }

    @Override
    public void visitAnonymousScriptExpr(AnonymousScript expr)
    {
        expr.getFunctions().forEachValue(this::visitFunctionScriptExpr);
        this.printExpressionListLn(expr.getExpressions());;
    }

    @Override
    public void visitFunctionScriptExpr(FunctionScript expr)
    {
        this.print("function ").print(expr.getName());
        if (expr.getExpressions().notEmpty())
        {
            this.print("(").print(expr.getParameterNames().makeString()).print(")");
        }

        this.newLine()
            .print("{").newLine().tab()
            .printExpressionListLn(expr.getExpressions())
            .tabBack()
            .print("}").newLine();
    }

    @Override
    public void visitStatementSequenceScript(StatementSequenceScript expr)
    {
        this.printExpressionListLn(expr.getExpressions());
    }

    @Override
    public void visitVarExpr(VarExpr expr)
    {
        this.print(expr.getVariableName());
    }

    @Override
    public void visitProjectionExpr(ProjectionExpr expr)
    {
        this.print("project {")
            .printExpressionList(expr.getProjectionElements())
            .print("}");

        if (expr.getWhereClause() != null)
        {
            this.print(" where ").printExpression(expr.getWhereClause());
        }
    }

    @Override
    public void visitAliasExpr(AliasExpr expr)
    {
        this.print(expr.getAlias()).print(" : ").printExpression(expr.getExpression());
    }

    @Override
    public void visitVectorExpr(VectorExpr expr)
    {
        this.print("(").printExpressionList(expr.getElements()).print(")");
    }

    @Override
    public void visitIndexExpr(IndexExpr expr)
    {
        this.printExpression(expr.getVectorExpr()).print("[").printExpression(expr.getIndexExpr()).print("]");
    }

    @Override
    public void visitIfElseExpr(IfElseExpr expr)
    {
        if (expr.isTernary())
        {
            this
                .printExpression(expr.getCondition())
                .print(" ? ")
                .printExpression(expr.getIfScript())
                .print(" : ")
                .printExpression(expr.getElseScript());
        }
        else
        {
            this.print("if ").printExpression(expr.getCondition()).print(" then").newLine();
            this.tab();
            expr.getIfScript().accept(this);
            this.tabBack();

            if (expr.hasElseSection())
            {
                this.print("else").newLine();
                this.tab();
                expr.getElseScript().accept(this);
                this.tabBack();
            }

            this.print("endif");
        }
    }

    private PrettyPrintVisitor print(String s)
    {
        if (this.startedNewLine)
        {
            this.printer.print(StringIterate.repeat(' ', this.offset * 2));
            this.startedNewLine = false;
        }
        this.printer.print(s);
        return this;
    }

    private PrettyPrintVisitor tab()
    {
        this.offset++;
        return this;
    }

    private PrettyPrintVisitor tabBack()
    {
        this.offset--;
        return this;
    }

    private PrettyPrintVisitor newLine()
    {
        this.printer.newLine();
        this.startedNewLine = true;
        return this;
    }
}