/*
 * 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("detekt:TooManyFunctions")

package net.ccbluex.liquidbounce.features.module.modules.render

import com.mojang.blaze3d.platform.InputConstants
import net.ccbluex.fastutil.enumSetOf
import net.ccbluex.liquidbounce.config.types.group.ToggleableValueGroup
import net.ccbluex.liquidbounce.config.types.list.Tagged
import net.ccbluex.liquidbounce.event.Event
import net.ccbluex.liquidbounce.event.EventManager
import net.ccbluex.liquidbounce.event.events.GameTickEvent
import net.ccbluex.liquidbounce.event.events.HealthUpdateEvent
import net.ccbluex.liquidbounce.event.events.MouseButtonEvent
import net.ccbluex.liquidbounce.event.events.MovementInputEvent
import net.ccbluex.liquidbounce.event.events.PacketEvent
import net.ccbluex.liquidbounce.event.events.PerspectiveEvent
import net.ccbluex.liquidbounce.event.events.PlayerMoveEvent
import net.ccbluex.liquidbounce.event.events.PlayerTickEvent
import net.ccbluex.liquidbounce.event.events.RotationUpdateEvent
import net.ccbluex.liquidbounce.event.events.WorldChangeEvent
import net.ccbluex.liquidbounce.event.handler
import net.ccbluex.liquidbounce.event.newEventHook
import net.ccbluex.liquidbounce.features.module.ClientModule
import net.ccbluex.liquidbounce.features.module.ModuleCategories
import net.ccbluex.liquidbounce.features.module.modules.movement.inventorymove.ModuleInventoryMove.shouldHandleInputs
import net.ccbluex.liquidbounce.utils.aiming.RotationManager
import net.ccbluex.liquidbounce.utils.aiming.RotationsValueGroup
import net.ccbluex.liquidbounce.utils.aiming.data.Rotation
import net.ccbluex.liquidbounce.utils.entity.getMovementDirectionOfInput
import net.ccbluex.liquidbounce.utils.entity.rotation
import net.ccbluex.liquidbounce.utils.entity.withStrafe
import net.ccbluex.liquidbounce.utils.input.InputTracker.isPressedOnAny
import net.ccbluex.liquidbounce.utils.input.isPressed
import net.ccbluex.liquidbounce.utils.kotlin.EventPriorityConvention.FIRST_PRIORITY
import net.ccbluex.liquidbounce.utils.kotlin.EventPriorityConvention.OBJECTION_AGAINST_EVERYTHING
import net.ccbluex.liquidbounce.utils.kotlin.Priority
import net.ccbluex.liquidbounce.utils.math.plus
import net.ccbluex.liquidbounce.utils.movement.DirectionalInput
import net.ccbluex.liquidbounce.utils.navigation.NavigationBaseValueGroup
import net.ccbluex.liquidbounce.utils.raytracing.traceFromPoint
import net.minecraft.client.CameraType
import net.minecraft.core.Direction
import net.minecraft.network.protocol.game.ClientboundPlayerPositionPacket
import net.minecraft.world.entity.Entity
import net.minecraft.world.phys.Vec3
import org.lwjgl.glfw.GLFW
import java.util.function.Predicate
import kotlin.math.abs

/**
 * FreeCam module
 *
 * Allows you to move out of your body.
 */
object ModuleFreeCam : ClientModule("FreeCam", ModuleCategories.RENDER, disableOnQuit = true) {

    private val speed by float("Speed", 1f, 0.1f..2f)

    /**
     * Allows to interact from the camera perspective. This is very useful to interact with blocks that
     * are behind the player or walls. Similar functionality to the GhostBlock module.
     */
    private object CameraInteract : ToggleableValueGroup(ModuleFreeCam, "AllowCameraInteract", true) {
        val lookAt by boolean("LookAt", true)
    }

    private class CancelTrigger<E : Event>(val eventType: Class<E>, val predicate: Predicate<E>)
    private inline fun <reified E : Event> cancelTrigger(predicate: Predicate<E>) =
        CancelTrigger(E::class.java, predicate)

    /**
     * This is useful for cancelling FreeCam on certain events.
     * For example, when the player takes damage.
     */
    private enum class CancelOn(
        override val tag: String,
        private val trigger: CancelTrigger<out Event>,
    ) : Tagged {
        DAMAGE("Damage", cancelTrigger<HealthUpdateEvent> { event ->
            event.health < event.previousHealth
        }),
        TELEPORT("Teleport", cancelTrigger<PacketEvent> { event ->
            // ClientboundPlayerPositionPacket not trigger PlayerMoveEvent
            event.packet is ClientboundPlayerPositionPacket
        }),
        MOVE("Move", cancelTrigger<PlayerMoveEvent> { event ->
            // Don't check movement.y because it's gravity / falling motion
            abs(event.movement.x) > 0 || abs(event.movement.z) > 0
        }),
        LIQUID("Liquid", cancelTrigger<PlayerTickEvent> {
            player.isInLiquid
        });

        init {
            EventManager.registerEventHook(
                this.trigger.eventType,
                @Suppress("UNCHECKED_CAST")
                newEventHook { event ->
                    if (this in cancelOn && (this.trigger.predicate as Predicate<Event>).test(event)) {
                        ModuleFreeCam.enabled = false
                    }
                }
            )
        }
    }

    private val cancelOn by multiEnumChoice("CancelOn", enumSetOf<CancelOn>())

    /**
     * Navigation configuration for the FreeCam module
     */
    private object Navigation : NavigationBaseValueGroup<Unit>(ModuleFreeCam, "Navigation", false) {

        private val controlKey by key("Key", InputConstants.KEY_LCONTROL)

        val shouldBeGoing
            get() = running && controlKey != InputConstants.UNKNOWN && controlKey.isPressed

        /**
         * Creates context for navigation
         */
        override fun createNavigationContext() {
            // Nothing to do
        }

        /**
         * Calculates the desired position to move towards
         *
         * @return Target position as [Vec3]
         */
        override fun calculateGoalPosition(context: Unit): Vec3? {
            return if (shouldBeGoing) {
                getCameraLookingAt()
            } else {
                null
            }
        }

    }

    private val midClickCameraTeleport by boolean("MidClickCameraTeleport", false)

    private val keepSneaking by boolean("KeepSneaking", false)

    private val rotations = tree(RotationsValueGroup(this))

    init {
        tree(CameraInteract)
        tree(Navigation)
    }

    object PositionState {
        var available: Boolean = false
            set(value) {
                if (value) {
                    pos = player.eyePosition
                    lastPos = pos

                    rot = player.rotation
                    lastRot = rot
                } else {
                    pos = Vec3.ZERO
                    lastPos = Vec3.ZERO

                    rot = Rotation.ZERO
                    lastRot = rot
                }
                field = value
            }

        @JvmField var pos: Vec3 = Vec3.ZERO
        @JvmField var lastPos: Vec3 = Vec3.ZERO
        @JvmField var rot: Rotation = Rotation.ZERO
        @JvmField var lastRot: Rotation = Rotation.ZERO

        fun set(target: Vec3) {
            lastPos = pos
            pos = target
        }
        fun set(target: Rotation) {
            lastRot = rot
            rot = target
        }

        fun update(velocity: Vec3) = set(pos + velocity)
        fun rotation(xDelta: Double, yDelta: Double) = set(
            Rotation(
                rot.yRot + xDelta.toFloat(),
                (rot.xRot + yDelta.toFloat()).coerceIn(-90f..90f)
            )
        )
        fun interpolate(partialTicks: Float) = lastPos.lerp(pos, partialTicks.toDouble())
        fun interpolateRot(partialTicks: Float) = lastRot.interpolateTo(rot, partialTicks)
    }

    override fun onEnabled() {
        PositionState.available = true
        super.onEnabled()
    }

    override fun onDisabled() {
        PositionState.available = false
        super.onDisabled()
    }

    @Suppress("unused")
    private val mouseHandler = handler<MouseButtonEvent> { event ->
        if (midClickCameraTeleport &&
            event.action == GLFW.GLFW_PRESS && event.button == GLFW.GLFW_MOUSE_BUTTON_MIDDLE) {
            val target = getCameraLookingAt() ?: return@handler

            // interpolate to prevent tp into block
            PositionState.set(PositionState.pos.lerp(target, 0.9))
        }
    }

    @Suppress("unused")
    private val inputHandler = handler<MovementInputEvent>(priority = FIRST_PRIORITY) { event ->
        event.directionalInput = DirectionalInput.NONE
        event.jump = false
        event.sneak = false
    }

    @Suppress("unused")
    private val tickHandler = handler<GameTickEvent> { event ->
        val directionalInput = DirectionalInput(mc.options)
        var yAxisMovement = 0.0

        if (shouldHandleInputs(mc.options.keyJump) && mc.options.keyJump.isPressedOnAny) yAxisMovement += 1.0f
        if (shouldHandleInputs(mc.options.keyShift) && mc.options.keyShift.isPressedOnAny) yAxisMovement -= 1.0f

        val speed = this.speed.toDouble()

        ModuleDebug.debugParameter(this, "DirectionalInput", directionalInput)
        val velocity = Vec3.ZERO
            .withStrafe(
                speed = speed,
                input = directionalInput,
                yaw = getMovementDirectionOfInput(PositionState.rot.yaw, directionalInput)
            )
            .with(Direction.Axis.Y, yAxisMovement * speed)
        ModuleDebug.debugParameter(this, "Velocity", velocity)
        PositionState.update(velocity)

        ModuleDebug.debugParameter(this, "Position", PositionState.pos)
        ModuleDebug.debugParameter(this, "Rotation", PositionState.rot)
    }

    @Suppress("unused")
    private val forceSneakHandler = handler<MovementInputEvent>(priority = OBJECTION_AGAINST_EVERYTHING) { event ->
        if (keepSneaking) {
            event.sneak = true
        }
    }

    @Suppress("unused")
    private val perspectiveHandler = handler<PerspectiveEvent> { event ->
        event.perspective = CameraType.FIRST_PERSON
    }

    @Suppress("unused")
    private val rotationHandler = handler<RotationUpdateEvent> {
        val lookAt = if (Navigation.shouldBeGoing) {
            // Look at target position
            Navigation.getMovementRotation()
        } else if (CameraInteract.running && CameraInteract.lookAt) {
            // Aim at crosshair target
            val crosshairTarget = mc.hitResult ?: return@handler
            Rotation.lookingAt(crosshairTarget.location, player.eyePosition)
        } else {
            return@handler
        }

        RotationManager.setRotationTarget(rotations.toRotationTarget(lookAt),
            Priority.NOT_IMPORTANT, ModuleFreeCam)
    }

    @Suppress("unused")
    private val alwaysCancelOnHandler = handler<WorldChangeEvent> {
        // If not, will get stuck when world change
        enabled = false
    }

    fun applyCameraPosition(entity: Entity?, partialTicks: Float) {
        if (!running || entity != player || !PositionState.available) {
            return
        }

        val camera = mc.gameRenderer.mainCamera()

        return camera.setPosition(PositionState.interpolate(partialTicks))
    }

    @JvmStatic
    fun shouldCameraInteractActive() = running && CameraInteract.running
    fun shouldDisableCameraInteract() = running && !CameraInteract.running

    private fun getCameraLookingAt(): Vec3? {
        if (!PositionState.available) return null

        val cameraPosition = PositionState.interpolate(1f)
        val target = traceFromPoint(
            range = 200.0,
            start = cameraPosition,
            direction = PositionState.rot.directionVector
        )

        return target.location
    }

}
