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

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.apache.flex.compiler.common.DependencyType;
import org.apache.flex.compiler.common.ISourceLocation;
import org.apache.flex.compiler.common.Multiname;
import org.apache.flex.compiler.definitions.IDefinition;
import org.apache.flex.compiler.internal.config.QNameNormalization;
import org.apache.flex.compiler.internal.definitions.ClassDefinition;
import org.apache.flex.compiler.internal.projects.FlexProject;
import org.apache.flex.compiler.internal.projects.ResourceBundleSourceFileHandler;
import org.apache.flex.compiler.internal.scopes.ASProjectScope;
import org.apache.flex.compiler.internal.units.ResourceBundleCompilationUnit;
import org.apache.flex.compiler.problems.ICompilerProblem;
import org.apache.flex.compiler.problems.ResourceBundleNotFoundForLocaleProblem;
import org.apache.flex.compiler.problems.ResourceBundleNotFoundProblem;
import org.apache.flex.compiler.projects.ICompilerProject;
import org.apache.flex.compiler.units.ICompilationUnit;
import org.apache.flex.compiler.units.requests.IFileScopeRequestResult;

public class ResourceBundleUtils
{
    public static final String CLASS_SUFFIX = "_" + ResourceBundleSourceFileHandler.EXTENSION;

    
    /**
     * Returns the qualified name for the auto generated resource bundle class for a
     * specified locale and a bundleName. These class names, such as
     * "foo.en_US$core_properties", are constructed in such a way that the locale
     * and bundle name can be extracted from them by the following two methods.
     * Note: The framework class mx.managers.SystemManager has similar logic
     * which must be kept in sync with this.
     * 
     * @param locale locale of the resource bundle
     * @param bundleName qualified name of the bundle
     * @return the fully qualified class name for this bundle and locale
     */
    public static String getQualifiedName(String locale, String bundleName)
    {
        String normalizedBundleName = QNameNormalization.normalize(bundleName);
        
        String packageName = Multiname.getPackageNameForQName(normalizedBundleName);
        String className = Multiname.getBaseNameForQName(normalizedBundleName);
        
        StringBuilder sb = new StringBuilder();
        if(packageName != null && packageName.length() > 0)
        {
            sb.append(packageName);
            sb.append(".");
        }
        sb.append(locale);
        sb.append("$");
        sb.append(className);
        sb.append(CLASS_SUFFIX);
        
        return sb.toString();
    }
    
    /**
     * Extracts the locale from the specified qualified name for an auto generated 
     * resource bundle class. Class names for ResourceBundles, such as
     * "foo.en_US$core_properties", are constructed in such a way that the locale
     * and bundle name can be extracted.
     * Note: The framework class mx.managers.SystemManager has similar logic
     * which must be kept in sync with this.
     * 
     * @param qualifiedName the fully qualified class name for a resource bundle
     * @return extracted locale of this qualified name
     */
    public static String getLocale(String qualifiedName)
    {
        String className = Multiname.getBaseNameForQName(qualifiedName);
        
        int indexOf = className.indexOf('$');
        if(indexOf > 0)
        {
            return className.substring(0, indexOf);
        }
        
        return null;
    }   
    
    /**
     * This methods converts a resource bundle qualified name in "dotted syntax"
     * to use "colon syntax". The reason is that Flex SDK expects the resource
     * bundle names in "colon syntax". 
     * [See Flex SDK's ResourceManagerImpl.installCompiledResourceBundle]
     * 
     * Example: foo.bar.xyz will be converted to foo.bar:xyz
     * 
     * @param bundleName qualified name of resource bundle
     * @return qualified name of the resource bundle in "colon syntax".
     */
    public static String convertBundleNameToColonSyntax(String bundleName)
    {
        String normalizedName = QNameNormalization.normalize(bundleName);
        
        String packageName = Multiname.getPackageNameForQName(normalizedName);
        if(packageName == null || packageName.length() == 0)
        {
            return bundleName;
        }
        
        String className = Multiname.getBaseNameForQName(normalizedName);
        
        return packageName + ":" + className;
    }

    
    /**
     * Resolving the references to the specified resourceBundleName and adds the
     * necessary dependency from the specified compilation unit to resolved
     * resource bundle's compilation unit.
     * 
     * @param bundleName resource bundle name
     * @param refCompUnit compilation unit that has a reference to the specified
     * resource bundle name
     * @param location location of the resource bundle's occurrence in the file
     * associated with the specified compilation unit
     * @param errors error collection to collect problems
     */
    public static void resolveDependencies(String bundleName, 
            final ICompilationUnit refCompUnit, 
            final ICompilerProject project, 
            final ISourceLocation location,
            final Collection<ICompilerProblem> errors) throws InterruptedException {
        
        Map<String, ICompilationUnit> qualifiedNameMap = findCompilationUnits(
            bundleName, project, refCompUnit.getAbsoluteFilename(), location, errors);
        for (Map.Entry<String, ICompilationUnit> entry : qualifiedNameMap.entrySet())
        {
            ((FlexProject)project).addDependency(refCompUnit, entry.getValue(), 
                    DependencyType.EXPRESSION, entry.getKey());
        }
    }
    
    /**
     * Find all the compilation units associated with the specified resource bundle name.
     * 
     * @param bundleName name of the resource bundle
     * @param project associated project
     * @param errors error collection to collect problems
     * @return the list of compilation units that exist to handle the specified resource bundle.
     */
    public static Collection<ICompilationUnit> findCompilationUnits(String bundleName,
            final ICompilerProject project,
            final Collection<ICompilerProblem> errors) throws InterruptedException {
        
        return findCompilationUnits(bundleName, project, null, null, errors).values();
    }

    /**
     * Find all the compilation units associated with the specified resource
     * bundle name.
     * 
     * @param bundleName name of the resource bundle
     * @param cach definition cache
     * @param refFilePath path of the file that has a reference to this resource
     * bundle or could be <code>null</code>.
     * @param location location where the reference in the specified file
     * occurred or could be <code>null</code>.
     * @param errors error collection to collect problems
     * @return the map of qualified name and compilation unit pairs that exist to handle the specified
     * resource bundle.
     * @throws InterruptedException
     */
    private static Map<String, ICompilationUnit> findCompilationUnits(String bundleName, 
            final ICompilerProject project,
            final String refFilePath,
            final ISourceLocation location,
            final Collection<ICompilerProblem> errors) throws InterruptedException {
        
        Map<String, ICompilationUnit> compilationUnits = new HashMap<String, ICompilationUnit>();
        
        if (project instanceof FlexProject)
        {
            final String normalizedBundleName = QNameNormalization.normalize(bundleName);
            final FlexProject flexProject = (FlexProject)project;
            final ASProjectScope scope = flexProject.getScope();
            final Collection<String> locales = flexProject.getLocales();
            
            if(locales.size() == 0)
            {
                //If the project's locale is empty, then don't try to resolve this 
                //node because the referenced bundle won't go into swf or swc anyways.
                return Collections.emptyMap();
            }
            
            Collection<String> unresolvedLocales = new ArrayList<String>(locales);
            for(String locale : locales) 
            {   
                //resolve the generated qualified name for the bundle name referenced in this metadata tag and this locale 
                String qualifiedName = ResourceBundleUtils.getQualifiedName(locale, normalizedBundleName);
                
                //find the compilation units for this qualified name
                IDefinition def = scope.findDefinitionByName(qualifiedName);
                if(def != null) 
                {
                    ICompilationUnit compUnit = scope.getCompilationUnitForDefinition(def);

                    if (isValidCompilationUnit(compUnit, project, bundleName))
                    {
                        compilationUnits.put(def.getQualifiedName(), compUnit);
                        unresolvedLocales.remove(locale);
                    }                 
                }
            }
           
            if (unresolvedLocales.size() > 0 || locales.size() == 0) 
            {
                //If we are at this point, it means that either project's locale is empty or 
                //we could not resolve the resource bundle for some or all locales. 
                //The last thing we can check is to see whether we can resolve it using the 
                //pure bundle name (not the generated name that contains locale$..._properties)
                //This is also acceptable because "mx.managers.SystemManager" uses the same logic 
                //to find a resource bundle. First, it searches for the generated class name and then the pure bundle name.
                IDefinition def = scope.findDefinitionByName(normalizedBundleName);
                if(def != null)
                {
                    ICompilationUnit compUnit = scope.getCompilationUnitForDefinition(def);

                    if (isValidCompilationUnit(compUnit, project, bundleName))
                    { 
                        //This means we resolved the reference by using pure bundle name which will apply 
                        //to all locales during runtime, therefore return from this method.
                        compilationUnits.put(def.getQualifiedName(), compUnit);
                        return compilationUnits;
                    } 
                }
                
                // At this point, it means that we were not able to resolve this resource bundle reference.
                // Therefore, report an error.
                if (unresolvedLocales.size() == locales.size())
                {
                    //we couldn't resolve it for any locales or there was no locale, 
                    //so show the generic error which doesn't mention any locales.
                    ResourceBundleNotFoundProblem problem = (location != null) ? 
                            new ResourceBundleNotFoundProblem(location, bundleName) :
                            new ResourceBundleNotFoundProblem(bundleName);
                    errors.add(problem);
                } 
                else 
                {
                    //some of the locales couldn't be resolved so show errors expressing which locales couldn't be resolved.
                    for(String locale : unresolvedLocales) 
                    {
                        ResourceBundleNotFoundForLocaleProblem problem = (location != null) ? 
                                new ResourceBundleNotFoundForLocaleProblem(location, bundleName, locale) :
                                new ResourceBundleNotFoundForLocaleProblem(bundleName, locale);
                        errors.add(problem);
                    }
                }
            }
            
        }
        
        return compilationUnits;
    }

    /**
     * Checks whether the definition associated with the specified compilation
     * unit can be used as a ResourceBundle. A compilation unit is valid if it
     * is associated with a properties file or a class that extends
     * mx.resources.ResourceBundle.
     * 
     * @param compUnit compilation unit to validate
     * @param project owner project
     * @param cache project's cache
     * @return <code>true</code> if the definition associated with the
     * compilation unit can be used as a ResourceBundle, <code>false</code>
     * otherwise.
     * @throws InterruptedException
     */
    private static boolean isValidCompilationUnit(final ICompilationUnit compUnit, 
                                                  final ICompilerProject project,
                                                  final String bundleName) throws InterruptedException
    {
        if (compUnit != null) 
        {
            //If we have a resource bundle compilation unit, then use it.
            if (compUnit instanceof ResourceBundleCompilationUnit)
            {
                //This guarantees that the bundle name used while referencing is in the same syntax 
                //as we are expecting. For example, if the properties file is in a package, as of now, 
                //only "colon" syntax is accepted such as foo.bar:Name. Tehrefore, this check will fail
                //if the user references the bundle with "dotted syntax" such as foo.bar.Name 
                //since Flex SDK does not know how to handle that.
                if (((ResourceBundleCompilationUnit)compUnit).getBundleNameInColonSyntax().equals(bundleName))
                    return true;
            } 
            else 
            {
                IFileScopeRequestResult result = compUnit.getFileScopeRequest().get();

                for (IDefinition def : result.getExternallyVisibleDefinitions())
                {
                    if (def instanceof ClassDefinition)
                    {
                        //check whether class extends ResourceBundle
                        if (project instanceof FlexProject &&
                           ((ClassDefinition)def).isInstanceOf(((FlexProject)project).getResourceBundleClass(), project))
                        {
                            return true;
                        }
                    }
                }
            }
        }

        return false;
    }
}
