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

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.lib.service.manager.ServiceItem.ServiceLevel;
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.EARDeployable;
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 list contains the managed service items.
     */
    private List<ServiceItem> serviceItems = 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 standby mode.
     */
    private static final String JONAS_STANDBY = "jonas.standby";

    /**
     * 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 (20 seconds).
     */
    private static final int WAITING_TIME = 20000;

    /**
     * Service registration timeout (10 seconds).
     */
    private static final int SERVICE_REGISTRATION_TIMEOUT = 10000;

    /**
     * True if the service states check need to be disabled.
     */
    private boolean checkServiceStates = false;

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

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

        updateServiceItems(true);

        if (getAllServices().contains("discovery")) {
            try {
                startService("discovery", ServiceLevel.MANDATORY);
            } catch (Exception e) {
                logger.error("Cannot start the discovery service", e);
            }
        }

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

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

    /**
     * Add a service item to manage.
     * @param serviceItem The service item to add
     */
    private void addServiceItem(final ServiceItem serviceItem) {
        serviceItems.add(serviceItem);
    }

    /**
     * Delete all service items.
     */
    private void deleteAllServiceItems() {
        serviceItems.clear();
    }

    /**
     * {@inheritDoc}
     */
    public List<String> getAllServices() {
        List<String> result = new ArrayList<String>();
        for (ServiceItem serviceItem: serviceItems) {
            result.add(serviceItem.getName());
        }
        return result;
    }

    /**
     * {@inheritDoc}
     */
    public List<String> getOptionalServices() {
        List<String> result = new ArrayList<String>();
        for (ServiceItem serviceItem: serviceItems) {
            if (serviceItem.getServiceLevel() != ServiceLevel.MANDATORY) {
                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 service The service name
     * @param state new state
     * @return the service state
     */
    public J2EEServiceState setServiceState(final String service, final J2EEServiceState state) {
        // Get the ServiceItem corresponding to this service in order to check
        // and update its state
        ServiceItem serviceItem = getServiceItem(service);
        if (serviceItem == null) {
            logger.error("Service " + service + " not known");
            return null;
        }

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

        // Do not treat state changes of mandatory services
        if (serviceItem.getServiceLevel() != ServiceLevel.MANDATORY) {
            checkServerState(service);
        }

        return state;
    }

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

    /**
     * Return the description of a given service.
     * @param service The service name
     * @return the service description
     */
    public String getServiceDescription(final String service) {
        ServiceItem serviceItem = getServiceItem(service);
        if (serviceItem != null) {
            return serviceItem.getDescription();
        }
        logger.error("getServiceDescription called but service " + service + " 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>();
        for (ServiceItem serviceItem: serviceItems) {
            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() {
        for (ServiceItem serviceItem: serviceItems) {
            J2EEServiceState state = serviceItem.getState();
            if (serviceItem.getServiceLevel() != ServiceLevel.MANDATORY && !state.equals(J2EEServiceState.STOPPED)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Implement state transition from STARTING to RUNNING and from STOPPING/RUNNING to STOPPED.
     * @param service The service that changed state
     */
    private void checkServerState(final String service) {
        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(service) && 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());
    }

    /**
     * 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 {
        startService(service, ServiceLevel.OPTIONAL);
    }

    /**
     * Start a JOnAS service.
     * @param service Name of the service to start
     * @param level The service level to set
     * @throws Exception If the startup of the service fails
     */
    private void startService(final String service, final ServiceLevel level) throws Exception {
        if (isServiceStopped(service)) {
            // Test if the service matches a JOnAS service
            // Else do not create a configuration for it
            if (configurationManager.matchService(service)) {
                // The service is STARTING
                ServiceItem serviceItem = getServiceItem(service);
                if (serviceItem == null) {
                    serviceItem = createServiceItem(service, level);
                    addServiceItem(serviceItem);
                }
                serviceItem.setState(J2EEServiceState.STARTING);

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

            // TODO: Remove this part when EasyBeans/OSGi will be more modular
            if ("ejb3".equals(service) || "jaxws".equals(service) || "ejb3-client".equals(service)) {
                // Deploy the EasyBeans/OSGi Core for JOnAS bundle
                deployEasyBeans();
            }
            if (!"ejb3-client".equals(service)) {
                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) {
        // Only in development mode
        if (serverProperties.isDevelopment()) {
            // EAR special case: analyses embedded deployables
            if (EARDeployable.class.isInstance(deployable)) {
                // Workaround for http://bugs.sun.com/view_bug.do?bug_id=6548436
                for (IDeployable<?> internalDeployable : (EARDeployable.class.cast(deployable)).getAllDeployables()) {
                    startRequiredServices(internalDeployable);
                }
            }

            // Analyses the deployable type and registers the required deployer by starting its associated service
            String service = DeployableEnumeration.getService(deployable);
            if (service != null) {
                try {
                    if (isServiceStopped(service)) {
                        startService(service, ServiceLevel.REQUIRED);
                        waitForServiceRegistration(service);
                    }
                } catch (Exception e) {
                    logger.error("Cannot start required service ''{0}''", service, e);
                }
            }
        }

        // Analyses the JOnAS-Require-Attribute of the MANIFEST.MF
        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, ServiceLevel.REQUIRED);
                } 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) {
            updateServiceItems();
        }

        // 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 {
                    checkServiceStates = true;
                    // 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() {
        List<String> services = getOptionalServices();

        // Stop the ear service before other services
        if (services.contains("ear")) {
            services.remove("ear");
            services.add(0, "ear");
        }

        // Stop the depmonitor service before other services
        if (services.contains("depmonitor")) {
            services.remove("depmonitor");
            services.add(0, "depmonitor");
        }

        for (String service : services) {
            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 serviceLevel The service level
     * @return The new service item
     */
    private ServiceItem createServiceItem(final String service, final ServiceLevel serviceLevel) {
        return createServiceItem(service, serviceLevel, J2EEServiceState.STOPPED);
    }

    /**
     * Create a new service item with a given state.
     * @param service The service name
     * @param serviceLevel The service level
     * @param state The service state to set
     * @return The new service item
     */
    private ServiceItem createServiceItem(final String name, final ServiceLevel serviceLevel, final J2EEServiceState state) {
        // Create a service item with the STOPPED state
        ServiceItem serviceItem = new ServiceItem();
        serviceItem.setName(name);
        serviceItem.setDescription(name + " description ...");
        serviceItem.setState(state);
        serviceItem.setServiceLevel(serviceLevel);
        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;
    }

    /**
     * {@inheritDoc}
     */
    public void disableServiceStatesCheck() {
        this.checkServiceStates = false;
    }

    /**
     * Update service items.
     */
    private void updateServiceItems() {
        updateServiceItems(false);
    }

    /**
     * Update service items.
     * @param startServiceListener True if service listener must be created
     */
    private void updateServiceItems(final boolean startServiceListener) {
        deleteAllServiceItems();

        for (String service : configurationManager.getMandatoryServices()) {
            // Add mandatory services in STOPPED state
            addServiceItem(createServiceItem(service, ServiceLevel.MANDATORY));
        }

        for (String service : configurationManager.getOptionalServices()) {
            // Add all optional services in STOPPED state
            addServiceItem(createServiceItem(service, ServiceLevel.OPTIONAL));
        }

        if (startServiceListener) {
            // Create an OSGi service listener
            ServiceTracker listener = new ServiceTracker(this);
            bundleContext.addServiceListener(listener);
        }

        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);
        }
    }

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

    /**
     * Check the service states and log an error message if the server cannot reach the RUNNING state.
     */
    private void checkServiceStates() {
        if (checkServiceStates && !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));
    }

    /**
     * Method that waits that a given service is started before returning.
     * The method returns if it receives a notification of the service startup or if the timeout is reached.
     * @param service The given service.
     */
    private void waitForServiceRegistration(final String service) {
        long endTime = System.currentTimeMillis() + SERVICE_REGISTRATION_TIMEOUT;

        synchronized (this) {
            while (!isServiceStarted(service)) {
                // Wait until the required service is started
                long timeout = endTime - System.currentTimeMillis();
                if (timeout > 0) {
                    try {
                        wait(timeout);
                    } catch (InterruptedException e) {
                        return;
                    }
                } else {
                    logger.error("Timout expired waiting for ''{0}'' service registration", service);
                    return;
                }
            }
        }
    }

    /**
     * Returns true if the service is started, false otherwise.
     * @param service The given service name
     * @return True if the service is started, false otherwise.
     */
    protected boolean isServiceStarted(final String service) {
        return (getServiceItem(service) != null && getServiceItem(service).getState() == J2EEServiceState.RUNNING);
    }

    /**
     * Returns true if the service is stopped, false otherwise.
     * @param service The given service name
     * @return True if the service is stopped, false otherwise.
     */
    protected boolean isServiceStopped(final String service) {
        return (getServiceItem(service) == null || getServiceItem(service).getState() == J2EEServiceState.STOPPED);
    }

    /**
     * Returns the service item for a given service.
     * @param service The given service name.
     * @return The service item for a given service name.
     */
    private ServiceItem getServiceItem(final String service) {
        for (ServiceItem serviceItem: serviceItems) {
            if (serviceItem.getName().equals(service)) {
                return serviceItem;
            }
        }
        return null;
    }
}
