package chocopy.pa2;

import chocopy.common.analysis.AbstractNodeAnalyzer;
import chocopy.common.analysis.SymbolTable;
import chocopy.common.analysis.types.*;
import chocopy.common.astnodes.*;

import java.util.*;

/** Analyzes declarations to create a top-level symbol table. */
public class DeclarationAnalyzer extends AbstractNodeAnalyzer<Type> 
{

    /** Current symbol table. Changes with new declarative region. */
    private SymbolTable<Type> sym = new SymbolTable<>();
    /** Global symbol table. */
    private final SymbolTable<Type> globals;
    /** Receiver for semantic error messages. */
    
    private final TypeChecker typeChecker;
    private final Errors errors;
    private final boolean firstPass; 
    // In the first pass declanalyzer will create the global symtable
    // In the second pass, typeAnalyzer will call declanalyzer to 
    // analyze local vars/func/class defs and create sub-scope symtable.
    private ClassVType current_class=null;
    private boolean postCheck = false;
    private String classDefError = null;
    /** A new declaration analyzer sending errors to ERRORS0. */
    public void initScope(SymbolTable<Type> s){
        // Symbol table entry for object class
        ClassVType cvt = new ClassVType("object"), obj = cvt;
        s.put("object", cvt);
        //Symbol table entry for int class
        cvt = new ClassVType("int");
        cvt.super_class = obj;
        s.put("int", cvt);
        //Symbol table entry for str class
        cvt = new ClassVType("str");
        cvt.super_class = obj;
        s.put("str", cvt);
        //Symbol table entry for bool class
        cvt = new ClassVType("bool");
        cvt.super_class = obj;
        s.put("bool", cvt);
        //Symbol table entry for None return type
        cvt = new ClassVType("<None>");
        cvt.super_class = obj;
        s.put("<None>", cvt);
        //Symbol table entry for inbuilt print function
        ArrayList<ValueType> param = new ArrayList<ValueType>();
        param.add(Type.OBJECT_TYPE);
        s.put("print", new FuncType(param, Type.NONE_TYPE));
        //Symbol table entry for inbuilt len function
        param = new ArrayList<ValueType>();
        param.add(Type.OBJECT_TYPE);
        s.put("len", new FuncType(param, Type.INT_TYPE));
        //Symbol table entry for inbuilt input function     
        s.put("input", new FuncType(new ArrayList<>(), Type.STR_TYPE));
    }
    public SymbolTable<Type> createScope(SymbolTable<Type> s){
        SymbolTable<Type> newScope = new SymbolTable<>(s);
        initScope(newScope);
        return newScope;
    }
    //Initializer for the first pass.
    public DeclarationAnalyzer(Errors errors0) 
    {
        firstPass = true;
        errors = errors0;
        globals = sym;
        initScope(sym);
        typeChecker = new TypeChecker(globals, errors);
    }
    //Initializer for the second pass.
    public DeclarationAnalyzer(Errors errors0, TypeChecker typeChecker, SymbolTable<Type> globals)
    {
        firstPass = false;
        this.typeChecker = typeChecker;
        errors = errors0;
        this.globals = globals;
    }
    public SymbolTable<Type> getGlobals() 
    {
        return globals;
    }
    private boolean putSymChecked(Node node, String name, Type ty)
    {
        if (ty == null) 
            return false;
        
        if (globals.get(name)!= null && !(ty instanceof ClassVType) && globals.get(name) instanceof ClassVType) //class names are only in global scope
            errors.semError(node, "Cannot shadow class name: %s", name);
        else if (sym.declares(name)) 
            errors.semError(
                        node, "Duplicate declaration of identifier in same scope: %s", name);
        else 
        {
            sym.put(name, ty);
            return true;
        }
        return false;
    }
    @Override
    public Type analyze(Program program) 
    {
        for (Declaration decl : program.declarations) 
        {
            Identifier id = decl.getIdentifier();
            String name = id.name;
            Type type = decl.dispatch(this);
        }
        // Check for return statements at top
        for (Stmt stmt : program.statements) 
        {
            if (stmt instanceof ReturnStmt)
                errors.semError(
                    stmt, "Return statement cannot appear at the top level");
        }
        return null;
    }
    
    @Override
    public Type analyze(FuncDef node) 
    {
        if(!postCheck){
            Type fTy = null;
            if(sym.declares(node.name.name))
                fTy =  sym.get(node.name.name);

            FuncType current_func=null;

            if(!(fTy instanceof FuncType))
            {
                if(fTy == null)
                {
                    current_func = new FuncType(new ArrayList<ValueType>(),
                        ValueType.annotationToValueType(node.returnType));
                    
                    for (TypedVar param : node.params) 
                    {
                        ValueType p = ValueType.annotationToValueType(param.type);
                        current_func.parameters.add(p);
                        if(classDefError != null && p.className().equals(classDefError))
                            errors.semError(param.type, "Invalid type annotation; there is no class named: %s", classDefError);
                   }
                    sym.put(node.name.name, current_func);
                    if(!firstPass)
                    {
                        SymbolTable<Type> parent = sym.getParent();
                        if(parent!=null && parent != globals){
                            parent.put(node.name.name, current_func);
                        }
                    }
                }
                else if(fTy instanceof ClassVType)
                    errors.semError(node.name, "Cannot shadow class name: %s", node.name.name);
                else 
                    errors.semError(
                        node.name, "Duplicate declaration of identifier in same scope: %s", node.name.name);

            }
            else if(firstPass || sym.declares(node.name.name))
                errors.semError(
                    node.name, "Duplicate declaration of identifier in same scope: %s", node.name.name);
            if(!firstPass){
                
            }
            return current_func;
        } else {
            postCheck = false;

            ValueType returnType = ValueType.annotationToValueType(node.returnType);
            if(returnType!=null && !returnType.isSpecialType() && !returnType.isListType() && !(globals.get(returnType.className()) instanceof ClassVType))
                errors.semError(
                    node.returnType, "Invalid type annotation; there is no class named: %s", returnType.className());

            for(TypedVar param : node.params)
            {
                ValueType pTy = ValueType.annotationToValueType(param.type);
                
                if(!(pTy.isListType() && !pTy.elementType().equals(Type.EMPTY_TYPE))&&!pTy.isSpecialType() && !(globals.get(pTy.className()) instanceof ClassVType))
                    errors.semError(param.type, "Invalid type annotation; there is no class named: %s", pTy.className());

                putSymChecked(param.identifier, param.identifier.name, pTy);
            }
            ArrayList<Declaration> varDefs = new ArrayList<>(), otherDefs = new ArrayList<>();
            for (Declaration decl : node.declarations) 
                if(decl instanceof VarDef || decl instanceof GlobalDecl || decl instanceof NonLocalDecl)
                    varDefs.add(decl);
                else
                    otherDefs.add(decl);
            for (Declaration decl : varDefs) 
                if(decl instanceof VarDef||decl instanceof NonLocalDecl)
                    decl.dispatch(this);
                else
                    decl.dispatch(typeChecker);
            for(Declaration decl : otherDefs)
                decl.dispatch(this);
            for(Declaration decl : otherDefs)
                if(decl instanceof FuncDef)
                    decl.dispatch(typeChecker);
        return null;

        }
    }

    public boolean compare_functions(FuncType fun1, FuncType fun2) 
    {
        if (fun1.returnType.equals(fun2.returnType)==false)
            return false;
        if (fun1.parameters.size() != fun2.parameters.size())
            return false;
        for (int i=1; i<fun1.parameters.size(); i++)
            if (fun1.parameters.get(i).equals(fun2.parameters.get(i))==false)
                return false;
        return true;
    }

    @Override
    public Type analyze(ClassDef node) 
    {
        ClassVType cvt=new ClassVType(node.name.name);
        if(!putSymChecked(node.name, node.name.name, cvt))
            classDefError = node.name.name;

        SymbolTable<Type> current_scope=createScope(sym);
        sym=current_scope;
        current_class=cvt;
        Type super_class = sym.get(node.superClass.name);
        if(super_class instanceof ClassVType)
            cvt.super_class = (ClassVType)super_class;//new ClassVType(super_class.className());
        
        SymbolTable<Type> super_scope=null;
        Set<String> super_syms=null;
        if (super_class == null) 
        {
            errors.semError(
                        node.superClass, "Super-class not defined: %s", node.superClass.name);
        }
        else if ((super_class instanceof ClassVType)==false) 
        {
            errors.semError(
                        node.superClass, "Super-class must be a class: %s", node.superClass.name);
        }
        else if (node.superClass.name.equals("int") || node.superClass.name.equals("bool") || node.superClass.name.equals("str")) 
        {
            errors.semError(
                        node.superClass, "Cannot extend special class: %s", node.superClass.name);
        }
        else
        {
            super_scope = cvt.super_class.scope;
            if(cvt.super_class.scope != null)
                super_syms = super_scope.getDeclaredSymbols();
            else
                super_syms = new HashSet<String>();
            HashSet<String> curr_syms = new HashSet<>();
            for (Declaration decl : node.declarations) 
            {
                Identifier id = decl.getIdentifier();
                String name = id.name;
                
                Type type = null;//decl.dispatch(this);
                type = decl.dispatch(this);
                if(type instanceof FuncType)
                {//For function declarations
                    FuncType current_func = (FuncType) type;
                    List<ValueType> params = current_func.parameters;
                    if(name.equals("__init__") )
                        if( params.size() != 1 || 
                            !(params.get(0) instanceof ClassValueType)|| 
                            !((ClassValueType)params.get(0)).className().equals(current_class.className))
                                errors.semError(id, "Method overridden with different type signature: __init__");
                        else 
                            sym.put(name, current_func);
                    if(params.size() < 1 || !(params.get(0) instanceof ClassValueType) || !((ClassValueType)params.get(0)).className().equals(current_class.className))
                        errors.semError(
                            id, "First parameter of the following method must be of the enclosing class: %s", name);    
                    
                    if(curr_syms.contains(name)){
                        errors.semError(id, "Duplicate declaration of identifier in same scope: %s", name);
                    }
                    else if (super_syms.contains(name)) 
                    {
                        if ((super_scope.get(id.name) instanceof FuncType)==false) 
                            errors.semError(id, "Cannot re-define attribute: %s", name);
                        else 
                        {
                            FuncType super_func = (FuncType) super_scope.get(id.name);
                            if (compare_functions(super_func, current_func)) 
                                sym.put(name, current_func);
                            else
                                errors.semError(
                                id, "Method overridden with different type signature: %s", name);
                        }
                    }
                    else
                        sym.put(name, current_func);
                } 
                else if (name.equals("__init__") && !(type instanceof FuncType))
                    errors.semError(id, "Cannot re-define attribute: %s", name);
                else if (super_syms.contains(name))
                    errors.semError(id, "Cannot re-define attribute: %s", name);
                else 
                    sym.put(name, type);
                curr_syms.add(name);
            }
        }
        if(super_syms != null)
            for (String super_sym : super_syms) 
            {
                if (sym.getDeclaredSymbols().contains(super_sym)==false)
                    sym.put(super_sym, super_scope.get(super_sym));
            }
        sym = sym.getParent();
        current_class.scope = current_scope;
        current_class=null;
        classDefError = null;
        return cvt;
    }
    boolean isVariableType(Type ty)
    {
        return ty.isSpecialType() || ty.equals(Type.OBJECT_TYPE)|| ty.isListType();
    }
    @Override
    public Type analyze(NonLocalDecl node) 
    {
        SymbolTable<Type> parent=sym.getParent();
        if (parent.getParent()!=null && parent.declares(node.variable.name) && isVariableType(sym.get(node.variable.name)))
        {
            sym.put(node.variable.name, sym.get(node.variable.name));
            return sym.get(node.variable.name);
        }
        errors.semError(
                    node.variable, "Not a nonlocal variable: %s", node.variable.name);
        return null;
    }
    @Override
    public Type analyze(VarDef node)
    {
        Type var_type = sym.get(node.var.identifier.name);
        if(firstPass || (sym != globals && (current_class==null || !sym.equals(current_class.scope)))){
            if (sym != globals && globals.get(node.var.identifier.name)!= null && globals.get(node.var.identifier.name) instanceof ClassVType) //class names are only in global scope
                errors.semError(node.var.identifier, "Cannot shadow class name: %s", node.var.identifier.name);
            else if(sym.getDeclaredSymbols().contains(node.var.identifier.name))
                errors.semError(
                    node.var.identifier, "Duplicate declaration of identifier in same scope: %s", node.var.identifier.name);
            var_type = ValueType.annotationToValueType(node.var.type);
            sym.put(node.var.identifier.name, var_type);
        }
        Type val_type = node.value.dispatch(typeChecker);
        if( !firstPass && var_type instanceof ClassValueType)
        {
            String className = ((ClassValueType)var_type).className();
            Type varVType = sym.get(className);
            if(!(className != null && varVType instanceof ClassVType))
                errors.semError(node.var.type, "Invalid type annotation; there is no class named: %s", (className!=null?className:""));
            else if((!val_type.equals(Type.NONE_TYPE) && !StudentAnalysis.subClassOf(varVType,val_type, sym))||
                    val_type.equals(Type.NONE_TYPE) && var_type.isSpecialType())
                errors.semError(node, "Expected type `%s`; got type `%s`", varVType, val_type);
        }
        return var_type;
    }

    public void setScope(SymbolTable<Type> currentScope) 
    {
        sym = currentScope;
    }
    public void setCurrClass(ClassVType current_class)
    {
        this.current_class = current_class;
    }
    public void setPostCheck(){
        postCheck = true;
    }
}
