/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2011 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id:RegistryServiceImpl.java 10372 2007-05-14 13:58:42Z sauthieg $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.configadmin.internal;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.osgi.framework.ServiceReference;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.ow2.jonas.configadmin.ConfigurationInfo;
import org.ow2.jonas.configadmin.XmlConfigurationAdapterRegistry;
import org.ow2.util.archive.api.ArchiveException;
import org.ow2.util.ee.deploy.api.deployable.IDeployable;
import org.ow2.util.ee.deploy.api.deployer.DeployerException;
import org.ow2.util.ee.deploy.api.deployer.IDeployerManager;
import org.ow2.util.ee.deploy.api.deployer.UnsupportedDeployerException;
import org.ow2.util.ee.deploy.impl.deployer.AbsDeployerList;
import org.ow2.util.file.FileUtils;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;
import org.ow2.util.xml.DocumentParser;
import org.ow2.util.xml.DocumentParserException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * A {@code ConfigAdminDeployer} knows how to deploy {@code ConfigAdminDeployable} objects.
 *
 * @author Guillaume Sauthier
 */
public class ConfigAdminDeployer extends AbsDeployerList<ConfigAdminDeployable> implements ConfigurationAssociationListener {

    private static final String CONFIG_ADMIN_DEPLOYABLE = "jonas.configadmin.deployable";

    /**
     * Logger.
     */
    private static final Log logger = LogFactory.getLog(ConfigAdminDeployer.class);

    /**
     * Configuration Admin service
     */
    private ConfigurationAdmin configAdmin;

    /**
     * Associate a namespace with available registries.
     * Multiple registries can support the same namespace.
     */
    private Map<String, Map<ServiceReference, XmlConfigurationAdapterRegistry>> registries;

    /**
     * List of instances interested in registry bind/unbind.
     */
    private List<AdapterRegistryListener> listeners;

    /**
     * Currently deployed URLs.
     */
    private Map<String, IDeployable<ConfigAdminDeployable>> deployed;

    public void validate() throws Exception {
        registries = new Hashtable<String, Map<ServiceReference, XmlConfigurationAdapterRegistry>>();
        listeners = new ArrayList<AdapterRegistryListener>();
        deployed = new HashMap<String, IDeployable<ConfigAdminDeployable>>();

        Configuration[] configurations = configAdmin.listConfigurations("(" + CONFIG_ADMIN_DEPLOYABLE + "=true)");
        if (configurations != null) {
            for (Configuration configuration : configurations) {
                configuration.delete();
            }
        }
    }

    public void invalidate() throws Exception {
        List<IDeployable<ConfigAdminDeployable>> deployables = new ArrayList<IDeployable<ConfigAdminDeployable>>(deployed.values());
        for (IDeployable<ConfigAdminDeployable> deployable : deployables) {
            undeploy(deployable);
        }
    }

    /**
     * Bind method for the DeployerManager.
     *
     * @param deployerManager the deployer manager.
     */
    public void registerDeployerManager(final IDeployerManager deployerManager) {
        deployerManager.register(this);
    }

    /**
     * Unbind method for the DeployerManager.
     *
     * @param deployerManager the deployer manager.
     */
    public void unregisterDeployerManager(final IDeployerManager deployerManager) {
        deployerManager.unregister(this);
    }

    /**
     * Bind the reference to the ConfigurationAdmin service
     *
     * @param admin CA service
     */
    public void bindConfigurationAdmin(ConfigurationAdmin admin) {
        this.configAdmin = admin;
    }

    /**
     * Bind a reference to a new {@code XmlConfigurationAdapterRegistry}.
     *
     * @param registry  the new registry
     * @param reference used to get the {@literal namespace} service attribute
     */
    public void bindXmlConfigurationAdapterRegistry(XmlConfigurationAdapterRegistry registry,
            ServiceReference reference) {
        // Get namespace
        String namespace = (String) reference.getProperty("namespace");

        // Store the registry handle
        Map<ServiceReference, XmlConfigurationAdapterRegistry> scoped = this.registries.get(namespace);
        if (scoped == null) {
            scoped = new Hashtable<ServiceReference, XmlConfigurationAdapterRegistry>();
            this.registries.put(namespace, scoped);
        }
        scoped.put(reference, registry);

        firesAdapterRegistryArrival(namespace, registry);
    }

    /**
     * Unbind a reference to an existing {@code XmlConfigurationAdapterRegistry}.
     *
     * @param reference reference to the registry service (its key)
     */
    public void unbindXmlConfigurationAdapterRegistry(ServiceReference reference) {

        // Get namespace
        String namespace = (String) reference.getProperty("namespace");

        // Remove the registry handle
        Map<ServiceReference, XmlConfigurationAdapterRegistry> scoped = this.registries.get(namespace);
        if (scoped != null) {
            XmlConfigurationAdapterRegistry registry = scoped.remove(reference);
            firesAdapterRegistryRemoval(namespace, registry);
        }

    }

    /**
     * Fires an {@code AdapterRegistryEvent} for a new registry arrival.
     *
     * @param namespace Namespace URI supported by the given registry
     * @param registry  arrived registry
     */
    private void firesAdapterRegistryArrival(String namespace, XmlConfigurationAdapterRegistry registry) {
        AdapterRegistryEvent event = new AdapterRegistryEvent(registry, namespace);
        for (AdapterRegistryListener listener : listeners) {
            listener.onAdapterRegistryArrival(event);
        }
    }

    /**
     * Fires an {@code AdapterRegistryEvent} for a registry removal.
     *
     * @param namespace Namespace URI supported by the given registry
     * @param registry  removed registry
     */
    private void firesAdapterRegistryRemoval(String namespace, XmlConfigurationAdapterRegistry registry) {
        AdapterRegistryEvent event = new AdapterRegistryEvent(registry, namespace);
        for (AdapterRegistryListener listener : listeners) {
            listener.onAdapterRegistryRemoval(event);
        }
    }

    public void deploy(IDeployable<ConfigAdminDeployable> untyped) throws DeployerException, UnsupportedDeployerException {
        ConfigAdminDeployable deployable = ConfigAdminDeployable.class.cast(untyped);
        URL url = getUrl(deployable);

        List<ConfigurationItem> items = new ArrayList<ConfigurationItem>();

        InputStream is = null;
        try {
            is = url.openStream();

            Document doc = null;
            try {
                doc = DocumentParser.getDocument(is, false, null);
            } catch (DocumentParserException e) {
                throw new DeployerException("Cannot parse XML " + url, e);
            }

            Element rootElement = doc.getDocumentElement();

            // Iterates on all child nodes
            NodeList children = rootElement.getChildNodes();
            for (int i = 0; i < children.getLength(); i++) {
                Node node = children.item(i);

                // Select only Element(s)
                if (Node.ELEMENT_NODE == node.getNodeType()) {
                    items.add(new ConfigurationItem((Element) node));
                }
            }

        } catch (IOException e) {
            throw new DeployerException("Cannot open " + deployable, e);
        } finally {
            FileUtils.close(is);
        }

        deployable.setAttachedData(items);
        int bound = 0;
        int failed = 0;
        // Process each new ConfigurationItem
        for (ConfigurationItem item : items) {
            processNewConfigurationItem(item);

            // This Configuration has been adapted
            if (item.getConfigurations() != null) {
                bound++;
            }

            // This Configuration has not been adapted
            if (item.isFailed()) {
                failed++;
            }
        }

        logger.info("Deployed {0} Configuration(s) [{1} bound, {2} unbound, {3} failed] for ''{4}''",
                    items.size(),
                    bound,
                    items.size() - bound,
                    failed,
                    deployable.getShortName());

        deployed.put(url.toExternalForm(), untyped);

    }

    private URL getUrl(IDeployable<?> deployable) throws DeployerException {
        URL url;
        try {
            url = deployable.getArchive().getURL();
        } catch (ArchiveException e) {
            throw new DeployerException("Cannot get URL of " + deployable, e);
        }
        return url;
    }

    public void undeploy(IDeployable<ConfigAdminDeployable> deployable) throws DeployerException {

        URL url = getUrl(deployable);

        logger.info("Undeploying Configurations for ''{0}''", deployable.getShortName());

        ConfigAdminDeployable cad = ConfigAdminDeployable.class.cast(deployable);
        if (cad.getAttachedData() == null) {
            // This may happen when using jonas-agent and JMX so lets use the cached one.
            cad = ConfigAdminDeployable.class.cast(deployed.get(url.toExternalForm()));
        }
        if (cad != null && cad.getAttachedData() != null) {
            for (ConfigurationItem item : cad.getAttachedData()) {
                listeners.remove(item);
                item.close();
            }
        }

        deployed.remove(url.toExternalForm());
    }

    public boolean isDeployed(IDeployable<ConfigAdminDeployable> deployable) throws DeployerException {
        String url = getUrl(deployable).toExternalForm();
        return deployed.containsKey(url);
    }

    public boolean supports(IDeployable<?> deployable) {
        return ConfigAdminDeployable.class.isAssignableFrom(deployable.getClass());
    }

    private void processNewConfigurationItem(ConfigurationItem item) {

        // Hook-up with the item
        item.addAssociationListener(this);
        this.listeners.add(item);

        // Test inject all registered adapters
        for (Map.Entry<String, Map<ServiceReference, XmlConfigurationAdapterRegistry>> entry : registries.entrySet()) {
            String namespace = entry.getKey();

            for (XmlConfigurationAdapterRegistry registry : entry.getValue().values()) {
                firesAdapterRegistryArrival(namespace, registry);
            }
        }
    }

    public void onAssociation(ConfigurationAssociationEvent event) {

        // Create & push appropriate Configurations
        Set<Configuration> configurations = new HashSet<Configuration>();
        Set<ConfigurationInfo> info = event.getInfo();
        for (ConfigurationInfo ci : info) {
            if (ci.isFactory()) {
                configurations.add(createFactoryConfiguration(ci));
            } else {
                configurations.add(createConfiguration(ci));
            }
        }
        // Stores the configurations back into the item
        event.getSource().getConfigurations().addAll(configurations);
    }

    public void onDissociation(ConfigurationAssociationEvent event) {
        ConfigurationItem item = event.getSource();
        for (Configuration configuration : item.getConfigurations()) {
            try {
                configuration.delete();
            } catch (IOException e) {
                // Ignored
                // TODO log trace
            }
        }
        item.getConfigurations().clear();

        // Tries to re-associate the item with remaining Registries
        for (Map.Entry<String, Map<ServiceReference, XmlConfigurationAdapterRegistry>> entry : registries.entrySet()) {
            String namespace = entry.getKey();

            for (XmlConfigurationAdapterRegistry registry : entry.getValue().values()) {
                AdapterRegistryEvent are = new AdapterRegistryEvent(registry, namespace);
                item.onAdapterRegistryArrival(are);
            }
        }

    }

    /**
     * Creates a CA Factory Configuration rom an adapted ConfigurationInfo
     *
     * @param configuration adapted configuration
     *
     * @return a CA Factory Configuration
     */
    private Configuration createFactoryConfiguration(final ConfigurationInfo configuration) {

        try {
            String servicePid = configuration.getPid();

            logger.debug("Create FactoryConfiguration for {0}", servicePid);

            // Create the factory configuration
            Configuration c = configAdmin.createFactoryConfiguration(servicePid, null);

            Dictionary<String, Object> properties = new Hashtable<String, Object>();
            for (Map.Entry<String, Object> entry : configuration.getProperties().entrySet()) {
                properties.put(entry.getKey(), entry.getValue());
            }
            properties.put(CONFIG_ADMIN_DEPLOYABLE, "true");

            c.update(properties);

            logger.debug("Updated Factory {0}", c);

            return c;

        } catch (IOException e) {
            logger.warn("Cannot push Factory Configuration {0}", configuration.getPid(), e);
        }

        return null;
    }

    /**
     * Creates a CA Configuration rom an adapted ConfigurationInfo
     *
     * @param configuration adapted configuration
     *
     * @return a CA Configuration
     */
    private Configuration createConfiguration(final ConfigurationInfo configuration) {

        try {
            String servicePid = configuration.getPid();

            logger.debug("Create Configuration for {0}", servicePid);

            // Create the Configuration
            Configuration c = configAdmin.getConfiguration(servicePid, null);

            Dictionary<String, Object> properties = c.getProperties();
            if (properties == null) {
                properties = new Hashtable<String, Object>();
            }

            for (Map.Entry<String, Object> entry : configuration.getProperties().entrySet()) {
                properties.put(entry.getKey(), entry.getValue());
            }
            properties.put(CONFIG_ADMIN_DEPLOYABLE, "true");

            if (c.getBundleLocation() != null) {
                c.setBundleLocation(null);
            }

            c.update(properties);

            logger.debug("Updated {0}", c);

            return c;

        } catch (IOException e) {
            logger.warn("Cannot push Configuration {0}", configuration.getPid(), e);
        }

        return null;
    }
}
