package de.arindy.dicetower

import io.quarkus.runtime.annotations.RegisterForReflection
import jakarta.enterprise.context.ApplicationScoped
import jakarta.ws.rs.Consumes
import jakarta.ws.rs.GET
import jakarta.ws.rs.POST
import jakarta.ws.rs.Path
import jakarta.ws.rs.PathParam
import jakarta.ws.rs.Produces
import jakarta.ws.rs.core.Context
import jakarta.ws.rs.core.MediaType
import jakarta.ws.rs.sse.OutboundSseEvent
import jakarta.ws.rs.sse.Sse
import jakarta.ws.rs.sse.SseBroadcaster
import jakarta.ws.rs.sse.SseEventSink
import org.eclipse.microprofile.config.inject.ConfigProperty
import java.util.Optional
import java.util.UUID

private const val LIMIT = 30

@Path("/dice/{id}")
@ApplicationScoped
final class DiceResource(@Context val sse: Sse) {

    @ConfigProperty(name = "dice.limit")
    private lateinit var diceLimit: Optional<Int>

    private var eventBuilder: OutboundSseEvent.Builder = sse.newEventBuilder()
    private var sseBroadcasters: MutableMap<String, SseBroadcaster> = HashMap()

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    fun parseCommand(@PathParam("id") id: String, data: RollPayload) {
        data.room = id.split(":")[0]
        data.user = id.split(":")[1]
        val results = ArrayList<Results>()
        if (data.results == null) {
            var numberOfDice = 0
            data.command.split(" ", "&", "and").filter { it.isNotEmpty() }.map { it.trim() }.toTypedArray<String>().forEach { command ->
                val dice = command.split("d")
                var amount = dice[0].toInt()
                val limit = diceLimit.orElse(LIMIT)
                if (limit < numberOfDice + amount) {
                    amount = limit - numberOfDice
                }
                numberOfDice += amount
                if (amount > 0) {
                    val result = IntArray(amount)
                    val sides = dice[1].split("+", "-")
                    val modifier = if (dice[1].contains("+")) sides[1].toInt() else if (dice[1].contains("+")) -1 * sides[1].toInt() else 0
                    repeat(amount) { index ->
                        result[index] = (Math.random() * sides[0].toInt() + 1).toInt()
                    }
                    results.add(Results(sides[0].toInt(), modifier, result.sum() + modifier, result.map { Roll(it) }.toTypedArray()))
                }
            }
        } else {
            results.addAll(data.results)
        }

        val map = results.map { r ->
            "${r.rolls.size}d${r.sides}@${
                if (r.sides == 100) r.rolls.map { roll -> Roll(roll.value / 10 * 10) }
                    .joinToString(",") else r.rolls.joinToString(",")
            }"
        }.toTypedArray()

        data.roll = map + results.filter { it.sides == 100 }.map { r -> "${r.rolls.size}d10@${r.rolls.map { roll -> Roll(roll.value % 10) }.joinToString(",")}"}.toTypedArray()
        if (data.roll.all { it.trim().isNotEmpty() }) {
            results(data.room!!, Result(data.name, data.user!!, data.faceColor, null))
        }
        sseBroadcasters[id]?.broadcast(
            eventBuilder.id((UUID.randomUUID()).toString())
                .mediaType(MediaType.APPLICATION_JSON_TYPE).data(data).build()
        )
        Thread.sleep(1000)
        results(data.room!!, Result(data.name, data.user!!, data.faceColor, results.toTypedArray()))
    }

    @POST
    @Path("/register")
    @Consumes(MediaType.APPLICATION_JSON)
    fun register(@PathParam("id") id: String, data: Any) {
        sseBroadcasters["register:$id"]?.broadcast(
            eventBuilder.id((UUID.randomUUID()).toString())
                .mediaType(MediaType.APPLICATION_JSON_TYPE).data(data).build())

    }

    @POST
    @Path("/results")
    @Consumes(MediaType.APPLICATION_JSON)
    fun results(@PathParam("id") id: String, data: Any) {
        sseBroadcasters[id]?.broadcast(
            eventBuilder.id((UUID.randomUUID()).toString())
                .mediaType(MediaType.APPLICATION_JSON_TYPE).data(data).build()
        )
    }

    @GET
    @Path("/stream")
    @Produces(MediaType.SERVER_SENT_EVENTS)
    fun stream(@PathParam("id") id: String, @Context sseEventSink: SseEventSink) {
        if (!sseBroadcasters.containsKey(id)) {
            sseBroadcasters[id] = sse.newBroadcaster()
        }
        sseBroadcasters[id]?.register(sseEventSink)
    }

    @GET
    @Path("/results")
    @Produces(MediaType.SERVER_SENT_EVENTS)
    fun results(@PathParam("id") id: String, @Context sseEventSink: SseEventSink) {
        if (!sseBroadcasters.containsKey(id)) {
            sseBroadcasters[id] = sse.newBroadcaster()
        }
        sseBroadcasters[id]?.register(sseEventSink)
    }

    @GET
    @Path("/users")
    @Produces(MediaType.SERVER_SENT_EVENTS)
    fun users(@PathParam("id") id: String, @Context sseEventSink: SseEventSink) {
        if (!sseBroadcasters.containsKey("register:$id")) {
            sseBroadcasters["register:$id"] = sse.newBroadcaster()
        }
        sseBroadcasters["register:$id"]?.register(sseEventSink)
    }

    @RegisterForReflection
    data class Result(val name: String, val user: String, val faceColor: String, val results: Array<Results>?) {
        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false

            other as Result

            if (name != other.name) return false
            if (user != other.user) return false
            if (faceColor != other.faceColor) return false
            if (results != null) {
                if (other.results == null) return false
                if (!results.contentEquals(other.results)) return false
            } else if (other.results != null) return false

            return true
        }

        override fun hashCode(): Int {
            var result = name.hashCode()
            result = 31 * result + user.hashCode()
            result = 31 * result + faceColor.hashCode()
            result = 31 * result + (results?.contentHashCode() ?: 0)
            return result
        }
    }

    @RegisterForReflection
    data class Results(val sides: Int, val modifier: Int, val value: Int, val rolls: Array<Roll>) {
        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false

            other as Results

            if (sides != other.sides) return false
            if (modifier != other.modifier) return false
            if (value != other.value) return false
            if (!rolls.contentEquals(other.rolls)) return false

            return true
        }

        override fun hashCode(): Int {
            var result = sides
            result = 31 * result + modifier
            result = 31 * result + value
            result = 31 * result + rolls.contentHashCode()
            return result
        }
    }

    @RegisterForReflection
    data class Roll(val value: Int)

}
