/*
 * Copyright (c) 2005-2014, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * WSO2 Inc. licenses this file to you 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.andes.configuration.qpid;

import org.apache.commons.configuration.*;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.andes.configuration.AndesConfigurationManager;
import org.wso2.andes.configuration.enums.AndesConfiguration;
import org.wso2.andes.configuration.modules.JKSStore;
import org.wso2.andes.configuration.qpid.plugins.ConfigurationPlugin;
import org.wso2.andes.kernel.AndesException;
import org.wso2.andes.server.registry.ApplicationRegistry;
import org.wso2.andes.server.virtualhost.VirtualHost;
import org.wso2.andes.server.virtualhost.VirtualHostRegistry;
import sun.misc.Signal;
import sun.misc.SignalHandler;

import java.io.File;
import java.util.*;
import java.util.Map.Entry;

import static org.wso2.andes.transport.ConnectionSettings.WILDCARD_ADDRESS;

public class ServerConfiguration extends ConfigurationPlugin implements SignalHandler {
    protected static final Log _logger = LogFactory.getLog(ServerConfiguration.class);

    // Default Configuration values
    public static final int DEFAULT_BUFFER_SIZE = 262144;
    public static final int DEFAULT_SOCKET_BUFFER_SIZE = 32768;
    public static final String DEFAULT_STATUS_UPDATES = "on";
    public static final String SECURITY_CONFIG_RELOADED = "SECURITY CONFIGURATION RELOADED";

    public static final int DEFAULT_FRAME_SIZE = 65536;
    public static final int DEFAULT_PORT = 5672;
    public static final int DEFAULT_SSL_PORT = 8672;
    public static final long DEFAULT_HOUSEKEEPING_PERIOD = 30000L;
    public static final int DEFAULT_JMXPORT = 8999;

    public static final String QPID_HOME = "QPID_HOME";
    public static final String QPID_WORK = "QPID_WORK";

    private Map<String, VirtualHostConfiguration> _virtualHosts = new HashMap<String, VirtualHostConfiguration>();

    private File _configFile;
    private File _vhostsFile;

    // Map of environment variables to config items
    private static final Map<String, String> envVarMap = new HashMap<String, String>();

    // Configuration values to be read from the configuration file
    public static final String MGMT_CUSTOM_REGISTRY_SOCKET = "management.custom-registry-socket";
    public static final String STATUS_UPDATES = "status-updates";
    public static final String ADVANCED_LOCALE = "advanced.locale";

    static {
        envVarMap.put("QPID_ENABLEDIRECTBUFFERS", "advanced.enableDirectBuffers");
        envVarMap.put("QPID_SSLPORT", "connector.ssl.port");
        envVarMap.put("QPID_WRITEBIASED", "advanced.useWriteBiasedPool");
        envVarMap.put("QPID_JMXPORT", "management.jmxport");
        envVarMap.put("QPID_FRAMESIZE", "advanced.framesize");
        envVarMap.put("QPID_MSGAUTH", "security.msg-auth");
        envVarMap.put("QPID_AUTOREGISTER", "auto_register");
        envVarMap.put("QPID_MANAGEMENTENABLED", "management.enabled");
        envVarMap.put("QPID_HEARTBEATDELAY", "heartbeat.delay");
        envVarMap.put("QPID_HEARTBEATTIMEOUTFACTOR", "heartbeat.timeoutFactor");
        envVarMap.put("QPID_MAXIMUMMESSAGEAGE", "maximumMessageAge");
        envVarMap.put("QPID_MAXIMUMMESSAGECOUNT", "maximumMessageCount");
        envVarMap.put("QPID_MAXIMUMQUEUEDEPTH", "maximumQueueDepth");
        envVarMap.put("QPID_MAXIMUMMESSAGESIZE", "maximumMessageSize");
        envVarMap.put("QPID_MAXIMUMCHANNELCOUNT", "maximumChannelCount");
        envVarMap.put("QPID_MINIMUMALERTREPEATGAP", "minimumAlertRepeatGap");
        envVarMap.put("QPID_QUEUECAPACITY", "capacity");
        envVarMap.put("QPID_FLOWRESUMECAPACITY", "flowResumeCapacity");
        envVarMap.put("QPID_SOCKETRECEIVEBUFFER", "connector.socketReceiveBuffer");
        envVarMap.put("QPID_SOCKETWRITEBUFFER", "connector.socketWriteBuffer");
        envVarMap.put("QPID_TCPNODELAY", "connector.tcpNoDelay");
        envVarMap.put("QPID_ENABLEPOOLEDALLOCATOR", "advanced.enablePooledAllocator");
        envVarMap.put("QPID_STATUS-UPDATES", "status-updates");
    }

    /**
     * Loads the given file and sets up the HUP signal handler.
     * <p/>
     * This will load the file and present the root level properties but will
     * not perform any virtualhost configuration.
     * <p/>
     * To perform this {@link #initialise()} must be called.
     * <p/>
     * This has been made a two step process to allow the Plugin Manager and
     * Configuration Manager to be initialised in the Application Registry.
     * <p/>
     * If using this ServerConfiguration via an ApplicationRegistry there is no
     * need to explictly call {@link #initialise()} as this is done via the
     * {@link ApplicationRegistry#initialise()} method.
     *
     * @param configurationURL
     * @throws org.apache.commons.configuration.ConfigurationException
     *
     */
    public ServerConfiguration(File configurationURL) throws ConfigurationException {
        this(parseConfig(configurationURL));
        _configFile = configurationURL;
        try {
            Signal sig = new sun.misc.Signal("HUP");
            sun.misc.Signal.handle(sig, this);
        } catch (Exception e) {
            _logger.info("Signal HUP not supported for OS: " + System.getProperty("os.name"));
            // We're on something that doesn't handle SIGHUP, how sad, Windows.
        }
    }

    /**
     * Wraps the given Commons Configuration as a ServerConfiguration.
     * <p/>
     * Mainly used during testing and in locations where configuration is not
     * desired but the interface requires configuration.
     * <p/>
     * If the given configuration has VirtualHost configuration then
     * {@link #initialise()} must be called to perform the required setup.
     * <p/>
     * This has been made a two step process to allow the Plugin Manager and
     * Configuration Manager to be initialised in the Application Registry.
     * <p/>
     * If using this ServerConfiguration via an ApplicationRegistry there is no
     * need to explictly call {@link #initialise()} as this is done via the
     * {@link ApplicationRegistry#initialise()} method.
     *
     * @param conf
     */
    public ServerConfiguration(org.apache.commons.configuration.Configuration conf) {
        _configuration = conf;
    }

    /**
     * Processes this configuration and setups any VirtualHosts defined in the
     * configuration.
     * <p/>
     * This has been separated from the constructor to allow the PluginManager
     * time to be created and provide plugins to the ConfigurationManager for
     * processing here.
     * <p/>
     * Called by {@link ApplicationRegistry#initialise()}.
     * <p/>
     * NOTE: A DEFAULT ApplicationRegistry must exist when using this method
     * or a new ApplicationRegistry will be created.
     *
     * @throws ConfigurationException
     */
    public void initialise() throws ConfigurationException {
        setConfiguration("", _configuration);
        setupVirtualHosts(_configuration);
    }

    public String[] getElementsProcessed() {
        return new String[]{""};
    }

    @Override
    public void validateConfiguration() throws ConfigurationException {
        // Support for security.jmx.access was removed when JMX access rights were incorporated into the main ACL.
        // This ensure that users remove the element from their configuration file.

        if (getListValue("security.jmx.access").size() > 0) {
            String message = "Validation error : security/jmx/access is no longer a supported element within the " +
                    "configuration xml."
                    + (_configFile == null ? "" : " Configuration file : " + _configFile);
            throw new ConfigurationException(message);
        }

        if (getListValue("security.jmx.principal-database").size() > 0) {
            String message = "Validation error : security/jmx/principal-database is no longer a supported element " +
                    "within the configuration xml."
                    + (_configFile == null ? "" : " Configuration file : " + _configFile);
            throw new ConfigurationException(message);
        }

        if (getListValue("security.principal-databases.principal-database(0).class").size() > 0) {
            String message = "Validation error : security/principal-databases is no longer supported within the " +
                    "configuration xml."
                    + (_configFile == null ? "" : " Configuration file : " + _configFile);
            throw new ConfigurationException(message);
        }
    }

    /*
     * Modified to enforce virtualhosts configuration in external file or main file, but not
     * both, as a fix for QPID-2360 and QPID-2361.
     */
    @SuppressWarnings("unchecked")
    protected void setupVirtualHosts(org.apache.commons.configuration.Configuration conf) throws ConfigurationException {
        
        String[] vhostFiles = conf.getStringArray("virtualhosts");
        org.apache.commons.configuration.Configuration vhostConfig = conf.subset("virtualhosts");

        // Only one configuration mechanism allowed
        if (!(vhostFiles.length == 0) && !vhostConfig.subset("virtualhost").isEmpty()) {
            throw new ConfigurationException("Only one of external or embedded virtualhosts configuration allowed.");
        }

        // We can only have one vhosts XML file included
        if (vhostFiles.length > 1) {
            throw new ConfigurationException("Only one external virtualhosts configuration file allowed, " +
                    "multiple filenames found.");
        }

        // Virtualhost configuration object
        org.apache.commons.configuration.Configuration vhostConfiguration = new HierarchicalConfiguration();

        // Load from embedded configuration if possible
        if (!vhostConfig.subset("virtualhost").isEmpty()) {
            vhostConfiguration = vhostConfig;
        } else {
            // Load from the external configuration if possible
            for (String fileName : vhostFiles) {
                // Open the vhosts XML file and copy values from it to our config
                _vhostsFile = new File(fileName);
                if (!_vhostsFile.exists()) {
                    throw new ConfigurationException("Virtualhosts file does not exist");
                }
                vhostConfiguration = parseConfig(new File(fileName));

                // save the default virtualhost name
                String defaultVirtualHost = vhostConfiguration.getString("default");
                _configuration.setProperty("virtualhosts.default", defaultVirtualHost);
            }
        }

        // Now extract the virtual host names from the configuration object
        List hosts = vhostConfiguration.getList("virtualhost.name");
        for (Object host : hosts) {
            String name = (String) host;

            // Add the virtual hosts to the server configuration
            VirtualHostConfiguration virtualhost = new VirtualHostConfiguration(name,
                    vhostConfiguration.subset("virtualhost." + name));
            _virtualHosts.put(virtualhost.getName(), virtualhost);
        }
    }

    private static void substituteEnvironmentVariables(org.apache.commons.configuration.Configuration conf) {
        for (Entry<String, String> var : envVarMap.entrySet()) {
            String val = System.getenv(var.getKey());
            if (val != null) {
                conf.setProperty(var.getValue(), val);
            }
        }
    }

    private static org.apache.commons.configuration.Configuration parseConfig(File file) throws ConfigurationException {
        ConfigurationFactory factory = new ConfigurationFactory();
        factory.setConfigurationFileName(file.getAbsolutePath());
        org.apache.commons.configuration.Configuration conf = factory.getConfiguration();

        Iterator<?> keys = conf.getKeys();
        if (!keys.hasNext()) {
            conf = flatConfig(file);
        }

        substituteEnvironmentVariables(conf);

        return conf;
    }

    /**
     * Check the configuration file to see if status updates are enabled.
     *
     * @return true if status updates are enabled
     */
    public boolean getStatusUpdatesEnabled() {
        // Retrieve the setting from configuration but default to on.
        String value = getStringValue(STATUS_UPDATES, DEFAULT_STATUS_UPDATES);

        return value.equalsIgnoreCase("on");
    }

    /**
     * The currently defined {@see Locale} for this broker
     *
     * @return the configuration defined locale
     */
    public Locale getLocale() {
        String localeString = getStringValue(ADVANCED_LOCALE);
        // Expecting locale of format langauge_country_variant

        // If the configuration does not have a defined locale use the JVM default
        if (localeString == null) {
            return Locale.getDefault();
        }

        String[] parts = localeString.split("_");

        Locale locale;
        switch (parts.length) {
            case 1:
                locale = new Locale(localeString);
                break;
            case 2:
                locale = new Locale(parts[0], parts[1]);
                break;
            default:
                StringBuilder variant = new StringBuilder(parts[2]);
                // If we have a variant such as the Java doc suggests for Spanish
                // Traditional_WIN we may end up with more than 3 parts on a
                // split with '_'. So we should recombine the variant.
                if (parts.length > 3) {
                    for (int index = 3; index < parts.length; index++) {
                        variant.append('_').append(parts[index]);
                    }
                }

                locale = new Locale(parts[0], parts[1], variant.toString());
        }

        return locale;
    }

    // Our configuration class needs to make the interpolate method
    // public so it can be called below from the config method.
    public static class MyConfiguration extends CompositeConfiguration {
        public String interpolate(String obj) {
            return super.interpolate(obj);
        }
    }

    public static org.apache.commons.configuration.Configuration flatConfig(File file) throws ConfigurationException {
        // We have to override the interpolate methods so that
        // interpolation takes place accross the entirety of the
        // composite configuration. Without doing this each
        // configuration object only interpolates variables defined
        // inside itself.
        final MyConfiguration conf = new MyConfiguration();
        conf.addConfiguration(new SystemConfiguration() {
            protected String interpolate(String o) {
                return conf.interpolate(o);
            }
        });
        conf.addConfiguration(new XMLConfiguration(file) {
            protected String interpolate(String o) {
                return conf.interpolate(o);
            }
        });
        return conf;
    }

    public void handle(Signal arg0) {
        try {
            reparseConfigFileSecuritySections();
        } catch (ConfigurationException e) {
            _logger.error("Could not reload configuration file security sections", e);
        }
    }

    public void reparseConfigFileSecuritySections() throws ConfigurationException {
        if (_configFile != null) {
            org.apache.commons.configuration.Configuration newConfig = parseConfig(_configFile);
            setConfiguration("", newConfig);
            ApplicationRegistry.getInstance().getSecurityManager().configureHostPlugins(this);

            // Reload virtualhosts from correct location
            org.apache.commons.configuration.Configuration newVhosts;
            if (_vhostsFile == null) {
                newVhosts = newConfig.subset("virtualhosts");
            } else {
                newVhosts = parseConfig(_vhostsFile);
            }

            VirtualHostRegistry vhostRegistry = ApplicationRegistry.getInstance().getVirtualHostRegistry();
            for (String hostName : _virtualHosts.keySet()) {
                VirtualHost vhost = vhostRegistry.getVirtualHost(hostName);
                Configuration vhostConfig = newVhosts.subset("virtualhost." + hostName);
                vhost.getConfiguration().setConfiguration("virtualhosts.virtualhost", vhostConfig); // XXX
                vhost.getSecurityManager().configureGlobalPlugins(this);
                vhost.getSecurityManager().configureHostPlugins(vhost.getConfiguration());
            }

            _logger.warn(SECURITY_CONFIG_RELOADED);
        }
    }

    public String getQpidWork() {
        return System.getProperty(QPID_WORK, System.getProperty("java.io.tmpdir"));
    }

    public void setJMXManagementPort(int mport) {
        getConfig().setProperty("management.jmxport", mport);
    }

    public int getJMXManagementPort() {
        return getIntValue("management.jmxport", DEFAULT_JMXPORT);
    }

    public boolean getUseCustomRMISocketFactory() {
        return getBooleanValue(MGMT_CUSTOM_REGISTRY_SOCKET, true);
    }

    public boolean getPlatformMbeanserver() {
        return getBooleanValue("management.platform-mbeanserver", true);
    }

    public String[] getVirtualHosts() {
        return _virtualHosts.keySet().toArray(new String[_virtualHosts.size()]);
    }

    public String getPluginDirectory() {
        return getStringValue("plugin-directory");
    }

    public String getCacheDirectory() {
        return getStringValue("cache-directory");
    }

    public VirtualHostConfiguration getVirtualHostConfig(String name) {
        return _virtualHosts.get(name);
    }

    public int getFrameSize() {
        return getIntValue("advanced.framesize", DEFAULT_FRAME_SIZE);
    }

    public boolean getSynchedClocks() {
        return getBooleanValue("advanced.synced-clocks");
    }

    public boolean getMsgAuth() {
        return getBooleanValue("security.msg-auth");
    }

    public String getManagementKeyStorePath() {
        return getStringValue("management.ssl.keyStorePath");
    }

    public boolean getManagementSSLEnabled() {
        return getBooleanValue("management.ssl.enabled", true);
    }

    public String getManagementKeyStorePassword() {
        return getStringValue("management.ssl.keyStorePassword");
    }

    public boolean getQueueAutoRegister() {
        return getBooleanValue("queue.auto_register", true);
    }

    public boolean getManagementEnabled() {
        return getBooleanValue("management.enabled", true);
    }

    public void setManagementEnabled(boolean enabled) {
        getConfig().setProperty("management.enabled", enabled);
    }

    public int getHeartBeatDelay() {
        return getIntValue("heartbeat.delay", 0);
    }

    public double getHeartBeatTimeout() {
        return getDoubleValue("heartbeat.timeoutFactor", 2.0);
    }

    public long getMaximumMessageAge() {
        return getLongValue("maximumMessageAge");
    }

    public long getMaximumMessageCount() {
        return getLongValue("maximumMessageCount");
    }

    public long getMaximumQueueDepth() {
        return getLongValue("maximumQueueDepth");
    }

    public long getMaximumMessageSize() {
        return getLongValue("maximumMessageSize");
    }

    public long getMinimumAlertRepeatGap() {
        return getLongValue("minimumAlertRepeatGap");
    }

    public long getCapacity() {
        return getLongValue("capacity");
    }

    public long getFlowResumeCapacity() {
        return getLongValue("flowResumeCapacity", getCapacity());
    }

    public int getConnectorProcessors() {
        return getIntValue("connector.processors", 4);
    }

    /**
     * Retrieve Port from Andes configurations(broker.xml).
     *
     * @return Port
     */
    public List getPorts() {
        Integer port = AndesConfigurationManager.readValue(AndesConfiguration.TRANSPORTS_AMQP_DEFAULT_CONNECTION_PORT);

        return Collections.singletonList(port);
    }

    public List getPortExclude010() {
        return getListValue("connector.non010port");
    }

    public List getPortExclude091() {
        return getListValue("connector.non091port");
    }

    public List getPortExclude09() {
        return getListValue("connector.non09port");
    }

    public List getPortExclude08() {
        return getListValue("connector.non08port");
    }

    /**
     * Retrieve bind address from Andes configurations(broker.xml).
     *
     * @return Bind address
     */
    public String getBind() {
        return AndesConfigurationManager.readValue(AndesConfiguration.TRANSPORTS_AMQP_BIND_ADDRESS);
    }

    public int getReceiveBufferSize() {
        return getIntValue("connector.socketReceiveBuffer", DEFAULT_SOCKET_BUFFER_SIZE);
    }

    public int getWriteBufferSize() {
        return getIntValue("connector.socketWriteBuffer", DEFAULT_SOCKET_BUFFER_SIZE);
    }

    public boolean getTcpNoDelay() {
        return getBooleanValue("connector.tcpNoDelay", true);
    }

    public boolean getEnableExecutorPool() {
        return getBooleanValue("advanced.filterchain[@enableExecutorPool]");
    }

    public boolean getEnableSSL() {
        return (Boolean)AndesConfigurationManager.readValue(AndesConfiguration.TRANSPORTS_AMQP_SSL_CONNECTION_ENABLED);
    }

    public boolean getSSLOnly() {
        return (Boolean)AndesConfigurationManager.readValue(AndesConfiguration.TRANSPORTS_AMQP_SSL_CONNECTION_ENABLED) &&
                !(Boolean)AndesConfigurationManager.readValue(AndesConfiguration.TRANSPORTS_AMQP_DEFAULT_CONNECTION_ENABLED);
    }

    public boolean getMQTTSSLOnly() {
        return (Boolean) AndesConfigurationManager.readValue(AndesConfiguration.TRANSPORTS_MQTT_SSL_CONNECTION_ENABLED) &&
               !(Boolean)AndesConfigurationManager.readValue(AndesConfiguration.TRANSPORTS_MQTT_DEFAULT_CONNECTION_ENABLED);
    }

    /**
     * Retrieve SSL Port from Andes configurations(broker.xml).
     *
     * @return SSL Port List
     */
    public List getSSLPorts() {
        Integer sslPort = AndesConfigurationManager.readValue(AndesConfiguration.TRANSPORTS_AMQP_SSL_CONNECTION_PORT);

        return Collections.singletonList(sslPort);
    }

    public String getKeystorePath() {
        return ((JKSStore)AndesConfigurationManager.readValue(AndesConfiguration.TRANSPORTS_AMQP_SSL_CONNECTION_KEYSTORE)).getStoreLocation();
    }

    public String getKeystorePassword() {
        return ((JKSStore)AndesConfigurationManager.readValue(AndesConfiguration.TRANSPORTS_AMQP_SSL_CONNECTION_KEYSTORE)).getPassword();
    }

    public String getKeyStoreCertType() {
        return ((JKSStore)AndesConfigurationManager.readValue(AndesConfiguration.TRANSPORTS_AMQP_SSL_CONNECTION_KEYSTORE)).getStoreAlgorithm();
    }
    
    public boolean getUseBiasedWrites() {
        return getBooleanValue("advanced.useWriteBiasedPool");
    }

    public String getDefaultVirtualHost() {
        return getStringValue("virtualhosts.default");
    }

    public void setHousekeepingExpiredMessageCheckPeriod(long value) {
        getConfig().setProperty("housekeeping.expiredMessageCheckPeriod", value);
    }

    public long getHousekeepingCheckPeriod() {
        return getLongValue("housekeeping.checkPeriod",
                getLongValue("housekeeping.expiredMessageCheckPeriod",
                        DEFAULT_HOUSEKEEPING_PERIOD));
    }

    public boolean isStatisticsGenerationBrokerEnabled() {
        return getConfig().getBoolean("statistics.generation.broker", false);
    }

    public boolean isStatisticsGenerationVirtualhostsEnabled() {
        return getConfig().getBoolean("statistics.generation.virtualhosts", false);
    }

    public boolean isStatisticsGenerationConnectionsEnabled() {
        return getConfig().getBoolean("statistics.generation.connections", false);
    }

    public long getStatisticsReportingPeriod() {
        return getConfig().getLong("statistics.reporting.period", 0L);
    }

    public boolean isStatisticsReportResetEnabled() {
        return getConfig().getBoolean("statistics.reporting.reset", false);
    }

    public int getMaxChannelCount() {
        return getIntValue("maximumChannelCount", 256);
    }

}
