/*
 * 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.servicemix.geronimo;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

import javax.jbi.component.Component;
import javax.jbi.component.ServiceUnitManager;
import javax.management.MalformedObjectNameException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.geronimo.common.DeploymentException;
import org.apache.geronimo.deployment.ConfigurationBuilder;
import org.apache.geronimo.deployment.DeploymentContext;
import org.apache.geronimo.deployment.ModuleIDBuilder;
import org.apache.geronimo.deployment.service.EnvironmentBuilder;
import org.apache.geronimo.deployment.util.DeploymentUtil;
import org.apache.geronimo.deployment.xbeans.EnvironmentType;
import org.apache.geronimo.gbean.AbstractName;
import org.apache.geronimo.gbean.AbstractNameQuery;
import org.apache.geronimo.gbean.GBeanData;
import org.apache.geronimo.gbean.GBeanInfo;
import org.apache.geronimo.gbean.GBeanInfoBuilder;
import org.apache.geronimo.j2ee.j2eeobjectnames.NameFactory;
import org.apache.geronimo.kernel.GBeanNotFoundException;
import org.apache.geronimo.kernel.Kernel;
import org.apache.geronimo.kernel.config.ConfigurationAlreadyExistsException;
import org.apache.geronimo.kernel.config.ConfigurationModuleType;
import org.apache.geronimo.kernel.config.ConfigurationStore;
import org.apache.geronimo.kernel.config.ConfigurationUtil;
import org.apache.geronimo.kernel.repository.Artifact;
import org.apache.geronimo.kernel.repository.ArtifactResolver;
import org.apache.geronimo.kernel.repository.Environment;
import org.apache.geronimo.kernel.repository.ImportType;
import org.apache.geronimo.kernel.repository.Repository;
import org.apache.servicemix.geronimo.deployment.SMJbiDocument;
import org.apache.servicemix.jbi.deployment.Descriptor;
import org.apache.servicemix.jbi.deployment.DescriptorFactory;
import org.apache.servicemix.jbi.deployment.ServiceUnit;
import org.apache.servicemix.jbi.deployment.SharedLibraryList;
import org.apache.xmlbeans.XmlObject;

public class ServiceMixConfigBuilder implements ConfigurationBuilder {

    private static final Log log = LogFactory.getLog(ServiceMixConfigBuilder.class);

    private final Environment defaultEnvironment;

    private final Collection repositories;

    private final AbstractNameQuery containerName;

    private final Kernel kernel;

    public static final GBeanInfo GBEAN_INFO;

    static {
        GBeanInfoBuilder infoFactory = new GBeanInfoBuilder(ServiceMixConfigBuilder.class, NameFactory.CONFIG_BUILDER);
        infoFactory.addInterface(ConfigurationBuilder.class);
        infoFactory.addAttribute("defaultEnvironment", Environment.class, true, true);
        infoFactory.addAttribute("kernel", Kernel.class, false);
        infoFactory.addAttribute("containerName", AbstractNameQuery.class, true, true);
        infoFactory.addReference("Repositories", Repository.class, "Repository");
        infoFactory.setConstructor(new String[] { "defaultEnvironment", "containerName", "Repositories", "kernel" });
        GBEAN_INFO = infoFactory.getBeanInfo();
    }

    public static GBeanInfo getGBeanInfo() {
        return GBEAN_INFO;
    }

    public ServiceMixConfigBuilder(Environment defaultEnvironment, AbstractNameQuery containerName, Collection repositories, Kernel kernel) {
        this.defaultEnvironment = defaultEnvironment;
        this.repositories = repositories;
        this.kernel = kernel;
        this.containerName = containerName;
    }

    /**
     * Builds a deployment plan specific to this builder from a planFile and/or
     * module if this builder can process it.
     * 
     * @param planFile
     *            the deployment plan to examine; can be null
     * @param module
     *            the URL of the module to examine; can be null
     * @return the deployment plan, or null if this builder can not handle the
     *         module
     * @throws org.apache.geronimo.common.DeploymentException
     *             if there was a problem with the configuration
     */
    public Object getDeploymentPlan(File planFile, JarFile module, ModuleIDBuilder idBuilder)
                    throws DeploymentException {
        log.debug("Checking for ServiceMix deployment.");
        if (module == null) {
            return null;
        }

        // Check that the jbi descriptor is present
        try {
            URL url = DeploymentUtil.createJarURL(module, "META-INF/jbi.xml");
            Descriptor descriptor = DescriptorFactory.buildDescriptor(url);
            if (descriptor == null) {
                return null;
            }
            DescriptorFactory.checkDescriptor(descriptor);
            
            XmlObject object = null;
			SMJbiDocument geronimoPlan = null;
			
			try {
				if (planFile != null) {
					object = XmlObject.Factory.parse(planFile);
				}
			} catch (Exception e) {
				log.info("error " + e);
			}
			
			if (object != null) {
				try {
					
					if (object instanceof SMJbiDocument) {
						geronimoPlan = (SMJbiDocument) object;	
					} else {
						geronimoPlan = (SMJbiDocument) object.changeType(SMJbiDocument.type);
					}
					
				} catch (Exception e) {
					throw new DeploymentException("Geronimo Plan found but wrong format!" + e.getMessage());
				}
			}
						
            return new DeploymentPlanWrapper(descriptor, geronimoPlan);
        } catch (Exception e) {
            log.debug("Not a ServiceMix deployment: no jbi.xml found.", e);
            // no jbi.xml, not for us
            return null;
        }
    }

    /**
     * Checks what configuration URL will be used for the provided module.
     * 
     * @param plan
     *            the deployment plan
     * @param module
     *            the module to build
     * @return the ID that will be used for the Configuration
     * @throws IOException
     *             if there was a problem reading or writing the files
     * @throws org.apache.geronimo.common.DeploymentException
     *             if there was a problem with the configuration
     */
    public Artifact getConfigurationID(Object plan, JarFile module, ModuleIDBuilder idBuilder) throws IOException,
                    DeploymentException {
        DeploymentPlanWrapper wrapper = (DeploymentPlanWrapper) plan;
        Descriptor descriptor = wrapper.getServicemixDescriptor();
        if (descriptor.getComponent() != null) {
            return new Artifact("servicemix-components", descriptor.getComponent().getIdentification().getName(),
                            "0.0", "car");
        } else if (descriptor.getServiceAssembly() != null) {
            return new Artifact("servicemix-assemblies", descriptor.getServiceAssembly().getIdentification().getName(),
                            "0.0", "car");
        } else if (descriptor.getSharedLibrary() != null) {
            return new Artifact("servicemix-libraries", descriptor.getSharedLibrary().getIdentification().getName(),
                            descriptor.getSharedLibrary().getVersion(), "car");
        } else {
            throw new DeploymentException("Unable to construct configuration ID " + module.getName()
                            + ": unrecognized jbi package. Should be a component, assembly or library.");
        }
    }

    /**
     * Build a configuration from a local file
     * 
     * @param inPlaceDeployment
     * @param configId
     * @param plan
     * @param earFile
     * @param configurationStores
     * @param artifactResolver
     * @param targetConfigurationStore
     * @return the DeploymentContext information
     * @throws IOException
     *             if there was a problem reading or writing the files
     * @throws org.apache.geronimo.common.DeploymentException
     *             if there was a problem with the configuration
     */
    public DeploymentContext buildConfiguration(boolean inPlaceDeployment, Artifact configId, Object plan,
                    JarFile jarFile, Collection configurationStores, ArtifactResolver artifactResolver,
                    ConfigurationStore targetConfigurationStore) throws IOException, DeploymentException {
        if (plan == null) {
            log.warn("Expected a Descriptor but received null");
            return null;
        }
        if (plan instanceof DeploymentPlanWrapper == false) {
            log.warn("Expected a Descriptor but received a " + plan.getClass().getName());
            return null;
        }
        if (((DeploymentPlanWrapper)plan).getServicemixDescriptor() == null) {
            log.warn("Expected a SM Descriptor but received null");
            return null;
        }
        File configurationDir;
        try {
            configurationDir = targetConfigurationStore.createNewConfigurationDir(configId);
        } catch (ConfigurationAlreadyExistsException e) {
            throw new DeploymentException(e);
        }

        Environment environment = new Environment();
        environment.setConfigId(configId);
        EnvironmentBuilder.mergeEnvironments(environment, defaultEnvironment);

        DeploymentPlanWrapper wrapper = (DeploymentPlanWrapper) plan;
        if (wrapper.getGeronimoPlan() != null) {
                	
        	if (wrapper.getGeronimoPlan().getJbi() != null) {
        		EnvironmentType environmentType = wrapper.getGeronimoPlan().getJbi().getEnvironment();
        		if (environmentType != null) {
        			log.debug("Environment found in Geronimo Plan for Servicemix " + environmentType);
        			Environment geronimoPlanEnvironment = EnvironmentBuilder.buildEnvironment(environmentType);
        			EnvironmentBuilder.mergeEnvironments(environment, geronimoPlanEnvironment);
        		} else {
        			log.debug("no additional environment entry found in deployment plan for JBI component");
        		}
        	}
        }

        DeploymentContext context = null;
        try {
            Descriptor descriptor = wrapper.getServicemixDescriptor();
            Map name = new HashMap();
            name.put("Config", configId.toString());
            context = new DeploymentContext(configurationDir,
                            inPlaceDeployment ? DeploymentUtil.toFile(jarFile) : null, environment,
                            new AbstractName(configId, name),
                            ConfigurationModuleType.SERVICE, kernel.getNaming(), ConfigurationUtil
                                            .getConfigurationManager(kernel), repositories);
            if (descriptor.getComponent() != null) {
                buildComponent(descriptor, context, jarFile);
            } else if (descriptor.getServiceAssembly() != null) {
                buildServiceAssembly(descriptor, context, jarFile);
            } else if (descriptor.getSharedLibrary() != null) {
                buildSharedLibrary(descriptor, context, jarFile);
            } else {
                throw new IllegalStateException("Invalid jbi descriptor");
            }
        } catch (Exception e) {
            if (context != null) {
                context.close();
            }
            DeploymentUtil.recursiveDelete(configurationDir);
            throw new DeploymentException("Unable to deploy", e);
        }

        return context;
    }

    protected void buildComponent(Descriptor descriptor, DeploymentContext context, JarFile module) throws Exception {
        Environment environment = context.getConfiguration().getEnvironment();
        // Unzip the component
        File targetDir = new File(context.getBaseDir(), "install");
        targetDir.mkdirs();
        unzip(context, module, new URI("install/"));
        // Create workspace dir
        File workDir = new File(context.getBaseDir(), "workspace");
        workDir.mkdirs();
        // Create the bootstrap and perform installation
        // TODO: Create the bootstrap and perform installation
        // Add classpath entries
        if ("self-first".equals(descriptor.getComponent().getComponentClassLoaderDelegation())) {
            context.getConfiguration().getEnvironment().setInverseClassLoading(true);
        }
        SharedLibraryList[] slList = descriptor.getComponent().getSharedLibraries();
        if (slList != null) {
            for (int i = 0; i < slList.length; i++) {
                Artifact sl = new Artifact("servicemix-libraries", slList[i].getName(), slList[i].getVersion(), "car");
                environment.addDependency(sl, ImportType.CLASSES);
            }
        }
        if (descriptor.getComponent().getComponentClassPath() != null) {
            String[] pathElements = descriptor.getComponent().getComponentClassPath().getPathElements();
            if (pathElements != null) {
                for (int i = 0; i < pathElements.length; i++) {
                    context.getConfiguration().addToClassPath(new URI("install/").resolve(pathElements[i]).toString());
                }
            }
        }
        // Create the JBI deployment managed object
        Properties props = new Properties();
        props.put("jbiType", "JBIComponent");
        props.put("name", descriptor.getComponent().getIdentification().getName());
        AbstractName name = new AbstractName(environment.getConfigId(), props);
        GBeanData gbeanData = new GBeanData(name, org.apache.servicemix.geronimo.Component.GBEAN_INFO);
        gbeanData.setAttribute("name", descriptor.getComponent().getIdentification().getName());
        gbeanData.setAttribute("description", descriptor.getComponent().getIdentification().getDescription());
        gbeanData.setAttribute("type", descriptor.getComponent().getType());
        gbeanData.setAttribute("className", descriptor.getComponent().getComponentClassName());
        gbeanData.setReferencePattern("container", containerName);
        context.addGBean(gbeanData);
    }

    protected void buildServiceAssembly(Descriptor descriptor, DeploymentContext context, JarFile module)
                    throws Exception {
        Environment environment = context.getConfiguration().getEnvironment();
        // Unzip the component
        File targetDir = new File(context.getBaseDir(), "install");
        targetDir.mkdirs();
        unzip(context, module, new URI("install/"));
        // Unzip SUs
        ServiceUnit[] sus = descriptor.getServiceAssembly().getServiceUnits();
        List<ServiceUnitReference> serviceUnitReferences = new LinkedList<ServiceUnitReference>();
        
        for (int i = 0; i < sus.length; i++) {
            String name = sus[i].getIdentification().getName();
            String zip = sus[i].getTarget().getArtifactsZip();
            String comp = sus[i].getTarget().getComponentName();
            URI installUri = new URI("sus/" + comp + "/" + name + "/");
            unzip(context, new JarFile(new File(targetDir, zip)), installUri);
            // Add component config as a dependency
            Artifact sl = new Artifact("servicemix-components", comp, "0.0", "car");
            environment.addDependency(sl, ImportType.ALL);
             // Deploy the SU on the component
             Component jbiServiceUnit = null;
             try {
                 jbiServiceUnit = getAssociatedJbiServiceUnit(comp, sl);
             } catch (GBeanNotFoundException e) {
                 throw new DeploymentException("Can not find the associated service unit for this service assembly. "
                         + "Check if it's deployed and started.", e);
             }
             ServiceUnitManager serviceUnitManager = jbiServiceUnit.getServiceUnitManager();
             File installDir = new File(context.getBaseDir(), installUri.toString());
             String deploy = serviceUnitManager.deploy(name, installDir.getAbsolutePath());  
             serviceUnitReferences.add(new ServiceUnitReference(sl, name, installDir.getAbsolutePath()));
             log.debug(deploy);
        }
        // Create the JBI deployment managed object
        Properties props = new Properties();
        props.put("jbiType", "JBIServiceAssembly");
        props.put("name", descriptor.getServiceAssembly().getIdentification().getName());
        AbstractName name = new AbstractName(environment.getConfigId(), props);
        GBeanData gbeanData = new GBeanData(name, ServiceAssembly.GBEAN_INFO);
        gbeanData.setAttribute("name", descriptor.getServiceAssembly().getIdentification().getName());
        gbeanData.setAttribute("serviceUnitReferences", serviceUnitReferences);
        gbeanData.setReferencePattern("container", containerName);
        for (int i = 0; i < sus.length; i++) {
            String comp = sus[i].getTarget().getComponentName();
            gbeanData.addDependency(getComponentName(comp));
        }
        context.addGBean(gbeanData);
    }

    /**
     * Returns the JBI ServiceUnit with the given 'ArtifactName' and
     * the given componentName. 
     * 
     * @param compName Name
     * @param artifactName Name
     * @return Component instance
     * @throws GBeanNotFoundException if the ServiceUnit cannot be found
     */
    private Component getAssociatedJbiServiceUnit(String compName, Artifact artifactName) throws GBeanNotFoundException {
        org.apache.servicemix.geronimo.Component serviceUnit = getComponentGBean(
				compName, artifactName);
        Component jbiServiceUnit = serviceUnit.getComponent();
        return jbiServiceUnit;
    }

    /**
     * Returns the sm.ger.Component with the given name
     * 
     * @param compName
     * @param artifactName
     * @return
     * @throws GBeanNotFoundException
     */
	private org.apache.servicemix.geronimo.Component getComponentGBean(
			String compName, Artifact artifactName)
			throws GBeanNotFoundException {
		Properties props = new Properties();
        props.put("jbiType", "JBIComponent");
        props.put("name", compName);
        org.apache.servicemix.geronimo.Component component = 
                (org.apache.servicemix.geronimo.Component) kernel.getGBean(new AbstractName(artifactName, props));
        return component;
    }

    protected void buildSharedLibrary(Descriptor descriptor, DeploymentContext context, JarFile module)
                    throws Exception {
        Environment environment = context.getConfiguration().getEnvironment();
        // Unzip the SL
        File targetDir = new File(context.getBaseDir(), "install");
        targetDir.mkdirs();
        unzip(context, module, new URI("install/"));
        // Create workspace dir
        File workDir = new File(context.getBaseDir(), "workspace");
        workDir.mkdirs();
        // Add classpath entries
        if ("self-first".equals(descriptor.getSharedLibrary().getClassLoaderDelegation())) {
            context.getConfiguration().getEnvironment().setInverseClassLoading(true);
        }
        if (descriptor.getSharedLibrary().getSharedLibraryClassPath() != null) {
            String[] pathElements = descriptor.getSharedLibrary().getSharedLibraryClassPath().getPathElements();
            if (pathElements != null) {
                for (int i = 0; i < pathElements.length; i++) {
                    log.debug("Processing pathElements[" + i + "]: " + pathElements[i]);
                    // We can not add includes directly, so move the file and
                    // include it
                    File include = new File(targetDir, pathElements[i]);
                    File temp = new File(workDir, pathElements[i]);
                    if (!include.isFile()) {
                        throw new Exception("Classpath element '" + pathElements[i] + "' not found");
                    }
                    temp.getParentFile().mkdirs();
                    include.renameTo(temp);
                    context.addInclude(new URI("install/").resolve(pathElements[i]), temp);
                    temp.delete();
                }
            } else {
                log.debug("SharedLibrary().getSharedLibraryClassPath().getPathElements() is null");
            }
        } else {
            log.debug("SharedLibrary().getSharedLibraryClassPath() is null");
        }
        // Create the JBI deployment managed object
        Properties props = new Properties();
        props.put("jbiType", "JBISharedLibrary");
        props.put("name", descriptor.getSharedLibrary().getIdentification().getName());
        AbstractName name = new AbstractName(environment.getConfigId(), props);
        GBeanData gbeanData = new GBeanData(name, SharedLibrary.GBEAN_INFO);
        gbeanData.setAttribute("name", descriptor.getSharedLibrary().getIdentification().getName());
        gbeanData.setAttribute("description", descriptor.getSharedLibrary().getIdentification().getDescription());
        gbeanData.setReferencePattern("container", containerName);
        context.addGBean(gbeanData);
    }

    protected void unzip(DeploymentContext context, JarFile module, URI targetUri) throws IOException {
        Enumeration entries = module.entries();
        while (entries.hasMoreElements()) {
            ZipEntry entry = (ZipEntry) entries.nextElement();
            URI target = targetUri.resolve(entry.getName());
            context.addFile(target, module, entry);
        }
    }

    protected AbstractNameQuery getComponentName(String name) throws MalformedObjectNameException {
        URI uri = URI.create("servicemix-components/" + name + "//car?jbiType=JBIComponent");
        return new AbstractNameQuery(uri);
    }

}
