package llc.arma.ble.data.repository

import android.app.Application
import android.os.SystemClock
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.data.db.RotationsDao
import llc.arma.ble.data.repository.extensions.checkPermission
import llc.arma.ble.data.repository.extensions.fromByte
import llc.arma.ble.data.repository.extensions.get4byteUIntAt
import llc.arma.ble.data.repository.extensions.info
import llc.arma.ble.data.repository.extensions.sendData
import llc.arma.ble.data.repository.extensions.to4ByteArrayInLittleEndian
import llc.arma.ble.data.repository.extensions.toTemperature
import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.repository.BleRepository
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import no.nordicsemi.android.common.core.DataByteArray
import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt
import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattCharacteristic
import no.nordicsemi.android.kotlin.ble.core.scanner.BleNumOfMatches
import no.nordicsemi.android.kotlin.ble.core.scanner.BleScanMode
import no.nordicsemi.android.kotlin.ble.core.scanner.BleScannerCallbackType
import no.nordicsemi.android.kotlin.ble.core.scanner.BleScannerMatchMode
import no.nordicsemi.android.kotlin.ble.core.scanner.BleScannerSettings
import no.nordicsemi.android.kotlin.ble.scanner.BleScanner
import java.util.Collections
import java.util.Timer
import java.util.TimerTask
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.PI
import kotlin.math.atan


val serviceUUID: UUID = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002")

val accelerometerReadUUID: UUID = UUID.fromString("00002713-0000-1000-8000-00805f9b34fb")
val hostHistoryReadUUID: UUID = UUID.fromString("a77db2d8-9bc4-11ed-a8fc-0242ac120002")
val temperatureHistoryReadUUID: UUID = UUID.fromString("a77db2d8-9bc4-11ed-a8fc-0242ac120002")
val accelerometerHistoryReadUUID: UUID = UUID.fromString("a77db2d8-9bc4-11ed-a8fc-0242ac120002")
val temperatureReadUUID: UUID = UUID.fromString("00002a6e-0000-1000-8000-00805f9b34fb")
val intervalReadUUID: UUID = UUID.fromString("a77db2d8-9bc4-11ed-a8fc-0242ac120002")
val intervalWriteUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120002")
val saveEnabledWriteUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120002")
val passwordWriteUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120002")
val txWriteUUID: UUID = UUID.fromString("00002a07-0000-1000-8000-00805f9b34fb")
val flashWriteUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120002")

@Singleton
class BleRepositoryImpl @Inject constructor(
    private val app: Application,
    private val rotationsDao: RotationsDao
) : BleRepository {

    val resultList: MutableMap<String, BleInfo> = Collections.synchronizedMap(mutableMapOf<String, BleInfo>())

    override fun getFoundBle(): List<BleInfo> {
        return resultList.values.toList()
    }

    override fun getBleAroundFlow(): Result<Flow<List<BleInfo>>, BleException> {

        return if(app.checkPermission()){

            Result.success(

                callbackFlow {

                    val job = BleScanner(app)
                        .scan(
                            settings = BleScannerSettings(
                                includeStoredBondedDevices = false,
                                scanMode = BleScanMode.SCAN_MODE_LOW_LATENCY,
                                callbackType = BleScannerCallbackType.CALLBACK_TYPE_ALL_MATCHES,
                                matchMode = BleScannerMatchMode.MATCH_MODE_AGGRESSIVE,
                                numOfMatches = BleNumOfMatches.MATCH_NUM_ONE_ADVERTISEMENT,
                                reportDelay = 0L
                            )
                        ).filter { it.device.name?.contains("ArmA") == true }
                        .onEach {
                            resultList[it.device.address] = it.info
                        }.launchIn(CoroutineScope(Dispatchers.IO))


                    val timer = Timer().apply {
                        schedule(object : TimerTask() {
                            override fun run() {
                                CoroutineScope(Dispatchers.IO).launch {
                                    send(resultList.values.toList())
                                }
                            }
                        }, 500, 500)
                    }

                    awaitClose {
                        job.cancel()
                        timer.cancel()
                    }

                }

            )

        } else {

            Result.failure(BleException.PermissionDenied)

        }

    }

    override suspend fun getBleBySerial(
        serial: String
    ): Result<Flow<Ble>, BleException> {

        val initialBle = resultList[serial]

        return if(initialBle == null){

            Result.failure(BleException.UnexpectedResponse)

        } else {

            fun BleInfo.updateBleInfo(): BleInfo {
                return copy(
                    rssi = if((SystemClock.elapsedRealtime() - scanTime) > 15_000) {
                        null
                    } else { rssi }
                )
            }

            fun BleInfo.updateState(): Ble.BleState {
                return Ble.BleState(
                    tx = Ble.BleState.TX.fromByte(tx.toByte())
                        ?: Ble.BleState.TX.ZERO
                )
            }

            val firstResult = when(initialBle.type){
                BleInfo.Type.HOST -> {

                    val tState = readHostState(
                        initialBle.serial
                    ).fold(
                        onFailure = { return Result.failure(it) },
                        onSuccess = { it }
                    )

                    Ble.Host(
                        info = initialBle.updateBleInfo(),
                        state = initialBle.updateState(),
                        hostState = tState
                    )

                }
                BleInfo.Type.BEACON -> {
                    Ble.Beacon(
                        info = initialBle.updateBleInfo(),
                        state = initialBle.updateState(),
                    )
                }
                BleInfo.Type.THERMOMETER -> {

                    val tState = readThermometerState(
                        initialBle.serial,
                        initialBle.tableStatus
                    ).fold(
                        onFailure = { return Result.failure(it) },
                        onSuccess = { it }
                    )

                    Ble.Thermometer(
                        info = initialBle.updateBleInfo(),
                        state = initialBle.updateState(),
                        thermometerState = tState
                    )

                }
                BleInfo.Type.ACCELEROMETER -> {

                    val tState = readAccelState(
                        initialBle.serial,
                        initialBle.tableStatus
                    ).fold(
                        onFailure = { return Result.failure(it) },
                        onSuccess = { it }
                    )

                    Ble.Accelerometer(
                        info = initialBle.updateBleInfo(),
                        state = initialBle.updateState(),
                        accelerometerState = tState
                    )

                }
            }

            Result.success(
                flow {
                    while (true){

                        resultList[serial]?.let { ble ->

                            firstResult.state = ble.updateState()
                            firstResult.info = ble.updateBleInfo()

                            emit(when(firstResult){
                                is Ble.Accelerometer -> {
                                    Ble.Accelerometer(
                                        info = ble.updateBleInfo(),
                                        state = ble.updateState(),
                                        accelerometerState = firstResult.accelerometerState
                                    )
                                }
                                is Ble.Beacon -> {
                                    Ble.Beacon(
                                        info = ble.updateBleInfo(),
                                        state = ble.updateState()
                                    )
                                }
                                is Ble.Host -> {
                                    Ble.Host(
                                        info = ble.updateBleInfo(),
                                        state = ble.updateState(),
                                        hostState = firstResult.hostState
                                    )
                                }
                                is Ble.Thermometer -> {
                                    Ble.Thermometer(
                                        info = ble.updateBleInfo(),
                                        state = ble.updateState(),
                                        thermometerState = firstResult.thermometerState
                                    )
                                }
                            })

                        }

                        delay(1_000)

                    }
                }
            )

        }

    }

    @OptIn(ExperimentalUnsignedTypes::class)
    private suspend fun readThermometerState(
        address: String,
        timer: BleInfo.HistoryTableStatus
    ): Result<Ble.Thermometer.ThermometerState, BleException> {

        return if(app.checkPermission()) {

            val connection =
                ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.IO))

            try {

                val service = connection.discoverServices().findService(serviceUUID)
                    ?: return Result.failure(BleException.UnexpectedResponse)

                var characteristic = service.findCharacteristic(temperatureReadUUID)
                    ?: return Result.failure(BleException.UnexpectedResponse)

                characteristic.write(DataByteArray.from(1, 1))

                delay(2_000)

                val temperature = characteristic.read().value.toUByteArray().toTemperature()

                characteristic = service.findCharacteristic(intervalReadUUID)
                    ?: return Result.failure(BleException.UnexpectedResponse)

                val interval = characteristic.readWriteInterval().fold(
                    onFailure = { return Result.failure(it) },
                    onSuccess = { it }
                )

                connection.close()

                return Result.success(
                    Ble.Thermometer.ThermometerState(
                        temperature = temperature,
                        saveHistory = timer !== BleInfo.HistoryTableStatus.DISABLED,
                        historyInterval = interval
                    )
                )

            } catch (err: Throwable){
                err.printStackTrace()
                return Result.failure(BleException.UnexpectedResponse)
            } finally {

                connection.close()

            }

        } else {

            Result.failure(BleException.PermissionDenied)

        }

    }

    private suspend fun readHostState(
        address: String
    ): Result<Ble.Host.HostState, BleException> {

        return if(app.checkPermission()) {

            val connection =
                ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.IO))

            try {

                val service = connection.discoverServices().findService(serviceUUID)
                    ?: return Result.failure(BleException.UnexpectedResponse)

                val characteristic = service.findCharacteristic(intervalReadUUID)
                    ?: return Result.failure(BleException.UnexpectedResponse)

                val interval = characteristic.readWriteInterval().fold(
                    onFailure = { return Result.failure(it) },
                    onSuccess = { it }
                )

                /*characteristic.write(DataByteArray.from(2, 0, 0, 0))

                val readTimeout = characteristic.read().value.let {
                    if(it.size == 4){
                        it.get4byteUIntAt(0).toLong()
                    } else {
                        0
                    }
                }*/

                return Result.success(
                    Ble.Host.HostState(
                        historyInterval = interval,
                        readInterval = interval
                    )
                )

            } catch (err: Throwable){
                err.printStackTrace()
                return Result.failure(BleException.UnexpectedResponse)
            } finally {

                connection.close()

            }

        } else {

            Result.failure(BleException.PermissionDenied)

        }

    }

    private suspend fun ClientBleGattCharacteristic.readWriteInterval(

    ): Result<Long, BleException> {

        return if(app.checkPermission().not()) {

            Result.failure(BleException.PermissionDenied)

        } else {

            write(DataByteArray.from(3, 0, 0, 0))

            val interval = read().value.let {
                if (it.size == 4) {
                    it.get4byteUIntAt(0).toLong()
                } else {
                    0
                }
            }

            Result.success(interval)

        }

    }

    private suspend fun readAccelState(
        address: String,
        timer: BleInfo.HistoryTableStatus
    ): Result<Ble.Accelerometer.AccelerometerState, BleException> {

        return if(app.checkPermission()) {

            val connection =
                ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.IO))

            try {

                val service = connection.discoverServices()
                    .findService(serviceUUID) ?: return Result.failure(BleException.UnexpectedResponse)

                var characteristic = service.findCharacteristic(intervalReadUUID)
                    ?: return Result.failure(BleException.UnexpectedResponse)

                val interval = characteristic.readWriteInterval().fold(
                    onFailure = { return Result.failure(it) },
                    onSuccess = { it }
                )

                val historySettingsParams = when(timer){
                    BleInfo.HistoryTableStatus.EMPTY,
                    BleInfo.HistoryTableStatus.NOT_EMPTY -> {

                        characteristic = service.findCharacteristic(accelerometerReadUUID)
                            ?: return Result.failure(BleException.UnexpectedResponse)

                        characteristic.write(DataByteArray.from(4))
                        characteristic.read().let {

                            val data = it.value

                            val scale = AccelScale.fromByte(data[1]) ?: return Result.failure(BleException.UnexpectedResponse)
                            val mode = AccelViewMode.fromByte(data[0]) ?: return Result.failure(BleException.UnexpectedResponse)

                            Ble.Accelerometer.HistorySettings.Enabled(
                                scale = scale,
                                mode = mode,
                                detailed = false
                            )
                        }
                    }
                    BleInfo.HistoryTableStatus.DISABLED -> Ble.Accelerometer.HistorySettings.Disabled
                }

                connection.close()

                return Result.success(
                    Ble.Accelerometer.AccelerometerState(
                        saveHistorySettings = historySettingsParams,
                        historyInterval = interval
                    )
                )

            } catch (err: Throwable){

                return Result.failure(BleException.UnexpectedResponse)

            } finally {

                connection.close()

            }

        } else {

            Result.failure(BleException.PermissionDenied)

        }

    }

    override suspend fun getAccelerometerSpectreBySerial(
        serial: String,
        accelScale: AccelScale,
        accelMode: AccelViewMode,
        fftAxis: FftAxis,
        fftMode: FftViewMode,
        frequency: FftFrequency
    ): Flow<Result<ProgressState<List<Ble.Accelerometer.SpectrePoint>>, BleException>> {

        return readAccelerometerSpectre(serial, app, accelScale, accelMode, fftAxis, fftMode, frequency)

    }

    override suspend fun getTemperatureHistoryBySerial(
        serial: String
    ): Flow<Result<ProgressState<List<Ble.Thermometer.HistoryPoint>>, BleException>> {

        return readThermometerHistory(serial, app)

    }

    override suspend fun getHostBleTableBySerial(
        serial: String
    ): Result<List<String>, BleException> {
        return readHostBleTable(serial, app)
    }

    override suspend fun addBleToHostTableBySerial(serial: String, ble: List<String>): Result<Int, BleException> {
        return editBleHostTable(serial, ble, app)
    }

    override suspend fun getHostHistoryBySerial(
        serial: String
    ): Flow<Result<ProgressState<List<Ble.Host.HistoryPoint>>, BleException>> {

        return readHostHistory(serial, app)

    }

    override suspend fun getAccelerometerHistoryBySerial(
        serial: String
    ): Flow<Result<ProgressState<List<Ble.Accelerometer.HistoryPoint>>, BleException>> {

        return getAccelerometerHistory(serial, app)

    }

    override suspend fun writeBle(
        serial: String,
        request: Ble.Thermometer.WriteRequest
    ): Result<Unit, BleException> {

        fun UInt.to4ByteArrayInLittleEndian(): ByteArray =
        (3 downTo 0).map {
            (this shr (it * Byte.SIZE_BITS)).toByte()
        }.toByteArray()

        return if(app.checkPermission()) {

            val connection =
                ClientBleGatt.connect(app, serial, CoroutineScope(Dispatchers.Default))

            try {

                val services = connection.discoverServices()
                val service = services.findService(serviceUUID) ?: return Result.failure(BleException.UnexpectedResponse)

                request.tx?.let {
                    service.findCharacteristic(txWriteUUID)!!.write(
                        DataByteArray.from(it.sendData)
                    )
                }

                request.saveHistory?.let {

                    service.findCharacteristic(saveEnabledWriteUUID)!!.write(
                        DataByteArray.from(
                            *mutableListOf<Byte>(4).apply {
                                add(if (it) 1 else 0)
                            }.toByteArray()
                        )
                    )

                }

                request.historyInterval?.let {

                    service.findCharacteristic(intervalWriteUUID)!!.write(
                        DataByteArray.from(
                            *mutableListOf<Byte>(3).apply {
                                addAll((it).toUInt().to4ByteArrayInLittleEndian().reversed().toList())
                            }.toByteArray()
                        )
                    )

                }

                service.findCharacteristic(flashWriteUUID)!!.write(
                    DataByteArray.from(9)
                )

                connection.close()

                Result.success(Unit)

            } catch (err: Throwable){

                err.printStackTrace()
                Result.failure(BleException.UnexpectedResponse)

            } finally {

                connection.close()

            }

        } else {

            Result.failure(BleException.PermissionDenied)

        }

    }

    override suspend fun writeBle(
        serial: String,
        request: Ble.Beacon.WriteRequest
    ): Result<Unit, BleException> {

        return if(app.checkPermission()) {

            val connection = ClientBleGatt.connect(app, serial, CoroutineScope(Dispatchers.Default))

            try {

                request.tx?.let {

                    connection.discoverServices().findService(serviceUUID)?.findCharacteristic(
                        txWriteUUID
                    )?.write(
                        DataByteArray.from(it.sendData)
                    ) ?: return Result.failure(BleException.UnexpectedResponse)

                }

                connection.discoverServices().findService(serviceUUID)?.findCharacteristic(
                    flashWriteUUID
                )!!.write(
                    DataByteArray.from(9)
                )

                connection.close()

                Result.success(Unit)

            } catch (err: Throwable){

                err.printStackTrace()
                Result.failure(BleException.UnexpectedResponse)

            } finally {

                connection.close()

            }

        } else {

            Result.failure(BleException.PermissionDenied)

        }

    }

    override suspend fun writeBle(
        serial: String,
        request: Ble.Host.WriteRequest
    ): Result<Unit, BleException> {

        return if(app.checkPermission()) {

            val connection = ClientBleGatt.connect(app, serial, CoroutineScope(Dispatchers.Default))

            try {

                val services = connection.discoverServices()
                val service = services.findService(serviceUUID) ?: return Result.failure(
                    BleException.UnexpectedResponse
                )

                request.tx?.let {

                    service.findCharacteristic(
                        txWriteUUID
                    )?.write(
                        DataByteArray.from(it.sendData)
                    ) ?: return Result.failure(BleException.UnexpectedResponse)

                }

                request.interval?.let {

                    service.findCharacteristic(intervalWriteUUID)!!.write(
                        DataByteArray.from(
                            *mutableListOf<Byte>(3).apply {
                                addAll(
                                    (it).toUInt().to4ByteArrayInLittleEndian().reversed().toList()
                                )
                            }.toByteArray()
                        )
                    )

                }

                request.readInterval?.let {

                    service.findCharacteristic(intervalWriteUUID)!!.write(
                        DataByteArray.from(
                            *mutableListOf<Byte>(2).apply {
                                addAll(
                                    (it / 1000).toUInt().to4ByteArrayInLittleEndian().reversed().toList()
                                )
                            }.toByteArray()
                        )
                    )

                }

                service.findCharacteristic(
                    flashWriteUUID
                )!!.write(
                    DataByteArray.from(9)
                )

                connection.close()

                Result.success(Unit)

            } catch (err: Throwable){
                err.printStackTrace()
                Result.failure(BleException.UnexpectedResponse)

            } finally {

                connection.close()

            }

        } else {

            Result.failure(BleException.PermissionDenied)

        }

    }

    override suspend fun writeBle(
        serial: String,
        request: Ble.Accelerometer.WriteRequest
    ): Result<Unit, BleException> {

        rotationsDao.deleteBySerial(serial)

        return if(app.checkPermission()) {

            val connection =
                ClientBleGatt.connect(app, serial, CoroutineScope(Dispatchers.Default))

            try {

                val services = connection.discoverServices()

                val service = services.findService(serviceUUID)
                    ?: return Result.failure(BleException.UnexpectedResponse)

                Log.d("write", request.toString())

                request.tx?.let {
                    service.findCharacteristic(txWriteUUID)!!.write(
                        DataByteArray.from(it.sendData)
                    )
                }

                request.saveHistorySettings?.let {

                    service.findCharacteristic(saveEnabledWriteUUID)!!.write(
                        DataByteArray.from(
                            *mutableListOf<Byte>(4).apply {
                                add(if (it is Ble.Accelerometer.HistorySettings.Enabled) 1 else 0)
                                if (it is Ble.Accelerometer.HistorySettings.Enabled) {
                                    add(it.mode.sendData)
                                    add(it.scale.sendData)
                                }
                            }.toByteArray()
                        )
                    )

                }

                request.historyInterval?.let {

                    service.findCharacteristic(intervalWriteUUID)!!.write(
                        DataByteArray.from(
                            *mutableListOf<Byte>(3).apply {
                                addAll(
                                    (it).toUInt().to4ByteArrayInLittleEndian().reversed().toList()
                                )
                            }.toByteArray()
                        )
                    )

                }

                service.findCharacteristic(flashWriteUUID)!!.write(
                    DataByteArray.from(9)
                )

                Result.success(Unit)

            } catch (err: Throwable){

                err.printStackTrace()
                Result.failure(BleException.UnexpectedResponse)

            } finally {

                connection.close()

            }

        } else {

            Result.failure(BleException.PermissionDenied)

        }

    }

    override suspend fun changeBlePassword(
        password: String,
        serial: String
    ): Result<Unit, BleException> {

        return if(app.checkPermission()) {

            val connection =
                ClientBleGatt.connect(app, serial, CoroutineScope(Dispatchers.Default))

            try {

                val services = connection.discoverServices()
                val service = services.findService(serviceUUID)
                    ?: return Result.failure(BleException.UnexpectedResponse)

                service.findCharacteristic(passwordWriteUUID)?.write(
                    DataByteArray(
                        mutableListOf(8.toByte()).apply {
                            addAll(password.toByteArray(Charsets.US_ASCII).toList())
                        }.toByteArray()
                    )
                ) ?: return Result.failure(BleException.UnexpectedResponse)

                connection.close()

                Result.success(Unit)

            } catch (err: Throwable) {

                err.printStackTrace()
                Result.failure(BleException.UnexpectedResponse)

            } finally {

                connection.close()

            }

        } else {
            Result.failure(BleException.PermissionDenied)
        }

    }

    override fun getAccelerometerMeasureBySerialFlow(
        serial: String,
        accelScale: AccelScale,
        accelMode: AccelViewMode,
        fftAxis: FftAxis,
        fftMode: FftViewMode,
        frequency: FftFrequency,
    ): Flow<Result<Ble.Accelerometer.RealtimePoint, BleException>> {

        return getAccelerometerRealtimeData(app, serial, accelScale, accelMode, fftAxis, fftMode, frequency)

    }

}

fun calculateAngle(
    first: Float,
    second: Float
): Float {

    var first = first

    if(first == 0f && second == 0f){
        first = 0.0001f
    }

    val result = when {
        first > 0 -> atan(second / first)
        first < 0 && second >= 0 -> atan(second / first) + PI.toFloat()
        first < 0 && second < 0 -> atan(second/first) - PI.toFloat()
        first == 0f && second > 0 -> PI.toFloat() / 2f
        first == 0f && second < 0 -> -PI.toFloat() / 2f
        else -> 0f
    }

    return result * 180f / Math.PI.toFloat()

}