/**
 * Copyright 2014 Netflix, Inc.
 *
 * 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 com.netflix.config.util;

import java.io.File;
import java.io.FileNotFoundException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Map.Entry;

import org.apache.commons.configuration.AbstractConfiguration;
import org.apache.commons.configuration.CombinedConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.netflix.config.ConcurrentCompositeConfiguration;
import com.netflix.config.ConcurrentMapConfiguration;
import com.netflix.config.ConfigurationManager;


/**
 * Utility class for configuration.
 * 
 * @author awang
 *
 */
public class ConfigurationUtils {
    
    private static final Logger logger = LoggerFactory.getLogger(ConfigurationUtils.class);
    /**
     * Convert CombinedConfiguration into {@link ConcurrentCompositeConfiguration} as the later has better performance
     * and thread safety. 
     * 
     * @param config Configuration to be converted
     */
    public static ConcurrentCompositeConfiguration convertToConcurrentCompositeConfiguration(CombinedConfiguration config) {
        ConcurrentCompositeConfiguration root = new ConcurrentCompositeConfiguration();
        IdentityHashMap<Configuration, String> reverseMap = new IdentityHashMap<Configuration, String>();
        for (String name: (Set<String>) config.getConfigurationNames()) {
            Configuration child = config.getConfiguration(name);
            reverseMap.put(child, name);
        } 
        for (int i = 0; i < config.getNumberOfConfigurations(); i++) {
            Configuration child = config.getConfiguration(i);
            String name = reverseMap.get(child);
            if (child instanceof CombinedConfiguration) {
                CombinedConfiguration combinedConf = (CombinedConfiguration) child;
                ConcurrentCompositeConfiguration newConf = convertToConcurrentCompositeConfiguration(combinedConf);
                root.addConfiguration(newConf, name);
            } else {
                Configuration conf = new ConcurrentMapConfiguration(child);
                root.addConfiguration((AbstractConfiguration) conf, name);
            }
        }
        return root;
    }
    
    /**
     * Gets all named sub-configuration from a configuration in a map. This method examines each sub-configuration
     * which is an instance of {@link ConcurrentCompositeConfiguration} or CombinedConfiguration and extract the 
     * named configurations out of them.
     *  
     * @param conf Configuration to get all the named sub-configurations
     * @return map where key is the name of the sub-configuration and value is the sub-configuration
     */
    public static Map<String, Configuration> getAllNamedConfiguration(Configuration conf) {
        List<Configuration> toProcess = new ArrayList<Configuration>();
        Map<String, Configuration> map = new HashMap<String, Configuration>();
        toProcess.add(conf);
        while (!toProcess.isEmpty()) {
            Configuration current = toProcess.remove(0);
            if (current instanceof ConcurrentCompositeConfiguration) {
                ConcurrentCompositeConfiguration composite = (ConcurrentCompositeConfiguration) current;
                for (String name: composite.getConfigurationNames()) {
                    map.put(name, composite.getConfiguration(name));
                }
                for (Configuration c: composite.getConfigurations()) {
                    toProcess.add(c);
                }
            } else if (current instanceof CombinedConfiguration) {
                CombinedConfiguration combined = (CombinedConfiguration) current;
                for (String name: (Set<String>) combined.getConfigurationNames()) {
                    map.put(name, combined.getConfiguration(name));
                }
                for (int i = 0; i < combined.getNumberOfConfigurations(); i++) {
                    toProcess.add(combined.getConfiguration(i));
                }
            }
        }
        return map;
    }
    
    /**
     * Utility method to obtain <code>Properties</code> given an instance of <code>AbstractConfiguration</code>.
     * Returns an empty <code>Properties</code> object if the config has no properties or is null.
     * @param config Configuration to get the properties
     * @return properties extracted from the configuration
     */
    public static Properties getProperties(Configuration config) {
 	   Properties p = new Properties();
 	   if (config != null){
	 	   Iterator<String> it = config.getKeys();	 	   
	 	   while (it.hasNext()){
	 		   String key = it.next();
	 		   if (key != null) {
	 		      Object value = config.getProperty(key);
                  if (value != null) {
                      p.put(key, value);
                  }	 		   }
	 	   }
 	   }
  	   return p;
    }
    
    public static void loadProperties(Properties props, Configuration config) {
        for (Entry<Object, Object> entry: props.entrySet()) {
            config.setProperty((String) entry.getKey(), entry.getValue());
        }
    }
    
    static void loadFromPropertiesFile(AbstractConfiguration config, String baseUrl, Set<String> loaded, String... nextLoadKeys) {
        String nextLoad = getNextLoad(config, nextLoadKeys);
        if (nextLoad == null) {
            return;
        }
        String[] filesToLoad = nextLoad.split(",");
        for (String fileName: filesToLoad) {
            fileName = fileName.trim();
            try {
                URL url = new URL(baseUrl + "/" + fileName);
                // avoid circle
                if (loaded.contains(url.toExternalForm())) {
                    logger.warn(url + " is already loaded");
                    continue;
                }
                loaded.add(url.toExternalForm());
                PropertiesConfiguration nextConfig = new OverridingPropertiesConfiguration(url);
                copyProperties(nextConfig, config);
                logger.info("Loaded properties file " + url);
                loadFromPropertiesFile(config, baseUrl, loaded, nextLoadKeys);
            } catch (Throwable e) {
                logger.warn("Unable to load properties file", e);
            }
        }
    }
    
    
    public static AbstractConfiguration getConfigFromPropertiesFile(URL startingUrl, Set<String> loaded, String... nextLoadKeys) 
            throws FileNotFoundException {
        if (loaded.contains(startingUrl.toExternalForm())) {
            logger.warn(startingUrl + " is already loaded");
            return null;
        }
        PropertiesConfiguration propConfig = null;
        try {
            propConfig = new OverridingPropertiesConfiguration(startingUrl);
            logger.info("Loaded properties file " + startingUrl);            
        } catch (ConfigurationException e) {
            Throwable cause = e.getCause();
            if (cause instanceof FileNotFoundException) {
                throw (FileNotFoundException) cause;
            } else {
                throw new RuntimeException(e);
            }
        }
        
        if (nextLoadKeys == null) {
            return propConfig;
        }
        String urlString = startingUrl.toExternalForm();
        String base = urlString.substring(0, urlString.lastIndexOf("/"));
        loaded.add(startingUrl.toString());
        loadFromPropertiesFile(propConfig, base, loaded, nextLoadKeys);
        return propConfig;
    }
    
    public static void copyProperties(Configuration from, Configuration to) {
        for (Iterator<String> i = from.getKeys(); i.hasNext(); ) {
            String key = i.next();
            if (key != null) {                
                Object value = from.getProperty(key);
                if (value != null) {
                    to.setProperty(key, value);
                }
            }
        }        
    }
    
    public static Properties getPropertiesFromFile(URL startingUrl, Set<String> loaded, String... nextLoadKeys) 
            throws FileNotFoundException {
        AbstractConfiguration config = getConfigFromPropertiesFile(startingUrl, loaded, nextLoadKeys);
        return getProperties(config);
    }


    private static String getNextLoad(Configuration propConfig, String... nextLoadPropertyKeys) {
        String nextLoadKeyToUse = null;
        for (String key: nextLoadPropertyKeys) {
            if (propConfig.getProperty(key) != null) {
                nextLoadKeyToUse = key;
                break;
            }
        }
        // there is no next load for this properties file
        if (nextLoadKeyToUse == null) {
            return null;
        }
        // make a copy of current existing properties
        ConcurrentMapConfiguration config = new ConcurrentMapConfiguration();
        
        // need to have all the properties to interpolate next load property value
        copyProperties(ConfigurationManager.getConfigInstance(), config);
        copyProperties(propConfig, config);
        // In case this is a list of files to load, always treat the value as a list
        List<Object> list = config.getList(nextLoadKeyToUse);
        StringBuilder sb = new StringBuilder();
        for (Object value: list) {
            sb.append(value).append(",");
        }
        String nextLoad = sb.toString();
        propConfig.clearProperty(nextLoadKeyToUse);
        return nextLoad;
    }    
}

class OverridingPropertiesConfiguration extends PropertiesConfiguration {

    public OverridingPropertiesConfiguration() {
        super();
    }

    public OverridingPropertiesConfiguration(File file)
            throws ConfigurationException {
        super(file);
    }

    public OverridingPropertiesConfiguration(String fileName)
            throws ConfigurationException {
        super(fileName);
    }

    public OverridingPropertiesConfiguration(URL url)
            throws ConfigurationException {
        super(url);
    }
    
    /**
     * Need to override this method for PDCLOUD-1809.
     * 
     */
    @Override
    public void addProperty(String name, Object value) {
        if (containsKey(name)) {
            // clear without triggering an event
            clearPropertyDirect(name);
        }
        super.addProperty(name, value);        
    }    
}
