/**
 * Copyright (C) 2012-2014 the original author or authors.
 *
 * Licensed 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 ninja.i18n;

import java.text.MessageFormat;
import java.util.Locale;
import java.util.Map;

import ninja.Context;
import ninja.Result;
import ninja.utils.NinjaConstant;
import ninja.utils.NinjaProperties;
import ninja.utils.SwissKnife;

import org.apache.commons.configuration.CompositeConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationConverter;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.configuration.reloading.FileChangedReloadingStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.Singleton;

@Singleton
public class MessagesImpl implements Messages {

    private static Logger logger = LoggerFactory.getLogger(MessagesImpl.class);

    private final Map<String, Configuration> langToKeyAndValuesMapping;

    private final NinjaProperties ninjaProperties;

    private final Lang lang;

    @Inject
    public MessagesImpl(
                        NinjaProperties ninjaProperties,
                        Lang lang) {

        this.ninjaProperties = ninjaProperties;
        this.lang = lang;
        this.langToKeyAndValuesMapping = loadAllMessageFilesForRegisteredLanguages();


    }
    
    
    @Override
    public Optional<String> get(String key,
                      Context context,
                      Optional<Result> result,
                      Object... parameter) {
        
        Optional<String> language = lang.getLanguage(context, result);
        return get(key, language, parameter);
        
    }

    @Override
    public Optional<String> get(String key, Optional<String> language, Object... params) {

        Configuration configuration = getLanguageConfigurationForLocale(language);

        String value = configuration.getString(key);

        if (value != null) {

            MessageFormat messageFormat = getMessageFormatForLocale(value, language);
            
            return Optional.of(messageFormat.format(params));
            
        } else {
            return Optional.absent();
        }

    }
    
    @Override
    public Map<Object, Object> getAll(Context context, Optional<Result> result) {
        
        Optional<String> language = lang.getLanguage(context, result);
        return getAll(language);
        
    }

    @Override
    public Map<Object, Object> getAll(Optional<String> language) {

        Configuration configuration = getLanguageConfigurationForLocale(language);

        return ConfigurationConverter.getMap(configuration);

    }
    
    @Override
    public String getWithDefault(String key,
                                 String defaultMessage,
                                 Context context,
                                 Optional<Result> result,
                                 Object... params) {
        
        Optional<String> language = lang.getLanguage(context, result);
        
        return getWithDefault(key, defaultMessage, language, params);

    }

    @Override
    public String getWithDefault(String key,
                                 String defaultMessage,
                                 Optional<String> language,
                                 Object... params) {

        Optional<String> value = get(key, language, params);

        if (value.isPresent()) {

            MessageFormat messageFormat = getMessageFormatForLocale(value.get(), language);
            
            return messageFormat.format(params);

        } else {
            
            // return default message
            MessageFormat messageFormat = getMessageFormatForLocale(defaultMessage, language);
            
            return messageFormat.format(params);

        }

    }

    /**
     * Attempts to load a message file and sets the file changed reloading
     * strategy on the configuration if the runtime mode is Dev.
     */
    private PropertiesConfiguration loadLanguageConfiguration(String fileOrUrl) {
        PropertiesConfiguration configuration = SwissKnife
                .loadConfigurationInUtf8(fileOrUrl);

        if (configuration != null && ninjaProperties.isDev()) {
            // enable runtime reloading of translations in dev mode
            FileChangedReloadingStrategy strategy = new FileChangedReloadingStrategy();
            configuration.setReloadingStrategy(strategy);
        }

        return configuration;
    }

    /**
     * Does all the loading of message files.
     * 
     * Only registered messages in application.conf are loaded.
     * 
     */
    private  Map<String, Configuration> loadAllMessageFilesForRegisteredLanguages() {
        
        Map<String, Configuration> langToKeyAndValuesMappingMutable = Maps.newHashMap();

        // Load default messages:
        Configuration defaultLanguage = loadLanguageConfiguration("conf/messages.properties");

        // Make sure we got the file.
        // Everything else does not make much sense.
        if (defaultLanguage == null) {
            throw new RuntimeException(
                    "Did not find conf/messages.properties. Please add a default language file.");
        } else {
            langToKeyAndValuesMappingMutable.put("", defaultLanguage);
        }

        // Get the languages from the application configuration.
        String[] applicationLangs = ninjaProperties
                .getStringArray(NinjaConstant.applicationLanguages);

        // If we don't have any languages declared we just return.
        // We'll use the default messages.properties file.
        if (applicationLangs == null) {
            return ImmutableMap.copyOf(langToKeyAndValuesMappingMutable);
        }

        // Load each language into the HashMap containing the languages:
        for (String lang : applicationLangs) {

            // First step: Load complete language eg. en-US
            Configuration configuration = loadLanguageConfiguration(String
                    .format("conf/messages_%s.properties", lang));

            Configuration configurationLangOnly = null;

            // If the language has a country code load the default values for
            // the language, too. For instance missing variables in en-US will
            // be
            // Overwritten by the default languages.
            if (lang.contains("-")) {
                // get the lang
                String langOnly = lang.split("-")[0];

                // And load the configuraion
                configurationLangOnly = loadLanguageConfiguration(String
                        .format("conf/messages_%s.properties", langOnly));

            }

            // This is strange. If you defined the language in application.conf
            // it should be there propably.
            if (configuration == null) {
                logger.info(
                        "Did not find conf/messages_{}.properties but it was specified in application.conf. Using default language instead.",
                        lang);

            } else {

                // add new language, but combine with default language if stuff
                // is missing...
                CompositeConfiguration compositeConfiguration = new CompositeConfiguration();
                // Add eg. "en-US"
                compositeConfiguration.addConfiguration(configuration);

                // Add eg. "en"
                if (configurationLangOnly != null) {
                    compositeConfiguration
                            .addConfiguration(configurationLangOnly);
                }
                // Add messages.conf (default pack)
                compositeConfiguration.addConfiguration(defaultLanguage);

                // and add the composed configuration to the hashmap with the
                // mapping.
                langToKeyAndValuesMappingMutable.put(lang,
                        (Configuration) compositeConfiguration);
            }

        }
        
        
        return ImmutableMap.copyOf(langToKeyAndValuesMappingMutable);

    }

    /**
     * Retrieves the matching language file from an arbitrary one or two part
     * locale String ("en-US", or "en" or "de"...).
     * <p>
     * 
     * @param language
     *            A two or one letter language code such as "en-US" or "en" or
     *            "en-US,en;q=0.8,de;q=0.6".
     * @return The matching configuration from the hashmap. Or the default
     *         mapping if no one has been found.
     */
    private Configuration getLanguageConfigurationForLocale(Optional<String> language) {

        // if language is null we return the default language.
        if (!language.isPresent()) {
            return langToKeyAndValuesMapping.get("");
        }

        // Check if we get a registered mapping for the language input string.
        // At that point the language may be either language-country or only country.
        // extract multiple languages from Accept-Language header
        String[] languages = language.get().split(",");
        for (String l: languages){
            l = l.trim();
            // Ignore the relative quality factor in Accept-Language header
            if (l.contains(";")){
                l = l.split(";")[0];
            }
            Configuration configuration = langToKeyAndValuesMapping.get(l);
            if (configuration != null) {
                return configuration;
            }

            // If we got a two part language code like "en-US" we split it and
            // search only for the language "en".
            if (l.contains("-")) {
                String[] array = l.split("-");
                String languageWithoutCountry = array[0];
                // Modify country code to upper case for IE and Firefox
                if(array.length > 1){
                    String country = array[1];
                    String languageWithUpperCaseCountry = languageWithoutCountry + "-" + country.toUpperCase();
                    configuration = langToKeyAndValuesMapping.get(languageWithUpperCaseCountry);
                    if (configuration != null) {
                        return configuration;
                    }
                }
                configuration = langToKeyAndValuesMapping
                        .get(languageWithoutCountry);

                if (configuration != null) {

                    return configuration;
                }

            }
        }

        // Oops. Nothing found. We return the default language (by convention guaranteed to work).
        return langToKeyAndValuesMapping.get("");

    }



    MessageFormat getMessageFormatForLocale(String value, Optional<String> language) {
    
        Locale locale = lang.getLocaleFromStringOrDefault(language);
        MessageFormat messageFormat = new MessageFormat(value, locale);
        
        return messageFormat;
    
    }



}
