package accrue.analysis.visit;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
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.Expr;
import polyglot.ast.Field;
import polyglot.ast.FieldDecl;
import polyglot.ast.FloatLit;
import polyglot.ast.Initializer;
import polyglot.ast.NewArray;
import polyglot.ast.Node;
import polyglot.ast.NodeFactory;
import polyglot.ast.NumLit;
import polyglot.ast.Receiver;
import polyglot.ast.TypeNode;
import polyglot.frontend.Job;
import polyglot.types.ArrayType;
import polyglot.types.ClassType;
import polyglot.types.Flags;
import polyglot.types.InitializerInstance;
import polyglot.types.SemanticException;
import polyglot.types.TypeSystem;
import polyglot.visit.ContextVisitor;
import polyglot.visit.NodeVisitor;

/**
 * Moves static field initializers into initializer blocks.
 */
public class MoveStaticFieldInitializers extends ContextVisitor {
    final boolean onlyReferenceType;
    final boolean skipDefaultInits;

    public MoveStaticFieldInitializers(Job job, TypeSystem ts, NodeFactory nf,
            boolean onlyReferenceType) {
        this(job, ts, nf, onlyReferenceType, false);
    }

    public MoveStaticFieldInitializers(Job job, TypeSystem ts, NodeFactory nf,
            boolean onlyReferenceType, boolean skipDefaultInits) {
        super(job, ts, nf);
        this.onlyReferenceType = onlyReferenceType;
        this.skipDefaultInits = skipDefaultInits;
    }

    @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;
            List<ClassMember> newMembers =
                    new ArrayList<ClassMember>(cb.members().size() * 2);
            boolean changed = false;
            for (ClassMember m : cb.members()) {
                if (m instanceof FieldDecl) {
                    FieldDecl fd = (FieldDecl) m;
                    if (fd.init() != null && !leaveInitializer(fd)) {
                        // move the init to an initializer
                        changed = true;
                        newMembers.addAll(moveInitializer(fd));
                    }
                    else {
                        newMembers.add(m);
                    }
                }
                else {
                    newMembers.add(m);
                }
            }
            if (!changed) return cb;
            return cb.members(newMembers);
        }
        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 boolean isDefaultValue(Expr e) {
        if (e.type().isNull())
            return true;
        else if (e instanceof NumLit) {
            return ((NumLit) e).longValue() == 0;
        }
        else if (e instanceof FloatLit) {
            return ((FloatLit) e).value() == 0.0;
        }
        else return false;
    }

    private Collection<ClassMember> moveInitializer(FieldDecl fd) {
        Expr init = fd.init();

        // If init is an array init, need to wrap it in a new array
        if (init instanceof ArrayInit) {
            ArrayInit ai = (ArrayInit) init;
            ArrayType at = init.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;
        }

        fd = fd.init(null);

        if (!skipDefaultInits || !isDefaultValue(init) || fd.flags().isFinal()) {
            Flags flags = Flags.NONE;
            Receiver target = null;
            boolean isTargetImplicit = false;
            if (fd.fieldInstance().flags().isStatic()) {
                flags = flags.Static();
                target =
                        nf.CanonicalTypeNode(init.position(),
                                             fd.fieldInstance().container())
                          .type(fd.fieldInstance().container());
                isTargetImplicit = true;
            }
            else {
                assert (false);
                target =
                        nf.This(init.position()).type(fd.fieldInstance()
                                                        .container());
            }

            Field f =
                    nf.Field(init.position(),
                             target,
                             nf.Id(init.position(), fd.fieldInstance().name()));
            f = (Field) f.type(fd.fieldInstance().type());
            f = f.fieldInstance(fd.fieldInstance());
            f = f.targetImplicit(isTargetImplicit);

            Block body =
                    nf.Block(fd.position(),
                             nf.Eval(init.position(),
                                     nf.FieldAssign(init.position(),
                                                    f,
                                                    Assign.ASSIGN,
                                                    init)
                                       .type(fd.fieldInstance().type())));
            Initializer initBlock =
                    this.nf.Initializer(fd.position(), flags, body);
            InitializerInstance ii =
                    ts.initializerInstance(fd.position(),
                                           (ClassType) fd.fieldInstance()
                                                         .container(),
                                           flags);
            initBlock = initBlock.initializerInstance(ii);

            List<ClassMember> newMembers = new ArrayList<ClassMember>(2);
            newMembers.add(fd);
            newMembers.add(initBlock);
            return newMembers;
        }
        else {
            return Collections.<ClassMember> singletonList(fd);
        }
    }
}
