package org.hso.texturesyncclient.controller.net

import com.google.gson.*
import javafx.scene.image.Image
import org.hso.texturesyncclient.model.Sha256
import org.hso.texturesyncclient.model.Texture
import java.io.*
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket
import java.util.*

@Suppress("MemberVisibilityCanBePrivate")
class Connection(val address: InetAddress, val port: Int = 10796) : Closeable {

    var socket: Socket? = null
    var output: DataOutputStream? = null
    var input: DataInputStream? = null

    @Throws(IOException::class)
    @Synchronized
    private fun getStreams(): Pair<DataInputStream, DataOutputStream> {
        val i: DataInputStream
        val o: DataOutputStream

        if (socket == null || !socket!!.isConnected) {
            val sock = Socket()
            sock.soTimeout = 10_000 /*ms*/
            sock.keepAlive = true
            sock.connect(InetSocketAddress(address, port), 1_000 /*ms*/)

            i = DataInputStream(BufferedInputStream(sock.getInputStream()))
            o = DataOutputStream(BufferedOutputStream(sock.getOutputStream()))

            input = i
            output = o
            socket = sock
        } else {
            i = input!!
            o = output!!
        }

        return Pair(i, o)
    }

    @Throws(IOException::class, ConnectionException::class)
    @Synchronized
    fun ping() {
        val io = getStreams()

        val obj = JsonObject()
        obj.add("ping", JsonObject())

        JsonPackage(obj).write(io.second)

        when (val pkg = Package.read(io.first)) {
            is JsonPackage -> return
            is BinaryPackage -> throw ConnectionUnexpectedPacketException()
            is ErrorPackage -> throw ConnectionErrorException(pkg)
            else -> throw RuntimeException("Unreachable")
        }
    }

    @Throws(IOException::class, ConnectionException::class)
    @Synchronized
    fun query(query: Array<String>): Array<Texture> {
        val io = getStreams()

        val obj = JsonObject()
        obj.add("query", {
            val inner = JsonObject()
            inner.add("query", {
                val array = JsonArray()
                for (queryString in query) {
                    array.add(queryString)
                }
                array
            }())
            inner
        }())

        JsonPackage(obj).write(io.second)

        when (val pkg = Package.read(io.first)) {
            is JsonPackage -> {
                try {
                    return Gson().fromJson<Array<InternalTexture>>(pkg.content, Array<InternalTexture>::class.java)
                        .map { tex ->
                            tex.toTexture()
                        }.toTypedArray()
                } catch (e: JsonSyntaxException) {
                    throw ConnectionInvalidJsonException()
                }
            }
            is BinaryPackage -> throw ConnectionUnexpectedPacketException()
            is ErrorPackage -> throw ConnectionErrorException(pkg)
            else -> throw RuntimeException("Unreachable")
        }
    }

    @Throws(IOException::class, ConnectionException::class)
    @Synchronized
    fun getTextureById(id: UUID): Texture? {
        val io = getStreams()

        val obj = JsonObject()
        obj.add("get_texture", {
            val inner = JsonObject()
            inner.addProperty("id", id.toString())
            inner
        }())

        JsonPackage(obj).write(io.second)

        when (val pkg = Package.read(io.first)) {
            is JsonPackage -> {
                return if (pkg.content.isJsonNull) {
                    null
                } else {
                    try {
                        Gson()
                            .fromJson<InternalTexture>(pkg.content, InternalTexture::class.java)
                            .toTexture()
                    } catch (e: JsonSyntaxException) {
                        throw ConnectionInvalidJsonException()
                    }
                }
            }
            is BinaryPackage -> throw ConnectionUnexpectedPacketException()
            is ErrorPackage -> throw ConnectionErrorException(pkg)
            else -> throw RuntimeException("Unreachable")
        }

    }

    @Throws(IOException::class, ConnectionException::class)
    @Synchronized
    fun getTextureByName(name: String): Texture? {
        val io = getStreams()

        val obj = JsonObject()
        obj.add("get_texture", {
            val inner = JsonObject()
            inner.addProperty("name", name)
            inner
        }())

        JsonPackage(obj).write(io.second)

        when (val pkg = Package.read(io.first)) {
            is JsonPackage -> {
                return if (pkg.content.isJsonNull) {
                    null
                } else {
                    try {
                        Gson()
                            .fromJson<InternalTexture>(pkg.content, InternalTexture::class.java)
                            .toTexture()
                    } catch (e: JsonSyntaxException) {
                        throw ConnectionInvalidJsonException()
                    }
                }
            }
            is BinaryPackage -> throw ConnectionUnexpectedPacketException()
            is ErrorPackage -> throw ConnectionErrorException(pkg)
            else -> throw RuntimeException("Unreachable")
        }

    }

    @Throws(IOException::class, ConnectionException::class)
    @Synchronized
    fun getTextureFile(hash: Sha256): ByteArray {
        val io = getStreams()

        val obj = JsonObject()
        obj.add("get_texture_file", {
            val inner = JsonObject()
            inner.addProperty("texture_hash", hash.toString())
            inner
        }())

        JsonPackage(obj).write(io.second)

        when (val pkg = Package.read(io.first)) {
            is JsonPackage -> throw ConnectionUnexpectedPacketException()
            is BinaryPackage -> return pkg.content
            is ErrorPackage -> throw ConnectionErrorException(pkg)
            else -> throw RuntimeException("Unreachable")
        }

    }

    @Throws(IOException::class, ConnectionException::class)
    @Synchronized
    fun getTexturePreview(hash: Sha256): Image {
        val io = getStreams()

        val obj = JsonObject()
        obj.add("get_texture_preview", {
            val inner = JsonObject()
            inner.addProperty("texture_hash", hash.toString())
            inner.addProperty("desired_format", "jpeg")
            inner
        }())

        JsonPackage(obj).write(io.second)

        when (val pkg = Package.read(io.first)) {
            is JsonPackage -> throw ConnectionUnexpectedPacketException()
            is BinaryPackage -> {
                return Image(ByteArrayInputStream(pkg.content))
            }
            is ErrorPackage -> throw ConnectionErrorException(pkg)
            else -> throw RuntimeException("Unreachable")
        }
    }

    @Throws(IOException::class, ConnectionException::class, IllegalArgumentException::class)
    @Synchronized
    private fun replaceTexture(old: Texture?, new: Texture?, image: ByteArray?) {
        val io = getStreams()

        val obj = JsonObject()
        obj.add("replace_texture", {
            val inner = JsonObject()
            if (old != null) {
                inner.add("old", Gson().toJsonTree(InternalTexture(old), InternalTexture::class.java))
            } else {
                inner.add("old", null)
            }
            if (new != null) {
                inner.add("new", Gson().toJsonTree(InternalTexture(new), InternalTexture::class.java))
            } else {
                inner.add("new", null)
            }
            inner
        }())

        JsonPackage(obj).write(io.second)

        when (val pkg = Package.read(io.first)) {
            is JsonPackage -> {
                if (pkg.content == JsonPrimitive(true)) {
                    // everthing is fine!
                    return
                } else if (image != null) {
                    // should be  { "get_texture_file": { texture_hash : <Hash> }}
                    // we don't check, since there is no good way to handle it.

                    BinaryPackage(image).write(io.second)
                    when (val ipkg = Package.read(io.first)) {
                        is JsonPackage -> {
                            if (ipkg.content != JsonPrimitive(true)) {
                                // Protokoll Assertion failed
                                throw ConnectionUnexpectedPacketException()
                            }
                        }
                        is BinaryPackage -> throw ConnectionUnexpectedPacketException()
                        is ErrorPackage -> throw ConnectionErrorException(ipkg)
                        else -> throw RuntimeException("Unreachable")
                    }
                } else {
                    ErrorPackage(404, "Texture not found!").write(io.second)
                    close() // gets re-opened on next request.
                    throw IllegalArgumentException("Image Argument was needed.")
                }
            }
            is BinaryPackage -> throw ConnectionUnexpectedPacketException()
            is ErrorPackage -> throw ConnectionErrorException(pkg)
            else -> throw RuntimeException("Unreachable")
        }
    }

    @Throws(IOException::class, ConnectionException::class, IllegalArgumentException::class)
    @Synchronized
    fun uploadTexture(texture: Texture, image: ByteArray) {
        if (texture.textureHash != Sha256(image)) {
            throw IllegalArgumentException("Sha256 of Image does not Match with Texture.")
        }

        replaceTexture(null, texture, image)
    }

    @Throws(IOException::class, ConnectionException::class, IllegalArgumentException::class)
    @Synchronized
    fun updateTexture(old: Texture, new: Texture, image: ByteArray) {
        if (new.textureHash != Sha256(image)) {
            throw IllegalArgumentException("Sha256 of Image does not Match with Texture.")
        }

        replaceTexture(old, new, image)
    }

    @Throws(IOException::class, ConnectionException::class, IllegalArgumentException::class)
    @Synchronized
    fun deleteTexture(texture: Texture) {
        replaceTexture(texture, null, null)
    }

    @Throws(IOException::class)
    @Synchronized
    override fun close() {
        if (output != null) {
            output!!.close()
            output = null
        }
        if (input != null) {
            input!!.close()
            input = null
        }
        if (socket != null) {
            socket!!.close()
            socket = null
        }
    }

}