package accrue.analysis.pointer;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import polyglot.ast.Binary;
import polyglot.ast.Catch;
import polyglot.ast.ClassBody;
import polyglot.ast.ClassMember;
import polyglot.ast.CodeNode;
import polyglot.ast.Expr;
import polyglot.ast.FieldDecl;
import polyglot.ast.Formal;
import polyglot.ast.Initializer;
import polyglot.ast.JLang;
import polyglot.ast.LocalDecl;
import polyglot.ast.Node;
import polyglot.ast.NodeFactory;
import polyglot.ast.ProcedureDecl;
import polyglot.ast.SourceFile;
import polyglot.ast.Try;
import polyglot.frontend.Job;
import polyglot.types.ClassType;
import polyglot.types.ConstructorInstance;
import polyglot.types.InitializerInstance;
import polyglot.types.SemanticException;
import polyglot.types.Type;
import polyglot.types.TypeSystem;
import polyglot.util.InternalCompilerError;
import polyglot.visit.ErrorHandlingVisitor;
import polyglot.visit.FindSharedASTVisitor;
import polyglot.visit.NodeVisitor;
import accrue.analysis.ext.AccrueExt;
import accrue.analysis.ext.AccrueExt_c;
import accrue.analysis.ext.ExtExpr;
import accrue.analysis.ext.ExtInitializerOrFieldDecl;
import accrue.analysis.pointer.StmtRegistrar.CodeInfo;
import accrue.analysis.pointer.StmtRegistrar.ExceptionHandlerReferenceVariable;

/**
 * Registers all procedures (methods, constructors, initializers) with
 * the InterAnalysisPass.
 */
public class RegisterPointerStmtsVisitor extends ErrorHandlingVisitor {

    private CodeInfo currentCode;
    private List<ExceptionHandlerReferenceVariable> exceptionHandlers;
    private ConstructorContext constructorContext;

    private boolean recordExprResults;

    public RegisterPointerStmtsVisitor(Job job, TypeSystem ts, NodeFactory nf) {
        super(job, ts, nf);
        this.currentCode = null;
        this.exceptionHandlers = Collections.emptyList();
        this.recordExprResults = false;
        this.constructorContext = ConstructorContext.NORMAL;
    }

    protected RegisterPointerStmtsVisitor codeInfo(CodeInfo codeInfo) {
        RegisterPointerStmtsVisitor newV =
                (RegisterPointerStmtsVisitor) this.copy();
        newV.currentCode = codeInfo;

        if (this.currentCode != null) {
            throw new InternalCompilerError("Looks like a nested procedure decl...");
        }
        if (!this.exceptionHandlers.isEmpty()) {
            throw new InternalCompilerError("Looks like problems with the exception handlers...");
        }
        return newV;
    }

    protected RegisterPointerStmtsVisitor recordExprResults(
            boolean recordExprResults) {
        if (this.recordExprResults == recordExprResults) {
            return this;
        }
        RegisterPointerStmtsVisitor newV =
                (RegisterPointerStmtsVisitor) this.copy();
        newV.recordExprResults = recordExprResults;
        return newV;
    }

    protected RegisterPointerStmtsVisitor exceptionHandlers(
            List<ExceptionHandlerReferenceVariable> exln) {
        RegisterPointerStmtsVisitor newV =
                (RegisterPointerStmtsVisitor) this.copy();
        newV.exceptionHandlers =
                new ArrayList<ExceptionHandlerReferenceVariable>(this.exceptionHandlers.size()
                        + exln.size());
        // important that exln goes first.
        newV.exceptionHandlers.addAll(exln);
        newV.exceptionHandlers.addAll(this.exceptionHandlers);
        return newV;
    }

    public RegisterPointerStmtsVisitor constructorContext(
            ConstructorContext constructorContext) {
        RegisterPointerStmtsVisitor newV =
                (RegisterPointerStmtsVisitor) this.copy();
        newV.constructorContext = constructorContext;
        return newV;
    }

    public CodeInfo currentCode() {
        return this.currentCode;
    }

    public ConstructorContext constructorContext() {
        return this.constructorContext;
    }

    public List<ExceptionHandlerReferenceVariable> exceptionHandlers() {
        return this.exceptionHandlers;
    }

    public boolean recordExprResults() {
        return this.recordExprResults;
    }

    @Override
    protected NodeVisitor enterCall(Node parent, Node n)
            throws SemanticException {
        if (n instanceof SourceFile) {
            // check that there are no shared AST nodes in the source file
            // This just helps prevents later bugs...
            n.visit(new FindSharedASTVisitor((JLang) lang()));
        }
        StmtRegistrar registrar =
                PointerAnalysisPass.singleton(job().extensionInfo())
                                   .registrar();

        // register all procedures when we enter the class
        if (n instanceof ClassBody) {
            ClassBody cb = (ClassBody) n;
            ClassType container = null;
            for (ClassMember m : cb.members()) {
                if (m instanceof ProcedureDecl) {
                    registrar.recordProcedureDecl((ProcedureDecl) m,
                                                  constructorContext());
                }
                if (m instanceof Initializer) {
                    registrar.recordInitializer((Initializer) m);
                    container =
                            ((Initializer) m).initializerInstance()
                                             .container()
                                             .toClass();
                }
                if (m instanceof FieldDecl) {
                    FieldDecl fd = (FieldDecl) m;
                    registrar.recordFieldDecl(fd);
                    if (fd.initializerInstance() != null) {
                        container =
                                fd.initializerInstance().container().toClass();
                    }
                }
            }
            if (container != null) {
                if (container.isAnonymous() || container.isInnerClass()
                        || container.isLocal()) {
                    throw new InternalCompilerError("Don't handle inner, anonymous, or local classes");
                }
                return this.bypass(registrar.getInstanceInitializers(container));
            }
        }
        // if we are entering a code decl, set it as the current CodeInfo
        if (n instanceof CodeNode) {
            CodeNode cn = (CodeNode) n;
            if (cn.codeBody() != null) {
                if (cn.codeInstance() instanceof InitializerInstance
                        && !cn.codeInstance().flags().isStatic()) {
                    // This is an instance initializer (or field decl).
                    // Keep the current code instance (which will be the constructor code instance
                    // for one of the constructors).

                    return this;
                }

                CodeInfo codeInfo = registrar.getCodeInfo((CodeNode) n);
                if (codeInfo == null)
                    throw new InternalCompilerError("No code info for " + n);
                RegisterPointerStmtsVisitor newV =
                        (RegisterPointerStmtsVisitor) this.codeInfo(codeInfo)
                                                          .visitChildren();

                return newV;
            }
            else if (cn instanceof ProcedureDecl) {
                ProcedureDecl pd = (ProcedureDecl) cn;
                if (pd.procedureInstance().flags().isNative()
                        || pd.procedureInstance().flags().isAbstract()) {
                    // this is  code decl with no body. Don't bother visiting children
                    return this.bypassChildren(cn);
                }
            }
        }

        if (n instanceof LocalDecl) {
            LocalDecl ld = (LocalDecl) n;
            registrar.registerLocalDecl(ld.localInstance(),
                                        this.currentCode,
                                        constructorContext());
        }
        if (n instanceof Formal) {
            Formal f = (Formal) n;
            if (f.localInstance().type().isReference()) {

                if (this.currentCode == null) {
                    throw new InternalCompilerError("No current code for formal "
                                                            + f
                                                            + ". Are we inside a new kind of code node?",
                                                    f.position());
                }
                if (!registrar.hasReferenceVariable(f.localInstance(),
                                                    this.currentCode,
                                                    constructorContext())) {
                    throw new InternalCompilerError("Unregistered formal!");
                }
            }
        }
        if (n instanceof Try) {
            Try t = (Try) n;
            List<ExceptionHandlerReferenceVariable> exln =
                    new ArrayList<ExceptionHandlerReferenceVariable>(t.catchBlocks()
                                                                      .size());

            for (Catch c : t.catchBlocks()) {
                ReferenceVariable ln =
                        registrar.registerLocalDecl(c.formal().localInstance(),
                                                    this.currentCode,
                                                    constructorContext());
                Type catchType = c.catchType();
                if (catchType.isSubtype(ts.Error())
                        || ts.Error().isSubtype(catchType)) {
                    // the catch block may catch an implicit error.
                    // Let's record a pointer statement to that effect.
                    registrar.addCatchImplicitError(ln,
                                                    c.formal().position(),
                                                    this.currentCode());
                }
                exln.add(new ExceptionHandlerReferenceVariable(catchType, ln));
            }

            return this.exceptionHandlers(exln);
        }

        // do we want to record the result of expression n?
        return this.recordExprResults(!subexprsUninteresting(parent, n));
    }

    /**
     * As a mild optimization, allow the visitor to specify
     * subexpressions that it is not interested in. These subexpressions
     * will not have points to information computed.
     */
    protected boolean subexprsUninteresting(Node parent, Node child) {
        if (parent instanceof Binary) return true;

        return false;
    }

    @Override
    protected Node leaveCall(Node old, Node n, NodeVisitor v)
            throws SemanticException {
        // this method should ensure the following holds:
        // - every expression has a local node, call it ln
        // - for New nodes, there is an assignment ln = new
        // - for field nodes there is an assignment ln = x.f (for some x)
        // - assignment statements are added for other kinds of assignment (e.g., intialization of fields and locals, explicit assignment, returns, throws)

        AccrueExt ext = AccrueExt_c.ext(n);
        RegisterPointerStmtsVisitor rv = (RegisterPointerStmtsVisitor) v;
        if (rv.currentCode() != null) {
            ext.registerPointerStmts(PointerAnalysisPass.singleton(job().extensionInfo())
                                                        .registrar(),
                                     rv);
        }

        return super.leaveCall(old, n, v);
    }

    public ReferenceVariable getReferenceVariable(Expr e) {
        ExtExpr ext = (ExtExpr) AccrueExt_c.ext(e);
        return ext.getReferenceVariable(this.constructorContext());
    }

    public void visitInstanceIntializer(CodeNode init) {
        if (!(init.codeInstance() instanceof InitializerInstance)) {
            throw new InternalCompilerError("This should only be called for initializers or field decls");
        }

        if (!(this.currentCode().codeInstance instanceof ConstructorInstance)) {
            throw new InternalCompilerError("Not in a constructor decl!");
        }
        RegisterPointerStmtsVisitor newV =
                this.constructorContext(new ConstructorContext((ConstructorInstance) this.currentCode().codeInstance));

        // Clear the bypass nodes and make sure we visit the initializer
        newV = (RegisterPointerStmtsVisitor) newV.visitChildren();

        // Set the "this node" for the initializer or field decl
        ExtInitializerOrFieldDecl ext =
                (ExtInitializerOrFieldDecl) AccrueExt_c.ext(init);
        ext.setThisNode(newV.constructorContext(), newV.currentCode().thisNode);

        init.visit(newV);

    }
}
