package barix

import codes.Somecodes
import com.fasterxml.jackson.databind.JsonNode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.tinylog.Logger
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetSocketAddress
import java.net.Socket
import java.nio.ByteBuffer
import java.util.function.Consumer

@Suppress("unused")
class BarixConnection(val index: UInt, var channel: String, val ipaddress: String, val port: Int = 5002) {
    private var _bR: Int = 0
    private var _sd: Int = 0
    private var _vu: Int = 0
    private var _onlinecounter = 0
    private val inet = InetSocketAddress(ipaddress, port)
    private val maxUDPsize = 1000
    private var _tcp: Socket? = null


    /**
     * Buffer remain in bytes
     */
    var bufferRemain: Int
        get() = _bR
        set(value) {
            _bR = value
            _onlinecounter = 5
        }

    /**
     * Status data, 0 = playback idle, 1 = playback running
     */
    var statusData: Int
        get() = _sd
        set(value) {
            _sd = if (value < 0) 0 else if (value > 1) 1 else value
            _onlinecounter = 5
        }

    /**
     * VU level 0-100
     */
    var vu: Int
        get() = _vu
        set(value) {
            _vu = if (value < 0) 0 else if (value > 100) 100 else value
            _onlinecounter = 5
        }

    /**
     * TCP command socket for communication with this Barix device
     */
    var commandsocket: Socket?
        get()  = _tcp
        set(value){
            _tcp = value
        }

    /**
     * Decrement online counter, if counter reaches 0, the device is considered offline
     */
    fun decrementOnlineCounter() {
        if (_onlinecounter > 0) {
            _onlinecounter--
        }
    }

    /**
     * Check if Barix device is online
     * @return true if online
     */
    fun isOnline(): Boolean {
        return _onlinecounter > 0
    }

    /**
     * Check if Barix device is idle (not playing)
     * @return true if idle
     */
    fun isIdle() : Boolean{
        return statusData == 0
    }

    /**
     * Check if Barix device is playing
     * @return true if playing
     */
    fun isPlaying() : Boolean{
        return statusData == 1
    }

    /**
     * Send data to Barix device via UDP
     * @param data The data to send
     */
    fun SendData(data: ByteArray, cbOK: Consumer<String>, cbFail: Consumer<String>) {
        if (data.isNotEmpty()) {
            CoroutineScope(Dispatchers.IO).launch {
                DatagramSocket().use{ udp ->
                    val bb = ByteBuffer.wrap(data)
                    while(bb.hasRemaining()){
                        try {
                            val chunk = ByteArray(if (bb.remaining() > maxUDPsize) maxUDPsize else bb.remaining())
                            bb.get(chunk)
                            //println("Buffer remain: $bufferRemain, sending chunk size: ${chunk.size}")
                            while(bufferRemain<chunk.size){
                                delay(10)
                                //println("Waiting until buffer enough..")
                            }
                            udp.send(DatagramPacket(chunk, chunk.size, inet))
                            delay(2)
                        } catch (e: Exception) {
                            cbFail.accept("SendData to $ipaddress:$port failed, message: ${e.message}")
                            return@launch
                        }
                    }
                    cbOK.accept("SendData to $channel ($ipaddress:$port) succeeded, ${data.size} bytes sent")
                }
            }

        } else cbFail.accept("SendData to $ipaddress:$port failed, data is empty")

    }

    /**
     * Convert BarixConnection to JsonNode
     * @return JsonNode representation of BarixConnection
     */
    fun toJsonNode(): JsonNode {
        // make json node from index, channel, ipaddress, port, bufferRemain, statusData, vu
        return Somecodes.objectmapper.createObjectNode().apply {
            put("index", index.toInt())
            put("channel", channel)
            put("ipaddress", ipaddress)
            put("port", port)
            put("bufferRemain", bufferRemain)
            put("statusData", statusData)
            put("vu", vu)
            put("isOnline", isOnline())
        }
    }

    /**
     * Convert BarixConnection to JSON string
     * @return JSON string representation of BarixConnection
     */
    fun toJsonString(): String {
        return Somecodes.toJsonString(toJsonNode())
    }

    /**
     * Activate relay on Barix device
     * @param relays The relay numbers to activate (1-8)
     * @return true if successful
     */
    fun ActivateRelay(vararg relays: Int){
        val command = StringBuilder("RELAY;")
        var binary = 0
        relays.forEach {
            if (it in 1..8) {
                binary = binary or (1 shl (it - 1))
            }
        }
        command.append(binary.toString()).append("@")
        SendCommand(command.toString())
    }

    fun ActivateRelay(relays: List<Int>){
        val command = StringBuilder("RELAY;")
        var binary = 0
        relays.forEach {
            if (it in 1..8) {
                binary = binary or (1 shl (it - 1))
            }
        }
        command.append(binary.toString()).append("@")
        SendCommand(command.toString())
    }

    /**
     * Deactivate relay on Barix device
     */
    fun DeactivateRelay(){
        SendCommand("RELAY;0@")
    }

    /**
     * Send command to Barix device
     * @param command The command to send
     * @return true if successful
     */
    fun SendCommand(command: String): Boolean {
        try {
            if (_tcp!=null){
                if (!_tcp!!.isClosed){
                    val bb = command.toByteArray()
                    val size = bb.size + 4
                    val b4 = byteArrayOf(
                        (size shr 24 and 0xFF).toByte(),
                        (size shr 16 and 0xFF).toByte(),
                        (size shr 8 and 0xFF).toByte(),
                        (size and 0xFF).toByte()
                    )
                    val out = _tcp!!.getOutputStream()
                    out.write(b4)
                    out.write(bb)
                    out.flush()
                    Logger.info { "SendCommand to $ipaddress : $command" }
                    return true
                }else {
                    Logger.error { "Socket to $ipaddress is not connected" }
                }
            } else {
                Logger.error { "Socket to $ipaddress is null" }
            }

        } catch (e: Exception) {
            Logger.error { "Failed to SendCommand to $ipaddress, Message : ${e.message}" }
        }
        return false
    }
}