/*
 * This file is part of LiquidBounce (https://github.com/CCBlueX/LiquidBounce)
 *
 * Copyright (c) 2015 - 2026 CCBlueX
 *
 * LiquidBounce is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * LiquidBounce is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with LiquidBounce. If not, see <https://www.gnu.org/licenses/>.
 */
package net.ccbluex.liquidbounce.utils.render.placement

import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.longs.LongArrayList
import net.ccbluex.fastutil.fastIterator
import net.ccbluex.liquidbounce.event.events.WorldRenderEvent
import net.ccbluex.liquidbounce.render.EMPTY_BOX
import net.ccbluex.liquidbounce.render.FULL_BOX
import net.ccbluex.liquidbounce.render.drawBox
import net.ccbluex.liquidbounce.render.renderEnvironment
import net.ccbluex.liquidbounce.render.withPositionRelativeToCamera
import net.ccbluex.liquidbounce.utils.math.expandToBoundingBox
import net.ccbluex.liquidbounce.utils.math.high32
import net.ccbluex.liquidbounce.utils.math.iterator
import net.ccbluex.liquidbounce.utils.math.low32
import net.minecraft.core.BlockPos
import net.minecraft.util.Mth
import net.minecraft.world.phys.AABB

/**
 * A renderer instance that can be added to a [PlacementRenderer], it contains the core logic.
 * Culling is handled in each handler for its boxes individually.
 *
 * This class is not thread-safe. You can use it on the render thread. (the most recommended way)
 */
@Suppress("TooManyFunctions")
class PlacementRenderHandler(private val placementRenderer: PlacementRenderer, val id: Int = 0) : BlockCuller.Owner {

    private val inList = Long2ObjectOpenHashMap<InOutBlockData>()
    private val currentList = Long2ObjectOpenHashMap<CurrentBlockData>()
    private val outList = Long2ObjectOpenHashMap<InOutBlockData>()

    private val culler = BlockCuller(this)

    @JvmRecord
    private data class InOutBlockData(val startTime: Long, val cullData: Long, val box: AABB) {
        fun toCurrent() = CurrentBlockData(cullData, box)
    }

    @JvmRecord
    private data class CurrentBlockData(val cullData: Long, val box: AABB) {
        fun toInOut(startTime: Long) = InOutBlockData(startTime, cullData, box)
    }

    private val blockPosCache = BlockPos.MutableBlockPos()
    private val blockPosCacheList = LongArrayList()

    fun render(event: WorldRenderEvent, time: Long) {
        with(placementRenderer) {
            val color = getColor(id)
            val outlineColor = getOutlineColor(id)

            event.renderEnvironment {
                fun drawEntryBox(blockPos: Long, cullData: Long, box: AABB, colorFactor: Float) {
                    withPositionRelativeToCamera(blockPosCache.set(blockPos)) {
                        drawBox(
                            box,
                            color.fade(colorFactor),
                            outlineColor.fade(colorFactor),
                            cullData.high32(),
                            cullData.low32(),
                        )
                    }
                }

                inList.long2ObjectEntrySet().removeIf { entry ->
                    // Do not use destructuring declaration which returns boxed [Long] values
                    val pos = entry.longKey
                    val value = entry.value

                    val sizeFactor = startSizeCurve.getFactor(value.startTime, time, inTime.toFloat())
                    val expand = Mth.lerp(sizeFactor, startSize, 1f)
                    val box = getBox(if (expand < 1f) 1f - expand else expand, value.box)
                    val colorFactor = fadeInCurve.getFactor(value.startTime, time, inTime.toFloat())

                    drawEntryBox(pos, value.cullData, box, colorFactor)

                    if (time - value.startTime >= inTime) {
                        if (keep) {
                            currentList.put(pos, value.toCurrent())
                        } else {
                            outList.put(pos, value.copy(startTime = time))
                        }
                        true
                    } else {
                        false
                    }
                }

                currentList.fastIterator().forEach { entry ->
                    // Do not use destructuring declaration which returns boxed [Long] values
                    val pos = entry.longKey
                    val value = entry.value
                    drawEntryBox(pos, value.cullData, value.box, 1f)
                }

                blockPosCacheList.clear()
                outList.long2ObjectEntrySet().removeIf { entry ->
                    // Do not use destructuring declaration which returns boxed [Long] values
                    val pos = entry.longKey
                    val value = entry.value

                    val sizeFactor = endSizeCurve.getFactor(value.startTime, time, outTime.toFloat())
                    val expand = 1f - Mth.lerp(sizeFactor, 1f, endSize)
                    val box = getBox(expand, value.box)
                    val colorFactor = 1f - fadeOutCurve.getFactor(value.startTime, time, outTime.toFloat())

                    drawEntryBox(pos, value.cullData, box, colorFactor)

                    if (time - value.startTime >= outTime) {
                        blockPosCacheList.add(pos)
                        true
                    } else {
                        false
                    }
                }

                for (i in blockPosCacheList.indices) {
                    updateNeighbors(blockPosCache.set(blockPosCacheList.getLong(i)))
                }

            }
        }
    }

    private fun getBox(expand: Float, box: AABB): AABB {
        return when (expand) {
            1f -> box
            0f -> EMPTY_BOX
            else -> {
                val f = if (expand < 1) -0.5 * expand else (expand - 1) * 0.5
                box.inflate(box.xsize * f, box.ysize * f, box.zsize * f)
            }
        }
    }

    fun isFinished(): Boolean = outList.isEmpty()

    /**
     * Updates the culling of all blocks around a position that has been removed or added.
     */
    private fun updateNeighbors(pos: BlockPos) {
        if (!placementRenderer.clump) {
            return
        }

        for (mutable in pos.expandToBoundingBox(1, 1, 1)) {
            val longValue = mutable.asLong()

            val inValue = inList[longValue]
            if (inValue != null) {
                inList.put(longValue, inValue.copy(cullData = this.culler.getCullData(longValue)))
                continue
            }

            val currentValue = currentList[longValue]
            if (currentValue != null) {
                currentList.put(longValue, currentValue.copy(cullData = this.culler.getCullData(longValue)))
                continue
            }
        }
    }

    /**
     * Checks whether the position (in long value) is rendered.
     */
    override operator fun contains(pos: Long): Boolean {
        return inList.containsKey(pos) || currentList.containsKey(pos) || outList.containsKey(pos)
    }

    /**
     * Adds a block to be rendered. First it will make an appear-animation, then
     * it will continue to get rendered until it's removed or the world changes.
     *
     * @param pos The position, can be [BlockPos.MutableBlockPos].
     */
    fun addBlock(pos: BlockPos, update: Boolean = true, box: AABB = FULL_BOX) {
        val longValue = pos.asLong()
        if (!currentList.containsKey(longValue) && !inList.containsKey(longValue)) {
            inList.put(longValue, InOutBlockData(System.currentTimeMillis(), -1L, box))
            if (update) {
                updateNeighbors(pos)
            }
        }

        outList.remove(longValue)
    }

    /**
     * Removes a block from the rendering, it will get an out animation tho.
     *
     * @param pos The position, can be [BlockPos.MutableBlockPos].
     */
    fun removeBlock(pos: BlockPos) {
        val longValue = pos.asLong()
        var cullData = 0L
        var box: AABB? = null

        currentList.remove(longValue)?.let {
            cullData = it.cullData
            box = it.box
        } ?: run {
            inList.remove(longValue)?.let {
                cullData = it.cullData
                box = it.box
            } ?: return
        }

        outList.put(longValue, InOutBlockData(System.currentTimeMillis(), cullData, box!!))
    }

    /**
     * Updates all culling data.
     *
     * This can be useful to reduce overhead when adding a bunch of positions,
     * so that positions don't get updated multiple times.
     */
    fun updateAll() {
        inList.fastIterator().forEach { entry ->
            val key = entry.longKey
            val value = entry.value
            entry.setValue(value.copy(cullData = this.culler.getCullData(key)))
        }

        currentList.fastIterator().forEach { entry ->
            val key = entry.longKey
            val value = entry.value
            entry.setValue(value.copy(cullData = this.culler.getCullData(key)))
        }
    }

    /**
     * Updates the box of [pos] to [box].
     *
     * This method won't affect positions that are in the state of fading out.
     */
    fun updateBox(pos: BlockPos, box: AABB) {
        val longValue = pos.asLong()

        inList[longValue]?.let {
            inList.put(longValue, it.copy(box = box))
        }

        currentList[longValue]?.let {
            currentList.put(longValue, it.copy(box = box))
        }
    }

    /**
     * Puts all currently rendered positions in the out-animation state and keeps it being rendered until
     * all animations have been finished even though the module might be already disabled.
     */
    fun clearSilently() {
        val startTime = System.currentTimeMillis()
        inList.fastIterator().forEach { entry ->
            val pos = entry.longKey
            val value = entry.value
            outList.put(pos, value.copy(startTime = startTime))
        }
        inList.clear()

        currentList.fastIterator().forEach { entry ->
            val pos = entry.longKey
            val value = entry.value
            outList.put(pos, value.toInOut(startTime = startTime))
        }
        currentList.clear()
    }

    /**
     * Removes all stored positions.
     */
    fun clear() {
        inList.clear()
        currentList.clear()
        outList.clear()
    }

}
