package tv.twitch.sdk;

import android.content.Context;
import android.support.annotation.NonNull;

import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;

import tv.twitch.CoreAPI;
import tv.twitch.CoreErrorCode;
import tv.twitch.CorePubSubState;
import tv.twitch.ErrorCode;
import tv.twitch.IChannelListener;
import tv.twitch.IChannelStatus;
import tv.twitch.ICoreAPIListener;
import tv.twitch.IModule;
import tv.twitch.IWebSocket;
import tv.twitch.IWebSocketFactory;
import tv.twitch.ModuleState;
import tv.twitch.ResultContainer;
import tv.twitch.UserInfo;
//import tv.twitch.android.api.KrakenApi;
//import tv.twitch.android.app.core.TwitchApplication;
//import tv.twitch.android.app.twitchbroadcast.BroadcastController;
//import tv.twitch.android.singletons.analytics.SdkEventTracker;
import tv.twitch.twiglib.Logger;

import com.unity3d.player.*;

/**
 * Entry point and main controller for anything related to the C++ SDK.
 * Responsible for maintaining the service handler that the SDK interacts with,
 * managing the CoreAPI lifecycle, and holding onto SDK modules (chat and social).
 */

/**
 * Adapted by loohill on 05/04/18
 * Last sync: 05/04/18
 * Source: twitch-apps/twitch-android/blob/master/Twitch/src/main/java/tv/twitch/android/sdk/SDKWebSocketHandler.java
 * Changes:
 *** TODO Commented 3 Chat, Social and Broadcast controller. May uncomment them later if we decided to use these components.
 *** TODO Store and read client id from somewhere
 *** Removed all codes for logger and added our own logger;
 *** Commented Crashlytics;
 */

public class SDKServicesController {

    private final static String TAG = "Twitch_ServicesController";
    private static String ClientId;

    public static void setClientId(String clientId){
        ClientId = clientId;
    }

    public static SDKServicesController getInstance() {
        SingletonHolder.Instance.setupAndStartSDKIfNecessary(); // has to be here, because setup needs to happen after a shutdown
        return SingletonHolder.Instance;
    }

    //NOTE: if using multi-dex, be wary of NoClassDefError: https://code.google.com/p/android/issues/detail?id=162774
    private static final class SingletonHolder {
        private static final SDKServicesController Instance = new SDKServicesController(UnityPlayer.currentActivity.getApplication());
    }

    public interface InitializationListener {
        void onSdkLoggedIn();
    }

    public enum ServicesState {
        Uninitialized,
        Initializing,
        InitializedCore,
        InitializingModules,
        Initialized,
        ShuttingDown,
    }

    private static class UpdateHandler extends android.os.Handler {

        private WeakReference<SDKServicesController> mController;

        UpdateHandler(@NonNull SDKServicesController controller) {
            super();
            mController = new WeakReference<>(controller);
        }

        void start() {
            doUpdate();
        }

        void doUpdate() {
            if (mController != null && mController.get() != null) {
                mController.get().update();
                postDelayed(mRunnable, UPDATE_INTERVAL);
            }
        }

        private Runnable mRunnable = new Runnable() {
            @Override
            public void run() {
                doUpdate();
            }
        };
    }

    public static final int UPDATE_INTERVAL = 250;
    public static final int SHUTDOWN_UPDATE_INTERVAL = 50;

    private UpdateHandler mUpdateHandler;
    private ChatController mChatController;
//    private SocialController mSocialController;
//    private BroadcastController mBroadcastController;
    private ConcurrentHashMap<IChannelListener, IChannelStatus> mChannelStatusMap = new ConcurrentHashMap<>();
    private Context mContext;
    private CoreAPI mCore = null;
    private Set<InitializationListener> mInitializationListeners = Collections.newSetFromMap(new ConcurrentHashMap<InitializationListener, Boolean>());
    private AtomicBoolean mLoginCommandIsPending = new AtomicBoolean(false);
    private int mUserId = 0;
    private String mAuthToken;
    private String[] mSDKScopes;
    private CorePubSubState mPubSubState = CorePubSubState.TTV_CORE_PUBSUB_STATE_DISCONNECTED;
    protected ServicesState mServicesState = ServicesState.Uninitialized;

    public SDKServicesController(Context context) {
        mContext = context;
    }

    private void setupAndStartSDKIfNecessary() {
        if (mUpdateHandler != null) {
            return;
        }

        if(ClientId == null || ClientId.isEmpty()){
            reportError("Client Id is empty. Call setClientId() first.");
            return;
        }

        mUpdateHandler = new UpdateHandler(this);

        SDKLibrary.getInstance().initialize("twitchsdk");
        SDKLibrary.getInstance().setHttpRequestProvider(new SDKHttpRequestProvider(mContext));
        SDKLibrary.getInstance().registerWebSocketFactory(mSocketFactory);
        SDKLibrary.getInstance().setClientId(ClientId);
//        SDKLibrary.getInstance().setEventTracker(SdkEventTracker.getInstance());
        Logger.d(TAG, "Library initialized");

        if (mCore == null) {
            mCore = new CoreAPI();
        }

        // create module instances
        if (mChatController == null) {
            mChatController = new ChatController();
        }
//        if (mSocialController == null) {
//            mSocialController = new SocialController();
//        }
//        if (mBroadcastController == null) {
//            mBroadcastController = new BroadcastController();
//        }
        mChatController.reset();
//        mSocialController.reset();
//        mBroadcastController.reset();

        initialize();

        // Start the update handler that will periodically update the controllers
        mUpdateHandler.start();
    }

    protected void reportError(String err) {
        Logger.e(TAG, err);
    }

    public ChatController getChat() {
        return mChatController;
    }
//
//    public SocialController getSocial() {
//        return mSocialController;
//    }
//
//    public BroadcastController getBroadcast() {
//        return mBroadcastController;
//    }

    public CorePubSubState getPubSubState() {
        return mPubSubState;
    }

    public void addInitializationListener(@NonNull InitializationListener listener) {
        mInitializationListeners.add(listener);
    }

    public void removeInitializationListener(@NonNull InitializationListener listener) {
        mInitializationListeners.remove(listener);
    }

    public String[] getSDKScopes() {
        return mSDKScopes;
    }

    public boolean isSdkLoggedIn() {
        return mUserId > 0;
    }

    public boolean logIn(final String oauthToken, final CoreAPI.LogInCallback logInCallback) {
        if (oauthToken == null || oauthToken.equals("")) {
            return false;
        }

        Logger.d(TAG, "Logging in");
        if (mLoginCommandIsPending.get()) {
            Logger.e(TAG, "Attempting to login to SDK with outstanding login");
            return false;
        }

        // switching auth token, log out
        if (mUserId > 0 && mAuthToken != null && !mAuthToken.equals(oauthToken)) {
            ErrorCode ec2 = mCore.logOut(mUserId, new CoreAPI.LogOutCallback() {
                @Override
                public void invoke(ErrorCode ec) {
                }
            });
            if (ec2.failed()) {
                reportError(String.format("logout failed after login: %s", SDKLibrary.getInstance().errorToString(ec2)));
            }
        }

        mLoginCommandIsPending.set(true);
        ErrorCode ec = mCore.logIn(oauthToken, new CoreAPI.LogInCallback() {
            @Override
            public void invoke(ErrorCode ec, UserInfo userInfo) {
                if (ec.succeeded() && userInfo != null) {
                    mAuthToken = oauthToken;
                    mUserId = userInfo.userId;
                    mChatController.setUserId(mUserId);
//                    mSocialController.setUserId(mUserId);
//                    mBroadcastController.setUserId(mUserId);
                }

                mLoginCommandIsPending.set(false);

                // forward response to provided listener
                if (logInCallback != null) {
                    logInCallback.invoke(ec, userInfo);
                }

                for (InitializationListener listener : mInitializationListeners) {
                    listener.onSdkLoggedIn();
                }
            }
        });

        if (ec.failed()) {
            mLoginCommandIsPending.set(false);
            return false;
        } else {
            return true;
        }
    }

    public boolean logOut() {
        if (mUserId > 0 && mAuthToken != null) {
            // NOTE: logout callback is actually synchronous
            ErrorCode ec = mCore.logOut(mUserId, new CoreAPI.LogOutCallback() {
                @Override
                public void invoke(ErrorCode ec) {
                    mUserId = 0;
                    mAuthToken = null;
                    mChatController.setUserId(0);
//                    mSocialController.setUserId(0);
//                    mBroadcastController.setUserId(0);
                }
            });
            if (ec.failed()) {
                reportError(String.format("logout failed: %s", SDKLibrary.getInstance().errorToString(ec)));
                return false;
            } else {
                return true;
            }
        } else {
            return false;
        }
    }

    public ErrorCode fetchUserInfo(String userName, CoreAPI.FetchUserInfoCallback callback) {
        return mCore.fetchUserInfo(userName, callback);
    }

    public void forceSyncShutdown() {
        if (mServicesState == ServicesState.Uninitialized) {
            return;
        }

        mUpdateHandler.removeCallbacksAndMessages(null);

        forceSyncShutdownInternal();

        SDKLibrary.getInstance().unregisterWebSocketFactory(mSocketFactory);
        SDKLibrary.getInstance().shutdown();
        mUpdateHandler = null;
    }

    public void setPubSubConnected(final boolean connected) {
        if (mUserId > 0) {
            ErrorCode ec = null;
            CorePubSubState state = getPubSubState();
            if (connected) {
                if (state == CorePubSubState.TTV_CORE_PUBSUB_STATE_DISCONNECTED || state == CorePubSubState.TTV_CORE_PUBSUB_STATE_DISCONNECTING) {
                    ec = mCore.connectPubSub(mUserId);
                }
            } else {
                if (state == CorePubSubState.TTV_CORE_PUBSUB_STATE_CONNECTED || state == CorePubSubState.TTV_CORE_PUBSUB_STATE_CONNECTING) {
                    ec = mCore.disconnectPubSub(mUserId);
                }
            }

            if (ec != null && ec.failed()) {
                reportError("Error updating pub sub connected state to " + connected + ": " + ec);
            }
        }
    }

    public void setLocalLanguage(final String language) {
        if (mServicesState != ServicesState.Initialized) {
            reportError("Setting local language with services not initialized");
            return;
        }

        mCore.setLocalLanguage(language);
    }

    private void update() {
        //Logger.d(TAG, "Updating...");
        if (mServicesState == ServicesState.Uninitialized) {
            return;
        } else if (mServicesState == ServicesState.InitializedCore) {

            if (!mChatController.initialize(mCore)) {
                reportError("Initializing sdk chat controller failed");
            }
//
//            if (!mSocialController.initialize(mCore)) {
//                reportError("Initializing sdk social controller failed");
//            }
//
//            if (!mBroadcastController.initialize(mCore)) {
//                reportError("Initializing sdk social controller failed");
//            }

            mServicesState = ServicesState.InitializingModules;
            Logger.d(TAG, "mServicesState: InitializingModules");
        } else if (mServicesState == ServicesState.InitializingModules) {
            if (mChatController.getControllerState() == ChatController.ChatState.Initialized
//                  && mSocialController.getState() == SocialController.SocialState.Initialized
//                  && mBroadcastController.getState() == ModuleState.Initialized
                ) {
                mServicesState = ServicesState.Initialized;
                Logger.d(TAG, "mServicesState: Initialized");
//
//                // everything is initialized, grab required scopes for all sdk modules
                ResultContainer<String[]> modules = new ResultContainer<>();
                ResultContainer<String[]> scopes = new ResultContainer<>();
                ErrorCode ec = mCore.getRequiredOAuthScopes(modules, scopes);
                if (ec.succeeded()) {
                    mSDKScopes = scopes.result;
                }
            }
        }

        mCore.update();
        mChatController.update();
//        mSocialController.update();
//        mBroadcastController.update();
    }

    private boolean initialize() {
        if (mServicesState != ServicesState.Uninitialized) {
            return false;
        }

        mServicesState = ServicesState.Initializing;

        ErrorCode ec;
        if (!SDKLibrary.getInstance().isInitialized()) {
            ec = CoreErrorCode.TTV_EC_NOT_INITIALIZED;
            mServicesState = ServicesState.Uninitialized;

            String err = SDKLibrary.getInstance().errorToString(ec);
            reportError(String.format("Error initializing Twitch sdk: %s", err));

            return false;
        }

        ec = mCore.initialize(new IModule.InitializeCallback() {
            @Override
            public void invoke(ErrorCode ec) {
                Logger.d(TAG, "mCore initialized");
            }
        });
        if (ec.failed()) {
            mServicesState = ServicesState.Uninitialized;
            String err = SDKLibrary.getInstance().errorToString(ec);
            reportError(String.format("Error initializing core sdk: %s", err));
            return false;
        } else {
            mCore.setListener(mCoreListener);
            Logger.d(TAG,"ServicesController initialized, mServicesState: Initializing");
            return true;
        }
    }

    private IWebSocketFactory mSocketFactory = new IWebSocketFactory() {
        @Override
        public boolean isProtocolSupported(String protocol) {
            return protocol.equals("ws") || protocol.equals("wss");
        }

        @Override
        public ErrorCode createWebSocket(String uri, ResultContainer<IWebSocket> result) {
            result.result = new SDKWebSocketHandler(uri);
            return CoreErrorCode.TTV_EC_SUCCESS;
        }
    };

    private void forceSyncShutdownInternal() {
        if (mServicesState == ServicesState.Uninitialized) {
            return;
        }

        // have to wait for chat and social finish initialization before shutting down
        while (mChatController.getControllerState() == ChatController.ChatState.Initializing
//              || mSocialController.getState() == SocialController.SocialState.Initializing
//              || mBroadcastController.getState() == ModuleState.Initializing
              ) {
            mCore.update();

            if (mChatController.getControllerState() == ChatController.ChatState.Initializing) {
                mChatController.update();
            }
//
//            if (mSocialController.getState() == SocialController.SocialState.Initializing) {
//                mSocialController.update();
//            }
//
//            if (mBroadcastController.getState() == ModuleState.Initializing) {
//                mBroadcastController.update();
//            }
//
            try {
                Thread.sleep(SHUTDOWN_UPDATE_INTERVAL);
            } catch (InterruptedException e) {
            }
        }

        // everything is verified initialized, shutdown modules
        mChatController.forceSyncShutdown(mCore);
//        mSocialController.forceSyncShutdown(mCore);
//        mBroadcastController.forceSyncShutdown(mCore);

        // modules shut down, shut down core
        if (mServicesState != ServicesState.Uninitialized) {

            ErrorCode ret = mCore.shutdown(new IModule.ShutdownCallback() {
                @Override
                public void invoke(ErrorCode ec) {
                    Logger.d(TAG,"mCore shut down");
                }
            });
            if (ret.failed()) {
                reportError(String.format("Error shutting down core: %s", SDKLibrary.getInstance().errorToString(ret)));
            } else {
                mServicesState = ServicesState.ShuttingDown;
            }

            if (mServicesState == ServicesState.ShuttingDown) {
                // wait for the shutdown to finish
                while (mServicesState != ServicesState.Uninitialized) {
                    try {
                        Thread.sleep(SHUTDOWN_UPDATE_INTERVAL);
                        update();
                    } catch (InterruptedException ignored) {
                    }
                }
            }
        }

    }

    private ICoreAPIListener mCoreListener = new ICoreAPIListener() {

        @Override
        public void coreUserLoginComplete(String oauthToken, int userId, ErrorCode ec) {

        }

        @Override
        public void coreUserLogoutComplete(int userId, ErrorCode ec) {

        }

        @Override
        public void coreUserAuthenticationIssue(int userId, String oauthToken, ErrorCode ec) {
            if (mUserId == userId) {
                Logger.e(TAG, "CORE AUTHENTICATION ISSUE: " + userId + ": " + oauthToken + ": " + ec.getName());
//                Crashlytics.setString("error_code", ec.toString());
//                Crashlytics.logException(new CoreAuthenticationException());
            }
        }

        @Override
        public void corePubSubStateChanged(int userId, CorePubSubState state, ErrorCode result) {
            if (mUserId == userId) {
                mPubSubState = state;
            }
        }

        @Override
        public void moduleStateChanged(IModule source, ModuleState state, ErrorCode result) {
            if (result.failed()) {
                reportError(String.format("Error in module state changed core sdk: %s", SDKLibrary.getInstance().errorToString(result)));
            }

            if (state == ModuleState.Initialized) {
                mServicesState = ServicesState.InitializedCore;
            } else if (state == ModuleState.Uninitialized) {
                mServicesState = ServicesState.Uninitialized;
            }
        }
    };

    private class CoreAuthenticationException extends Exception {
    }

    public void connectChannelListener(int userId, int channelId, @NonNull IChannelListener listener) {
        if (mChannelStatusMap.containsKey(listener)) {
            reportError("already registered that ChannelListener to channelId " + channelId + ", not adding");
        } else {
            ResultContainer<IChannelStatus> channelStatusResultContainer = new ResultContainer<>();
            ErrorCode ec = mCore.createChannelStatus(userId, channelId, listener, channelStatusResultContainer);
            if (ec != null && ec.failed()) {
                reportError("Error adding channel listener to: " + ec.getName());
                return;
            }
            mChannelStatusMap.put(listener, channelStatusResultContainer.result);
        }
    }

    public void disconnectChannelListener(@NonNull IChannelListener listener) {
        IChannelStatus channelStatus = mChannelStatusMap.get(listener);
        if (channelStatus == null) {
            reportError("couldn't find channel listener to remove");
            return;
        }
        ErrorCode ec = mCore.disposeChannelStatus(channelStatus);
        mChannelStatusMap.remove(listener);
        if (ec != null && ec.failed()) {
            reportError("Error disposing channel status: " + ec.getName());
        }
    }
}


