package com.blazemeter.jmeter.xmpp;


import com.blazemeter.jmeter.xmpp.actions.*;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.log.Logger;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.bosh.BOSHConfiguration;
import org.jivesoftware.smack.bosh.XMPPBOSHConnection;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.tcp.XMPPTCPConnection;

import java.io.IOException;
import java.io.Serializable;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class JMeterXMPPConnection extends JMeterXMPPConnectionBase {
    private static final Logger log = LoggingManager.getLoggerForClass();
    private static final BlockingQueue<XMPPConnection> connectionRegistry = new LinkedBlockingQueue<>();
    private XMPPConnection conn;
    private Map<String, AbstractXMPPAction> actions = getAvailableActions();

    public JMeterXMPPConnection() {
        super();
    }


    @Override
    public void testEnded(String host) {
        log.debug("Test ended: " + host);
        for (XMPPConnection conn : connectionRegistry) {
            connectionRegistry.remove(conn);
            if (conn.isConnected()) {
                try {
                    log.debug("Disconnecting: " + conn.getConnectionID());
                    if(conn instanceof AbstractXMPPConnection){
                        ((AbstractXMPPConnection)conn).disconnect();
                    }
                } catch (SmackException.NotConnectedException e) {
                    log.error("Not connected, nothing to disconnect");
                }
            }
        }
    }

    /**
     * Creates new connection or returns previously created
     *
     * @return XMPPConnection
     */
    public XMPPConnection getConnection() throws NoSuchAlgorithmException, KeyManagementException, SmackException, InterruptedException {
        if (conn == null) {
            String address = getAddress();
            String serv_name = getServiceName();
            if (serv_name.isEmpty()) serv_name = address;
            int port = Integer.parseInt(getPort());

            log.debug("Creating connection: " + address + ":" + port + "/" + serv_name);

            XMPPConnection newConn;
            if (Type.valueOf(getConnectionType()) == Type.BOSH) {
                BOSHConfiguration conf = new BOSHConfiguration(isBOSHSSL(), address, port, getBOSHURL(), serv_name);
                conf.setCustomSSLContext(getSSLContext());
                conf.setRosterLoadedAtLogin(true);
                conf.setSendPresence(false);
                newConn = new XMPPBOSHConnection(conf);
            } else {
                ConnectionConfiguration conf = new ConnectionConfiguration(address, port, serv_name);
                conf.setRosterLoadedAtLogin(true);
                conf.setSendPresence(false);

                // accept all TLS certs
                conf.setCustomSSLContext(getSSLContext());
                newConn = new XMPPTCPConnection(conf);
            }

            connectionRegistry.put(newConn);
            setUpConnection(newConn);
        }

        return conn;
    }

    private void setUpConnection(XMPPConnection newConn) {
        conn = newConn;
        conn.setPacketReplyTimeout(Integer.parseInt(getPacketReplyTimeout()));
        conn.setFromMode(getFromMode());

        if (log.isDebugEnabled()) {
            conn.addConnectionListener(new Loggers.LogConn(conn));
            conn.addPacketListener(new Loggers.LogRecv(conn), new AndFilter());
            conn.addPacketSendingListener(new Loggers.LogSent(conn), new AndFilter());
        }

        for (AbstractXMPPAction action : actions.values()) {
            if (action instanceof PacketListener) {
                conn.addPacketListener((PacketListener) action, action.getPacketFilter());
            }
            if (action instanceof ConnectionListener) {
                conn.addConnectionListener((ConnectionListener) action);
            }
        }
    }

    public static Map<String, AbstractXMPPAction> getAvailableActions() {
        Map<String, AbstractXMPPAction> actions = new TreeMap<>(new OrderComparator());

        try {
            for (String cls : JMeterUtils.findClassesThatExtend(AbstractXMPPAction.class)) {
                actions.put(cls, (AbstractXMPPAction) Class.forName(cls).newInstance());
            }
        } catch (IOException | InstantiationException | IllegalAccessException | ClassNotFoundException e) {
            log.error("Error loading actions", e);
        }

        return actions;
    }

    public Map<String, AbstractXMPPAction> getActions() {
        return actions;
    }

    @Override
    public void resetConnection() {
        connectionRegistry.remove(conn);
        conn = null;
    }

    private static class OrderComparator implements Comparator<String>, Serializable {
        private static final List<String> fixed = Arrays.asList(
                Connect.class.getCanonicalName(),
                Login.class.getCanonicalName(),
                RosterAction.class.getCanonicalName(),
                SendPresence.class.getCanonicalName(),
                SendMessage.class.getCanonicalName(),
                RawXML.class.getCanonicalName(),
                NoOp.class.getCanonicalName(),
                Disconnect.class.getCanonicalName()
        );

        @Override
        public int compare(String s1, String s2) {
            if (s1.equals(s2)) return 0;

            int i1 = fixed.indexOf(s1);
            int i2 = fixed.indexOf(s2);

            if (i1 < 0 && i2 < 0)
                return s1.compareTo(s2);
            else if (i1 < 0)
                return 1;
            else if (i2 < 0)
                return -1;
            else
                return i1 > i2 ? 1 : -1;
        }
    }
}
