package com.epfl.esl.sportstracker

import android.Manifest
import android.annotation.SuppressLint
import android.app.Application
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothProfile
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.content.Context
import android.location.Address
import android.location.Geocoder
import android.os.Handler
import android.util.Log
import androidx.annotation.RequiresPermission
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import co.yml.charts.common.model.Point
import com.google.android.gms.location.LocationServices
import com.google.android.gms.tasks.Tasks
import com.google.android.gms.wearable.DataClient
import com.google.android.gms.wearable.DataEvent
import com.google.android.gms.wearable.DataEventBuffer
import com.google.android.gms.wearable.DataMapItem
import com.google.android.gms.wearable.MessageClient
import com.google.android.gms.wearable.Wearable
import java.io.IOException
import java.util.Locale
import java.util.UUID

data class LocationData(val latitude: Double,
                        val longitude: Double,
                        val description: String)

class ExerciseLiveViewModel(application: Application) :
    AndroidViewModel(application),
    DataClient.OnDataChangedListener {

    private val _heartRate = MutableLiveData<Int>(0)
    val heartRate: LiveData<Int>
        get() = _heartRate

    private val _heartRateList = ArrayList<Point>()
    private val _heartRateListLiveData = MutableLiveData<List<Point>>()
    val heartRateList: LiveData<List<Point>>
        get() = _heartRateListLiveData

    private val _locationData = MutableLiveData<LocationData>(
        LocationData(
            46.5197,
            6.6323,
            "Lausanne"
        )
    ) //Lausanne as an initial location

    val locationData: LiveData<LocationData>
        get() = _locationData

    private val bluetoothAdapter: BluetoothAdapter? =
        BluetoothAdapter.getDefaultAdapter()
    private val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
    private var scanning = false
    private val handler = Handler()
    private val SCAN_PERIOD: Long = 5000

    private val myDeviceAddress = "<YOUR_BLE_ADDRESS_HERE>" // e.g. "AB:DC:12:34:56:AA"
    private val context = getApplication<Application>().applicationContext
    private var bluetoothGatt: BluetoothGatt? = null
    private val HEART_RATE_SERVICE = "0000180D-0000-1000-8000-00805f9b34fb"
    private val HEART_RATE_MEASUREMENT = "00002a37-0000-1000-8000-00805f9b34fb"
    private val CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb"

    @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
    fun scanLeDevice() {
        if (!scanning) { // Stops scanning after a pre-defined scan period.
            handler.postDelayed( {
                scanning = false
                bluetoothLeScanner?.stopScan(leScanCallback)
            }, SCAN_PERIOD)
            scanning = true
            bluetoothLeScanner?.startScan(leScanCallback)
        } else {
            scanning = false
            bluetoothLeScanner?.stopScan(leScanCallback)
        }
    }

    private val leScanCallback: ScanCallback = object : ScanCallback() {
        @RequiresPermission(allOf = [Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN])
        override fun onScanResult(callbackType: Int, result: ScanResult) {
            super.onScanResult(callbackType, result)
            Log.i(
                ExerciseLiveViewModel::class.simpleName,
                "Name: ${result.device.name}, " +
                        "Address: ${result.device.address}," +
                        " RSSI: ${result.rssi}"
            )
            if (result.device.address == myDeviceAddress){
                bluetoothLeScanner?.stopScan(this)
                result.device.connectGatt(context, false, gattCallback)
            }
        }
    }

    private val gattCallback = object : BluetoothGattCallback() {
        @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
        override fun onConnectionStateChange(gatt: BluetoothGatt,
                                             status: Int, newState: Int) {
            val deviceAddress = gatt.device.address
            if (status == BluetoothGatt.GATT_SUCCESS) {
                if (newState == BluetoothProfile.STATE_CONNECTED) {
                    Log.w("BluetoothGattCallback",
                        "Successfully connected to $deviceAddress")
                    bluetoothGatt = gatt
                    bluetoothGatt?.discoverServices()
                } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                    Log.w("BluetoothGattCallback",
                        "Successfully disconnected from $deviceAddress")
                    gatt.close()
                }
            } else {
                Log.w(
                    "BluetoothGattCallback",
                    "Error $status encountered for $deviceAddress! Disconnecting..."
                )
                gatt.close()
            }
        }

        @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
        override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                val gattService = bluetoothGatt?.
                getService(UUID.fromString(HEART_RATE_SERVICE))
                val gattCharacteristics = gattService?.
                getCharacteristic(UUID.fromString(HEART_RATE_MEASUREMENT))
                setHeartRateCharacteristicNotification(gattCharacteristics!!, true)
            } else {
                Log.w("BluetoothGattCallback", "onServicesDiscovered received: $status")
            }
        }

        override fun onCharacteristicChanged(gatt: BluetoothGatt,
                                             characteristic: BluetoothGattCharacteristic) {
            val heartRateReceived = characteristic.value.get(1).toInt()
            Log.i(ExerciseLiveViewModel::class.simpleName,
                "HR : ${characteristic.value.get(1)}")
            _heartRate.postValue(heartRateReceived)
// Update HR plot series
            val size = _heartRateList.size
            _heartRateList.add(Point(size.toFloat(), heartRateReceived.toFloat()))
            _heartRateListLiveData.postValue(_heartRateList)
        }
    }

    @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
    private fun setHeartRateCharacteristicNotification(
        characteristic: BluetoothGattCharacteristic,
        enabled: Boolean) {
        bluetoothGatt?.let  { gatt ->
            gatt.setCharacteristicNotification(characteristic, enabled)
            val descriptor = characteristic.getDescriptor(
                UUID.fromString(CLIENT_CHARACTERISTIC_CONFIG))
            descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
            gatt.writeDescriptor(descriptor)
        } ?: run {
            Log.w("BluetoothCallback", "BluetoothGatt not initialized")
        }
    }

    override fun onDataChanged(dataEvents: DataEventBuffer) {
        dataEvents
            .filter { it.type == DataEvent.TYPE_CHANGED &&
                    it.dataItem.uri.path == "/heart_rate" }
            .forEach { event ->
                val newValue = DataMapItem.fromDataItem(event.dataItem)
                    .dataMap.getIntegerArrayList("HEART_RATE")
                newValue?.let {
                    var nextIndex = _heartRateList.size
                    it.forEach { i ->
                        _heartRateList
                            .add(Point(nextIndex.toFloat(), i.toFloat()))
                        nextIndex += 1
                    }
                    val lastElementIndex = _heartRateList.lastIndex
                    _heartRate.value = _heartRateList
                        .get(lastElementIndex).y.toInt()
                    _heartRateListLiveData.value = _heartRateList
                }
            }
    }

    fun sendCommandToWear(command: String, context: Context) {
        Thread(Runnable {
            val connectedNodes: List<String> = Tasks
                .await(
                    Wearable
                        .getNodeClient(context).connectedNodes
                )
                .map { it.id }
            connectedNodes.forEach {
                val messageClient: MessageClient = Wearable
                    .getMessageClient(context)
                messageClient.sendMessage(it, "/command", command.toByteArray())
            }
        }).start()
    }

    @SuppressLint("MissingPermission")
    fun getLastLocation(context: Context) {
        val locationProvider =
            LocationServices.getFusedLocationProviderClient(context)
        locationProvider.lastLocation.addOnCompleteListener { task ->
            if (task.isSuccessful && task.result != null) {
                val location = task.result
                val address = getAddress(
                    context, location.latitude,
                    location.longitude
                )
                val locationData = LocationData(
                    location.latitude,
                    location.longitude, address
                )
                _locationData.postValue(locationData)
            }
        }
    }

    private fun getAddress(context: Context, latitude: Double, longitude: Double): String {
        var address = ""
        val gcd = Geocoder(context, Locale.getDefault())
        val addresses: List<Address>
        try {
            addresses = gcd.getFromLocation(
                latitude,
                longitude,
                1
            ) as List<Address>
            if (addresses.isNotEmpty()) {
                address = addresses[0].getAddressLine(0)
            }
        } catch (e: IOException) {
            Log.e(ExerciseLiveViewModel::class.java.simpleName, "${e.message}")
        }
        return address
    }

    @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
    fun stopBLE() {
        bluetoothGatt?.let { gatt ->
            gatt.close()
            bluetoothGatt = null
        }
    }
}