/**
 * Copyright (c) 2007-2012 EBM WebSourcing, 2012-2015 Linagora
 * 
 * This program/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 program/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 program/library; If not, see <http://www.gnu.org/licenses/>
 * for the GNU Lesser General Public License version 2.1.
 */
package org.ow2.petals.microkernel.server;

import static org.ow2.petals.microkernel.api.server.FractalHelper.COMMUNICATION_COMPOSITE;
import static org.ow2.petals.microkernel.api.server.FractalHelper.CONFIGURATION_COMPONENT;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.logging.LogManager;
import java.util.logging.Logger;

import javax.naming.InitialContext;
import javax.naming.NamingException;

import org.objectweb.fractal.adl.ADLException;
import org.objectweb.fractal.api.Component;
import org.objectweb.fractal.api.NoSuchInterfaceException;
import org.objectweb.fractal.api.control.BindingController;
import org.objectweb.fractal.api.control.ContentController;
import org.objectweb.fractal.api.control.IllegalBindingException;
import org.objectweb.fractal.api.control.IllegalContentException;
import org.objectweb.fractal.api.control.IllegalLifeCycleException;
import org.objectweb.fractal.api.control.LifeCycleController;
import org.objectweb.fractal.util.Fractal;
import org.ow2.petals.basisapi.exception.PetalsException;
import org.ow2.petals.clientserverapi.configuration.ContainerConfiguration;
import org.ow2.petals.commons.log.Level;
import org.ow2.petals.commons.log.PetalsExecutionContext;
import org.ow2.petals.launcher.api.server.PetalsServer;
import org.ow2.petals.launcher.api.server.conf.ConfigurationProperties;
import org.ow2.petals.launcher.api.server.exception.PetalsServerException;
import org.ow2.petals.launcher.api.service.ServiceEndpoint;
import org.ow2.petals.microkernel.api.admin.PetalsAdminInterface;
import org.ow2.petals.microkernel.api.communication.topology.TopologyService;
import org.ow2.petals.microkernel.api.configuration.ConfigurationService;
import org.ow2.petals.microkernel.api.configuration.DomainConfiguration;
import org.ow2.petals.microkernel.api.jbi.management.ManagementException;
import org.ow2.petals.microkernel.api.jbi.messaging.RouterService;
import org.ow2.petals.microkernel.api.jbi.messaging.registry.EndpointRegistry;
import org.ow2.petals.microkernel.api.jbi.messaging.registry.RegistryException;
import org.ow2.petals.microkernel.api.server.FractalHelper;
import org.ow2.petals.microkernel.api.server.PetalsStopThread;
import org.ow2.petals.microkernel.api.util.LoggingUtil;
import org.ow2.petals.microkernel.communication.jndi.client.JNDIService;
import org.ow2.petals.microkernel.container.ContainerService;
import org.ow2.petals.microkernel.jbi.management.recovery.SystemRecoveryService;
import org.ow2.petals.microkernel.service.ServiceEndpointImpl;
import org.ow2.petals.microkernel.transport.Transporter;
import org.ow2.petals.microkernel.util.JNDIUtil;
import org.ow2.petals.topology.generated.Topology;

import com.ebmwebsourcing.easycommons.io.IOHelper;
import com.ebmwebsourcing.easycommons.properties.PropertiesException;
import com.ebmwebsourcing.easycommons.properties.PropertiesHelper;

/**
 * This class allows to start and stop the Petals Server.
 * @author Roland Naudin - EBM WebSourcing
 */
public class PetalsServerImpl implements PetalsServer {

    /**
     * Thread used to stop Petals in an isolate thread
     */
    private final PetalsStopThread petalsStopThread;

    /**
     * The domain configuration
     */
    private DomainConfiguration domainConfiguration;

    /**
     * The local properties of the current container (ie: the content of '
     * <code>server.properties</code>'.
     */
    private Properties serverLocalProperties;

    /**
     * The topology in which the current container is a node.
     */
    private Topology topology;

    /**
     * The container configuration
     */
    private ContainerConfiguration containerConfiguration;

    /**
     * Petals composite
     */
    private Component petalsComposite;

    /**
     * Petals Content Controller
     */
    private ContentController petalsContentController;

    private EndpointRegistry registry;

    /**
     * Flag indicating the state of the Petals container. <code>true</code> if the container is starting, started;
     * <code>false</code> if the container is stopping or stopped.
     */
    private volatile boolean isRunning = false;

    /**
     * Logger wrapper. <b>Available once the logging system is initialized.</b>
     */
    private LoggingUtil log = new LoggingUtil(Logger.getLogger(PetalsServer.COMPONENT_LOGGER_NAME));

    /**
     * Creates a new instance of {@link PetalsServerImpl}
     * 
     * @throws PetalsException
     */
    public PetalsServerImpl() throws PetalsException {
        this(false);
    }

    /**
     * Creates a new instance of {@link PetalsServerImpl}
     * 
     * @throws PetalsException
     */
    public PetalsServerImpl(boolean observer) throws PetalsException {
        this.petalsStopThread = new PetalsStopThread(this);
    }

    /**
     * {@inheritDoc}
     */
    public void init(final Properties serverLocalProperties, final Topology topology,
            final File dataRootPath) throws PetalsServerException {

        this.serverLocalProperties = serverLocalProperties;
        this.topology = topology;

        // The data root path MUST be set as a property to be resolved, if
        // needed, in other property values
        this.serverLocalProperties.setProperty(
                ConfigurationProperties.DATA_ROOT_DIRECTORY_PROPERTY_NAME,
                dataRootPath.getAbsolutePath());

        // set the JMX server
        System.setProperty("javax.management.builder.initial", "mx4j.server.MX4JMBeanServerBuilder");

        try {
            initializeLogging();
            // initialize Petals Fractal composite
            this.initializePetalsComposite();

        } catch (final Exception exception) {
            // clean up the petals server
            throw new PetalsServerException("Problem while initializing Petals", exception);
        }
    }
    
    private class ShutdownHook extends Thread {
        
        private final PetalsServer petalsServer;

        public ShutdownHook(final PetalsServer petalsServer) {
            this.petalsServer = petalsServer;
        }
        
        @Override
        public void run() {
            try {
                if (this.petalsServer.isRunning()) {
                    petalsServer.stop();
                }
            } catch (final PetalsException e) {
                System.err.println("ERROR: " + e.getMessage());
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public void start() throws PetalsServerException {

        this.isRunning = true;

        try {
            // start PEtALS Fractal composite
            this.startPetalsComposite();

            // register the PEtALS server in the Petals admin.
            this.registerPetalsServer();

            // expose PEtALS MBean object into the JMX server
            MBeanHelper.registerMBeans(this.petalsComposite);

            // recover all JBI entities
            this.recoverSystem();

            // set the container as active in the topology
            this.startupDone();
        } catch (Exception exception) {
            this.log.error("An error occurs starting Petals ESB.", exception);
            // clean up the petals server
            if (this.petalsComposite != null) {
                this.log.info("Problem while starting PEtALS, try to stop PEtALS cleanly...");
                try {
                    FractalHelper.stopComponent(this.petalsComposite);
                } catch (Throwable e) {
                    this.log.error("Failed to stop PEtALS cleanly");
                }
            }
            throw new PetalsServerException("Failed to start PEtALS", exception);
        }

        Runtime.getRuntime().addShutdownHook(new ShutdownHook(this));            

        this.log.info("Server STARTED");
    }

    /**
     * {@inheritDoc}
     */
    public void stop() throws PetalsServerException {

        this.isRunning = false;

        PetalsServerException exception = null;
        try {
            this.stopPetalsComposite();
        } catch (final Exception e) {
            exception = new PetalsServerException(e);
        }

        if (exception == null) {
            this.log.info("Server STOPPED");
        } else {
            this.log.error("ERROR: " + exception.getMessage(), exception);
        }

        synchronized (this) {
            this.notifyAll();
        }
        if (exception != null) {
            throw exception;
        }
    }

    /**
     * {@inheritDoc}
     */
    public String getContainerConfiguration() throws PetalsServerException {
        if (this.containerConfiguration == null) {
            throw new PetalsServerException("The container configuration is not properly set");
        }

        return this.containerConfiguration.toString();
    }

    /**
     * {@inheritDoc}
     */
    public String browseJNDI() throws PetalsServerException {
        if (this.containerConfiguration == null) {
            throw new PetalsServerException("The container configuration is not properly set");
        }

        String result = null;
        Component jndiComponent = FractalHelper.getRecursiveComponentByName(
                this.petalsContentController, FractalHelper.JNDI_COMPONENT);
        try {
            JNDIService jndiService = (JNDIService) jndiComponent.getFcInterface("service");
            InitialContext initialContext = jndiService.getInitialContext();
            if (this.domainConfiguration.getJndiConfiguration() == null) {
                result = JNDIUtil.browseJNDI(initialContext, (String) null, 0);
            } else {
                URI providerUrl = this.domainConfiguration.getJndiConfiguration()
                        .getJndiProviderUrl();
                result = JNDIUtil.browseJNDI(initialContext, providerUrl.getHost(),
                        providerUrl.getPort());
            }

        } catch (final NoSuchInterfaceException e) {
            throw new PetalsServerException(e);
        } catch (final NamingException e) {
            throw new PetalsServerException(e);
        }

        return result;
    }

    /**
     * {@inheritDoc}
     */
    public List<ServiceEndpoint> getServiceEndpoints(final boolean global)
            throws PetalsServerException {
        List<ServiceEndpoint> result = new ArrayList<ServiceEndpoint>();
        try {
            List<org.ow2.petals.microkernel.api.jbi.messaging.ServiceEndpoint> eps = this.registry
                    .getEndpoints();
            if (eps != null) {
                for (ServiceEndpoint serviceEndpoint : eps) {
                    ServiceEndpointImpl se = new ServiceEndpointImpl();
                    // TODO !
                    se.setInterfacesName(serviceEndpoint.getInterfacesName());
                    se.setEndpointName(serviceEndpoint.getEndpointName());
                    se.setServiceName(serviceEndpoint.getServiceName());
                    se.setDescription(serviceEndpoint.getDescription());
                    se.setLocation(serviceEndpoint.getLocation());
                    result.add(se);
                }
            }
        } catch (final RegistryException e) {
            throw new PetalsServerException("Can not get endpoints");
        }
        return result;
    }

    /**
     * Initialize petals composite
     * 
     * @throws PetalsException
     * 
     */
    private void initializePetalsComposite() throws PetalsException {
        try {
            this.petalsComposite = FractalHelper.createNewComponent(FractalHelper.PETALS_COMPOSITE);
            this.petalsContentController = Fractal.getContentController(this.petalsComposite);
        } catch (NoSuchInterfaceException e) {
            throw new PetalsException("Error creating PEtALS Fractal Composite", e);
        } catch (ADLException e) {
            throw new PetalsException("Error creating PEtALS Fractal Composite", e);
        }
    }

    /**
     * Start the Petals Fractal composite. TODO create a Fractal component to
     * handle the starting and stopping of Fractal composites/components.
     * 
     * </br> <b>NOTE :</b> Container name is set to the PetalsFormatter
     * 
     * @throws NoSuchInterfaceException
     * @throws IllegalLifeCycleException
     * @throws ADLException
     * @throws IllegalBindingException
     * @throws IllegalContentException
     * @throws PetalsException
     */
    private void startPetalsComposite() throws NoSuchInterfaceException, IllegalLifeCycleException,
            ADLException, IllegalContentException, IllegalBindingException, PetalsException {

        // start first the configuration component
        Component configurationComponent = FractalHelper.getComponentByName(
                this.petalsContentController, CONFIGURATION_COMPONENT);
        if (!FractalHelper.startComponent(configurationComponent)) {
            throw new PetalsException("Failed to start PEtALS Fractal component "
                    + CONFIGURATION_COMPONENT);
        }

        // launch the configuration loading of the configuration service
        ConfigurationService configurationService = (ConfigurationService) configurationComponent
                .getFcInterface("service");
        configurationService.loadConfiguration(this.serverLocalProperties, this.topology);

        // retrieve the configuration from the Configuration Service
        this.containerConfiguration = configurationService.getContainerConfiguration();

        // Set the container name into the Petals MDC
        PetalsExecutionContext.putContainerName(this.containerConfiguration.getName());

        this.domainConfiguration = configurationService.getDomainConfiguration();

        // start the communication composite
        Component communicationComposite = FractalHelper.getRecursiveComponentByName(
                this.petalsContentController, COMMUNICATION_COMPOSITE);
        if (!FractalHelper.startComponent(communicationComposite)) {
            throw new PetalsException("Failed to start PEtALS Fractal composite "
                    + COMMUNICATION_COMPOSITE);
        }

        // finally, start the others components
        if (!FractalHelper.startComponent(this.petalsComposite)) {
            throw new PetalsException("Failed to start PEtALS Fractal components");
        }

        Component registryComponent = FractalHelper.getRecursiveComponentByName(
                this.petalsContentController, FractalHelper.ENDPOINT_COMPONENT);

        if (registryComponent == null) {
            throw new PetalsException("Can not find the registry component "
                    + FractalHelper.ENDPOINT_COMPONENT);
        }
        this.registry = (EndpointRegistry) registryComponent.getFcInterface("service");
    }

    /**
     * Stop the Petals Fractal composite.
     * 
     * @throws Exception
     */
    private void stopPetalsComposite() throws Exception {

        LifeCycleController lifeCycleController = Fractal
                .getLifeCycleController(this.petalsComposite);

        if (LifeCycleController.STARTED.equals(lifeCycleController.getFcState())) {

            Component containerComponent = FractalHelper.getComponentByName(
                    this.petalsContentController, FractalHelper.CONTAINER_COMPOSITE);
            ContentController containerContentController = Fractal
                    .getContentController(containerComponent);

            // First stop and shutdown all the Fractal components associated to
            // the JBI components and SAs
            List<Component> components = FractalHelper.getComponentListByPrefix(
                    containerContentController, ContainerService.PREFIX_COMPONENT_LIFE_CYCLE_NAME);
            Collections.reverse(components);
            for (Component component : components) {
                FractalHelper.stopComponent(component);
            }

            List<Component> sas = FractalHelper.getComponentListByPrefix(
                    containerContentController,
                    ContainerService.PREFIX_SERVICE_ASSEMBLY_LIFE_CYCLE_NAME);
            Collections.reverse(sas);
            for (Component component : sas) {
                FractalHelper.stopComponent(component);
            }

            // Then, prepare the stop of the Transporters, to stop the
            // ongoing transfers
            Component tcpTransporterComponent = FractalHelper.getRecursiveComponentByName(
                    this.petalsContentController, FractalHelper.TCP_TRANSPORTER_COMPONENT);
            Transporter tcpTransporter = (Transporter) tcpTransporterComponent
                    .getFcInterface("service");
            tcpTransporter.stopTraffic();
            Component localTransporterComponent = FractalHelper.getRecursiveComponentByName(
                    this.petalsContentController, FractalHelper.LOCAL_TRANSPORTER_COMPONENT);
            Transporter localTransporter = (Transporter) localTransporterComponent
                    .getFcInterface("service");
            localTransporter.stopTraffic();

            // After, prepare the stop of the router
            Component routerComponent = FractalHelper.getRecursiveComponentByName(
                    this.petalsContentController, FractalHelper.ROUTER_COMPONENT);
            RouterService router = (RouterService) routerComponent.getFcInterface("service");
            router.stopTraffic();

            // Finally, stop the others components, we must stop the Composite
            // elements in an opposite way than their start order
            FractalHelper.stopComposite(this.petalsComposite);
        }
    }

    /**
     * Register to the PetalsAdmin service this Petals server instance
     * 
     * @throws NoSuchInterfaceException
     * @throws PetalsException
     */
    private void registerPetalsServer() throws NoSuchInterfaceException, PetalsException {
        Component petalsAdminComponent = FractalHelper.getRecursiveComponentByName(
                this.petalsContentController, FractalHelper.PETALSADMIN_COMPONENT);
        PetalsAdminInterface petalsAdminService = (PetalsAdminInterface) petalsAdminComponent
                .getFcInterface("service");

        petalsAdminService.setPetalsStopThread(this.petalsStopThread);
    }

    /**
     * Recover the system state. All state elements are recovered (components,
     * share libraries, service assemblies).
     * 
     * @throws NoSuchInterfaceException
     * @throws ManagementException
     * @throws ADLException
     * @throws PetalsException
     * @throws IllegalLifeCycleException
     * @throws IllegalBindingException
     * @throws IllegalContentException
     */
    private void recoverSystem() throws NoSuchInterfaceException, ManagementException,
            ADLException, PetalsException, IllegalBindingException, IllegalLifeCycleException,
            IllegalContentException {
        Component managementComposite = FractalHelper.getComponentByName(
                this.petalsContentController, FractalHelper.JBI_MANAGEMENT_COMPOSITE);
        ContentController managementContentController = Fractal
                .getContentController(managementComposite);

        Component systemRecoveryComponent = FractalHelper.getComponentByName(
                managementContentController, FractalHelper.SYSTEMRECOVERY_COMPONENT);
        SystemRecoveryService systemRecoveryService = (SystemRecoveryService) systemRecoveryComponent
                .getFcInterface("service");

        systemRecoveryService.recoverAllEntities();

        // Then remove this service as it is no more necessary
        FractalHelper.stopComponent(systemRecoveryComponent);
        BindingController systemRecoveryBC = Fractal.getBindingController(systemRecoveryComponent);
        for (String bindingName : systemRecoveryBC.listFc()) {
            systemRecoveryBC.unbindFc(bindingName);
        }
        managementContentController.removeFcSubComponent(systemRecoveryComponent);
    }

    /**
     * Set the local container state to started. The local container state is in
     * unknown state until all is really started.
     * 
     * @throws Exception
     */
    private void startupDone() throws Exception {
        Component topologyComponent = FractalHelper.getRecursiveComponentByName(
                this.petalsContentController, FractalHelper.TOPOLOGY_COMPONENT);

        final TopologyService service = (TopologyService) topologyComponent
                .getFcInterface("service");
        service.setContainerState(this.containerConfiguration.getName(),
                ContainerConfiguration.ContainerState.STARTED);
        this.containerConfiguration.setState(ContainerConfiguration.ContainerState.STARTED);
    }

    /**
     * <p>
     * Initialize the logging system.
     * </p>
     * 
     * @see PetalsServer
     * 
     * @throws IOException
     * @throws PropertiesException
     */
    private void initializeLogging() throws IOException, PropertiesException {
        // Load Petals customized log level
        Level.initialize();

        try {
            final String configurationPath = System
                    .getProperty(ConfigurationProperties.LOGGING_SYSTEM_CONF_FILE_PROPERTY_NAME);
            final File configurationFile;
            if (configurationPath == null) {
                System.out.println("Default conf file for log");
                // Retrieve the logging.properties path
                final URL configurationURL = this.getClass().getResource(
                        "/" + ConfigurationProperties.DEFAULT_LOGGER_FILENAME);
                if (configurationURL == null) {
                    throw new IOException("Failed to reach the resource ["
                            + ConfigurationProperties.DEFAULT_LOGGER_FILENAME + "]");
                }
                configurationFile = new File(configurationURL.toURI().normalize());
            } else {
                configurationFile = new File(configurationPath);
            }

            final InputStream fisInitial = new FileInputStream(configurationFile);
            try {
                final Properties existingProperties = new Properties(System.getProperties());
                existingProperties.putAll(this.serverLocalProperties);
                final InputStream fisResolved = PropertiesHelper.resolvePropertiesForInputStream(
                        fisInitial, existingProperties);
                try {
                    LogManager.getLogManager().readConfiguration(fisResolved);
                } finally {
                    IOHelper.close(fisResolved);
                }
            } finally {
                IOHelper.close(fisInitial);
            }
        } catch (final URISyntaxException e) {
            throw new IOException(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean isRunning() {
        return this.isRunning;
    }
}
