package com.thibaudperso.sonycamera.timelapse.control.connection;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkInfo;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Handler;
import android.support.v4.content.ContextCompat;
import android.util.Log;

import com.thibaudperso.sonycamera.BuildConfig;
import com.thibaudperso.sonycamera.sdk.CameraAPI;
import com.thibaudperso.sonycamera.sdk.model.Device;
import com.thibaudperso.sonycamera.timelapse.TimelapseApplication;
import com.thibaudperso.sonycamera.timelapse.control.DeviceManager;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static com.thibaudperso.sonycamera.timelapse.Constants.LOG_TAG;
import static com.thibaudperso.sonycamera.timelapse.control.connection.StateMachineConnection.State.BAD_API_ACCESS;
import static com.thibaudperso.sonycamera.timelapse.control.connection.StateMachineConnection.State.CHECK_API;
import static com.thibaudperso.sonycamera.timelapse.control.connection.StateMachineConnection.State.GOOD_API_ACCESS;
import static com.thibaudperso.sonycamera.timelapse.control.connection.StateMachineConnection.State.INIT;
import static com.thibaudperso.sonycamera.timelapse.control.connection.StateMachineConnection.State.TRY_TO_CONNECT_TO_SSID;
import static com.thibaudperso.sonycamera.timelapse.control.connection.StateMachineConnection.State.WIFI_DISABLED;
import static com.thibaudperso.sonycamera.timelapse.control.connection.StateMachineConnection.State.WIFI_SCAN;


public class StateMachineConnection {

    private TimelapseApplication mApplication;
    private WifiHandler mWifiHandler;
    private CameraAPI mCameraAPI;
    private DeviceManager mDeviceManager;

    private State mCurrentState;
    private StateRegistry mStateRegistry;
    private WifiManager.WifiLock mWifiLock;


    private class StateRegistry {
        WifiInfo wifiInfo;
        List<ScanResult> mScansResults;
        List<WifiConfiguration> mKnownWifiConfigurations;
        int forceConnectionToNetworkId = -1;
        int apiAttempts = 0;
    }


    public StateMachineConnection(TimelapseApplication application) {
        mApplication = application;
        mWifiHandler = application.getWifiHandler();
        mCameraAPI = application.getCameraAPI();
        mDeviceManager = application.getDeviceManager();
        mStateRegistry = new StateRegistry();
        mCurrentState = INIT;
    }


    public void start() {

        Log.d(LOG_TAG, " ----------- StateMachineConnection START -----------");
        mCurrentState.process(this);

        mWifiHandler.setListener(mWifiListener);
        mDeviceManager.addDeviceChangedListener(mDeviceChangedListener);

        WifiManager wifiManager = (WifiManager) mApplication.getSystemService(Context.WIFI_SERVICE);
        mWifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "WifiLock2");
        mWifiLock.acquire();
    }

    public void stop() {

        Log.d(LOG_TAG, " ----------- StateMachineConnection STOP -----------");
        mCurrentState.stopAsyncTasks();

        mWifiHandler.setListener(null);
        mDeviceManager.removeDeviceChangedListener(mDeviceChangedListener);

        if (mWifiLock != null && mWifiLock.isHeld())
            mWifiLock.release();

    }


    public void reset() {
        setCurrentState(INIT);
    }

    interface StateInterface {

        void process(StateMachineConnection sm);

        State[] previousPossibleStates();

        void stopAsyncTasks();
    }


    public enum State implements StateInterface {

        INIT {
            @Override
            public void process(StateMachineConnection sm) {
                boolean isWifiEnabled = sm.mWifiHandler.isEnabled();
                if (isWifiEnabled) {
                    sm.setCurrentState(State.WIFI_ENABLED);
                } else {
                    sm.setCurrentState(State.WIFI_DISABLED);
                }
            }

            @Override
            public State[] previousPossibleStates() {
                return State.values();
            }

            @Override
            public void stopAsyncTasks() {
            }
        },

        WIFI_ENABLED {
            @Override
            public void process(StateMachineConnection sm) {
                WifiInfo connectedWifi = sm.mWifiHandler.getConnectedWifi();
                if (connectedWifi != null) {
                    sm.mStateRegistry.wifiInfo = connectedWifi;
                    sm.setCurrentState(State.WIFI_CONNECTED);
                } else {
                    sm.setCurrentState(State.WIFI_DISCONNECTED);
                }
            }

            @Override
            public State[] previousPossibleStates() {
                return new State[]{WIFI_DISABLED, INIT};
            }

            @Override
            public void stopAsyncTasks() {
            }
        },

        WIFI_DISABLED {
            @Override
            public void process(StateMachineConnection sm) {
                //TODO: notify wifi disabled
            }

            @Override
            public State[] previousPossibleStates() {
                return State.values();
            }

            @Override
            public void stopAsyncTasks() {
            }
        },

        WIFI_CONNECTED {
            @Override
            public void process(StateMachineConnection sm) {

                if (sm.mStateRegistry.forceConnectionToNetworkId
                        == sm.mStateRegistry.wifiInfo.getNetworkId()) {
                    sm.mStateRegistry.forceConnectionToNetworkId = -1;
                }

                String ssid = WifiHandler.parseSSID(sm.mStateRegistry.wifiInfo.getSSID());
                if (WifiHandler.isSonyCameraSSID(ssid)) {
                    sm.setCurrentState(State.SONY_WIFI);
                } else {
                    sm.setCurrentState(State.NOT_SONY_WIFI);
                }
            }

            @Override
            public State[] previousPossibleStates() {
                return new State[]{WIFI_ENABLED, WIFI_DISCONNECTED, SONY_WIFI, NOT_SONY_WIFI,
                        WIFI_SCAN, GOOD_API_ACCESS, BAD_API_ACCESS, TRY_TO_CONNECT_TO_SSID,
                        CHECK_API};
            }

            @Override
            public void stopAsyncTasks() {
            }
        },

        WIFI_DISCONNECTED {
            @Override
            public void process(StateMachineConnection sm) {
                sm.mStateRegistry.wifiInfo = null;
                sm.setCurrentState(State.WIFI_SCAN);
            }

            @Override
            public State[] previousPossibleStates() {
                return new State[]{WIFI_ENABLED, WIFI_CONNECTED, SONY_WIFI, NOT_SONY_WIFI,
                        WIFI_SCAN, GOOD_API_ACCESS, BAD_API_ACCESS, WIFI_SCAN_FINISHED, CHECK_API};
            }

            @Override
            public void stopAsyncTasks() {
            }
        },

        SONY_WIFI {
            @Override
            public void process(final StateMachineConnection sm) {

                // Workaround when there is a data connection more than the wifi one
                // http://stackoverflow.com/questions/33237074/request-over-wifi-on-android-m
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
                    ConnectivityManager connectivityManager = (ConnectivityManager)
                            sm.mApplication.getSystemService(Context.CONNECTIVITY_SERVICE);
                    for (Network net : connectivityManager.getAllNetworks()) {
                        NetworkInfo netInfo = connectivityManager.getNetworkInfo(net);
                        if (netInfo != null
                                && netInfo.getType() == ConnectivityManager.TYPE_WIFI
                                && netInfo.getExtraInfo() != null
                                && netInfo.getExtraInfo()
                                .equals(sm.mStateRegistry.wifiInfo.getSSID())) {
                            connectivityManager.bindProcessToNetwork(net);
                            break;
                        }
                    }
                }

                sm.mStateRegistry.apiAttempts = 1;
                sm.setCurrentState(State.CHECK_API);
            }

            @Override
            public void stopAsyncTasks() {
            }

            @Override
            public State[] previousPossibleStates() {
                return new State[]{WIFI_CONNECTED};
            }
        },

        CHECK_API {

            boolean mIgnoreNextAsyncResponse = true;

            @Override
            public void process(final StateMachineConnection sm) {

                mIgnoreNextAsyncResponse = false;
                sm.mCameraAPI.testConnection(new CameraAPI.TestConnectionListener() {
                    @Override
                    public void isConnected(boolean isConnected) {

                        if (mIgnoreNextAsyncResponse) return;

                        if (isConnected) {
                            sm.setCurrentState(State.GOOD_API_ACCESS);
                        } else {
                            sm.setCurrentState(BAD_API_ACCESS);
                        }
                    }
                });
            }

            @Override
            public void stopAsyncTasks() {
                mIgnoreNextAsyncResponse = true;
            }

            @Override
            public State[] previousPossibleStates() {
                return new State[]{SONY_WIFI, CHECK_API, BAD_API_ACCESS, GOOD_API_ACCESS};
            }
        },

        NOT_SONY_WIFI {
            @Override
            public void process(StateMachineConnection sm) {
                sm.setCurrentState(State.WIFI_SCAN);
            }

            @Override
            public State[] previousPossibleStates() {
                return new State[]{WIFI_CONNECTED};
            }

            @Override
            public void stopAsyncTasks() {
            }
        },

        WIFI_SCAN {
            @Override
            public void process(StateMachineConnection sm) {
                if (ContextCompat.checkSelfPermission(sm.mApplication, Manifest.permission
                        .ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
                    sm.mWifiHandler.startScan();
                } else {
                    sm.setCurrentState(NO_WIFI_SCAN_PERMISSION);
                }
            }

            @Override
            public State[] previousPossibleStates() {
                return new State[]{WIFI_SCAN_FINISHED, WIFI_DISCONNECTED, NOT_SONY_WIFI};
            }

            @Override
            public void stopAsyncTasks() {
            }
        },

        NO_WIFI_SCAN_PERMISSION {
            @Override
            public void process(StateMachineConnection sm) {

            }

            @Override
            public State[] previousPossibleStates() {
                return new State[]{WIFI_SCAN};
            }

            @Override
            public void stopAsyncTasks() {
            }
        },

        WIFI_SCAN_FINISHED {
            @Override
            public void process(StateMachineConnection sm) {
                List<ScanResult> scansResults = sm.mStateRegistry.mScansResults;
                List<WifiConfiguration> knownWifiConfigurations =
                        sm.mStateRegistry.mKnownWifiConfigurations;

                /*
                 * No Sony Camera network found in scan
                 */
                if (scansResults.size() == 0) {
                    sm.setCurrentState(WIFI_SCAN);
                }

                /*
                 * No Sony Camera network registered on this phone but we found only one in scan
                 */
                else if (knownWifiConfigurations.size() == 0 && scansResults.size() == 1) {
                    sm.setCurrentState(ASK_PASSWORD_FOR_WIFI);
                }

                /*
                 * No Sony Camera network registered on this phone but we found more than one in scan
                 */
                else if (knownWifiConfigurations.size() == 0) {
                    sm.setCurrentState(MULTIPLE_SONY_SCAN_DETECTED);
                }

                /*
                 * There is only one Sony Camera known network connected
                 */
                else if (knownWifiConfigurations.size() == 1) {
                    sm.mStateRegistry.forceConnectionToNetworkId =
                            knownWifiConfigurations.get(0).networkId;
                    sm.mWifiHandler.connectToNetworkId(knownWifiConfigurations.get(0).networkId);
                    sm.setCurrentState(TRY_TO_CONNECT_TO_SSID);
                }

                /*
                 * There is more than one Sony Camera known network connected
                 */
                else {
                    sm.setCurrentState(MULTIPLE_SONY_CONF_DETECTED);
                }
            }

            @Override
            public State[] previousPossibleStates() {
                return new State[]{WIFI_SCAN};
            }

            @Override
            public void stopAsyncTasks() {
            }
        },


        MULTIPLE_SONY_CONF_DETECTED {
            @Override
            public void process(StateMachineConnection sm) {

            }

            @Override
            public State[] previousPossibleStates() {
                return new State[]{WIFI_SCAN_FINISHED};
            }

            @Override
            public void stopAsyncTasks() {
            }
        },


        MULTIPLE_SONY_SCAN_DETECTED {
            @Override
            public void process(StateMachineConnection sm) {

            }

            @Override
            public State[] previousPossibleStates() {
                return new State[]{WIFI_SCAN_FINISHED};
            }

            @Override
            public void stopAsyncTasks() {
            }
        },


        ASK_PASSWORD_FOR_WIFI {
            @Override
            public void process(StateMachineConnection sm) {

            }

            @Override
            public State[] previousPossibleStates() {
                return new State[]{WIFI_SCAN_FINISHED};
            }

            @Override
            public void stopAsyncTasks() {
            }
        },


        TRY_TO_CONNECT_TO_SSID {
            @Override
            public void process(StateMachineConnection sm) {
                sm.mWifiHandler.connectToNetworkId(sm.mStateRegistry.forceConnectionToNetworkId);
            }

            @Override
            public State[] previousPossibleStates() {
                return new State[]{MULTIPLE_SONY_CONF_DETECTED, WIFI_SCAN_FINISHED,
                        ASK_PASSWORD_FOR_WIFI, MULTIPLE_SONY_SCAN_DETECTED};
            }

            @Override
            public void stopAsyncTasks() {
            }
        },

        GOOD_API_ACCESS {
            @Override
            public void process(StateMachineConnection sm) {
            }

            @Override
            public State[] previousPossibleStates() {
                return new State[]{CHECK_API};
            }

            @Override
            public void stopAsyncTasks() {
            }
        },

        BAD_API_ACCESS {

            boolean mIgnoreNextAsyncResponse = true;

            @Override
            public void process(final StateMachineConnection sm) {

                mIgnoreNextAsyncResponse = false;
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        if (mIgnoreNextAsyncResponse) return;
                        sm.mStateRegistry.apiAttempts++;
                        sm.setCurrentState(CHECK_API);
                    }
                }, 1000);
            }

            @Override
            public State[] previousPossibleStates() {
                return new State[]{CHECK_API};
            }

            @Override
            public void stopAsyncTasks() {
                mIgnoreNextAsyncResponse = true;
            }
        }

    }


    private final WifiHandler.Listener mWifiListener = new WifiHandler.Listener() {
        @Override
        public void wifiEnabled() {
            setCurrentState(State.WIFI_ENABLED);
        }

        @Override
        public void wifiDisabled() {
            setCurrentState(State.WIFI_DISABLED);
        }

        @Override
        public void wifiConnected(NetworkInfo networkInfo) {
            mStateRegistry.wifiInfo = mWifiHandler.getConnectedWifi();
            setCurrentState(State.WIFI_CONNECTED);
        }

        @Override
        public void wifiDisconnected(NetworkInfo networkInfo) {
            if (mCurrentState == TRY_TO_CONNECT_TO_SSID || mCurrentState == WIFI_DISABLED) return;
            setCurrentState(State.WIFI_DISCONNECTED);
        }

        @Override
        public void onWifiScanFinished(List<ScanResult> sonyCameraScanResults,
                                       List<WifiConfiguration> configurations) {

            if (mCurrentState != WIFI_SCAN) return;

            mStateRegistry.mKnownWifiConfigurations = configurations;
            mStateRegistry.mScansResults = sonyCameraScanResults;
            setCurrentState(State.WIFI_SCAN_FINISHED);
        }
    };


    private final DeviceManager.DeviceChangedListener
            mDeviceChangedListener = new DeviceManager.DeviceChangedListener() {
        @Override
        public void onNewDevice(Device device) {
            if (mCurrentState == BAD_API_ACCESS || mCurrentState == GOOD_API_ACCESS ||
                    mCurrentState == CHECK_API) {
                setCurrentState(CHECK_API);
            }
        }
    };


    private void setCurrentState(State newState) {

        if (BuildConfig.DEBUG &&
                !Arrays.asList(newState.previousPossibleStates()).contains(mCurrentState)) {
            throw new RuntimeException("Current State: " + mCurrentState + ", " +
                    "new State: " + newState);
        }

        Log.d(LOG_TAG, "State: " + mCurrentState + " ---> " + newState);
        for (Listener listener : mListeners) listener.onNewState(mCurrentState, newState);

        mCurrentState.stopAsyncTasks();
        mCurrentState = newState;
        mCurrentState.process(this);

    }


    public State getCurrentState() {
        return mCurrentState;
    }


    public void notifyWifiScanPermissionAccepted() {
        setCurrentState(INIT);
    }

    public void tryToConnectToNetworkId(int networkId) {
        mStateRegistry.forceConnectionToNetworkId = networkId;
        mWifiHandler.connectToNetworkId(networkId);
        setCurrentState(TRY_TO_CONNECT_TO_SSID);
    }

    public void createNetwork(String networkSSID, String networkPassword) {
        mWifiHandler.createIfNeededThenConnectToWifi(networkSSID, networkPassword);
        setCurrentState(TRY_TO_CONNECT_TO_SSID);
    }

    public List<WifiConfiguration> getWifiConfigurations() {
        return mStateRegistry.mKnownWifiConfigurations;
    }

    public List<ScanResult> getScanResults() {
        return mStateRegistry.mScansResults;
    }

    private List<Listener> mListeners = new ArrayList<>();

    public void addListener(Listener listener) {
        mListeners.add(listener);
    }

    public void removeListener(Listener listener) {
        mListeners.remove(listener);
    }

    public interface Listener {
        void onNewState(State previousState, State newState);
    }

}