package org.openrs2.game.net.js5

import com.google.common.util.concurrent.AbstractExecutionThreadService
import io.netty.buffer.ByteBufAllocator
import org.openrs2.buffer.use
import org.openrs2.cache.Js5Archive
import org.openrs2.cache.Js5Compression
import org.openrs2.cache.Js5CompressionType
import org.openrs2.cache.Js5MasterIndex
import org.openrs2.cache.Store
import org.openrs2.cache.VersionTrailer
import org.openrs2.protocol.js5.downstream.Js5Response
import org.openrs2.protocol.js5.upstream.Js5Request
import org.openrs2.util.collect.UniqueQueue
import java.io.FileNotFoundException
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
public class Js5Service @Inject constructor(
    private val store: Store,
    private val masterIndex: Js5MasterIndex,
    private val alloc: ByteBufAllocator
) : AbstractExecutionThreadService() {
    private val lock = Object()
    private val clients = UniqueQueue<Js5Client>()

    override fun run() {
        while (true) {
            var client: Js5Client
            var request: Js5Request.Group

            synchronized(lock) {
                while (true) {
                    if (!isRunning) {
                        return
                    }

                    val next = clients.removeFirstOrNull()
                    if (next == null) {
                        lock.wait()
                        continue
                    }

                    client = next
                    request = client.pop() ?: continue
                    break
                }
            }

            serve(client, request)
        }
    }

    private fun serve(client: Js5Client, request: Js5Request.Group) {
        val ctx = client.ctx
        if (!ctx.channel().isActive) {
            return
        }

        val buf = if (request.archive == Js5Archive.ARCHIVESET && request.group == Js5Archive.ARCHIVESET) {
            alloc.buffer().use { uncompressed ->
                masterIndex.write(uncompressed)

                Js5Compression.compress(uncompressed, Js5CompressionType.UNCOMPRESSED)
            }
        } else {
            try {
                store.read(request.archive, request.group).use { buf ->
                    if (request.archive != Js5Archive.ARCHIVESET) {
                        VersionTrailer.strip(buf)
                    }

                    buf.retain()
                }
            } catch (ex: FileNotFoundException) {
                ctx.close()
                return
            }
        }

        val response = Js5Response(request.prefetch, request.archive, request.group, buf)
        ctx.writeAndFlush(response, ctx.voidPromise())

        synchronized(lock) {
            if (client.isReady()) {
                clients.add(client)
            }

            if (client.isNotFull()) {
                ctx.read()
            }
        }
    }

    public fun push(client: Js5Client, request: Js5Request.Group) {
        synchronized(lock) {
            client.push(request)

            if (client.isReady()) {
                clients.add(client)
                lock.notifyAll()
            }

            if (client.isNotFull()) {
                client.ctx.read()
            }
        }
    }

    public fun readIfNotFull(client: Js5Client) {
        synchronized(lock) {
            if (client.isNotFull()) {
                client.ctx.read()
            }
        }
    }

    public fun notifyIfNotEmpty(client: Js5Client) {
        synchronized(lock) {
            if (client.isNotEmpty()) {
                lock.notifyAll()
            }
        }
    }

    override fun triggerShutdown() {
        synchronized(lock) {
            lock.notifyAll()
        }
    }
}
