package org.fdroid.fdroid.views.swap;

import android.annotation.TargetApi;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.wifi.WifiConfiguration;
import android.support.annotation.ColorRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.widget.SwitchCompat;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;

import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.localrepo.SwapService;
import org.fdroid.fdroid.localrepo.peers.Peer;
import org.fdroid.fdroid.net.WifiStateChangeService;

import java.util.ArrayList;

import cc.mvdan.accesspoint.WifiApControl;
import rx.Subscriber;
import rx.Subscription;

public class StartSwapView extends RelativeLayout implements SwapWorkflowActivity.InnerView {

    private static final String TAG = "StartSwapView";

    // TODO: Is there a way to guarantee which of these constructors the inflater will call?
    // Especially on different API levels? It would be nice to only have the one which accepts
    // a Context, but I'm not sure if that is correct or not. As it stands, this class provides
    // constructors which match each of the ones available in the parent class.
    // The same is true for the other views in the swap process too.

    public StartSwapView(Context context) {
        super(context);
    }

    public StartSwapView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @TargetApi(11)
    public StartSwapView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(21)
    public StartSwapView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    private class PeopleNearbyAdapter extends ArrayAdapter<Peer> {

        PeopleNearbyAdapter(Context context) {
            super(context, 0, new ArrayList<Peer>());
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            if (convertView == null) {
                convertView = LayoutInflater.from(getContext()).inflate(R.layout.swap_peer_list_item, parent, false);
            }

            Peer peer = getItem(position);
            ((TextView) convertView.findViewById(R.id.peer_name)).setText(peer.getName());
            ((ImageView) convertView.findViewById(R.id.icon)).setImageDrawable(getResources().getDrawable(peer.getIcon()));

            return convertView;
        }

    }

    private SwapWorkflowActivity getActivity() {
        return (SwapWorkflowActivity) getContext();
    }

    private SwapService getManager() {
        return getActivity().getState();
    }

    @Nullable /* Emulators typically don't have bluetooth adapters */
    private final BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();

    private SwitchCompat wifiSwitch;
    private SwitchCompat bluetoothSwitch;
    private TextView textWifiVisible;
    private TextView viewBluetoothId;
    private TextView textBluetoothVisible;
    private TextView viewWifiId;
    private TextView viewWifiNetwork;
    private TextView peopleNearbyText;
    private ListView peopleNearbyList;
    private ProgressBar peopleNearbyProgress;

    private PeopleNearbyAdapter peopleNearbyAdapter;

    /**
     * When peers are emitted by the peer finder, add them to the adapter
     * so that they will show up in the list of peers.
     */
    private final Subscriber<Peer> onPeerFound = new Subscriber<Peer>() {

        @Override
        public void onCompleted() {
            uiShowNotSearchingForPeers();
        }

        @Override
        public void onError(Throwable e) {
            uiShowNotSearchingForPeers();
        }

        @Override
        public void onNext(Peer peer) {
            Utils.debugLog(TAG, "Found peer: " + peer + ", adding to list of peers in UI.");
            peopleNearbyAdapter.add(peer);
        }
    };

    private Subscription peerFinderSubscription;

    /**
     * Remove relevant listeners/subscriptions/etc so that they do not receive and process events
     * when this view is not in use.
     *
     * TODO: Not sure if this is the best place to handle being removed from the view.
     */
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (peerFinderSubscription != null) {
            peerFinderSubscription.unsubscribe();
            peerFinderSubscription = null;
        }

        if (wifiSwitch != null) {
            wifiSwitch.setOnCheckedChangeListener(null);
        }

        if (bluetoothSwitch != null) {
            bluetoothSwitch.setOnCheckedChangeListener(null);
        }

        LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(onWifiSwapStateChanged);
        LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(onBluetoothSwapStateChanged);
        LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(onWifiNetworkChanged);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        if (peerFinderSubscription == null) {
            peerFinderSubscription = getManager().scanForPeers().subscribe(onPeerFound);
        }

        uiInitPeers();
        uiInitBluetooth();
        uiInitWifi();
        uiInitButtons();
        uiShowSearchingForPeers();

        LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
                onWifiNetworkChanged, new IntentFilter(WifiStateChangeService.BROADCAST));
    }

    private final BroadcastReceiver onWifiNetworkChanged = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            uiUpdateWifiNetwork();
        }
    };

    private void uiInitButtons() {
        findViewById(R.id.btn_send_fdroid).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                getActivity().sendFDroid();
            }
        });

        findViewById(R.id.btn_qr_scanner).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                getActivity().startQrWorkflow();
            }
        });
    }

    /**
     * Setup the list of nearby peers with an adapter, and hide or show it and the associated
     * message for when no peers are nearby depending on what is happening.
     */
    private void uiInitPeers() {

        peopleNearbyText = (TextView) findViewById(R.id.text_people_nearby);
        peopleNearbyList = (ListView) findViewById(R.id.list_people_nearby);
        peopleNearbyProgress = (ProgressBar) findViewById(R.id.searching_people_nearby);

        peopleNearbyAdapter = new PeopleNearbyAdapter(getContext());
        peopleNearbyList.setAdapter(peopleNearbyAdapter);

        peopleNearbyList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Peer peer = peopleNearbyAdapter.getItem(position);
                onPeerSelected(peer);
            }
        });
    }

    private void uiShowSearchingForPeers() {
        peopleNearbyText.setText(getContext().getString(R.string.swap_scanning_for_peers));
        peopleNearbyProgress.setVisibility(View.VISIBLE);
    }

    private void uiShowNotSearchingForPeers() {
        peopleNearbyProgress.setVisibility(View.GONE);
        if (peopleNearbyList.getAdapter().getCount() > 0) {
            peopleNearbyText.setText(getContext().getString(R.string.swap_people_nearby));
        } else {
            peopleNearbyText.setText(getContext().getString(R.string.swap_no_peers_nearby));
        }
    }

    private void uiInitBluetooth() {
        if (bluetooth != null) {

            textBluetoothVisible = (TextView) findViewById(R.id.bluetooth_visible);

            viewBluetoothId = (TextView) findViewById(R.id.device_id_bluetooth);
            viewBluetoothId.setText(bluetooth.getName());
            viewBluetoothId.setVisibility(bluetooth.isEnabled() ? View.VISIBLE : View.GONE);

            int textResource = getManager().isBluetoothDiscoverable() ? R.string.swap_visible_bluetooth : R.string.swap_not_visible_bluetooth;
            textBluetoothVisible.setText(textResource);

            bluetoothSwitch = (SwitchCompat) findViewById(R.id.switch_bluetooth);
            Utils.debugLog(TAG, getManager().isBluetoothDiscoverable() ? "Initially marking switch as checked, because Bluetooth is discoverable." : "Initially marking switch as not-checked, because Bluetooth is not discoverable.");
            bluetoothSwitch.setOnCheckedChangeListener(onBluetoothSwitchToggled);
            setBluetoothSwitchState(getManager().isBluetoothDiscoverable(), true);

            LocalBroadcastManager.getInstance(getContext()).registerReceiver(onBluetoothSwapStateChanged, new IntentFilter(SwapService.BLUETOOTH_STATE_CHANGE));

        } else {
            findViewById(R.id.bluetooth_info).setVisibility(View.GONE);
        }
    }

    /**
     * @see StartSwapView#onWifiSwapStateChanged
     */
    private final BroadcastReceiver onBluetoothSwapStateChanged = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.hasExtra(SwapService.EXTRA_STARTING)) {
                Utils.debugLog(TAG, "Bluetooth service is starting (setting toggle to disabled, not checking because we will wait for an intent that bluetooth is actually enabled)");
                bluetoothSwitch.setEnabled(false);
                textBluetoothVisible.setText(R.string.swap_setting_up_bluetooth);
                // bluetoothSwitch.setChecked(true);
            } else {
                if (intent.hasExtra(SwapService.EXTRA_STARTED)) {
                    Utils.debugLog(TAG, "Bluetooth service has started (updating text to visible, but not marking as checked).");
                    textBluetoothVisible.setText(R.string.swap_visible_bluetooth);
                    bluetoothSwitch.setEnabled(true);
                    // bluetoothSwitch.setChecked(true);
                } else {
                    Utils.debugLog(TAG, "Bluetooth service has stopped (setting switch to not-visible).");
                    textBluetoothVisible.setText(R.string.swap_not_visible_bluetooth);
                    setBluetoothSwitchState(false, true);
                }
            }
        }
    };

    /**
     * @see StartSwapView#setWifiSwitchState(boolean, boolean)
     */
    private void setBluetoothSwitchState(boolean isChecked, boolean isEnabled) {
        bluetoothSwitch.setOnCheckedChangeListener(null);
        bluetoothSwitch.setChecked(isChecked);
        bluetoothSwitch.setEnabled(isEnabled);
        bluetoothSwitch.setOnCheckedChangeListener(onBluetoothSwitchToggled);
    }

    /**
     * @see StartSwapView#onWifiSwitchToggled
     */
    private final CompoundButton.OnCheckedChangeListener onBluetoothSwitchToggled = new CompoundButton.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            if (isChecked) {
                Utils.debugLog(TAG, "Received onCheckChanged(true) for Bluetooth swap, prompting user as to whether they want to enable Bluetooth.");
                getActivity().startBluetoothSwap();
                textBluetoothVisible.setText(R.string.swap_visible_bluetooth);
                viewBluetoothId.setVisibility(View.VISIBLE);
                Utils.debugLog(TAG, "Received onCheckChanged(true) for Bluetooth swap (prompting user or setup Bluetooth complete)");
                // TODO: When they deny the request for enabling bluetooth, we need to disable this switch...
            } else {
                Utils.debugLog(TAG, "Received onCheckChanged(false) for Bluetooth swap, disabling Bluetooth swap.");
                getManager().getBluetoothSwap().stop();
                textBluetoothVisible.setText(R.string.swap_not_visible_bluetooth);
                viewBluetoothId.setVisibility(View.GONE);
                Utils.debugLog(TAG, "Received onCheckChanged(false) for Bluetooth swap, Bluetooth swap disabled successfully.");
            }
        }
    };

    private void uiInitWifi() {

        viewWifiId = (TextView) findViewById(R.id.device_id_wifi);
        viewWifiNetwork = (TextView) findViewById(R.id.wifi_network);

        wifiSwitch = (SwitchCompat) findViewById(R.id.switch_wifi);
        wifiSwitch.setOnCheckedChangeListener(onWifiSwitchToggled);
        setWifiSwitchState(getManager().isBonjourDiscoverable(), true);

        textWifiVisible = (TextView) findViewById(R.id.wifi_visible);
        int textResource = getManager().isBonjourDiscoverable() ? R.string.swap_visible_wifi : R.string.swap_not_visible_wifi;
        textWifiVisible.setText(textResource);

        // Note that this is only listening for the WifiSwap, whereas we start both the WifiSwap
        // and the Bonjour service at the same time. Technically swap will work fine without
        // Bonjour, and that is more of a convenience. Thus, we should show feedback once wifi
        // is ready, even if Bonjour is not yet.
        LocalBroadcastManager.getInstance(getContext()).registerReceiver(onWifiSwapStateChanged, new IntentFilter(SwapService.WIFI_STATE_CHANGE));

        viewWifiNetwork.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                getActivity().promptToSelectWifiNetwork();
            }
        });

        uiUpdateWifiNetwork();
    }

    /**
     * When the WiFi swap service is started or stopped, update the UI appropriately.
     * This includes both the in-transit states of "Starting" and "Stopping". In these two cases,
     * the UI should be disabled to prevent the user quickly switching back and forth - causing
     * multiple start/stop actions to be sent to the swap service.
     */
    private final BroadcastReceiver onWifiSwapStateChanged = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.hasExtra(SwapService.EXTRA_STARTING)) {
                Utils.debugLog(TAG, "WiFi service is starting (setting toggle to checked, but disabled).");
                textWifiVisible.setText(R.string.swap_setting_up_wifi);
                setWifiSwitchState(true, false);
            } else if (intent.hasExtra(SwapService.EXTRA_STOPPING)) {
                Utils.debugLog(TAG, "WiFi service is stopping (setting toggle to unchecked and disabled).");
                textWifiVisible.setText(R.string.swap_stopping_wifi);
                setWifiSwitchState(false, false);
            } else {
                if (intent.hasExtra(SwapService.EXTRA_STARTED)) {
                    Utils.debugLog(TAG, "WiFi service has started (setting toggle to visible).");
                    textWifiVisible.setText(R.string.swap_visible_wifi);
                    setWifiSwitchState(true, true);
                } else {
                    Utils.debugLog(TAG, "WiFi service has stopped (setting toggle to not-visible).");
                    textWifiVisible.setText(R.string.swap_not_visible_wifi);
                    setWifiSwitchState(false, true);
                }
            }
            uiUpdateWifiNetwork();
        }
    };

    /**
     * Helper function to set the "enable wifi" switch, but prevents the listeners from
     * being notified. This enables the UI to be updated without triggering further enable/disable
     * events being queued.
     *
     * This is required because the SwitchCompat and its parent classes will always try to notify
     * their listeners if there is one (e.g. http://stackoverflow.com/a/15523518).
     *
     * The fact that this method also deals with enabling/disabling the switch is more of a convenience
     * Nigh on all times this UI wants to change the state of the switch, it is also interested in
     * ensuring the enabled state of the switch.
     */
    private void setWifiSwitchState(boolean isChecked, boolean isEnabled) {
        wifiSwitch.setOnCheckedChangeListener(null);
        wifiSwitch.setChecked(isChecked);
        wifiSwitch.setEnabled(isEnabled);
        wifiSwitch.setOnCheckedChangeListener(onWifiSwitchToggled);
    }

    /**
     * When the wifi switch is:
     *
     * Toggled on: Ask the swap service to ensure wifi swap is running.
     * Toggled off: Ask the swap service to prevent the wifi swap service from running.
     *
     * Both of these actions will be performed in a background thread which will send broadcast
     * intents when they are completed.
     */
    private final CompoundButton.OnCheckedChangeListener onWifiSwitchToggled = new CompoundButton.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            if (isChecked) {
                Utils.debugLog(TAG, "Received onCheckChanged(true) for WiFi swap, asking in background thread to ensure WiFi swap is running.");
                getManager().getWifiSwap().ensureRunningInBackground();
            } else {
                Utils.debugLog(TAG, "Received onCheckChanged(false) for WiFi swap, disabling WiFi swap in background thread.");
                getManager().getWifiSwap().stopInBackground();
            }
            uiUpdateWifiNetwork();
        }
    };

    private void uiUpdateWifiNetwork() {

        viewWifiId.setText(FDroidApp.ipAddressString);
        viewWifiId.setVisibility(TextUtils.isEmpty(FDroidApp.ipAddressString) ? View.GONE : View.VISIBLE);

        WifiApControl wifiAp = WifiApControl.getInstance(getActivity());
        if (wifiAp != null && wifiAp.isWifiApEnabled()) {
            WifiConfiguration config = wifiAp.getConfiguration();
            viewWifiNetwork.setText(getContext().getString(R.string.swap_active_hotspot, config.SSID));
        } else if (TextUtils.isEmpty(FDroidApp.ssid)) {
            // not connected to or setup with any wifi network
            viewWifiNetwork.setText(R.string.swap_no_wifi_network);
        } else {
            // connected to a regular wifi network
            viewWifiNetwork.setText(FDroidApp.ssid);
        }
    }

    private void onPeerSelected(Peer peer) {
        getActivity().swapWith(peer);
    }

    @Override
    public boolean buildMenu(Menu menu, @NonNull MenuInflater inflater) {
        return false;
    }

    @Override
    public int getStep() {
        return SwapService.STEP_INTRO;
    }

    @Override
    public int getPreviousStep() {
        // TODO: Currently this is handleed by the SwapWorkflowActivity as a special case, where
        // if getStep is STEP_INTRO, don't even bother asking for getPreviousStep. But that is a
        // bit messy. It would be nicer if this was handled using the same mechanism as everything
        // else.
        return SwapService.STEP_INTRO;
    }

    @Override
    @ColorRes
    public int getToolbarColour() {
        return R.color.swap_bright_blue;
    }

    @Override
    public String getToolbarTitle() {
        return getResources().getString(R.string.swap_nearby);
    }

}
