/*
 * 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.features.module.modules.world.fucker

import it.unimi.dsi.fastutil.longs.LongOpenHashSet
import net.ccbluex.liquidbounce.config.types.group.ToggleableValueGroup
import net.ccbluex.liquidbounce.config.types.list.Tagged
import net.ccbluex.liquidbounce.event.events.CancelBlockBreakingEvent
import net.ccbluex.liquidbounce.event.events.RotationUpdateEvent
import net.ccbluex.liquidbounce.event.handler
import net.ccbluex.liquidbounce.event.tickHandler
import net.ccbluex.liquidbounce.event.waitTicks
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.ModuleDebug
import net.ccbluex.liquidbounce.features.module.modules.render.ModuleDebug.debugGeometry
import net.ccbluex.liquidbounce.features.module.modules.render.ModuleDebug.debugParameter
import net.ccbluex.liquidbounce.features.module.modules.world.ModuleAutoTool
import net.ccbluex.liquidbounce.features.module.modules.world.packetmine.ModulePacketMine
import net.ccbluex.liquidbounce.render.engine.type.Color4b
import net.ccbluex.liquidbounce.utils.aiming.RotationManager
import net.ccbluex.liquidbounce.utils.aiming.RotationsValueGroup
import net.ccbluex.liquidbounce.utils.aiming.utils.raytraceBlockRotation
import net.ccbluex.liquidbounce.utils.block.DIRECTIONS_EXCLUDING_DOWN
import net.ccbluex.liquidbounce.utils.block.bed.isSelfBedChoices
import net.ccbluex.liquidbounce.utils.block.doBreak
import net.ccbluex.liquidbounce.utils.block.getBlock
import net.ccbluex.liquidbounce.utils.block.isAnyChest
import net.ccbluex.liquidbounce.utils.math.distanceToSqr
import net.ccbluex.liquidbounce.utils.block.isNotBreakable
import net.ccbluex.liquidbounce.utils.block.outlineBox
import net.ccbluex.liquidbounce.utils.block.searchBlocksInRangeSorted
import net.ccbluex.liquidbounce.utils.block.outlineShape
import net.ccbluex.liquidbounce.utils.block.raycast
import net.ccbluex.liquidbounce.utils.block.state
import net.ccbluex.liquidbounce.utils.inventory.Slots
import net.ccbluex.liquidbounce.utils.inventory.findBlocksEndingWith
import net.ccbluex.liquidbounce.utils.kotlin.Priority
import net.ccbluex.liquidbounce.utils.math.forAllFaces
import net.ccbluex.liquidbounce.utils.math.samplePointOnSide
import net.ccbluex.liquidbounce.utils.math.sq
import net.ccbluex.liquidbounce.utils.math.withLength
import net.ccbluex.liquidbounce.utils.raytracing.raytraceBlock
import net.ccbluex.liquidbounce.utils.render.BreakingProgress
import net.ccbluex.liquidbounce.utils.render.BreakingProgressRenderer
import net.ccbluex.liquidbounce.utils.render.placement.PlacementRenderer
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
import net.minecraft.core.BlockPos
import net.minecraft.world.InteractionHand
import net.minecraft.world.InteractionResult
import net.minecraft.world.level.ClipContext
import net.minecraft.world.level.block.BedBlock
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.HitResult
import net.minecraft.world.phys.Vec3
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import java.util.function.ToDoubleFunction
import java.util.function.ToIntFunction
import kotlin.math.max

/**
 * Fucker module
 *
 * Destroys/Uses selected blocks around you.
 */
object ModuleFucker : ClientModule(
    "Fucker",
    ModuleCategories.WORLD,
    aliases = listOf("BedBreaker", "IdNuker"),
), BreakingProgress.Provider {

    private val range by float("Range", 5F, 1F..6F)
    private val wallRange by float("WallRange", 0f, 0F..6F).onChange {
        minOf(range, it)
    }

    /**
     * Entrance requires the target block to have an entrance. It does not matter if we can see it or not.
     * If this condition is true, it will override the wall range to range
     * and act as if we were breaking normally.
     *
     * Useful for Hypixel and CubeCraft
     */
    private object FuckerEntrance : ToggleableValueGroup(this, "Entrance", false) {
        /**
         * Breaks the weakest block around target block and makes an entrance
         */
        val breakFree by boolean("BreakFree", true)
    }

    init {
        tree(FuckerEntrance)
    }

    private val surroundings by boolean("Surroundings", true)
    private val targets by blocks("Targets", findBlocksEndingWith("_BED", "DRAGON_EGG"))
    private val delay by int("Delay", 0, 0..20, "ticks")
    private val action by enumChoice("Action", DestroyAction.DESTROY).apply(::tagBy)
    private val forceImmediateBreak by boolean("ForceImmediateBreak", false)

    private val ignoreOpenInventory by boolean("IgnoreOpenInventory", true)
    private val ignoreUsingItem by boolean("IgnoreUsingItem", true)
    private val prioritizeOverKillAura by boolean("PrioritizeOverKillAura", false)

    private val chestAsFullBlock by boolean("ChestAsFullBlock", false)

    private val isSelfBedMode = choices("SelfBed", 0, ::isSelfBedChoices)

    // Rotation
    private val rotations = tree(RotationsValueGroup(this))
    private val targetRenderer = tree(
        PlacementRenderer("TargetRendering", true, this,
            defaultColor = Color4b(255, 0, 0, 90)
        )
    )
    private val progressRenderer = targetRenderer.tree(
        BreakingProgressRenderer(targetRenderer, this)
    )

    private val availableToolSlots
        get() = if (ModuleAutoTool.isInventoryConsidered) Slots.HotbarAndInventory else Slots.Hotbar

    private fun miningDuration(pos: BlockPos, state: BlockState): Double {
        val bestMiningSpeed = availableToolSlots.maxOf { it.itemStack.getDestroySpeed(state) }
        return state.getDestroySpeed(world, pos).toDouble() / bestMiningSpeed.toDouble()
    }

    private var currentTarget: DestroyerTarget? = null
    private var oldTarget: DestroyerTarget? = null

    override fun breakingProgress(): BreakingProgress? {
        val target = currentTarget?.takeIf { it.action == DestroyAction.DESTROY } ?: return null
        if (ModulePacketMine.running) {
            return null
        }

        if (forceImmediateBreak) {
            return BreakingProgress(target.pos, 1f)
        }

        return BreakingProgress.Provider.Default.breakingProgress(target.pos)
    }

    private val targetPointProportions = doubleArrayOf(0.1, 0.3, 0.5, 0.7, 0.9)
    private const val MAX_SURROUNDING_PATH_BLOCKS = 8
    private const val RAYCAST_TARGET_EPSILON = 0.005

    override fun onDisabled() {
        clearCurrentTarget()
        oldTarget = null
        targetRenderer.clearSilently()
    }

    @Suppress("unused")
    private val targetUpdater = handler<RotationUpdateEvent> {
        if (!ignoreOpenInventory && mc.gui.screen() is AbstractContainerScreen<*>) {
            return@handler
        }

        if (!ignoreUsingItem && player.isUsingItem) {
            return@handler
        }

        oldTarget = currentTarget
        updateCurrentTarget()
    }

    @Suppress("unused")
    private val breaker = tickHandler {
        if (!ignoreOpenInventory && mc.gui.screen() is AbstractContainerScreen<*>) {
            return@tickHandler
        }

        // If we don't have any new target, and we had one before, stop breaking.
        if (oldTarget != null && currentTarget == null) {
            interaction.stopDestroyBlock()
            return@tickHandler
        } else if (oldTarget != currentTarget && delay > 0) {
            interaction.stopDestroyBlock()
            waitTicks(delay)
        }

        // Check if blink is enabled - if so, we don't want to do anything.
        if (ModuleBlink.running) {
            return@tickHandler
        }

        val destroyerTarget = currentTarget ?: return@tickHandler
        val currentRotation = RotationManager.serverRotation
        targetRenderer.addBlock(destroyerTarget.pos)

        if (ModulePacketMine.running && destroyerTarget.action == DestroyAction.DESTROY) {
            ModulePacketMine.setTarget(destroyerTarget.pos)
            return@tickHandler
        }

        // Check if we are already looking at the block
        val rayTraceResult = raytraceBlock(
            max(range, wallRange).toDouble(),
            currentRotation,
            destroyerTarget.pos,
            destroyerTarget.pos.state ?: return@tickHandler
        ) ?: return@tickHandler

        val raytracePos = rayTraceResult.blockPos

        // Check if the raytrace result includes a block, if not we don't want to deal with it.
        val raytraceState = raytracePos.state
        if (rayTraceResult.type != HitResult.Type.BLOCK || raytracePos != destroyerTarget.pos ||
            raytraceState == null || raytraceState.isNotBreakable(raytracePos)) {
            return@tickHandler
        }

        // Use action should be used if the block is the same as the current target and the action is set to use.
        if (destroyerTarget.action == DestroyAction.USE) {
            if (interaction.useItemOn(player, InteractionHand.MAIN_HAND, rayTraceResult) == InteractionResult.SUCCESS) {
                player.swing(InteractionHand.MAIN_HAND)
            }

            waitTicks(delay)
        } else {
            doBreak(rayTraceResult, immediate = forceImmediateBreak)
        }
    }

    @Suppress("unused")
    private val cancelBlockBreakingHandler = handler<CancelBlockBreakingEvent> { event ->
        if (currentTarget != null && !ModulePacketMine.running) {
            event.cancelEvent()
        }
    }

    private fun updateCurrentTarget() {
        val possibleBlocks = searchPossibleTargetPositions()

        validateCurrentTarget(possibleBlocks)

        if (possibleBlocks.isEmpty()) {
            return
        }

        val range = range.toDouble()

        // Find direct targets first
        if (possibleBlocks.any { pos ->
            // If the block has an entrance, we should ignore the wall range and act as if we are breaking normally.
            val wallRange = if (FuckerEntrance.enabled && pos.hasEntrance) range else wallRange.toDouble()
            considerAsTarget(DestroyerTarget(pos, action, isTarget = true), range, wallRange) == true
        } || currentTarget != null) {
            return
        }

        // Surrounding / Entrance
        for (pos in possibleBlocks) {
            // Is there any block in the way?
            if (FuckerEntrance.enabled && FuckerEntrance.breakFree) {
                val weakBlock = pos.weakestNeighbor ?: continue
                considerAsTarget(DestroyerTarget(weakBlock, DestroyAction.DESTROY), range, range)
            } else if (surroundings) {
                updateSurroundings(pos)
            }
        }
    }

    private fun clearCurrentTarget() {
        interaction.stopDestroyBlock()

        currentTarget?.let { target ->
            targetRenderer.removeBlock(target.pos)
        }
        currentTarget = null
    }

    private fun searchPossibleTargetPositions(): List<BlockPos> {
        return player.eyePosition.searchBlocksInRangeSorted(range) { pos, state ->
            when (val block = state.block) {
                !in targets -> false
                is BedBlock if isSelfBedMode.activeMode.isSelfBed(block, pos) -> false
                else -> true
            }
        }.map { it.first }
    }

    private fun validateCurrentTarget(possibleBlocks: Collection<BlockPos>) {
        val currentTarget = currentTarget ?: return

        var removed = false
        val actualTargetPos = currentTarget.surroundingInfo?.actualTargetPos ?: currentTarget.pos
        if (actualTargetPos !in possibleBlocks) {
            removed = true
        }
        if (currentTarget.isTarget && currentTarget.action != action) {
            removed = true
        }

        // Stick with the current target because it's still valid.
        val validationResult =
            considerAsTarget(currentTarget, range.toDouble(), wallRange.toDouble(), isCurrentTarget = true)

        if (validationResult == false) {
            removed = true
        }

        if (removed) {
            clearCurrentTarget()
        }
    }

    /**
     * @return true if it is the best target, false if it's invalid and null if it's not better than the current target
     */
    private fun considerAsTarget(
        target: DestroyerTarget,
        range: Double,
        throughWallsRange: Double,
        isCurrentTarget: Boolean = false
    ): Boolean? {
        val state = target.pos.state?.takeUnless { it.isAir } ?: return false

        val raytrace = raytraceBlockRotation(
            eyes = player.eyePosition,
            pos = target.pos,
            state = state,
            range = range,
            wallsRange = throughWallsRange
        ) ?: return false

        val currentTarget = currentTarget

        if (!isCurrentTarget && currentTarget != null && target >= currentTarget) {
            return null
        }

        if (!ModulePacketMine.running) {
            RotationManager.setRotationTarget(
                rotation = raytrace.rotation,
                considerInventory = !ignoreOpenInventory,
                valueGroup = rotations,
                priority = if (prioritizeOverKillAura) {
                    Priority.IMPORTANT_FOR_USAGE_3
                } else {
                    Priority.IMPORTANT_FOR_USAGE_1
                },
                provider = this@ModuleFucker
            )
        }

        clearCurrentTarget()
        ModuleFucker.currentTarget = target

        return true
    }

    private fun updateSurroundings(initialPosition: BlockPos): Boolean {
        val eyePos = player.eyePosition
        val path = findBestSurroundingPath(initialPosition, eyePos) ?: return false

        debugGeometry("targetPoint") {
            ModuleDebug.DebuggedPoint(path.info.targetPoint, Color4b.RED.alpha(100))
        }

        debugGeometry("initialPosition") {
            ModuleDebug.DebuggedBox(initialPosition.outlineBox.move(initialPosition), Color4b.GREEN.alpha(50))
        }

        debugGeometry("raytraceResult") {
            ModuleDebug.DebuggedBox(path.firstBlock.outlineBox.move(path.firstBlock), Color4b.BLUE.alpha(50))
        }

        debugParameter("wayToTarget") { path.blocks }

        return considerAsTarget(
            DestroyerTarget(path.firstBlock, DestroyAction.DESTROY, path.info),
            range.toDouble(),
            wallRange.toDouble(),
        ) == true
    }

    @Suppress("CognitiveComplexMethod")
    private fun findBestSurroundingPath(target: BlockPos, eyePos: Vec3): SurroundingPath? {
        val rangeSquared = range.toDouble().sq()

        fun createSurroundingPath(targetPoint: Vec3): SurroundingPath? {
            val blocks = traceBlocksToTarget(target, eyePos, targetPoint)?.takeIf { it.isNotEmpty() } ?: return null
            var resistance = 0.0

            for (pos in blocks) {
                val state = pos.state?.takeUnless { it.isAir } ?: return null
                resistance += miningDuration(pos, state)
            }

            val firstBlock = blocks.first()
            return SurroundingPath(
                firstBlock = firstBlock,
                blocks = blocks,
                info = SurroundingInfo(
                    actualTargetPos = target,
                    targetPoint = targetPoint,
                    resistance = resistance,
                    blockerCount = blocks.size,
                    firstBlockDistanceToTarget = firstBlock.distToCenterSqr(targetPoint),
                    firstBlockDistanceToEyes = firstBlock.distToCenterSqr(eyePos),
                )
            )
        }

        var bestPath: SurroundingPath? = null

        val targetState = target.state ?: return bestPath
        val targetShape = if (chestAsFullBlock && targetState.isAnyChest) {
            Shapes.block()
        } else {
            targetState.getShape(world, target)
        }

        targetShape.move(target).forAllFaces { side, minX, minY, minZ, maxX, maxY, maxZ ->
            val face = AABB(minX, minY, minZ, maxX, maxY, maxZ)
            for (a in targetPointProportions) {
                for (b in targetPointProportions) {
                    val targetPoint = face.samplePointOnSide(side, a, b)

                    if (eyePos.distanceToSqr(targetPoint) <= rangeSquared) {
                        val path = createSurroundingPath(targetPoint) ?: continue
                        val currentBestPath = bestPath
                        if (currentBestPath == null || currentBestPath >= path) {
                            bestPath = path
                        }
                    }
                }
            }
        }

        return bestPath
    }

    private fun traceBlocksToTarget(target: BlockPos, eyePos: Vec3, targetPoint: Vec3): List<BlockPos>? {
        val direction = targetPoint.subtract(eyePos).withLength(RAYCAST_TARGET_EPSILON)
        val end = targetPoint.add(direction)
        val clipContext = ClipContext(eyePos, end, ClipContext.Block.OUTLINE, ClipContext.Fluid.NONE, player)
        val ignoredBlocks = ArrayList<BlockPos>(MAX_SURROUNDING_PATH_BLOCKS)
        val visited = LongOpenHashSet(MAX_SURROUNDING_PATH_BLOCKS)
        var reachedTarget = false
        var invalidPath = false

        while (!reachedTarget && !invalidPath) {
            val raytraceResult = world.raycast(
                context = clipContext,
                exclude = ignoredBlocks,
                include = null,
                maxBlastResistance = null,
            ).takeIf { result -> result.type == HitResult.Type.BLOCK }

            if (raytraceResult == null) {
                invalidPath = true
            } else {
                val blockPos = raytraceResult.blockPos
                reachedTarget = blockPos == target

                if (!reachedTarget) {
                    val state = blockPos.state
                    invalidPath = !visited.add(blockPos.asLong()) ||
                        state == null || state.isAir || state.isNotBreakable(blockPos) ||
                        ignoredBlocks.size >= MAX_SURROUNDING_PATH_BLOCKS
                    if (!invalidPath) {
                        ignoredBlocks += blockPos
                    }
                }
            }
        }

        return ignoredBlocks.takeIf { reachedTarget }
    }

    @JvmRecord
    private data class DestroyerTarget(
        val pos: BlockPos,
        val action: DestroyAction,
        val surroundingInfo: SurroundingInfo? = null,
        val isTarget: Boolean = false
    ) : Comparable<DestroyerTarget> {
        override fun compareTo(other: DestroyerTarget): Int {
            val currentSurrounding = this.surroundingInfo
            val otherSurrounding = other.surroundingInfo

            return when {
                this.isTarget && !other.isTarget -> -1
                !this.isTarget && other.isTarget -> 1
                this.isTarget && other.isTarget -> 0
                currentSurrounding == null && otherSurrounding != null -> -1
                currentSurrounding != null && otherSurrounding == null -> 1
                currentSurrounding == null && otherSurrounding == null -> 0
                currentSurrounding != null && otherSurrounding != null -> currentSurrounding.compareTo(otherSurrounding)
                else -> 0
            }
        }
    }

    /**
     * A candidate path that opens line of sight to a target outline point.
     *
     * @param firstBlock the block that should be broken first
     * @param blocks all blocks that need to be broken for the path
     * @param info scoring and target metadata for this path
     */
    private data class SurroundingPath(
        val firstBlock: BlockPos,
        val blocks: List<BlockPos>,
        val info: SurroundingInfo
    ) : Comparable<SurroundingPath> {
        override fun compareTo(other: SurroundingPath): Int = info.compareTo(other.info)
    }

    /**
     * @param actualTargetPos the target block this surrounding path opens
     * @param targetPoint the outline point that should become visible from the player's eyes
     * @param resistance proportional to the time it will take until the actual target is reached
     * @param blockerCount number of blocks that need to be broken for the path
     */
    private data class SurroundingInfo(
        val actualTargetPos: BlockPos,
        val targetPoint: Vec3,
        val resistance: Double,
        val blockerCount: Int,
        val firstBlockDistanceToTarget: Double,
        val firstBlockDistanceToEyes: Double,
    ) : Comparable<SurroundingInfo> {
        override fun compareTo(other: SurroundingInfo): Int = SURROUNDING_INFO_COMPARATOR.compare(this, other)
    }

    private val SURROUNDING_INFO_COMPARATOR = Comparator
        .comparingDouble(ToDoubleFunction<SurroundingInfo> { it.resistance })
        .thenComparingInt(ToIntFunction { it.blockerCount })
        .thenComparingDouble(ToDoubleFunction { it.firstBlockDistanceToTarget })
        .thenComparingDouble(ToDoubleFunction { it.firstBlockDistanceToEyes })

    private enum class DestroyAction(override val tag: String) : Tagged {
        DESTROY("Destroy"), USE("Use")
    }

    private val BlockPos.hasEntrance: Boolean
        get() {
            val block = this.getBlock()
            val cache = BlockPos.MutableBlockPos()
            return DIRECTIONS_EXCLUDING_DOWN.any {
                val neighbor = cache.setWithOffset(this, it)
                neighbor.outlineShape.isEmpty && neighbor.getBlock() !== block
            }
        }

    private val BlockPos.weakestNeighbor: BlockPos?
        get() {
            val block = this.getBlock()
            val cache = BlockPos.MutableBlockPos()
            val neighbors = DIRECTIONS_EXCLUDING_DOWN.mapNotNullTo(mutableListOf()) {
                val neighbor = cache.setWithOffset(this, it)
                val state = neighbor.state ?: return@mapNotNullTo null
                if (state.block !== block && !state.isAir) neighbor.immutable() to state else null
            }

            return neighbors.minWithOrNull(comparator)?.first
        }

    private val comparator: Comparator<Pair<BlockPos, BlockState>> = Comparator
        .comparingDouble(ToDoubleFunction<Pair<BlockPos, BlockState>> { (pos, state) ->
            miningDuration(pos, state)
        })
        .thenComparingDouble(ToDoubleFunction { (pos, state) ->
            state.getShape(world, pos, CollisionContext.of(player))
                .move(pos)
                .distanceToSqr(player.eyePosition)
        })

}
