package de.dhbwstuttgart.target.generate;

import de.dhbwstuttgart.exceptions.NotImplementedException;
import de.dhbwstuttgart.parser.SyntaxTreeGenerator.AssignToLocal;
import de.dhbwstuttgart.parser.scope.JavaClassName;
import de.dhbwstuttgart.syntaxtree.*;
import de.dhbwstuttgart.syntaxtree.factory.PrimitiveMethodsGenerator;
import de.dhbwstuttgart.syntaxtree.statement.*;
import de.dhbwstuttgart.syntaxtree.type.*;
import de.dhbwstuttgart.target.tree.MethodParameter;
import de.dhbwstuttgart.target.tree.expression.*;
import de.dhbwstuttgart.target.tree.type.*;

import javax.swing.text.html.Option;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public class StatementToTargetExpression implements ASTVisitor {

    public StatementToTargetExpression(ASTToTargetAST converter) {
        this.converter = converter;
    }

    public TargetExpression result;
    private final ASTToTargetAST converter;

    @Override
    public void visit(ArgumentList argumentList) {
        throw new NotImplementedException();
    }

    @Override
    public void visit(LambdaExpression lambdaExpression) {
        var parameters = StreamSupport.stream(lambdaExpression.params.spliterator(), false)
                .map(p -> (FormalParameter) p)
                .map(p -> new MethodParameter(new TargetTypePattern(converter.convert(p.getType()), p.getName())))
                .toList();

        List<MethodParameter> captures = new ArrayList<>();
        lambdaExpression.methodBody.accept(new TracingStatementVisitor() {
            // TODO The same mechanism is implemented in Codegen, maybe use it from there?
            final Stack<Set<String>> localVariables = new Stack<>();
            {
                localVariables.push(new HashSet<>());
            }

            boolean hasLocalVar(String name) {
                for (var localVariables : this.localVariables) {
                    if (localVariables.contains(name))
                        return true;
                }
                return false;
            }

            @Override
            public void visit(Block block) {
                localVariables.push(new HashSet<>());
                super.visit(block);
                localVariables.pop();
            }

            @Override
            public void visit(LocalVar localVar) {
                super.visit(localVar);
                var capture = new MethodParameter(new TargetTypePattern(converter.convert(localVar.getType()), localVar.name));
                if (!hasLocalVar(localVar.name) && !parameters.contains(capture) && !captures.contains(capture))
                    captures.add(capture);
            }

            @Override
            public void visit(LocalVarDecl varDecl) {
                var localVariables = this.localVariables.peek();
                localVariables.add(varDecl.getName());
            }

            @Override
            public void visit(LambdaExpression lambda) {
            } // Don't look at lambda expressions
        });

        result = new TargetLambdaExpression(converter.convert(lambdaExpression.getType()), captures, parameters, converter.convert(lambdaExpression.getReturnType()), converter.convert(lambdaExpression.methodBody));
    }

    @Override
    public void visit(Assign assign) {
        TargetExpression left;
        if (assign.lefSide instanceof AssignToLocal) {
            left = converter.convert(((AssignToLocal) assign.lefSide).localVar);
        } else {
            left = converter.convert(((AssignToField) assign.lefSide).field);
        }

        result = new TargetAssign(converter.convert(assign.getType()), left, converter.convert(assign.rightSide));
    }

    @Override
    public void visit(BinaryExpr binary) {
        result = switch (binary.operation) {
        case ADD -> new TargetBinaryOp.Add(converter.convert(binary.getType()), converter.convert(binary.lexpr), converter.convert(binary.rexpr));
        case SUB -> new TargetBinaryOp.Sub(converter.convert(binary.getType()), converter.convert(binary.lexpr), converter.convert(binary.rexpr));
        case MUL -> new TargetBinaryOp.Mul(converter.convert(binary.getType()), converter.convert(binary.lexpr), converter.convert(binary.rexpr));
        case MOD -> new TargetBinaryOp.Rem(converter.convert(binary.getType()), converter.convert(binary.lexpr), converter.convert(binary.rexpr));
        case AND -> new TargetBinaryOp.And(converter.convert(binary.getType()), converter.convert(binary.lexpr), converter.convert(binary.rexpr));
        case OR -> new TargetBinaryOp.Or(converter.convert(binary.getType()), converter.convert(binary.lexpr), converter.convert(binary.rexpr));
        case DIV -> new TargetBinaryOp.Div(converter.convert(binary.getType()), converter.convert(binary.lexpr), converter.convert(binary.rexpr));
        case LESSTHAN -> new TargetBinaryOp.Less(converter.convert(binary.getType()), converter.convert(binary.lexpr), converter.convert(binary.rexpr));
        case BIGGERTHAN -> new TargetBinaryOp.Greater(converter.convert(binary.getType()), converter.convert(binary.lexpr), converter.convert(binary.rexpr));
        case LESSEQUAL -> new TargetBinaryOp.LessOrEqual(converter.convert(binary.getType()), converter.convert(binary.lexpr), converter.convert(binary.rexpr));
        case BIGGEREQUAL -> new TargetBinaryOp.GreaterOrEqual(converter.convert(binary.getType()), converter.convert(binary.lexpr), converter.convert(binary.rexpr));
        case EQUAL -> new TargetBinaryOp.Equal(converter.convert(binary.getType()), converter.convert(binary.lexpr), converter.convert(binary.rexpr));
        case NOTEQUAL -> new TargetBinaryOp.NotEqual(converter.convert(binary.getType()), converter.convert(binary.lexpr), converter.convert(binary.rexpr));
        };
    }

    @Override
    public void visit(BoolExpression bool) {
        System.out.println("BoolExpression");
    }

    @Override
    public void visit(Block block) {
        result = converter.convert(block);
    }

    @Override
    public void visit(CastExpr castExpr) {
        result = new TargetCast(converter.convert(castExpr.getType()), converter.convert(castExpr.expr));
    }

    @Override
    public void visit(EmptyStmt emptyStmt) {
        result = null;
    }

    @Override
    public void visit(FieldVar fieldVar) {
        result = new TargetFieldVar(converter.convert(fieldVar.getType()), converter.convert(fieldVar.receiver.getType()), fieldVar.isStatic, converter.convert(fieldVar.receiver), fieldVar.fieldVarName);
    }

    @Override
    public void visit(ForStmt forStmt) {
        result = new TargetFor(
                forStmt.initializer.stream().map(converter::convert).toList(),
                forStmt.condition != null ? converter.convert(forStmt.condition) : null,
                forStmt.loopExpr.stream().map(converter::convert).toList(),
                converter.convertWrapInBlock(forStmt.block)
        );
    }

    @Override
    public void visit(ForEachStmt forEachStmt) {
        result = new TargetForEach(converter.convert(forEachStmt.statement), converter.convert(forEachStmt.expression), converter.convertWrapInBlock(forEachStmt.block));
    }

    @Override
    public void visit(IfStmt ifStmt) {
        result = new TargetIf(converter.convert(ifStmt.expr), converter.convertWrapInBlock(ifStmt.then_block), ifStmt.else_block != null ? converter.convertWrapInBlock(ifStmt.else_block) : null);
    }

    @Override
    public void visit(InstanceOf instanceOf) {
        result = new TargetInstanceOf(converter.convert(instanceOf.getExpression()), converter.convert(instanceOf.getPattern()));
    }

    @Override
    public void visit(LocalVar localVar) {
        result = new TargetLocalVar(converter.convert(localVar.getType()), localVar.name);
    }

    @Override
    public void visit(LocalVarDecl localVarDecl) {
        // TODO No value, is this correct?
        result = new TargetVarDecl(converter.convert(localVarDecl.getType()), localVarDecl.getName(), null);
    }

    static boolean convertsTo(TargetType from, TargetType to) {
        if (to.equals(TargetType.Object))
            return true; // TODO Consider type coercion and subtyping
        return to.equals(from);
    }

    Optional<Method> findMethod(JavaClassName className, String name, List<TargetType> args) {
        return converter.findMethod(converter.compiler.getClass(className), name, args);
    }

    @Override
    public void visit(MethodCall methodCall) {
        var receiverType = converter.convert(methodCall.receiver.getType());
        var isFunNType = receiverType instanceof TargetFunNType;

        var returnType = isFunNType ? TargetType.Object : converter.convert(methodCall.signature.get(methodCall.signature.size() - 1));
        var receiverName = new JavaClassName(converter.convert(methodCall.receiver.getType()).name());
        var argList = methodCall.signature.stream().map(converter::convert).toList();
        argList = argList.subList(0, argList.size() - 1);

        Method foundMethod = null;
        var isStatic = false;
        var isInterface = true;
        var signature = methodCall.signatureArguments().stream().map(converter::convert).toList();

        var receiverClass = converter.currentClass;
        if (methodCall.receiver instanceof ExpressionReceiver expressionReceiver && expressionReceiver.expr instanceof This) {
            var thisMethod = converter.findMethod(converter.currentClass, methodCall.name, signature);
            foundMethod = thisMethod.orElseGet(() -> findMethod(converter.currentClass.getSuperClass().getName(), methodCall.name, signature).orElseThrow());
        } else if (!isFunNType) {
            receiverClass = converter.compiler.getClass(receiverName);
            foundMethod = findMethod(receiverName, methodCall.name, signature).orElseThrow();
        }

        if (!isFunNType) {
            returnType = converter.convert(foundMethod.getReturnType());
            argList = foundMethod.getParameterList().getFormalparalist().stream().map(e -> converter.convert(e.getType())).toList();
            isStatic = Modifier.isStatic(foundMethod.modifier);
            isInterface = receiverClass.isInterface();
        }

        result = new TargetMethodCall(converter.convert(methodCall.getType()), returnType, argList, converter.convert(methodCall.receiver), methodCall.getArgumentList().getArguments().stream().map(converter::convert).toList(), receiverType, methodCall.name, isStatic, isInterface);
    }

    @Override
    public void visit(NewClass newClass) {
        result = new TargetNew(new TargetRefType(newClass.name), newClass.getArgumentList().getArguments().stream().map(converter::convert).toList());
    }

    @Override
    public void visit(NewArray newArray) {
        // TODO
        throw new NotImplementedException();
    }

    @Override
    public void visit(Return aReturn) {
        result = new TargetReturn(converter.convert(aReturn.retexpr));
    }

    @Override
    public void visit(ReturnVoid aReturn) {
        result = new TargetReturn(null);
    }

    @Override
    public void visit(Break aBreak) {
        result = new TargetBreak();
    }

    @Override
    public void visit(StaticClassName staticClassName) {
        result = new TargetClassName(converter.convert(staticClassName.getType()));
    }

    @Override
    public void visit(Super aSuper) {
        result = new TargetSuper(converter.convert(aSuper.getType()));
    }

    @Override
    public void visit(This aThis) {
        result = new TargetThis(converter.convert(aThis.getType()));
    }

    @Override
    public void visit(WhileStmt whileStmt) {
        result = new TargetWhile(converter.convert(whileStmt.expr), converter.convert(whileStmt.loopBlock));
    }

    @Override
    public void visit(DoStmt whileStmt) {
        throw new NotImplementedException();
    }

    // TODO These two might not be necessary
    @Override
    public void visit(AssignToField assignLeftSide) {
        result = converter.convert(assignLeftSide.field);
    }

    @Override
    public void visit(AssignToLocal assignLeftSide) {
        result = converter.convert(assignLeftSide.localVar);
    }

    @Override
    public void visit(SuperCall superCall) {
        var aSuper = converter.convert(converter.currentClass.getSuperClass());
        var type = converter.convert(superCall.getType());
        var parameters = superCall.arglist.getArguments().stream().map(par -> converter.convert(par.getType())).toList();

        result = new TargetMethodCall(type, type, parameters, new TargetSuper(aSuper), superCall.getArgumentList().getArguments().stream().map(converter::convert).toList(), aSuper, superCall.name, false, false);
    }

    @Override
    public void visit(ExpressionReceiver expressionReceiver) {
        result = converter.convert(expressionReceiver.expr);
    }

    @Override
    public void visit(UnaryExpr unaryExpr) {
        result = switch (unaryExpr.operation) {
        case NOT -> new TargetUnaryOp.Not(converter.convert(unaryExpr.getType()), converter.convert(unaryExpr.expr));
        case MINUS -> new TargetUnaryOp.Negate(converter.convert(unaryExpr.getType()), converter.convert(unaryExpr.expr));
        case PREINCREMENT -> new TargetUnaryOp.PreIncrement(converter.convert(unaryExpr.getType()), converter.convert(unaryExpr.expr));
        case PREDECREMENT -> new TargetUnaryOp.PreDecrement(converter.convert(unaryExpr.getType()), converter.convert(unaryExpr.expr));
        case POSTINCREMENT -> new TargetUnaryOp.PostIncrement(converter.convert(unaryExpr.getType()), converter.convert(unaryExpr.expr));
        case PLUS -> new TargetUnaryOp.Add(converter.convert(unaryExpr.getType()), converter.convert(unaryExpr.expr));
        case POSTDECREMENT -> new TargetUnaryOp.PostDecrement(converter.convert(unaryExpr.getType()), converter.convert(unaryExpr.expr));
        };
    }

    @Override
    public void visit(Literal literal) {
        if (literal.value instanceof Integer || literal.value instanceof Short || literal.value instanceof Byte) {

            result = new TargetLiteral.IntLiteral((int) literal.value);
        } else if (literal.value instanceof Float) {
            result = new TargetLiteral.FloatLiteral((float) literal.value);
        } else if (literal.value instanceof Double) {
            result = new TargetLiteral.DoubleLiteral((double) literal.value);
        } else if (literal.value instanceof Long) {
            result = new TargetLiteral.LongLiteral((long) literal.value);
        } else if (literal.value instanceof Character) {
            result = new TargetLiteral.CharLiteral((char) literal.value);
        } else if (literal.value instanceof String) {
            result = new TargetLiteral.StringLiteral((String) literal.value);
        } else if (literal.value instanceof Boolean) {
            result = new TargetLiteral.BooleanLiteral((boolean) literal.value);
        } else if (literal.value == null) {
            result = new TargetLiteral.Null();
        }
    }

    @Override
    public void visit(Throw aThrow) {
        result = new TargetThrow(converter.convert(aThrow.expr));
    }

    @Override
    public void visit(Switch switchStmt) {
        var cases = switchStmt.getBlocks().stream().filter(s -> !s.isDefault()).map(converter::convert).toList();

        TargetSwitch.Case default_ = null;
        for (var block : switchStmt.getBlocks()) {
            if (block.isDefault()) {
                default_ = new TargetSwitch.Case(converter.convert((Block) block), block.isExpression);
            }
        }
        result = new TargetSwitch(converter.convert(switchStmt.getSwitch()), cases, default_ , converter.convert(switchStmt.getType()), !switchStmt.getStatement());
    }

    @Override
    public void visit(SwitchBlock switchBlock) {}

    @Override
    public void visit(SwitchLabel switchLabel) {
        result = converter.convert(switchLabel.getPattern());
    }

    @Override
    public void visit(Yield aYield) {
        result = new TargetYield(converter.convert(aYield.retexpr));
    }

    @Override
    public void visit(SourceFile sourceFile) {

    }

    @Override
    public void visit(GenericTypeVar genericTypeVar) {

    }

    @Override
    public void visit(GenericDeclarationList genericTypeVars) {

    }

    @Override
    public void visit(Field field) {

    }

    @Override
    public void visit(de.dhbwstuttgart.syntaxtree.Method field) {

    }

    @Override
    public void visit(Constructor field) {

    }

    @Override
    public void visit(ParameterList formalParameters) {

    }

    @Override
    public void visit(ClassOrInterface classOrInterface) {

    }

    @Override
    public void visit(RefType refType) {

    }

    @Override
    public void visit(SuperWildcardType superWildcardType) {

    }

    @Override
    public void visit(TypePlaceholder typePlaceholder) {

    }

    @Override
    public void visit(ExtendsWildcardType extendsWildcardType) {

    }

    @Override
    public void visit(GenericRefType genericRefType) {

    }

    @Override
    public void visit(FormalParameter aPattern) {
        result = new TargetTypePattern(converter.convert(aPattern.getType()), aPattern.getName());
    }

    @Override
    public void visit(ExpressionPattern aPattern) {
        result = converter.convert(aPattern.getExpression());
    }

    @Override
    public void visit(RecordPattern aRecordPattern) {
        result = new TargetComplexPattern(
            converter.convert(aRecordPattern.getType()),
            aRecordPattern.getName(),
            aRecordPattern.getSubPattern().stream().map(x -> (TargetPattern) converter.convert(x)).toList()
        );
    }

    @Override
    public void visit(GuardedPattern aGuardedPattern) {
        result = new TargetGuard((TargetPattern) converter.convert(aGuardedPattern.getNestedPattern()), converter.convert(aGuardedPattern.getCondition()));
    }
}