/*******************************************************************************
 * Copyright (c) 2008, 2010 VMware Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   VMware Inc. - initial contribution
 *******************************************************************************/

package org.eclipse.virgo.snaps.core.internal;

import java.util.Collection;
import java.util.Dictionary;
import java.util.HashSet;
import java.util.Hashtable;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;

import org.eclipse.virgo.medic.eventlog.EventLogger;
import org.eclipse.virgo.snaps.core.RequestRouter;
import org.eclipse.virgo.snaps.core.internal.Snap;
import org.eclipse.virgo.snaps.core.SnapRegistry;
import org.eclipse.virgo.snaps.core.internal.deployer.SnapFactory;
import org.eclipse.virgo.util.osgi.ServiceRegistrationTracker;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.util.tracker.ServiceTracker;
import org.osgi.util.tracker.ServiceTrackerCustomizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

final class SnapFactoryMonitor implements ServiceTrackerCustomizer<SnapFactory, Object> {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private final BundleContext bundleContext;

    private final ServiceTracker<SnapFactory, Object> snapFactoryTracker;

    private final EventLogger eventLogger;

    private final SnapRegistry snapRegistry;

    public SnapFactoryMonitor(BundleContext bundleContext, EventLogger eventLogger, SnapRegistry snapRegistry) {
        this.bundleContext = bundleContext;
        this.snapFactoryTracker = new ServiceTracker<SnapFactory, Object>(bundleContext, SnapFactory.class, this);
        this.eventLogger = eventLogger;
        this.snapRegistry = snapRegistry;
    }

    public void start() {
        this.snapFactoryTracker.open();
    }

    public void stop() {
        this.snapFactoryTracker.close();
    }

    public Object addingService(ServiceReference<SnapFactory> reference) {
        SnapFactory snapFactory = this.bundleContext.getService(reference);
        if (snapFactory != null) {
            BundleContext snapBundleContext = reference.getBundle().getBundleContext();
            SnapBinder snapBinder = new SnapBinder(snapBundleContext, snapFactory, SnapHostDefinition.fromServiceReference(reference),
                this.eventLogger, this.snapRegistry);
            snapBinder.start();
            return snapBinder;
        }
        logger.warn("Unable to create SnapBinder due to missing SnapFactory");
        return null;
    }

    public void modifiedService(ServiceReference<SnapFactory> reference, Object service) {
    }

    public void removedService(ServiceReference<SnapFactory> reference, Object service) {
        logger.info("Destroying SnapBinder for bundle '{}'", reference.getBundle());
        ((SnapBinder) service).destroy();
    }

    private static enum SnapLifecycleState {
    	AWAITING_INIT,
    	INIT_SUCCEEDED,
    	INIT_FAILED
    }
    
    private static final class SnapBinder implements ServiceListener {

        private static final String SNAP_ORDER = "snap.order";

        private final Logger logger = LoggerFactory.getLogger(this.getClass());

        private final BundleContext context;

        private final SnapFactory factory;

        private final HostSelector hostSelector;

        private final Object hostStateMonitor = new Object();

        private final Object snapStateMonitor = new Object();

        private boolean queriedInitialHosts = false;

        private ServiceReference<ServletContext> hostReference;

        private final ServiceRegistrationTracker registrationTracker = new ServiceRegistrationTracker();

        private final EventLogger eventLogger;

        private final SnapRegistry snapRegistry;

        private Snap snap;                

        public SnapBinder(final BundleContext context, final SnapFactory factory, final SnapHostDefinition hostDefinition, final EventLogger eventLogger, final SnapRegistry snapRegistry) {
            this.context = context;
            this.factory = factory;
            this.hostSelector = new HostSelector(hostDefinition, (String)context.getBundle().getHeaders().get("Module-Scope"));
            this.eventLogger = eventLogger;
            this.snapRegistry = snapRegistry;
        }

        private void start() {
            registerHostListener();
        }

        private void registerHostListener() {
            try {
                this.context.addServiceListener(this, "(objectClass=javax.servlet.ServletContext)");
                logger.info("Listening for hosts to be registered.");
                searchForExistingHost();
            } catch (InvalidSyntaxException e) {
                logger.error("Filter syntax invalid");
            }
        }

        private void hostPublished(ServiceReference<ServletContext> hostReference) {
            assert (!Thread.holdsLock(this.hostStateMonitor));

            ServletContext servletContext = this.context.getService(hostReference);
            if (servletContext != null) {
                synchronized (this.hostStateMonitor) {
                    
                	Collection<ServiceReference<ServletContext>> references = new HashSet<ServiceReference<ServletContext>>();
                	references.add(hostReference);
                    ServiceReference<ServletContext> matchedHost = this.hostSelector.selectHost(references);
                    
                    if (matchedHost == null) {
                        logger.info("Host {} did not match {} ", hostReference.getBundle().getSymbolicName(),
                            this.hostSelector.getHostDefinition().toString());
                        return;
                    }
                }
                
                Bundle hostBundle = hostReference.getBundle();

                SnapLifecycleState newState = SnapLifecycleState.INIT_FAILED;

                Snap snap = this.factory.createSnap(new Host(hostBundle, servletContext, new RequestRouter(this.snapRegistry, servletContext)));
                try {
                    logger.info("Initializing snap '{}'", snap.getContextPath());
                    snap.init();
                    
                    newState = SnapLifecycleState.INIT_SUCCEEDED;
                    
                    logger.info("Publishing snap '{}'", snap.getContextPath());
                    publishSnapService(snap, hostBundle);
                    
                } catch (ServletException e) {
                    this.eventLogger.log(SnapsLogEvents.SNAP_INIT_FAILURE, SnapUtils.boundContextPath(servletContext.getContextPath(), snap.getContextPath()));
                } finally {
                	synchronized (this.snapStateMonitor) {                		
						if (newState == SnapLifecycleState.INIT_SUCCEEDED) {
							this.snap = snap;
						}
                	}                	
                }
            }
        }

        private void publishSnapService(Snap snap, Bundle hostBundle) {
        	Hashtable<Object, Object> props = snap.getSnapProperties();
            Dictionary<String, Object> serviceProperties = new Hashtable<String, Object>();
            
        	for(Object key : props.keySet()){
        		serviceProperties.put(key.toString(), props.get(key));
        	}
        	            
            String snapOrder = (String) serviceProperties.get(SNAP_ORDER);
            if (snapOrder != null) {
                serviceProperties.put(Constants.SERVICE_RANKING, Integer.parseInt(snapOrder));
            }
            serviceProperties.put("snap.host.id", Long.toString(hostBundle.getBundleId()));
            serviceProperties.put("snap.context.path", snap.getContextPath());
            serviceProperties.put("snap.name", (String) this.context.getBundle().getHeaders().get("Bundle-Name"));

            ServiceRegistration<Snap> registration = this.context.registerService(Snap.class, snap, serviceProperties);
            this.registrationTracker.track(registration);
            logger.info("Published snap service for '{}'", snap.getContextPath());
        }

        private void destroy() {
            try {
                destroySnap();
            } finally {
                unregisterHostListener();
            }
        }

        private void unregisterHostListener() {
            logger.info("No longer listening for hosts to be registered.");
            this.context.removeServiceListener(this);
        }

		public void serviceChanged(ServiceEvent event) {
            synchronized (this.hostStateMonitor) {
                while (!queriedInitialHosts) {
                    try {
                        this.hostStateMonitor.wait();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            }

            int type = event.getType();
            @SuppressWarnings("unchecked")
			ServiceReference<ServletContext> serviceReference = (ServiceReference<ServletContext>) event.getServiceReference();

            if (type == ServiceEvent.REGISTERED && this.hostReference == null) {
                hostPublished(serviceReference);
            } else if (type == ServiceEvent.UNREGISTERING) {
                if (serviceReference.equals(this.hostReference)) {
                    hostRetracted(serviceReference);
                }
            }
        }

        private void hostRetracted(ServiceReference<ServletContext> serviceReference) {
            try {
                destroySnap();
            } finally {
                synchronized (this.hostStateMonitor) {
                    this.hostReference = null;
                }
            }
        }

        private void destroySnap() {
            Snap s = null;
            synchronized (this.snapStateMonitor) {                
                s = this.snap;
                this.snap = null;
            }
            this.registrationTracker.unregisterAll();
            if(s != null) {
            logger.info("Retracted snap service for '{}'", s.getContextPath());
            	s.destroy();
            }
        }

        private void searchForExistingHost() {
            ServiceReference<ServletContext> existingHost = null;
            Collection<ServiceReference<ServletContext>> candidates = findHostCandidiates();
            if (candidates != null && !candidates.isEmpty()) {
                logger.info("{} host candidates found", candidates.size());
            } else {
                logger.info("No host candidates found");
            }

            synchronized (this.hostStateMonitor) {
                try {
                    existingHost = this.hostSelector.selectHost(candidates);
                    this.queriedInitialHosts = true;
                } finally {
                    this.hostStateMonitor.notifyAll();
                }
            }
            if (existingHost != null) {
                hostPublished(existingHost);
            }
        }

        private Collection<ServiceReference<ServletContext>> findHostCandidiates() {
            try {
                return this.context.getServiceReferences(ServletContext.class, null);
            } catch (InvalidSyntaxException ise) {
                throw new IllegalStateException("Unexpected invalid filter syntax with null filter", ise);
            }
        }
    }
}
