/*
 * 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/>.
 */
@file:Suppress("WildcardImport")

package net.ccbluex.liquidbounce.features.module.modules.movement.autododge

import net.ccbluex.fastutil.mapToArray
import net.ccbluex.liquidbounce.config.types.group.ToggleableValueGroup
import net.ccbluex.liquidbounce.config.types.list.Tagged
import net.ccbluex.liquidbounce.event.events.MovementInputEvent
import net.ccbluex.liquidbounce.event.handler
import net.ccbluex.liquidbounce.event.once
import net.ccbluex.liquidbounce.features.blink.BlinkManager
import net.ccbluex.liquidbounce.features.module.ClientModule
import net.ccbluex.liquidbounce.features.module.ModuleCategories
import net.ccbluex.liquidbounce.features.module.modules.player.ModuleBlink
import net.ccbluex.liquidbounce.features.module.modules.render.murdermystery.ModuleMurderMystery
import net.ccbluex.liquidbounce.features.module.modules.world.scaffold.ModuleScaffold
import net.ccbluex.liquidbounce.utils.client.Timer
import net.ccbluex.liquidbounce.utils.entity.CachedPlayerSimulation
import net.ccbluex.liquidbounce.utils.entity.PlayerSimulation
import net.ccbluex.liquidbounce.utils.entity.PlayerSimulationCache
import net.ccbluex.liquidbounce.utils.entity.SimulatedArrow
import net.ccbluex.liquidbounce.utils.inventory.InventoryManager
import net.ccbluex.liquidbounce.utils.kotlin.Priority
import net.minecraft.client.gui.screens.inventory.ContainerScreen
import net.minecraft.client.multiplayer.ClientLevel
import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.projectile.arrow.Arrow
import net.minecraft.world.entity.projectile.arrow.SpectralArrow
import net.minecraft.world.entity.projectile.arrow.ThrownTrident
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3

object ModuleAutoDodge : ClientModule("AutoDodge", ModuleCategories.COMBAT) {
    private const val MIN_PACKET_DISTANCE = 0.9
    private const val MIN_PACKET_DISTANCE_SQ = MIN_PACKET_DISTANCE * MIN_PACKET_DISTANCE

    private object AllowRotationChange : ToggleableValueGroup(this, "AllowRotationChange", false) {
        val allowJump by boolean("AllowJump", true)
    }

    private object AllowTimer : ToggleableValueGroup(this, "AllowTimer", false) {
        val timerSpeed by float("TimerSpeed", 2.0F, 1.0F..10.0F, suffix = "x")
    }

    private val ignore by multiEnumChoice("Ignore", Ignore.entries)

    init {
        tree(AllowRotationChange)
        tree(AllowTimer)
    }

    override val running: Boolean get() =
        super.running
           // We aren't where we are because of blink. So this module shall not cause any disturbance in that case.
        && !ModuleBlink.running
        && !ModuleMurderMystery.disallowsArrowDodge()
        && !(Ignore.OPEN_INVENTORY !in ignore
            && (InventoryManager.isInventoryOpen || mc.gui.screen() is ContainerScreen))
        && !(Ignore.USING_ITEM !in ignore && player.isUsingItem)
        && !(Ignore.USING_SCAFFOLD !in ignore && ModuleScaffold.running)

    @Suppress("unused")
    val tickRep = handler<MovementInputEvent> { event ->
        val arrows = world.findFlyingArrows()

        val simulatedPlayer = CachedPlayerSimulation(PlayerSimulationCache.getSimulationForLocalPlayer())

        val inflictedHit =
            getInflictedHits(simulatedPlayer, arrows, hitboxExpansion = DodgePlanner.SAFE_DISTANCE_WITH_PADDING)
                ?: return@handler

        val dodgePlan =
            planEvasion(DodgePlannerConfig(allowRotations = AllowRotationChange.enabled), inflictedHit)
                ?: return@handler

        event.directionalInput = dodgePlan.directionalInput

        dodgePlan.yawChange?.let { yawChange ->
            player.yRot = yawChange
        }

        if (dodgePlan.shouldJump && AllowRotationChange.allowJump && player.onGround()) {
            once<MovementInputEvent> { movementInputEvent ->
                movementInputEvent.jump = true
            }
        }

        if (AllowTimer.enabled && dodgePlan.useTimer) {
            Timer.requestTimerSpeed(AllowTimer.timerSpeed, Priority.IMPORTANT_FOR_PLAYER_LIFE, this@ModuleAutoDodge)
        }
    }

    private fun ClientLevel.findFlyingArrows() = entitiesForRendering().filter { entity ->
        (entity is Arrow || entity is SpectralArrow ||
                (entity is ThrownTrident && entity.clientSideReturnTridentTickCount == 0)) && !entity.isInGround
    }

    private fun <T : PlayerSimulation> getInflictedHits(
        simulatedPlayer: T,
        arrows: List<Entity>,
        maxTicks: Int = 80,
        hitboxExpansion: Double = 0.7,
    ): HitInfo? {
        val simulatedArrows = arrows.mapToArray {
            SimulatedArrow(it.level(), it.position(), it.deltaMovement, false)
        }

        for (i in 0 until maxTicks) {
            simulatedPlayer.tick()

            simulatedArrows.forEachIndexed { arrowIndex, arrow ->
                if (arrow.inGround) {
                    return@forEachIndexed
                }

                val lastPos = arrow.pos

                arrow.tick()

                val playerHitBox =
                    AABB(-0.3, 0.0, -0.3, 0.3, 1.8, 0.3)
                        .inflate(hitboxExpansion)
                        .move(simulatedPlayer.pos)
                val raycastResult = playerHitBox.clip(lastPos, arrow.pos)

                raycastResult.orElse(null)?.let { hitPos ->
                    return HitInfo(i, arrows[arrowIndex], hitPos, lastPos, arrow.velocity)
                }
            }
        }

        return null
    }

    data class EvadingPacket(
        val idx: Int,
        /**
         * Ticks until impact. Null if evaded
         */
        val ticksToImpact: Int?
    )

    /**
     * Returns the index of the first position packet that avoids all arrows in the next X seconds
     */
    @Suppress("ReturnCount")
    fun findAvoidingArrowPosition(): EvadingPacket? {
        var packetIndex = 0

        var lastPosition: Vec3? = null

        var bestPacketPosition: Vec3? = null
        var bestPacketIdx: Int? = null
        var bestTimeToImpact = 0

        for (position in BlinkManager.positions) {
            packetIndex += 1

            // Process packets only if they are at least some distance away from each other
            if (lastPosition != null) {
                if (lastPosition.distanceToSqr(position) < MIN_PACKET_DISTANCE_SQ) {
                    continue
                }
            }

            lastPosition = position

            val inflictedHit = getInflictedHit(position)

            if (inflictedHit == null) {
                return EvadingPacket(packetIndex - 1, null)
            } else if (inflictedHit.tickDelta > bestTimeToImpact) {
                bestTimeToImpact = inflictedHit.tickDelta
                bestPacketIdx = packetIndex - 1
                bestPacketPosition = position
            }
        }

        // If the evading packet is less than one player hitbox away from the current position, we should rather
        // call the evasion a failure
        if (bestPacketIdx != null && bestPacketPosition!!.distanceToSqr(player.position()) > MIN_PACKET_DISTANCE_SQ) {
            return EvadingPacket(bestPacketIdx, bestTimeToImpact)
        }

        return null
    }

    fun getInflictedHit(pos: Vec3): HitInfo? {
        val arrows = world.findFlyingArrows()
        val playerSimulation = PlayerSimulation.Rigid(pos)

        return getInflictedHits(playerSimulation, arrows, maxTicks = 40)
    }

    data class HitInfo(
        val tickDelta: Int,
        val arrowEntity: Entity,
        val hitPos: Vec3,
        val prevArrowPos: Vec3,
        val arrowVelocity: Vec3,
    )

    private enum class Ignore(
        override val tag: String
    ) : Tagged {
        OPEN_INVENTORY("OpenInventory"),
        USING_ITEM("UsingItem"),
        USING_SCAFFOLD("UsingScaffold")
    }
}
