/*
 * 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.exploit

import net.ccbluex.fastutil.enumSetOf
import net.ccbluex.liquidbounce.config.types.group.Mode
import net.ccbluex.liquidbounce.config.types.group.ModeValueGroup
import net.ccbluex.liquidbounce.config.types.list.Tagged
import net.ccbluex.liquidbounce.event.events.NotificationEvent
import net.ccbluex.liquidbounce.event.events.OverlayRenderEvent
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.movement.fly.ModuleFly
import net.ccbluex.liquidbounce.render.engine.type.Color4b
import net.ccbluex.liquidbounce.utils.block.canStandOn
import net.ccbluex.liquidbounce.utils.block.state
import net.ccbluex.liquidbounce.utils.client.notification
import net.ccbluex.liquidbounce.utils.client.scaledDimension
import net.ccbluex.liquidbounce.utils.collection.Pools
import net.ccbluex.liquidbounce.utils.math.toRadians
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.tags.FluidTags
import net.minecraft.tags.TagKey
import net.minecraft.world.level.material.Fluid
import kotlin.math.cos
import kotlin.math.sin

/**
 * Clip module
 *
 * Allows you to clip through blocks by jumping or sneaking.
 */
object ModuleClip : ClientModule("Clip", ModuleCategories.MOVEMENT) {

    private val modes = choices("Choice", Fancy, arrayOf(Fancy, Old))

    private enum class AllowedLiquids(override val tag: String, val fluidTag: TagKey<Fluid>) : Tagged {
        WATER("Water", FluidTags.WATER),
        LAVA("Lava", FluidTags.LAVA);
    }

    private object Old : Mode("Old") {

        override val parent: ModeValueGroup<Mode>
            get() = modes

        private val horizontal by float("Horizontal", 0f, -10f..10f)
        private val vertical by float("Vertical", 5f, -10f..10f)
        private val resetVelocity by boolean("ResetVelocity", true)

        override fun enable() {
            val yaw = player.yRot.toRadians()
            val x = -sin(yaw) * horizontal
            val z = cos(yaw) * horizontal

            player.absSnapTo(player.x + x, player.y + vertical, player.z + z)
            if (resetVelocity) {
                player.setDeltaMovement(0.0, 0.0, 0.0)
            }
            enabled = false
        }
    }

    private object Fancy : Mode("Fancy") {

        override val parent: ModeValueGroup<Mode>
            get() = modes

        private val horizontal by int("Horizontal", 0, 0..6)
        private val vertical by int("Vertical", 5, 0..10)
        private val allowedLiquids by multiEnumChoice("AllowedLiquids", enumSetOf<AllowedLiquids>())

        private val requiresStandOn by boolean("RequiresStandOn", true)

        private val possibleClipDirections = enumSetOf<Direction>()

        @Suppress("unused")
        val repeatable = tickHandler {
            possibleClipDirections.clear()

            if (ModuleFly.running) {
                return@tickHandler
            }

            arrayOf(Direction.UP, Direction.DOWN).forEach { direction ->
                tryClip(direction, vertical) {
                    possibleClipDirections.add(direction)
                }
            }

            val movementDirection = when {
                player.horizontalCollision -> when {
                    mc.options.keyUp.isDown -> player.direction
                    mc.options.keyDown.isDown -> player.direction.opposite
                    mc.options.keyLeft.isDown -> player.direction.getClockWise(Direction.Axis.Y).opposite
                    mc.options.keyRight.isDown -> player.direction.getClockWise(Direction.Axis.Y)
                    else -> return@tickHandler
                }

                mc.options.keyShift.isDown -> Direction.DOWN
                mc.options.keyJump.isDown -> Direction.UP
                else -> return@tickHandler
            }

            val clipLength = when (movementDirection) {
                Direction.DOWN, Direction.UP -> vertical
                else -> horizontal
            }

            tryClip(movementDirection, clipLength) { blockPos ->
                // Set the new position
                player.absSnapTo(blockPos.x + 0.5, blockPos.y.toDouble(), blockPos.z + 0.5)

                notification("Clip", message("whoosh"), NotificationEvent.Severity.SUCCESS)
            }

            waitTicks(5)
        }

        @Suppress("unused")
        val overlayRenderHandler = handler<OverlayRenderEvent> { event ->
            if (possibleClipDirections.isEmpty()) {
                return@handler
            }

            val directionString = Pools.buildStringPooled {
                append("[ ")
                if (possibleClipDirections.contains(Direction.UP)) {
                    append('▲')
                }
                if (possibleClipDirections.contains(Direction.DOWN)) {
                    append('▼')
                }
                append(" ]")
            }

            // Draw it to the right of the crosshair
            val (width, height) = mc.window.scaledDimension

            // Since we cannot use the LiquidBounce FontRenderer for characters like ▲ and ▼
            // we have to use the Minecraft FontRenderer.
            // Another option would be to pass the text to the JCEF HUD and render it there.
            // But that would be a lot of work for a simple arrow.
            // todo: maybe add a font renderer that supports unicode characters?
            //   or a way to pass text to the JCEF HUD
            event.context.text(
                mc.font,
                directionString,
                width / 2 + 10,
                height / 2 - mc.font.lineHeight / 2 + 1,
                Color4b.WHITE.argb
            )
        }

        private inline fun tryClip(movementDirection: Direction, length: Int, clip: (BlockPos) -> Unit) {
            if (length == 0) {
                return
            }

            var wallBetween = false

            // Calculate the new position
            // Find the closest free block to clip into
            val position = player.blockPosition().mutable()
            repeat(length) {
                position.move(movementDirection)

                if (isPossibleLocation(
                        position, requiresStandOn = requiresStandOn &&
                            movementDirection != Direction.UP
                    )
                ) {
                    // We do not want to clip if there is no wall between us
                    if (wallBetween) {
                        clip(position)
                        return
                    }
                } else {
                    wallBetween = true
                }
            }
        }

        private fun isPossibleLocation(blockPos: BlockPos, requiresStandOn: Boolean = true): Boolean {
            if (requiresStandOn && !blockPos.below().canStandOn()) {
                return false
            }

            val blockState = blockPos.state ?: return false
            val aboveState = blockPos.above().state ?: return false

            return (blockState.isAir && aboveState.isAir) || allowedLiquids.any {
                blockState.fluidState.`is`(it.fluidTag) &&
                    (aboveState.isAir || aboveState.fluidState.`is`(it.fluidTag))
            }
        }
    }

}
