package ru.dbotthepony.mc.otm.item.weapon

import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.Minecraft
import net.minecraft.client.model.HumanoidModel
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemDisplayContext
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Rarity
import net.minecraft.world.level.block.state.BlockState
import net.minecraftforge.client.event.InputEvent
import net.minecraftforge.client.event.RenderHandEvent
import net.minecraftforge.client.event.RenderPlayerEvent
import net.minecraftforge.client.event.ViewportEvent
import net.minecraftforge.event.TickEvent
import net.minecraftforge.fml.LogicalSide
import net.minecraftforge.network.NetworkEvent
import ru.dbotthepony.mc.otm.*
import ru.dbotthepony.mc.otm.capability.matteryEnergy
import ru.dbotthepony.mc.otm.client.font
import ru.dbotthepony.mc.otm.client.render.RenderGravity
import ru.dbotthepony.mc.otm.client.render.drawAligned
import ru.dbotthepony.mc.otm.client.render.renderRect
import ru.dbotthepony.mc.otm.core.*
import ru.dbotthepony.mc.otm.core.math.Angle
import ru.dbotthepony.mc.otm.core.math.RGBAColor
import ru.dbotthepony.mc.otm.core.math.Vector
import ru.dbotthepony.mc.otm.core.math.bezierCurve
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.linearInterpolation
import ru.dbotthepony.mc.otm.core.math.rotateX
import ru.dbotthepony.mc.otm.core.math.rotateY
import ru.dbotthepony.mc.otm.core.math.rotateZ
import ru.dbotthepony.mc.otm.core.nbt.EMPTY_UUID
import ru.dbotthepony.mc.otm.core.nbt.booleans
import ru.dbotthepony.mc.otm.core.nbt.ints
import ru.dbotthepony.mc.otm.core.nbt.uuids
import ru.dbotthepony.mc.otm.core.util.formatPower
import ru.dbotthepony.mc.otm.network.MatteryPacket
import ru.dbotthepony.mc.otm.network.WeaponNetworkChannel
import java.util.*
import java.util.function.Supplier
import kotlin.math.PI
import kotlin.math.abs
import kotlin.reflect.KClass
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.primaryConstructor

private val ItemStack.weaponDataTable get() = tag?.let(::WeaponDataTable)

enum class WeaponScopePacket(val scope: Boolean) : MatteryPacket {
	SCOPE_IN(true), SCOPE_OUT(false);

	override fun write(buff: FriendlyByteBuf) {
		buff.writeBoolean(scope)
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.get().packetHandled = true

		// TODO: Manual synchronization
		context.get().enqueueWork {
			val stack = context.get().sender!!.mainHandItem
			val item = stack.item as? AbstractWeaponItem<*> ?: return@enqueueWork

			item.dataTable(stack).wantsToScope = scope
		}
	}

	companion object {
		fun read(buff: FriendlyByteBuf) = if (buff.readBoolean()) SCOPE_IN else SCOPE_OUT
	}
}

enum class WeaponFireInputPacket(val primary: Boolean) : MatteryPacket {
	PRIMARY(true), SECONDARY(false);

	override fun write(buff: FriendlyByteBuf) {
		buff.writeBoolean(primary)
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.get().packetHandled = true

		// TODO: Manual synchronization
		context.get().enqueueWork {
			val stack = context.get().sender!!.mainHandItem
			val item = stack.item as? AbstractWeaponItem<*> ?: return@enqueueWork

			// Listen server: client and server thread compete for lock
			// so it is very likely item is being in predicted context
			val predictedData = item.dataTable
			item.dataTable = null

			if (primary)
				item.tryPrimaryFire(stack, context.get().sender!!)
			else
				item.trySecondaryFire(stack, context.get().sender!!)

			(item as AbstractWeaponItem<WeaponDataTable>).dataTable = predictedData
		}
	}

	companion object {
		fun read(buff: FriendlyByteBuf) = if (buff.readBoolean()) PRIMARY else SECONDARY
	}
}

open class WeaponDataTable(val tag: CompoundTag) {
	var shotCooldown by tag.ints
	var wantsToScope by tag.booleans
	var scoped by tag.booleans
	var scopeTicks by tag.ints
	var nextPrimaryFire by tag.ints
	var nextSecondaryFire by tag.ints
	var uuid by tag.uuids

	var fireAnim = 0.0
	var fireAnimDeviation = Angle.ZERO

	fun doFireAnim(amount: Double = 0.8, deviation: Angle? = null) {
		fireAnim = (fireAnim + amount).coerceAtLeast(0.0).coerceAtMost(1.0)

		if (deviation != null) {
			fireAnimDeviation = Angle(
				deviation.pitch * RANDOM.nextGaussian(),
				deviation.yaw * RANDOM.nextGaussian(),
				deviation.roll * RANDOM.nextGaussian(),
			)
		} else {
			fireAnimDeviation = Angle.ZERO
		}
	}

	var deployTime = 0.0

	companion object {
		private val RANDOM = Random()
	}
}

abstract class AbstractWeaponItem<D : WeaponDataTable>(val tables: KClass<D>, properties: Properties = Properties().stacksTo(1).rarity(Rarity.RARE)) : Item(properties) {
	fun makeDataTable(tag: CompoundTag) = tables.primaryConstructor!!.call(tag)

	/**
	 * Predicted data table, for client use only
	 */
	var dataTable: D? = null
		set(value) {
			field = if (value == null || value::class.isSubclassOf(tables)) {
				value
			} else {
				throw ClassCastException("Reified generics: ${value::class.qualifiedName} cannot be cast to ${tables.qualifiedName}")
			}
		}

	fun compatibleDataTable(value: WeaponDataTable?) = value == null || value::class.isSubclassOf(tables)

	@Suppress("unchecked_cast")
	fun dataTable(itemStack: ItemStack): D {
		if (dataTable != null) {
			return dataTable!!
		}

		return makeDataTable(itemStack.tagNotNull)
	}

	open fun holster(itemStack: ItemStack, player: Player, dt: D = dataTable(itemStack)) {
		dt.wantsToScope = false
	}

	open fun deploy(itemStack: ItemStack, player: Player, dt: D = dataTable(itemStack)) {
		dt.wantsToScope = false

		if (dt.uuid == EMPTY_UUID) {
			dt.uuid = UUID.randomUUID()
		}
	}

	abstract val roundsPerMinute: Int
	val roundsPerSecond get() = roundsPerMinute / 60
	val fireCooldown get() = (1200 / roundsPerMinute).coerceAtLeast(1)

	open val positionHolstered get() = Vector(1.0, -2.5, -1.0)
	open val positionIdle get() = Vector(1.0, -0.5, -1.0)
	open val positionIronSights get() = Vector(0.0, -0.23, -1.0)
	open val rotIdle get() = Angle(PI / 36, PI / 18)
	open val rotIronSights get() = Angle.ZERO
	open val rotFireAnim get() = Angle.deg(20.0, 0.0, 0.0)
	open val rotFireAnimDeviation get() = Angle.deg(2.0, 1.0, 1.0)

	open val fireAnimRecovery = 1.0
	open val deployAnimSpeed = 1.0

	open val primaryAutomatic = true
	open val secondaryAutomatic = true

	open val ironSightsFOV: Double get() = 2.0

	fun ironSightsProgress(itemStack: ItemStack, partialTicks: Double = 0.0, dt: D = dataTable(itemStack)): Double {
		if (!dt.wantsToScope)
			return (((dt.scopeTicks.toDouble() - partialTicks).coerceAtLeast(0.0) / scopingTime.toDouble()) * 1.4).coerceAtMost(1.0)

		return (((dt.scopeTicks.toDouble() + partialTicks) / scopingTime.toDouble()) * 1.4).coerceAtMost(1.0)
	}

	abstract val scopingTime: Int

	open fun think(itemStack: ItemStack, player: Player, dt: D = dataTable(itemStack)) {
		if (dt.wantsToScope && dt.scopeTicks < scopingTime) {
			dt.scopeTicks++
		} else if (!dt.wantsToScope && dt.scopeTicks > 0) {
			dt.scopeTicks--
		}
	}

	open fun thinkHolstered(itemStack: ItemStack, player: Player, dt: D = dataTable(itemStack)) {
		if (dt.shotCooldown > 0) {
			dt.shotCooldown--
		}
	}

	open fun think2(itemStack: ItemStack, player: Player, dt: D = dataTable(itemStack)) {
		if (dt.nextPrimaryFire > 0) {
			dt.nextPrimaryFire--
		}

		if (dt.nextSecondaryFire > 0) {
			dt.nextSecondaryFire--
		}
	}

	open fun primaryFire(itemStack: ItemStack, player: Player, dt: D = dataTable(itemStack)): Boolean {
		return false
	}

	open fun secondaryFire(itemStack: ItemStack, player: Player, dt: D = dataTable(itemStack)): Boolean {
		return false
	}

	open fun canPrimaryFire(itemStack: ItemStack, player: Player, dt: D = dataTable(itemStack)): Boolean {
		if (player is ServerPlayer) {
			// allow a little deviation, because nothing is perfect
			return dt.nextPrimaryFire <= 2
		}

		return dt.nextPrimaryFire <= 0
	}

	fun tryPrimaryFire(itemStack: ItemStack, player: Player, dt: D = dataTable(itemStack)): Boolean {
		if (canPrimaryFire(itemStack, player, dt) && primaryFire(itemStack, player, dt)) {
			dt.nextPrimaryFire = fireCooldown
			return true
		}

		return false
	}

	fun trySecondaryFire(itemStack: ItemStack, player: Player, dt: D = dataTable(itemStack)): Boolean {
		if (dt.nextSecondaryFire <= 0 && secondaryFire(itemStack, player, dt)) {
			dt.nextSecondaryFire = fireCooldown
			return true
		}

		return false
	}

	override fun getDestroySpeed(p_41425_: ItemStack, p_41426_: BlockState): Float {
		return 0f
	}

	override fun onLeftClickEntity(stack: ItemStack?, player: Player?, entity: Entity?): Boolean {
		return true
	}

	companion object {
		// shared
		private val items = WeakHashMap<Player, ItemStack>()

		// client only
		private var inRender = false
		var predictedData: WeaponDataTable? = null
		private var lastFov = 1.0
		private var lastFovTime = 0L

		fun fovHook(event: ViewportEvent.ComputeFov) {
			val player = event.camera.entity as? Player ?: return

			val it = (player.mainHandItem.item as? AbstractWeaponItem<*>)

			if (it != null) {
				@Suppress("unchecked_cast")
				if (it.compatibleDataTable(predictedData))
					(it as AbstractWeaponItem<WeaponDataTable>).dataTable = predictedData

				val interp = ru.dbotthepony.mc.otm.core.math.linearInterpolation(
					it.ironSightsProgress(
						player.mainHandItem,
						event.partialTick
					) * 1.7 - 0.6, 1.0, it.ironSightsFOV
				)

				val time = System.nanoTime()
				val diff = abs(lastFovTime - time)
				lastFov = ru.dbotthepony.mc.otm.core.math.linearInterpolation(
					diff.coerceIn(0L, 10_000_000L).toDouble() / 10_000_000.0, lastFov, interp
				).coerceAtLeast(0.001)
				lastFovTime = time

				event.fov /= lastFov

				it.dataTable = null
			} else {
				lastFov = 1.0
				lastFovTime = 0L
			}
		}

		fun playerRenderHook(event: RenderPlayerEvent.Pre) {
			if (event.entity.mainHandItem.item is AbstractWeaponItem<*> && event.entity.offhandItem.isEmpty) {
				event.renderer.model.rightArmPose = HumanoidModel.ArmPose.BOW_AND_ARROW
				event.renderer.model.leftArmPose = HumanoidModel.ArmPose.EMPTY
			}
		}

		fun clickHook(event: InputEvent.InteractionKeyMappingTriggered) {
			val player = Minecraft.getInstance().player!!

			if (player.mainHandItem.item is AbstractWeaponItem<*> && player.offhandItem.isEmpty && event.isAttack) {
				event.isCanceled = true
				event.setSwingHand(false)
			}
		}

		private var lastClientRender = 0L

		private val fireAnimInterp = doubleArrayOf(
			0.0,
			0.01,
			0.03,
			0.04,
			0.05,
			0.06,
			0.07,
			0.1,
			0.15,
			0.3,
			0.45,
			0.6,
			0.8,
			0.85,
			0.9,
			1.0,
			0.9,
			0.6,
		)

		@Suppress("unchecked_cast")
		fun renderViewModel(event: RenderHandEvent) {
			if (inRender) {
				return
			}

			val time = System.nanoTime()
			val diff = abs(lastClientRender - time)
			lastClientRender = time

			val stack = event.itemStack
			val item = stack.item as? AbstractWeaponItem<*> ?: return

			inRender = true
			event.isCanceled = true

			(item as AbstractWeaponItem<WeaponDataTable>).dataTable = predictedData

			predictedData?.deployTime = (predictedData!!.deployTime + diff.toDouble() / (300_000_000.0 * item.deployAnimSpeed)).coerceAtMost(1.0).coerceAtLeast(0.0)
			predictedData?.fireAnim = (predictedData!!.fireAnim - diff.toDouble() / (400_000_000.0 * item.fireAnimRecovery)).coerceAtMost(1.0).coerceAtLeast(0.0)

			val player = Minecraft.getInstance().player!!
			val pose = event.poseStack
			val itemInHandRenderer = Minecraft.getInstance().entityRenderDispatcher.itemInHandRenderer

			pose.pushPose()

			val progress = item.ironSightsProgress(stack, event.partialTick.toDouble())

			val (x, y, z) = linearInterpolation(
				// 1.0 - event.equipProgress.toDouble(),
				item.dataTable?.deployTime ?: 0.0,
				item.positionHolstered,
				bezierCurve(
					progress,
					item.positionIdle,
					item.positionIdle,
					item.positionIdle,
					Vector(0.0, -1.0, -1.0),
					item.positionIronSights,
					item.positionIronSights,
					item.positionIronSights,
				)
			)

			pose.translate(x, y, z)

			var (pitch, yaw, roll) = bezierCurve(
				progress,
				item.rotIdle,
				item.rotIdle,
				item.rotIdle,
				item.rotIdle.copy(roll = -PI.toFloat() / 6f + item.rotIdle.roll),
				item.rotIronSights,
				item.rotIronSights,
				item.rotIronSights,
				item.rotIronSights,
				item.rotIronSights,
			)

			val fireAnim = bezierCurve(
				predictedData?.fireAnim ?: 0.0,
				fireAnimInterp
			)

			val rotFire = item.rotFireAnim
			pitch += (rotFire.pitch + (predictedData?.fireAnimDeviation?.pitch ?: 0.0)) * fireAnim * (1.0 - progress * 0.6)
			yaw += (rotFire.yaw + (predictedData?.fireAnimDeviation?.yaw ?: 0.0)) * fireAnim * (1.0 - progress * 0.6)
			roll += (rotFire.roll + (predictedData?.fireAnimDeviation?.roll ?: 0.0)) * fireAnim * (1.0 - progress * 0.6)

			pose.rotateZ(roll.toFloat())
			pose.rotateY(yaw.toFloat())
			pose.rotateX(pitch.toFloat())

			itemInHandRenderer.renderItem(
				player,
				stack,
				ItemDisplayContext.FIRST_PERSON_RIGHT_HAND,
				false,
				pose,
				event.multiBufferSource,
				event.packedLight
			)

			if (item is PlasmaWeaponItem<*>) {
				// RenderSystem.setShader(GameRenderer::getPositionColorShader)
				// RenderSystem.disableTexture()
				// RenderSystem.enableBlend()

				RenderSystem.setShaderColor(1f, 1f, 1f, 1f)

				pose.translate(-0.85, 0.25, -0.25)
				pose.rotateZ(PI.toFloat())
				pose.rotateY(PI.toFloat())
				pose.scale(0.01f, 0.01f, 0.01f)

				renderRect(pose.last().pose(), -2f, -2f, 72f, 34f, color = holoHudBackground)

				stack.matteryEnergy?.let {
					pose.pushPose()
					pose.translate(0.0, 0.0, -1.0)
					pose.scale(0.7f, 0.7f, 0.7f)
					val text = it.batteryLevel.formatPower()
					font.drawAligned(pose, text, RenderGravity.TOP_LEFT, 2f, 2f, RGBAColor.WHITE.toRGB())
					pose.popPose()
				}

				pose.translate(60.0, 0.0, 0.0)

				renderRect(pose.last().pose(), -1f, -1f, 9f, 32f, color = heatBackground)

				val heat = item.heatProgress(stack, event.partialTick.toDouble()).toFloat()
				renderRect(pose.last().pose(), 0f, 30f * (1f - heat), 7f, 30f * heat, color = linearInterpolation(heat, initialHeatColor, finalHeatColor))
			}

			pose.popPose()

			item.dataTable = null
			inRender = false
		}

		private val holoHudBackground get() = RGBAColor(75, 75, 75, 160)
		private val heatBackground get() = RGBAColor(40, 144, 210, 160)
		private val initialHeatColor get() = RGBAColor(85, 68, 7, 160)
		private val finalHeatColor get() = RGBAColor(245, 118, 41, 160)

		private val localPlayer: Player? get() {
			return Minecraft.getInstance().player
		}

		private var firedPrimary = false
		private var firedSecondary = false

		private fun processClientInputs(item: AbstractWeaponItem<*>, itemStack: ItemStack) {
			val minecraft = Minecraft.getInstance()
			val player = minecraft.player ?: return
			val dt = item.dataTable(itemStack)

			if (!dt.wantsToScope && minecraft.options.keyUse.isDown) {
				dt.wantsToScope = true
				WeaponNetworkChannel.sendToServer(WeaponScopePacket.SCOPE_IN)
			} else if (dt.wantsToScope && !minecraft.options.keyUse.isDown) {
				dt.wantsToScope = false
				WeaponNetworkChannel.sendToServer(WeaponScopePacket.SCOPE_OUT)
			}

			if (minecraft.options.keyAttack.isDown && (item.primaryAutomatic || !firedPrimary)) {
				firedPrimary = true

				if (item.tryPrimaryFire(itemStack, player)) {
					WeaponNetworkChannel.sendToServer(WeaponFireInputPacket.PRIMARY)
				}
			} else if (!minecraft.options.keyAttack.isDown) {
				firedPrimary = false
			}

			if (minecraft.options.keyAttack.isDown && (item.secondaryAutomatic || !firedSecondary)) {
				firedSecondary = true

				if (item.trySecondaryFire(itemStack, player)) {
					WeaponNetworkChannel.sendToServer(WeaponFireInputPacket.SECONDARY)
				}
			} else if (!minecraft.options.keyAttack.isDown) {
				firedSecondary = false
			}
		}

		@Suppress("unchecked_cast")
		fun tick(event: TickEvent.PlayerTickEvent) {
			if (event.phase != TickEvent.Phase.START)
				return

			if (event.side == LogicalSide.CLIENT && event.player != localPlayer)
				return

			val held = event.player.mainHandItem

			for (stack in event.player.inventory.items) {
				if (held !== stack) {
					(stack.item as? AbstractWeaponItem<*>)?.thinkHolstered(stack, event.player)
				}

				(stack.item as? AbstractWeaponItem<*>)?.let {
					if (event.side == LogicalSide.CLIENT && stack.weaponDataTable?.uuid != predictedData?.uuid)
						return@let

					if (event.side == LogicalSide.CLIENT) {
						(it as AbstractWeaponItem<WeaponDataTable>).dataTable = predictedData
					}

					it.think2(stack, event.player)

					if (event.side == LogicalSide.CLIENT) {
						it.dataTable = null
					}
				}
			}

			val prev = items[event.player]
			val heldItem = held.item

			if (held.weaponDataTable?.uuid != prev?.weaponDataTable?.uuid) {
				(prev?.item as? AbstractWeaponItem<*>)?.let {
					if (event.side == LogicalSide.CLIENT) {
						(it as AbstractWeaponItem<WeaponDataTable>).dataTable = predictedData
					}

					it.holster(prev, event.player)

					if (event.side == LogicalSide.CLIENT) {
						it.dataTable = null
					}
				}

				(held.item as? AbstractWeaponItem<*>)?.deploy(held, event.player)

				items[event.player] = held

				if (event.side == LogicalSide.CLIENT && heldItem is AbstractWeaponItem<*>) {
					predictedData = heldItem.makeDataTable(held.tagNotNull.copy())

					firedPrimary = false
					firedSecondary = false
				}
			}

			if (heldItem is AbstractWeaponItem<*>) {
				// this can fail horribly, and die horribly, if weapon item changed,
				// but its UUID did not (compound tag fraud)
				if (event.side == LogicalSide.CLIENT) {
					(heldItem as AbstractWeaponItem<WeaponDataTable>).dataTable = predictedData
					processClientInputs(heldItem, held)
				}

				heldItem.think(held, event.player)

				if (event.side == LogicalSide.CLIENT) {
					heldItem.dataTable = null
				}
			}
		}
	}
}
