/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2008 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 (at your option) 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.lib.service.manager;

import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.ow2.jonas.configuration.ConfigurationManager;
import org.ow2.jonas.configuration.ServiceDeployer;
import org.ow2.jonas.depmonitor.MonitoringService;
import org.ow2.jonas.lib.management.javaee.J2EEServiceState;
import org.ow2.jonas.management.J2EEServerService;
import org.ow2.jonas.management.ServiceManager;
import org.ow2.util.archive.api.IArchiveMetadata;
import org.ow2.util.ee.deploy.api.deployable.IDeployable;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;

/**
 * Class to manage the JOnAS services.
 * @author Francois Fornaciari
 */
public class ServiceManagerImpl implements ServiceManager {

    /**
     * Logger.
     */
    private Log logger = LogFactory.getLog(J2EEServerService.class);

    /**
     * This table contains the managed services state. The keys are the service
     * names.
     */
    private Map<String, ServiceItem> servicesState = null;

    /**
     * BundleContext.
     */
    private BundleContext bc;

    /**
     * {@ConfigurationManager} reference.
     */
    private ConfigurationManager configurationManager = null;

    /**
     * Reference to the J2EEServer service.
     */
    private J2EEServerService j2eeServer = null;

    /**
     * Reference to the {@link ServiceDeployer} service.
     */
    private ServiceDeployer serviceDeployer = null;

    /**
     * Property name for the service requirements of an application.
     */
    private static final String REQUIRE_JONAS_SERVICES = "Require-JOnAS-Services";

    /**
     * Construct a service manager for JOnAS server.
     * @param bc The bundleContext reference
     */
    public ServiceManagerImpl(final BundleContext bc) {
        this.bc = bc;
        this.servicesState = new Hashtable<String, ServiceItem>();
    }

    /**
     * Initialization method.
     */
    public void start() {
        try {
            update();
        } catch (Exception e) {
            logger.error("Cannot initialize the service states");
        }

        // Create OsgiServiceListener
        ServiceTracker listener = new ServiceTracker(this);
        bc.addServiceListener(listener);
    }

    /**
     * Add a service item at server startup or restart.
     * @param serviceItem the service item
     */
    public void addService(final ServiceItem serviceItem) {
        servicesState.put(serviceItem.getName(), serviceItem);
    }

    /**
     * Set mandatory attribute to TRUE.
     * @param serviceName the service name.
     */
    public void setMandatoryService(final String serviceName) {
        ServiceItem serviceItem = servicesState.get(serviceName);
        if (serviceItem != null) {
            serviceItem.setMandatory(true);
        }
    }

    /**
     * Delete all services.
     */
    public void deleteAllServices() {
        servicesState.clear();
    }

    /**
     * @return The services names.
     */
    public List<String> getServices() {
        List<String> sl = new ArrayList<String>();
        Iterator<ServiceItem> items = servicesState.values().iterator();
        while (items.hasNext()) {
            ServiceItem serviceItem = items.next();
            sl.add(serviceItem.getName());
        }
        return sl;
    }

    /**
     * Modify a service state. This may arrive at startup when a service
     * registers itself before the J2EEServer server initialization phase, or
     * due to a service state notification (ServiceEvent in OSGI).
     * @param serviceName name of the service
     * @param state new state
     * @return the service state
     */
    public J2EEServiceState setServiceState(final String serviceName, final J2EEServiceState state) {
        if (servicesState == null) {
            logger.error("setServiceState called but servicesState table null");
            return null;
        }
        /**
         * Get the ServiceItem corresponding to this service in order to check
         * and update its state
         */
        ServiceItem serviceItem = servicesState.get(serviceName);
        if (serviceItem == null) {
            logger.error("setServiceState called but service " + serviceName + " not known");
            return null;
        }
        J2EEServiceState prevState = serviceItem.getState();
        if (prevState != null && prevState.equals(state)) {
            // State didn't changed (bizZare)
            return prevState;
        } else {
            // State changed, update it
            serviceItem.setState(state);
            // Pass service name for special cases of service restart
            if (servicesState.get(serviceName) != null) {
                checkServerState(serviceName);
            }
            return state;
        }
    }

    /**
     * Return the state of a given service.
     * @param serviceName service name
     * @return the service state
     */
    public String getServiceState(final String serviceName) {
        ServiceItem serviceItem = servicesState.get(serviceName);
        if (serviceItem != null) {
            return serviceItem.getState().toString();
        }
        logger.error("getServiceState called but service " + serviceName + " not known");
        return null;
    }

    /**
     * Return the description of a given service.
     * @param serviceName service name
     * @return the service description
     */
    public String getServiceDescription(final String serviceName) {
        ServiceItem serviceItem = servicesState.get(serviceName);
        if (serviceItem != null) {
            return serviceItem.getDescription();
        }
        logger.error("getServiceDescription called but service " + serviceName + " not known");
        return null;
    }

    /**
     * @return True if all the managed services are in RUNNING state, False
     *         otherwise.
     */
    private boolean allServicesRunning() {
        boolean servicesRunning = true;
        Iterator<ServiceItem> items = servicesState.values().iterator();
        while (items.hasNext()) {
            ServiceItem serviceItem = items.next();
            J2EEServiceState state = serviceItem.getState();
            if (!state.equals(J2EEServiceState.RUNNING)) {
                servicesRunning = false;
                break;
            }
        }
        return servicesRunning;
    }

    /**
     * @return True if all the managed services, except the mandatory ones, are
     *         in RUNNING state. False otherwise.
     */
    private boolean allServicesStopped() {
        boolean servicesStopped = true;
        Iterator<ServiceItem> items = servicesState.values().iterator();
        while (items.hasNext()) {
            ServiceItem serviceItem = items.next();
            J2EEServiceState state = serviceItem.getState();
            if (!state.equals(J2EEServiceState.STOPPED)) {
                // Check if this is not a mandatory service
                if (!serviceItem.isMandatory()) {
                    servicesStopped = false;
                    break;
                }
            }
        }

        return servicesStopped;
    }

    /**
     * Implement state transition from STARTING to RUNNING and from
     * STOPPING/RUNNING to STOPPED.
     * @param serviceName the name of the service that changed state
     */
    protected void checkServerState(final String serviceName) {
        if (j2eeServer != null) {
            // Implement STARTING to RUNNING transition.
            if (j2eeServer.isStarting()) {
                // Check if all services are running
                if (allServicesRunning()) {
                    if (!startMonitoring()) {
                        j2eeServer.setRunning();
                    }
                }
            }

            // RUNNING to RUNNING detect a service restart.
            if (j2eeServer.isRunning()) {
                // Special case to treat in case of depmonitor service restart
                if ("depmonitor".equals(serviceName) && depMonitorRunning()) {
                    startMonitoring();
                }
            }

            // Implement RUNNING --> STOPPED transition
            // Implement STOPPING --> STOPPED transition
            // When all services are stopped
            if (j2eeServer.isRunning() || j2eeServer.isStopping()) {
                // Check if all services are stopped
                if (allServicesStopped()) {
                    j2eeServer.setStopped();
                }
            }
        }
    }

    /**
     * Call the startMonitoring() method of the DeployableMonitor.
     * @return True if the deployment monitoring has been started
     */
    private boolean startMonitoring() {
        ServiceReference reference = depMonitorReference();
        if (reference != null) {
            // Start the monitoring of deployables
            MonitoringService monitoringService = (MonitoringService) bc.getService(reference);
            monitoringService.startMonitoring();
            return true;
        }
        return false;
    }

    /**
     * @return True only if the depmonitor service is running
     */
    private boolean depMonitorRunning() {
        if (depMonitorReference() != null) {
            return true;
        }
        return false;
    }

    /**
     * @return The reference of the {@link MonitoringService}
     */
    private ServiceReference depMonitorReference() {
        return bc.getServiceReference(MonitoringService.class.getName());
    }


    /**
     * @see org.ow2.jonas.management.ServiceManager#update()
     */
    public synchronized void update() throws Exception {
        deleteAllServices();

        for (String service : configurationManager.getMandatoryServices()) {
            // Add mandatory services in STOPPED state
            addService(createServiceItem(service, Boolean.TRUE));
        }

        for (String service : configurationManager.getOptionalServices()) {
            // Add optional services in STOPPED state
            addService(createServiceItem(service, Boolean.FALSE));
        }

        // Get already running services list
        List<String> runningServices = ServiceUtil.runningServices(bc);
        for (String service : runningServices) {
            setServiceState(service, J2EEServiceState.RUNNING);
        }
    }

    /**
     * Start a JOnAS service.
     * @see org.ow2.jonas.management.ServiceManager#startService(java.lang.String)
     * @param service name of the service to start
     */
    public void startService(final String service) {
        if (servicesState.get(service) == null || servicesState.get(service).getState() != J2EEServiceState.RUNNING) {
            try {
                serviceDeployer.deployService(service);

                // Test if the service matches a JOnAS service
                // Else do not create a configuration for it
                if(configurationManager.matchService(service)) {
                    // Add the service in STOPPED state
                    addService(createServiceItem(service, false));

                    // Update the service configuration
                    configurationManager.updateServiceConfiguration(service);
                }
            } catch (Exception e) {
                logger.error(e.getMessage());
            }
        } else {
            logger.debug("Service ''{0 }}' is already running", service);
        }
    }

    /**
     * Start all required services for a given deployable.
     * @param deployable The deployable to analyse
     */
    public void startRequiredServices(final IDeployable<?> deployable) {
        IArchiveMetadata metadata = deployable.getArchive().getMetadata();
        // Metadata may be null
        if (metadata != null) {
            String services = metadata.get(REQUIRE_JONAS_SERVICES);
            for (String service: convertToList(services)) {
                startService(service);
            }
        }
    }

    /**
     * Stop a JOnAS service.
     * @see org.ow2.jonas.management.ServiceManager#stopService(java.lang.String)
     * @param service name of the service to stop
     */
    public void stopService(final String service) {
        if (servicesState.get(service) != null && servicesState.get(service).getState() == J2EEServiceState.RUNNING) {
            try {
                // Delete the service configuration
                configurationManager.deleteServiceConfiguration(service);
            } catch (Exception e) {
                logger.error("Cannot delete configuration for the ''{0 }}' service", service);
            }
        } else {
            logger.debug("Service ''0}'' is not running", service);
        }
    }

    /**
     * Create a new service item with the STOPPED state.
     * @param service The service name
     * @param mandatory If the service is mandatory
     * @return The new service item
     */
    private ServiceItem createServiceItem(final String service, final boolean mandatory) {
        // Create a service item with the STOPPED state
        ServiceItem serviceItem = new ServiceItem();
        serviceItem.setName(service);
        serviceItem.setDescription(service + " description ...");
        serviceItem.setState(J2EEServiceState.STOPPED);
        serviceItem.setMandatory(mandatory);
        return serviceItem;
    }

    /**
     * @param j2eeServer The j2eeServer to set
     */
    public void bindJ2EEServer(final J2EEServerService j2eeServer) {
        this.j2eeServer = j2eeServer;
    }

    /**
     * @param j2eeServer the j2eeServer to unset
     */
    public void unbindJ2EEServer(final J2EEServerService j2eeServer) {
        this.j2eeServer = null;
    }

    /**
     * @param configurationManager the configurationManager to set
     */
    public void setConfigurationManager(final ConfigurationManager configurationManager) {
        this.configurationManager = configurationManager;
    }

    /**
     * @param serviceDeployer the serviceDeployer to set
     */
    public void setServiceDeployer(final ServiceDeployer serviceDeployer) {
        this.serviceDeployer = serviceDeployer;
    }

    /**
     * Convert a comma-separated string to a list.
     * @param services The given string
     * @return A {@link List} of services
     */
    private List<String> convertToList(final String param) {
        List<String> result = new ArrayList<String>();
        if (param != null && !param.equals("")) {
            for (String element: param.split(",")) {
                result.add(element);
            }
        }
        return result;
    }
}
