/*
 *
 *  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.royale.compiler.internal.definitions;

import static org.apache.royale.compiler.common.ISourceLocation.UNKNOWN;

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

import org.apache.royale.compiler.common.DependencyType;
import org.apache.royale.compiler.constants.IASKeywordConstants;
import org.apache.royale.compiler.constants.IMetaAttributeConstants;
import org.apache.royale.compiler.definitions.IClassDefinition;
import org.apache.royale.compiler.definitions.IDefinition;
import org.apache.royale.compiler.definitions.IFunctionDefinition;
import org.apache.royale.compiler.definitions.IGetterDefinition;
import org.apache.royale.compiler.definitions.IInterfaceDefinition;
import org.apache.royale.compiler.definitions.INamespaceDefinition;
import org.apache.royale.compiler.definitions.IPackageDefinition;
import org.apache.royale.compiler.definitions.IParameterDefinition;
import org.apache.royale.compiler.definitions.ISetterDefinition;
import org.apache.royale.compiler.definitions.ITypeDefinition;
import org.apache.royale.compiler.definitions.references.INamespaceReference;
import org.apache.royale.compiler.definitions.metadata.IMetaTag;
import org.apache.royale.compiler.definitions.references.IReference;
import org.apache.royale.compiler.internal.projects.CompilerProject;
import org.apache.royale.compiler.projects.ICompilerProject;
import org.apache.royale.compiler.scopes.IASScope;
import org.apache.royale.compiler.scopes.IDefinitionSet;
import org.apache.royale.compiler.tree.as.IASNode;
import org.apache.royale.compiler.tree.as.IContainerNode;
import org.apache.royale.compiler.tree.as.IDefinitionNode;
import org.apache.royale.compiler.tree.as.IFunctionNode;

/**
 * Each instance of this class represents the definition of an ActionScript
 * function in the symbol table.
 * <p>
 * After a function definition is in the symbol table, it should always be
 * accessed through the read-only <code>IFunctionDefinition</code> interface.
 */
public class FunctionDefinition extends ScopedDefinitionBase implements IFunctionDefinition
{
    private static final ParameterDefinition[] NO_PARAMETERS = new ParameterDefinition[0];

    public FunctionDefinition(String name)
    {
        super(name);
    }

    private ParameterDefinition[] parameters = NO_PARAMETERS;

    // This field is similar to typeReference on DefinitionBase.
    // For a function, typeReference is always a reference to the Function class,
    // so this field store a reference to the return type.
    private IReference returnTypeReference;

    @Override
    public void setNode(IDefinitionNode node)
    {
        super.setNode(node);

        // If we don't have a name offset, maybe we are an anonymous function
        if (getNameStart() == UNKNOWN)
        {
            if (node instanceof IFunctionNode)
            {
                IFunctionNode functionNode = (IFunctionNode)node;
                if (functionNode.getFunctionClassification() == FunctionClassification.LOCAL)
                {
                    // well, we are a local function without a name. So let's make up a name offset.
                    // so for something like: function():void
                    // we want the name offset to be the location right before the open parent of the
                    // parameter list.
                    // We will derive this offset by getting the "parameters" node and subtracting one
                    IContainerNode parametersNode = functionNode.getParametersContainerNode();

                    int synthesizedNameOffset = parametersNode.getStart();
                    if (synthesizedNameOffset > 0)
                        synthesizedNameOffset--;

                    this.setNameLocation(synthesizedNameOffset, synthesizedNameOffset);
                }
            }
        }
    }

    @Override
    public FunctionClassification getFunctionClassification()
    {
        IDefinition parent = getParent();

        if (parent instanceof IFunctionDefinition)
            return FunctionClassification.LOCAL;
        if (parent instanceof IClassDefinition)
            return FunctionClassification.CLASS_MEMBER;
        if (parent instanceof IInterfaceDefinition)
            return FunctionClassification.INTERFACE_MEMBER;
        if (parent instanceof IPackageDefinition)
            return FunctionClassification.PACKAGE_MEMBER;
        if (parent == null)
        {
            if (inPackageNamespace())
                return FunctionClassification.PACKAGE_MEMBER;

            return FunctionClassification.FILE_MEMBER;
        }

        assert false;
        return null;
    }

    @Override
    public ParameterDefinition[] getParameters()
    {
        return parameters;
    }

    @Override
    public boolean hasRequiredParameters()
    {
        for (ParameterDefinition parameter : parameters)
        {
            if (!(parameter.hasDefaultValue()) && (!parameter.isRest()))
                return true;
        }

        return false;
    }

    public void setParameters(ParameterDefinition[] value)
    {
        assert value != null : "setParameters() wants an empty array, not null";

        parameters = value;

        // Parameters from ABC may be unnamed, so we may have avoided the setContainingScope
        // logic in addDefinition().
        for (ParameterDefinition p : parameters)
        {
            p.setContainingScope(this.getContainedScope());
        }
    }

    @Override
    public String getReturnTypeAsDisplayString()
    {
        return returnTypeReference != null ? returnTypeReference.getDisplayString() : "";
    }

    /**
     * Sets a reference to the return type for this function.
     * 
     * @param returnTypeReference An {@link IReference} to a class or interface.
     */
    public void setReturnTypeReference(IReference returnTypeReference)
    {
        this.returnTypeReference = returnTypeReference;
    }

    @Override
    public IReference getReturnTypeReference()
    {
        return returnTypeReference;
    }

    @Override
    public ITypeDefinition resolveReturnType(ICompilerProject project)
    {
        // The return type of a constructor is its class,
        // not <code>void</code>.
        if (isConstructor())
        {
            IDefinition typeDef = getParent();
            if (typeDef instanceof ITypeDefinition)
                return (ITypeDefinition)typeDef;
            else
                return null;
        }

        // TODO We don't really need to make this a signature dependency
        // if this function is a function closure.  If this function
        // is a closure then we could make this an expression dependency
        // instead.
        DependencyType dt = DependencyType.SIGNATURE;
        return resolveType(returnTypeReference, project, dt);
    }

    @Override
    public boolean isConstructor()
    {
        return (flags & FLAG_CONSTRUCTOR) != 0;
    }

    public void setAsConstructor(ClassDefinition classDef)
    {
        flags |= FLAG_CONSTRUCTOR;

        classDef.setConstructor(this);
    }

    @Override
    public boolean isCastFunction()
    {
        return (flags & FLAG_CAST_FUNCTION) != 0;
    }

    public void setCastFunction()
    {
        flags |= FLAG_CAST_FUNCTION;
    }

    @Override
    public boolean inlineFunction()
    {
        // only attempt to inline a function if it has the inline m
        if (!isInline())
            return false;

        if (canFunctionBeInlined())
            return true;

        return false;
    }

    @Override
    public final boolean isInline()
    {
        IMetaTag inlineMetaData = getMetaTagByName(IMetaAttributeConstants.ATTRIBUTE_INLINE);
        if (inlineMetaData == null)
            return false;

        return true;
    }

    protected final boolean canFunctionBeInlined()
    {
        // only final or static functions and top level functions can be inlined
        if (!(isFinal() || isStatic() || isTopLevelDefinition()))
            return false;

        // methods on an interface can't be inlined, only the implementation methods
        IDefinition containingDef = getContainingScope().getDefinition();
        if (containingDef instanceof InterfaceDefinition)
            return false;

        return true;
    }

    @Override
    public boolean overridesAncestor(ICompilerProject project)
    {
        return (resolveOverriddenFunction(project) != null);
    }

    @Override
    public FunctionDefinition resolveOverriddenFunction(ICompilerProject project)
    {
        // The function must be a class method.
        if (getFunctionClassification() != FunctionClassification.CLASS_MEMBER)
            return null;

        // Get the method's class.
        ClassDefinition cls = (ClassDefinition)getParent();

        // Iterate over the superclasses of this method's class.
        ClassDefinition base = (ClassDefinition)cls.resolveBaseClass(project);

        if (base != null)
        {
            // Adjust the namespace if this is the protected namespace
            INamespaceDefinition namespace = resolveNamespace(project);
            if (namespace == null)
                return null; // can't resolve namespace, hence overridden function can't be resolved

            INamespaceDefinition protectedNS = cls.getProtectedNamespaceReference().resolveNamespaceReference(project);
            if (namespace.equals(protectedNS))
                namespace = base.getProtectedNamespaceReference().resolveNamespaceReference(project);

            // Look for a property with the same name as this function in the base class
            // the lookup will search up the inheritance chain, so we don't have to worry about
            // walking up the inheritance chain here.
            IDefinition baseFunc = base.getContainedScope().getQualifiedPropertyFromDef(
                    project, base, this.getBaseName(), namespace, false);

            return baseFunc instanceof FunctionDefinition ? (FunctionDefinition)baseFunc : null;
        }
        return null;
    }

    @Override
    public boolean isImplementation(ICompilerProject project)
    {
        return resolveImplementedFunction(project) != null;
    }

    /**
     * Return a list of FunctionDefinitions from the base interfaces that this FunctionDefinition overrides.
     * We do not allow overriding of Interface methods, but we need to find the methods that would be overriden
     * so that we can issue a diagnostic.
     *
     * This method will only work for FunctionDefinitions that are interface members.
     *
     * @param project  The project to use to resolve references
     * @return         A list of IFunctionDefinitions from any base interfaces that have the same base name
     *                 as this FunctionDefinition.  If there are none, an empty list will be returned
     */
    public List<IFunctionDefinition> resolveOverridenInterfaceFunctions(ICompilerProject project)
    {
        if( getFunctionClassification() != FunctionClassification.INTERFACE_MEMBER )
            return Collections.emptyList();

        final InterfaceDefinition interf = (InterfaceDefinition)getParent();

        // Find the methods from the base interfaces by calling getPropertiesByNameForMemberAccess
        // and passing in the namespace set that has only the interface namespaces from the extended interface
        // this way we won't find any methods declared in the interface declaring the function
        List<IDefinition> funcs = getContainingASScope().getPropertiesByNameForMemberAccess(
                (CompilerProject)project,
                this.getBaseName(),
                interf.getInterfaceNamespaceSet(project, InterfaceDefinition.InterfaceNamespaceSetOptions.DONT_INCLUDE_THIS));

        if( funcs.size() == 0 )
            return Collections.emptyList();

        List<IFunctionDefinition> conflicts = new ArrayList<IFunctionDefinition>(funcs.size());

        // Convert to a list of IFunctionDefinitions
        for( IDefinition d : funcs )
        {
            if( d instanceof IFunctionDefinition)
                conflicts.add((IFunctionDefinition)d);
        }
        return conflicts;
    }

    @Override
    public IFunctionDefinition resolveImplementedFunction(ICompilerProject project)
    {
        // The function must be a class method.
        if (getFunctionClassification() != FunctionClassification.CLASS_MEMBER)
            return null;

        // Get the method's class.
        final ClassDefinitionBase cls = (ClassDefinitionBase)getParent();

        // Iterate over all the interfaces implemented by this method's class.
        final Iterator<IInterfaceDefinition> iter = cls.interfaceIterator(project);
        while (iter.hasNext())
        {
            final IInterfaceDefinition intf = iter.next();

            // In each interface, look for a method matching this one.
            final IFunctionDefinition f = findMatchingMethod(intf, project);
            if (f != null)
                return f;
        }

        return null;
    }

    // Look in an specified class or interface for a method
    // that matches (i.e., has the same name as) this method.
    private FunctionDefinition findMatchingMethod(ITypeDefinition type, ICompilerProject project)
    {
        final String baseName = getBaseName();
        final boolean isInterface = type instanceof IInterfaceDefinition;

        // Look at the type's local definitions that have the same name as this function.
        final IASScope typeScope = type.getContainedScope();
        final IDefinitionSet definitionSet = typeScope.getLocalDefinitionSetByName(baseName);
        final int n = definitionSet != null ? definitionSet.getSize() : 0;
        for (int i = 0; i < n; i++)
        {
            IDefinition member = definitionSet.getDefinition(i);
            
            // Just look at functions.
            if (member instanceof FunctionDefinition)
            {
                // If one has the same signature as this function, return it.
                final FunctionDefinition f = (FunctionDefinition)member;
                if (hasSameNameAndSignature(f, isInterface, project))
                    return f;
            }
        }

        return null;
    }

    /**
     * Compares the signatures of two methods.
     * 
     * @param other is a function to compare "this" to. Must be from a super
     * type
     * @param otherIsInterface true if "other" is a function from an interface
     */
    private boolean hasSameNameAndSignature(IFunctionDefinition other, boolean otherIsInterface, ICompilerProject project)
    {
        // Compare method names.
        String name1 = getBaseName();
        String name2 = other.getBaseName();
        if (!name1.equals(name2))
            return false;

        // If other is an interface, then we don't need to compare namespaces.
        // As long as the signature match, and the caller guarantees to us that "other" is in fact an
        // interface above us in the derivation chain, then we can ignore the namespaces.
        // If we did not ignore them, they would not match, since by definition interfaces are in a different 
        // namespace

        if (!otherIsInterface)
        {
            // Compare method namespaces.
            // Note that equals() for namespace references
            // actually compares URIs for non-builtin namespaces.
            INamespaceReference nsRef1 = getNamespaceReference();
            INamespaceReference nsRef2 = other.getNamespaceReference();
            if (!nsRef1.equals(nsRef2))
                return false;
        }
        else
        {
            // you must only call this when "this" is a member of a class
            assert (getFunctionClassification() == FunctionClassification.CLASS_MEMBER);

            // we can only implement an interface if we are public
            if (!NamespaceDefinition.getPublicNamespaceDefinition().equals(this.getNamespaceReference()))
                return false;
        }

        return hasCompatibleSignature(other, project);
    }

    private boolean copiedMetaData = false;
    
    public boolean hasCompatibleSignature(IFunctionDefinition other, ICompilerProject project)
    {
    	if (!copiedMetaData)
    	{
    		if (other.isImplementation(project))
    		{
	    		copiedMetaData = true;
	    		IMetaTag myTag = this.getMetaTagByName(IMetaAttributeConstants.ATTRIBUTE_SWFOVERRIDE);
	    		if (myTag == null)
	    		{
		    		IMetaTag tag = other.getMetaTagByName(IMetaAttributeConstants.ATTRIBUTE_SWFOVERRIDE);
		    		if (tag != null)
		    		{
		    			this.addMetaTag(tag);
		    		}
	    		}
    		}
    	}
        // Compare return types.
        ITypeDefinition returnType1 = resolveReturnType(project);
        ITypeDefinition returnType2 = other.resolveReturnType(project);
        if (!project.isCompatibleOverrideReturnType(this, returnType1, returnType2))
            return false;
            
        // Compare parameters.
        IParameterDefinition[] params1 = getParameters();
        IParameterDefinition[] params2 = other.getParameters();

        // Compare number of parameters.
        int n1 = params1 != null ? params1.length : 0;
        int n2 = params2 != null ? params2.length : 0;
        if (n1 != n2)
            return false;

        for (int i = 0; i < n1; i++)
        {
            IParameterDefinition param1 = params1[i];
            IParameterDefinition param2 = params2[i];

            // Compare ith parameter types.
            // The types must be resolved because one might be
            // "Sprite" and the other "flash.display.Sprite".
            ITypeDefinition type1 = param1.resolveType(project);
            ITypeDefinition type2 = param2.resolveType(project);
            if (type1 != type2)
            {
                if (!project.isCompatibleOverrideParameterType(this, type1, type2, i))
                    return false;
            }

            // Compare ith parameter 'rest' flag.
            boolean rest1 = param1.isRest();
            boolean rest2 = param2.isRest();
            if (rest1 != rest2)
                return false;

            // Compare ith parameter optionality.
            boolean hasDefault1 = param1.hasDefaultValue();
            boolean hasDefault2 = param2.hasDefaultValue();
            if (hasDefault1 != hasDefault2)
                return false;
        }

        // The signatures are the same.
        return true;
    }

    @Override
    public IFunctionNode getFunctionNode()
    {
        IASNode node = getNode();
        if (node instanceof IFunctionNode)
            return (IFunctionNode)node;
        return null;
    }

    /**
     * Is this definition a toplevel definition - constructors are special,
     * which is why this is overriden here
     * 
     * @return true if this definition is declared at file scope, or package
     * scope.
     */
    @Override
    public boolean isTopLevelDefinition()
    {
        // Constructors are toplevel if their class is toplevel
        if (isConstructor())
            return ((DefinitionBase)getParent()).isTopLevelDefinition();
        // Not a constructor, just follow the usual rules
        return super.isTopLevelDefinition();
    }

    // TODO Remove everything below here when Royale has been integrated into Fb and Fc.

    @Override
    public boolean matches(DefinitionBase definition)
    {
        boolean match = super.matches(definition);
        if (!match)
            return false;

        IFunctionDefinition functionDefinition = (IFunctionDefinition)definition;

        FunctionClassification classification = functionDefinition.getFunctionClassification();
        if (classification != getFunctionClassification())
            return false;

        // Along with local and file member, name offsets needs to be compared for class/interface members also.
        // This is required to differentiate members having same name belonging to different class/interface
        // within the same AS file - See FBG-3494 for an example.
        if (classification == FunctionClassification.LOCAL || classification == FunctionClassification.FILE_MEMBER
                || classification == FunctionClassification.CLASS_MEMBER || classification == FunctionClassification.INTERFACE_MEMBER)
        {
            if (functionDefinition.getNameStart() != getNameStart() ||
                functionDefinition.getNameEnd() != functionDefinition.getNameEnd())
            {
                return false;
            }
        }
        if (functionDefinition instanceof ISetterDefinition && !(this instanceof ISetterDefinition))
            return false;
        if (functionDefinition instanceof IGetterDefinition && !(this instanceof IGetterDefinition))
            return false;
        if (functionDefinition.isConstructor() && !isConstructor())
            return false;

        //TODO match params?
        if (functionDefinition.getParameters().length != getParameters().length)
            return false;

        return true;
    }

    /**
     * For debugging only. Produces a string such as
     * <code>public function f(int, String):void</code>.
     */
    @Override
    protected void buildInnerString(StringBuilder sb)
    {
        sb.append(getNamespaceReferenceAsString());
        sb.append(' ');

        if (isStatic())
        {
            sb.append(IASKeywordConstants.STATIC);
            sb.append(' ');
        }

        sb.append(IASKeywordConstants.FUNCTION);
        sb.append(' ');

        sb.append(getBaseName());

        sb.append('(');
        IParameterDefinition[] params = getParameters();
        int n = params != null ? params.length : 0;
        for (int i = 0; i < n; i++)
        {
            sb.append(params[i].toString());
            if (i < n - 1)
            {
                sb.append(',');
                sb.append(' ');
            }
        }
        sb.append(')');

        String returnType = getReturnTypeAsDisplayString();
        if (!returnType.isEmpty())
        {
            sb.append(':');
            sb.append(returnType);
        }
    }
}
