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

import com.google.inject.AbstractModule;
import com.google.inject.CreationException;
import com.google.inject.name.Names;
import com.google.inject.spi.Message;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Properties;

/**
 * Injects everything from the a property file as a Named value.<br/>
 * Uses the {@literal rave.shindig.properties} file unless the system property
 * {@literal rave-shindig.override.properties} defines a different location.
 */
public class ConfigurablePropertiesModule extends AbstractModule {

    private static final String DEFAULT_PROPERTIES = "rave.shindig.properties";
    private static final String SHINDIG_OVERRIDE_PROPERTIES = "rave-shindig.override.properties";

    private static final String CLASSPATH = "classpath:";

    private final Properties properties;


    public ConfigurablePropertiesModule() {
        super();
        this.properties = initProperties();
    }

    /**
     * Injects everything from the a property file as a Named value.
     * Only the entire properties file can be overridden.
     * {@inheritDoc}
     */
    @Override
    protected void configure() {
        bindPropertiesAsConstants();
        bindNonConstantProperties();
    }

    private void bindPropertiesAsConstants() {
        for(String propertyName: constantGuiceProperties()) {
            this.binder().bindConstant().annotatedWith(Names.named(propertyName))
                    .to(getProperties().getProperty(propertyName));
        }
    }

    private void bindNonConstantProperties() {
        Properties p = getProperties();
        for (String overridableProperty : constantGuiceProperties()) {
            p.remove(overridableProperty);
        }
        Names.bindProperties(this.binder(), p);
    }

    /**
     * Initializes the Properties. Reads the properties file, overrides values by System properties if set.
     * Replaces placeholders with actual values.
     *
     * @return {@link Properties} for the container
     * @throws com.google.inject.CreationException
     *          if the Properties file cannot be read
     */
    protected Properties initProperties() {
        Resource propertyResource = getPropertyResource();
        try {
            Properties initProperties = loadPropertiesFromPropertyResource(propertyResource);
            overridePropertyValuesWithSystemProperties(initProperties, overridableProperties());
            replacePlaceholderWithValue(initProperties, "%contextRoot%", getContextRootValue(initProperties));
            return initProperties;
        } catch (IOException e) {
            throw new CreationException(Arrays.asList(
                    new Message("Unable to load properties from resource"
                            + ". " + e.getMessage())
            ));
        }
    }

    /**
     * Reads properties from the given {@link Resource}
     *
     * @param propertyResource with the properties
     * @return {@link Properties}
     * @throws java.io.IOException if the Resource cannot be read
     */
    private Properties loadPropertiesFromPropertyResource(Resource propertyResource) throws IOException {
        Properties properties = new Properties();
        properties.load(propertyResource.getInputStream());
        return properties;
    }

    /**
     * Returns a Resource that contains property key-value pairs.
     * If no system property is set for the resource location, the default location is used
     *
     * @return the {@link Resource} with the
     */
    private Resource getPropertyResource() {
        final String overrideProperty = System.getProperty(SHINDIG_OVERRIDE_PROPERTIES);
        if (StringUtils.isBlank(overrideProperty)) {
            return new ClassPathResource(DEFAULT_PROPERTIES);
        } else if (overrideProperty.startsWith(CLASSPATH)) {
            return new ClassPathResource(overrideProperty.trim().substring(CLASSPATH.length()));
        } else {
            return new FileSystemResource(overrideProperty.trim());
        }
    }

    /**
     * If a property is overridable and a system property has been set, the value of the system
     * property overrides the property that was set in the properties file.
     *
     * @param properties               with some property values replaced by system property values
     * @param overridablePropertyNames List of property names which value can be overridden by a system property
     *                                 with the same name
     */
    static void overridePropertyValuesWithSystemProperties(Properties properties,
                                                                  List<String> overridablePropertyNames) {
        for (String propertyName : overridablePropertyNames) {
            properties.setProperty(propertyName, getOverridablePropertyValue(propertyName, properties));
        }
    }

    /**
     * Replaces the placeholder for contextRoot with the actual value
     *
     * @param properties  {@link java.util.Properties}
     * @param placeholder (part of) property value that should be replaced
     * @param value       that replaces the placeholder
     */
    static void replacePlaceholderWithValue(Properties properties, String placeholder, String value) {
        for (Map.Entry entry : properties.entrySet()) {
            String propertyValue = (String) entry.getValue();
            if (propertyValue != null && propertyValue.contains(placeholder)) {
                properties.put(entry.getKey(),
                        propertyValue.replace((placeholder), value));
            }
        }
    }

    /**
     * Returns the value of a property. First checks if there is a system property set,
     * if not, then checks if a {@link Properties} contains this property. Otherwise it
     * returns an empty String.
     *
     * @param propertyKey name of the property
     * @param properties  {@link Properties} that may contain a key by this name
     * @return value for this propertyKey, can be an empty String
     */
    static String getOverridablePropertyValue(String propertyKey, Properties properties) {
        if (System.getProperty(propertyKey) != null) {
            return System.getProperty(propertyKey);
        } else if (properties.get(propertyKey) != null) {
            return properties.getProperty(propertyKey);
        } else {
            return "";
        }
    }
    
    /**
     * Looks up the context root property value in a {@link Properties}
     *
     * @param properties {@link Properties}
     * @return value of the context root property, can be {@literal null}
     */
    private String getContextRootValue(Properties properties) {
        return properties.getProperty("shindig.contextroot");
    }

    /**
     * @return {@link Properties}
     */
    protected Properties getProperties() {
        return properties;
    }

    /**
     *
     * @return List of property keys that can be overridden by system properties
     */
    private static List<String> overridableProperties() {
        List<String> propertyNames = new ArrayList<String>();
        propertyNames.add("shindig.host");
        propertyNames.add("shindig.port");
        propertyNames.add("shindig.contextroot");
        return propertyNames;
    }

    /**
     *
     * @return List of property keys that should be bound as constants in Guice
     */
    private static List<String> constantGuiceProperties() {
        List<String> propertyNames = new ArrayList<String>();
        propertyNames.add("shindig.host");
        propertyNames.add("shindig.port");
        propertyNames.add("shindig.contextroot");
        return propertyNames;
    }

}