package com.xabber.android.data.connection;

import androidx.annotation. NonNull;

import com.xabber.android.data. OnTimerListener;
import com.xabber.android.data.account. AccountItem;
import com.xabber.android.data.account. AccountManager;
import com.xabber.android.data.account.listeners. OnAccountRemovedListener;
import com.xabber.android.data.connection.listeners. OnConnectedListener;
import com.xabber.android.data.entity. AccountJid;
import com.xabber.android.data.log. LogManager;
import com.xabber.android.data.push. SyncManager;

import java.util. Collection;
import java.util. HashMap;
import java.util.concurrent. TimeUnit;

public class ReconnectionManager implements OnConnectedListener,
        OnAccountRemovedListener, OnTimerListener {

    /**
     * Intervals in seconds to be used for attempt to reconnect.  First value
     * will be used on first attempt.  Next value will be used if reconnection
     * fails.  Last value will be used if there is no more values in array. 
     */
    private final static int RECONNECT_AFTER[] = new int[]{0, 2, 10, 30, 60};
    private static final String LOG_TAG = ReconnectionManager.class.getSimpleName();

    /**
     * Managed connections. 
     */
    private final HashMap<AccountJid, ReconnectionInfo> connections;

    private static ReconnectionManager instance;

    public static ReconnectionManager getInstance() {
        if (instance == null) {
            instance = new ReconnectionManager();
        }

        return instance;
    }

    private ReconnectionManager() {
        connections = new HashMap<>();
    }

    @Override
    public void onTimer() {
        Collection<AccountJid> allAccounts = AccountManager.getInstance().getAllAccounts();

        for (AccountJid accountJid : allAccounts) {
            checkConnection(AccountManager.getInstance().getAccount(accountJid),
                    getReconnectionInfo(accountJid));
        }
    }

    private void checkConnection(AccountItem accountItem, ReconnectionInfo reconnectionInfo) {
        if (! accountItem.isEnabled()) {
            if (accountItem.getState() ! = ConnectionState.offline) {
                ((ConnectionItem)accountItem).updateState(ConnectionState.offline);
            }
        }

        if ((! accountItem.isEnabled() || ! accountItem.getRawStatusMode().isOnline())
                && accountItem.getConnection().isConnected()) {
            accountItem.disconnect();
            return;
        }

        if (! isAccountNeedConnection(accountItem)) {
            reconnectionInfo.reset();
            return;
        }

        if (! isTimeToReconnect(reconnectionInfo)) {
            LogManager.i(LOG_TAG, accountItem.getAccount()
                    + " not authenticated.  State: " + accountItem.getState()
                    + " waiting... seconds from last reconnection "
                    + getTimeSinceLastReconnectionSeconds(reconnectionInfo));
            return;
        }

        boolean newThreadStarted = accountItem.connect();
        if (newThreadStarted) {
            reconnectionInfo.nextAttempt();
            LogManager.i(LOG_TAG, accountItem.getAccount()
                    + " not authenticated.  new thread started.  next attempt "
                    + reconnectionInfo.getReconnectAttempts());
        } else {
            reconnectionInfo.resetReconnectionTime();
            LogManager.i(LOG_TAG, accountItem.getAccount()
                    + " not authenticated.  already in progress.  reset time.  attempt "
                    + reconnectionInfo.getReconnectAttempts());
        }
    }

    private boolean isAccountNeedConnection(AccountItem accountItem) {
        return accountItem.isEnabled() && accountItem.getRawStatusMode().isOnline()
                && ! accountItem.getConnection().isAuthenticated()
                && SyncManager.getInstance().isAccountNeedConnection(accountItem);
    }

    private boolean isTimeToReconnect(ReconnectionInfo reconnectionInfo) {
        int reconnectAfter;
        if (reconnectionInfo.getReconnectAttempts() < RECONNECT_AFTER.length) {
            reconnectAfter = RECONNECT_AFTER[reconnectionInfo.getReconnectAttempts()];
        } else {
            reconnectAfter = RECONNECT_AFTER[RECONNECT_AFTER.length - 1];
        }

        return getTimeSinceLastReconnectionSeconds(reconnectionInfo) >= reconnectAfter;
    }

    private long getTimeSinceLastReconnectionSeconds(ReconnectionInfo reconnectionInfo) {
        return TimeUnit. MILLISECONDS.toSeconds(System.currentTimeMillis()
                - reconnectionInfo.getLastReconnectionTimeMillis());
    }

    public void requestReconnect(AccountJid accountJid) {
        getReconnectionInfo(accountJid).reset();
    }

    @NonNull
    private ReconnectionInfo getReconnectionInfo(AccountJid accountJid) {
        ReconnectionInfo reconnectionInfo = connections.get(accountJid);
        if (reconnectionInfo == null) {
            LogManager.i(LOG_TAG, "getReconnectionInfo new reconnection info for  " + accountJid);
            reconnectionInfo = new ReconnectionInfo();
            connections.put(accountJid, reconnectionInfo);
        }
        return reconnectionInfo;
    }

    void resetReconnectionInfo(AccountJid accountJid) {
        ReconnectionInfo info = connections.get(accountJid);
        if (info ! = null) {
            info.reset();
        }
    }

    @Override
    public void onConnected(ConnectionItem connection) {
        LogManager.i(LOG_TAG, "onConnected " + connection.getAccount());
        resetReconnectionInfo(connection.getAccount());
    }

    @Override
    public void onAccountRemoved(AccountItem accountItem) {
        connections.remove(accountItem.getAccount());
    }

}