/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF 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.apache.synapse.transport.fix;

import org.apache.axis2.AxisFault;
import org.apache.axis2.description.AxisService;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.transport.base.BaseUtils;
import org.apache.axis2.transport.base.threads.WorkerPool;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quickfixj.jmx.JmxExporter;
import quickfix.*;

import javax.management.JMException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

/**
 * The FIXSessionFactory is responsible for creating and managing FIX sessions. A FIX session can be
 * initiated in one of two modes, namely the acceptor mode and the initiator mode. FIX sessions
 * requested by the transport listener at service deployment are created in acceptor mode. When
 * the transport sender is about to send a FIX message it will check whether a valid FIX session exists.
 * If not it will request the FIXSessionFactory to create a new session in the initiator mode.
 * <p/>
 * To create a new FIX session (in either mode) the FIXSessionFactory has to create a LogFactory (nullable),
 * and a MessageStoreFactroy. By default this implementation attempts to pass null as the LogFactory and a
 * MemoryStoreFactory as the MessageStoreFactory. These can be configured in the services.xml as follows.
 * <p/>
 * <parameter name="transport.fix.AcceptorLogFactory">file</parameter>
 * (acceptable values: console, file, jdbc)
 * <p/>
 * <parameter name="transport.fix.AcceptorMessageStore">file</parameter>
 * (acceptable values: file, jdbc, memory, sleepycat)
 * <p/>
 * The configuraion details related to these factories has to be specified in the FIX configuration file
 * as requested by the Quickfix/J API.
 */
public class FIXSessionFactory {

    /** A Map Containing all the FIX Acceptors created by this factory, keyed by the service name */
    private Map<String,Acceptor> acceptorStore;
    /** A Map containing all the FIX Initiators created by this factory, keyed by FIX EPR */
    private Map<String,Initiator> initiatorStore;
    /** A Map containing all the FIX applications created for initiators, keyed by FIX EPR */
    private Map<String, Application> applicationStore;
    /** An ApplicationFactory handles creating FIX Applications (FIXIncomingMessageHandler Objects) */
    private static FIXApplicationFactory applicationFactory = null;

    private WorkerPool listenerThreadPool;
    private WorkerPool senderThreadPool;

    private Log log;

    private static FIXSessionFactory INSTANCE = new FIXSessionFactory();

    public static FIXSessionFactory getInstance(FIXApplicationFactory af) {
        if (applicationFactory == null) {
            applicationFactory = af;
        }
        return INSTANCE;   
    }

    private FIXSessionFactory() {
        this.log = LogFactory.getLog(this.getClass());
        this.acceptorStore = new HashMap<String,Acceptor>();
        this.initiatorStore = new HashMap<String, Initiator>();
        this.applicationStore = new HashMap<String, Application>();
        this.listenerThreadPool = null;
        this.senderThreadPool = null;
    }

    /**
     * Get the FIX configuration settings and initialize a new FIX session for the specified
     * service. Create an Acceptor and a new FIX Application. Put the Acceptor into the
     * acceptorStore keyed by the service name and start it.
     *
     * @param service the AxisService
     * @return true if the acceptor is successfully initialized and false otherwise
     * @throws AxisFault if the acceptor cannot be created
     */
    public boolean createFIXAcceptor(AxisService service) throws AxisFault {

        //Try to get an InputStream to the FIX configuration file
        InputStream fixConfigStream = getFIXConfigAsStream(service, true);

        if (fixConfigStream != null) {
            try {
                if (log.isDebugEnabled()) {
                    log.debug ("Initializing a new FIX session for the service " +
                            service.getName());
                }

                SessionSettings settings = new SessionSettings(fixConfigStream);
                MessageStoreFactory storeFactory = getMessageStoreFactory(service, settings, true);
                MessageFactory messageFactory = new DefaultMessageFactory();
                quickfix.LogFactory logFactory = getLogFactory(service, settings, true);
                //Get a new FIX Application
                Application messageHandler = applicationFactory.getFIXApplication(service,
                        listenerThreadPool, true);
                //Create a new FIX Acceptor
                Acceptor acceptor = new SocketAcceptor(
                        messageHandler,
                        storeFactory,
                        settings,
                        logFactory,
                        messageFactory);

                acceptor.start();
                initJMX(acceptor, service.getName());
                acceptorStore.put(service.getName(),acceptor);
                return true;
            } catch (ConfigError e) {
                String msg = "Error in the specified FIX configuration. Unable to initialize a " +
                        "FIX session for the service " + service.getName();
                log.error(msg, e);
                throw new AxisFault(msg, e);
            }

        } else {
            return false;
        }
    }

    /**
     * Extract the parameters embedded in the given EPR and initialize a new FIX session.
     * Create a new FIX initiator and a new FIX Application.Put the initiator into the
     * initiatorStore keyed by the EPR and start the initiator.
     *
     * @param fixEPR the EPR to send FIX messages
     * @param service the AxisService
     * @param sessionID the SessionID of the session created
     * @throws org.apache.axis2.AxisFault Exception thrown
     */
    public void createFIXInitiator(String fixEPR, AxisService service,
                                   SessionID sessionID) throws AxisFault {

        if (log.isDebugEnabled()) {
            log.debug("Initializing a new FIX initiator for the service " + service.getName());
        }
        SessionSettings settings;
        InputStream fixConfigStream = getFIXConfigAsStream(service, false);

        if (fixConfigStream == null) {
            settings = new SessionSettings();
            settings.setLong(sessionID, FIXConstants.HEART_BY_INT,
                    FIXConstants.DEFAULT_HEART_BT_INT_VALUE);
            settings.setString(sessionID, FIXConstants.START_TIME,
                    FIXConstants.DEFAULT_START_TIME_VALUE);
            settings.setString(sessionID, FIXConstants.END_TIME,
                    FIXConstants.DEFAULT_END_TIME_VALUE);
        } else {
            try {
                settings = new SessionSettings(fixConfigStream);
            } catch (ConfigError e) {
                throw new AxisFault("Error in the specified FIX configuration for the initiator. " +
                        "Unable to initialize a FIX session for the service " +
                        service.getName(), e);
            }
        }

        Hashtable<String,String> properties = BaseUtils.getEPRProperties(fixEPR);
        for (Map.Entry<String,String> entry : properties.entrySet()) {
            settings.setString(sessionID, entry.getKey(), entry.getValue());
        }

        String[] socketAddressElements = FIXUtils.getSocketAddressElements(fixEPR);
        settings.setString(sessionID, FIXConstants.CONNECTION_TYPE, FIXConstants.FIX_INITIATOR);
        settings.setString(sessionID, FIXConstants.SOCKET_CONNECT_HOST, socketAddressElements[0]);
        settings.setString(sessionID, FIXConstants.SOCKET_CONNECT_PORT, socketAddressElements[1]);

        quickfix.LogFactory logFactory = getLogFactory(service, settings, false);
        MessageStoreFactory storeFactory = getMessageStoreFactory(service, settings, false);
        MessageFactory messageFactory = new DefaultMessageFactory();
        //Get a new FIX application
        Application messageHandler = applicationFactory.getFIXApplication(service,
                senderThreadPool, false);

        try {
           //Create a new FIX initiator
            Initiator initiator = new SocketInitiator(
                    messageHandler,
                    storeFactory,
                    settings,
                    logFactory,
                    messageFactory);

            initiator.start();
            initJMX(initiator, service.getName());
            initiatorStore.put(fixEPR, initiator);
            applicationStore.put(fixEPR, messageHandler);

            FIXIncomingMessageHandler fixMessageHandler = (FIXIncomingMessageHandler) messageHandler;
            log.info("Waiting for logon procedure to complete...");
            fixMessageHandler.acquire();

        } catch (ConfigError e) {
            throw new AxisFault("Error in the specified FIX configuration for the initiator. Unable " +
                    "to initialize a FIX initiator.", e);
        } catch (InterruptedException ignore) { }
    }

    public boolean createFIXInitiator(AxisService service) throws AxisFault {

        InputStream fixConfigStream = getFIXConfigAsStream(service, false);
        if (fixConfigStream != null) {

            if (log.isDebugEnabled()) {
                log.debug("Attempting to initialize a new FIX initiator " +
                    "for the service " + service.getName());
            }

            try {
                SessionSettings settings = new SessionSettings(fixConfigStream);

                /*Stop and Clean-up if there is already existing initiator with the same key */
                String[] existingEPRs = FIXUtils.getEPRs(settings);
                for (String epr : existingEPRs) {
                    if (initiatorStore.get(epr) != null) {
                        initiatorStore.get(epr).stop();
                        initiatorStore.remove(epr);
                    }
                    if (applicationStore.get(epr) != null) {
                        applicationStore.remove(epr);
                    }
                }

                MessageStoreFactory storeFactory = getMessageStoreFactory(service, settings, false);
                MessageFactory messageFactory = new DefaultMessageFactory();
                quickfix.LogFactory logFactory = getLogFactory(service, settings, false);
                //Get a new FIX Application
                Application messageHandler = applicationFactory.getFIXApplication(service,
                        senderThreadPool, false);

                Initiator initiator = new SocketInitiator(
                    messageHandler,
                    storeFactory,
                    settings,
                    logFactory,
                    messageFactory);

                initiator.start();
                initJMX(initiator, service.getName());
                String[] EPRs = FIXUtils.getEPRs(settings);
                for (String EPR : EPRs) {
                    initiatorStore.put(EPR, initiator);
                    applicationStore.put(EPR, messageHandler);
                }
                return true;

            } catch (FieldConvertError e) {
                String msg = "FIX configuration file for the initiator session of the service " +
                        service.getName() + " is either incomplete or invalid." +
                        " Not creating the initiator session at this stage.";
                log.error(msg, e);
                throw new AxisFault(msg, e);
            } catch (ConfigError e) {
                String msg = "FIX configuration file for the initiator session of the service " +
                        service.getName() + " is either incomplete or invalid." +
                        " Not creating the initiator session at this stage.";
                log.error(msg, e);
                throw new AxisFault(msg, e);
            }

        } else {
            // FIX initiator session is not configured
            // It could be intentional - So not an error (we don't need initiators at all times)
            log.info("The " + FIXConstants.FIX_INITIATOR_CONFIG_URL_PARAM + " parameter is " +
                    "not specified. Unable to initialize the initiator session at this stage.");
        }

        return false;
    }


    /**
     * Get the FIX Acceptor for the specified service from the sessionStore Map and
     * stop it. Then remove the Acceptor from the Map.
     *
     * @param service the AxisService
     */
    public void disposeFIXAcceptor(AxisService service) {
        if (log.isDebugEnabled()) {
            log.debug("Stopping the FIX acceptor for the service " + service.getName());
        }
        //Get the Acceptor for the service
        Acceptor acceptor = acceptorStore.get(service.getName());
        if (acceptor != null) {
            //Stop the Acceptor
            acceptor.stop();
            log.info("FIX session for service " + service.getName() + " terminated...");
            //Remove the Acceptor from the store
            acceptorStore.remove(service.getName());
        }
    }

    /**
     * Stops all the FIX initiators created so far and cleans up all the mappings
     * related to them
     */
    public void disposeFIXInitiators() {
        boolean debugEnabled = log.isDebugEnabled();

        for (String key : initiatorStore.keySet()) {
            initiatorStore.get(key).stop();
            if (debugEnabled) {
                log.debug("FIX initiator to the EPR " + key + " stopped");
            }
        }

        initiatorStore.clear();
        applicationStore.clear();
    }

    /**
     * Returns an array of Strings representing EPRs for the specified service
     *
     * @param serviceName the name of the service
     * @param ip the IP address of the host
     * @return an array of EPRs for the specified service
     */
    public String[] getServiceEPRs(String serviceName, String ip) {
        if (log.isDebugEnabled()) {
            log.debug("Getting EPRs for the service " + serviceName);
        }
        //Get the acceptpr for the specified service
        SocketAcceptor acceptor = (SocketAcceptor) acceptorStore.get(serviceName);

        if (acceptor != null) {
            return FIXUtils.generateEPRs(acceptor, serviceName, ip);
        } else {
            return new String[]{};
        }
    }

    /**
     * Finds a FIX Acceptor for the specified service from the acceptorStore
     *
     * @param serviceName the name of the AxisService
     * @return a FIX Acceptor for the service
     */
    public Acceptor getAcceptor(String serviceName) {
        return acceptorStore.get(serviceName);
    }

    /**
     * Finds a FIX initiator for the specified EPR from the initiatorStore
     *
     * @param fixEPR a valid FIX EPR
     * @return  a FIX initiator for the EPR
     */
   public Initiator getInitiator(String fixEPR) {
        return initiatorStore.get(fixEPR);
   }

    /**
     * Get the FIX configuration URL from the services.xml.
     *
     * @param service the AxisService
     * @param acceptor boolean value indicating the FIX application type
     * @return an InputStream to the FIX configuration file/resource
     */
    private InputStream getFIXConfigAsStream(AxisService service, boolean acceptor) {
        InputStream fixConfigStream = null;
        Parameter fixConfigURLParam;

        if (acceptor) {
            fixConfigURLParam = service.getParameter(FIXConstants.FIX_ACCEPTOR_CONFIG_URL_PARAM);
        } else {
            fixConfigURLParam = service.getParameter(FIXConstants.FIX_INITIATOR_CONFIG_URL_PARAM);
        }

        if (fixConfigURLParam != null) {
            String fixConfigURLValue = fixConfigURLParam.getValue().toString();
            try {
                URL url = new URL(fixConfigURLValue);
                fixConfigStream = url.openStream();
            } catch (MalformedURLException e) {
                log.error("The FIX configuration URL " + fixConfigURLValue + " is" +
                       " malformed.", e);
            } catch (IOException e) {
                log.error("Error while reading from the URL " + fixConfigURLValue, e);
            }
        } else {
            log.info("FIX configuration URL is not specified for the service " + service.getName());
        }

        return fixConfigStream;
    }

    /**
     * Creates a Quickfix LogFactory object for logging as specified in the services.xml and
     * the FIX configuration file. Default is null.
     *
     * @param service the AxisService
     * @param settings SessionSettings to be used with the service
     * @param acceptor a boolean value indicating the type of the FIX application
     * @return a LogFactory for the FIX application
     */
    private quickfix.LogFactory getLogFactory(AxisService service, SessionSettings settings,
                                              boolean acceptor) {

        quickfix.LogFactory logFactory = null;
        Parameter fixLogMethod;

        //Read the parameter from the services.xml
        if (acceptor) {
           fixLogMethod = service.getParameter(FIXConstants.FIX_ACCEPTOR_LOGGER_PARAM);
        } else {
            fixLogMethod = service.getParameter(FIXConstants.FIX_INITIATOR_LOGGER_PARAM);
        }

        if (fixLogMethod != null) {
               String method = fixLogMethod.getValue().toString();
                log.info("FIX logging method = " + method);

                if (FIXConstants.FILE_BASED_MESSAGE_LOGGING.equals(method)) {
                    logFactory = new FileLogFactory(settings);
                } else if (FIXConstants.JDBC_BASED_MESSAGE_LOGGING.equals(method)) {
                    logFactory = new JdbcLogFactory(settings);
                } else if (FIXConstants.CONSOLE_BASED_MESSAGE_LOGGING.equals(method)) {
                    logFactory = new ScreenLogFactory();
                } else {
                    log.warn("Invalid acceptor log method " + method + ". Using defaults.");
                }
        }
        return logFactory;
    }

    /**
     * Creates a Quickfix MessageStoreFactory for storing FIX messages as specified in the services.xml
     * and the FIX configuration file. Default is FileStoreFactory.
     *
     * @param service the AxisService
     * @param settings SessionSettings to be used with the service
     * @param acceptor a boolean value indicating the type of the FIX application
     * @return a MessageStoreFactory for the FIX application
     */
    private MessageStoreFactory getMessageStoreFactory(AxisService service, SessionSettings settings,
                                                       boolean acceptor) {

        MessageStoreFactory storeFactory = new MemoryStoreFactory();
        Parameter msgLogMethod;

        //Read the parameter from the services.xml
        if (acceptor) {
            msgLogMethod = service.getParameter(FIXConstants.FIX_ACCEPTOR_MESSAGE_STORE_PARAM);
        } else {
            msgLogMethod = service.getParameter(FIXConstants.FIX_INITIATOR_MESSAGE_STORE_PARAM);
        }

        if (msgLogMethod != null) {
            String method = msgLogMethod.getValue().toString();
            log.info("FIX message logging method = " + method);

            if (FIXConstants.JDBC_BASED_MESSAGE_STORE.equals(method)) {
                storeFactory = new JdbcStoreFactory(settings);
            } else if (FIXConstants.SLEEPYCAT_BASED_MESSAGE_STORE.equals(method)) {
                storeFactory = new SleepycatStoreFactory(settings);
            } else if (FIXConstants.FILE_BASED_MESSAGE_STORE.equals(method)) {
                storeFactory = new FileStoreFactory(settings);
            } else if (!FIXConstants.MEMORY_BASED_MESSAGE_STORE.equals(method)) {
                log.warn("Invalid message store " + method + ". Using defaults.");
            }
        }

        return storeFactory;
    }
    
    public Application getApplication(String fixEPR) {
        Application app = applicationStore.get(fixEPR);
        if (app == null) {
            for (String epr : applicationStore.keySet()) {
                if (FIXUtils.compareURLs(epr, fixEPR)) {
                    app = applicationStore.get(epr);
                    applicationStore.remove(epr);
                    applicationStore.put(fixEPR, app);
                    break;
                }
            }
        }
        return app;
    }

    public void setListenerThreadPool(WorkerPool listenerThreadPool) {
        this.listenerThreadPool = listenerThreadPool;
    }

    public void setSenderThreadPool(WorkerPool senderThreadPool) {
        this.senderThreadPool = senderThreadPool;
    }

    private void initJMX(Connector connector, String service) {
        try {
            JmxExporter jmxExporter = new JmxExporter();
            jmxExporter.setRegistrationBehavior(JmxExporter.REGISTRATION_IGNORE_EXISTING);
            jmxExporter.register(connector);
        } catch (JMException e) {
            log.error("Error while initializing JMX support for the service: " + service, e);
        }
    }
}


