/**
 *  Copyright 2005-2015 Red Hat, Inc.
 *
 *  Red Hat 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 io.fabric8.configadmin;

import io.fabric8.api.Constants;
import io.fabric8.api.Container;
import io.fabric8.api.FabricService;
import io.fabric8.api.Profile;
import io.fabric8.api.Profiles;
import io.fabric8.api.jcip.ThreadSafe;
import io.fabric8.api.scr.AbstractComponent;
import io.fabric8.api.scr.ValidatingReference;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import io.fabric8.utils.NamedThreadFactory;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Reference;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.url.URLStreamHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ThreadSafe
@Component(name = "io.fabric8.configadmin.bridge", label = "Fabric8 Config Admin Bridge", metatype = false)
public final class FabricConfigAdminBridge extends AbstractComponent implements Runnable {

    public static final String FABRIC_ZOOKEEPER_PID = "fabric.zookeeper.pid";
    /**
     * Configuration property that indicates if old configuration should be preserved.
     * By default, previous configuration is discarded and new is used instead.
     * When setting this property to <code>true</code>, new values override existing ones
     * and the old values are kept unchanged.
     */
    public static final String FABRIC_CONFIG_MERGE = "fabric.config.merge";
    public static final String LAST_MODIFIED = "lastModified";

    private static final Logger LOGGER = LoggerFactory.getLogger(FabricConfigAdminBridge.class);

    @Reference(referenceInterface = ConfigurationAdmin.class)
    private final ValidatingReference<ConfigurationAdmin> configAdmin = new ValidatingReference<ConfigurationAdmin>();
    @Reference(referenceInterface = FabricService.class)
    private final ValidatingReference<FabricService> fabricService = new ValidatingReference<FabricService>();
    @Reference(referenceInterface = URLStreamHandlerService.class, target = "(url.handler.protocol=profile)")
    private final ValidatingReference<URLStreamHandlerService> urlHandler = new ValidatingReference<URLStreamHandlerService>();

    private final ExecutorService executor = Executors.newSingleThreadExecutor(new NamedThreadFactory("fabric-configadmin"));

    @Activate
    void activate() {
        fabricService.get().trackConfiguration(this);
        activateComponent();
        submitUpdateJob();
    }

    @Deactivate
    void deactivate() {
        deactivateComponent();
        fabricService.get().untrackConfiguration(this);
        executor.shutdown();
        try {
            executor.awaitTermination(1, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            // Ignore
        }
        executor.shutdownNow();
    }

    @Override
    public void run() {
        submitUpdateJob();
    }

    private void submitUpdateJob() {
        executor.submit(new Runnable() {
            @Override
            public void run() {
                if (isValid()) {
                    try {
                        updateInternal();
                    } catch (Throwable th) {
                        if (isValid()) {
                            LOGGER.warn("Exception when tracking configurations. This exception will be ignored.", th);
                        } else {
                            LOGGER.debug("Exception when tracking configurations. This exception will be ignored because services have been unbound in the mean time.", th);
                        }
                    }
                }
            }
        });
    }

    private synchronized void updateInternal() throws Exception {
        
        Container currentContainer = fabricService.get().getCurrentContainer();
        if (currentContainer == null) {
            LOGGER.warn("No current container yet so cannot update!");
            return;
        }
        Profile overlayProfile = currentContainer.getOverlayProfile();
        Profile effectiveProfile = Profiles.getEffectiveProfile(fabricService.get(), overlayProfile);
        
        Map<String, Map<String, String>> configurations = effectiveProfile.getConfigurations();
        List<Configuration> zkConfigs = asList(configAdmin.get().listConfigurations("(" + FABRIC_ZOOKEEPER_PID + "=*)"));
        
        // FABRIC-803: the agent may use the configuration provided by features definition if not managed
        //   by fabric.  However, in order for this to work, we need to make sure managed configurations
        //   are all registered before the agent kicks in.  Hence, the agent configuration is updated
        //   after all other configurations.
        
        // Process all configurations but agent
        for (String pid : configurations.keySet()) {
            if (!pid.equals(Constants.AGENT_PID)) {
                Hashtable<String, Object> c = new Hashtable<String, Object>();
                c.putAll(configurations.get(pid));
                updateConfig(zkConfigs, pid, c);
            }
        }
        // Process agent configuration last
        for (String pid : configurations.keySet()) {
            if (pid.equals(Constants.AGENT_PID)) {
                Hashtable<String, Object> c = new Hashtable<String, Object>();
                c.putAll(configurations.get(pid));
                c.put(Profile.HASH, String.valueOf(effectiveProfile.getProfileHash()));
                updateConfig(zkConfigs, pid, c);
            }
        }
        for (Configuration config : zkConfigs) {
            LOGGER.info("Deleting configuration {}", config.getPid());
            fabricService.get().getPortService().unregisterPort(fabricService.get().getCurrentContainer(), config.getPid());
            config.delete();
        }
    }

    private void updateConfig(List<Configuration> configs, String pid, Hashtable<String, Object> c) throws Exception {
        String p[] = parsePid(pid);
        //Get the configuration by fabric zookeeper pid, pid and factory pid.
        Configuration config = getConfiguration(configAdmin.get(), pid, p[0], p[1]);
        configs.remove(config);
        Dictionary<String, Object> props = config.getProperties();
        Hashtable<String, Object> old = props != null ? new Hashtable<String, Object>() : null;
        if (old != null) {
            for (Enumeration<String> e = props.keys(); e.hasMoreElements();) {
                String key = e.nextElement();
                Object val = props.get(key);
                old.put(key, val);
            }
            old.remove(FABRIC_ZOOKEEPER_PID);
            old.remove(org.osgi.framework.Constants.SERVICE_PID);
            old.remove(ConfigurationAdmin.SERVICE_FACTORYPID);
        }
        if (!c.equals(old)) {
            LOGGER.info("Updating configuration {}", config.getPid());
            c.put(FABRIC_ZOOKEEPER_PID, pid);
            // FABRIC-1078. Do *not* set the bundle location to null
            // causes org.apache.felix.cm.impl.ConfigurationManager.locationChanged() to be fired and:
            // - one of the listeners (SCR) tries to getConfiguration(pid)
            // - bundle location is checked to be null
            // - bundle location is set
            // - org.apache.felix.cm.impl.ConfigurationBase.storeSilently() is invoked
            // and all of this *after* the below config.update(), only with *old* values
//            if (config.getBundleLocation() != null) {
//                config.setBundleLocation(null);
//            }
            if ("true".equals(c.get(FABRIC_CONFIG_MERGE)) && old != null) {
                // CHECK: if we remove FABRIC_CONFIG_MERGE property, profile update
                // would remove the original properties - only profile-defined will be used
                //c.remove(FABRIC_CONFIG_MERGE);
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("");
                }
                old.putAll(c);
                c = old;
            }
            config.update(c);
        } else {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Ignoring configuration {} (no changes)", config.getPid());
            }
        }
    }

    @SuppressWarnings("unchecked")
    private <T> List<T> asList(T... a) {
        List<T> l = new ArrayList<T>();
        if (a != null) {
            Collections.addAll(l, a);
        }
        return l;
    }

    /**
     * Splits a pid into service and factory pid.
     *
     * @param pid The pid to parse.
     * @return An arrays which contains the pid[0] the pid and pid[1] the factory pid if applicable.
     */
    private String[] parsePid(String pid) {
        int n = pid.indexOf('-');
        if (n > 0) {
            String factoryPid = pid.substring(n + 1);
            pid = pid.substring(0, n);
            return new String[]{pid, factoryPid};
        } else {
            return new String[]{pid, null};
        }
    }

    private Configuration getConfiguration(ConfigurationAdmin configAdmin, String zooKeeperPid, String pid, String factoryPid) throws Exception {
        String filter = "(" + FABRIC_ZOOKEEPER_PID + "=" + zooKeeperPid + ")";
        Configuration[] oldConfiguration = configAdmin.listConfigurations(filter);
        if (oldConfiguration != null && oldConfiguration.length > 0) {
            return oldConfiguration[0];
        } else {
            Configuration newConfiguration;
            if (factoryPid != null) {
                newConfiguration = configAdmin.createFactoryConfiguration(pid, null);
            } else {
                newConfiguration = configAdmin.getConfiguration(pid, null);
            }
            return newConfiguration;
        }
    }

    void bindConfigAdmin(ConfigurationAdmin service) {
        this.configAdmin.bind(service);
    }

    void unbindConfigAdmin(ConfigurationAdmin service) {
        this.configAdmin.unbind(service);
    }

    void bindFabricService(FabricService fabricService) {
        this.fabricService.bind(fabricService);
    }

    void unbindFabricService(FabricService fabricService) {
        this.fabricService.unbind(fabricService);
    }

    void bindUrlHandler(URLStreamHandlerService urlHandler) {
        this.urlHandler.bind(urlHandler);
    }

    void unbindUrlHandler(URLStreamHandlerService urlHandler) {
        this.urlHandler.unbind(urlHandler);
    }

}
