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

import net.minecraft.core.BlockPos
import net.minecraft.nbt.CompoundTag
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.ItemStack
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.state.BlockState
import net.minecraftforge.common.ForgeConfigSpec
import ru.dbotthepony.mc.otm.block.entity.JobContainer
import ru.dbotthepony.mc.otm.block.entity.JobStatus
import ru.dbotthepony.mc.otm.block.entity.MachineItemJob
import ru.dbotthepony.mc.otm.block.entity.MachineJobEventLoop
import ru.dbotthepony.mc.otm.block.entity.MatteryWorkerBlockEntity
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.*
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.math.getDecimal
import ru.dbotthepony.mc.otm.core.math.set
import ru.dbotthepony.mc.otm.core.nbt.map
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.core.util.WriteOnce
import ru.dbotthepony.mc.otm.graph.matter.MatterNode
import ru.dbotthepony.mc.otm.matter.MatterManager
import ru.dbotthepony.mc.otm.menu.matter.MatterReplicatorMenu
import ru.dbotthepony.mc.otm.registry.MBlockEntities
import ru.dbotthepony.mc.otm.registry.MNames

class MatterReplicatorBlockEntity(p_155229_: BlockPos, p_155230_: BlockState) :
	MatteryWorkerBlockEntity<MatterReplicatorBlockEntity.ReplicatorJob>(MBlockEntities.MATTER_REPLICATOR, p_155229_, p_155230_, {
		try {
			ReplicatorJob(it)
		} catch(err: NoSuchElementException) {
			null
		}
	}) {

	class ReplicatorJob : MachineItemJob {
		val matterPerTick: Decimal
		val task: ReplicationTask
		var matterValue: Decimal
		val pattern: PatternState?
		val asDust: Boolean

		constructor(tag: CompoundTag) : super(tag) {
			matterPerTick = tag.getDecimal(MATTER_PER_TICK_KEY)
			matterValue = tag.getDecimal(MATTER_VALUE_KEY)
			pattern = tag.map(PATTERN_KEY, PatternState::deserializeNBT)
			asDust = tag.getBoolean(AS_DUST_KEY)
			task = tag.map(TASK_KEY, ReplicationTask::deserializeNBT) ?: throw NoSuchElementException("Unable to deserialize matter task")
		}

		constructor(
			itemStack: ItemStack,
			matterPerTick: Decimal,
			task: ReplicationTask,
			matterValue: Decimal,
			pattern: PatternState?,
			asDust: Boolean,
			ticks: Double,
		) : super(itemStack, ticks, BASE_CONSUMPTION) {
			this.matterPerTick = matterPerTick
			this.task = task
			this.matterValue = matterValue
			this.pattern = pattern
			this.asDust = asDust
		}

		override fun serializeNBT(): CompoundTag {
			return super.serializeNBT().also {
				it[MATTER_PER_TICK_KEY] = this.matterPerTick
				it[TASK_KEY] = this.task.serializeNBT()
				it[MATTER_VALUE_KEY] = this.matterValue

				if (this.pattern != null)
					it[PATTERN_KEY] = this.pattern.serializeNBT()

				it[AS_DUST_KEY] = this.asDust
			}
		}

		companion object {
			const val MATTER_PER_TICK_KEY = "matterPerTick"
			const val MATTER_VALUE_KEY = "matterValue"
			const val PATTERN_KEY = "pattern"
			const val AS_DUST_KEY = "asDust"
			const val TASK_KEY = "task"
		}
	}

	override val upgrades = UpgradeContainer(this::setChangedLight, 3, UpgradeType.REPLICATOR)
	val energy = ProfiledEnergyStorage(WorkerEnergyStorage(::energyLevelUpdated, upgrades.transform(ENERGY_VALUES)))
	val matter = ProfiledMatterStorage(MatterStorageImpl(::matterLevelUpdated, FlowDirection.INPUT, upgrades.matterCapacity(::MATTER_CAPACITY)))
	val container = MatteryContainer(::itemContainerUpdated, 5).also(::addDroppableContainer)

	val energyConfig = ConfigurableEnergy(energy)
	val itemConfig = ConfigurableItemHandler(output = container.handler(HandlerFilter.OnlyOut))

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

		override fun onMatterTaskCreated(task: IReplicationTask<*>) {
			jobEventLoops[0].notify(MachineJobEventLoop.IdleReason.OBSERVING)
		}

		override fun <T : IReplicationTask<*>> onMatterTaskUpdated(newState: T, oldState: T) {
			jobEventLoops[0].notify(MachineJobEventLoop.IdleReason.OBSERVING)
		}

		override fun onPatternAdded(state: IPatternState) {
			jobEventLoops[0].notify(MachineJobEventLoop.IdleReason.OBSERVING)
		}
	}

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

		savetables.stateful(::energy, ENERGY_KEY)
		savetables.stateful(::matter, MATTER_STORAGE_KEY)
		savetables.stateful(::container, INVENTORY_KEY)
		savetables.stateful(::upgrades)
	}

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

	override fun onJobFinish(job: ReplicatorJob, id: Int): JobStatus {
		if (job.asDust) {
			job.matterValue = moveMatterAsDustIntoContainer(job.matterValue, container, OUTPUT_DUST_MAIN, OUTPUT_DUST_STACKING)

			if (!job.matterValue.isZero) {
				return JobStatus.FAILURE_WAIT
			}

			matterNode.graph.notifyTaskCompletion(job.task.id)
			return JobStatus.SUCCESS
		}

		if (!container.fullyAddItem(job.itemStack, FIRST_ACTUAL_OUTPUT_SLOT .. LAST_ACTUAL_OUTPUT_SLOT)) {
			return JobStatus.FAILURE_ITEM
		}

		matterNode.graph.notifyTaskCompletion(job.task.id)
		return JobStatus.SUCCESS
	}

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

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

	override fun jobUpdated(new: ReplicatorJob?, old: ReplicatorJob?, id: Int) {
		visualItemStack = new?.itemStack ?: ItemStack.EMPTY
		visualProgress = 0f
	}

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

	var visualProgress by synchronizer.float().property
		private set

	var renderRotation = 0f
	var lastRender = 0L
	var particleRenderScore = 0L

	override fun tick() {
		super.tick()

		if (jobEventLoops[0].currentJob == null && matter.storedMatter.isPositive) {
			matter.extractMatter(matterNode.graph.receiveMatter(matter.storedMatter, false), false)
		}
	}

	override fun computeNextJob(id: Int): JobContainer<ReplicatorJob> {
		if (energy.batteryLevel < BASE_CONSUMPTION) {
			return JobContainer.noEnergy()
		}

		val allocation = matterNode.graph.allocateTask(simulate = false) ?: return JobContainer.observe()
		val stack = allocation.task.stack(1)
		val matter = MatterManager.get(stack)

		// ????????
		if (!matter.hasMatterValue) return JobContainer.failure()

		val ticks = matter.complexity

		return JobContainer.success(ReplicatorJob(
			itemStack = stack,
			matterPerTick = matter.matter / ticks,
			task = allocation.task.asImmutable(),
			matterValue = matter.matter,
			pattern = allocation.pattern?.asImmutable(),
			asDust = (level?.random?.nextDouble() ?: 1.0) * upgrades.failureMultiplier > (allocation.pattern?.researchPercent ?: 2.0),
			ticks = ticks,
		))
	}

	override fun onWorkTick(requiredPower: Decimal, extractedPower: Decimal, ticksAdvanced: Double, job: ReplicatorJob, id: Int): JobStatus {
		val drainPerTick = job.matterPerTick * ticksAdvanced

		if (matter.extractMatter(drainPerTick, true) < drainPerTick) {
			// в машине недостаточно материи

			if (drainPerTick > matter.maxStoredMatter) {
				// в тик требуется больше материи, чем её может хранить репликатор
				val toExtract = drainPerTick - matter.extractMatter(drainPerTick, true)
				val drain = matterNode.graph.extractMatter(toExtract, true)

				if (drain != toExtract) {
					// недостаточно материи в сети
					return JobStatus.FAILURE_MATTER
				}

				// достаточно материи в сети + внутри машины
				matter.extractMatter(drainPerTick, false)
				matterNode.graph.extractMatter(drain, false)
				return JobStatus.SUCCESS
			} else {
				// в тик требуется меньше материи, чем её может хранить репликатор
				// примем из сети недостающее количество бака материи, или 200 тиков репликации, что меньше
				val drain = matterNode.graph.extractMatter((drainPerTick * DRAIN_MULT)
													.coerceAtMost(job.matterPerTick * (job.ticks - jobEventLoops[0].workTicks - ticksAdvanced))
													.coerceAtLeast(Decimal.ONE)
													.coerceAtMost(matter.missingMatter), false)

				if (drain.isZero) {
					// в сети нет материи
					return JobStatus.FAILURE_MATTER
				}

				matter.receiveMatter(drain, false)

				// получили материю, проверяем возможность работы
				if (matter.extractMatter(drainPerTick, true) >= drainPerTick) {
					matter.extractMatter(drainPerTick, false)
					return JobStatus.SUCCESS
				} else {
					// :(
					return JobStatus.FAILURE_WAIT
				}
			}
		}

		// в машине достаточно материи
		matter.extractMatter(drainPerTick, false)
		visualProgress = jobEventLoops[0].workProgress
		return JobStatus.SUCCESS
	}

	companion object {
		private val BASE_CONSUMPTION get() = _BASE_CONSUMPTION.get()
		private val DRAIN_MULT get() = _DRAIN_MULT.get()
		private val MATTER_CAPACITY get() = _MATTER_CAPACITY.get()

		private var _BASE_CONSUMPTION: DecimalConfigValue by WriteOnce()
		private var _DRAIN_MULT: DecimalConfigValue by WriteOnce()
		private var _MATTER_CAPACITY: DecimalConfigValue by WriteOnce()

		var ENERGY_VALUES: ConciseBalanceValues by WriteOnce()
			private set

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

			ENERGY_VALUES = BlockEnergyStorageImpl.makeConfigEntry(builder, capacity = Decimal(200_000), throughput = Decimal(4_000))

			_BASE_CONSUMPTION = builder.defineDecimal("basePowerConsumption", Decimal(400), Decimal.ONE)
			_DRAIN_MULT = builder.comment("How much 'ticks' of replication should replicator drain matter from network when running low on internal matter buffer. This is ultimately a performance value.").defineDecimal("drainMultiplier", Decimal(200), Decimal.ONE)
			_MATTER_CAPACITY = builder.defineDecimal("matterCapacity", Decimal(1_000), Decimal.ONE_TENTH)

			builder.pop()
		}

		const val FIRST_ACTUAL_OUTPUT_SLOT = 0
		const val LAST_ACTUAL_OUTPUT_SLOT = 2
		const val OUTPUT_DUST_MAIN = 3
		const val OUTPUT_DUST_STACKING = 4
	}
}
