package org.hso.texturesyncclient.controller.net

import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import com.google.gson.JsonParser
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.IOException

internal abstract class Package {

    @Throws(IOException::class)
    abstract fun write(out: DataOutputStream)

    companion object {

        const val TYPE_ERROR: Byte = 0
        protected const val TYPE_ERROR_MAX_PAYLOAD: Int = 1024

        const val TYPE_JSON: Byte = 1
        protected const val TYPE_JSON_MAX_PAYLOAD: Int = 16 * 1024 * 1024

        const val TYPE_BINARY: Byte = 2
        protected const val TYPE_BINARY_MAX_PAYLOAD: Int = 512 * 1024 * 1024

        @Throws(PacketException::class, IOException::class)
        fun read(input: DataInputStream): Package {
            // Type Byte
            val type = input.readByte()

            // 3 Reserved Bytes
            input.readByte()
            input.readByte()
            input.readByte()

            // 4 Len Bytes
            val length = input.readInt()

            when (type) {
                TYPE_ERROR -> if (length > TYPE_ERROR_MAX_PAYLOAD) {
                    throw PacketTooLongException()
                }

                TYPE_JSON -> if (length > TYPE_JSON_MAX_PAYLOAD) {
                    throw PacketTooLongException()
                }

                TYPE_BINARY -> if (length > TYPE_BINARY_MAX_PAYLOAD) {
                    throw PacketTooLongException()
                }

                else -> throw PacketInvalidType()
            }

            val payload = ByteArray(length)
            input.readFully(payload)

            when (type) {
                TYPE_ERROR -> {
                    val msg = String(payload)
                    val results = msg.split(' ', ignoreCase = false, limit = 2)

                    return if (results.size == 2) {
                        val code = results[0].toIntOrNull()

                        if (code == null) {
                            ErrorPackage(0, msg)
                        } else {
                            ErrorPackage(code, results[1])
                        }
                    } else {
                        ErrorPackage(0, msg)
                    }
                }

                TYPE_JSON -> {
                    try {
                        val obj = JsonParser().parse(String(payload))

                        return JsonPackage(obj)
                    } catch (e: JsonParseException) {
                        throw PacketInvalidData()
                    }
                }

                TYPE_BINARY -> {
                    return BinaryPackage(payload)
                }

                else -> {
                    // Unreachable
                    throw PacketInvalidType()
                }
            }
        }
    }

}

internal data class JsonPackage(val content: JsonElement) : Package() {
    override fun write(out: DataOutputStream) {
        val payload = content.toString().toByteArray()
        // Tag Byte
        out.writeByte(TYPE_JSON.toInt())

        // 3 Reserved Bytes
        out.writeByte(0x42)
        out.writeByte(0x42)
        out.writeByte(0x42)

        // Length of Payload
        out.writeInt(payload.size)

        // Payload
        out.write(payload)

        out.flush()
    }
}

@Suppress("ArrayInDataClass")
internal data class BinaryPackage(val content: ByteArray) : Package() {
    override fun write(out: DataOutputStream) {
        // Tag Byte
        out.writeByte(TYPE_BINARY.toInt())

        // 3 Reserved Bytes
        out.writeByte(0x42)
        out.writeByte(0x42)
        out.writeByte(0x42)

        // Length of Payload
        out.writeInt(content.size)

        // Payload
        out.write(content)

        out.flush()
    }
}

internal data class ErrorPackage(val code: Int, val message: String) : Package() {
    override fun write(out: DataOutputStream) {
        val payload = "$code $message".toByteArray()
        // Tag Byte
        out.writeByte(TYPE_ERROR.toInt())

        // 3 Reserved Bytes
        out.writeByte(0x42)
        out.writeByte(0x42)
        out.writeByte(0x42)

        // Length of Payload
        out.writeInt(payload.size)

        // Payload
        out.write(payload)

        out.flush()
    }
}