/*
 * Copyright (C) 2014 Lucien Loiseau
 *
 * This file is part of Rumble.
 *
 * Rumble is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Rumble is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Rumble.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.disrupted.rumble.network.linklayer.bluetooth;


import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.*;

import org.disrupted.rumble.util.Log;

import org.disrupted.rumble.app.RumbleApplication;
import org.disrupted.rumble.network.events.ScannerNeighbourSensed;
import org.disrupted.rumble.network.events.ScannerNeighbourTimeout;
import org.disrupted.rumble.network.linklayer.events.BluetoothScanEnded;
import org.disrupted.rumble.network.linklayer.events.BluetoothScanStarted;
import org.disrupted.rumble.network.events.ChannelConnected;
import org.disrupted.rumble.network.events.ChannelDisconnected;
import org.disrupted.rumble.network.linklayer.LinkLayerNeighbour;
import org.disrupted.rumble.network.linklayer.Scanner;

import java.util.HashSet;
import java.lang.Math;
import java.util.concurrent.locks.ReentrantLock;

import de.greenrobot.event.EventBus;

/**
 * @author Lucien Loiseau
 */
public class BluetoothScanner extends HandlerThread implements SensorEventListener, Scanner {

    private static final String TAG = "BluetoothScanner";

    private static final ReentrantLock lock = new ReentrantLock();
    private static BluetoothScanner instance;
    private static int openedSocket;

    /*
     * Scanning consumes a lot of  resources, especially  battery.
     * in order  to save battery, the period  between two  successive
     * scan follow the trickle algorithm defined in the RFC 6206:
     *
     *         http://tools.ietf.org/html/rfc6206
     *
     * The idea is to increase the non-scanning period if the
     * neighborhood stay consistent between two scan.
     *
     * The trickle is increased first in a linear way, then in an exponential fashion
     *    - Slow Start  mode: in which we increase linearly the trickle timer
     *    - Exponential mode: in which we increase the timer exponentially
     *    - Beta Mode: in which we only scan rarely when connected to a device
     *
     *  Whenever the neighborhood change between to scan, a change is the detection of a new
     *  neighbour or a previous neighbour being unreachable. If the number of such inconsistency
     *  is more than a certain threshold, we reset the trickle timer.
     */
    private enum ScanningState {
        SCANNING_OFF, SCANNING_IDLE, SCANNING_SCHEDULED, SCANNING_IN_PROGRESS
    }
    private ScanningState scanningState;
    private HashSet<BluetoothNeighbour>  btNeighborhood;

    private static final double SCANNING_TIMEOUT        = 15000; // max scanning time 15 seconds
    private static final double START_TRICKLE_TIMER     = 10000; // 10 seconds
    private static final double BETA_TRICKLE_TIMER      = 60000; // 1 minute
    private static final double LINEAR_STEP             = 5000;  // 5 seconds
    private static final double EXPONENTIAL_THRESHOLD   = 60000; // 1 minute
    private static final double IMAX_TRICKLE_TIMER      = 4;     // 2^4 = 16 minutes
    private static final int    INCONSISTENCY_THRESHOLD = 2;

    private double              trickleTimer = START_TRICKLE_TIMER;
    private HashSet<BluetoothNeighbour>  lastTrickleState;
    private boolean             betamode;

    private Handler             handler;

    /*
     * reset the trickle timer when phone is moving only if the timer is already long enough
     */
    private static final double RESET_TRICKLE_THRESHOLD = 30000;
    private SensorManager       mSensorManager;
    private Sensor              mAccelerometer;
    private long                lastUpdate;
    final float alpha = 0.8f;
    float[] gravity = new float[3];
    float[] linear_acceleration = new float[3];

    private boolean registered;
    private boolean sensorregistered;

    public static BluetoothScanner getInstance() {
        try {
            lock.lock();
            if(instance == null)
                instance = new BluetoothScanner();

            return instance;
        } finally {
            lock.unlock();
        }
    }

    private BluetoothScanner() {
        super(TAG);
        super.start();
        btNeighborhood     = new HashSet<BluetoothNeighbour>();
        lastTrickleState = new HashSet<BluetoothNeighbour>();
        trickleTimer     = START_TRICKLE_TIMER;
        scanningState    = ScanningState.SCANNING_OFF;
        registered   = false;
        betamode     = false;
        openedSocket = 0;

        /*
         * using the sensor on google nexus s has supposedly broken the bluetooth stack, stucking it
         * in a while loop at boot, had to factory reset
        mSensorManager = (SensorManager) RumbleApplication.getContext().getSystemService(Context.SENSOR_SERVICE);
        if (mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null){
            mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        } else {
            mAccelerometer = null;
        }
        for(int i = 0; i < 3; i++) {
            gravity[i] = 0;
            linear_acceleration[i] = 0;
        }
        */
    }

    @Override
    protected void finalize() throws Throwable {
        super.quit();
        super.finalize();
    }

    @Override
    public void startScanner() {
        try {
            lock.lock();
            if (!scanningState.equals(ScanningState.SCANNING_OFF))
                return;
            scanningState = ScanningState.SCANNING_IDLE;

            Log.d(TAG, "--- Bluetooth Scanner started ---");

            if (!registered) {
                IntentFilter filter = new IntentFilter();
                filter.addAction(BluetoothDevice.ACTION_FOUND);
                filter.addAction(BluetoothDevice.ACTION_UUID);
                filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
                filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
                filter.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);

                handler = new Handler(getLooper());
                RumbleApplication.getContext().registerReceiver(mReceiver, filter, null, handler);
                registered = true;
            }
            EventBus.getDefault().register(this);
        } finally {
            lock.unlock();
        }
        performScan(true);
    }

    public void stopScanner() {
        try {
            lock.lock();
            if (scanningState.equals(ScanningState.SCANNING_OFF))
                return;

            BluetoothAdapter mBluetoothAdapter = BluetoothUtil.getBluetoothAdapter(RumbleApplication.getContext());
            if (mBluetoothAdapter != null) {
                if (mBluetoothAdapter.isEnabled() && mBluetoothAdapter.isDiscovering())
                    mBluetoothAdapter.cancelDiscovery();
            }

            switch (scanningState) {
                case SCANNING_IDLE:
                    break;
                case SCANNING_SCHEDULED:
                    handler.removeCallbacks(scanScheduleFires);
                    break;
                case SCANNING_IN_PROGRESS:
                    handler.removeCallbacks(scanTimeoutFires);
                    EventBus.getDefault().post(new BluetoothScanEnded());
                    break;
            }
            scanningState = ScanningState.SCANNING_OFF;

            Log.d(TAG, "--- Bluetooth Scanner stopped ---");

            if (EventBus.getDefault().isRegistered(this))
                EventBus.getDefault().unregister(this);

            if (registered) {
                RumbleApplication.getContext().unregisterReceiver(mReceiver);
                registered = false;
            }

            btNeighborhood.clear();
            resetTrickleTimer();
            /*
            if((mAccelerometer != null) && sensorregistered) {
                mSensorManager.unregisterListener(this);
                sensorregistered = false;
            }
            */
        } finally {
            lock.unlock();
        }
    }

    private void performScan(boolean force) {
        try {
            lock.lock();

            switch (scanningState) {
                case SCANNING_OFF:
                    return;
                case SCANNING_IN_PROGRESS:
                    return;
                case SCANNING_SCHEDULED:
                    if(!force)
                        return;
                    else {
                        handler.removeCallbacks(scanScheduleFires);
                        scanningState = ScanningState.SCANNING_IDLE;
                    }
                case SCANNING_IDLE:
                    break;
            }

            /*
            if((mAccelerometer != null) && !sensorregistered) {
                mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL);
                sensorregistered = true;
            }
            */
            BluetoothAdapter mBluetoothAdapter = BluetoothUtil.getBluetoothAdapter(RumbleApplication.getContext());

            if (mBluetoothAdapter == null) {
                Log.d(TAG, "Bluetooth is not supported on this platform");
                return;
            }

            if( mBluetoothAdapter.isEnabled() ){
                btNeighborhood.clear();

                /*
                 * it is possible that the device is already discovering if another app
                 * ran a discovery procedure
                 */
                if (!mBluetoothAdapter.isDiscovering())
                    mBluetoothAdapter.startDiscovery();
                scanningState = ScanningState.SCANNING_IN_PROGRESS;
                EventBus.getDefault().post(new BluetoothScanStarted());

                /*
                 * we set a timeout in case the scanning discovery procedure doesn't stop by itself
                 * (yes. it does happen too.)
                 */
                handler.postDelayed(scanTimeoutFires, (long)SCANNING_TIMEOUT);
            }
        }
        catch (Exception e) {
            Log.d(TAG, "Exception:"+e.toString());
        } finally {
            lock.unlock();
        }
    }


    private Runnable scanTimeoutFires = new Runnable() {
        @Override
        public void run() {
            try {
                lock.lock();

                switch (scanningState) {
                    case SCANNING_OFF:
                    case SCANNING_IDLE:
                    case SCANNING_SCHEDULED:
                        return;
                    case SCANNING_IN_PROGRESS:
                        break;
                }

                Log.d(TAG, "[-] timeout expires: force scan procedure to stop");
                BluetoothAdapter mBluetoothAdapter = BluetoothUtil.getBluetoothAdapter(RumbleApplication.getContext());
                if (mBluetoothAdapter.isDiscovering())
                    mBluetoothAdapter.cancelDiscovery();

                EventBus.getDefault().post(new BluetoothScanEnded());

                recomputeTrickleTimer();

                handler.postDelayed(scanScheduleFires, (long) trickleTimer);
                scanningState = ScanningState.SCANNING_SCHEDULED;

            } finally {
                lock.unlock();
            }
        }
    };

    private Runnable scanScheduleFires = new Runnable() {
        @Override
        public void run() {
            try {
                lock.lock();
                switch (scanningState) {
                    case SCANNING_OFF:
                    case SCANNING_IDLE:
                    case SCANNING_IN_PROGRESS:
                        return;
                    case SCANNING_SCHEDULED:
                        scanningState = ScanningState.SCANNING_IDLE;
                }
                /*
                 * just in case a neighbour connect while we were in this critical section
                 * in which case the state would be SCANNING_SCHEDULE with the callback attached
                 */
                handler.removeCallbacks(scanScheduleFires);
            } finally {
                lock.unlock();
            }
            performScan(false);
        }
    };

    @Override
    public void forceDiscovery() {
        resetTrickleTimer();
        performScan(true);
    }

    /*
     * The trickle timer increase if the neighborhood is consistent
     * It is resetted if the neighborhood is inconsistant
     */
    private void recomputeTrickleTimer() {
        int inconsistency = 0;
        HashSet<BluetoothNeighbour> tmp = new HashSet<BluetoothNeighbour>();

        /*
         * first we detect for new neighbour inconsistency
         */
        for (BluetoothNeighbour neighbor : btNeighborhood) {
            tmp.add(neighbor);
            // the neighbour reachable event has already been sent ACTION_DEVICE_FOUND
            if (!lastTrickleState.remove(neighbor))
                inconsistency++;
        }

        /*
         * Then we detect for neighbour that disappeared inconsistency
         */
        for (BluetoothNeighbour neighbor : lastTrickleState) {
            EventBus.getDefault().post(new ScannerNeighbourTimeout(neighbor));
            inconsistency++;
        }

        /*
         * then we save the state for the next trickle recomputation
         */
        lastTrickleState.clear();
        lastTrickleState = tmp;

        if(betamode) {
            trickleTimer = BETA_TRICKLE_TIMER;
        } else {
            if (inconsistency < INCONSISTENCY_THRESHOLD) {
                if (trickleTimer > EXPONENTIAL_THRESHOLD)
                    trickleTimer = (((trickleTimer * 2) > (START_TRICKLE_TIMER * (Math.pow(2, IMAX_TRICKLE_TIMER)))) ? Math.pow(2, IMAX_TRICKLE_TIMER) : (trickleTimer * 2));
                else
                    trickleTimer += LINEAR_STEP;
            } else {
                trickleTimer = START_TRICKLE_TIMER;
            }
        }
    }
    private void resetTrickleTimer() {
        lastTrickleState.clear();
        trickleTimer = START_TRICKLE_TIMER;
    }

    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            if(scanningState.equals(ScanningState.SCANNING_OFF))
                return;

            String action = intent.getAction();

            if (BluetoothDevice.ACTION_FOUND.equals(action)) {
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                if (device.getAddress() == null)
                    return;
                BluetoothNeighbour btPeerDevice = new BluetoothNeighbour(device.getAddress());
                lock.lock();
                try {
                    if(btNeighborhood.add(btPeerDevice))
                        EventBus.getDefault().post(new ScannerNeighbourSensed(btPeerDevice));
                } finally {
                    lock.unlock();
                }
            }

            /*
             * It is possible that another application (like firechat) is also sending
             * discovery intent to bluetooth. In that case we silently use the response
             * of these one
             */
            if(BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(action)){
                SystemClock.sleep(2000);
                try {
                    lock.lock();
                    switch (scanningState) {
                        case SCANNING_OFF:
                            return;
                        case SCANNING_IDLE:
                            Log.d(TAG, "another app has triggered a scan, ignoring");
                            return;
                        case SCANNING_SCHEDULED:
                            Log.d(TAG, "another app has triggered a scan before our scheduled one");
                            handler.removeCallbacks(scanScheduleFires);
                            scanningState = ScanningState.SCANNING_IDLE;
                            break;
                        case SCANNING_IN_PROGRESS:
                            return;

                    }
                } finally {
                    lock.unlock();
                }
                performScan(true);
            }


            if(BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)){
                try {
                    lock.lock();

                    switch (scanningState) {
                        case SCANNING_OFF:
                        case SCANNING_SCHEDULED:
                        case SCANNING_IDLE:
                            return;
                        case SCANNING_IN_PROGRESS:
                            break;
                    }

                    handler.removeCallbacks(scanTimeoutFires);
                    EventBus.getDefault().post(new BluetoothScanEnded());

                    recomputeTrickleTimer();
                    handler.postDelayed(scanScheduleFires, (long) trickleTimer);
                    scanningState = ScanningState.SCANNING_SCHEDULED;
                    Log.d(TAG, "[->] next scan in: "+trickleTimer/1000L+" seconds");
                } finally {
                    lock.unlock();
                }
            }

            if(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED.equals(action)){
                int oldState = intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, 0);
                int newState = intent.getIntExtra(BluetoothAdapter.EXTRA_PREVIOUS_SCAN_MODE, 0);
                Log.d(TAG, "[+] Scan Mode Changed "+oldState+" -> "+newState);
            }

        }
    };


    @Override
    public void onSensorChanged(SensorEvent event) {
        long curTime = System.currentTimeMillis();
        if ((curTime - lastUpdate) > 100) {
            lastUpdate = curTime;

            gravity[0] = alpha * gravity[0] + (1 - alpha) * event.values[0];
            gravity[1] = alpha * gravity[1] + (1 - alpha) * event.values[1];
            gravity[2] = alpha * gravity[2] + (1 - alpha) * event.values[2];

            float x =  event.values[0] - gravity[0];
            float y = event.values[1] - gravity[1];
            float z = event.values[2] - gravity[2];

            float speed = Math.abs(x+y+z-linear_acceleration[0]-linear_acceleration[1]-linear_acceleration[2]);

            if(speed > 2) {
                if(trickleTimer > RESET_TRICKLE_THRESHOLD) {
                    Log.d(TAG, "[!] phone moved, reset trickle timer");
                    forceDiscovery();
                }
            }

            linear_acceleration[0] = x;
            linear_acceleration[1] = y;
            linear_acceleration[2] = z;
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int i) {
    }

    @Override
    public boolean isScanning() {
        return scanningState.equals(ScanningState.SCANNING_IN_PROGRESS);
    }

    @Override
    public HashSet<LinkLayerNeighbour> getNeighbourList() {
        try {
            lock.lock();
            HashSet<LinkLayerNeighbour> neighbourSet = new HashSet<LinkLayerNeighbour>();
            for (BluetoothNeighbour bn : btNeighborhood) {
                neighbourSet.add(bn);
            }
            return neighbourSet;
        } finally {
            lock.unlock();
        }
    }

    /*
     * discovering node disrupt connections...
     * when a neighbour is connected,  we scan much less
     */
    public void onEvent(ChannelConnected event) {
        if (!event.neighbour.getLinkLayerIdentifier().equals(BluetoothLinkLayerAdapter.LinkLayerIdentifier))
            return;
        try {
            lock.lock();
            openedSocket++;

            if(openedSocket == 1) {
                Log.d(TAG, "[+] entering slow scan mode ");
                betamode = true;

                switch (scanningState) {
                    case SCANNING_OFF:
                        return;
                    case SCANNING_IDLE:
                        /*
                         * most probably we are in between a call to performScan(false)
                         * out from scanScheduleFires(). or for some reason the scanner stopped scanning
                         */
                        break;
                    case SCANNING_IN_PROGRESS:
                        Log.d(TAG, "[-] cancelling current scan");
                        handler.removeCallbacks(scanTimeoutFires);
                        BluetoothAdapter mBluetoothAdapter = BluetoothUtil.getBluetoothAdapter(RumbleApplication.getContext());
                        if (mBluetoothAdapter.isDiscovering())
                            mBluetoothAdapter.cancelDiscovery();
                        EventBus.getDefault().post(new BluetoothScanEnded());
                        break;
                    case SCANNING_SCHEDULED:
                        Log.d(TAG, "[-] cancelling previous scan scheduling");
                        handler.removeCallbacks(scanScheduleFires);
                        break;
                }

                handler.postDelayed(scanScheduleFires, (long) BETA_TRICKLE_TIMER);
                Log.d(TAG, "[->] next scan in: "+BETA_TRICKLE_TIMER/1000L+" seconds");
                scanningState = ScanningState.SCANNING_SCHEDULED;
            }
        } finally {
            lock.unlock();
        }
    }


    public void onEvent(ChannelDisconnected event) {
        if (!event.neighbour.getLinkLayerIdentifier().equals(BluetoothLinkLayerAdapter.LinkLayerIdentifier))
            return;
        try {
            lock.lock();
            openedSocket--;
            if(openedSocket < 0) {
                Log.e(TAG, "[!] opened socket cannot be below zero: "+openedSocket);
                openedSocket = 0;
            }

            if (openedSocket == 0) {
                Log.d(TAG, "[+] leaving slow scan mode, entering trickle strategy ");
                betamode = false;

                switch (scanningState) {
                    case SCANNING_OFF:         // we do nothing
                    case SCANNING_IDLE:        // most probably a scan will start shortly
                    case SCANNING_IN_PROGRESS: // it will reschedule a new scan by itself
                        return;
                    case SCANNING_SCHEDULED:   // that is the slow scan mode scheduled
                        handler.removeCallbacks(scanScheduleFires);
                        Log.d(TAG, "[-] cancelling previous scan scheduling");
                        break;
                }

                resetTrickleTimer();
                handler.postDelayed(scanScheduleFires, (long) trickleTimer);
                scanningState = ScanningState.SCANNING_SCHEDULED;
                Log.d(TAG, "[->] next scan in: "+trickleTimer/1000L+" seconds");
                /*
                    if((mAccelerometer != null) && !sensorregistered) {
                        mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL);
                        sensorregistered = true;
                    }
                */
            }
        } finally {
            lock.unlock();
        }
    }
}