FrameworkBleBackend.kt

package io.github.tritbool.euc.ble.integration

import android.Manifest
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import io.github.tritbool.euc.ble.core.BLEConstants
import io.github.tritbool.euc.ble.core.BLEManager
import io.github.tritbool.euc.ble.core.DataCallback
import io.github.tritbool.euc.ble.core.ErrorCallback
import io.github.tritbool.euc.ble.core.ListenerConnectionCallback
import io.github.tritbool.euc.ble.exceptions.BLEException
import io.github.tritbool.euc.ble.models.EUCDevice
import io.github.tritbool.euc.ble.protocols.EUCProtocol
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import java.util.UUID

/**
 * Framework-based BLE backend implementation.
 *
 * This backend delegates BLE operations to a [BLEManager] instance and forwards events
 * to a registered [BleBackendListener]. It uses Kotlin coroutines for asynchronous operations.
 *
 * @param bleManager The BLE manager that handles actual BLE operations
 * @param ownsScope If true, this backend owns and will cancel the coroutine scope on cleanup
 * @param scope The coroutine scope for background operations
 */
internal class FrameworkBleBackend(
    private val bleManager: BLEManager,
    private val ownsScope: Boolean = true,
    private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
) : BleBackend {

    override val type: BleBackendType = BleBackendType.FRAMEWORK
    private var listener: BleBackendListener? = null
    private var rawFrameJob: Job? = null
    private var isInitialized: Boolean = false

    @Synchronized
    override fun initialize() {
        bleManager.initialize()
        installCallbacks()
        installRawFrameForwarder()
        isInitialized = true
    }

    override fun registerProtocol(protocol: EUCProtocol) {
        bleManager.registerProtocol(protocol)
    }

    @Synchronized
    override fun setListener(listener: BleBackendListener?) {
        this.listener = listener
        if (isInitialized) {
            installRawFrameForwarder()
        }
    }

    @RequiresApi(Build.VERSION_CODES.M)
    @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN)
    override fun startScan() {
        bleManager.startScan()
    }

    @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN)
    override fun stopScan() {
        bleManager.stopScan()
    }

    @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
    override fun connect(device: EUCDevice) {
        bleManager.connect(device)
    }

    @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
    override fun disconnect() {
        bleManager.disconnect()
    }

    @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
    override fun sendCommand(command: ByteArray, characteristicUuid: UUID) {
        bleManager.sendCommand(command, characteristicUuid)
    }

    override fun getConnectionState(): BLEConstants.ConnectionState = bleManager.getConnectionState()

    override fun getConnectedDevice(): EUCDevice? = bleManager.getConnectedDevice()

    @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
    override fun cleanup() {
        rawFrameJob?.cancel()
        rawFrameJob = null
        if (ownsScope) {
            scope.cancel()
        }
        bleManager.cleanup()
    }

    private fun installCallbacks() {
        bleManager.setConnectionCallback(ListenerConnectionCallback(listener, bleManager) )

        bleManager.setDataCallback(object : DataCallback {
            override fun onDataReceived(data: io.github.tritbool.euc.ble.models.EUCData) {
                listener?.onEvent(BleBackendEvent.DataReceived(data))
            }
        })

        bleManager.setErrorCallback(object : ErrorCallback {
            override fun onError(error: BLEException) {
                listener?.onEvent(BleBackendEvent.Error(error))
            }
        })
    }

    @Synchronized
    private fun installRawFrameForwarder() {
        rawFrameJob?.cancel()
        rawFrameJob = bleManager.rawFrameFlow
            .onEach { payload ->
                listener?.onEvent(BleBackendEvent.RawFrameReceived(payload))
            }
            .launchIn(scope)
    }
}