package accrue.analysis.visit;

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

import polyglot.ast.ArrayInit;
import polyglot.ast.Assign;
import polyglot.ast.Block;
import polyglot.ast.ClassBody;
import polyglot.ast.ClassMember;
import polyglot.ast.ConstructorCall;
import polyglot.ast.ConstructorDecl;
import polyglot.ast.Expr;
import polyglot.ast.Field;
import polyglot.ast.FieldDecl;
import polyglot.ast.FloatLit;
import polyglot.ast.IntLit;
import polyglot.ast.NewArray;
import polyglot.ast.Node;
import polyglot.ast.NodeFactory;
import polyglot.ast.Receiver;
import polyglot.ast.Stmt;
import polyglot.ast.TypeNode;
import polyglot.frontend.Job;
import polyglot.types.ArrayType;
import polyglot.types.ClassType;
import polyglot.types.PrimitiveType;
import polyglot.types.SemanticException;
import polyglot.types.Type;
import polyglot.types.TypeSystem;
import polyglot.util.Position;
import polyglot.visit.ContextVisitor;
import polyglot.visit.DeepCopy;
import polyglot.visit.NodeVisitor;

/*
 * Make initialization order explicit by moving field initializers into constructors.
 */
public class MakeInstanceInitializersExplicit extends ContextVisitor {
    private final DeepCopy deepCopier;

    public MakeInstanceInitializersExplicit(Job job, TypeSystem ts,
            NodeFactory nf) {
        super(job, ts, nf);
        deepCopier = new DeepCopy(nf.lang());
    }

    @Override
    public Node leaveCall(Node old, Node n, NodeVisitor v)
            throws SemanticException {
        ContextVisitor cv = (ContextVisitor) v;
        if (n instanceof ClassBody
                && !cv.context().currentClass().flags().isInterface()) {
            ClassBody cb = (ClassBody) n;

            // Go through the fields removing initializers
            List<FieldDecl> initializersToMove =
                    new ArrayList<FieldDecl>(cb.members().size());
            List<ClassMember> afterFields =
                    new ArrayList<ClassMember>(cb.members().size());
            for (ClassMember m : cb.members()) {
                if (m instanceof FieldDecl) {
                    FieldDecl fd = (FieldDecl) m;
                    if (fd.init() != null && !leaveInitializer(fd)) {
                        initializersToMove.add(fd);
                        m = fd.init(null);
                    }
                    if (fd.init() == null && !leaveInitializer(fd)) {
                        FieldDecl fdInit = fd.init(getDefaultInitializer(fd));
                        initializersToMove.add(fdInit);
                    }
                }
                afterFields.add(m);
            }
            cb = cb.members(afterFields);

            List<ClassMember> afterCtors =
                    new ArrayList<ClassMember>(cb.members().size());
            boolean foundCtor = false;
            for (ClassMember m : cb.members()) {
                if (m instanceof ConstructorDecl) {
                    ConstructorDecl cd = (ConstructorDecl) m;
                    foundCtor = true;
                    m =
                            rewriteConstructorDecl(cv.context().currentClass(),
                                                   cd,
                                                   initializersToMove);
                }
                afterCtors.add(m);
            }
            assert (initializersToMove.isEmpty() || foundCtor);
            cb = cb.members(afterCtors);

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

    protected boolean leaveInitializer(FieldDecl fd) {
        // if it is a jlc type info field, then leave it.
        if (fd.name().startsWith("jlc$")) {
            return true;
        }
        if (fd.flags().isStatic()) {
            return true;
        }
        return false;
    }

    private ConstructorDecl rewriteConstructorDecl(ClassType ct,
            ConstructorDecl cd, List<FieldDecl> initializersToMove) {
        Block block = cd.body();
        ConstructorCall constructorCall = null;
        boolean doFieldInitializers = true;
        // any statements other than the constructor call:
        List<Stmt> remainingStatements = new ArrayList<Stmt>();

        if (block.statements().size() > 0) {
            Stmt fst = block.statements().get(0);
            if (fst instanceof ConstructorCall) {
                remainingStatements.addAll(block.statements()
                                                .subList(1,
                                                         block.statements()
                                                              .size()));
                constructorCall = (ConstructorCall) fst;
//                ConstructorInstance ci = constructorCall.constructorInstance();
                if (constructorCall.kind() == ConstructorCall.THIS) {
                    // Not a super call, don't do field initializers
                    doFieldInitializers = false;
                }
            }
            else {
                remainingStatements.addAll(block.statements());
            }
        }

        List<Stmt> statements = new ArrayList<Stmt>();
        if (constructorCall != null) {
            // original constructor had a constructor call, add it
            statements.add(constructorCall);
        }
        if (doFieldInitializers) {
            for (FieldDecl fd : initializersToMove) {
                statements.add(addFieldInitializer(ct, fd));
            }
        }
        // add any remaining statements from the original constructor
        statements.addAll(remainingStatements);

        block = block.statements(statements);
        cd = (ConstructorDecl) cd.body(block);

        return cd;
    }

    protected Stmt addFieldInitializer(ClassType container, FieldDecl fd) {
        assert !fd.fieldInstance().flags().isStatic();

        Expr init = (Expr) fd.init().visit(deepCopier);
        Position pos = init.position();

        // If init is an array init, need to wrap it in a new array
        if (init instanceof ArrayInit) {
            ArrayInit ai = (ArrayInit) init;
            ArrayType at = fd.fieldInstance().type().toArray();
            TypeNode atn =
                    nf.CanonicalTypeNode(init.position(), at)
                      .type(at.ultimateBase());
            NewArray na =
                    (NewArray) nf.NewArray(init.position(), atn, at.dims(), ai)
                                 .type(at);
            init = na;
        }

        Receiver target = nf.This(pos).type(container);
        Field f = nf.Field(pos, target, nf.Id(pos, fd.fieldInstance().name()));
        f = (Field) f.type(fd.fieldInstance().type());
        f = f.fieldInstance(fd.fieldInstance());
        f = f.targetImplicit(false);

        Stmt initStmt =
                nf.Eval(pos,
                        nf.FieldAssign(pos, f, Assign.ASSIGN, init)
                          .type(fd.fieldInstance().type()));

        return initStmt;
    }

    /**
     * Java Language Specification SE 7 - Section 4.12.5
     * 
     * Data Type        Default Value
     * byte             (byte) 0
     * short            (short) 0
     * int              0
     * long             0L
     * float            0.0f
     * double           0.0d
     * char             '\u0000'
     * boolean          false
     * ReferenceType    null
     * 
     * @param fd 
     *          field decl with no initializer
     * @return Default initializer expression of the correct type and value
     */
    private Expr getDefaultInitializer(FieldDecl fd) {
        Type t = fd.declType();
        Position pos = fd.position();
        if (t instanceof PrimitiveType) {
            PrimitiveType pt = (PrimitiveType) t;
            Expr expr = null;
            if (pt.kind() == PrimitiveType.BYTE) {
                expr = nf.IntLit(pos, IntLit.INT, (byte) 0);
            }
            if (pt.kind() == PrimitiveType.SHORT) {
                expr = nf.IntLit(pos, IntLit.INT, (short) 0);
            }
            if (pt.kind() == PrimitiveType.INT) {
                expr = nf.IntLit(pos, IntLit.INT, 0);
            }
            if (pt.kind() == PrimitiveType.LONG) {
                expr = nf.IntLit(pos, IntLit.LONG, 0L);
            }
            if (pt.kind() == PrimitiveType.FLOAT) {
                expr = nf.FloatLit(pos, FloatLit.FLOAT, 0.0f);
            }
            if (pt.kind() == PrimitiveType.DOUBLE) {
                expr = nf.FloatLit(pos, FloatLit.DOUBLE, 0.0d);
            }
            if (pt.kind() == PrimitiveType.CHAR) {
                expr = nf.CharLit(fd.position(), '\u0000');
            }
            if (pt.kind() == PrimitiveType.BOOLEAN) {
                expr = nf.BooleanLit(fd.position(), false);
            }
            expr = expr.type(pt);
            return expr;
        }
        Expr e = nf.NullLit(fd.position());
        e = e.type(ts.Null());
        return e;
    }
}
