package com.nisemoe.nise.service

import com.aayushatharva.brotli4j.Brotli4jLoader
import com.aayushatharva.brotli4j.decoder.Decoder
import com.aayushatharva.brotli4j.encoder.Encoder
import com.nisemoe.nise.integrations.CircleguardService
import org.springframework.stereotype.Service
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import kotlin.math.round

@Service
class CompressJudgements {

    val brotliParameters: Encoder.Parameters = Encoder.Parameters()
        .setQuality(11)

    init {
        Brotli4jLoader.ensureAvailability()
    }

    fun ByteBuffer.putVLQ(value: Int) {
        var currentValue = value
        do {
            var temp = (currentValue and 0x7F)
            currentValue = currentValue ushr 7
            if (currentValue != 0) {
                temp = temp or 0x80
            }
            this.put(temp.toByte())
        } while (currentValue != 0)
    }

    fun ByteBuffer.getVLQ(): Int {
        var result = 0
        var shift = 0
        var b: Byte
        do {
            b = this.get()
            result = result or ((b.toInt() and 0x7F) shl shift)
            shift += 7
        } while (b.toInt() and 0x80 != 0)
        return result
    }

    fun serialize(judgements: List<CircleguardService.ScoreJudgement>): ByteArray {
        val byteStream = ByteArrayOutputStream()
        var lastTimestamp = 0.0

        judgements.forEach { judgement ->
            byteStream.use { stream ->
                /**
                 * We allocate an arbitrary amount of buffer which *hopefully* is enough.
                 */
                ByteBuffer.allocate(4096).let { buffer ->
                    buffer.putVLQ((judgement.time - lastTimestamp).toInt())
                    buffer.putVLQ(round(judgement.x * 100).toInt())
                    buffer.putVLQ(round(judgement.y * 100).toInt())
                    buffer.put(judgement.type.ordinal.toByte())
                    buffer.putVLQ((judgement.distance_center * 100).toInt())
                    buffer.putVLQ((judgement.distance_edge * 100).toInt())
                    buffer.putVLQ(judgement.error.toInt())

                    lastTimestamp = judgement.time
                    stream.write(buffer.array(), 0, buffer.position())
                }
            }
        }

        return Encoder.compress(byteStream.toByteArray(), brotliParameters)
    }

    fun deserialize(compressedData: ByteArray): List<CircleguardService.ScoreJudgement> {
        val data = Decoder.decompress(compressedData).decompressedData ?: return emptyList()

        val buffer = ByteBuffer.wrap(data)
        val judgements = mutableListOf<CircleguardService.ScoreJudgement>()
        var lastTime = 0.0

        while (buffer.hasRemaining()) {
            val deltaTime = buffer.getVLQ()
            lastTime += deltaTime

            judgements.add(
                CircleguardService.ScoreJudgement(
                    time = lastTime,
                    x = buffer.getVLQ() / 100.0,
                    y = buffer.getVLQ() / 100.0,
                    type = CircleguardService.JudgementType.entries[buffer.get().toInt()],
                    distance_center = buffer.getVLQ() / 100.0,
                    distance_edge = buffer.getVLQ() / 100.0,
                    error = buffer.getVLQ().toDouble()
                ))
        }

        return judgements
    }

}