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

import com.mojang.datafixers.Products
import com.mojang.serialization.Codec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtOps
import net.minecraft.world.item.ItemStack
import net.minecraftforge.common.util.INBTSerializable
import org.apache.logging.log4j.LogManager
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.weakEqualDoubles
import ru.dbotthepony.mc.otm.core.math.weakGreaterThan
import ru.dbotthepony.mc.otm.core.math.weakLessThan
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.data.DecimalCodec
import ru.dbotthepony.mc.otm.data.minRange

private fun isReason(status: Any?, reason: Any) = status == null || status == reason
private val LOGGER = LogManager.getLogger()

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

open class Job(
	final override val ticks: Double,
	final override val powerUsage: Decimal = Decimal.ZERO,
	final override val experience: Float = 0f
) : IJob {
	companion object {
		fun <T : Job> basicCodec(builder: RecordCodecBuilder.Instance<T>): Products.P3<RecordCodecBuilder.Mu<T>, Double, Decimal, Float> {
			return builder.group(
				Codec.doubleRange(0.0, Double.MAX_VALUE).fieldOf("Ticks").forGetter(Job::ticks),
				DecimalCodec.fieldOf("EnergyUsage").forGetter(Job::powerUsage), // не надо указывать минимальную энергию как 0,
				// ибо мы можем таким образом использовать это для создания работ генератора
				Codec.floatRange(0f, Float.MAX_VALUE).optionalFieldOf("Experience", 0f).forGetter(Job::experience),
			)
		}

		fun <T : Job> plainCodec(builder: RecordCodecBuilder.Instance<T>): Products.P2<RecordCodecBuilder.Mu<T>, Double, Decimal> {
			return builder.group(
				Codec.doubleRange(0.0, Double.MAX_VALUE).fieldOf("Ticks").forGetter(Job::ticks),
				DecimalCodec.fieldOf("EnergyUsage").forGetter(Job::powerUsage), // не надо указывать минимальную энергию как 0,
				// ибо мы можем таким образом использовать это для создания работ генератора
			)
		}

		val CODEC: Codec<Job> by lazy {
			RecordCodecBuilder.create {
				basicCodec(it).apply(it, ::Job)
			}
		}
	}
}

open class ItemJob(
	val itemStack: ItemStack,
	ticks: Double,
	power: Decimal = Decimal.ZERO,
	experience: Float = 0f
) : Job(ticks, power, experience) {
	companion object {
		fun <T : ItemJob> itemCodec(builder: RecordCodecBuilder.Instance<T>): Products.P4<RecordCodecBuilder.Mu<T>, ItemStack, Double, Decimal, Float> {
			return builder.group(ItemStack.CODEC.fieldOf("Item").forGetter(ItemJob::itemStack)).and(basicCodec(builder))
		}

		val CODEC: Codec<ItemJob> by lazy {
			RecordCodecBuilder.create {
				itemCodec(it).apply(it, ::ItemJob)
			}
		}
	}
}

class JobStatus<T : IJob>(
	var requiredPower: Decimal,
	val extractedPower: Decimal,
	ticksAdvanced: Double,
	val job: T,
	val workTicks: Double,
) : IJob by job {
	constructor(job: T, workTicks: Double) : this(Decimal.ZERO, Decimal.ZERO, 0.0, job, workTicks)

	val workProgress: Float
		get() {
			return ((workTicks + workTicks) / job.ticks).coerceAtMost(1.0).toFloat()
		}

	var success = true

	var ticksAdvanced = ticksAdvanced
		set(value) {
			require(value >= 0.0) { "Invalid amount of ticks to advance: $value" }
			field = value
		}

	var throttleTicks = 0
		set(value) {
			require(value >= 0) { "Invalid amount of ticks to throttle: $value" }
			if (value != 0) success = false
			field = value
		}

	var idleReason: Any? = null
		set(value) {
			if (value != null) success = false
			field = value
		}

	/**
	 * You don't have to actually call this, JobStatus is in this state by default
	 *
	 * But this is handy to call this when doing early returns, to clearly state that "we are in success"
	 */
	fun success() {
		this.success = true
		this.throttleTicks = 0
		this.idleReason = null
	}

	fun throttle() {
		this.success = false
		this.throttleTicks = 100
	}

	fun throttleFast() {
		this.success = false
		this.throttleTicks = 20
	}

	fun noItem(throttleTicks: Int = 20) {
		this.success = false
		idleReason = MachineJobEventLoop.IdleReason.ITEM
		this.throttleTicks = throttleTicks
	}

	fun noPower(throttleTicks: Int = 20) {
		this.success = false
		idleReason = MachineJobEventLoop.IdleReason.POWER
		this.throttleTicks = throttleTicks
	}

	fun noMatter(throttleTicks: Int = 20) {
		this.success = false
		idleReason = MachineJobEventLoop.IdleReason.MATTER
		this.throttleTicks = throttleTicks
	}

	fun noPattern(throttleTicks: Int = 20) {
		this.success = false
		idleReason = MachineJobEventLoop.IdleReason.PATTERN
		this.throttleTicks = throttleTicks
	}

	fun observe(throttleTicks: Int = 20) {
		this.success = false
		idleReason = MachineJobEventLoop.IdleReason.OBSERVING
		this.throttleTicks = throttleTicks
	}

	fun failure(reason: Any) {
		this.success = false
		idleReason = reason
	}

	fun failure(throttleTicks: Int = 0) {
		this.success = false
		this.throttleTicks = throttleTicks
	}

	fun scale(value: Decimal) {
		require(value >= Decimal.ZERO) { "Attempted to scale by negative value: $value" }
		if (value == Decimal.ONE)
			return

		if (value.isZero) {
			ticksAdvanced = 0.0
			requiredPower = Decimal.ZERO
			success = false
		} else {
			requiredPower *= value
			ticksAdvanced *= value.toDouble()
		}
	}

	fun scale(value: Double) {
		require(value >= 0.0) { "Attempted to scale by negative value: $value" }
		if (value == 1.0)
			return

		if (value == 0.0) {
			ticksAdvanced = 0.0
			requiredPower = Decimal.ZERO
			success = false
		} else {
			requiredPower *= value
			ticksAdvanced *= value
		}
	}
}

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

		if (job != null && idleReason != null) {
			throw IllegalArgumentException("Can't have both job and idle reason specified")
		}

		if (job != null && throttleTicks != 0) {
			throw IllegalArgumentException("Can't have both job and throttle ticks specified")
		}
	}

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

		fun <JobType : IJob> 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 : IJob> failure(): JobContainer<JobType> {
			return empty as JobContainer<JobType>
		}

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

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

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

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

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

abstract class MachineJobEventLoop<JobType : IJob>(val codec: Codec<JobType>) : INBTSerializable<CompoundTag?> {
	protected abstract val energy: IMatteryEnergyStorage?
	protected abstract val isBlockedByRedstone: Boolean
	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
		}
	}

	private var lastTickWasError = false

	val isUnableToProcess: Boolean
		get() = throttleTicks > 0 || lastTickWasError

	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 {
				codec.encode(it, NbtOps.INSTANCE, NbtOps.INSTANCE.empty()).get().map(
					{
						nbt["Job"] = it
					},
					{
						LOGGER.error("Failed to serialize job data, it will not persist", RuntimeException(it.message()))
					}
				)
			}
		}
	}

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

		workTicks = nbt.getDouble("WorkTicks")
		currentJob = null

		if ("Job" in nbt) {
			codec.decode(NbtOps.INSTANCE, nbt["Job"]!!).get().map(
				{
					currentJob = it.first
				},
				{
					LOGGER.error("Failed to deserialize job data from storage", RuntimeException(it.message()))
				}
			)
		}

		if (currentJob == null)
			workTicks = 0.0
	}

	protected open fun jobUpdated(new: JobType?, old: JobType?) {}

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

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

	protected open fun onJobTick(status: JobStatus<JobType>) {}

	/**
	 * 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
		}

		lastTickWasError = 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++
				lastTickWasError = true
				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 if (requiredPower.isZero) {
						ticksAdvanced = ticksLeft
					} else {
						extractedPower = -energy!!.receiveEnergy(-requiredPower, true)
						ticksAdvanced = (extractedPower / requiredPower).toDouble().coerceIn(0.0, 1.0) * ticksLeft
					}
				}

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

				val status = JobStatus(requiredPower ?: Decimal.ZERO, extractedPower ?: Decimal.ZERO, ticksAdvanced, currentJob, workTicks)
				onJobTick(status)

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

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

					break
				} else if (status.requiredPower.isPositive && energy == null) {
					idleReason = IdleReason.POWER
					isIdling = true
					idleTicksAnim++
					break
				}

				workingTicksAnim++
				errorTicksAnim = 0

				workTicks += status.ticksAdvanced
				availableTicks -= status.ticksAdvanced

				if (energy != null && status.requiredPower.isPositive) {
					energy.extractEnergy(status.requiredPower, false)
				} else if (energy != null && status.requiredPower.isNegative) {
					energy.receiveEnergy(-status.requiredPower, false)
				}

				continue
			}

			val status = JobStatus(currentJob, workTicks)
			onJobFinish(status)

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

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

				errorTicksAnim++
				break
			}
		}
	}
}
