/*
 * 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 flex.messaging.util;

import java.io.InputStream;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Iterator;
import java.util.Locale;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;

import flex.messaging.log.Log;
import flex.messaging.log.Logger;
import flex.messaging.log.LogCategories;

/**
 * Implementation of <code>ResourceLoader</code> that loads string resources
 * from property files.
 * <p>
 * This class uses <code>MessageFormat</code> to perform substitutions
 * within parameterized strings.
 * </p>
 *
 * @see MessageFormat
 */
public class PropertyStringResourceLoader implements ResourceLoader {
    // The property file bundle that contains localized error strings for BlazeDS.
    public static final String PROPERTY_BUNDLE = "flex/messaging/errors";

    // The property file bundle that contains localized error strings for BlazeDS 
    // code specific to vendors (eg. LoginCommands for specific application serves)
    public static final String VENDORS_BUNDLE = "flex/messaging/vendors";

    // The property file bundle that contains localized error strings for LCDS.
    public static final String LCDS_PROPERTY_BUNDLE = "flex/data/errors";

    // The category to write log entries under.
    private static final String LOG_CATEGORY = LogCategories.RESOURCE;

    // The property bundle names to use in string lookups.
    private String[] propertyBundles;

    // The default FDS locale.
    private Locale defaultLocale;

    // The set of locales that have strings loaded.
    private Set loadedLocales = new TreeSet();

    // A map of all loaded strings.
    private Map strings = new HashMap();

    // The logger for this instance.
    private Logger logger;

    /**
     * Constructs a <code>PropertyStringResourceLoader</code> using the default
     * property bundles specified by the <code>PROPERTY_BUNDLE</code> and
     * <code>LCDS_PROPERTY_BUNDLE</code> fields.
     */
    public PropertyStringResourceLoader() {
        this(new String[]{PROPERTY_BUNDLE, LCDS_PROPERTY_BUNDLE});
    }

    /**
     * Constructs a <code>PropertyStringResourceLoader</code> that will use the
     * specified property bundle to use for string lookups.
     *
     * @param propertyBundle The property bundles to use for lookups.
     */
    public PropertyStringResourceLoader(String propertyBundle) {
        this(new String[]{propertyBundle});
    }

    /**
     * Constructs a <code>PropertyStringResourceLoader</code> that will use the
     * specified property bundles to use for string lookups.
     *
     * @param propertyBundles The list of the property bundles to use for lookups.
     */
    public PropertyStringResourceLoader(String[] propertyBundles) {
        this.propertyBundles = propertyBundles;
        logger = Log.getLogger(LOG_CATEGORY);
    }

    // Implements flex.messaging.util.ResourceLoader.init; inherits javadoc specification.
    public void init(Map properties) {
    }

    // Implements flex.messaging.util.ResourceLoader.getString; inherits javadoc specification.
    public String getString(String key) {
        return getString(key, null, null);
    }

    // Implements flex.messaging.util.ResourceLoader.getString; inherits javadoc specification.
    public String getString(String key, Object[] arguments) {
        return getString(key, null, arguments);
    }

    // Implements flex.messaging.util.ResourceLoader.getString; inherits javadoc specification.
    public String getString(String key, Locale locale) {
        return getString(key, locale, null);
    }

    // Implements flex.messaging.util.ResourceLoader.getString; inherits javadoc specification.
    public String getString(String key, Locale locale, Object[] arguments) {
        synchronized (strings) {
            if (defaultLocale == null) {
                defaultLocale = getDefaultLocale();
            }
        }
        String value = null;
        String stringKey = null;
        String localeKey = (locale != null) ?
                generateLocaleKey(locale) :
                generateLocaleKey(defaultLocale);
        String originalStringKey = generateStringKey(key, localeKey);
        int trimIndex = 0;

        /*
         * Attempt to get a string for the target locale - fail back to less specific
         * versions of the locale.
         */
        while (true) {
            loadStrings(localeKey);
            stringKey = generateStringKey(key, localeKey);
            synchronized (strings) {
                value = (String) strings.get(stringKey);
                if (value != null) {
                    if (!stringKey.equals(originalStringKey)) {
                        strings.put(originalStringKey, value);
                    }
                    return substituteArguments(value, arguments);
                }
            }
            trimIndex = localeKey.lastIndexOf('_');
            if (trimIndex != -1) {
                localeKey = localeKey.substring(0, trimIndex);
            } else {
                break;
            }
        }

        /*
         * Attempt to get the string in our default locale if it is
         * different than the requested locale.
         */
        if ((locale != null) && (!locale.equals(defaultLocale))) {
            localeKey = generateLocaleKey(defaultLocale);
            stringKey = generateStringKey(key, localeKey);
            synchronized (strings) {
                value = (String) strings.get(stringKey);
                if (value != null) {
                    strings.put(originalStringKey, value);
                    return substituteArguments(value, arguments);
                }
            }
        }

        // As a last resort, try to get a non-locale-specific string.
        loadStrings("");
        stringKey = generateStringKey(key, "");
        synchronized (strings) {
            value = (String) strings.get(stringKey);
            if (value != null) {
                strings.put(originalStringKey, value);
                return substituteArguments(value, arguments);
            }
        }

        // No string is available. Return a formatted missing string value.
        return ("???" + key + "???");
    }

    /**
     * Sets the default locale to be used when locating resources. The
     * string will be converted into a Locale.
     *
     * @param locale The default locale to be used.
     */
    public void setDefaultLocale(String locale) {
        defaultLocale = LocaleUtils.buildLocale(locale);
    }

    /**
     * Sets the default locale to be used when locating resources.
     *
     * @param locale The default locale to be used.
     */
    public void setDefaultLocale(Locale locale) {
        defaultLocale = locale;
    }

    /**
     * The default locale to be used when locating resources.
     *
     * @return Locale the default Locale object
     */
    public Locale getDefaultLocale() {
        if (defaultLocale == null)
            defaultLocale = Locale.getDefault();

        return defaultLocale;
    }

    /**
     * Loads localized strings for the specified locale from a property file.
     *
     * @param localeKey The locale to load strings for.
     */
    protected synchronized void loadStrings(String localeKey) {
        if (loadedLocales.contains(localeKey)) {
            return;
        }

        if (propertyBundles != null) {
            for (int i = 0; i < propertyBundles.length; i++) {
                String propertyBundle = propertyBundles[i];
                loadProperties(localeKey, propertyBundle);
            }
        }
    }

    protected InputStream loadFile(String filename) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        InputStream stream = loader.getResourceAsStream(filename);

        // Try the properties file in our classloader too - just in case
        if (stream == null) {
            stream = PropertyStringResourceLoader.class.getClassLoader().getResourceAsStream(filename);
        }

        return stream;
    }

    // Helper method for loadStrings.
    protected void loadProperties(String localeKey, String propertyBundle) {
        // Build the path to the target property file.
        String filename = propertyBundle;
        if (localeKey.length() > 0) {
            filename += "_" + localeKey;
        }
        filename += ".properties";
        // Load the property file.
        InputStream stream = loadFile(filename);

        Properties props = new Properties();
        if (stream != null) {
            try {
                props.load(stream);
            } catch (IOException ioe) {
                logger.warn("There was a problem reading the string resource property file '" + filename + "' stream.", ioe);
            } catch (IllegalArgumentException iae) {
                logger.warn("The string resource property file '" + filename + "' contains a malformed Unicode escape sequence.", iae);
            } finally {
                try {
                    stream.close();
                } catch (IOException ioe) {
                    logger.warn("The string resource property file '" + filename + "' stream failed to close.", ioe);
                }
            }
        } else {
            logger.warn("The class loader could not locate the string resource property file '" + filename + "'. This may not be an issue if a property file is available for a less specific locale or the default locale.");
        }
        // Move strings into string cache.
        if (props.size() > 0) {
            synchronized (strings) {
                Iterator iter = props.keySet().iterator();
                while (iter.hasNext()) {
                    String key = (String) iter.next();
                    strings.put(generateStringKey(key, localeKey), props.getProperty(key));
                }
            }
        }
    }

    /**
     * Generates a locale cache key.
     *
     * @param locale The locale to generate a cache key for.
     * @return The generated cache key.
     */
    private String generateLocaleKey(Locale locale) {
        return (locale == null) ? "" : locale.toString();
    }

    /**
     * Generates a cache key for a string resource.
     *
     * @param key    The string to generate a cache key for.
     * @param locale The locale to retrieve the string for.
     * @return The generated cache key for the string resource.
     */
    private String generateStringKey(String key, String locale) {
        return (key + "-" + locale);
    }

    /**
     * Substitutes the specified arguments into a parameterized string.
     *
     * @param parameterized The string containing parameter tokens for substitution.
     * @param arguments     The arguments to substitute into the parameterized string.
     * @return The resulting substituted string.
     */
    private String substituteArguments(String parameterized, Object[] arguments) {
        return MessageFormat.format(parameterized, arguments).trim();
    }

}