/*
 *
 *  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 org.apache.royale.compiler.common.DependencyType;
import org.apache.royale.compiler.constants.IASLanguageConstants;
import org.apache.royale.compiler.definitions.IDefinition;
import org.apache.royale.compiler.definitions.IInterfaceDefinition;
import org.apache.royale.compiler.definitions.INamespaceDefinition;
import org.apache.royale.compiler.definitions.references.IResolvedQualifiersReference;
import org.apache.royale.compiler.internal.definitions.AmbiguousDefinition;
import org.apache.royale.compiler.internal.definitions.ClassDefinitionBase;
import org.apache.royale.compiler.internal.definitions.ConstantDefinition;
import org.apache.royale.compiler.internal.definitions.TypeDefinitionBase;
import org.apache.royale.compiler.internal.projects.CompilerProject;
import org.apache.royale.compiler.projects.ICompilerProject;

import com.google.common.collect.MapMaker;

import java.lang.ref.SoftReference;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;

/**
 * Class to manage cached lookups in a given project. Each scope object will
 * have one of these per project the scope object is used in. This class is
 * intended to provide thread safe access to the various caches it maintains.
 * The caches will be maintained via SoftReferences - this is meant to make the
 * caches stick around unless the VM really needs the memory back. The caches
 * can be rebuilt on the fly, so in a worst case scenario where the VM is
 * constantly low on memory, the results should be correct, but performance will
 * be slower.
 */
public class ASScopeCache
{
    /**
     * The concurrency level to use for the various ConcurrentMaps in use by
     * this cache. If the concurrency level is too high, you waste time and
     * space, if it is too low, then you have contention on updates of the map.
     * 4 seems to be a good tradeoff performance wise, and it is unlikely that
     * any individual scope cache is being updated from more than 4 threads
     * simultaneously. If our threading strategy changes (like if we make the CG
     * multithreaded within one file), we may want to revisit this setting.
     */
    private static final int CONCURRENCY_LEVEL = 4;
    private static final MapMaker mapMaker = new MapMaker()
                                            .concurrencyLevel(CONCURRENCY_LEVEL);

    public ASScopeCache(CompilerProject project, ASScope scope)
    {
        this.scope = scope;
        this.project = project;
    }

    private final ASScope scope;
    private final CompilerProject project;

    /**
     * Cache results of unqualified lookups over the scope chain
     * (ASScopeBase.findProperty). This is for caching the results of
     * ASScope.findProperty().
     */
    private SoftReference<ConcurrentMap<String, IDefinition>> findPropCache;

    /**
     * Cache results of lookups of qualified names over the scope chain
     * (ASScopeBase.findPropertyQualified).
     */
    private SoftReference<ConcurrentMap<QName, IDefinition>> findPropQualifiedCache;

    /**
     * Cache the set of open namespaces
     */
    private SoftReference<Set<INamespaceDefinition>> openNamespaceCache = null;

    /**
     * Cache the open namespace set per name
     */
    private SoftReference<ConcurrentMap<String, Set<INamespaceDefinition>>> namespacesForNameCache;

    private SoftReference<ConcurrentMap<IResolvedQualifiersReference, IDefinition>> multinameLookupCache;

    /**
     * Cache the compile time values of constants
     */
    private SoftReference<ConcurrentMap<IDefinition, Object>> constValueLookupCache;

    /**
     * Cache the needs Event dispatch flag
     */
    private Boolean needsEventDispatcherCache;

    /**
     * Cache the extended or implemented interfaces of an interface or class.
     */
    private SoftReference<IInterfaceDefinition[]> interfacesCache;

    /**
     * Cache the builtin types we've already added dependencies on
     */
    private SoftReference<Set<IASLanguageConstants.BuiltinType>> builtinTypeDependencyCache;

    /**
     * Version of findProperty that uses a cache. Checks the cache first, and
     * only queries the scope if the we don't have a cached result.
     * 
     * @param scope The scope to perform the lookup in
     * @param name Name of the property to find
     * @param cache ASDefinitionCache to use for the lookup - this is only used
     * to get at the ICompilerProject
     * @param dt Which type of dependency to introduce when we do the lookup
     * @return The IDefinition for the property, or null if it wasn't found
     */
    IDefinition findProperty(String name, DependencyType dt)
    {
        ConcurrentMap<String, IDefinition> map = getScopeChainMap();

        IDefinition result = map.get(name);
        if (result != null)
        {
            assert result.isInProject(project);
            // We found a cached result - we're done
            return result;
        }

        // It is possible for 2+ threads to get in here for the same name.
        // This is intentional - the worst that happens is that we duplicate the resolution work
        // the benefit is that we avoid any sort of locking, which was proving expensive (time wise,
        // and memory wise).

        IDefinition def = null;
        Set<INamespaceDefinition> namespaceSet = scope.getNamespaceSetForName(project, name);
        // Look for the definition in the scope
        List<IDefinition> defs = scope.findProperty(project, name, namespaceSet, dt);
        switch (defs.size())
        {
            case 0:
                // No definition found!
                def = null;
                break;
            case 1:
                // found single definition!
                def = defs.get(0);
                assert def.isInProject(project);
                break;
            default:
                IDefinition d = AmbiguousDefinition.resolveAmbiguities(project, defs);
                if (d != null)
                    def = d;
                else {
                    if (defs.size() == 2) 
                    {
                        def = project.doubleCheckAmbiguousDefinition(scope, name, defs.get(0), defs.get(1));
                        if (def != null)
                            return def;
                    }
                    def = AmbiguousDefinition.get();
                }
        }
        if (def != null)
        {
            assert def.isInProject(project);
            assert result == null;
            // If we have a non-null dependency type
            // then we can cache the result of the name resolution.
            // If the dependency type is null we can't cache the name
            // resolution result, because the name resolution cache will not
            // be properly invalidated when the file containing the definition changes.
            if (dt != null)
            {
                result = map.putIfAbsent(name, def);
                if (result == null)
                    result = def;
            }
            else
            {
                result = def;
            }
        }
        return result;

    }

    private ConcurrentMap<String, IDefinition> getScopeChainMap()
    {
        ConcurrentMap<String, IDefinition> map = findPropCache != null ? findPropCache.get() : null;
        if (map == null)
        {
            synchronized (this)
            {
                // Check again, in case another thread updated the map first
                map = findPropCache != null ? findPropCache.get() : null;
                if (map == null)
                {
                    map = mapMaker.<String, IDefinition> makeMap();
                    findPropCache = new SoftReference<ConcurrentMap<String, IDefinition>>(map);
                }
            }
        }
        return map;
    }

    private ConcurrentMap<QName, IDefinition> getQualifiedScopeChainMap()
    {
        ConcurrentMap<QName, IDefinition> map = findPropQualifiedCache != null ? findPropQualifiedCache.get() : null;
        if (map == null)
        {
            synchronized (this)
            {
                // Check again, in case another thread updated the map first
                map = findPropQualifiedCache != null ? findPropQualifiedCache.get() : null;
                if (map == null)
                {
                    map = mapMaker.<QName, IDefinition> makeMap();
                    findPropQualifiedCache = new SoftReference<ConcurrentMap<QName, IDefinition>>(map);
                }
            }
        }
        return map;
    }

    /**
     * Version of findPropertyQualified that uses a cache. Checks the cache
     * first, and only queries the scope if the we don't have a cached result.
     * 
     * @param scope The scope to perform the lookup in
     * @param name Name of the property to find
     * @param cache ASDefinitionCache to use for the lookup - this is only used
     * to get at the ICompilerProject
     * @param dt Which type of dependency to introduce when we do the lookup
     * @return The IDefinition for the property, or null if it wasn't found
     */
    IDefinition findPropertyQualified(INamespaceDefinition qualifier, String name,
                                      DependencyType dt)
    {
        QName qname = new QName(name, qualifier);
        ConcurrentMap<QName, IDefinition> map = getQualifiedScopeChainMap();

        IDefinition result = map.get(qname);
        if (result != null)
        {
            assert result.isInProject(project);
            // We found a cached result - we're done
            return result;
        }

        // If we get this far, then we did not find a cached entry
        // It is possible for 2+ threads to get in here for the same name.
        // This is intentional - the worst that happens is that we duplicate the resolution work
        // the benefit is that we avoid any sort of locking, which was proving expensive (time wise,
        // and memory wise).

        IDefinition def;
        // Look for the definition in the scope
        Set<INamespaceDefinition> namespaceSet = Collections.singleton(qualifier);
        List<IDefinition> defs = scope.findProperty(project, name, namespaceSet, dt);
        switch (defs.size())
        {
            case 0:
                def = null;
                break;

            case 1:
                def = defs.get(0);
                assert def.isInProject(project);
                break;

            default:
                IDefinition d = AmbiguousDefinition.resolveAmbiguities(project, defs);
                if (d != null)
                    def = d;
                else
                    def = AmbiguousDefinition.get();
                break;
        }
        if (def != null)
        {
            assert def.isInProject(project);
            assert result == null;
            // If we have a non-null dependency type
            // then we can cache the result of the name resolution.
            // If the dependency type is null we can't cache the name
            // resolution result, because the name resolution cache will not
            // be properly invalidated when the file containing the definition changes.
            if (dt != null)
            {
                result = map.putIfAbsent(qname, def);
                if (result == null)
                    result = def;
            }
            else
            {
                result = def;
            }
        }
        return result;
    }

    /**
     * Resolves the specified reference to a definition and adds a dependency to
     * the dependency graph if needed.
     * <p>
     * This method is only public so that the implementation of
     * IResolveQualifiersReference.resolve can call it. This method should only
     * be called from the implementation of {@link IResolvedQualifiersReference}.
     * 
     * @param ref The reference to resolve.
     * @param dt The type of dependency to add if a new edge needs to be added
     * to the dependency graph.
     * @return The definition the reference resolves to, null, or the ambiguous
     * definition.
     */
    public IDefinition findPropertyMultiname(IResolvedQualifiersReference ref, DependencyType dt)
    {
        ConcurrentMap<IResolvedQualifiersReference, IDefinition> cache = getMultinameLookupMap();
        IDefinition result = cache.get(ref);
        if (result != null)
            return result;

        IDefinition def;

        // Look for the definition in the scope
        List<IDefinition> defs = scope.findProperty(project, ref.getName(), ref.getQualifiers(), dt);
        switch (defs.size())
        {
            case 0:
                // No definition found!
                def = null;
                break;
            case 1:
                // found single definition!
                def = defs.get(0);
                assert def.isInProject(project);
                break;
            default:
                IDefinition d = AmbiguousDefinition.resolveAmbiguities(project, defs);
                if (d != null)
                    def = d;
                else
                    def = AmbiguousDefinition.get();
        }
        if (def != null)
        {
            assert def.isInProject(project);
            assert result == null;
            // If we have a non-null dependency type
            // then we can cache the result of the name resolution.
            // If the dependency type is null we can't cache the name
            // resolution result, because the name resolution cache will not
            // be properly invalidated when the file containing the definition changes.
            if (dt != null)
            {
                result = cache.putIfAbsent(ref, def);
                if (result == null)
                    result = def;
            }
            else
            {
                result = def;
            }
        }
        return result;
    }

    /**
     * Version of getNamespaceSet that caches the results.
     * 
     * @return the namespace set to use for unqualified lookups in this scope
     */
    Set<INamespaceDefinition> getNamespaceSet()
    {
        Set<INamespaceDefinition> nsSet = openNamespaceCache != null ? openNamespaceCache.get() : null;
        if (nsSet != null)
            return nsSet;

        nsSet = scope.getNamespaceSetImpl(project);
        openNamespaceCache = new SoftReference<Set<INamespaceDefinition>>(nsSet);
        return nsSet;
    }

    private ConcurrentMap<String, Set<INamespaceDefinition>> getNamespacesForNameMap()
    {
        ConcurrentMap<String, Set<INamespaceDefinition>> map = namespacesForNameCache != null ? namespacesForNameCache.get() : null;
        if (map == null)
        {
            synchronized (this)
            {
                // Check again, in case another thread updated the map first
                map = namespacesForNameCache != null ? namespacesForNameCache.get() : null;
                if (map == null)
                {
                    map = mapMaker.<String, Set<INamespaceDefinition>> makeMap();
                    namespacesForNameCache = new SoftReference<ConcurrentMap<String, Set<INamespaceDefinition>>>(map);
                }
            }
        }
        return map;
    }

    private ConcurrentMap<IResolvedQualifiersReference, IDefinition> getMultinameLookupMap()
    {
        ConcurrentMap<IResolvedQualifiersReference, IDefinition> map = multinameLookupCache != null ? multinameLookupCache.get() : null;
        if (map == null)
        {
            synchronized (this)
            {
                // Check again, in case another thread updated the map first
                map = multinameLookupCache != null ? multinameLookupCache.get() : null;
                if (map == null)
                {
                    map = mapMaker.<IResolvedQualifiersReference, IDefinition> makeMap();
                    multinameLookupCache = new SoftReference<ConcurrentMap<IResolvedQualifiersReference, IDefinition>>(map);
                }
            }
        }
        return map;
    }

    private ConcurrentMap<IDefinition, Object> getConstantValueLookupMap()
    {
        ConcurrentMap<IDefinition, Object> map = constValueLookupCache != null ? constValueLookupCache.get() : null;
        if (map == null)
        {
            synchronized (this)
            {
                // Check again, in case another thread updated the map first
                map = constValueLookupCache != null ? constValueLookupCache.get() : null;
                if (map == null)
                {
                    map = mapMaker.<IDefinition, Object> makeMap();
                    constValueLookupCache = new SoftReference<ConcurrentMap<IDefinition, Object>>(map);
                }
            }
        }
        return map;
    }

    private Set<IASLanguageConstants.BuiltinType> getBuiltinTypeMap()
    {
        Set<IASLanguageConstants.BuiltinType> set = builtinTypeDependencyCache != null ? builtinTypeDependencyCache.get() : null;
        if (set == null)
        {
            synchronized (this)
            {
                // Check again, in case another thread updated the set first
                set = builtinTypeDependencyCache != null ? builtinTypeDependencyCache.get() : null;
                if (set == null)
                {
                    set = Collections.newSetFromMap(mapMaker.<IASLanguageConstants.BuiltinType, Boolean> makeMap());
                    builtinTypeDependencyCache = new SoftReference<Set<IASLanguageConstants.BuiltinType>>(set);
                }
            }
        }
        return set;
    }

    /**
     * Determines if the {@link TypeScope} this cache is associated with needs
     * an implicit 'implements flash.events.IEventDispatcher' due to the class,
     * or some of its members being marked bindable. The result of this method
     * is cached.
     * <p>
     * Only
     * {@link ClassDefinitionBase#needsEventDispatcher(ICompilerProject)}
     * should call this method. All other code should call
     * {@link ClassDefinitionBase#needsEventDispatcher(ICompilerProject)}.
     * 
     * @return true if this class needs to add IEventDispatcher to its interface
     * list, and should implement the IEventDispatcher methods.
     */
    public boolean needsEventDispatcher()
    {
        assert scope instanceof TypeScope : "needsEventDispatcher should only be called on scope cache's for TypeScopes!";
        assert scope.getDefinition() instanceof ClassDefinitionBase : "needsEventDispatcher should only be called on scope cache's for the scopes contained by classes!";
        Boolean valueObject = needsEventDispatcherCache;
        if (valueObject != null)
            return valueObject.booleanValue();
        synchronized (this)
        {
            // Check again, in case another thread updated the value first
            valueObject = needsEventDispatcherCache;
            if (valueObject != null)
                return valueObject.booleanValue();
            boolean computedValue = ((ClassDefinitionBase)scope.getDefinition()).computeNeedsEventDispatcher(project);
            needsEventDispatcherCache = computedValue;
            return computedValue;
        }
    }

    public IInterfaceDefinition[] resolveInterfaces()
    {
        assert scope instanceof TypeScope : "resolveInterfacesImpl should only be called on scope cache's for TypeScopes!";
        assert scope.getDefinition() instanceof TypeDefinitionBase : "resolveInterfacesImpl should only be called on scope cache's for the scopes contained by types!";

        IInterfaceDefinition[] interfs = interfacesCache != null ? interfacesCache.get() : null;

        if( interfs != null )
            return interfs;

        synchronized (this)
        {
            // check again in case another thread updated the value first
            interfs = interfacesCache != null ? interfacesCache.get() : null;
            if( interfs != null )
                return interfs;

            interfs = ((TypeDefinitionBase)scope.getDefinition()).resolveInterfacesImpl(project);
            interfacesCache = new SoftReference<IInterfaceDefinition[]>(interfs);
            return interfs;
        }
    }
    /**
     * Version of getNamespaceSetForName that caches the results - this is used
     * to get the namespace set to use to lookup a name - If name is an
     * explicitly imported definition, then the namespace set will consist of
     * the package name from the import(s) plus the open namespace set. If name
     * was not explitly imported then the open namespace set will be returned
     * 
     * @param name the name to lookup
     * @return the namespace set to use to lookup name
     */
    Set<INamespaceDefinition> getNamespaceSetForName(String name)
    {
        ConcurrentMap<String, Set<INamespaceDefinition>> map = getNamespacesForNameMap();

        Set<INamespaceDefinition> result = map.get(name);
        if (result != null)
        {
            // We found a cached result - we're done
            return result;
        }

        // It is possible for 2+ threads to get in here for the same name.
        // This is intentional - the worst that happens is that we duplicate the resolution work
        // the benefit is that we avoid any sort of locking, which was proving expensive (time wise,
        // and memory wise).

        Set<INamespaceDefinition> newResult = scope.getNamespaceSetForNameImpl(project, name);
        result = map.putIfAbsent(name, newResult);
        if (result == null)
            result = newResult;
        return result;

    }

    /**
     * Version of addDependencyOnBuiltinType that uses a cache to determine if the dependency actually
     * needs to be added.  The act of adding a dependency is somewhat expensive, so using a cache
     * is much faster
     *
     * @param builtinType   the type to depend on
     * @param dt            the type of Dependency to add
     */
    void addDependencyOnBuiltinType(IASLanguageConstants.BuiltinType builtinType, DependencyType dt)
    {
        Set<IASLanguageConstants.BuiltinType> set = getBuiltinTypeMap();

        if( set.contains(builtinType) )
        {
            // We found a cached result - we're done
            return;
        }

        // It is possible for 2+ threads to get in here for the same name.
        // This is intentional - the worst that happens is that we duplicate the dependency work
        // the benefit is that we avoid any sort of locking, which was proving expensive (time wise,
        // and memory wise).
        scope.addDependencyOnBuiltinTypeImpl(project, builtinType, dt);
        set.add(builtinType);

        return;
    }

    /**
     * Used to cache no constant value results for the const value cache.
     * Computing the value is expensive whether there is a value or not, and
     * many constants will not have compile time constant values, so we want to
     * cache those as well.
     */
    private static final Object NO_CONST_VALUE = new Object();

    /**
     * get the constant value for the given const definition. If a compile time
     * constant can not be computed for the definition, this will return null.
     * 
     * @param constDef The constant definition you want the constant value of
     * @return The constant value, or null if a compie time constant could not
     * be computed
     */
    public Object getConstantValue(ConstantDefinition constDef)
    {
        ConcurrentMap<IDefinition, Object> map = getConstantValueLookupMap();

        Object result = map.get(constDef);
        if (result != null)
        {
            // We found a cached result - we're done
            if (result == NO_CONST_VALUE)
                return null;
            return result;
        }

        // It is possible for 2+ threads to get in here for the same name.
        // This is intentional - the worst that happens is that we duplicate the resolution work
        // the benefit is that we avoid any sort of locking, which was proving expensive (time wise,
        // and memory wise).

        Object newResult = constDef.resolveValueImpl(project);
        if (newResult == null)
            newResult = NO_CONST_VALUE;
        result = map.putIfAbsent(constDef, newResult);
        if (result == null)
            result = newResult;

        if (result == NO_CONST_VALUE)
            return null;
        else
            return result;
    }

    /**
     * Helper class - to be used as a key for caching qualified name lookups.
     */
    private static class QName
    {
        String name;
        INamespaceDefinition ns;

        QName(String name, INamespaceDefinition ns)
        {
            assert ns != null;
            this.name = name;
            this.ns = ns;
        }

        @Override
        public int hashCode()
        {
            return name.hashCode() ^ ns.hashCode();
        }

        @Override
        public boolean equals(Object o)
        {
            if (o == this)
                return true;
            if (o instanceof QName)
            {
                QName other = (QName)o;
                return name.equals(other.name) && ns.equals(other.ns);
            }
            return false;
        }
    }
}
