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

import it.unimi.dsi.fastutil.ints.Int2ObjectFunction
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.Reference2IntMap
import net.minecraft.core.BlockPos
import net.minecraft.core.SectionPos
import net.minecraft.core.Vec3i
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.AbstractContainerMenu
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.state.BlockState
import ru.dbotthepony.mc.otm.block.entity.MatteryBlockEntity
import ru.dbotthepony.mc.otm.block.entity.MatteryDeviceBlockEntity
import ru.dbotthepony.mc.otm.capability.FlowDirection
import ru.dbotthepony.mc.otm.capability.energy.IMatteryEnergyStorage
import ru.dbotthepony.mc.otm.capability.energy.ProfiledEnergyStorage
import ru.dbotthepony.mc.otm.config.MachinesConfig
import ru.dbotthepony.mc.otm.core.getBlockStateNow
import ru.dbotthepony.mc.otm.core.getChunkNow
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.math.plus
import ru.dbotthepony.mc.otm.core.math.times
import ru.dbotthepony.mc.otm.core.multiblock.MultiblockStatus
import ru.dbotthepony.mc.otm.core.multiblock.ShapedMultiblock
import ru.dbotthepony.mc.otm.core.multiblock.ShapedMultiblockFactory
import ru.dbotthepony.mc.otm.core.multiblock.Strategy
import ru.dbotthepony.mc.otm.core.multiblock.shapedMultiblock
import ru.dbotthepony.mc.otm.data.FlywheelMaterials
import ru.dbotthepony.mc.otm.menu.tech.FlywheelBatteryMenu
import ru.dbotthepony.mc.otm.registry.game.MBlockEntities
import ru.dbotthepony.mc.otm.registry.game.MBlocks

class FlywheelBatteryBlockEntity(blockPos: BlockPos, blockState: BlockState) : MatteryDeviceBlockEntity(MBlockEntities.FLYWHEEL_BATTERY, blockPos, blockState), EnergyInterfaceBlockEntity.Target {
	private var multiblock: ShapedMultiblock? = null
	private var lastHeight = -1
	var batteryLevel = Decimal.ZERO
		set(value) {
			if (field != value) {
				field = value
				markDirtyFast()
			}
		}

	private var cachedChargeEfficiency = Decimal.ONE

	val formed: Boolean
		get() = multiblock?.isValid == MultiblockStatus.VALID

	// dangerous as Reference2IntMap.Entry reference live data, and its behavior is undefined once flywheel updates again
	val currentlyUsedCore: Reference2IntMap.Entry<Block>? get() {
		return multiblock?.blocks(FLYWHEEL_MATERIAL)?.reference2IntEntrySet()?.firstOrNull()
	}

	var currentLossPerTick: Decimal = Decimal.ZERO
		private set

	private inner class Storage : IMatteryEnergyStorage {
		override fun extractEnergy(howMuch: Decimal, simulate: Boolean): Decimal {
			if (batteryLevel <= Decimal.ZERO) {
				return Decimal.ZERO
			}

			val new = maxOf(batteryLevel - howMuch, Decimal.ZERO)
			val diff = batteryLevel - new

			if (!simulate)
				batteryLevel = new

			return diff
		}

		override fun receiveEnergy(howMuch: Decimal, simulate: Boolean): Decimal {
			if (batteryLevel >= maxBatteryLevel) {
				return Decimal.ZERO
			}

			val new = minOf(batteryLevel + howMuch * cachedChargeEfficiency, maxBatteryLevel)
			val diff = new - batteryLevel

			if (!simulate)
				batteryLevel = new

			return diff
		}

		override var batteryLevel: Decimal by this@FlywheelBatteryBlockEntity::batteryLevel
		override var maxBatteryLevel: Decimal = Decimal.ZERO
		override val energyFlow: FlowDirection
			get() = FlowDirection.BI_DIRECTIONAL

		override val canSetBatteryLevel: Boolean
			get() = true
	}

	private val energy = ProfiledEnergyStorage(Storage())

	override val energyInterfaceTarget: ProfiledEnergyStorage<*>
		get() = energy

	init {
		savetables.stateful(::energy, "energy")
		savetablesLevel.decimal(::batteryLevel, "battery")

		exposeEnergySideless(energy)
	}

	override fun setRemoved() {
		super.setRemoved()

		lastHeight = -1
		multiblock?.close()
		multiblock = null
	}

	override fun tick() {
		super.tick()

		val level = level!!
		var isUnloaded = false

		if (multiblock == null || multiblock!!.update(level, blockRotation.front) != MultiblockStatus.VALID) {
			val chunk = level.chunkSource.getChunkNow(blockPos)

			if (chunk == null) {
				isUnloaded = true
			} else {
				var height = 0
				val base = blockPos + blockRotation.back.normal * 2

				while (chunk.getBlockState(base.atY(blockPos.y + height)).block == MBlocks.FLYWHEEL_SHAFT) {
					height++
				}

				if (height <= 1) {
					lastHeight = 0
					multiblock?.close()
					multiblock = null
				} else if (multiblock == null || lastHeight != height) {
					lastHeight = height
					multiblock?.close()
					multiblock = getConfiguration(height).create(blockPos)
				}
			}
		}

		if (multiblock?.isValid == MultiblockStatus.VALID) {
			val entry = multiblock!!.blocks(FLYWHEEL_MATERIAL).reference2IntEntrySet().first()
			val material = FlywheelMaterials[entry.key]!!
			energy.parent.maxBatteryLevel = material.storage * entry.intValue
			cachedChargeEfficiency = material.receiveEfficiency
			currentLossPerTick = energy.parent.batteryLevel * MachinesConfig.Flywheel.PASSIVE_LOSS * material.momentumLossSpeed
			energy.extractEnergy(currentLossPerTick, false)
		} else if (!isUnloaded && multiblock?.isValid != MultiblockStatus.NOT_LOADED) {
			energy.parent.maxBatteryLevel = Decimal.ZERO
			currentLossPerTick = energy.parent.batteryLevel * MachinesConfig.Flywheel.ACTIVE_LOSS
			energy.extractEnergy(currentLossPerTick, false)
		}
	}

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

	companion object {
		private val FLYWHEEL_MATERIAL = Any()
		private val outerRing = listOf(
			// front
			BlockPos(-1, 0, 0),
			BlockPos(-2, 0, 0),
			BlockPos( 0, 0, 0),
			BlockPos( 1, 0, 0),
			BlockPos( 2, 0, 0),

			// back
			BlockPos(-1, 0, 4),
			BlockPos(-2, 0, 4),
			BlockPos( 0, 0, 4),
			BlockPos( 1, 0, 4),
			BlockPos( 2, 0, 4),

			// right side
			BlockPos(-2, 0, 1),
			BlockPos(-2, 0, 2),
			BlockPos(-2, 0, 3),

			// left side
			BlockPos( 2, 0, 1),
			BlockPos( 2, 0, 2),
			BlockPos( 2, 0, 3),
		)

		private val configCache = Int2ObjectOpenHashMap<ShapedMultiblockFactory>()

		private fun getConfiguration(height: Int): ShapedMultiblockFactory {
			return configCache.computeIfAbsent(height, Int2ObjectFunction { createConfiguration(it) })
		}

		private fun createConfiguration(height: Int): ShapedMultiblockFactory {
			return shapedMultiblock {
				strategy = Strategy.AND
				block(MBlocks.FLYWHEEL_BATTERY)
				tag(EnergyInterfaceBlockEntity.TARGET)

				builder.customCheck {
					it.blocks(FLYWHEEL_MATERIAL).size == 1
				}

				for (x in -2 .. 2) {
					for (z in 0 .. 4) {
						relative(Vec3i(x, -1, z)) {
							if (x == 0 && z == 2) {
								block(MBlocks.FLYWHEEL_BEARING)
							} else {
								block(MBlocks.FLYWHEEL_HOUSING)
							}
						}

						relative(Vec3i(x, height, z)) {
							if (x == 0 && z == 2) {
								block(MBlocks.FLYWHEEL_BEARING)
							} else {
								block(MBlocks.FLYWHEEL_HOUSING)
							}
						}
					}
				}

				for (y in 0 until height) {
					for (x in -1 .. 1) {
						for (z in 1 .. 3) {
							relative(Vec3i(x, y, z)) {
								if (z == 2 && x == 0) {
									block(MBlocks.FLYWHEEL_SHAFT)
								} else {
									if (y == height - 1) {
										block(MBlocks.GENERATOR_BLOCK)
									} else {
										predicate { pos, access, blockState, blockEntity ->
											blockState in FlywheelMaterials
										}

										tagBlock(FLYWHEEL_MATERIAL)
									}
								}
							}
						}
					}

					for (rPos in outerRing) {
						if (y == 0 && rPos.x == 0) continue

						relative(rPos.atY(y)) {
							block(MBlocks.FLYWHEEL_HOUSING)

							if (y == height - 1) {
								// wrap in subnode to narrow blockentity lookups
								or {
									block(MBlocks.ENERGY_INPUT_INTERFACE)
									block(MBlocks.ENERGY_OUTPUT_INTERFACE)
									tag(EnergyInterfaceBlockEntity.INPUT_TAG)
									tag(EnergyInterfaceBlockEntity.OUTPUT_TAG)
								}
							}
						}
					}
				}
			}
		}
	}
}
