/*
 *
 *  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.scopes;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Set;

import org.apache.royale.compiler.constants.IASLanguageConstants;
import org.apache.royale.compiler.definitions.IClassDefinition;
import org.apache.royale.compiler.definitions.IDefinition;
import org.apache.royale.compiler.definitions.INamespaceDefinition;
import org.apache.royale.compiler.definitions.ITypeDefinition;
import org.apache.royale.compiler.internal.definitions.AppliedVectorDefinition;
import org.apache.royale.compiler.internal.definitions.ClassDefinition;
import org.apache.royale.compiler.internal.definitions.ClassDefinitionBase;
import org.apache.royale.compiler.internal.definitions.DefinitionBase;
import org.apache.royale.compiler.internal.definitions.InterfaceDefinition;
import org.apache.royale.compiler.internal.definitions.TypeDefinitionBase;
import org.apache.royale.compiler.internal.projects.CompilerProject;
import org.apache.royale.compiler.internal.tree.as.ScopedBlockNode;
import org.apache.royale.compiler.projects.ICompilerProject;
import org.apache.royale.compiler.scopes.IASScope;
import com.google.common.base.Predicate;

/**
 * IASScope implementation for class & interface Necessary to implement AS3 name
 * resolution semantics correctly because we are using 1 scope to represent
 * instance and static scopes. Does not override findAllDefinition methods, as
 * they return everything that matches in the entire scope chain.
 */
public class TypeScope extends ASScope
{
    private static final StaticPredicate STATIC_ONLY_PREDICATE = new StaticPredicate(true);
    private static final StaticPredicate INSTANCE_ONLY_PREDICATE = new StaticPredicate(false);

    public TypeScope(ASScope containingScope, ScopedBlockNode block, TypeDefinitionBase owningType)
    {
        super(containingScope, block);
        this.owningType = owningType;

        initScopes(containingScope);
    }

    public TypeScope(ASScope containingScope, TypeDefinitionBase owningType)
    {
        super(containingScope);
        this.owningType = owningType;

        initScopes(containingScope);
    }

    private void initScopes(ASScope containingScope)
    {
        staticScope = createStaticScope(containingScope);
        instanceScope = createInstanceScope();
    }

    private StaticScope createStaticScope(ASScope containingScope)
    {
        // Static scope is contained by whatever scope contains the TypeScope
        return new StaticScope(containingScope, this);
    }

    private InstanceScope createInstanceScope()
    {
        // Instance scope always has the static scope as it's containing scope.
        return new InstanceScope(staticScope, this);
    }

    private final TypeDefinitionBase owningType;

    private StaticScope staticScope;
    private InstanceScope instanceScope;

    private boolean needsProtected;
    
    /**
     * Get the scope of the super class - this method will do the right thing depending on
     * if this scope is the instance of static scope.
     * @param project  the Project to resolve things in
     * @return         the scope to use as the scope of the super class.  This may be the instance
     *                 scope of 'Class' (for static scopes), or the instance scope of the base class (for instance scopes),
     *                 or the Type Scope of the baseclass if we are looking for static+instance scopes
     */
    public ASScope resolveSuperScope(ICompilerProject project, ScopeKind kind)
    {
        ASScope result = null;
        if ( owningType instanceof IClassDefinition)
        {
            if( kind == ScopeKind.STATIC )
            {
                // For a static scope, the base scope will be the instance scope of 'Class'
                ITypeDefinition typeDef = project.getBuiltinType(IASLanguageConstants.BuiltinType.CLASS);
                if( typeDef != null )
                {
                    IASScope s = typeDef.getContainedScope();
                    if( s instanceof TypeScope )
                        result = ((TypeScope) s).getInstanceScope();
                }
            }
            else
            {
                // Not static, so resolve the super class and get it's scope
                IClassDefinition superClass = ((IClassDefinition) owningType).resolveBaseClass(project);
                if( superClass instanceof TypeDefinitionBase)
                {
                    IASScope superScope = superClass.getContainedScope();
                    if( superScope instanceof TypeScope )
                    {
                        switch (kind)
                        {
                            case INSTANCE_AND_STATIC:
                                // CM only - if we started with a plain old TypeScope, then just return the super
                                // TypeScope
                                result = (ASScope) superScope;
                                break;
                            case INSTANCE:
                                // Want the instance scope, so grab the instance scope from the base classes contained
                                // scope
                                result = ((TypeScope) superScope).getInstanceScope();
                                break;
                            default:
                                // nothing to do - STATIC was handled above in the if check
                                break;
                        }
                    }
                }
            }
        }
        return result;
    }
    /**
     * Adds the specified definition to this scope.
     * 
     * @param definition The IDefinition to be added.
     */
    @Override
    public void addDefinition(IDefinition definition)
    {
        if (definition != null)
        {
            addDefinitionToStore(definition);

            // Set up the containing scope to correctly point to the static or instance scopes
            if (definition instanceof DefinitionBase)
            {
                boolean isStatic = definition.isStatic();
                final ASScope containingScope;
                if (isStatic)
                {
                    containingScope = getStaticScope();
                }
                else
                {
                    containingScope = getInstanceScope();
                    if (!needsProtected)
                    {
                        INamespaceDefinition protectedNameSpace = owningType.getProtectedNamespaceReference();
                        needsProtected = (protectedNameSpace != null) && (protectedNameSpace.equals(definition.getNamespaceReference()));
                    }
                }
                ((DefinitionBase)definition).setContainingScope(containingScope);

            }
        }
    }

    @Override
    protected void getPropertyForScopeChain(CompilerProject project, Collection<IDefinition> defs, String baseName, NamespaceSetPredicate namespaceSet, boolean findAll)
    {
        getPropertyForScopeChain(project, defs, baseName, namespaceSet, findAll, ScopeKind.INSTANCE_AND_STATIC);
    }

    /**
     * Enum to determine what kind of scope we are emulating.
     */
    static enum ScopeKind
    {
        /**
         * The classic CodeModel view of a class scope as a single scope
         * that contains both instance and static members.
         */
        INSTANCE_AND_STATIC,
        
        /**
         * A filtered view of a class scope that contains only instance members,
         * for use by the compiler.
         */
        INSTANCE,
        
        /**
         * A filtered view of a class scope that contains only static members,
         * for use by the compiler.
         */
        STATIC;

        /**
         * Should this kind of scope find instance members?
         */
        boolean findInstance()
        {
            return this == INSTANCE || this == INSTANCE_AND_STATIC;
        }

        /**
         * Should this kind of scope find static members?
         */
        boolean findStatics()
        {
            return this == STATIC || this == INSTANCE_AND_STATIC;
        }
        
        /**
         * Should this kind of scope find the specified definition?
         */
        boolean findDefinition(IDefinition definition)
        {
            if (this == INSTANCE)
            {
                // An INSTANCE scope sees only non-static members.
                return !definition.isStatic();
            }
            else if (this == STATIC)
            {
                // A STATIC scope sees only static members.
                return definition.isStatic();
            }
            else if (this == INSTANCE_AND_STATIC)
            {
                // An INSTANCE_AND_STATIC scope sees all members.
                return true;
            }
            else
            {
                assert false : "Unknown ScopeKind";
                return false;
            }
        }
    }

    void getPropertyForScopeChain(CompilerProject project, Collection<IDefinition> defs, String baseName, NamespaceSetPredicate nsPred, boolean findAll, ScopeKind lookupKind)
    {
        ArrayList<ITypeDefinition> types = new ArrayList<ITypeDefinition>();
        for (ITypeDefinition type : owningType.typeIteratable(project, false))
        {
            types.add(type);
        }

        int originalDefCount = defs.size();

        // Have to do instances first, in case we're looking for instance and statics
        if (lookupKind.findInstance())
        {
            // We will need to propagate protected namespaces up the chain, but only
            // if we were passed a namespace set that implies "protected" is open

            boolean needProtectedNS = nsPred.containsNamespace(owningType.getProtectedNamespaceReference());

            Collection<IDefinition> iDefs = new FilteredCollection<IDefinition>(INSTANCE_ONLY_PREDICATE, defs);
            for (ITypeDefinition type : types)
            {
                ASScope typeScope = (ASScope)type.getContainedScope();
                if( needProtectedNS )
                {
                    nsPred.setExtraNamespace(type.getProtectedNamespaceReference());
                }

                typeScope.getLocalProperty(project, iDefs, baseName, true);
                if ((!findAll) && (defs.size() > originalDefCount)) {
                    return;
                }
            }
        }
        if (lookupKind.findStatics())
        {
            Collection<IDefinition> sDefs = new FilteredCollection<IDefinition>(STATIC_ONLY_PREDICATE, defs);
            for (ITypeDefinition type : types)
            {
                ASScope typeScope = (ASScope)type.getContainedScope();
                typeScope.getLocalProperty(project, sDefs, baseName, true);
                if ((!findAll) && (defs.size() > originalDefCount)) {
                    return;
                }
            }
        }
    }

    @Override
    protected void getPropertyForMemberAccess(CompilerProject project, Collection<IDefinition> defs, String baseName, NamespaceSetPredicate namespaceSet, boolean findAll)
    {
        getPropertyForMemberAccess(project, defs, baseName, namespaceSet, findAll, ScopeKind.INSTANCE_AND_STATIC);
    }

    protected void getPropertyForMemberAccess(CompilerProject project, Collection<IDefinition> defs, String baseName, NamespaceSetPredicate namespaceSet, boolean findAll, ScopeKind lookupKind)
    {
        int originalDefCount = defs.size();
        boolean needProtectedNamespaces = namespaceSet == ASScopeBase.allNamespacesSet || namespaceSet.containsNamespace(owningType.getProtectedNamespaceReference());

        NamespaceSetPredicate nsPred = namespaceSet;

        Collection<IDefinition> iDefs = new FilteredCollection<IDefinition>(INSTANCE_ONLY_PREDICATE, defs);
        if (lookupKind.findInstance())
        {
            for (ITypeDefinition type : owningType.typeIteratable(project, false))
            {
                ASScope typeScope = (ASScope)type.getContainedScope();
                if (needProtectedNamespaces)
                {
                    nsPred.setExtraNamespace(type.getProtectedNamespaceReference());
                }

                typeScope.getLocalProperty(project,
                                            iDefs,
                                            baseName,
                                            true);

                if ((defs.size() > originalDefCount) && (!findAll))
                    return;
            }
        }
        if (lookupKind.findStatics())
        {
            Collection<IDefinition> sDefs = new FilteredCollection<IDefinition>(STATIC_ONLY_PREDICATE, defs);
            for (ITypeDefinition type : owningType.staticTypeIterable(project, false))
            {
                if (type == null)
                {
                    continue;
                }
                ASScope typeScope = (ASScope)type.getContainedScope();
                typeScope.getLocalProperty(project,
                                            // Only lookup static properties in this scope - for any inherited scopes, we should lookup instance properties
                                            // because C$ (class) extends Class (instance), not Class$ (class)
                                            (typeScope == this) ? sDefs : iDefs,
                                            baseName,
                                            true);

                if ((defs.size() > originalDefCount) && (!findAll))
                    return;
            }
        }
    }

    /**
     * Helper to calculate filtered collections for this scope and the base
     * scopes. This varies because for static lookups, we want to check this
     * scope for "static" properties, but we want to check the base scopes for
     * "instance" properties
     * 
     * @param defs The collection to wrap with the filter
     * @param lookupKind The kind of lookup we're performing
     * @return A MemberAccessCollections with 2 Collections: one to use for this
     * scope, and another to use for base scopes. When we are looking up
     * instance properties, or instance + static properties, then the 2
     * collections will be the same When we are looking up static properties,
     * then the 2 collections will differ (the first one will find only statics,
     * but the second will find only instance properties).
     */
    private MemberAccessCollections getFilteredCollectionsForMemberAccess(Collection<IDefinition> defs, ScopeKind lookupKind)
    {
        MemberAccessCollections p = null;
        if (lookupKind.findInstance() && lookupKind.findStatics())
        {
            // Do nothing, we want both statics and instances
            p = new MemberAccessCollections(defs, defs);
        }
        else if (lookupKind.findStatics())
        {
            // Only lookup static properties in this scope - for any inherited scopes, we should lookup instance properties
            // because C$ (class) extends Class (instance), not Class$ (class)
            p = new MemberAccessCollections(
                    new FilteredCollection<IDefinition>(STATIC_ONLY_PREDICATE, defs),
                    new FilteredCollection<IDefinition>(INSTANCE_ONLY_PREDICATE, defs));
        }
        else if (lookupKind.findInstance())
        {
            Collection<IDefinition> c = new FilteredCollection<IDefinition>(INSTANCE_ONLY_PREDICATE, defs);
            p = new MemberAccessCollections(c, c);
        }
        return p;
    }

    /**
     * Helper class to return 2 collections - my kingdom for a struct, or
     * generic Pair class
     */
    private static class MemberAccessCollections
    {
        public MemberAccessCollections(Collection<IDefinition> thisDefs, Collection<IDefinition> baseDefs)
        {
            this.thisScopeCollection = thisDefs;
            this.baseScopeCollection = baseDefs;
        }

        public final Collection<IDefinition> thisScopeCollection;
        public final Collection<IDefinition> baseScopeCollection;
    }

    @Override
    public void getAllPropertiesForScopeChain(CompilerProject project, Collection<IDefinition> defs, Set<INamespaceDefinition> namespaceSet)
    {
        getAllPropertiesForScopeChain(project, defs, namespaceSet, ScopeKind.INSTANCE_AND_STATIC);
    }

    @Override
    public void getAllPropertiesForMemberAccess(CompilerProject project, Collection<IDefinition> defs, Set<INamespaceDefinition> namespaceSet)
    {
        getAllPropertiesForMemberAccess(project, defs, namespaceSet, ScopeKind.INSTANCE_AND_STATIC);
    }

    protected void getAllPropertiesForMemberAccess(CompilerProject project, Collection<IDefinition> defs, Set<INamespaceDefinition> namespaceSet, ScopeKind lookupKind)
    {
        ArrayList<ITypeDefinition> types = new ArrayList<ITypeDefinition>();
        // For static property lookup we have a different inheritance chain.  
        // Class objects extend Class, not whatever is specified in the extends clause (that only applies to the instance objects)
        //
        // class C{}
        //   C$ (the class object) extends Class
        //   C (instance object) extends Object
        //

        for (ITypeDefinition type : lookupKind == ScopeKind.STATIC ?
                owningType.staticTypeIterable(project, false) :
                owningType.typeIteratable(project, false))
        {
            types.add(type);
        }

        MemberAccessCollections p = getFilteredCollectionsForMemberAccess(defs, lookupKind);
        Collection<IDefinition> thisScopeDefs = p.thisScopeCollection;
        Collection<IDefinition> baseScopeDefs = p.baseScopeCollection;

        boolean needsProtected = false;
        if (namespaceSet != null)
            needsProtected = namespaceSet.contains(owningType.getProtectedNamespaceReference());

        for (ITypeDefinition type : types)
        {
            ASScope typeScope = (ASScope)type.getContainedScope();
            INamespaceDefinition protectedNamespace = null;
            if (needsProtected)
            {
                protectedNamespace = type.getProtectedNamespaceReference();
            }
            // Only lookup static properties in this scope - for any inherited scopes, we should lookup instance properties
            // because C$ (class) extends Class (instance), not Class$ (class)
            typeScope.getAllLocalProperties(project, typeScope == this ? thisScopeDefs : baseScopeDefs, namespaceSet, protectedNamespace);
        }
    }

    protected void getAllPropertiesForScopeChain(CompilerProject project, Collection<IDefinition> defs, Set<INamespaceDefinition> namespaceSet, ScopeKind lookupKind)
    {
        ArrayList<ITypeDefinition> types = new ArrayList<ITypeDefinition>();
        for (ITypeDefinition type : owningType.typeIteratable(project, false))
        {
            types.add(type);
        }
        boolean needsProtected = false;
        if (namespaceSet != null)
            needsProtected = namespaceSet.contains(owningType.getProtectedNamespaceReference());

        defs = getFilteredCollectionForScopeChainLookup(defs, lookupKind);

        for (ITypeDefinition type : types)
        {
            ASScope typeScope = (ASScope)type.getContainedScope();
            INamespaceDefinition protectedNamespace = null;
            if (needsProtected)
            {
                protectedNamespace = type.getProtectedNamespaceReference();
            }
            typeScope.getAllLocalProperties(project, defs, namespaceSet, protectedNamespace);
        }
    }

    /**
     * Helper method to apply the appropriate predicate to the Collection passed
     * in.
     * 
     * @param defs the target collection
     * @param lookupKind what kind of lookup we're performing
     * @return A Collection with the appropriate filters to implement the lookup
     * 1. If looking for statics and instances, do nothing, just return the
     * original collection 2. If looking only for statics, then return a
     * filtered collection, which wraps the collection passed in, and will
     * filter out non-static definitions 3. If looking only for instance
     * properties, then return a filtered collection, which wraps the collection
     * passed in, and will filter out any static definitions
     */
    private Collection<IDefinition> getFilteredCollectionForScopeChainLookup(Collection<IDefinition> defs, ScopeKind lookupKind)
    {
        if (lookupKind.findInstance() && lookupKind.findStatics())
        {
            // do nothing
        }
        else if (lookupKind.findStatics())
        {
            defs = new FilteredCollection<IDefinition>(STATIC_ONLY_PREDICATE, defs);
        }
        else if (lookupKind.findInstance())
        {
            defs = new FilteredCollection<IDefinition>(INSTANCE_ONLY_PREDICATE, defs);
        }
        return defs;
    }

    @Override
    protected boolean namespaceSetSameAsContainingScopeNamespaceSet()
    {
        return false;
    }

    @Override
    public void addImplicitOpenNamespaces(CompilerProject compilerProject, Set<INamespaceDefinition> result)
    {
        addImplicitOpenNamespaces(compilerProject, result, ScopeKind.INSTANCE_AND_STATIC);
    }

    protected void addImplicitOpenNamespaces(CompilerProject compilerProject, Set<INamespaceDefinition> result, ScopeKind lookupKind)
    {
        IDefinition currentDefinition = this.getDefinition();
        if (currentDefinition instanceof ClassDefinition)
        {
            ClassDefinition initialClassDef = (ClassDefinition)currentDefinition;
            // Private namespace always added - same private namespace can be used for static and instance properties
            result.add(initialClassDef.getPrivateNamespaceReference());

            // Protected namespaces differ for static & instance scopes
            if (lookupKind.findInstance())
            {
                result.add(initialClassDef.getProtectedNamespaceReference());
            }
            // Add all the static protected namespaces to the namespace set.
            // Instance protected namespaces are added to the namespace set
            // in TypeScope as we walk up the class hierarchy.  The instance
            // protected namespace needs to change as we walk up the class hierarchy.
            if (lookupKind.findStatics())
            {
                for (IClassDefinition classDef : initialClassDef.classIterable(compilerProject, true))
                    result.add(((ClassDefinition)classDef).getStaticProtectedNamespaceReference());
            }

        }
        else if (currentDefinition instanceof InterfaceDefinition)
        {
            InterfaceDefinition interfaceDefinition = (InterfaceDefinition)currentDefinition;
            result.add(interfaceDefinition.getInterfaceNamespaceReference());
        }
        else if (currentDefinition instanceof AppliedVectorDefinition)
        {
            compilerProject.addGlobalUsedNamespacesToNamespaceSet(result);
            for (IClassDefinition classDef : ((AppliedVectorDefinition)currentDefinition).classIterable(compilerProject, true))
                result.add(((ClassDefinitionBase)classDef).getStaticProtectedNamespaceReference());
        }
    }

    @Override
    public String getContainingSourcePath(String baseName, ICompilerProject project)
    {
        return super.getContainingSourcePath(owningType.getQualifiedName(), project);
    }

    /**
     * Get the ASScope representing the itraits - this scope will only appear to
     * contain instance properties
     */
    public ASScope getInstanceScope()
    {
        return instanceScope;
    }

    /**
     * Get the ASScope representing the ctraits - this scope will only appear to
     * contain static properties
     */
    public ASScope getStaticScope()
    {
        return staticScope;
    }

    @Override
    public void setContainingScope(ASScope containingScope)
    {
        super.setContainingScope(containingScope);
        if (staticScope != null)
            staticScope.setContainingScope(containingScope);
    }

    /**
     * Wrapper class that makes a TypeScope appear to be an instance scope - it
     * will filter out all static properties, and appear to only contain
     * instance properties
     */
    private static class InstanceScope extends ScopeView
    {
        InstanceScope(ASScope containingScope, TypeScope typeScope)
        {
            super(containingScope, typeScope);
        }

        @Override
        protected ScopeKind getScopeKind()
        {
            return ScopeKind.INSTANCE;
        }
    }

    /**
     * Wrapper class that makes a TypeScope appear to be an static scope - it
     * will filter out all instance properties, and appear to only contain
     * static properties
     */
    private static class StaticScope extends ScopeView
    {
        StaticScope(ASScope containingScope, TypeScope typeScope)
        {
            super(containingScope, typeScope);
        }

        @Override
        protected ScopeKind getScopeKind()
        {
            return ScopeKind.STATIC;
        }

    }

    public boolean getNeedsProtected()
    {
        return needsProtected;
    }

    private static class StaticPredicate implements Predicate<IDefinition>
    {
        private boolean findStatics;

        public StaticPredicate(boolean b)
        {
            this.findStatics = b;
        }

        @Override
        public boolean apply(IDefinition definition)
        {
            return findStatics == definition.isStatic();
        }
    }
}
