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

import net.minecraft.nbt.CompoundTag
import net.minecraft.world.item.ItemStack
import net.minecraftforge.common.util.INBTSerializable
import ru.dbotthepony.mc.otm.capability.IMatteryUpgrade
import ru.dbotthepony.mc.otm.capability.energy.IMatteryEnergyStorage
import ru.dbotthepony.mc.otm.config.MachinesConfig
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.math.getDecimal
import ru.dbotthepony.mc.otm.core.math.set
import ru.dbotthepony.mc.otm.core.math.weakEqualDoubles
import ru.dbotthepony.mc.otm.core.math.weakGreaterThan
import ru.dbotthepony.mc.otm.core.math.weakLessThan
import ru.dbotthepony.mc.otm.core.nbt.map
import ru.dbotthepony.mc.otm.core.nbt.set

private fun isReason(status: Any?, reason: Any) = status == null || status == reason

interface IMachineJob {
	val ticks: Double
	val powerUsage: Decimal
	val experience: Float get() = 0f

	fun serializeNBT(): CompoundTag
}

open class MachineJob : IMachineJob {
	final override val ticks: Double
	final override val powerUsage: Decimal
	final override val experience: Float

	constructor(
		ticks: Double,
		powerUsage: Decimal = Decimal.ZERO,
		experience: Float = 0f,
	) {
		this.ticks = ticks
		this.powerUsage = powerUsage
		this.experience = experience
	}

	constructor(
		tag: CompoundTag
	) : this(tag.getDouble("Ticks"), tag.getDecimal("EnergyUsage"), tag.getFloat("Experience"))

	override fun serializeNBT(): CompoundTag {
		return CompoundTag().also {
			it["Ticks"] = ticks
			it["EnergyUsage"] = powerUsage
			it["Experience"] = experience
		}
	}
}

open class MachineItemJob : MachineJob {
	val itemStack: ItemStack

	constructor(
		itemStack: ItemStack,
		ticks: Double,
		power: Decimal = Decimal.ZERO,
		experience: Float = 0f,
	) : super(ticks, power, experience) {
		this.itemStack = itemStack
	}

	constructor(
		tag: CompoundTag
	) : super(tag) {
		this.itemStack = (tag["Item"] as? CompoundTag)?.let { ItemStack.of(it) } ?: ItemStack.EMPTY
	}

	override fun serializeNBT(): CompoundTag {
		return super.serializeNBT().also {
			it["Item"] = itemStack.serializeNBT()
		}
	}
}

data class JobStatus(
	val success: Boolean,
	val throttleTicks: Int = 0,
	val idleReason: MachineJobEventLoop.IdleReason? = null,
	val newDrainedPower: Decimal? = null
) {
	init {
		require(throttleTicks >= 0) { "Negative amount of ticks to throttle: $throttleTicks" }
	}

	companion object {
		val SUCCESS = JobStatus(true)
		val FAILURE = JobStatus(false)
		val FAILURE_ITEM = JobStatus(false, 20, MachineJobEventLoop.IdleReason.ITEM)
		val FAILURE_MATTER = JobStatus(false, 20, MachineJobEventLoop.IdleReason.MATTER)
		val FAILURE_WAIT = JobStatus(false, 100)
		val FAILURE_WAIT_FAST = JobStatus(false, 20)
	}
}

data class JobContainer<JobType : IMachineJob>(val job: JobType? = null, val idleReason: Any? = null, val throttleTicks: Int = 0) {
	init {
		require(throttleTicks >= 0) { "Negative amount of ticks to throttle: $throttleTicks" }
	}

	companion object {
		fun <JobType : IMachineJob> success(job: JobType): JobContainer<JobType> {
			return JobContainer(job, null)
		}

		fun <JobType : IMachineJob> failure(reason: Any?): JobContainer<JobType> {
			return JobContainer(null, reason)
		}

		private val empty = JobContainer(null, null)
		private val noItem = JobContainer(null, MachineJobEventLoop.IdleReason.ITEM)
		private val noEnergy = JobContainer(null, MachineJobEventLoop.IdleReason.POWER)
		private val noPattern = JobContainer(null, MachineJobEventLoop.IdleReason.PATTERN)
		private val noMatter = JobContainer(null, MachineJobEventLoop.IdleReason.MATTER)
		private val observe = JobContainer(null, MachineJobEventLoop.IdleReason.OBSERVING)

		@Suppress("unchecked_cast")
		fun <JobType : IMachineJob> failure(): JobContainer<JobType> {
			return empty as JobContainer<JobType>
		}

		@Suppress("unchecked_cast")
		fun <JobType : IMachineJob> noItem(): JobContainer<JobType> {
			return noItem as JobContainer<JobType>
		}

		@Suppress("unchecked_cast")
		fun <JobType : IMachineJob> noEnergy(): JobContainer<JobType> {
			return noEnergy as JobContainer<JobType>
		}

		@Suppress("unchecked_cast")
		fun <JobType : IMachineJob> noMatter(): JobContainer<JobType> {
			return noMatter as JobContainer<JobType>
		}

		@Suppress("unchecked_cast")
		fun <JobType : IMachineJob> noPattern(): JobContainer<JobType> {
			return noPattern as JobContainer<JobType>
		}

		@Suppress("unchecked_cast")
		fun <JobType : IMachineJob> observe(): JobContainer<JobType> {
			return observe as JobContainer<JobType>
		}
	}
}

abstract class MachineJobEventLoop<JobType : IMachineJob> : INBTSerializable<CompoundTag?> {
	protected abstract val energy: IMatteryEnergyStorage?
	protected abstract val isBlockedByRedstone: Boolean
	protected abstract fun deserializeJob(nbt: CompoundTag): JobType?
	protected abstract val upgrades: IMatteryUpgrade?

	var currentJob: JobType? = null
		set(value) {
			if (field != value) {
				val old = field
				field = value
				jobUpdated(value, old)
			}
		}

	var workTicks = 0.0
		protected set
	var throttleTicks = 0
		protected set
	var idleTicksAnim = 0
		private set
	var workingTicksAnim = 0
		private set
	var errorTicksAnim = 0
		private set

	/**
	 * Can be whatever you want, but [IdleReason] certainly contains all cases
	 */
	var idleReason: Any? = null
		protected set

	open var isIdling = false

	open fun notify(event: Any) {
		if (isReason(idleReason, event)) {
			isIdling = false
			throttleTicks = 0
		}
	}

	val isUnableToProcess: Boolean
		get() = throttleTicks > 0

	val workProgress: Float
		get() {
			val currentJob = currentJob ?: return 0.0f
			return (workTicks / currentJob.ticks).coerceAtMost(1.0).toFloat()
		}

	enum class IdleReason {
		ITEM,
		POWER,
		MATTER,
		PATTERN,
		OBSERVING
	}

	override fun serializeNBT(): CompoundTag {
		return CompoundTag().also { nbt ->
			nbt["WorkTicks"] = workTicks
			currentJob?.let { nbt["Job"] = it.serializeNBT() }
		}
	}

	override fun deserializeNBT(nbt: CompoundTag?) {
		nbt ?: return

		workTicks = nbt.getDouble("WorkTicks")
		currentJob = nbt.map("Job", ::deserializeJob)

		if (currentJob == null)
			workTicks = 0.0
	}

	protected abstract fun jobUpdated(new: JobType?, old: JobType?)

	/**
	 * Called whenever reaching desired amount of ticks at job
	 */
	protected abstract fun onJobFinish(job: JobType): JobStatus

	/**
	 * Called when there is nothing to do and we are not idling
	 */
	protected abstract fun computeNextJob(): JobContainer<JobType>

	protected open fun onWorkTick(requiredPower: Decimal, extractedPower: Decimal, ticksAdvanced: Double, job: JobType): JobStatus {
		return JobStatus.SUCCESS
	}

	/**
	 * Advances job loop by specified [ticks]. If machine is speed up by upgrades,
	 * [ticks] should reflect this, same as slowdown.
	 */
	fun think(ticks: Double = 1.0) {
		if (throttleTicks > 0) {
			workingTicksAnim = 0
			idleTicksAnim = 0
			throttleTicks--
			errorTicksAnim++

			if (throttleTicks > 0)
				return
			else
				isIdling = false
		}

		if (isIdling) {
			workingTicksAnim = 0
			errorTicksAnim = 0
			idleTicksAnim++
			return
		}

		val upgrades = upgrades ?: IMatteryUpgrade.Companion
		var availableTicks = ticks * (1.0 + upgrades.speedBonus.coerceIn(MachinesConfig.Upgrades.MIN_SPEED, MachinesConfig.Upgrades.MAX_SPEED))
		val energy = energy

		while (!isIdling && weakGreaterThan(availableTicks, 0.0) && throttleTicks <= 0) {
			if (isBlockedByRedstone) {
				isIdling = true
				break
			}

			var currentJob = currentJob

			if (currentJob == null) {
				val (job, reason, throttleTicks) = computeNextJob()

				if (job == null) {
					this.throttleTicks = throttleTicks
					this.idleReason = reason
					this.isIdling = reason != null
					workingTicksAnim = 0
					break
				}

				this.currentJob = job
				currentJob = job
			}

			if (!currentJob.powerUsage.isZero && (energy == null || energy.batteryLevel.isZero)) {
				idleReason = IdleReason.POWER
				isIdling = true
				idleTicksAnim++
				break
			}

			idleTicksAnim = 0

			if (weakLessThan(workTicks, currentJob.ticks)) {
				val ticksLeft = (currentJob.ticks - workTicks).coerceAtMost(availableTicks)
				val ticksAdvanced: Double

				var requiredPower: Decimal? = null
				var extractedPower: Decimal? = null

				if (currentJob.powerUsage.isZero) {
					ticksAdvanced = ticksLeft
				} else {
					requiredPower = currentJob.powerUsage * (Decimal.ONE + upgrades.energyConsumed.coerceIn(MachinesConfig.Upgrades.MIN_ENERGY, MachinesConfig.Upgrades.MAX_ENERGY)) * ticksLeft

					if (requiredPower.isPositive) {
						extractedPower = energy!!.extractEnergy(requiredPower, true)
						ticksAdvanced = (extractedPower / requiredPower).toDouble().coerceIn(0.0, 1.0) * ticksLeft
					} else {
						ticksAdvanced = ticksLeft
					}
				}

				if (weakEqualDoubles(ticksAdvanced, 0.0)) {
					break
				}

				val status = onWorkTick(requiredPower ?: Decimal.ZERO, extractedPower ?: Decimal.ZERO, ticksAdvanced, currentJob)

				if (!status.success) {
					throttleTicks += status.throttleTicks

					if (status.idleReason != null) {
						idleReason = status.idleReason
						isIdling = true
					}

					break
				}

				workingTicksAnim++
				errorTicksAnim = 0

				workTicks += ticksAdvanced
				availableTicks -= ticksAdvanced

				extractedPower = status.newDrainedPower ?: extractedPower

				if (extractedPower != null) {
					energy!!.extractEnergy(extractedPower, false)
				}

				continue
			}

			val status = onJobFinish(currentJob)

			if (status.success) {
				this.currentJob = null
				workTicks = 0.0
				errorTicksAnim = 0
			} else {
				throttleTicks += status.throttleTicks

				if (status.idleReason != null) {
					idleReason = status.idleReason
					isIdling = true
				}

				errorTicksAnim++
				break
			}
		}
	}
}
