/*
 * Copyright 2009 OW2 Chameleon
 * 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 org.ow2.chameleon.core;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.List;
import java.util.Properties;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Activators to starts and parsed configuration.
 * @author <a href="mailto:chameleon-dev@ow2.org">Chameleon Project Team</a>
 */
public class BundleInstaller implements BundleActivator, ServiceListener {

    /**
     * Chameleon logger.
     */
    private static Logger logger = LoggerFactory
            .getLogger(Chameleon.CHAMELEON_LOGGER_NAME);

    /**
     * The directory to analyze.
     */
    private File directory;

    /**
     * The embedded framework bundle context.
     */
    private BundleContext context;

    /**
     * The list of the .cfg files.
     */
    private List configs;

    /**
     * Initializes the installer.
     * @param dir the directory.
     */
    public BundleInstaller(File dir) {
        directory = dir;
    }

    /**
     * Collects bundles to install and begins the ConfigurationAdmin service
     * tracking.
     * @param arg0 the bundle context
     * @throws Exception if the Bundle Installer cannot be started correctly
     * @see org.osgi.framework.BundleActivator
     *  #start(org.osgi.framework.BundleContext)
     */
    public void start(BundleContext arg0) throws Exception {
        context = arg0;
        List/* <File> */jars = new ArrayList();
        List/* <File> */exploded = new ArrayList();
        configs = new ArrayList();
        traverse(jars, exploded, configs, directory);

        installBundles(arg0, jars, exploded);
        context.addServiceListener(this, "(" + Constants.OBJECTCLASS + "="
                + ConfigurationAdmin.class.getName() + ")");

        if (context.getServiceReference(ConfigurationAdmin.class.getName())
                != null) {
            parseConfiguration(context, configs);
            logger.info("Configurations from "
                    + directory.getAbsoluteFile() + " parsed");
        }

    }

    /**
     * Parses configuration file.
     * @param context the context
     * @param configs the configuration file list.
     * @throws Exception if the configuration cannot be parsed correctly, or if
     *         the Configuration Admin is not available.
     */
    private void parseConfiguration(BundleContext context, List configs)
            throws Exception {
        ConfigurationAdmin admin = getConfigurationAdmin(context);
        for (int i = 0; i < configs.size(); i++) {
            File f = (File) configs.get(i);
            Properties p = new Properties();
            InputStream in = new FileInputStream(f);
            p.load(in);
            in.close();
            String[] pid = parsePid(f.getName());
            Hashtable ht = new Hashtable();
            ht.putAll(p);
            Configuration config = getConfiguration(pid[0], pid[1], admin);
            if (config.getBundleLocation() != null) {
                config.setBundleLocation(null);
            }
            logger.info("A configuration will be pushed " + ht);
            config.update(ht);
        }

    }

    /**
     * Gets the configuration admin service.
     * @param context the bundle context.
     * @return the Configuration Admin service object
     * @throws Exception if the configuration admin is unavailable.
     */
    private ConfigurationAdmin getConfigurationAdmin(BundleContext context)
            throws Exception {
        // Should be there !
        ServiceReference ref = context
                .getServiceReference(ConfigurationAdmin.class.getName());
        if (ref == null) {
            throw new Exception("Configuration Admin unavailable !");
        } else {
            return (ConfigurationAdmin) context.getService(ref);
        }

    }

    /**
     * Installs and starts the list of found bundles.
     * @param context the context
     * @param jars the list of bundles
     * @param exploded the list of exploded bundles
     * @throws IOException if the bundle cannot be read
     * @throws BundleException if the bundle cannot be started correctly
     */
    private void installBundles(BundleContext context, List jars, List exploded)
            throws IOException, BundleException {
        List bundles = new ArrayList();
        for (int i = 0; i < jars.size(); i++) {
            File file = (File) jars.get(i);
            if (file.getName().endsWith("jar")) {
                JarFile jar = new JarFile(file);
                if (jar.getManifest().getMainAttributes().getValue(
                        "Bundle-ManifestVersion") != null) {
                    logger.info("Installing " + file.getName());
                    Bundle bundle = context.installBundle("reference:" + file.toURI().toURL()
                            .toExternalForm());
                    bundles.add(bundle);
                } else {
                    logger.warn("The file " + file.getName()
                            + " is not a valid bundle -> ignored");
                }
            }
        }

        for (int i = 0; i < exploded.size(); i++) {
        	// Check that the manifest contains Bundle-ManifestVersion
        	File dir = (File) exploded.get(i);
        	File manifestFile = new File(dir, "META-INF/MANIFEST.MF");
        	// We already know that the manifest exist
        	Manifest manifest = new Manifest(new FileInputStream(manifestFile));
        	if (manifest.getMainAttributes().getValue("Bundle-ManifestVersion") != null) {
    			logger.info("Installing " + dir.getName());
    			Bundle bundle = context.installBundle("reference:" + dir.toURI().toURL()
    					.toExternalForm());
    			bundles.add(bundle);
        	} else {
        		logger.warn("The file " + dir.getName()
        				+ " is not a valid bundle -> ignored");
        	}
        }


        for (int i = 0; i < bundles.size(); i++) {
            Bundle bundle = (Bundle) bundles.get(i);
            if (isFragment(bundle)) {
                logger.info("Fragment detected " + bundle.getSymbolicName());
            } else {
                logger.info("Starting " + bundle.getSymbolicName());
                bundle.start();
            }
        }
    }

    /**
     * Stops configuration admin tracking.
     * @param arg0 the bundle context
     * @throws Exception if the Bundle Installer cannot be stopped correctly
     * @see org.osgi.framework.BundleActivator
     *  #stop(org.osgi.framework.BundleContext)
     */
    public void stop(BundleContext arg0) throws Exception {
        context.removeServiceListener(this);
    }

    /**
     * Traverses the directory structure to find bundles and configuration
     * files.
     * @param jars the found bundle list
     * @param exploded the found expanded bundle list
     * @param configs the found .cfg files.
     * @param dir the directory to traverse.
     */
    private void traverse(List/* <File> */jars, List/* <File> */exploded, List/* <File> */configs,
            File dir) {
        String[] list = dir.list();
        for (int i = 0; i < list.length; i++) {
            File file = new File(dir, list[i]);
            if (file.isFile()) {
	            if (list[i].endsWith(".jar")) {
	                jars.add(file);
	            } else if (list[i].endsWith(".cfg")) {
	                configs.add(file);
	            }
            } else {
            	// Directory, check if it's an exploded bundles by checking the presence of the
            	// Manifest file
            	File manifest = new File(file, "META-INF/MANIFEST.MF");
            	if (manifest.exists()) {
            		exploded.add(file);
            	}
            }

        }
    }

    /**
     * Parses cfg file associated PID. This supports both ManagedService PID and
     * ManagedServiceFactory PID
     * @param path the path
     * @return structure {pid, factory pid} or {pid, <code>null</code> if not a
     *         Factory configuration.
     */
    String[] parsePid(String path) {
        String pid = path.substring(0, path.length() - ".cfg".length());
        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};
        }
    }

    /**
     * Gets a Configuration object.
     * @param pid the pid
     * @param factoryPid the factory pid
     * @param cm the config admin service
     * @return the Configuration object (used to update the configuration)
     * @throws Exception if the Configuration object cannot be retrieved
     */
    Configuration getConfiguration(String pid, String factoryPid,
            ConfigurationAdmin cm) throws Exception {
        Configuration newConfiguration = null;
        if (factoryPid != null) {
            newConfiguration = cm.createFactoryConfiguration(pid, null);
        } else {
            newConfiguration = cm.getConfiguration(pid, null);
        }
        return newConfiguration;
    }

    /**
     * Service Listener method to track the configuration admin service.
     * @param event the service event
     * @see org.osgi.framework.ServiceListener
     *  #serviceChanged(org.osgi.framework.ServiceEvent)
     */
    public void serviceChanged(ServiceEvent event) {
        if (event.getType() == ServiceEvent.REGISTERED) {
            try {
                parseConfiguration(context, configs);
                logger.info("Configurations from "
                        + directory.getAbsoluteFile() + " parsed");
            } catch (Exception e) {
                logger
                        .error("Configurations from "
                                + directory.getAbsoluteFile()
                                + " can not be parsed correctly : "
                                + e.getMessage(), e);
            }

        }
    }

    /**
     * Checks if a bundle is a fragment.
     * It checks if the manifest contains the fragment
     * host header.
     * @param bundle the bundle to check
     * @return true if the bundle is a fragment.
     */
    private boolean isFragment(Bundle bundle) {
        Dictionary headers = bundle.getHeaders();
        Object header = (Object) headers.get(Constants.FRAGMENT_HOST);
        return (header != null);
    }


}
