package ru.dbotthepony.mc.otm.android.feature

import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.PoseStack
import net.minecraft.client.Camera
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.renderer.LevelRenderer
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.core.Vec3i
import net.minecraft.network.protocol.game.ClientboundSoundEntityPacket
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundEvents
import net.minecraft.sounds.SoundSource
import net.minecraft.tags.BlockTags
import net.minecraft.world.level.ClipContext
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.phys.HitResult
import net.minecraft.world.phys.shapes.BooleanOp
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraftforge.client.event.RenderLevelStageEvent
import net.minecraftforge.event.ForgeEventFactory
import net.minecraftforge.event.entity.living.LivingDeathEvent
import ru.dbotthepony.mc.otm.NULLABLE_MINECRAFT_SERVER
import ru.dbotthepony.mc.otm.OverdriveThatMatters
import ru.dbotthepony.mc.otm.android.AndroidActiveFeature
import ru.dbotthepony.mc.otm.capability.MatteryPlayerCapability
import ru.dbotthepony.mc.otm.capability.energy.extractEnergyExact
import ru.dbotthepony.mc.otm.capability.matteryPlayer
import ru.dbotthepony.mc.otm.client.render.DynamicBufferSource
import ru.dbotthepony.mc.otm.client.render.ResearchIcons
import ru.dbotthepony.mc.otm.client.render.linesIgnoreZRenderType
import ru.dbotthepony.mc.otm.client.render.sprite
import ru.dbotthepony.mc.otm.config.AndroidConfig
import ru.dbotthepony.mc.otm.core.genericPositions
import ru.dbotthepony.mc.otm.core.holder
import ru.dbotthepony.mc.otm.core.isFall
import ru.dbotthepony.mc.otm.core.math.RGBAColor
import ru.dbotthepony.mc.otm.core.math.Vector
import ru.dbotthepony.mc.otm.core.math.asVector
import ru.dbotthepony.mc.otm.core.math.component1
import ru.dbotthepony.mc.otm.core.math.component2
import ru.dbotthepony.mc.otm.core.math.component3
import ru.dbotthepony.mc.otm.core.math.plus
import ru.dbotthepony.mc.otm.core.math.rotateXDegrees
import ru.dbotthepony.mc.otm.core.math.rotateYDegrees
import ru.dbotthepony.mc.otm.core.math.shortestDistanceBetween
import ru.dbotthepony.mc.otm.core.math.times
import ru.dbotthepony.mc.otm.milliTime
import ru.dbotthepony.mc.otm.registry.AndroidFeatures
import ru.dbotthepony.mc.otm.triggers.EnderTeleporterFallDeathTrigger
import java.util.*
import kotlin.math.sin

class EnderTeleporterFeature(capability: MatteryPlayerCapability) : AndroidActiveFeature(AndroidFeatures.ENDER_TELEPORTER, capability) {
	var lastTeleport = 0
		private set

	override val maxCooldown: Int
		get() = AndroidConfig.EnderTeleporter.COOLDOWN

	private fun canUse(): Boolean {
		return !isOnCooldown && android.androidEnergy.extractEnergyExact(AndroidConfig.EnderTeleporter.ENERGY_COST, true)
	}

	private fun isValidGround(blockPos: BlockPos): Boolean {
		return canSupportPlayer(blockPos) && !isWall(blockPos)
	}

	private fun canSupportPlayer(blockPos: BlockPos): Boolean {
		val state = ply.level().getBlockState(blockPos)

		if (state.`is`(BlockTags.CLIMBABLE)) {
			return true // ladders can always support player
		}

		val shape = state.getCollisionShape(ply.level(), blockPos, CollisionContext.of(ply))
		return Shapes.joinIsNotEmpty(shape, SHAPE_CHECK_SUPPORT_PLAYER, BooleanOp.AND)
	}

	private fun isWall(blockPos: BlockPos): Boolean {
		val shape = ply.level().getBlockState(blockPos).getCollisionShape(ply.level(), blockPos, CollisionContext.of(ply))
		return Shapes.joinIsNotEmpty(shape, SHAPE_CHECK_NOT_FENCE, BooleanOp.AND)
	}

	private fun isAirGap(blockPos: BlockPos): Boolean {
		val state = ply.level().getBlockState(blockPos)

		if (state.isAir) {
			return true
		}

		val shape = state.getCollisionShape(ply.level(), blockPos, CollisionContext.of(ply))
		return shape.isEmpty || !Shapes.joinIsNotEmpty(shape, SHAPE_CHECK_SUPPORT_PLAYER, BooleanOp.AND)
	}

	private fun isValidPosition(blockPos: BlockPos): Boolean {
		if (!canSupportPlayer(blockPos.below()) && !isWall(blockPos.below().below())) {
			return false
		}

		return !isWall(blockPos.below()) && isAirGap(blockPos) && isAirGap(blockPos.above())
	}

	private data class TraceResult(val pos: BlockPos? = null, val phasedBlocks: Collection<BlockPos> = listOf()) {
		companion object {
			val EMPTY = TraceResult()
		}
	}

	private fun tryToPhaseThroughWall(blockPos: BlockPos, normal: Vec3i): TraceResult? {
		val phasedBlocks = ArrayList<BlockPos>(AndroidConfig.EnderTeleporter.MAX_PHASE_DISTANCE)
		phasedBlocks.add(blockPos)

		for (extend in 1 .. AndroidConfig.EnderTeleporter.MAX_PHASE_DISTANCE) {
			val pos = blockPos + normal * extend

			if (isAirGap(pos)) {
				for (y in 0 .. AndroidConfig.EnderTeleporter.MAX_PHASE_DISTANCE - extend) {
					val newPos = BlockPos(pos.x, pos.y - y, pos.z)

					if (isValidPosition(newPos)) {
						return TraceResult(newPos, phasedBlocks)
					} else {
						phasedBlocks.add(newPos)
					}
				}
			} else {
				phasedBlocks.add(pos)
			}
		}

		return null
	}

	private fun trace(): TraceResult {
		val headPosition = ply.eyePosition
		val aimVector = ply.getViewVector(1f)

		val result = ply.level().clip(ClipContext(
			headPosition,
			headPosition + aimVector * (AndroidConfig.EnderTeleporter.MAX_DISTANCE * 2.0),
			ClipContext.Block.COLLIDER,
			ClipContext.Fluid.NONE,
			ply
		))

		if (result.isInside || result.type == HitResult.Type.MISS) {
			return TraceResult.EMPTY
		}

		val testPositions = ply.genericPositions()

		if (
			!ply.isShiftKeyDown &&
			result.direction == Direction.UP &&
			isValidPosition(result.blockPos.above()) &&
			shortestDistanceBetween(testPositions, result.blockPos.above().asVector()) <= AndroidConfig.EnderTeleporter.MAX_DISTANCE
		) {
			return TraceResult(result.blockPos.above())
		}

		val candidates = LinkedList<TraceResult>()

		if (
			!ply.isShiftKeyDown &&
			result.direction != Direction.UP &&
			result.direction != Direction.DOWN &&
			!isAirGap(result.blockPos) &&
			ply.xRot in -15f .. 15f &&
			ply.eyePosition.distanceTo(result.blockPos.asVector()) <= 3.5
		) {
			val (vx, vy, vz) = ply.getViewVector(1f)
			val nearest = Direction.getNearest(vx, vy, vz)

			if (nearest != Direction.UP && nearest != Direction.DOWN) {
				// attempt one
				var phaseResult = tryToPhaseThroughWall(result.blockPos, nearest.normal)

				if (phaseResult != null) {
					candidates.add(phaseResult)
				}

				// attempt two
				phaseResult = tryToPhaseThroughWall(result.blockPos, result.direction.opposite.normal)

				if (phaseResult != null) {
					candidates.add(phaseResult)
				}
			}
		}

		var phasedBlocks = 0
		val phasedBlocksList = ArrayList<BlockPos>()

		val searchDirection: IntProgression

		if (ply.isShiftKeyDown) {
			searchDirection = -1 downTo ply.level().minBuildHeight - ply.level().maxBuildHeight
		} else {
			searchDirection = (if (ply.level().getBlockState(result.blockPos).`is`(BlockTags.CLIMBABLE)) -1 else 0) .. ply.level().maxBuildHeight - ply.level().minBuildHeight
		}

		for (y in searchDirection) {
			val pos = BlockPos(result.blockPos.x, result.blockPos.y + y + 1, result.blockPos.z)
			val state = ply.level().getBlockState(pos)

			if (state.`is`(Blocks.BEDROCK)) {
				// Can't phase through bedrock
				break
			}

			if (!isAirGap(pos)) {
				phasedBlocks++

				if (phasedBlocks >= AndroidConfig.EnderTeleporter.MAX_PHASE_DISTANCE) {
					break
				}

				phasedBlocksList.add(pos)
			}

			if (shortestDistanceBetween(testPositions, pos.asVector()) > AndroidConfig.EnderTeleporter.MAX_DISTANCE) {
				break
			}

			if (isValidPosition(pos)) {
				candidates.add(TraceResult(pos, phasedBlocksList))
				break
			}
		}

		if (candidates.isNotEmpty()) {
			var candiate = candidates.first
			var distance = candiate.pos!!.distSqr(result.blockPos)

			val iterator = candidates.listIterator()
			iterator.next()

			for (next in iterator) {
				val newDistance = next.pos!!.distSqr(result.blockPos)

				if (newDistance < distance) {
					candiate = next
					distance = newDistance
				}
			}

			return candiate
		}

		// no valid positions...
		if (isAirGap(result.blockPos)) {
			// try to find ground below...
			phasedBlocks = 0
			phasedBlocksList.clear()

			for (y in 0 downTo ply.level().maxBuildHeight - ply.level().minBuildHeight) {
				val pos = BlockPos(result.blockPos.x, result.blockPos.y + y, result.blockPos.z)
				val state = ply.level().getBlockState(pos)

				if (state.`is`(Blocks.BEDROCK)) {
					// Can't phase through bedrock
					break
				}

				if (!isAirGap(pos)) {
					phasedBlocks++

					if (phasedBlocks >= AndroidConfig.EnderTeleporter.MAX_PHASE_DISTANCE) {
						break
					}

					phasedBlocksList.add(pos)
				}

				if (shortestDistanceBetween(testPositions, pos.asVector()) > AndroidConfig.EnderTeleporter.MAX_DISTANCE) {
					break
				}

				if (isValidPosition(pos)) {
					return TraceResult(pos, phasedBlocksList)
				}
			}
		}

		return TraceResult.EMPTY
	}

	override fun activate(isClient: Boolean): Boolean {
		val canUse = canUse()

		if (!canUse && isClient) return false
		if (isClient) return true

		val (blockPos) = trace()
		blockPos ?: return false

		val event = ForgeEventFactory.onEnderTeleport(ply, blockPos.x + 0.5, blockPos.y.toDouble(), blockPos.z + 0.5)

		if (event.isCanceled) {
			(ply as ServerPlayer).connection.send(ClientboundSoundEntityPacket(SoundEvents.ITEM_BREAK.holder, SoundSource.PLAYERS, ply, 0.3f, 0.5f, ply.level().random.nextLong()))
			return false
		}

		putOnCooldown()
		lastTeleport = ply.server!!.tickCount
		android.androidEnergy.extractEnergy(AndroidConfig.EnderTeleporter.ENERGY_COST, false)

		ply.level().playSound(null, ply, SoundEvents.ENDERMAN_TELEPORT, SoundSource.PLAYERS, 0.3f, 0.8f + ply.level().random.nextFloat() * 0.4f)
		ply.teleportTo(event.targetX, event.targetY, event.targetZ)
		ply.level().playSound(null, ply, SoundEvents.ENDERMAN_TELEPORT, SoundSource.PLAYERS, 1f, 0.8f + ply.level().random.nextFloat() * 0.4f)

		ply.deltaMovement = Vector(0.0, 0.0, 0.0)
		ply.resetFallDistance()
		return true
	}

	override val previewRenderStage: RenderLevelStageEvent.Stage
		get() = RenderLevelStageEvent.Stage.AFTER_PARTICLES

	override fun renderPreview(poseStack: PoseStack, camera: Camera) {
		if (!canUse()) return
		val (pos, phasedBlocks) = trace()

		if (pos != null) {
			val (x, y, z) = pos.asVector()
			val (vx, vy, vz) = camera.position

			if (phasedBlocks.isNotEmpty())
				poseStack.pushPose()

			poseStack.translate(x - vx, y - vy, z - vz)
			poseStack.rotateYDegrees(-camera.yRot)
			poseStack.rotateXDegrees(camera.xRot)

			val size = 1f + sin(milliTime / 250.0).toFloat() * 0.2f
			val half = size / -2f

			RenderSystem.disableDepthTest()

			RenderSystem.setShaderColor(1f, 1f, 1f, 0.5f)
			SPRITE.render(poseStack, x = half, y = half, width = size, height = size)
			RenderSystem.setShaderColor(1f, 1f, 1f, 1f)

			if (phasedBlocks.isNotEmpty()) {
				poseStack.popPose()
				val collisionContext = CollisionContext.of(ply)
				val builder = source.getBuffer(linesIgnoreZRenderType)

				for (blockPos in phasedBlocks) {
					val blockState = ply.level().getBlockState(blockPos)
					val shape = blockState.getShape(ply.level(), blockPos, collisionContext)

					LevelRenderer.renderShape(
						poseStack,
						builder,
						shape,
						blockPos.x - vx,
						blockPos.y - vy,
						blockPos.z - vz,
						1f, 1f, 1f, 1f
					)
				}

				source.endBatch(linesIgnoreZRenderType)
			}

			RenderSystem.enableDepthTest()
		}
	}

	override fun renderIcon(graphics: GuiGraphics, x: Float, y: Float, width: Float, height: Float, color: RGBAColor) {
		if (cooldown > 0) {
			RenderSystem.setShaderColor(1f, 0.4f, 0.4f, 1f)
		}

		ResearchIcons.ICON_ENDER_TELEPORT.render(graphics, x, y, width, height, color = color)

		if (cooldown > 0) {
			RenderSystem.setShaderColor(1f, 1f, 1f, 1f)
		}
	}

	companion object {
		private val source by lazy {
			DynamicBufferSource(maximalInitialBufferSize = 256)
		}

		private val SHAPE_CHECK_NOT_FENCE = Block.box(6.0, 17.0, 6.0, 10.0, 31.0, 10.0)
		private val SHAPE_CHECK_SUPPORT_PLAYER = Block.box(6.0, 0.0, 6.0, 10.0, 16.0, 10.0)

		val SPRITE = ResourceLocation(OverdriveThatMatters.MOD_ID, "textures/item/black_hole.png").sprite(0f, 0f, 16f, 16f, 16f, 16f)

		fun onEntityDeath(event: LivingDeathEvent) {
			val android = event.entity.matteryPlayer ?: return
			val server = NULLABLE_MINECRAFT_SERVER ?: return

			if (android.isAndroid && event.source.isFall) {
				val feature = android.getFeature(AndroidFeatures.ENDER_TELEPORTER) as EnderTeleporterFeature? ?: return

				if (server.tickCount - feature.lastTeleport <= 120) {
					EnderTeleporterFallDeathTrigger.trigger(event.entity as ServerPlayer)
				}
			}
		}
	}
}
