/**
 *  Copyright (c) 2009, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.wso2.carbon.mediation.initializer.configurations;

import org.apache.synapse.ServerContextInformation;
import org.apache.synapse.task.TaskDescriptionRepository;
import org.apache.synapse.task.TaskScheduler;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.mediation.initializer.services.*;
import org.wso2.carbon.mediation.initializer.utils.ConfigurationHolder;
import org.wso2.carbon.registry.core.session.UserRegistry;
import org.wso2.carbon.registry.core.exceptions.RegistryException;
import org.wso2.carbon.mediation.initializer.ServiceBusConstants;
import org.wso2.carbon.mediation.initializer.ServiceBusUtils;
import org.wso2.carbon.mediation.initializer.persistence.MediationPersistenceManager;
import org.wso2.carbon.mediation.registry.WSO2Registry;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.AxisFault;
import org.apache.axis2.engine.AxisConfiguration;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;
import org.apache.synapse.config.*;
import org.apache.synapse.config.xml.MultiXMLConfigurationBuilder;
import org.apache.synapse.core.SynapseEnvironment;
import org.apache.synapse.core.axis2.MessageContextCreatorForAxis2;
import org.apache.synapse.core.axis2.Axis2SynapseEnvironment;
import org.apache.synapse.core.axis2.ProxyService;
import org.apache.synapse.SynapseConstants;
import org.apache.synapse.ServerConfigurationInformation;
import org.apache.synapse.mediators.base.SequenceMediator;
import org.apache.synapse.registry.Registry;
import org.apache.synapse.eventing.SynapseEventSource;
import org.wso2.carbon.utils.multitenancy.MultitenantConstants;

import java.io.File;
import java.util.Properties;
import java.util.Enumeration;
import java.util.List;

/**
 * Responsible for managing the differrence configurations inside a single ConfigurationContext.
 */
public class ConfigurationManager {
    private static Log log = LogFactory.getLog(ConfigurationManager.class);

    public static final String CONFIGURATION_MANAGER = "CONFIGURATION_MANAGER";

    /** We are defaulting to this, we need to take this from a configuration */
    private String synpaseConfigurationsRoot = ServiceBusConstants.DEFAULT_SYNAPSE_CONFIGS_LOCATION;

    /** Class to track the multiple configurations */
    private ConfigurationTracker tracker;

    /** Registry */
    private UserRegistry registry = null;

    /** The configuration context of the server */
    private ConfigurationContext configurationContext;

    /** Tenant for this configuration belongs */
    private int tenantId = MultitenantConstants.SUPER_TENANT_ID;

    /**
     *
     * @param registry the configuration registry
     * @param configurationContext servers configuration context
     */
    public ConfigurationManager(UserRegistry registry,
                                ConfigurationContext configurationContext) {
        if (registry == null) {
            throw new IllegalArgumentException("Registry is required");
        }

        if (configurationContext == null) {
            throw new IllegalArgumentException("ConfigurationContext is required");
        }

        this.registry = registry;
        this.configurationContext = configurationContext;
    }

    /**
     * Initilize the configuration manager
     *
     * @throws ConfigurationInitilizerException if an error occurs
     */
    public void init() throws ConfigurationInitilizerException {
        tracker = new ConfigurationTracker(registry);
        try {
            tracker.init();

            Parameter parameter = configurationContext.getAxisConfiguration().getParameter(
                    SynapseConstants.Axis2Param.SYNAPSE_CONFIG_LOCATION);

            if (parameter != null && parameter.getValue() != null) {
                synpaseConfigurationsRoot = parameter.getValue().toString();
            }

            tenantId = PrivilegedCarbonContext.
                    getThreadLocalCarbonContext().getTenantId();
        } catch (RegistryException e) {
            handleException("Cannot initialize the configuration tracker: ", e);
        }
    }

    /**
     * Get the configuration tracker
     * @return configuration tracker used by this manager
     */
    public ConfigurationTracker getTracker() {
        return tracker;
    }

    /**
     * Activate the given configuration. This will intern deactivate the current
     * configuration and then activate the new configuration.
     *
     * @param name name of the configuration to be activated
     * @return true if activation is successful
     * @throws ConfigurationInitilizerException if an error occurs while activating
     */
    public boolean activate(String name) throws ConfigurationInitilizerException {
        try {
            if (!tracker.isConfigurationExists(name)) {
                handleException("Trying to load the configuration: " + name +
                " that doesn't have a record");
            }

            SynapseConfiguration newSynapseConfiguration = null;
            SynapseConfiguration oldSynapseConfiguration = getSynapseConfiguration();
            SynapseEnvironment oldSynapseEnvironment = getSynapseEnvironment();

            TaskDescriptionRepository repository = getSynapseEnvironment().getTaskManager().
                    getTaskDescriptionRepository();
            TaskScheduler taskScheduler = getSynapseEnvironment().getTaskManager().
                    getTaskScheduler();

            int loadLocation = 0;

            // if this configuration is not created before we are going to
            // create a default configuration
            if (!tracker.isConfigurationCreated(name)) {
                ConfigurationInformation information = tracker.getConfigurationInformation(name);
                newSynapseConfiguration = createDefaultConfiguration();
                if (information != null && information.getDescription() != null) {
                    newSynapseConfiguration.setDescription(information.getDescription());
                }
                loadLocation = 1;
                log.info("A default Synapse Configuration is created " +
                        "for the configuration: " + name);
            }

            Properties properties = SynapsePropertiesLoader.loadSynapseProperties();
            if (getServerConfigurationInformation().getResolveRoot() != null) {
                properties.put(SynapseConstants.RESOLVE_ROOT,
                        getServerConfigurationInformation().getResolveRoot());
            }

            if (getServerConfigurationInformation().getSynapseHome() != null) {
                properties.put(SynapseConstants.SYNAPSE_HOME,
                        getServerConfigurationInformation().getSynapseHome());
            }

            if (loadLocation != 1) {
                if (isLoadFromRegistry()) {
                    newSynapseConfiguration = ConfigurationUtils.loadFromRegistry(
                            registry, name, synpaseConfigurationsRoot +
                                    File.separator + name, properties);
                    if (newSynapseConfiguration != null) {
                        log.info("Successfully loaded the Synapse Configuration: "
                                + name + " from the registry");
                        loadLocation = 2;
                    } else {
                        log.warn("Failed to load Synapse Configuration: " +
                                name + " from the registry, "
                                + "trying to load from the file system");
                    }
                }

                // we have to try to load the configuration from the file system
                if (newSynapseConfiguration == null) {
                    newSynapseConfiguration = SynapseConfigurationBuilder.
                            getConfiguration(synpaseConfigurationsRoot + File.separator + name,
                                    properties);

                    if (newSynapseConfiguration != null) {
                        log.info("Successfully loaded the Synapse Configuration: " +
                                name + " from the file system");
                        loadLocation = 3;
                    } else {
                        log.warn("Failed to load the Synapse Configuration: " + name);
                        return false;
                    }
                }
            }

            // destroy the old configuration
            synchronized (oldSynapseConfiguration) {
                // first make sure we have completed all the persistence requests
                getMediationPersistenceManager().destroy();

                try {
                    ConfigurationUtils.destroyConfiguration(oldSynapseConfiguration,
                        configurationContext.getAxisConfiguration(), oldSynapseEnvironment);
                } catch (Exception e) {
                    // we are going to ignore and continue
                    log.warn("Error while destroying the current configuration.. " +
                            "Continuing to load the new configuration");
                }
            }

            // we are going to switch to the new configuration location
            newSynapseConfiguration.setPathToConfigFile(
                    synpaseConfigurationsRoot + File.separator + name);
            getServerConfigurationInformation().
                    setSynapseXMLLocation(synpaseConfigurationsRoot + File.separator + name);

            if (loadLocation == 1) {
                ConfigurationUtils.saveToRegistry(registry, newSynapseConfiguration, name);
                ConfigurationUtils.saveToFileSystem(newSynapseConfiguration);
            } else if (loadLocation == 2) {
                if ("true".equals(ConfigurationUtils.getParameter(ServiceBusConstants.SAVE_TO_FILE))) {
                    // If the configuration was loaded from the registry and the 'saveConfigToFile'
                    // system property is set we should serialize the current configuration to the
                    // file system
                    ConfigurationUtils.saveToFileSystem(newSynapseConfiguration);
                }
            } else {
                // If the configuration was loaded from the file system we should
                // save it to the registry
                ConfigurationUtils.saveToRegistry(registry, newSynapseConfiguration, name);
            }

            tracker.activate(name);
            configurationContext.getAxisConfiguration().addParameter(
                            new Parameter(ServiceBusConstants.SYNAPSE_CURRENT_CONFIGURATION, name));
            tracker.update(name, true);

            // initilze the configuration
            initializeConfiguration(synpaseConfigurationsRoot + File.separator + name,
                    oldSynapseConfiguration, newSynapseConfiguration, repository, taskScheduler);

            // initialize the persistence
            ConfigurationUtils.initPersistence(newSynapseConfiguration, registry,
                    synpaseConfigurationsRoot + File.separator + name,
                    configurationContext.getAxisConfiguration(), name);

            // un-register the old configuration
            unregisterServices();

            registerServices();
            
            return true;
        } catch (RegistryException e) {
            handleException("Error accessing the registry resources", e);
        } catch (AxisFault axisFault) {
            handleException("Error while setting up the new configuration");
        }

        return false;
    }

    private void registerServices() {
        SynapseEnvironment synEnv = getSynapseEnvironment();
        SynapseConfiguration synConfig = getSynapseConfiguration();

        SynapseConfigurationService synCfgSvc
                = new SynapseConfigurationServiceImpl(synConfig,
                tenantId, configurationContext);

        //Properties props = new Properties();
        ServiceRegistration confRegistration =
                ConfigurationHolder.getInstance().getBundleContext().registerService(
                        SynapseConfigurationService.class.getName(), synCfgSvc, null);

        //props = new Properties();
        SynapseEnvironmentService synEnvSvc
                = new SynapseEnvironmentServiceImpl(synEnv,
                tenantId, configurationContext);
        ServiceRegistration envRegistration =
                ConfigurationHolder.getInstance().getBundleContext().registerService(
                        SynapseEnvironmentService.class.getName(), synEnvSvc, null);

        //props = new Properties();
        SynapseRegistrationsService synRegistrationsSvc
                = new SynapseRegistrationsServiceImpl(
                confRegistration, envRegistration, tenantId, configurationContext);

        ServiceRegistration synapseRegistration =
                ConfigurationHolder.getInstance().getBundleContext().registerService(
                        SynapseRegistrationsService.class.getName(),
                        synRegistrationsSvc, null);

    }

    private void unregisterServices() {
        BundleContext bundleContext = ConfigurationHolder.getInstance().getBundleContext();

        ServiceRegistration serviceRegistration = ConfigurationHolder.getInstance().
                getSynapseRegistration(tenantId);

        if (serviceRegistration != null) {
            SynapseRegistrationsService synapseRegistrationsService =
                (SynapseRegistrationsService) bundleContext.getService(serviceRegistration.getReference());

            ServiceRegistration synConfigServiceRegistration =
                    synapseRegistrationsService.getSynapseConfigurationServiceRegistration();

            if (synConfigServiceRegistration != null) {
                bundleContext.ungetService(synConfigServiceRegistration.getReference());
            }

            ServiceRegistration synEnvServiceRegistration =
                    synapseRegistrationsService.getSynapseEnvironmentServiceRegistration();

            if (synEnvServiceRegistration != null) {
                bundleContext.ungetService(synEnvServiceRegistration.getReference());
            }

            bundleContext.ungetService(serviceRegistration.getReference());
        }
    }

    /**
     * Create the default configuration
     *    
     * @return the default configuration
     */
    private SynapseConfiguration createDefaultConfiguration() {
        SynapseConfiguration newSynapseConfiguration;
        newSynapseConfiguration = SynapseConfigurationBuilder.getDefaultConfiguration();
        SequenceMediator mainSequence = (SequenceMediator)
                newSynapseConfiguration.getMainSequence();
        mainSequence.setFileName(mainSequence.getName());

        SequenceMediator faultSequence = (SequenceMediator)
                newSynapseConfiguration.getFaultSequence();
        faultSequence.setFileName(faultSequence.getName());

        // set the governance registry
        Registry registry = new WSO2Registry();        
        newSynapseConfiguration.setRegistry(registry);

        newSynapseConfiguration.setProperty(
                MultiXMLConfigurationBuilder.SEPARATE_REGISTRY_DEFINITION, "true");
        return newSynapseConfiguration;
    }

    /**
     * Delete a configuration. This will delete the registry entry, but it won't delete the
     * actual files and registry persitant information.
     *
     * @param name name of the configuration to be deleted
     * @throws ConfigurationInitilizerException if an error occurs
     */
    public void delete(String name) throws ConfigurationInitilizerException {
        try {
            tracker.delete(name);
        } catch (RegistryException e) {
            handleException("Failed to delete the configuration: " + name, e);
        }
    }

    /**
     * Create a new configuration. This won't write anything to the disk until this
     * configuration is activated.
     *
     * @param name name of the configuration
     * @param description description of the configuration
     * @throws ConfigurationInitilizerException if an error occurs
     */
    public void create(String name, String description) throws ConfigurationInitilizerException {
        try {
            tracker.create(name, description);
        } catch (RegistryException e) {
            handleException("Failed to create configuration: " + name, e);
        }
    }

    /**
     * Return the list of configurations available.
     * @return list of configuration information
     * @throws ConfigurationInitilizerException if an error occurs
     */
    public List<ConfigurationInformation> getConfigurationsList()
            throws ConfigurationInitilizerException {
        try {
            return tracker.getConfigurationList();
        } catch (RegistryException e) {
            handleException("Failed to get the configurations list", e);
        }
        return null;
    }


    public void addExistingConfiguration(String name) throws ConfigurationInitilizerException {
        // try to see weather file or registry entry exists
        
        // if exists delete it

        // add the information to the tracker
        try {
            if (tracker.isConfigurationExists(name)) {
                String msg = "Configuration already exists in the " +
                        "available configurations list. Cannot add it again.";
                log.warn(msg);
                throw new ConfigurationInitilizerException(msg);  
            } else {
                tracker.create(name, "");
                tracker.update(name, true);
            }
        } catch (RegistryException e) {
            handleException("Failed to add the existing configuration: " + name, e);
        }
    }

    /**
     * Initialize a newly created configuration. This will initialize the Synapse Env as well.
     *
     * @param configurationLocation file path which this configuration is based on
     * @param oldSynapseConfiguration previous synapse configuration
     * @param newSynapseConfiguration newly created synapse configuration     
     * @param repository task repository
     * @param taskScheduler @throws ConfigurationInitilizerException if an error occurs
     * @throws org.apache.axis2.AxisFault if an error occurs
     * @throws ConfigurationInitilizerException if an error occurs
     */
    private void initializeConfiguration(String configurationLocation,
                                         SynapseConfiguration oldSynapseConfiguration,
                                         SynapseConfiguration newSynapseConfiguration,
                                         TaskDescriptionRepository repository,
                                         TaskScheduler taskScheduler)
            throws ConfigurationInitilizerException, AxisFault {
        AxisConfiguration axisConfiguration = configurationContext.getAxisConfiguration();
        // Set the Axis2 ConfigurationContext to the SynapseConfiguration
        newSynapseConfiguration.setAxisConfiguration(axisConfiguration);

        Entry hostEntry = oldSynapseConfiguration.getEntryDefinition(SynapseConstants.SERVER_HOST);
        Entry ipEntry = oldSynapseConfiguration.getEntryDefinition(SynapseConstants.SERVER_IP);

        // setup the properties
        Properties properties = SynapsePropertiesLoader.loadSynapseProperties();
        if (properties != null) {
            Enumeration keys = properties.keys();
            while (keys.hasMoreElements()) {
                String key = (String) keys.nextElement();
                newSynapseConfiguration.setProperty(key, properties.getProperty(key));
            }
        }

        // Add the old parameters to the new configuration
        newSynapseConfiguration.setPathToConfigFile(configurationLocation);
        newSynapseConfiguration.addEntry(SynapseConstants.SERVER_HOST, hostEntry);
        newSynapseConfiguration.addEntry(SynapseConstants.SERVER_IP, ipEntry);

        // Check for the main sequence and add a default main sequence if not present
        if (newSynapseConfiguration.getMainSequence() == null) {
            SynapseConfigUtils.setDefaultMainSequence(newSynapseConfiguration);
        }

        // Check for the fault sequence and add a deafult fault sequence if not present
        if (newSynapseConfiguration.getFaultSequence() == null) {
            SynapseConfigUtils.setDefaultFaultSequence(newSynapseConfiguration);
        }

        ServerContextInformation contextInformation = getServerContextInformation();

        // set the synapse configuration to the axis2 configuration
        newSynapseConfiguration.setAxisConfiguration(axisConfiguration);
        Parameter synapseCtxParam = new Parameter(SynapseConstants.SYNAPSE_CONFIG, null);
        synapseCtxParam.setValue(newSynapseConfiguration);
        MessageContextCreatorForAxis2.setSynConfig(newSynapseConfiguration);

        //set up synapse env
        Parameter synapseEnvParam = new Parameter(SynapseConstants.SYNAPSE_ENV, null);
        Axis2SynapseEnvironment synEnv = new Axis2SynapseEnvironment(configurationContext,
                newSynapseConfiguration, contextInformation);
        synapseEnvParam.setValue(synEnv);
        MessageContextCreatorForAxis2.setSynEnv(synEnv);

        if (contextInformation != null) {
            // set the new information to the server context
            contextInformation.setSynapseEnvironment(synEnv);
            contextInformation.setSynapseConfiguration(newSynapseConfiguration);
        } else {
            throw new IllegalStateException("ServerContextInformation not found");
        }

        try {
            axisConfiguration.addParameter(synapseCtxParam);
            axisConfiguration.addParameter(synapseEnvParam);
        } catch (AxisFault e) {
            String msg =
                    "Could not set parameters '" + SynapseConstants.SYNAPSE_CONFIG +
                            "' and/or '" + SynapseConstants.SYNAPSE_ENV +
                            "'to the Axis2 configuration : " + e.getMessage();
            throw new ConfigurationInitilizerException(msg, e);
        }

        // redeploy proxy services
        if (log.isTraceEnabled()) {
            log.trace("Re-deploying Proxy services...");
        }

        for (ProxyService proxyService : newSynapseConfiguration.getProxyServices()) {
            if (proxyService != null) {
                proxyService.buildAxisService(newSynapseConfiguration, axisConfiguration);
                if (log.isDebugEnabled()) {
                    log.debug("Deployed Proxy service : " + proxyService.getName());
                }
                if (!proxyService.isStartOnLoad()) {
                    proxyService.stop(newSynapseConfiguration);
                }
            }
        }

        if (log.isTraceEnabled()) {
            log.trace("Re-deploying Event Sources...");
        }

        for (SynapseEventSource eventSource : newSynapseConfiguration.getEventSources()) {
            if (eventSource != null) {
                eventSource.buildService(axisConfiguration);
                if (log.isDebugEnabled()) {
                    log.debug("Deployed Event Source : " + eventSource.getName());
                }
            }
        }

		synEnv.getTaskManager().init(repository, taskScheduler,
				newSynapseConfiguration.getTaskManager());
        // init the synapse configuration
        newSynapseConfiguration.init(synEnv);
        synEnv.setInitialized(true);
    }

    /**
     * Return true if we have to load the configuration from the registry
     * @return true if we have to load the configuration from the registry
     */
    private boolean isLoadFromRegistry() {
        return "true".equals(
                        ConfigurationUtils.getParameter(ServiceBusConstants.LOAD_FROM_REGISTRY));
    }

    private SynapseEnvironment getSynapseEnvironment() {
        return (SynapseEnvironment) configurationContext.getAxisConfiguration().getParameter(
                SynapseConstants.SYNAPSE_ENV).getValue();
    }

    private SynapseConfiguration getSynapseConfiguration() {
        return (SynapseConfiguration) configurationContext.getAxisConfiguration().getParameter(
                SynapseConstants.SYNAPSE_CONFIG).getValue();
    }

    private MediationPersistenceManager getMediationPersistenceManager() {
        return ServiceBusUtils.getMediationPersistenceManager(
                configurationContext.getAxisConfiguration());
    }

    private ServerConfigurationInformation getServerConfigurationInformation() {
        Parameter p = configurationContext.getAxisConfiguration().
                getParameter(SynapseConstants.SYNAPSE_SERVER_CONFIG_INFO);
        if (p != null) {
            return (ServerConfigurationInformation) p.getValue();
        }
        return null;
    }

    private ServerContextInformation getServerContextInformation() {
        Parameter p = configurationContext.getAxisConfiguration().
                getParameter(SynapseConstants.SYNAPSE_SERVER_CTX_INFO);
        if (p != null) {
            return (ServerContextInformation) p.getValue();
        }
        return null;
    }

    private void handleException(String msg) throws ConfigurationInitilizerException {
        log.warn(msg);
        throw new ConfigurationInitilizerException(msg);
    }

    private void handleException(String msg, Exception e) throws ConfigurationInitilizerException {
        log.warn(msg, e);
        throw new ConfigurationInitilizerException(msg, e);
    }    
}
