/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2008-2009 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.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.ow2.jonas.configuration.ConfigurationManager;
import org.ow2.jonas.configuration.DeploymentPlanDeployer;
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.jonas.properties.ServerProperties;
import org.ow2.util.archive.api.IArchiveMetadata;
import org.ow2.util.ee.deploy.api.deployable.IDeployable;
import org.ow2.util.ee.deploy.api.deployer.IDeployerManager;
import org.ow2.util.ee.deploy.api.deployer.IDeployerManagerCallback;
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 service states. The keys are the service names.
     */
    private Map<String, ServiceItem> serviceStates = null;

    /**
     * OSGi BundleContext.
     */
    private BundleContext bundleContext;

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

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

    /**
     * Reference to the {@link IDeployerManager} service.
     */
    private IDeployerManager deployerManager = null;

    /**
     * Reference to the {@link DeploymentPlanDeployer} service.
     */
    private DeploymentPlanDeployer deploymentPlanDeployer = null;

    /**
     * Instance of the {@link RequireJOnASServicesHandler} handler.
     */
    private IDeployerManagerCallback deployerManagerCallback = null;

    /**
     * Server properties.
     */
    private ServerProperties serverProperties = null;

    /**
     * Property name for the bootstrap mode.
     */
    private static final String JONAS_BOOTSTRAP = "jonas.bootstrap";

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

    /**
     * Property name for the JPA provider.
     */
    private static final String JPA_PROVIDER = "jpa.provider";

    /**
     * Property name for the prefix of the Easybeans/OSGi for JOnAS bundle symbolic name.
     */
    private static final String EASYBANS_PREFIX_SYMBOLIC_NAME = "org.ow2.easybeans.core.for.jonas.";

    /**
     * Waiting time before checking the service states (10 seconds).
     */
    private static final int WAITING_TIME = 10000;

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

    /**
     * Initialization method.
     */
    public void start() {
        // Add a callback to the deployer manager
        deployerManagerCallback = new RequireJOnASServicesHandler(this);
        deployerManager.addCallback(deployerManagerCallback);

        updateServiceStates();

        if (!Boolean.getBoolean(JONAS_BOOTSTRAP)) {
            startServices(false);
        }

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

    /**
     * Finalization method.
     */
    public void stop() {
        // Remove the callback to the deployer manager
        deployerManager.removeCallback(deployerManagerCallback);
    }

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

    /**
     * Delete all service states.
     */
    private void deleteAllServices() {
        serviceStates.clear();
    }

    /**
     * {@inheritDoc}
     */
    public List<String> getAllServices() {
        List<String> result = new ArrayList<String>();
        Iterator<ServiceItem> items = serviceStates.values().iterator();
        while (items.hasNext()) {
            ServiceItem serviceItem = items.next();
            result.add(serviceItem.getName());
        }
        return result;
    }

    /**
     * {@inheritDoc}
     */
    public List<String> getOptionalServices() {
        List<String> result = new ArrayList<String>();
        Iterator<ServiceItem> items = serviceStates.values().iterator();
        while (items.hasNext()) {
            ServiceItem serviceItem = items.next();
            if (!serviceItem.isMandatory()) {
                result.add(serviceItem.getName());
            }
        }
        return result;
    }

    /**
     * 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) {
        // Get the ServiceItem corresponding to this service in order to check
        // and update its state
        ServiceItem serviceItem = serviceStates.get(serviceName);
        if (serviceItem == null) {
            logger.error("Service " + serviceName + " not known");
            return null;
        }

        // State has changed, update it
        serviceItem.setState(state);

        // Do not treat state changes of mandatory services
        if (!serviceItem.isMandatory()) {
            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 = serviceStates.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 = serviceStates.get(serviceName);
        if (serviceItem != null) {
            return serviceItem.getDescription();
        }
        logger.error("getServiceDescription called but service " + serviceName + " not known");
        return null;
    }

    /**
     * Returns the list of services that are not in RUNNING state.
     * @return The list of services that are not in RUNNING state
     */
    private List<String> getNonRunningServices() {
        List<String> services = new ArrayList<String>();
        Iterator<ServiceItem> serviceItems = serviceStates.values().iterator();
        while (serviceItems.hasNext()) {
            ServiceItem serviceItem = serviceItems.next();
            J2EEServiceState state = serviceItem.getState();
            if (!state.equals(J2EEServiceState.RUNNING)) {
                services.add(serviceItem.getName());
            }
        }
        return services;
    }

    /**
     * @return True if all the optional services are in STOPPED state. False otherwise.
     */
    private boolean allOptionalServicesStopped() {
        Iterator<ServiceItem> serviceItems = serviceStates.values().iterator();
        while (serviceItems.hasNext()) {
            ServiceItem serviceItem = serviceItems.next();
            J2EEServiceState state = serviceItem.getState();
            if (!serviceItem.isMandatory() && !state.equals(J2EEServiceState.STOPPED)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Implement state transition from STARTING to RUNNING and from STOPPING/RUNNING to STOPPED.
     * @param serviceName the name of the service that changed state
     */
    private void checkServerState(final String serviceName) {
        if (j2eeServer != null) {
            // Implement STARTING to RUNNING transition.
            if (j2eeServer.isStarting()) {
                // Check if all services are running
                if (getNonRunningServices().isEmpty()) {
                    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 optional services are stopped
                if (allOptionalServicesStopped()) {
                    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) bundleContext.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 bundleContext.getServiceReference(MonitoringService.class.getName());
    }

    /**
     * @see org.ow2.jonas.management.ServiceManager#updateServiceStates()
     */
    public void updateServiceStates() {
        deleteAllServices();

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

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

        try {
            // Get already running services list
            List<String> runningServices = ServiceUtil.getRunningServices(bundleContext);
            for (String service : runningServices) {
                setServiceState(service, J2EEServiceState.RUNNING);
            }
        } catch (InvalidSyntaxException e) {
            logger.error("Unable to get running services", e);
        }
    }

    /**
     * Start a JOnAS service.
     * @param service name of the service to start
     * @throws Exception If the startup of the service fails
     */
    public void startService(final String service) throws Exception {
        if (serviceStates.get(service) == null || serviceStates.get(service).getState() == J2EEServiceState.STOPPED) {
            // Test if the service matches a JOnAS service
            // Else do not create a configuration for it
            if (configurationManager.matchService(service)) {
                // The service is STARTING
                addService(createServiceItem(service, false, J2EEServiceState.STARTING));

                // Update the service configuration
                configurationManager.updateServiceConfiguration(service);
            }

            // TODO: Remove this part when EasyBeans/OSGi will be more modular
            if (service.equals("ejb3") || service.equals("jaxws")) {
                // Deploy the EasyBeans/OSGi Core for JOnAS bundle
                deployEasyBeans();
            }

            deploymentPlanDeployer.deploy(service);
        } else {
            logger.debug("Service ''{0}'' is already starting or 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)) {
                try {
                    startService(service);
                } catch (Exception e) {
                    logger.error("Cannot start required service ''{0}''", service, e);
                }
            }
        }
    }

    /**
     * Stop a JOnAS service.
     * @param service name of the service to stop
     * @throws Exception If the stop of the service fails
     */
    public void stopService(final String service) throws Exception {
        try {
            // Delete the service configuration
            configurationManager.deleteServiceConfiguration(service);
        } catch (Exception e) {
            logger.error("Cannot delete configuration for the ''{0 }}' service", service);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void startServices() {
        startServices(true);
    }

    /**
     * Start optional JOnAS services defined in the server configuration. Some JOnAS services may requires other services which
     * will also be started.
     * @param updateServiceStates If the services states have to be updated
     */
    private void startServices(final boolean updateServiceStates) {
        if (updateServiceStates) {
            updateServiceStates();
        }

        // Start JOnAS services in a new Thread
        new Thread() {
            @Override
            public void run() {
                // Start the work cleaner service if JOnAS is in development mode
                if (serverProperties.isDevelopment()) {
                    try {
                        startService("wc");
                    } catch (Exception e) {
                        logger.error("Cannot start the work cleaner service", e);
                    }
                }
                for (String service : getOptionalServices()) {
                    try {
                        startService(service);
                    } catch (Exception e) {
                        logger.error("Cannot start the ''{0}'' service", service, e);
                    }
                }

                try {
                    // Wait for a few seconds after the startup of all services and check the service states
                    Thread.sleep(WAITING_TIME);
                    checkServiceStates();
                } catch (InterruptedException e) {
                    // No nothing
                }

            }
        }.start();
    }

    /**
     * {@inheritDoc}
     */
    public void stopServices() {
        for (String service : reverseList(getOptionalServices())) {
            try {
                stopService(service);
            } catch (Exception e) {
                logger.error("Cannot stop the ''{}'' service", e);
            }
        }
    }

    /**
     * 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) {
        return createServiceItem(service, mandatory, J2EEServiceState.STOPPED);
    }

    /**
     * Create a new service item with a given state.
     * @param service The service name
     * @param mandatory If the service is mandatory
     * @param state The service state to set
     * @return The new service item
     */
    private ServiceItem createServiceItem(final String service, final boolean mandatory, final J2EEServiceState state) {
        // Create a service item with the STOPPED state
        ServiceItem serviceItem = new ServiceItem();
        serviceItem.setName(service);
        serviceItem.setDescription(service + " description ...");
        serviceItem.setState(state);
        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 deployerManager the deployerManager to set
     */
    public void setDeployerManager(final IDeployerManager deployerManager) {
        this.deployerManager = deployerManager;
    }

    /**
     * @param deploymentPlanDeployer the deploymentPlanDeployer to set
     */
    public void setDeploymentPlanDeployer(final DeploymentPlanDeployer deploymentPlanDeployer) {
        this.deploymentPlanDeployer = deploymentPlanDeployer;
    }

    /**
     * @param serverProperties the serverProperties to set
     */
    public void setServerProperties(final ServerProperties serverProperties) {
        this.serverProperties = serverProperties;
    }

    /**
     * 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;
    }

    /**
     * Returns the reversed list of the given list.
     * @param list the given list
     * @return the reversed list
     */
    private List<String> reverseList(final List<String> list) {
        List<String> reversedList = new ArrayList<String>();
        for (String element : list) {
            reversedList.add(0, element);
        }
        return reversedList;
    }

    /**
     * Check the service states and log an error message if the server cannot reach the RUNNING state.
     */
    private void checkServiceStates() {
        if (!getNonRunningServices().isEmpty()) {
            logger.error("JOnAS server cannot reach the RUNNING state");
            for (String service : getNonRunningServices()) {
                logger.error("Service ''{0}'' is not running", service);
            }
            logger.error("Please check the JOnAS server configuration.");
        }
    }

    /**
     * Deploy the EasyBeans/OSGi Core for JOnAS bundle depending on the chosen JPA provider. Undeploy a previously deployed
     * EasyBeans/OSGi Core for JOnAS bundle if the JPA provider to use is different than the one set in the configuration, as we
     * currently support only one JPA provider at same time.
     * @throws Exception If the deployment/undeployment of the EasyBeans/OSGi Core for JOnAS bundle fails.
     */
    private void deployEasyBeans() throws Exception {
        String jpaProvider = configurationManager.getServiceProperties("ejb3").get(JPA_PROVIDER);

        // Try to undeploy a previously JPA provider which is not the same than the one set in the configuration
        Bundle[] bundles = bundleContext.getBundles();
        for (Bundle bundle : bundles) {
            String symbolicName = bundle.getSymbolicName();
            if (symbolicName.startsWith(EASYBANS_PREFIX_SYMBOLIC_NAME) && !symbolicName.endsWith(jpaProvider)) {
                bundle.uninstall();
            }
        }

        deploymentPlanDeployer.deploy("easybeans-".concat(jpaProvider));
    }
}
