/*
 *
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

package org.apache.flex.compiler.internal.abc;

import static org.apache.flex.abc.ABCConstants.OP_dup;
import static org.apache.flex.abc.ABCConstants.OP_findpropstrict;
import static org.apache.flex.abc.ABCConstants.OP_getlocal0;
import static org.apache.flex.abc.ABCConstants.OP_getproperty;
import static org.apache.flex.abc.ABCConstants.OP_getscopeobject;
import static org.apache.flex.abc.ABCConstants.OP_initproperty;
import static org.apache.flex.abc.ABCConstants.OP_newclass;
import static org.apache.flex.abc.ABCConstants.OP_popscope;
import static org.apache.flex.abc.ABCConstants.OP_pushscope;
import static org.apache.flex.abc.ABCConstants.OP_returnvoid;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Vector;

import org.apache.flex.abc.ABCConstants;
import org.apache.flex.abc.instructionlist.InstructionList;
import org.apache.flex.abc.semantics.ClassInfo;
import org.apache.flex.abc.semantics.InstanceInfo;
import org.apache.flex.abc.semantics.MethodBodyInfo;
import org.apache.flex.abc.semantics.MethodInfo;
import org.apache.flex.abc.semantics.Name;
import org.apache.flex.abc.semantics.Namespace;
import org.apache.flex.abc.semantics.PooledValue;
import org.apache.flex.abc.semantics.Trait;
import org.apache.flex.abc.visitors.IABCVisitor;
import org.apache.flex.abc.visitors.IClassVisitor;
import org.apache.flex.abc.visitors.IMethodBodyVisitor;
import org.apache.flex.abc.visitors.IMethodVisitor;
import org.apache.flex.abc.visitors.IScriptVisitor;
import org.apache.flex.abc.visitors.ITraitVisitor;
import org.apache.flex.abc.visitors.ITraitsVisitor;
import org.apache.flex.compiler.definitions.IClassDefinition;
import org.apache.flex.compiler.internal.as.codegen.LexicalScope;
import org.apache.flex.compiler.internal.definitions.ClassDefinition;
import org.apache.flex.compiler.projects.ICompilerProject;

/**
 * Utility class for writing a class definition into an IABCVisitor.
 * <p>
 * After construction:
 * <ul>
 * <li>Instance trait's can be added to the class by calling
 * {@link #getITraitsVisitor()} and visiting each trait to be added.</li>
 * <li>Class trait's can be added to the class by calling
 * {@link #getCTraitsVisitor()} and visiting each trait to be added.</li>
 * <li>Instance methods can be added to the class by calling
 * {@link #addITraitsMethod(Name, Collection, Name, Collection, boolean, boolean, boolean, InstructionList)}
 * .</li>
 * </ul>
 * <p>
 * After all the trait's and instructions have been added to the class, either
 * {@link #finishClass(InstructionList)} or {@link #finishScript()} must be
 * called.
 */
public class ClassGeneratorHelper
{
    /**
     * @return An instruction list with only one "returnvoid" instruction.
     */
    public static InstructionList returnVoid()
    {
        final InstructionList inst = new InstructionList(1);
        inst.addInstruction(OP_returnvoid);
        return inst;
    }
    
    /**
     * Constuctor that thunks to
     * {@link #ClassGeneratorHelper(ICompilerProject, IABCVisitor, Name, ClassDefinition, Collection, Collection, InstructionList, boolean)}.
     * 
     * @param project A compiler project.
     * @param visitor An ABC visitor.
     * @param className The ABC name of the new class.
     * @param baseClass The definition of the class being extended.
     */
    public ClassGeneratorHelper(ICompilerProject project, IABCVisitor visitor, Name className, ClassDefinition baseClass, InstructionList constructorInstructions)
    {
        this(project, visitor, className, baseClass, Collections.<Name> emptyList(), Collections.<Name> emptyList(), constructorInstructions, false);
    }

    /**
     * Constuctor that thunks to
     * {@link #ClassGeneratorHelper(ICompilerProject, IABCVisitor, Name, ClassDefinition, Collection, Collection, InstructionList, boolean)}.
     * 
     * @param project A compiler project.
     * @param visitor An ABC visitor.
     * @param className The ABC name of the new class.
     * @param baseClass The definition of the class being extended.
     * @param implementedInterfaces The ABC name of the interfaces being implemented.
     */
    public ClassGeneratorHelper(ICompilerProject project, IABCVisitor visitor, Name className, ClassDefinition baseClass, Collection<Name> implementedInterfaces, InstructionList constructorInstructions)
    {
        this(project, visitor, className, baseClass, implementedInterfaces, Collections.<Name> emptyList(), constructorInstructions, false);
    }

    /**
     * Generate an ABC class with constructor instructions.
     * 
     * @see #ClassGeneratorHelper(ICompilerProject, IABCVisitor, Name, ClassDefinition, Collection, Collection, InstructionList, InstructionList, boolean)
     */
    public ClassGeneratorHelper(ICompilerProject project, IABCVisitor visitor, Name className, ClassDefinition baseClass, Collection<Name> implementedInterfaces, Collection<Name> constructorParamTypes, InstructionList constructorInstructions, boolean hasProtectedMembers)
    {
        this(project, visitor, className, baseClass, implementedInterfaces, constructorParamTypes, constructorInstructions, returnVoid(), hasProtectedMembers);
    }
    
    /**
     * Constructor
     * 
     * @param project {@link ICompilerProject} project for which ABC is being
     * generated.
     * @param visitor {@link IABCVisitor} to write the new class into.
     * @param className {@link Name} of the class to generate
     * @param baseClass {@link ClassDefinition} for the base class the generated
     * class extends.
     * @param implementedInterfaces Collection of {@link Name}'s that at runtime
     * will refer to interfaces the generated class implement.
     * @param iinitParameterTypes Collection of {@link Name}'s that at runtime
     * will refer to the types of the constructor arguments.
     * @param iinitInstructions Instructions for the constructor.
     * @param cinitInstructions Instructions for the {@code cinit()} method.
     * @param hasProtectedMembers whether or not this class has protected members
     * There must be one {@code returnvoid} instruction.
     */
    public ClassGeneratorHelper(final ICompilerProject project,
                                final IABCVisitor visitor,
                                final Name className,
                                final ClassDefinition baseClass,
                                final Collection<Name> implementedInterfaces,
                                final Collection<Name> iinitParameterTypes,
                                final InstructionList iinitInstructions,
                                final InstructionList cinitInstructions,
                                boolean hasProtectedMembers)
    {
        if (iinitInstructions.canFallThrough())
            throw new IllegalArgumentException("Expected a 'returnvoid' instruction in the iinit() instructions.");
        
        if (cinitInstructions.canFallThrough())
            throw new IllegalArgumentException("Expected a 'returnvoid' instruction in the cinit() instructions.");

        this.project = project;
        this.className = className;
        this.baseClass = baseClass;
        cinfo = new ClassInfo();
        cinfo.cInit = new MethodInfo();
        IMethodVisitor cInitVisitor = visitor.visitMethod(cinfo.cInit);
        cInitVisitor.visit();
        MethodBodyInfo cInitMethodBodyInfo = new MethodBodyInfo();
        cInitMethodBodyInfo.setMethodInfo(cinfo.cInit);
        IMethodBodyVisitor cInitMethodBodyVisitor = cInitVisitor.visitBody(cInitMethodBodyInfo);
        cInitMethodBodyVisitor.visit();
        cInitMethodBodyVisitor.visitInstructionList(cinitInstructions);
        cInitMethodBodyVisitor.visitEnd();
        cInitVisitor.visitEnd();

        iinfo = new InstanceInfo();
        
        if(hasProtectedMembers) 
        {
            iinfo.flags |= ABCConstants.CONSTANT_ClassProtectedNs;
            iinfo.protectedNs = new Namespace(ABCConstants.CONSTANT_ProtectedNs, 
                    className.getSingleQualifier().getName() + ":" +className.getBaseName());
        }
        
        iinfo.interfaceNames = implementedInterfaces.toArray(new Name[implementedInterfaces.size()]);
        iinfo.name = className;
        iinfo.superName = baseClass.getMName(project);
        iinfo.iInit = new MethodInfo();
        iinfo.iInit.setParamTypes(new Vector<Name>(iinitParameterTypes));
        iTraitsInitMethodVisitor = visitor.visitMethod(iinfo.iInit);
        iTraitsInitMethodVisitor.visit();
        MethodBodyInfo iTraitsInitMethodBodyInfo = new MethodBodyInfo();
        iTraitsInitMethodBodyInfo.setMethodInfo(iinfo.iInit);
        iTraitsInitMethodBodyVisitor = iTraitsInitMethodVisitor.visitBody(iTraitsInitMethodBodyInfo);
        iTraitsInitMethodBodyVisitor.visit();
        iTraitsInitMethodBodyVisitor.visitInstructionList(iinitInstructions);

        this.visitor = visitor;
        classVisitor = visitor.visitClass(iinfo, cinfo);
        classVisitor.visit();
        itraits = classVisitor.visitInstanceTraits();
        itraits.visit();

        ctraits = classVisitor.visitClassTraits();
        ctraits.visit();
    }
    
    private final ICompilerProject project;
    private final Name className;
    private final ClassDefinition baseClass;
    private final ClassInfo cinfo;
    private final InstanceInfo iinfo;
    private final IABCVisitor visitor;
    private final IClassVisitor classVisitor;
    private final ITraitsVisitor itraits;
    private final ITraitsVisitor ctraits;
    private final IMethodVisitor iTraitsInitMethodVisitor;
    private final IMethodBodyVisitor iTraitsInitMethodBodyVisitor;
    
    /**
     * Adds a new script to the ABC with a trait for the class closure and an
     * init with instructions to create the class closure for the generated
     * class.
     * 
     * <p>
     * After this method is called no other methods on this class should be called.
     */
    public void finishScript()
    {
        IScriptVisitor sv = visitor.visitScript();
        sv.visit();
        ITraitsVisitor scriptTraits = sv.visitTraits();
        scriptTraits.visit();
        scriptTraits.visitClassTrait(ABCConstants.TRAIT_Class, className, 0, cinfo);
        scriptTraits.visitEnd();
        MethodInfo scriptInitMethodInfo = new MethodInfo();
        IMethodVisitor scriptInitMethodVisitor = visitor.visitMethod(scriptInitMethodInfo);
        scriptInitMethodVisitor.visit();
        MethodBodyInfo scriptInitMethodBodyInfo = new MethodBodyInfo();
        scriptInitMethodBodyInfo.setMethodInfo(scriptInitMethodInfo);
        IMethodBodyVisitor scriptInitMethodBodyVisitor = scriptInitMethodVisitor.visitBody(scriptInitMethodBodyInfo);
        scriptInitMethodBodyVisitor.visit();
        InstructionList scriptInitInstructions = new InstructionList();
        scriptInitInstructions.addInstruction(OP_getlocal0);
        scriptInitInstructions.addInstruction(OP_pushscope);
        finishClass(scriptInitInstructions);
        if (scriptInitInstructions.canFallThrough())
            scriptInitInstructions.addInstruction(OP_returnvoid);
        scriptInitMethodBodyVisitor.visitInstructionList(scriptInitInstructions);
        scriptInitMethodBodyVisitor.visitEnd();
        scriptInitMethodVisitor.visitEnd();
        sv.visitInit(scriptInitMethodInfo);
        sv.visitEnd();
    }
    
    /**
     * Generates the instructions to create the class closure and adds them to
     * the specified {@link InstructionList}.
     * 
     * @param scriptInitInstructions {@link InstructionList} to add the
     * instructions that create the class closure to.
     */
    public void finishClass(InstructionList scriptInitInstructions)
    {
        iTraitsInitMethodBodyVisitor.visitEnd();
        iTraitsInitMethodVisitor.visitEnd();
        
        LinkedList<Name> ancestorNames = new LinkedList<Name>();
        
        for (IClassDefinition ancestorIClass : baseClass.classIterable(project, true))
        {
            final ClassDefinition ancestorClass = (ClassDefinition) ancestorIClass;
            ancestorNames.addFirst(ancestorClass.getMName(project));
        }
        
        scriptInitInstructions.addInstruction(OP_getscopeobject, 0);
        
        //  Push ancestor classes onto the scope stack in
        //  order by superclass relationship; the immediate
        //  superclass is handled specially just below.
        
        assert ancestorNames.size() > 0;
        Name superclassName = ancestorNames.removeLast();
        for (Name ancestorName : ancestorNames)
        {
            scriptInitInstructions.addInstruction(OP_findpropstrict, ancestorName);
            scriptInitInstructions.addInstruction(OP_getproperty, ancestorName);
            scriptInitInstructions.addInstruction(OP_pushscope);
        }
        
        scriptInitInstructions.addInstruction(OP_findpropstrict, superclassName);
        scriptInitInstructions.addInstruction(OP_getproperty, superclassName);
        scriptInitInstructions.addInstruction(OP_dup);
        scriptInitInstructions.addInstruction(OP_pushscope);
     
        scriptInitInstructions.addInstruction(OP_newclass, cinfo);
        
        for ( int i = 0; i < ancestorNames.size(); i++ )
            scriptInitInstructions.addInstruction(OP_popscope);

        scriptInitInstructions.addInstruction(OP_popscope);
        
        scriptInitInstructions.addInstruction(OP_initproperty, className);
        
        ctraits.visitEnd();
        itraits.visitEnd();
        
        classVisitor.visitEnd();
        
    }
    
    /**
     * @return The {@link ITraitsVisitor} for the instance trait's of the generated class.
     */
    public ITraitsVisitor getITraitsVisitor()
    {
        return itraits;
    }
    
    /**
     * @return The {@link ITraitsVisitor} for the class trait's of the generated class.
     */
    public ITraitsVisitor getCTraitsVisitor()
    {
        return ctraits;
    }
    
    private ITraitVisitor addMethodToTraits(ITraitsVisitor traits, Name methodName,
            Collection<Name> parameterTypes,
            Name returnType,
            Collection<Object> defaultParameterValues,
            boolean needsRest,
            int functionTraitKind,
            InstructionList body)
    {
        MethodInfo mi = new MethodInfo();
        for (Object defaultParameterValue : defaultParameterValues)
            mi.addDefaultValue(new PooledValue(defaultParameterValue));
        mi.setParamTypes(new Vector<Name>(parameterTypes));
        mi.setReturnType(returnType);
        if (needsRest)
            mi.setFlags(mi.getFlags() | ABCConstants.METHOD_Needrest);
        FunctionGeneratorHelper.generateFunction(visitor, mi, body);
        return traits.visitMethodTrait(functionTraitKind, methodName, 0, mi);
    }
    
    /**
     * Utility method to add an instance method to the generated class.
     * 
     * @param methodName {@link Name} of the method to add.
     * @param parameterTypes Collection of {@link Name}'s of the parameters to
     * the method.
     * @param returnType {@link Name} of the return type of the method.
     * @param defaultParameterValues Collection of object's that can be
     * converted to ABC constants for the default values of parameters.
     * @param needsRest true if the method needs the rest parameter, false otherwise
     * @param isFinal true if the method is final, false otherwise
     * @param isOverride true if the method is an override of another method, false otherwise
     * @param body An {@link InstructionList} for the body of the method.
     */
    public void addITraitsMethod(Name methodName,
            Collection<Name> parameterTypes,
            Name returnType,
            Collection<Object> defaultParameterValues,
            boolean needsRest,
            boolean isFinal,
            boolean isOverride,
            InstructionList body)
    {
        addITraitsMethod(methodName, parameterTypes, returnType, defaultParameterValues,
                needsRest, isFinal, isOverride, body, ABCConstants.TRAIT_Method);
    }
    
    /**
     * Utility method to add an instance method to the generated class.
     * 
     * @param methodName {@link Name} of the method to add.
     * @param parameterTypes Collection of {@link Name}'s of the parameters to
     * the method.
     * @param returnType {@link Name} of the return type of the method.
     * @param defaultParameterValues Collection of object's that can be
     * converted to ABC constants for the default values of parameters.
     * @param needsRest true if the method needs the rest parameter, false otherwise
     * @param isFinal true if the method is final, false otherwise
     * @param isOverride true if the method is an override of another method, false otherwise
     * @param body An {@link InstructionList} for the body of the method.
     * @param functionKindTrait One of ABCConstants, TRAIT_Method, TRAIT_Getter, 
     * TRAIT_Setter.
     */
    public void addITraitsMethod(Name methodName,
            Collection<Name> parameterTypes,
            Name returnType,
            Collection<Object> defaultParameterValues,
            boolean needsRest,
            boolean isFinal,
            boolean isOverride,
            InstructionList body,
            int functionKindTrait)
    {
        ITraitVisitor traitVisitor = addMethodToTraits(itraits, methodName, parameterTypes, 
                returnType, defaultParameterValues, needsRest, functionKindTrait, body);
        traitVisitor.visitStart();
        if (isFinal)
            traitVisitor.visitAttribute(Trait.TRAIT_FINAL, true);
        if (isOverride)
            traitVisitor.visitAttribute(Trait.TRAIT_OVERRIDE, true);
        traitVisitor.visitEnd();
    }

    /**
     * Utility method to add a static method to the generated class.
     * 
     * @param methodName {@link Name} of the method to add.
     * @param parameterTypes Collection of {@link Name}'s of the parameters to
     * the method.
     * @param returnType {@link Name} of the return type of the method.
     * @param defaultParameterValues Collection of object's that can be
     * converted to ABC constants for the default values of parameters.
     * @param needsRest true if the method needs the rest parameter, false otherwise
     * @param body An {@link InstructionList} for the body of the method.
     */
    public void addCTraitsMethod(Name methodName,
            Collection<Name> parameterTypes,
            Name returnType,
            Collection<Object> defaultParameterValues,
            boolean needsRest,
            InstructionList body) 
    {
        ITraitVisitor traitVisitor = addMethodToTraits(ctraits, methodName, parameterTypes, returnType, defaultParameterValues, needsRest, ABCConstants.TRAIT_Method, body);
        traitVisitor.visitStart();
        traitVisitor.visitEnd();
    }

    /**
     * Utility method to add an instance getter to the generated class.
     * 
     * @param getterName {@link Name} of the method to add.
     * @param returnType {@link Name} of the return type of the method.
     * @param body An {@link InstructionList} for the body of the method.
     */
    public void addITraitsGetter(Name getterName, Name returnType, InstructionList body)
    {
        ITraitVisitor traitVisitor = addMethodToTraits(itraits, getterName, Collections.<Name>emptyList(), returnType, Collections.<Object>emptyList(), false, ABCConstants.TRAIT_Getter, body);
        traitVisitor.visitStart();
        traitVisitor.visitEnd();
    }
    
    /**
     * Utility method to add a static getter to the generated class.
     * 
     * @param getterName {@link Name} of the method to add.
     * @param returnType {@link Name} of the return type of the method.
     * @param body An {@link InstructionList} for the body of the method.
     */
    public void addCTraitsGetter(Name getterName, Name returnType, InstructionList body)
    {
        ITraitVisitor traitVisitor = addMethodToTraits(ctraits, getterName, Collections.<Name>emptyList(), returnType, Collections.<Object>emptyList(), false, ABCConstants.TRAIT_Getter, body);
        traitVisitor.visitStart();
        traitVisitor.visitEnd();
    }

    /**
     * Utility method to add a member variable to a class.
     * 
     * @param variableName {@link Name} of the member variable to add.
     */
    public void addMemberVariable(Name variableName, Name type)
    {
        ITraitVisitor traitVisitor = itraits.visitSlotTrait(ABCConstants.TRAIT_Var, variableName, ITraitsVisitor.RUNTIME_SLOT, type, LexicalScope.noInitializer);
        traitVisitor.visitStart();
        traitVisitor.visitEnd();
    }

    /**
     * @return protected namespace if it is asked for while creating this helper class
     */
    public Namespace getProtectedNamespace() {
        assert iinfo.protectedNs != null : "protected namespace is only available if you pass true for the hasProtectedMembers argument of this class' constructor";
        return iinfo.protectedNs;
    }
}
