/**
 * Copyright 2005-2016 The Kuali Foundation
 *
 * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php
 *
 * 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.kuali.rice.core.impl.services;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.Logger;
import org.kuali.rice.core.api.config.property.ConfigContext;
import org.kuali.rice.core.api.config.property.ConfigurationService;
import org.kuali.rice.core.api.exception.RiceRuntimeException;
import org.kuali.rice.core.api.util.Truth;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;

/**
 * Implementation of the {@link org.kuali.rice.core.api.config.property.ConfigurationService} that loads
 * messages from the configured rice config files and stores them in an internal property holder
 *
 * @author Kuali Rice Team (rice.collab@kuali.org)
 */
public class ConfigurationServiceImpl implements ConfigurationService {

    // TODO - this was copied from KRADConstants to avoid a dependency between KRAD and the core, this needs to be
    // examined to determine if we really want to be doing something like this in the configuration service
    private static final String MESSAGE_RESOURCES = "rice.struts.message.resources";

    private final PropertyHolder propertyHolder = new PropertyHolder();

    /**
     * Default constructor
     */
    public ConfigurationServiceImpl() {
        this.propertyHolder.getHeldProperties().putAll(ConfigContext.getCurrentContextConfig().getProperties());

        // TODO: remove loading of property files once KNS legacy code is removed
        String propertyConfig = (String) ConfigContext.getCurrentContextConfig().getProperties().get(MESSAGE_RESOURCES);
        propertyConfig = removeSpacesAround(propertyConfig);

        String[] bundleNames = StringUtils.split(propertyConfig, ",");
        for (String bundleName : bundleNames) {
            ResourceBundle bundle = ResourceBundle.getBundle(bundleName);

            for (String key : bundle.keySet()) {
                String message = bundle.getString(key);
                this.propertyHolder.getHeldProperties().put(key, message);
            }
        }
    }

    /**
     * @see org.kuali.rice.core.api.config.property.ConfigurationService#getPropertyValueAsString(String)
     */
    @Override
    public String getPropertyValueAsString(String key) {
        if (key == null) {
            throw new IllegalArgumentException("invalid (null) key");
        }

        return this.propertyHolder.getProperty(key);
    }

    /**
     * @see org.kuali.rice.core.api.config.property.ConfigurationService#getPropertyValueAsBoolean(String)
     */
    @Override
    public boolean getPropertyValueAsBoolean(String key) {
        if (key == null) {
            throw new IllegalArgumentException("invalid (null) key");
        }

        String property = this.propertyHolder.getProperty(key);
        Boolean b = Truth.strToBooleanIgnoreCase(property);
        if (b == null) {
            return false;
        }

        return b;
    }

    /**
     * @see org.kuali.rice.core.api.config.property.ConfigurationService#getAllProperties()
     */
    @Override
    public Map<String, String> getAllProperties() {
        return (Map) Collections.unmodifiableMap(propertyHolder.getHeldProperties());
    }

    /**
     * Removes the spaces around the elements on a csv list of elements
     *
     * <p>
     * A null input will return a null output.
     * </p>
     *
     * @param csv a list of elements in csv format e.g. foo, bar, baz
     * @return a list of elements in csv format without spaces e.g. foo,bar,baz
     */
    private String removeSpacesAround(String csv) {
        if (csv == null) {
            return null;
        }

        final StringBuilder result = new StringBuilder();
        for (final String value : csv.split(",")) {
            if (!"".equals(value.trim())) {
                result.append(value.trim());
                result.append(",");
            }
        }

        //remove trailing comma
        int i = result.lastIndexOf(",");
        if (i != -1) {
            result.deleteCharAt(i);
        }

        return result.toString();
    }

    /**
     * Interface for a source for properties
     */
    protected static interface PropertySource {

        /**
         * @return Properties loaded from this PropertySource
         */
        public Properties loadProperties();
    }

    /**
     * This class is a Property container. It is able to load properties from various property-sources.
     */
    protected static class PropertyHolder {
        private static Logger LOG = Logger.getLogger(PropertyHolder.class);

        Properties heldProperties;

        /**
         * Default constructor.
         */
        public PropertyHolder() {
            this.heldProperties = new Properties();
        }

        /**
         * @return true if this container currently has no properties
         */
        public boolean isEmpty() {
            return this.heldProperties.isEmpty();
        }

        /**
         * @param key
         * @return true if a property with the given key exists in this container
         * @throws IllegalArgumentException if the given key is null
         */
        public boolean containsKey(String key) {
            validateKey(key);

            return this.heldProperties.containsKey(key);
        }

        /**
         * @param key
         * @return the current value of the property with the given key, or null if no property exists with that key
         * @throws IllegalArgumentException if the given key is null
         */
        public String getProperty(String key) {
            validateKey(key);

            return this.heldProperties.getProperty(key);
        }

        /**
         * Associates the given value with the given key
         *
         * @param key
         * @param value
         * @throws IllegalArgumentException if the given key is null
         * @throws IllegalArgumentException if the given value is null
         */
        public void setProperty(String key, String value) {
            setProperty(null, key, value);
        }

        /**
         * Associates the given value with the given key
         *
         * @param source
         * @param key
         * @param value
         * @throws IllegalArgumentException if the given key is null
         * @throws IllegalArgumentException if the given value is null
         */
        public void setProperty(PropertySource source, String key, String value) {
            validateKey(key);
            validateValue(value);

            if (containsKey(key)) {
                if (source != null && source instanceof FilePropertySource && ((FilePropertySource) source)
                        .isAllowOverrides()) {
                    LOG.info("Duplicate Key: Override is enabled [key="
                            + key
                            + ", new value="
                            + value
                            + ", old value="
                            + this.heldProperties.getProperty(key)
                            + "]");
                } else {
                    throw new RiceRuntimeException("duplicate key '" + key + "'");
                }
            }
            this.heldProperties.setProperty(key, value);
        }

        /**
         * Removes the property with the given key from this container
         *
         * @param key
         * @throws IllegalArgumentException if the given key is null
         */
        public void clearProperty(String key) {
            validateKey(key);

            this.heldProperties.remove(key);
        }

        /**
         * Copies all name,value pairs from the given PropertySource instance into this container.
         *
         * @param source
         * @throws IllegalStateException if the source is invalid (improperly initialized)
         * as an existing property
         */
        public void loadProperties(PropertySource source) {
            if (source == null) {
                throw new IllegalArgumentException("invalid (null) source");
            }

            Properties newProperties = source.loadProperties();

            for (Iterator i = newProperties.keySet().iterator(); i.hasNext(); ) {
                String key = (String) i.next();
                setProperty(source, key, newProperties.getProperty(key));
            }
        }

        /**
         * Removes all properties from this container.
         */
        public void clearProperties() {
            this.heldProperties.clear();
        }

        /**
         * @return iterator over the keys of all properties in this container
         */
        public Iterator getKeys() {
            return this.heldProperties.keySet().iterator();
        }

        /**
         * @param key
         * @throws IllegalArgumentException if the given key is null
         */
        private void validateKey(String key) {
            if (key == null) {
                throw new IllegalArgumentException("invalid (null) key");
            }
        }

        /**
         * @throws IllegalArgumentException if the given value is null
         */
        private void validateValue(String value) {
            if (value == null) {
                throw new IllegalArgumentException("invalid (null) value");
            }
        }

        public Properties getHeldProperties() {
            return heldProperties;
        }

        public void setHeldProperties(Properties heldProperties) {
            this.heldProperties = heldProperties;
        }
    }

    /**
     * Used to obtain properties from a properties file
     */
    protected static class FilePropertySource implements PropertySource {
        private static Log log = LogFactory.getLog(FilePropertySource.class);

        private String fileName;
        private boolean allowOverrides;

        public void setFileName(String fileName) {
            this.fileName = fileName;
        }

        public String getFileName() {
            return this.fileName;
        }

        public boolean isAllowOverrides() {
            return this.allowOverrides;
        }

        public void setAllowOverrides(boolean allowOverrides) {
            this.allowOverrides = allowOverrides;
        }

        /**
         * Attempts to load properties from a properties file which has the current fileName and is located on the
         * classpath
         *
         * @throws IllegalStateException if the fileName is null or empty
         * @see ConfigurationServiceImpl.PropertySource#loadProperties()
         */
        public Properties loadProperties() {
            if (StringUtils.isBlank(getFileName())) {
                throw new IllegalStateException("invalid (blank) fileName");
            }

            Properties properties = new Properties();

            ClassLoader loader = Thread.currentThread().getContextClassLoader();
            URL url = loader.getResource(getFileName());
            if (url == null) {
                throw new RiceRuntimeException("unable to locate properties file '" + getFileName() + "'");
            }

            InputStream in = null;

            try {
                in = url.openStream();
                properties.load(in);
            } catch (IOException e) {
                throw new RiceRuntimeException("error loading from properties file '" + getFileName() + "'", e);
            } finally {
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        log.error("caught exception closing InputStream: " + e);
                    }

                }
            }

            return properties;
        }
    }
}
