package com.imofan.android.pns;

import android.content.Context;
import android.os.Build;
import android.os.Handler;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.StreamError;
import org.jivesoftware.smackx.XHTMLManager;
import org.jivesoftware.smackx.ping.PingFailedListener;
import org.jivesoftware.smackx.ping.PingManager;
import org.jivesoftware.smackx.ServiceDiscoveryManager;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

/**
 *
 */
class XmppManager {
    private final static String LOG_TAG = "Mofang_PNS_XmppManager";

    // my first measuring showed that the disconnect in fact does not hang
    // but takes sometimes a lot of time
    // disconnectED xmpp connection. Took: 1048.576 s
    public static final int DISCON_TIMEOUT = 1000 * 10; // 10s
    // The timeout for XMPP connections that get created with
    // DNS SRV information
    public static final int DNSSRV_TIMEOUT = 1000 * 30; // 30s

    public static final int DISCONNECTED = 1;
    // A "transient" state - will only be CONNECTING *during* a call to start()
    public static final int CONNECTING = 2;
    public static final int CONNECTED = 3;
    // A "transient" state - will only be DISCONNECTING *during* a call to stop()
    public static final int DISCONNECTING = 4;
    // This state means we are waiting for a retry attempt etc.
    // mostly because a connection went down
    public static final int WAITING_TO_CONNECT = 5;
    // We are waiting for a valid data connection
    public static final int WAITING_FOR_NETWORK = 6;

    private static XmppManager sXmppManager = null;
    private static int sReusedConnectionCount = 0;
    private static int sNewConnectionCount = 0;

    // Indicates the current state of the service (disconnected/connecting/connected)
    private int mStatus = DISCONNECTED;

    private List<XmppConnectionChangeListener> mConnectionChangeListeners;
    private XMPPConnection mConnection = null;
    private PacketListener mPacketListener = null;
    private PacketListener mPresencePacketListener = null;
    private PingManager mPingManager = null;
    private XmppPresenceStatus mXmppPresenceStatus;
    private ConnectionListener mConnectionListener = null;

    // Our current retry attempt, plus a runnable and handler to implement retry
    private int mCurrentRetryCount = 0;
    private Runnable mReconnectRunnable = new Runnable() {
        public void run() {
            PNService.start(PNService.getInstance(), null, false);
        }
    };

    private Handler mReconnectHandler;

    private Context mContext;
    protected SmackAndroid mSmackAndroid;

    private XmppManager(Context context, XMPPConnection connection) {
        if (PNService.DEBUG) {
            Connection.DEBUG_ENABLED = true;
        }

        mSmackAndroid = SmackAndroid.init(context);

        mReconnectHandler = new Handler(PNService.getServiceLooper());

        mConnectionChangeListeners = new ArrayList<XmppConnectionChangeListener>();
        mContext = context;
        mXmppPresenceStatus = XmppPresenceStatus.getInstance(context);
        mXmppPresenceStatus.registerListener(this);
//        XmppLocalS5BProxyManager.getInstance(context).registerListener(this);
        sReusedConnectionCount = 0;
        sNewConnectionCount = 0;
        //TODO
        ServiceDiscoveryManager.setIdentityName(PNService.SERVICE_NAME);
        ServiceDiscoveryManager.setIdentityType("bot"); // http://xmpp.org/registrar/disco-categories.html

        // Smack Settings
//        SmackConfiguration.setKeepAliveInterval(1000 * 60 * 12);  // 12 min
        SmackConfiguration.setKeepAliveInterval(1000 * PNService.KEEP_ALIVE_INTERVAL);
        SmackConfiguration.setPacketReplyTimeout(1000 * PNService.PACKET_REPLY_TIMEOUT);      // 40 sec
        SmackConfiguration.setLocalSocks5ProxyEnabled(true);
        SmackConfiguration.setLocalSocks5ProxyPort(-7777);        // negative number means try next port if already in use

        // connection can be null, it is created on demand
        mConnection = connection;
    }

    public static XmppManager getInstance(Context ctx) {
        if (sXmppManager == null) {
            sXmppManager = new XmppManager(ctx, null);
        }
        return sXmppManager;
    }

    private void start(int initialState) {
        switch (initialState) {
            case CONNECTED:
                initConnection();
                break;
            case WAITING_TO_CONNECT:
            case WAITING_FOR_NETWORK:
                updateStatus(initialState);
                break;
            default:
                throw new IllegalStateException("xmppMgr start() Invalid State: " + initialState);
        }
    }

    /**
     * calls cleanupConnection and
     * sets _status to DISCONNECTED
     */
    private void stop() {
        updateStatus(DISCONNECTING);
        cleanupConnection();
        updateStatus(DISCONNECTED);
        mConnection = null;
//        PNService.releaseWakeLock();
    }

    /**
     * Removes all references to the old connection.
     *
     * Spawns a new disconnect runnable if the connection
     * is still connected and removes packetListeners and
     * Callbacks for the reconnectHandler.
     *
     * synchronized because cleanupConnection() ->
     * maybeStartReconnect() -> connectionClosedOnError()
     * is called from a different thread
     */
    private synchronized void cleanupConnection() {
        mReconnectHandler.removeCallbacks(mReconnectRunnable);

        if (mConnection != null) {
            if (mPacketListener != null) {
                mConnection.removePacketListener(mPacketListener);
            }
            if (mConnectionListener != null) {
                mConnection.removeConnectionListener(mConnectionListener);
            }
            if (mPresencePacketListener != null) {
                mConnection.removePacketListener(mPresencePacketListener);
            }

            if (mConnection.isConnected()) {
                // Try to disconnect
                Thread t = new Thread(new Runnable() {
                    public void run() {
                        try {
                            mConnection.disconnect();
                        } catch (Exception e) {}
                    }
                }, "xmpp-disconnector");
                // we don't want this thread to hold up process shutdown so mark as daemon.
                t.setDaemon(true);
                t.start();

                try {
                    t.join(DISCON_TIMEOUT);
                } catch (InterruptedException e) {
                    mConnection = null;
                    mPingManager = null;
                }
            }
        }
        mPacketListener = null;
        mConnectionListener = null;
        mPresencePacketListener = null;
    }

    /**
     * This method *requests* a state change - what state things actually
     * wind up in is impossible to know (eg, a request to connect may wind up
     * with a state of CONNECTED, DISCONNECTED or WAITING_TO_CONNECT...
     */
    protected void xmppRequestStateChange(int newState) {
        int currentState = getConnectionStatus();
        PNLog.i(LOG_TAG, "xmppRequestStateChange " + statusAsString(currentState) + " => " + statusAsString(newState));
        switch (newState) {
            case XmppManager.CONNECTED:
                if (!isXmppConnected()) {
                    cleanupConnection();
                    start(XmppManager.CONNECTED);
                }
                break;
            case XmppManager.DISCONNECTED:
                stop();
                break;
            case XmppManager.WAITING_TO_CONNECT:
                cleanupConnection();
                start(XmppManager.WAITING_TO_CONNECT);
                break;
            case XmppManager.WAITING_FOR_NETWORK:
                cleanupConnection();
                start(XmppManager.WAITING_FOR_NETWORK);
                break;
            default:
                PNLog.w(LOG_TAG, "xmppRequestStateChange() invalid state to switch to: " + statusAsString(newState));
        }
        // Now we have requested a new state, our state receiver will see when
        // the state actually changes and update everything accordingly.
    }

    /**
     * updates the connection status
     * and calls broadCastStatus()
     *
     * @param status
     */
    private void updateStatus(int status) {
        if (status != mStatus) {
            // ensure _status is set before broadcast, just in-case
            // a receiver happens to wind up querying the state on
            // delivery.
//            int old = mStatus;
            mStatus = status;
//            PNLog.i(LOG_TAG, "broadcasting state transition from " + statusAsString(old) + " to " + statusAsString(status) + " via Intent " + MainService.ACTION_XMPP_CONNECTION_CHANGED);
//            broadcastStatus(mContext, old, status);
        }
    }

    private void maybeStartReconnect() {
        int timeout;
        updateStatus(WAITING_TO_CONNECT);
        cleanupConnection();
        mCurrentRetryCount += 1;
        if (mCurrentRetryCount < 20) {
            // a simple linear-backoff strategy.
            timeout = 5000 * mCurrentRetryCount;
        } else {
            // every 5 min
            timeout = 1000 * 60 * 5;
        }
        PNLog.i(LOG_TAG, "maybeStartReconnect scheduling retry in " + timeout + "ms. Retry #" + mCurrentRetryCount);
        mReconnectHandler.postDelayed(mReconnectRunnable, timeout);
    }


    /**
     * Initializes the XMPP connection
     *
     * 1. Creates a new XMPPConnection object if necessary
     * 2. Connects the XMPPConnection
     * 3. Authenticates the user with the server
     *
     * Calls maybeStartReconnect() if something went wrong
     *
     */
    private void initConnection() {
//        PNService.acquireWakeLock(PNService.CPU_ACTIVE_TIME);

        try {
            XMPPConnection connection;

            // assert we are only ever called from one thread
            assert (!Thread.currentThread().getName().equals(PNService.SERVICE_NAME));

            // everything is ready for a connection attempt
            updateStatus(CONNECTING);

            // create a new connection if the connection is obsolete or if the
            // old connection is still active
            if (mConnection == null
                    || mConnection.isConnected() ) {

                try {
                    connection = createNewConnection();
                } catch (Exception e) {
                    // connection failure
                    PNLog.e(LOG_TAG, "Exception creating new XMPP Connection");
                    e.printStackTrace();
                    maybeStartReconnect();
                    return;
                }
                if (!connectAndAuth(connection)) {
                    // connection failure
                    return;
                }
                sNewConnectionCount++;
            } else {
                // reuse the old connection settings
                connection = mConnection;
                // we reuse the xmpp connection so only connect() is needed
                if (!connectAndAuth(connection)) {
                    // connection failure
                    return;
                }
                sReusedConnectionCount++;
            }
            // this code is only executed if we have an connection established
            onConnectionEstablished(connection);
        }finally {
//            PNService.releaseWakeLock();
        }
    }

    private void onConnectionEstablished(XMPPConnection connection) {
        mConnection = connection;
        mConnectionListener = new ConnectionListener() {
            public void connectionClosed() {
                // connection was closed by the foreign host
                // or we have closed the connection
                PNLog.i(LOG_TAG, "ConnectionListener: connectionClosed() called - connection was shutdown by foreign host or by us");
                xmppRequestStateChange(getConnectionStatus());
            }

            public void connectionClosedOnError(Exception e) {
                // this happens mainly because of on IOException
                // eg. connection timeouts because of lost connectivity
                PNLog.w(LOG_TAG, "xmpp disconnected due to error: ");
                e.printStackTrace();
                if (e.getMessage().startsWith("Attr.value missing")) {
                    PNLog.w(LOG_TAG, (android.util.Log.getStackTraceString(e)));
                }
                // We update the state to disconnected (mainly to cleanup listeners etc)
                // then schedule an automatic reconnect.
                maybeStartReconnect();
            }

            public void reconnectingIn(int arg0) {
                throw new IllegalStateException("Reconnection Manager is running");
            }

            public void reconnectionFailed(Exception arg0) {
                throw new IllegalStateException("Reconnection Manager is running");
            }

            public void reconnectionSuccessful() {
                throw new IllegalStateException("Reconnection Manager is running");
            }
        };
        mConnection.addConnectionListener(mConnectionListener);

        try {
            informListeners(mConnection);

            PacketFilter filter = new XmppMessageFilter(Message.Type.chat);
            mPacketListener = new XmppPacketListener(mConnection, mContext);
            mConnection.addPacketListener(mPacketListener, filter);

            //It is important that we query the server for offline messages BEFORE we send the first presence stanza
//            XmppOfflineMessages.handleOfflineMessages(mConnection);
        } catch (Exception e) {
            // see issue 126 for an example where this happens because
            // the connection drops while we are in initConnection()
            PNLog.e(LOG_TAG, "xmppMgr exception caught");
            e.printStackTrace();
            maybeStartReconnect();
            return;
        }

        PNLog.i(LOG_TAG, "connection established with parameters: con=" + mConnection.isConnected() +
                " auth=" + mConnection.isAuthenticated() +
                " enc=" + mConnection.isUsingTLS() +
                " comp=" + mConnection.isUsingCompression());

        mCurrentRetryCount = 0;
        updateStatus(CONNECTED);
    }

    private void informListeners(XMPPConnection connection) {
        for (XmppConnectionChangeListener listener : mConnectionChangeListeners) {
            listener.newConnection(connection);
        }
    }

    /**
     * Tries to fully establish the given XMPPConnection
     * Calls maybeStartReconnect() or stop() in an error case
     *
     * @param connection
     * @return true if we are connected and authenticated, false otherwise
     */
    private boolean connectAndAuth(XMPPConnection connection) {
        try {
            connection.connect();
        } catch (Exception e) {
            PNLog.w(LOG_TAG, "XMPP connection failed");
            e.printStackTrace();
            if (e instanceof XMPPException) {
                XMPPException xmppEx = (XMPPException) e;
                StreamError error = xmppEx.getStreamError();
                // Make sure the error is not null
                if (error != null) {
                    PNLog.w(LOG_TAG, "XMPP connection failed because of stream error: " + error.toString());
                }
            }
            maybeStartReconnect();
            mConnection = null;
            return false;
        }

        // if we reuse the connection and the auth was done with the connect()
        if (connection.isAuthenticated()) {
            return true;
        }

        //TODO
        ServiceDiscoveryManager serviceDiscoMgr = ServiceDiscoveryManager.getInstanceFor(connection);
        mPingManager = PingManager.getInstaceFor(connection);
        mPingManager.registerPingFailedListener(new PingFailedListener() {
            public void pingFailed() {
                // TODO remember that maybeStartReconnect is called from a
                // different thread (the PingTask) here, it may causes
                // synchronization problems
                PNLog.d(LOG_TAG, "PingManager reported failed ping, calling maybeStartReconnect()");
                maybeStartReconnect();

            }
        });

        XHTMLManager.setServiceEnabled(connection, false);
        //TODO
        serviceDiscoMgr.addFeature("http://jabber.org/protocol/disco#info");
        serviceDiscoMgr.addFeature("bug-fix-gtalksms");

        try {
            connection.login(PNService.udid, PNService.password, PNService.SERVICE_NAME);
        } catch (Exception e) {
            cleanupConnection();

            PNLog.e(LOG_TAG, "xmpp login failed: " + e.getMessage());
            // sadly, smack throws the same generic XMPPException for network
            // related messages (eg "no response from the server") as for
            // authoritative login errors (ie, bad password).  The only
            // differentiator is the message itself which starts with this
            // hard-coded string.
            if (e.getMessage().indexOf("SASL authentication") == -1) {
                // doesn't look like a bad username/password, so retry
                maybeStartReconnect();
            } else {
                stop();
            }
            return false;
        }
        return true;
    }

    /**
     * Parses the current preferences and returns an new unconnected
     * XMPPConnection
     * @return
     * @throws XMPPException
     */
    private static XMPPConnection createNewConnection() throws XMPPException {
        ConnectionConfiguration conf;

        // DNS SRV lookup, yeah! :)
        // Note: The Emulator will throw here an BadAddressFamily Exception
        // but on a real device it just works fine
        // see: http://stackoverflow.com/questions/2879455/android-2-2-and-bad-address-family-on-socket-connect
        // and http://code.google.com/p/android/issues/detail?id=9431
        conf = new AndroidConnectionConfiguration(PNService.PUSHER_HOST, DNSSRV_TIMEOUT);

        conf.setSocketFactory(new XmppSocketFactory());

        if (Build.VERSION.SDK_INT >= 14) {  //ICE_CREAM_SANDWICH, 4.0
            conf.setTruststoreType("AndroidCAStore");
            conf.setTruststorePassword(null);
            conf.setTruststorePath(null);
        } else {
            conf.setTruststoreType("BKS");
            String path = System.getProperty("javax.net.ssl.trustStore");
            if (path == null) {
                path = System.getProperty("java.home") + File.separator + "etc"
                        + File.separator + "security" + File.separator
                        + "cacerts.bks";
            }
            conf.setTruststorePath(path);
        }

//        switch (settings.xmppSecurityModeInt) {
//        case SettingsManager.XMPPSecurityOptional:
        conf.setSecurityMode(ConnectionConfiguration.SecurityMode.enabled);
//            break;
//        case SettingsManager.XMPPSecurityRequired:
//            conf.setSecurityMode(ConnectionConfiguration.SecurityMode.required);
//            break;
//        case SettingsManager.XMPPSecurityDisabled:
//        default:
//            conf.setSecurityMode(ConnectionConfiguration.SecurityMode.disabled);
//            break;
//        }

        conf.setCompressionEnabled(false);

        // disable the built-in ReconnectionManager since we handle this
        conf.setReconnectionAllowed(false);
        conf.setSendPresence(false);
        conf.setRosterLoadedAtLogin(false);

        XMPPConnection connection = new XMPPConnection(conf);
        return connection;
    }

    /** returns the current connection state */
    public int getConnectionStatus() {
        return mStatus;
    }

    public boolean getTLSStatus() {
        return mConnection == null ? false : mConnection.isUsingTLS();
    }

    public boolean getCompressionStatus() {
        return mConnection == null ? false : mConnection.isUsingCompression();
    }

    public boolean isConnected() {
        return isXmppConnected() && mStatus == CONNECTED;
    }

    public boolean isXmppConnected() {
        return mConnection != null && mConnection.isConnected();
    }

    public void registerConnectionChangeListener(XmppConnectionChangeListener listener) {
        mConnectionChangeListeners.add(listener);
    }

    public static int getNewConnectionCount() {
        return sNewConnectionCount;
    }

    public static int getReusedConnectionCount() {
        return sReusedConnectionCount;
    }

    public static String statusAsString(int state) {
        String res = "??";
        switch(state) {
            case DISCONNECTED:
                res = "Disconnected";
                break;
            case CONNECTING:
                res = "Connecting";
                break;
            case CONNECTED:
                res = "Connected";
                break;
            case DISCONNECTING:
                res = "Disconnecting";
                break;
            case WAITING_TO_CONNECT:
                res = "Waiting to connect";
                break;
            case WAITING_FOR_NETWORK:
                res = "Waiting for network";
                break;
        }
        return res;
    }

    public String statusString() {
        return statusAsString(mStatus);
    }
}