/*
 * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors.
 *
 * 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.switchyard.deploy.osgi.internal;

import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.xml.XMLConstants;
import javax.xml.validation.Schema;

import org.osgi.framework.Bundle;
import org.osgi.framework.Constants;
import org.osgi.framework.Filter;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.framework.wiring.BundleWire;
import org.osgi.framework.wiring.BundleWiring;
import org.osgi.service.blueprint.container.BlueprintEvent;
import org.osgi.service.blueprint.container.BlueprintListener;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.util.tracker.ServiceTracker;
import org.osgi.util.tracker.ServiceTrackerCustomizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.switchyard.ServiceDomain;
import org.switchyard.common.io.pull.ElementPuller;
import org.switchyard.common.property.CompoundPropertyResolver;
import org.switchyard.common.property.PropertyResolver;
import org.switchyard.config.Configuration;
import org.switchyard.config.model.Descriptor;
import org.switchyard.config.model.Marshaller;
import org.switchyard.config.model.ModelPuller;
import org.switchyard.config.model.composite.BindingModel;
import org.switchyard.config.model.composite.ComponentImplementationModel;
import org.switchyard.config.model.composite.ComponentModel;
import org.switchyard.config.model.composite.CompositeReferenceModel;
import org.switchyard.config.model.composite.CompositeServiceModel;
import org.switchyard.config.model.switchyard.SwitchYardModel;
import org.switchyard.config.model.transform.TransformsModel;
import org.switchyard.deploy.Activator;
import org.switchyard.deploy.Component;
import org.switchyard.deploy.internal.Deployment;
import org.switchyard.deploy.osgi.ComponentRegistry;
import org.switchyard.deploy.osgi.NamespaceHandler;
import org.switchyard.deploy.osgi.NamespaceHandlerSet;
import org.switchyard.deploy.osgi.SwitchYardContainer;
import org.switchyard.deploy.osgi.SwitchYardEvent;
import org.switchyard.deploy.osgi.base.SimpleExtension;
import org.switchyard.transform.internal.DuplicateTransformerException;
import org.switchyard.transform.internal.TransformerRegistryLoader;
import org.switchyard.transform.osgi.internal.TransformSource;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * SwitchYardContainerImpl.
 */
public class SwitchYardContainerImpl extends SimpleExtension
        implements BlueprintListener, NamespaceHandlerSet.Listener, ComponentRegistry.Listener, Runnable, SwitchYardContainer {

    public static final String SWITCHYARD_DEPLOYMENT_BUNDLE = "switchyard.deployment.bundle";
    public static final String CONTAINER_SYMBOLIC_NAME_PROPERTY = "switchyard.container.symbolicname";
    public static final String CONTAINER_VERSION_PROPERTY = "switchyard.container.version";

    private static final Logger logger = LoggerFactory.getLogger(SwitchYardExtender.class);

    public enum State {
        Unknown,
        WaitForCdi,
        WaitForNamespaceHandlers,
        WaitForComponents,
        Created,
        Failed,
    }


    private final SwitchYardExtender _extender;
    private final Bundle _extenderBundle;
    private State _state = State.Unknown;
    private Element _xml;
    private SwitchYardModel _model;
    private Set<URI> _namespaces;
    private NamespaceHandlerSet _nhs;
    private Set<String> _types;
    private ServiceDomain _domain;
    private Deployment _deployment;
    private final AtomicBoolean _scheduled = new AtomicBoolean();
    private final AtomicBoolean _destroyed = new AtomicBoolean(false);
    private final ExecutorService _executors;
    private ServiceRegistration<SwitchYardContainer> _registration;
    private ServiceTracker _cdiContainerTracker;
    private Object _cdiContainer;
    private ServiceRegistration<BlueprintListener> _blueprintListenerRegistration;

    public SwitchYardContainerImpl(SwitchYardExtender extender, Bundle bundle, ExecutorService executor) {
        super(bundle);
        _extender = extender;
        _extenderBundle = extender.getBundleContext().getBundle();
        _executors = executor != null ? new ExecutorServiceWrapper(executor) : null;
    }

    @Override
    protected Object getLock() {
        return _scheduled;
    }

    public void schedule() {
        if (_scheduled.compareAndSet(false, true) && !_destroyed.get()) {
            _executors.submit(this);
        }
    }

    public void run() {
        _scheduled.set(false);
        synchronized (_scheduled) {
            doStart();
        }
    }

    @Override
    protected void doStart() {
        ClassLoader tccl = Thread.currentThread().getContextClassLoader();
        try {
            Thread.currentThread().setContextClassLoader(_bundle.adapt(BundleWiring.class).getClassLoader());
            for (;;) {
                if (_destroyed.get()) {
                    return;
                }
                if (_bundle.getState() != Bundle.ACTIVE && _bundle.getState() != Bundle.STARTING) {
                    return;
                }
                if (_bundle.getBundleContext() != _bundleContext) {
                    return;
                }
                logger.debug("Running switchyard container for bundle {} in state {}", _bundle.getSymbolicName(), _state);
                switch (_state) {
                    case Unknown: {
                        dispatch(SwitchYardEvent.CREATING);
                        boolean needsCdi = false;
                        List<BundleWire> wires = _bundle.adapt(BundleWiring.class).getRequiredWires("osgi.extender");
                        for (BundleWire wire : wires) {
                            String filterStr = wire.getRequirement().getDirectives().get("filter");
                            Filter filter = FrameworkUtil.createFilter(filterStr);
                            Dictionary<String, Object> props = new Hashtable<String, Object>();
                            props.put("osgi.extender", "pax.cdi");
                            needsCdi = filter.match(props);
                        }
                        if (needsCdi) {
                            String filter = "(&(objectClass=org.ops4j.pax.cdi.spi.CdiContainer)(bundleId=" + _bundle.getBundleId() + "))";
                            _cdiContainerTracker = new ServiceTracker(_bundleContext, FrameworkUtil.createFilter(filter), new ServiceTrackerCustomizer() {
                                @Override
                                public Object addingService(ServiceReference reference) {
                                    synchronized (_scheduled) {
                                        _cdiContainer = _bundleContext.getService(reference);
                                    }
                                    schedule();
                                    return _cdiContainer;
                                }
                                @Override
                                public void modifiedService(ServiceReference reference, Object service) {
                                }
                                @Override
                                public void removedService(ServiceReference reference, Object service) {
                                    _bundleContext.ungetService(reference);
                                    enterGracePeriod();
                                }
                            });
                            _cdiContainerTracker.open();
                        }
                        _blueprintListenerRegistration = _bundle.getBundleContext().registerService(BlueprintListener.class, this, null);
                        _state = State.WaitForCdi;
                        break;
                    }
                    case WaitForCdi: {
                        if (_cdiContainerTracker != null) {
                            if (_cdiContainer == null) {
                                String filter = "(&(objectClass=org.ops4j.pax.cdi.spi.CdiContainer)(bundleId=" + _bundle.getBundleId() + "))";
                                dispatch(SwitchYardEvent.GRACE_PERIOD, Collections.singleton(filter));
                                return;
                            }
                        }
                        if (_nhs == null) {
                            URL configUrl = getBundle().getEntry(SwitchYardExtender.SWITCHYARD_XML);
                            if (configUrl == null) {
                                configUrl = getBundle().getEntry(SwitchYardExtender.WEBINF_SWITCHYARD_XML);
                            }
                            InputStream configStream = configUrl.openStream();
                            try {
                                _xml = new ElementPuller().pull(configStream);
                            } finally {
                                configStream.close();
                            }
                            _namespaces = findNamespaces(new HashSet<URI>(), _xml);
                            _nhs = _extender.getNamespaceHandlerRegistry().getNamespaceHandlers(_namespaces, getBundle());
                            _nhs.addListener(this);
                        }
                        _state = State.WaitForNamespaceHandlers;
                        break;
                    }
                    case WaitForNamespaceHandlers: {
                        List<String> missing = new ArrayList<String>();
                        List<URI> missingURIs = new ArrayList<URI>();
                        for (URI ns : _namespaces) {
                            if (_nhs.getNamespaceHandler(ns) == null) {
                                missing.add("(&(" + Constants.OBJECTCLASS + "=" + NamespaceHandler.class.getName() + ")(" + NamespaceHandler.NAMESPACES + "=" + ns + "))");
                                missingURIs.add(ns);
                                dispatch(SwitchYardEvent.GRACE_PERIOD, missing);
                            }
                        }
                        if (missing.size() > 0) {
                            logger.info("Bundle {} is waiting for namespace handlers {}", getBundle().getSymbolicName(), missingURIs);
                            return;
                        }
                        _model = new ModelPuller<SwitchYardModel>(new OsgiDescriptor(_nhs)).pull(_xml);
                        OsgiPropertyResolver.set(_model, _bundle);
                        _types = new HashSet<String>();
                        if (_model.getComposite() != null) {
                            for (CompositeReferenceModel reference : _model.getComposite().getReferences()) {
                                for (BindingModel binding : reference.getBindings()) {
                                    _types.add(binding.getType());
                                }
                            }
                            for (ComponentModel component : _model.getComposite().getComponents()) {
                                ComponentImplementationModel impl = component.getImplementation();
                                if (impl == null) {
                                    throw new IllegalStateException("Component implementation should not be null");
                                }
                                _types.add(impl.getType());
                            }
                            for (CompositeServiceModel service : _model.getComposite().getServices()) {
                                for (BindingModel binding : service.getBindings()) {
                                    _types.add(binding.getType());
                                }
                            }
                        } else {
                            logger.info("A composite element is missing from the switchyard.xml");
                        }
                        _extender.getComponentRegistry().addListener(this);
                        _state = State.WaitForComponents;
                        break;
                    }
                    case WaitForComponents: {
                        List<Component> components = new ArrayList<Component>();
                        List<String> missingTypes = new ArrayList<String>();
                        for (String type : _types) {
                            Component component = _extender.getComponentRegistry().getComponent(type);
                            if (component == null) {
                                missingTypes.add(type);
                            } else {
                                components.add(component);
                            }
                        }
                        if (!missingTypes.isEmpty()) {
                            logger.info("Bundle {} is waiting for components {}", getBundle().getSymbolicName(), missingTypes);
                            dispatch(SwitchYardEvent.GRACE_PERIOD, missingTypes);
                            return;
                        }

                        ClassLoader oldTccl = Thread.currentThread().getContextClassLoader();
                        ClassLoader newTccl = oldTccl;
                        if (_cdiContainer != null) {
                            try {
                                Method method = _cdiContainer.getClass().getMethod("getContextClassLoader");
                                newTccl = (ClassLoader) method.invoke(_cdiContainer);
                            } catch (Throwable t) {
                                // Ignore
                            }
                        }
                        try {
                            Thread.currentThread().setContextClassLoader(newTccl);
                            _domain = _extender.getDomainManager().createDomain(getBundleContext(), _model.getQName(), _model);
                            _domain.setProperty(SWITCHYARD_DEPLOYMENT_BUNDLE, getBundle());
                            
                            registerOOTBTransformers();
                            
                            List<Activator> activators = new ArrayList<Activator>();
                            for (Component component : components) {
                                activators.add(component.createActivator(_domain));
                            }
                            _deployment = new Deployment(_model);
                            _deployment.init(_domain, activators);
                            _deployment.start();
                        } finally {
                            Thread.currentThread().setContextClassLoader(oldTccl);
                        }
                        // Register the BlueprintContainer in the OSGi registry
                        int bs = _bundle.getState();
                        if (_registration == null && (bs == Bundle.ACTIVE || bs == Bundle.STARTING)) {
                            Dictionary<String, Object> props = new Hashtable<String, Object>();
                            props.put(CONTAINER_SYMBOLIC_NAME_PROPERTY, _bundle.getSymbolicName());
                            props.put(CONTAINER_VERSION_PROPERTY, _bundle.getVersion());
                            _registration = _bundleContext.registerService(SwitchYardContainer.class, this, props);
                        }
                        dispatch(SwitchYardEvent.CREATED);
                        _state = State.Created;
                        break;
                    }
                    case Created:
                    case Failed:
                        return;
                }
            }
        } catch (Throwable t) {
            try {
                _state = State.Failed;
                logger.error("Unable to start switchyard for bundle " + getBundle().getSymbolicName(), t);
                dispatch(SwitchYardEvent.FAILURE, t);
                destroyDeployment();
            } catch (RuntimeException re) {
                logger.debug("Tidying up components failed. ", re);
                throw re;
            }
        } finally {
            Thread.currentThread().setContextClassLoader(tccl);
        }
    }

    @Override
    protected void doDestroy() throws Exception {
        dispatch(SwitchYardEvent.DESTROYING);
        _executors.shutdownNow();
        try {
            if (_registration != null) {
                _registration.unregister();
                _registration = null;
            }
        } catch (Throwable t) {
            logger.debug("Error unregistering Switchyard container", t);
        }
        if (_cdiContainerTracker != null) {
            _cdiContainerTracker.close();
            _cdiContainerTracker = null;
        }
        if (_nhs != null) {
            _nhs.removeListener(this);
            _nhs.destroy();
            _nhs = null;
        }
        if (_blueprintListenerRegistration != null) {
            _blueprintListenerRegistration.unregister();
            _blueprintListenerRegistration = null;
        }
        try {
            _executors.awaitTermination(5 * 60, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            logger.debug("Interrupted waiting for executor to shut down");
        }
        destroyDeployment();
        dispatch(SwitchYardEvent.DESTROYED);
        logger.debug("Switchyard container destroyed: {}", _bundleContext);
    }

    private void dispatch(int type) {
        dispatch(new SwitchYardEvent(type, getBundle(), _extenderBundle));
    }

    private void dispatch(int type, Collection<String> deps) {
        String[] depsArray = deps.toArray(new String[deps.size()]);
        dispatch(new SwitchYardEvent(type, getBundle(), _extenderBundle, depsArray));
    }

    private void dispatch(int type, Throwable t) {
        dispatch(new SwitchYardEvent(type, getBundle(), _extenderBundle, t));
    }

    private void dispatch(SwitchYardEvent event) {
        _extender.getEventDispatcher().switchyardEvent(event);
    }

    @Override
    public void namespaceHandlerRegistered(URI uri) {
        if (_nhs.isComplete()) {
            schedule();
        }
    }

    @Override
    public void namespaceHandlerUnregistered(URI uri) {
        if (!_nhs.isComplete()) {
            enterGracePeriod();
        }
    }

    @Override
    public void componentRegistered(String type) {
        if (_types != null && _types.contains(type)) {
            schedule();
        }
    }

    @Override
    public void componentUnregistered(String type) {
        if (_types != null && _types.contains(type)) {
            enterGracePeriod();
        }
    }

    @Override
    public void blueprintEvent(BlueprintEvent be) {
        if (be.getType() == BlueprintEvent.DESTROYING) {
            try {
                logger.info("Received BlueprintEvent.DESTROYING event for " + be.getBundle().getSymbolicName() + ", trying to stop SwitchYard first");
                // Undeploy SwitchYard application before blueprint container is destroyed
                enterGracePeriod();
            } catch (Exception e) {
                logger.error("Error while stopping SwitchYard", e);
            }
        }
    }

    private void destroyDeployment() {
        _extender.getComponentRegistry().removeListener(this);
        if (_deployment != null) {
            _deployment.stop();
            _deployment.destroy();
            _deployment = null;
        }
        if (_domain != null) {
            _domain.destroy();
            _domain = null;
        }
    }

    private void enterGracePeriod() {
        synchronized (_scheduled) {
            if (_destroyed.get()) {
                return;
            }
            try {
                destroyDeployment();
            } catch (Exception e) {
                logger.error("Error while stopping switchyard", e);
            }
            _cdiContainer = null;
            _state = State.WaitForCdi;
            schedule();
        }
    }

    private Set<URI> findNamespaces(Set<URI> namespaces, Node node) {
        if (node instanceof Element || node instanceof Attr) {
            String ns = node.getNamespaceURI();
            if (ns != null && !isIgnorableAttributeNamespace(ns)) {
                namespaces.add(URI.create(ns));
            }
        }
        NamedNodeMap nnm = node.getAttributes();
        if (nnm != null) {
            for (int i = 0; i< nnm.getLength(); i++) {
                findNamespaces(namespaces, nnm.item(i));
            }
        }
        NodeList nl = node.getChildNodes();
        for (int i = 0; i < nl.getLength(); i++) {
            findNamespaces(namespaces, nl.item(i));
        }
        return namespaces;
    }

    private boolean isIgnorableAttributeNamespace(String ns) {
        return XMLConstants.RELAXNG_NS_URI.equals(ns) 
                || XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI.equals(ns)
                || XMLConstants.W3C_XML_SCHEMA_NS_URI.equals(ns)
                || XMLConstants.W3C_XPATH_DATATYPE_NS_URI.equals(ns)
                || XMLConstants.W3C_XPATH_DATATYPE_NS_URI.equals(ns)
                || XMLConstants.XML_DTD_NS_URI.equals(ns)
                || XMLConstants.XML_NS_URI.equals(ns)
                || XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(ns);
    }
    
    /**
     * OSGi container code needs to register these outside of 
     * TransformerRegistryLoader.registerOOTBTransformers() due to the fact that 
     * descriptors cannot be parsed directly from the class path.
     */
    private void registerOOTBTransformers() throws Exception {
        Collection<ServiceReference<TransformSource>> refs = 
                _bundleContext.getServiceReferences(TransformSource.class, null);
        
        // Find all SY bundles which contain transformer definitions
        for (final ServiceReference<TransformSource> ref : refs) {
            // Create a customized transformer loader which loads classes via the bundle
            TransformerRegistryLoader loader = 
                    new TransformerRegistryLoader(_domain.getTransformerRegistry()) {
                @Override
                protected Class<?> getClass(String className) {
                    Class<?> clazz = null;
                    try {
                        clazz = ref.getBundle().loadClass(className);
                    } catch (ClassNotFoundException ex) {
                        logger.warn("Failed to load transformer class " + className + 
                                " from bundle " + ref.getBundle().getSymbolicName());
                    }
                    return clazz;
                }  
            };
            TransformSource trs = _bundleContext.getService(ref);
            InputStream tStream = null;
            
            // parse and register the transformer definitions
            try {
                tStream = trs.getTransformsURL().openStream();
                Element tConfig = new ElementPuller().pull(tStream);
                Set<URI> tNamespaces = findNamespaces(new HashSet<URI>(), tConfig);
                NamespaceHandlerSet tHandlers = _extender.getNamespaceHandlerRegistry()
                        .getNamespaceHandlers(tNamespaces, getBundle());
                TransformsModel tm = new ModelPuller<TransformsModel>(
                        new OsgiDescriptor(tHandlers)).pull(tConfig);
                
                loader.registerTransformers(tm, false);
            } catch (final DuplicateTransformerException e) {
                // duplicate OOTB transformers are not an error - log for visibility
                logger.debug(e.getMessage());
            } catch (Exception ex) {
                logger.warn("Failed to load transformers from bundle: " 
                        + ref.getBundle().getSymbolicName(), ex);
            } finally {
                if (tStream != null) {
                    tStream.close();
                }
                _bundleContext.ungetService(ref);
            }
        }
    }
    
    /**
     * OSGi wrapper for Descriptor which loads marshallers and schema based on 
     * registered namespace handlers.
     */
    private class OsgiDescriptor extends Descriptor {
        
        private NamespaceHandlerSet _namespaceHandlers;
        
        /**
         * Create a new OSGi wrapper Descriptor.
         * @param namespaceHandlers namespace handlers to use in descriptor
         */
        OsgiDescriptor(NamespaceHandlerSet namespaceHandlers) {
            super();
            _namespaceHandlers = namespaceHandlers;
        }
        
        @Override
        public synchronized Schema getSchema(Set<String> namespaces) {
            try {
                return _namespaceHandlers.getSchema();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public synchronized Marshaller getMarshaller(String namespace) {
            return _namespaceHandlers.getNamespaceHandler(URI.create(namespace)).createMarshaller(namespace, this);
        }
        
        @Override
        public boolean equals(Object obj) {
            return super.equals(obj);
        }
        
        @Override
        public int hashCode() {
            return super.hashCode();
        }
        
    };

    private static final class OsgiPropertyResolver implements PropertyResolver {

        private org.osgi.service.cm.Configuration _wrapped;

        private OsgiPropertyResolver(org.osgi.service.cm.Configuration wrapped) {
            _wrapped = wrapped;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Object resolveProperty(String key) {
            return _wrapped.getProperties() != null ? _wrapped.getProperties().get(key) : null;
        }

        private static void set(SwitchYardModel switchyardModel, Bundle bundle) throws Exception {
            ServiceReference configurationAdminReference = bundle.getBundleContext().getServiceReference(ConfigurationAdmin.class.getName());
            ConfigurationAdmin configAdmin = (ConfigurationAdmin) bundle.getBundleContext().getService(configurationAdminReference);
            org.osgi.service.cm.Configuration osgiConfig = configAdmin.getConfiguration(bundle.getSymbolicName());
            if(osgiConfig != null) {
                Configuration config = switchyardModel.getModelConfiguration();
                config.setPropertyResolver(CompoundPropertyResolver.compact(config.getPropertyResolver(), new OsgiPropertyResolver(osgiConfig)));
            }
        }
    }

}


