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

import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.nbt.DoubleTag
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.ExperienceOrb
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.state.BlockState
import net.minecraftforge.common.util.INBTSerializable
import net.minecraftforge.fluids.FluidStack
import net.minecraftforge.fluids.capability.IFluidHandler
import ru.dbotthepony.mc.otm.block.INeighbourChangeListener
import ru.dbotthepony.mc.otm.block.entity.tech.EssenceStorageBlockEntity
import ru.dbotthepony.mc.otm.core.math.plus
import ru.dbotthepony.mc.otm.registry.MFluids
import java.util.function.DoubleSupplier

class ExperienceStorage(val maxExperience: DoubleSupplier = DoubleSupplier { Double.POSITIVE_INFINITY }) : IFluidHandler, INBTSerializable<DoubleTag?>, INeighbourChangeListener {
	constructor(max: Double) : this({ max })

	var experience = 0.0
		private set(value) {
			require(value >= 0.0) { "Negative experience: $value" }
			field = value
		}

	fun popExperience(player: ServerPlayer) {
		val whole = experience.toInt()

		if (whole > 0) {
			experience -= whole
			ExperienceOrb.award(player.level() as ServerLevel, player.position(), whole)
		}
	}

	fun storeExperience(experience: Double): Boolean {
		check(experience >= 0.0) { "Invalid experience amount to store: $experience" }
		val max = maxExperience.asDouble
		val overflow = this.experience + experience > max
		this.experience = if (overflow) max else this.experience + experience
		return !overflow
	}

	fun storeExperience(experience: Float): Boolean {
		return storeExperience(experience.toDouble())
	}

	fun storeExperience(experience: Double, pointOfReference: BlockEntity): Boolean {
		check(experience >= 0.0) { "Invalid experience amount to store: $experience" }
		this.experience += experience

		for (dir in Direction.entries) {
			val tile = pointOfReference.level?.getBlockEntity(pointOfReference.blockPos + dir)

			if (tile is EssenceStorageBlockEntity) {
				tile.experienceStored += this.experience.toLong()
				this.experience %= 1.0
				break
			}
		}

		val max = maxExperience.asDouble
		val overflow = this.experience > max
		if (overflow) this.experience = max
		return !overflow
	}

	fun storeExperience(experience: Float, pointOfReference: BlockEntity): Boolean {
		return storeExperience(experience.toDouble(), pointOfReference)
	}

	fun tryTransferExperience(pointOfReference: BlockEntity) {
		if (experience >= 1.0) {
			for (dir in Direction.entries) {
				val tile = pointOfReference.level?.getBlockEntity(pointOfReference.blockPos + dir)

				if (tile is EssenceStorageBlockEntity) {
					tile.experienceStored += experience.toLong()
					experience %= 1.0
					break
				}
			}
		}
	}

	override fun neighborChanged(state: BlockState, level: Level, pos: BlockPos, neighbour: Block, neighbourPos: BlockPos, movedByPiston: Boolean) {
		if (experience >= 1.0) {
			val tile = level.getBlockEntity(neighbourPos)

			if (tile is EssenceStorageBlockEntity) {
				tile.experienceStored += experience.toLong()
				experience %= 1.0
			}
		}
	}

	override fun serializeNBT(): DoubleTag {
		return DoubleTag.valueOf(experience)
	}

	override fun deserializeNBT(nbt: DoubleTag?) {
		experience = (nbt?.asDouble ?: 0.0).coerceAtLeast(0.0)
	}

	override fun getTanks(): Int {
		return 1
	}

	override fun getFluidInTank(tank: Int): FluidStack {
		if (tank != 0)
			return FluidStack.EMPTY

		return FluidStack(MFluids.LIQUID_XP, (experience * XP_TO_LIQUID_RATIO).toInt())
	}

	override fun getTankCapacity(tank: Int): Int {
		return if (tank == 0) (maxExperience.asDouble * XP_TO_LIQUID_RATIO).toInt() else 0
	}

	override fun isFluidValid(tank: Int, stack: FluidStack): Boolean {
		return tank == 0 && stack.fluid.fluidType == MFluids.LIQUID_XP_TYPE
	}

	override fun fill(resource: FluidStack, action: IFluidHandler.FluidAction): Int {
		return 0
	}

	override fun drain(resource: FluidStack, action: IFluidHandler.FluidAction): FluidStack {
		if (resource.fluid.fluidType != MFluids.LIQUID_XP_TYPE)
			return FluidStack.EMPTY

		return drain(resource.amount, action)
	}

	override fun drain(maxDrain: Int, action: IFluidHandler.FluidAction): FluidStack {
		val actualDrain = maxDrain.coerceAtMost((experience * XP_TO_LIQUID_RATIO).toInt()).let { it / XP_TO_LIQUID_RATIO * XP_TO_LIQUID_RATIO }

		if (actualDrain <= 0)
			return FluidStack.EMPTY

		if (action.execute())
			experience -= actualDrain / XP_TO_LIQUID_RATIO

		return FluidStack(MFluids.LIQUID_XP, actualDrain)
	}

	companion object {
		const val XP_TO_LIQUID_RATIO = 10
	}
}
