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

import java.io.File;
import java.util.Properties;

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.Splitter;
import com.google.common.collect.Iterables;
import com.google.inject.Binder;
import com.google.inject.Singleton;
import com.google.inject.name.Names;

@Singleton
public class NinjaPropertiesImpl implements NinjaProperties {

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

    private NinjaMode ninjaMode;
    
    private String contextPath = "";

    private final String ERROR_KEY_NOT_FOUND = "Key %s does not exist. Please include it in your application.conf. Otherwise this app will not work";

    /**
     * This is the final configuration holding all information from 
     * 1. application.conf. 
     * 2. Special properties for the mode you are running on extracted 
     *    from application.conf 
     * 3. An external configuration file defined
     *    by a system property and on the classpath.
     */
    private CompositeConfiguration compositeConfiguration;

    public NinjaPropertiesImpl(
            NinjaMode ninjaMode) {
        
        this.ninjaMode = ninjaMode;

        // This is our main configuration.
        // In the following we'll read the individual configurations and merge
        // them into the composite configuration at the end.
        compositeConfiguration = new CompositeConfiguration();

        // That is the default config.
        PropertiesConfiguration defaultConfiguration = null;

        // Config of prefixed mode corresponding to current mode (eg.
        // %test.myproperty=...)
        Configuration prefixedDefaultConfiguration = null;

        // (Optional) Config set via a system property
        PropertiesConfiguration externalConfiguration = null;

        // (Optional) Config of prefixed mode corresponding to current mode (eg.
        // %test.myproperty=...)
        Configuration prefixedExternalConfiguration = null;

        // First step => load application.conf and also merge properties that
        // correspond to a mode into the configuration.

        defaultConfiguration = SwissKnife
                .loadConfigurationInUtf8(NinjaProperties.CONF_FILE_LOCATION_BY_CONVENTION);

        if (defaultConfiguration != null) {
            // Second step:
            // Copy special prefix of mode to parent configuration
            // By convention it will be something like %test.myproperty
            prefixedDefaultConfiguration = defaultConfiguration.subset("%"
                    + ninjaMode.name());

            // allow application.conf to be reloaded on changes in dev mode
            if (NinjaMode.dev == ninjaMode) {
                defaultConfiguration
                        .setReloadingStrategy(new FileChangedReloadingStrategy());
            }

        } else {

            // If the property was set, but the file not found we emit
            // a RuntimeException
            String errorMessage = String
                    .format("Error reading configuration file. Make sure you got a default config file %s",
                            NinjaProperties.CONF_FILE_LOCATION_BY_CONVENTION);

            logger.error(errorMessage);

            throw new RuntimeException(errorMessage);
        }

        // third step => load external configuration when a system property is
        // defined.
        String ninjaExternalConf = System.getProperty(NINJA_EXTERNAL_CONF);

        if (ninjaExternalConf != null) {

            // only load it when the property is defined.

            externalConfiguration = SwissKnife
                    .loadConfigurationInUtf8(ninjaExternalConf);

            // this should not happen:
            if (externalConfiguration == null) {

                String errorMessage = String
                        .format("Ninja was told to use an external configuration%n"
                                + " %s = %s %n."
                                + "But the corresponding file cannot be found.%n"
                                + " Make sure it is visible to this application and on the classpath.",
                                NINJA_EXTERNAL_CONF, ninjaExternalConf);

                logger.error(errorMessage);

                throw new RuntimeException(errorMessage);

            } else {

                // allow the external configuration to be reloaded at
                // runtime based on detected file changes
                final boolean shouldReload = Boolean.getBoolean(NINJA_EXTERNAL_RELOAD);
                if (shouldReload) {
                    externalConfiguration
                            .setReloadingStrategy(new FileChangedReloadingStrategy());
                }

                // Copy special prefix of mode to parent configuration
                // By convention it will be something like %test.myproperty
                prefixedExternalConfiguration = externalConfiguration
                        .subset("%" + ninjaMode.name());
            }

        }

        // /////////////////////////////////////////////////////////////////////
        // Finally add the stuff to the composite configuration
        // Note: Configurations added earlier will overwrite configurations
        // added later.
        // /////////////////////////////////////////////////////////////////////

        if (prefixedExternalConfiguration != null) {
            compositeConfiguration
                    .addConfiguration(prefixedExternalConfiguration);
        }

        if (externalConfiguration != null) {
            compositeConfiguration.addConfiguration(externalConfiguration);
        }

        if (prefixedDefaultConfiguration != null) {
            compositeConfiguration
                    .addConfiguration(prefixedDefaultConfiguration);
        }

        if (defaultConfiguration != null) {
            compositeConfiguration.addConfiguration(defaultConfiguration);
        }
        
        // /////////////////////////////////////////////////////////////////////
        // Check that the secret is set or generate a new one if the property
        // does not exist
        // /////////////////////////////////////////////////////////////////////
        NinjaPropertiesImplTool.checkThatApplicationSecretIsSet(
                isProd(), 
                new File("").getAbsolutePath(), 
                defaultConfiguration, 
                compositeConfiguration);

    }

    @Override
    public String get(String key) {

        String value;

        try {
            value = compositeConfiguration.getString(key);
        } catch (Exception e) {
            // Fail silently because we handle errors differently. Simply set
            // them null.
            value = null;
        }

        return value;

    }

    @Override
    public String getOrDie(String key) {

        String value = get(key);

        if (value == null) {
            logger.error(String.format(ERROR_KEY_NOT_FOUND, key));
            throw new RuntimeException(String.format(ERROR_KEY_NOT_FOUND, key));
        } else {
            return value;
        }

    }

    @Override
    public Integer getInteger(String key) {

        Integer value;

        try {
            value = compositeConfiguration.getInt(key);
        } catch (Exception e) {
            // Fail silently because we handle errors differently. Simply set
            // them null.
            value = null;
        }

        return value;

    }

    @Override
    public Integer getIntegerOrDie(String key) {

        Integer value = getInteger(key);

        if (value == null) {
            logger.error(String.format(ERROR_KEY_NOT_FOUND, key));
            throw new RuntimeException(String.format(ERROR_KEY_NOT_FOUND, key));
        } else {
            return value;
        }

    }

    @Override
    public Boolean getBooleanOrDie(String key) {

        Boolean value = getBoolean(key);

        if (value == null) {
            logger.error(String.format(ERROR_KEY_NOT_FOUND, key));
            throw new RuntimeException(String.format(ERROR_KEY_NOT_FOUND, key));
        } else {
            return value;
        }

    }

    @Override
    public Boolean getBoolean(String key) {

        Boolean value;

        try {
            value = compositeConfiguration.getBoolean(key);
        } catch (Exception e) {
            // Fail silently because we handle errors differently. Simply set
            // them null.
            value = null;
        }

        return value;

    }

    public void setProperty(String key, String value) {
        compositeConfiguration.setProperty(key, value);
    }

    public void bindProperties(Binder binder) {
        Names.bindProperties(binder,
                ConfigurationConverter.getProperties(compositeConfiguration));
    }

    @Override
    public boolean isProd() {
        return (ninjaMode.equals(NinjaMode.prod));
    }

    @Override
    public boolean isDev() {
        return (ninjaMode.equals(NinjaMode.dev));
    }

    @Override
    public boolean isTest() {
        return (ninjaMode.equals(NinjaMode.test));
    }
    
    /**
     * Get the context path on which the application is running
     * 
     * That means:
     * - when running on root the context path is empty
     * - when running on context there is NEVER a trailing slash
     * 
     * We conform to the following rules:
     * Returns the portion of the request URI that indicates the context of the 
     * request. The context path always comes first in a request URI. 
     * The path starts with a "/" character but does not end with a "/" character. 
     * For servlets in the default (root) context, this method returns "". 
     * The container does not decode this string.
     * 
     * As outlined by: http://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#getContextPath()
     * 
     * @return the context-path with a leading "/" or "" if running on root
     */
    @Override
    public String getContextPath() {
        
        return contextPath;
        
    }
    
    @Override
    public void setContextPath(String contextPath) {
        
        this.contextPath = contextPath;
        
    }

    @Override
    public Properties getAllCurrentNinjaProperties() {

        return ConfigurationConverter.getProperties(compositeConfiguration);

    }

    @Override
    public String[] getStringArray(String key) {
        String value = compositeConfiguration.getString(key);
        if (value != null) {
            return Iterables.toArray(Splitter.on(",").trimResults()
                    .omitEmptyStrings().split(value), String.class);
        } else {
            return null;
        }
    }

    @Override
    public String getWithDefault(String key, String defaultValue) {
        String value = get(key);
        if (value != null) {
            return value;
        } else {
            return defaultValue;
        }
    }

    @Override
    public Integer getIntegerWithDefault(String key, Integer defaultValue) {
        Integer value = getInteger(key);
        if (value != null) {
            return value;
        } else {
            return defaultValue;
        }
    }

    @Override
    public Boolean getBooleanWithDefault(String key, Boolean defaultValue) {
        Boolean value = getBoolean(key);
        if (value != null) {
            return value;
        } else {
            return defaultValue;
        }
    }
    
}
