package cloud.molberg.picar

import android.annotation.SuppressLint
import android.bluetooth.*
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
import android.os.Build
import android.os.ParcelUuid
import android.os.Handler
import android.os.Looper
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withTimeout
import org.json.JSONObject
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap

@SuppressLint("MissingPermission")
class BleClient(private val context: Context) {
    companion object {
        val SERVICE_UUID: UUID = UUID.fromString("6c7a0001-7c6d-4f74-9bb0-c5a4e5efc001")
        val RX_UUID: UUID = UUID.fromString("6c7a0002-7c6d-4f74-9bb0-c5a4e5efc001")
        val TX_UUID: UUID = UUID.fromString("6c7a0003-7c6d-4f74-9bb0-c5a4e5efc001")
        val CCC_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
    }

    val state = MutableStateFlow(ConnectionState.STOPPED)
    private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)
    private var gatt: BluetoothGatt? = null
    private var rx: BluetoothGattCharacteristic? = null
    private val incoming = StringBuilder()
    private val pending = ConcurrentHashMap<String, CompletableDeferred<JSONObject>>()
    private val writes = ArrayDeque<ByteArray>()
    private var writeInProgress = false
    private val handler = Handler(Looper.getMainLooper())
    private val reconnect: Runnable = Runnable { connect() }
    private val restartScan: Runnable = Runnable {
        if (!stopped && state.value == ConnectionState.SCANNING) {
            bluetoothManager.adapter?.bluetoothLeScanner?.stopScan(scanCallback)
            scheduleReconnect(1_000)
        }
    }
    private var stopped = true

    fun connect() {
        stopped = false
        handler.removeCallbacks(reconnect)
        handler.removeCallbacks(restartScan)
        val adapter = bluetoothManager.adapter ?: run {
            state.value = ConnectionState.DISCONNECTED
            scheduleReconnect()
            return
        }
        adapter.bluetoothLeScanner?.stopScan(scanCallback)
        state.value = ConnectionState.SCANNING
        val filter = ScanFilter.Builder().setServiceUuid(ParcelUuid(SERVICE_UUID)).build()
        adapter.bluetoothLeScanner?.startScan(listOf(filter), ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(), scanCallback)
        handler.postDelayed(restartScan, 15_000)
    }

    fun disconnect() {
        stopped = true
        handler.removeCallbacks(reconnect)
        handler.removeCallbacks(restartScan)
        bluetoothManager.adapter?.bluetoothLeScanner?.stopScan(scanCallback)
        gatt?.close(); gatt = null; rx = null
        resetWrites()
        state.value = ConnectionState.STOPPED
    }

    suspend fun request(operation: String, token: String?, payload: JSONObject = JSONObject()): JSONObject {
        val id = UUID.randomUUID().toString()
        val deferred = CompletableDeferred<JSONObject>()
        pending[id] = deferred
        val message = JSONObject().put("id", id).put("op", operation).put("payload", payload)
        if (token != null) message.put("token", token)
        enqueue((message.toString() + "\n").toByteArray())
        return try {
            withTimeout(20_000) { deferred.await() }
        } catch (error: TimeoutCancellationException) {
            handler.post { gatt?.disconnect() }
            throw error
        } finally {
            pending.remove(id)
        }
    }

    private val scanCallback: ScanCallback = object : ScanCallback() {
        override fun onScanResult(callbackType: Int, result: ScanResult) {
            handler.removeCallbacks(restartScan)
            bluetoothManager.adapter.bluetoothLeScanner?.stopScan(this)
            state.value = ConnectionState.CONNECTING
            gatt?.close()
            gatt = result.device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
        }
        override fun onScanFailed(errorCode: Int) {
            handler.removeCallbacks(restartScan)
            state.value = ConnectionState.DISCONNECTED
            scheduleReconnect()
        }
    }

    private val gattCallback: BluetoothGattCallback = object : BluetoothGattCallback() {
        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
            if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
                this@BleClient.gatt = gatt
                // Android GATT operations are serialized. Starting service
                // discovery while the MTU request is still pending can leave
                // the client in CONNECTING forever on some phones.
                if (!gatt.requestMtu(247)) gatt.discoverServices()
            }
            else {
                rx = null; gatt.close()
                if (this@BleClient.gatt === gatt) this@BleClient.gatt = null
                resetWrites(); state.value = ConnectionState.DISCONNECTED
                scheduleReconnect()
            }
        }

        override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
            gatt.discoverServices()
        }

        override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
            val service = gatt.getService(SERVICE_UUID)
            val nextRx = service?.getCharacteristic(RX_UUID)
            val tx = service?.getCharacteristic(TX_UUID)
            val descriptor = tx?.getDescriptor(CCC_UUID)
            if (status != BluetoothGatt.GATT_SUCCESS || nextRx == null || tx == null || descriptor == null) {
                gatt.disconnect()
                return
            }
            rx = nextRx
            if (!gatt.setCharacteristicNotification(tx, true)) {
                gatt.disconnect()
                return
            }
            val descriptorStarted = if (Build.VERSION.SDK_INT >= 33) {
                gatt.writeDescriptor(descriptor, BluetoothGattDescriptor.ENABLE_INDICATION_VALUE) == BluetoothStatusCodes.SUCCESS
            } else {
                descriptor.value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
                gatt.writeDescriptor(descriptor)
            }
            if (!descriptorStarted) gatt.disconnect()
        }

        override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
            if (descriptor.uuid == CCC_UUID && status == BluetoothGatt.GATT_SUCCESS) {
                state.value = ConnectionState.CONNECTED
            } else {
                gatt.disconnect()
            }
        }
        @Deprecated("Used on Android 12 and earlier")
        override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
            handleIncoming(characteristic.value)
        }

        override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray) {
            handleIncoming(value)
        }
        override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
            if (status == BluetoothGatt.GATT_SUCCESS) writeNext() else {
                synchronized(this@BleClient) { writeInProgress = false }
                gatt.disconnect()
            }
        }
    }

    private fun handleIncoming(value: ByteArray) {
        incoming.append(value.toString(Charsets.UTF_8))
        while (incoming.contains("\n")) {
            val index = incoming.indexOf("\n")
            val line = incoming.substring(0, index); incoming.delete(0, index + 1)
            runCatching { JSONObject(line) }.getOrNull()?.let { response ->
                pending.remove(response.optString("id"))?.let { deferred ->
                    if (response.optBoolean("ok")) deferred.complete(response.getJSONObject("data"))
                    else deferred.completeExceptionally(IllegalStateException(response.optJSONObject("error")?.optString("message") ?: "Bluetooth request failed"))
                }
            }
        }
    }

    @Synchronized private fun enqueue(bytes: ByteArray) {
        val chunks = bytes.asList().chunked(180)
        chunks.forEach { chunk -> writes.add(chunk.toByteArray()) }
        if (!writeInProgress) {
            writeInProgress = true
            writeNext()
        }
    }

    @Synchronized private fun writeNext() {
        val characteristic = rx
        val currentGatt = gatt
        if (characteristic == null || currentGatt == null) {
            writeInProgress = false
            return
        }
        val next = writes.removeFirstOrNull()
        if (next == null) {
            writeInProgress = false
            return
        }
        val started = if (Build.VERSION.SDK_INT >= 33) {
            currentGatt.writeCharacteristic(characteristic, next, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) == BluetoothStatusCodes.SUCCESS
        } else {
            characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
            characteristic.value = next
            currentGatt.writeCharacteristic(characteristic)
        }
        if (!started) {
            writeInProgress = false
            currentGatt.disconnect()
        }
    }

    @Synchronized private fun resetWrites() {
        writes.clear()
        writeInProgress = false
    }

    private fun scheduleReconnect(delayMillis: Long = 3_000) {
        if (stopped) return
        handler.removeCallbacks(reconnect)
        handler.postDelayed(reconnect, delayMillis)
    }
}
