package ru.dbotthepony.mc.otm.block.entity.matter

import net.minecraft.core.BlockPos
import net.minecraft.nbt.StringTag
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.AbstractContainerMenu
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.state.BlockState
import net.minecraftforge.common.ForgeConfigSpec
import net.minecraftforge.common.ForgeConfigSpec.ConfigValue
import net.minecraftforge.registries.ForgeRegistries
import ru.dbotthepony.mc.otm.block.entity.MatteryPoweredBlockEntity
import ru.dbotthepony.mc.otm.capability.FlowDirection
import ru.dbotthepony.mc.otm.capability.MatteryCapability
import ru.dbotthepony.mc.otm.capability.UpgradeType
import ru.dbotthepony.mc.otm.capability.energy.BlockEnergyStorageImpl
import ru.dbotthepony.mc.otm.capability.energy.ProfiledEnergyStorage
import ru.dbotthepony.mc.otm.capability.energy.WorkerEnergyStorage
import ru.dbotthepony.mc.otm.capability.matter.IMatterStorage
import ru.dbotthepony.mc.otm.capability.matter.IPatternState
import ru.dbotthepony.mc.otm.capability.matter.MatterStorageImpl
import ru.dbotthepony.mc.otm.capability.matter.ProfiledMatterStorage
import ru.dbotthepony.mc.otm.config.ConciseBalanceValues
import ru.dbotthepony.mc.otm.container.HandlerFilter
import ru.dbotthepony.mc.otm.container.MatteryContainer
import ru.dbotthepony.mc.otm.container.UpgradeContainer
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.math.DecimalConfigValue
import ru.dbotthepony.mc.otm.core.math.defineDecimal
import ru.dbotthepony.mc.otm.core.registryName
import ru.dbotthepony.mc.otm.core.util.WriteOnce
import ru.dbotthepony.mc.otm.graph.matter.MatterGraph
import ru.dbotthepony.mc.otm.graph.matter.MatterNode
import ru.dbotthepony.mc.otm.matter.IMatterValue
import ru.dbotthepony.mc.otm.matter.MatterManager
import ru.dbotthepony.mc.otm.menu.matter.MatterReconstructorMenu
import ru.dbotthepony.mc.otm.registry.MBlockEntities
import ru.dbotthepony.mc.otm.registry.MNames

class MatterReconstructorBlockEntity(blockPos: BlockPos, blockState: BlockState) : MatteryPoweredBlockEntity(MBlockEntities.MATTER_RECONSTRUCTOR, blockPos, blockState) {
	val repairContainer = MatteryContainer(::containerChanged, 1).also(::addDroppableContainer)

	private var matterPerTick = Decimal.ZERO
	private var progressPerTick = 0.0
	private var repairProgress = 0.0
	private var failureChance = 0.0
	private var lastItem: Item? = null
	private var initialDamage = 0.0

	var visualItemStack by synchronizer.item(observe = false)
		private set

	var visualProgress = 0f
		private set

	var isUnableToProcess = false
		private set

	val upgrades = UpgradeContainer(this::setChangedLight, 3, UpgradeType.REPLICATOR)
	val matter = ProfiledMatterStorage(MatterStorageImpl(::setChangedLight, FlowDirection.INPUT, upgrades.matterCapacity(::CAPACITY)))
	val energy = ProfiledEnergyStorage(WorkerEnergyStorage(::setChangedLight, upgrades.transform(ENERGY_VALUES)))

	val matterNode = object : MatterNode() {
		override fun getMatterHandler(): IMatterStorage {
			return matter
		}

		override fun onPatternAdded(state: IPatternState) {
			containerChanged()
		}

		override fun onPatternRemoved(state: IPatternState) {
			containerChanged()
		}

		override fun onPatternUpdated(newState: IPatternState, oldState: IPatternState) {
			containerChanged()
		}
	}

	init {
		exposeGlobally(MatteryCapability.MATTER, matter)
		exposeGlobally(MatteryCapability.MATTER_NODE, matterNode)

		savetables.stateful(::repairContainer)
		savetables.stateful(::matter)
		savetables.stateful(::energy)
		savetables.stateful(::upgrades)

		savetables.decimal(::matterPerTick)
		savetables.double(::progressPerTick)
		savetables.double(::repairProgress)

		savetables.Stateless(::lastItem, type = StringTag::class.java)
			.withSerializer { it?.registryName?.toString()?.let(StringTag::valueOf) }
			.withDeserializer { ResourceLocation.tryParse(it.asString)?.let { ForgeRegistries.ITEMS.getValue(it) } }
	}

	val energyConfig = ConfigurableEnergy(energy)
	val itemConfig = ConfigurableItemHandler(
		inputOutput = repairContainer.handler(object : HandlerFilter {
			override fun canInsert(slot: Int, stack: ItemStack): Boolean {
				if (!stack.isRepairable || !stack.isDamaged) {
					return false
				}

				if (ALLOW_TO_SKIP_ANVIL && !ONLY_ANVIL) {
					if (MatterManager.get(stack.item).hasMatterValue) {
						return true
					}
				}

				return matterNode.graph
					.patterns
					.filter { stack.item.isValidRepairItem(stack, ItemStack(it.item, 1)) }
					.findFirst().orElse(null).let {
						if (it == null) {
							IMatterValue.ZERO
						} else {
							MatterManager.get(it.item)
						}
					}.hasMatterValue
			}

			override fun canExtract(slot: Int, amount: Int, stack: ItemStack): Boolean {
				return progressPerTick <= 0.0 || matterPerTick <= Decimal.ZERO
			}
		})
	)

	override fun createMenu(containerID: Int, inventory: Inventory, ply: Player): AbstractContainerMenu {
		return MatterReconstructorMenu(containerID, inventory, this)
	}

	override fun setRemoved() {
		super.setRemoved()
		matterNode.isValid = false
	}

	override fun setLevel(level: Level) {
		super.setLevel(level)
		matterNode.discover(this)
	}

	private var changeset = 0

	private fun containerChanged() {
		matterPerTick = Decimal.ZERO
		progressPerTick = 0.0

		val item = repairContainer[0]
		val thisChangeset = ++changeset

		tickList.once {
			if (thisChangeset != changeset) return@once

			if (lastItem != item.item) {
				lastItem = item.item
				repairProgress = 0.0
				initialDamage = item.damageValue.toDouble()
				visualItemStack = item
				visualProgress = 0f
			}

			if (item.isEmpty || !item.isRepairable || !item.isDamaged) {
				matterPerTick = Decimal.ZERO
				progressPerTick = 0.0
				initialDamage = 0.0
				visualItemStack = item
				visualProgress = 0f
			} else {
				if (ALLOW_TO_SKIP_ANVIL && !ONLY_ANVIL) {
					val matter = MatterManager.get(item.item)

					if (matter.hasMatterValue) {
						failureChance = FAILURE_CHANCE
						progressPerTick = (item.maxDamage / matter.complexity) / DIVISOR
						matterPerTick = (matter.matter / matter.complexity) / DIVISOR
						return@once
					}
				}

				val found = matterNode.graph.patterns.filter { item.item.isValidRepairItem(item, ItemStack(it.item, 1)) }.findFirst().orElse(null)

				if (found != null) {
					failureChance = (1.0 - found.researchPercent) + FAILURE_CHANCE

					if (!ONLY_ANVIL) {
						val matter = MatterManager.get(item.item)

						if (matter.hasMatterValue) {
							progressPerTick = (item.maxDamage / matter.complexity) / DIVISOR
							matterPerTick = (matter.matter / matter.complexity) / DIVISOR
							return@once
						}
					}

					@Suppress("name_shadowing")
					val matter = MatterManager.get(found.item) * 2

					progressPerTick = (item.maxDamage / matter.complexity) / DIVISOR
					matterPerTick = (matter.matter / matter.complexity) / DIVISOR
				} else {
					matterPerTick = Decimal.ZERO
					progressPerTick = 0.0
					initialDamage = 0.0
					visualItemStack = item
					visualProgress = 0f
				}
			}
		}
	}

	override fun tick() {
		super.tick()

		if (!redstoneControl.isBlockedByRedstone) {
			isUnableToProcess = false

			val item = repairContainer[0]

			val thisProgressPerTick = progressPerTick * (1.0 + upgrades.speedBonus)
			val matterPerTick = matterPerTick * (1.0 + upgrades.speedBonus)
			val energyConsumption = ENERGY_CONSUMPTION * (1.0 + upgrades.speedBonus) * (upgrades.energyConsumed + Decimal.ONE)

			if (!item.isEmpty && matterPerTick.isPositive && thisProgressPerTick > 0.0 && item.isRepairable && item.isDamaged) {
				var progressPerTick = (repairProgress + thisProgressPerTick).coerceAtMost(item.damageValue.toDouble()) - repairProgress
				if (progressPerTick <= 0.0) return

				if (energyConsumption.isPositive) {
					if (!energy.batteryLevel.isPositive) return
					val multEnergy = energyConsumption * (progressPerTick / thisProgressPerTick)
					progressPerTick *= (energy.extractEnergy(multEnergy, true) / multEnergy).toDouble()

					if (progressPerTick <= 0.0) {
						isUnableToProcess = true
						return
					}
				}

				if (matter.storedMatter < matterPerTick) {
					val toDrain = (matterPerTick * EXTRACT_TICKS
						.coerceAtMost(item.damageValue) - matter.storedMatter)
						.coerceAtLeast(Decimal.ZERO)
						.coerceAtMost(matter.missingMatter)

					matter.receiveMatter(matterNode.graph.extractMatter(toDrain, false), false)
				}

				val toDrain = matterPerTick * (progressPerTick / thisProgressPerTick)
				val drain = matter.extractMatter(toDrain, true)

				if (!drain.isPositive) {
					isUnableToProcess = true
					return
				}

				progressPerTick *= (drain / toDrain).toDouble()

				if (progressPerTick <= 0.0) {
					isUnableToProcess = true
					return
				}

				if (failureChance * upgrades.failureMultiplier <= 0.0 || level!!.random.nextDouble() >= failureChance * upgrades.failureMultiplier)
					repairProgress += progressPerTick

				energy.extractEnergy(energyConsumption * (progressPerTick / thisProgressPerTick), false)
				matter.extractMatter(matterPerTick * (progressPerTick / thisProgressPerTick), false)

				if (repairProgress >= 1.0) {
					item.damageValue = (item.damageValue - repairProgress.toInt()).coerceAtLeast(0)
					repairProgress %= 1.0
				}

				visualItemStack = item
				visualProgress = 1f - ((item.damageValue - repairProgress) / initialDamage).toFloat()
			} else {
				initialDamage = 0.0
				visualItemStack = item
				visualProgress = 0f

				if (matter.storedMatter.isPositive) {
					matter.extractMatter(matterNode.graph.receiveMatter(matter.storedMatter, false), false)
				}
			}
		}
	}

	companion object {
		val CAPACITY get() = _CAPACITY.get()
		val ENERGY_CONSUMPTION get() = _ENERGY_CONSUMPTION.get()
		val DIVISOR: Double get() = _DIVISOR.get()
		val EXTRACT_TICKS: Int get() = _EXTRACT_TICKS.get()
		val FAILURE_CHANCE: Double get() = _FAILURE_CHANCE.get()
		val ONLY_ANVIL: Boolean get() = _ONLY_ANVIL.get()
		val ALLOW_TO_SKIP_ANVIL: Boolean get() = _ALLOW_TO_SKIP_ANVIL.get()

		private var _CAPACITY: DecimalConfigValue by WriteOnce()
		private var _ENERGY_CONSUMPTION: DecimalConfigValue by WriteOnce()
		private var _DIVISOR: ConfigValue<Double> by WriteOnce()
		private var _FAILURE_CHANCE: ConfigValue<Double> by WriteOnce()
		private var _EXTRACT_TICKS: ConfigValue<Int> by WriteOnce()
		private var _ONLY_ANVIL: ConfigValue<Boolean> by WriteOnce()
		private var _ALLOW_TO_SKIP_ANVIL: ConfigValue<Boolean> by WriteOnce()

		var ENERGY_VALUES: ConciseBalanceValues by WriteOnce()
			private set

		fun registerConfig(builder: ForgeConfigSpec.Builder) {
			builder.push(MNames.MATTER_RECONSTRUCTOR)

			ENERGY_VALUES = BlockEnergyStorageImpl.makeConfigEntry(builder, capacity = Decimal(400_000), throughput = Decimal(2_000))

			_CAPACITY = builder.defineDecimal("matterCapacity", Decimal(800), Decimal.ONE_TENTH)
			_ENERGY_CONSUMPTION = builder.defineDecimal("energyConsumption", Decimal(600), Decimal.ONE)

			_ALLOW_TO_SKIP_ANVIL = builder
				.comment("Allow to repair tools without having their anvil-repair items researched")
				.comment("Ignored if onlyAnvil is true")
				.define("allowToSkipAnvil", false)

			_ONLY_ANVIL = builder
				.comment("Force repairing only by matter value of anvil materials")
				.comment("Doesn't make logical sense but might be good for balancing")
				.define("onlyAnvil", false)

			_EXTRACT_TICKS = builder
				.comment("How much ticks of matter work will be attempted to be extracted from matter network")
				.comment("on matter starvation")
				.comment("---")
				.comment("This is a performance tuning value, do not change it if you don't know what you are doing!")
				.defineInRange("extractTicks", 200, 1, Int.MAX_VALUE)

			_FAILURE_CHANCE = builder
				.comment("Constant additional chance (over pattern research factor) that replication will fail at any given tick")
				.comment("In event of failure repair tick is wasted, wasting resources")
				.defineInRange("failureChance", 0.01, 0.0, 0.99)

			_DIVISOR = builder
				.comment("Magnitute of **slowdown** of repairer.")
				.comment("If this value is 1, repairer will repair item from 0% to 100% in COMPLEXITY (of tool) ticks")
				.comment("(or twice the complexity of anvil repair ingredients if tool itself has no matter value)")
				.comment("---")
				.comment("If value is smaller than 1, repairer will repair items faster")
				.comment("If value is bigger than 1, repairer will repair items slower")
				.defineInRange("divisor", 3.0, 0.1, Double.MAX_VALUE)

			builder.pop()
		}
	}
}
