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

import net.minecraft.ChatFormatting
import net.minecraft.core.Direction
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundSource
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.TooltipFlag
import net.minecraft.world.level.Level
import net.minecraftforge.common.capabilities.Capability
import net.minecraftforge.common.capabilities.ForgeCapabilities
import net.minecraftforge.common.capabilities.ICapabilityProvider
import net.minecraftforge.common.util.INBTSerializable
import net.minecraftforge.common.util.LazyOptional
import ru.dbotthepony.mc.otm.*
import ru.dbotthepony.mc.otm.capability.*
import ru.dbotthepony.mc.otm.capability.energy.IMatteryEnergyStorage
import ru.dbotthepony.mc.otm.capability.energy.ItemEnergyStorageImpl
import ru.dbotthepony.mc.otm.capability.energy.getBarColor
import ru.dbotthepony.mc.otm.capability.energy.getBarWidth
import ru.dbotthepony.mc.otm.core.*
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.math.bezierCurve
import ru.dbotthepony.mc.otm.core.nbt.doubles
import ru.dbotthepony.mc.otm.core.nbt.ints
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.core.util.formatPower
import ru.dbotthepony.mc.otm.registry.MSoundEvents
import kotlin.reflect.KClass

class PlasmaWeaponEnergy(val itemStack: ItemStack, private val innerCapacity: Decimal) : IMatteryEnergyStorage, ICapabilityProvider, INBTSerializable<CompoundTag> {
	private val energyResolver = LazyOptional.of { this }
	private var innerBatteryLevel = Decimal.ZERO

	override val energyFlow: FlowDirection
		get() = FlowDirection.INPUT

	var battery: ItemStack = ItemStack.EMPTY

	override fun <T : Any> getCapability(cap: Capability<T>, side: Direction?): LazyOptional<T> {
		if (cap == MatteryCapability.ENERGY || cap == ForgeCapabilities.ENERGY) {
			return energyResolver.cast()
		}

		return LazyOptional.empty()
	}

	override fun serializeNBT(): CompoundTag {
		val nbt = CompoundTag()
		nbt["battery_level"] = innerBatteryLevel.serializeNBT()
		nbt["battery"] = battery.serializeNBT()
		return nbt
	}

	override fun deserializeNBT(nbt: CompoundTag) {
		innerBatteryLevel = Decimal.deserializeNBT(nbt["battery_level"])
		battery = ItemStack.of(nbt["battery"] as CompoundTag)
	}

	override fun extractEnergy(howMuch: Decimal, simulate: Boolean): Decimal {
		if (!howMuch.isPositive)
			return Decimal.ZERO

		@Suppress("NAME_SHADOWING")
		var howMuch = howMuch
		var totalExtracted = Decimal.ZERO

		if (!battery.isEmpty) {
			battery.getCapability(ForgeCapabilities.ENERGY).ifPresentK {
				val extracted = it.extractEnergy(howMuch, simulate)

				if (extracted >= howMuch) {
					return extracted
				}

				howMuch -= extracted
				totalExtracted += extracted
			}
		}

		val newEnergy = (innerBatteryLevel - howMuch).moreThanZero()
		val diff = innerBatteryLevel - newEnergy

		if (!simulate) {
			innerBatteryLevel = newEnergy
		}

		return diff + totalExtracted
	}

	override fun receiveEnergy(howMuch: Decimal, simulate: Boolean): Decimal {
		if (!howMuch.isPositive)
			return Decimal.ZERO

		@Suppress("NAME_SHADOWING")
		var howMuch = howMuch
		var totalReceived = Decimal.ZERO

		if (!battery.isEmpty) {
			battery.getCapability(ForgeCapabilities.ENERGY).ifPresentK {
				val received = it.receiveEnergy(howMuch, simulate)

				if (received >= howMuch) {
					return received
				}

				howMuch -= received
				totalReceived += received
			}
		}

		if (innerBatteryLevel >= innerCapacity) {
			return totalReceived
		}

		val newEnergy = (innerBatteryLevel + howMuch).coerceAtMost(innerCapacity)
		val diff = newEnergy - innerBatteryLevel

		if (!simulate) {
			innerBatteryLevel = newEnergy
		}

		return diff + totalReceived
	}

	fun think() {
		if (!battery.isEmpty && innerBatteryLevel < innerCapacity) {
			battery.getCapability(ForgeCapabilities.ENERGY).ifPresentK {
				innerBatteryLevel += it.extractEnergy(innerCapacity - innerBatteryLevel, false)
			}
		}
	}

	override fun drainBattery(): Boolean {
		innerBatteryLevel = Decimal.ZERO

		if (!battery.isEmpty) {
			battery.getCapability(MatteryCapability.ENERGY).ifPresentK {
				return it.drainBattery()
			}
		}

		return true
	}

	override fun fillBattery(): Boolean {
		innerBatteryLevel = innerCapacity

		if (!battery.isEmpty) {
			battery.getCapability(MatteryCapability.ENERGY).ifPresentK {
				return it.fillBattery()
			}
		}

		return true
	}

	override var batteryLevel: Decimal
		get() = (battery.energy?.energyStoredMattery ?: Decimal.ZERO) + innerBatteryLevel
		set(value) {
			innerBatteryLevel = value
		}

	override val maxBatteryLevel: Decimal
		get() = (battery.energy?.maxEnergyStoredMattery ?: Decimal.ZERO) + innerCapacity
}

open class PlasmaWeaponDataTable(tag: CompoundTag) : WeaponDataTable(tag) {
	var heatScore by tag.doubles
	var heatCooldown by tag.ints
}

abstract class PlasmaWeaponItem<D : PlasmaWeaponDataTable>(tables: KClass<D>, private val energyCapacity: Decimal) : AbstractWeaponItem<D>(tables) {
	override fun appendHoverText(
		itemStack: ItemStack,
		p_41422_: Level?,
		p_41423_: MutableList<Component>,
		p_41424_: TooltipFlag
	) {
		super.appendHoverText(itemStack, p_41422_, p_41423_, p_41424_)

		ItemEnergyStorageImpl.appendHoverText(itemStack, p_41423_)
	}

	fun energyData(itemStack: ItemStack) = itemStack.matteryEnergy as PlasmaWeaponEnergy

	open val cooldownSpeed get() = 1.0
	open val maxHeat = 40.0
	open val overheatCooldown = 30

	open val cooldownSpline = doubleArrayOf(
		0.1,
		0.14,
		0.18,
		0.25,
		0.35,
		0.45,
		0.6,
		0.8,
		0.97,
		1.0,
		1.0,
		1.0,
		1.0,
		1.0,
		1.0,
		1.1,
		1.2,
		1.3,
		1.4,
	)

	override fun canPrimaryFire(itemStack: ItemStack, player: Player, dt: D): Boolean {
		if (player is ServerPlayer) {
			// allow little deviation because nothing is perfect
			return super.canPrimaryFire(itemStack, player, dt) && dt.heatCooldown <= 2
		}

		return super.canPrimaryFire(itemStack, player, dt) && dt.heatCooldown <= 0
	}

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

	override fun isBarVisible(p_150899_: ItemStack): Boolean {
		return p_150899_.matteryEnergy != null
	}

	override fun getBarWidth(p_150900_: ItemStack): Int {
		return p_150900_.matteryEnergy?.getBarWidth() ?: super.getBarWidth(p_150900_)
	}

	override fun getBarColor(p_150901_: ItemStack): Int {
		return p_150901_.matteryEnergy?.getBarColor() ?: super.getBarColor(p_150901_)
	}

	protected open fun coolWeaponDown(itemStack: ItemStack, player: Player, dt: D) {
		if (!canCooldown(itemStack, player, dt))
			return

		if (dt.heatScore > 0.0)
			dt.heatScore = (dt.heatScore - cooldownSpeed * bezierCurve(heatProgress(itemStack, dt = dt), cooldownSpline)).coerceAtLeast(0.0)

		if (dt.heatCooldown > 0)
			dt.heatCooldown--
	}

	open fun heatProgress(itemStack: ItemStack, partialTicks: Double = 0.0, dt: D = dataTable(itemStack)): Double {
		if (dt.heatScore <= 0)
			return 0.0

		if (dt.heatScore - cooldownSpeed > maxHeat)
			return 1.0

		val result = dt.heatScore - cooldownSpeed * partialTicks
		return result / maxHeat
	}

	open fun overheatProgress(itemStack: ItemStack, partialTicks: Double = 0.0, dt: D = dataTable(itemStack)): Double {
		if (dt.heatCooldown <= 0)
			return 0.0

		if (dt.heatCooldown - partialTicks > overheatCooldown)
			return 1.0

		val result = dt.heatCooldown - partialTicks
		return result / overheatCooldown
	}

	open fun receiveHeat(itemStack: ItemStack, player: Player, byTicks: Double, dt: D = dataTable(itemStack)) {
		dt.heatScore = (dt.heatScore + byTicks).coerceAtMost(maxHeat * 2)

		if (dt.heatScore > maxHeat) {
			val before = dt.heatCooldown > 0

			dt.heatCooldown = overheatCooldown

			if (!before) {
				player.level().playSound(player, player, MSoundEvents.PLASMA_WEAPON_OVERHEAT, SoundSource.PLAYERS, 1f, 1f)
			}
		}
	}

	override fun think2(itemStack: ItemStack, player: Player, dt: D) {
		super.think2(itemStack, player, dt)

		coolWeaponDown(itemStack, player, dt)

		itemStack.getCapability(MatteryCapability.ENERGY).ifPresentK {
			it as PlasmaWeaponEnergy
			it.think()
		}
	}

	override fun initCapabilities(stack: ItemStack, nbt: CompoundTag?): ICapabilityProvider {
		return PlasmaWeaponEnergy(stack, energyCapacity)
	}
}
