package chocopy.pa2;

import chocopy.common.analysis.AbstractNodeAnalyzer;
import chocopy.common.analysis.SymbolTable;
import chocopy.common.analysis.types.ClassValueType;
import chocopy.common.analysis.types.FuncType;
import chocopy.common.analysis.types.SymbolType;
import chocopy.common.analysis.types.ValueType;
import chocopy.common.astnodes.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

/**
 * Analyzes declarations to create a top-level symbol table.
 * Recolic: This class will ONLY fill the symTable. Every `Program` and `FuncDef` has a sym table.
 */
public class DeclarationAnalyzer extends AbstractNodeAnalyzer<SymbolType> {

    /** Current symbol table.  Changes with new declarative region. */
    private SymbolTable<SymbolType> sym = new SymbolTable<>();
    /** Global symbol table. */
    private final SymbolTable<SymbolType> globals = sym;
    /** Receiver for semantic error messages. */
    private final Errors errors;

    /** A new declaration analyzer sending errors to ERRORS0. */
    public DeclarationAnalyzer(Errors errors0) {
        errors = errors0;
    }


    public SymbolTable<SymbolType> getGlobals() {
        return globals;
    }

    @Override
    public SymbolType analyze(Program program) {
        for (Declaration decl : program.declarations) {
            Identifier id = decl.getIdentifier();
            String name = id.name;

            SymbolType type = decl.dispatch(this);
            System.out.println("decl type " + type.toString());

            // recolic: what's the teacher willing to do???
            if (type == null) {
                continue;
            }

            // TODO: DO NOT throw on duplicate id. generate a compiler error.
            sym.put(name, type);
        }

        return null;
    }

    @Override
    public SymbolType analyze(VarDef varDef) {
        System.out.println("debug");
        return analyze(varDef.var);
    }

    @Override
    public SymbolType analyze(ClassDef classDef) {
        assert classDef.memberMap == null;
        classDef.memberMap = new HashMap<>();
        assert classDef.memberMap != null;

        ClassValueType result_type = new ClassValueType(classDef.name.name);
        SymbolTable<SymbolType> symLayer = new SymbolTable<>(sym);
        symLayer.put("self", result_type);
        sym = symLayer;

        for(Declaration decl : classDef.declarations) {
            String name = decl.getIdentifier().name;
            SymbolType type = decl.dispatch(this);
            classDef.memberMap.put(name, type);
        }

        sym = sym.getParent();
        return new ClassValueType(classDef.name.name);
    }

    @Override
    public SymbolType analyze(FuncDef funcDef) {
        System.out.println("debug: start funcDef: " + funcDef.getClass().getName());
        assert funcDef.symTable == null;
        sym = funcDef.symTable = new SymbolTable<SymbolType>(sym);
        assert funcDef.symTable != null; // may suck if partial-compiling happens...

        // Func parameter list
        List<ValueType> args = new ArrayList<>();
        for(TypedVar param : funcDef.params) {
            String name = param.identifier.name;
            ValueType type = ValueType.annotationToValueType(param.type);
            sym.put(name, type);
            args.add(type);
        }

        for(Declaration decl : funcDef.declarations) {
            String name = decl.getIdentifier().name;
            SymbolType type = decl.dispatch(this);
            sym.put(name, type);
        }

        // function scope ends.. backtracking...
        sym = sym.getParent();
        if(sym == null)
            throw new RuntimeException("logic error: sym parent is null while returning..");
        FuncType funcT = new FuncType(args, ValueType.annotationToValueType(funcDef.returnType));
        return funcT;
    }

    @Override
    public SymbolType analyze(GlobalDecl globalDecl) {
        String name = globalDecl.variable.name;
        SymbolType T = globals.get(name);
        if(T == null)
            errors.semError(globalDecl, "global id '" + name + "' not found in global scope..");
        if(sym == globals)
            errors.semError(globalDecl, "global declaration '" + name + "' not allowed in global scope.");
        //sym.put(name, T);
        return T;
    }

    @Override
    public SymbolType analyze(NonLocalDecl nonLocalDecl) {
        String name = nonLocalDecl.variable.name;
        SymbolType T = sym.get(name); // auto-iterate through the tree.
        if(T == null)
            errors.semError(nonLocalDecl, "nonlocal id '" + name + "' not found in parent scope..");
        if(sym == globals)
            errors.semError(nonLocalDecl, "nonlocal declaration '" + name + "' not allowed in global scope.");
        return T;
    }

    // Just a helper..
    @Override
    public SymbolType analyze(TypedVar node) {
        return ValueType.annotationToValueType(node.type);
    }


    /*
    @Override
    public SymbolType analyze(BinaryExpr node) {
        String op = node.operator;

        // Check operand type.
        SymbolType left_type = node.left.getInferredType();
        SymbolType right_type = node.left.getInferredType();
        switch(op) {
            case "-":
            case "*":
            case "//":
            case "%":
            case ">":
            case "<":
            case ">=":
            case "<=":
                if(left_type != ValueType.INT_TYPE || right_type != ValueType.INT_TYPE)
                    node.setErrorMsg("Syntax Error: operand should be INT");
                break;
            case "and":
            case "or":
                if(left_type != ValueType.BOOL_TYPE || right_type != ValueType.BOOL_TYPE)
                    node.setErrorMsg("Syntax Error: operand should be BOOL");
                break;
            case "+":
                if(left_type != ValueType.INT_TYPE && left_type != ValueType.STR_TYPE && ! left_type.isListType())
                    node.setErrorMsg("Syntax Error: operand of + should be INT or STR or LIST");
                // fallthrough
            case "==":
            case "!=":
            case "is":
                if(node.left.getInferredType() != node.right.getInferredType())
                    node.setErrorMsg("Syntax Error: binary operator operand type mismatch");
                break;
            default:
                node.setErrorMsg("Syntax Error: binary operator operand type not supported.");
        }

        // Now set target type.
        SymbolType result_type = ValueType.NONE_TYPE;
        switch(op) {
            case "-":
            case "*":
            case "//":
            case "%":
            case "and":
            case "or":
            case "+":
                result_type = left_type;
                break;
            case ">":
            case "<":
            case ">=":
            case "<=":
            case "==":
            case "!=":
            case "is":
                result_type = ValueType.BOOL_TYPE;
                break;
            default:
                throw new RuntimeException("Logic error");
        }

        return result_type;
    }
    */


}
