/**
 * 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.aries.blueprint.compendium.cm;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Hashtable;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.cm.ConfigurationPlugin;

public class CmUtils {

    private CmUtils() {        
    }
    
    public static Configuration getConfiguration(ConfigurationAdmin configAdmin, String persistentId) throws IOException {
        String filter = '(' + Constants.SERVICE_PID + '=' + persistentId + ')';
        Configuration[] configs;
        try {
            configs = configAdmin.listConfigurations(filter);
        } catch (InvalidSyntaxException e) {
            // this should not happen
            throw new RuntimeException("Invalid filter: " + filter);
        }
        if (configs != null && configs.length > 0) {
            return configs[0];
        } else {
            // TODO: what should we do?
            // throw new RuntimeException("No configuration object for pid=" + persistentId);
            return null;
        }
    }

    public static Dictionary<String, Object> getProperties(ServiceReference service, String persistentId) throws IOException {
        BundleContext bc = service.getBundle().getBundleContext();
        ServiceReference<ConfigurationAdmin> caRef = bc.getServiceReference(ConfigurationAdmin.class);
        try {
            ConfigurationAdmin ca = bc.getService(caRef);
            Configuration config = getConfiguration(ca, persistentId);
            if (config != null) {
                Dictionary<String, Object> props = new CaseInsensitiveDictionary(config.getProperties());
                Bundle bundle = caRef.getBundle();
                if (bundle != null) {
                    BundleContext caBc = bundle.getBundleContext();
                    if (caBc != null) {
                        try {
                            callPlugins(caBc, props, service, persistentId, null);
                        } catch (IllegalStateException ise) {
                            // we don't care it doesn't exist so, shrug.
                        }
                    }
                }
                return props;
            } else {
                return null;
            }
        } finally {
            bc.ungetService(caRef);
        }
    }

    private static void callPlugins(final BundleContext bundleContext,
                                    final Dictionary<String, Object> props,
                                    final ServiceReference sr,
                                    final String configPid,
                                    final String factoryPid) {
        ServiceReference[] plugins = null;
        try {
            final String targetPid = (factoryPid == null) ? configPid : factoryPid;
            // String filter = "(|(!(cm.target=*))(cm.target=" + targetPid + "))";
            // patch for https://issues.apache.org/jira/browse/ARIES-2076
            String filter = "(|(cm.target=*)(cm.target=" + targetPid + "))";
            plugins = bundleContext.getServiceReferences(ConfigurationPlugin.class.getName(), filter);
        } catch (InvalidSyntaxException ise) {
            // no filter, no exception ...
        }

        // abort early if there are no plugins
        if (plugins == null || plugins.length == 0) {
            return;
        }

        // sort the plugins by their service.cmRanking
        if (plugins.length > 1) {
            Arrays.sort(plugins, CM_RANKING);
        }

        // call the plugins in order
        for (ServiceReference pluginRef : plugins) {
            ConfigurationPlugin plugin = (ConfigurationPlugin) bundleContext.getService(pluginRef);
            if (plugin != null) {
                try {
                    plugin.modifyConfiguration(sr, props);
                } catch (Throwable t) {
                    // Ignore
                } finally {
                    // ensure ungetting the plugin
                    bundleContext.ungetService(pluginRef);
                }
                setAutoProperties(props, configPid, factoryPid);
            }
        }
    }

    private static void setAutoProperties( Dictionary<String, Object> properties, String pid, String factoryPid )
    {
        replaceProperty(properties, Constants.SERVICE_PID, pid);
        replaceProperty(properties, ConfigurationAdmin.SERVICE_FACTORYPID, factoryPid);
        properties.remove(ConfigurationAdmin.SERVICE_BUNDLELOCATION);
    }

    private static void replaceProperty(Dictionary<String, Object> properties, String key, String value) {
        if (value == null) {
            properties.remove(key);
        } else {
            properties.put(key, value);
        }
    }

    private static Comparator<ServiceReference> CM_RANKING = new Comparator<ServiceReference>() {
        @Override
        public int compare(ServiceReference sr1, ServiceReference sr2) {
            final long rank1 = getLong(sr1, ConfigurationPlugin.CM_RANKING);
            final long rank2 = getLong(sr2, ConfigurationPlugin.CM_RANKING);
            if (rank1 == rank2) {
                return 0;
            }
            return (rank1 < rank2) ? -1 : 1;
        }

        protected long getLong(ServiceReference sr, String property) {
            Object rankObj = sr.getProperty(property);
            if (rankObj instanceof Number) {
                return ((Number) rankObj).longValue();
            }
            return 0;
        }
    };

    private static class CaseInsensitiveDictionary extends Dictionary<String, Object> {
        private final Hashtable<String, Object> internalMap = new Hashtable<String, Object>();
        private final Hashtable<String, String> originalKeys = new Hashtable<String, String>();

        public CaseInsensitiveDictionary(Dictionary<String, Object> props) {
            if (props != null) {
                Enumeration<String> keys = props.keys();
                while (keys.hasMoreElements()) {
                    // check the correct syntax of the key
                    String key = checkKey(keys.nextElement());
                    // check uniqueness of key
                    String lowerCase = key.toLowerCase();
                    if (internalMap.containsKey(lowerCase)) {
                        throw new IllegalArgumentException("Key [" + key + "] already present in different case");
                    }
                    // check the value
                    Object value = props.get(key);
                    checkValue(value);
                    // add the key/value pair
                    internalMap.put(lowerCase, value);
                    originalKeys.put(lowerCase, key);
                }
            }
        }

        public Enumeration<Object> elements() {
            return Collections.enumeration(internalMap.values());
        }

        public Object get(Object keyObj) {
            String lowerCase = checkKey(keyObj == null ? null : keyObj.toString()).toLowerCase();
            return internalMap.get(lowerCase);
        }

        public boolean isEmpty() {
            return internalMap.isEmpty();
        }

        public Enumeration<String> keys() {
            return Collections.enumeration(originalKeys.values());
        }

        public Object put(String key, Object value) {
            String lowerCase = checkKey(key).toLowerCase();
            checkValue(value);
            originalKeys.put(lowerCase, key);
            return internalMap.put(lowerCase, value);
        }

        public Object remove(Object keyObj) {
            String lowerCase = checkKey(keyObj == null ? null : keyObj.toString()).toLowerCase();
            originalKeys.remove(lowerCase);
            return internalMap.remove(lowerCase);
        }

        public int size() {
            return internalMap.size();
        }

        static String checkKey(String key) {
            if (key == null || key.length() == 0) {
                throw new IllegalArgumentException("Key must not be null nor an empty string");
            }
            return key;
        }

        static Object checkValue(Object value) {
            if (value == null) {
                throw new IllegalArgumentException("Value must not be null");
            }
            return value;
        }

        public String toString() {
            return internalMap.toString();
        }

    }

}
