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

import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.properties.BlockStateProperties
import net.neoforged.neoforge.capabilities.Capabilities
import ru.dbotthepony.mc.otm.SERVER_IS_LIVE
import ru.dbotthepony.mc.otm.block.CableBlock
import ru.dbotthepony.mc.otm.block.entity.MatteryBlockEntity
import ru.dbotthepony.mc.otm.capability.FlowDirection
import ru.dbotthepony.mc.otm.capability.MatteryCapability
import ru.dbotthepony.mc.otm.capability.energy.IMatteryEnergyStorage
import ru.dbotthepony.mc.otm.config.CablesConfig
import ru.dbotthepony.mc.otm.core.get
import ru.dbotthepony.mc.otm.core.math.BlockRotation
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.math.RelativeSide
import ru.dbotthepony.mc.otm.core.set
import ru.dbotthepony.mc.otm.core.util.TickList
import ru.dbotthepony.mc.otm.core.util.countingLazy
import ru.dbotthepony.mc.otm.graph.GraphNode
import ru.dbotthepony.mc.otm.once
import ru.dbotthepony.mc.otm.onceServer
import java.util.Collections
import java.util.EnumMap
import java.util.function.Consumer

// after some thinking, team decided to settle with IC2's side (techreborn, gregtech, integrated dynamics*, pipez*, p2p tunnels, ...) of implementation,
// where cables have no residue capacitance, and never pull/push energy by themselves

// this allows simpler implementation and faster code, while also reducing possibility of duplication exploits

abstract class EnergyCableBlockEntity(type: BlockEntityType<*>, blockPos: BlockPos, blockState: BlockState) : MatteryBlockEntity(type, blockPos, blockState) {
	inner class CableSide(val side: RelativeSide) : IMatteryEnergyStorage {
		var isEnabled = true
			set(value) {
				field = value

				if (value) {
					node.graph.addLivelyNode(node)
				}
			}

		init {
			check(side !in energySidesInternal)
			energySidesInternal[side] = this
			exposeSided(side, Capabilities.EnergyStorage.BLOCK, this)
		}

		val neighbour = CapabilityCache(side, Capabilities.EnergyStorage.BLOCK)

		init {
			waitForServerLevel {
				neighbour.addListener(Consumer {
					if (isEnabled) {
						if (neighbour.isPresent) {
							if (neighbour.get() !is CableSide) {
								node.graph.addLivelyNode(node)
							}
						}

						onceServer {
							if (!isRemoved)
								updateBlockState(blockRotation.side2Dir(side), neighbour.isPresent || node.neighboursView[GraphNode.link(blockRotation.side2Dir(side))] != null)
						}
					}
				})
			}
		}

		override fun extractEnergy(howMuch: Decimal, simulate: Boolean): Decimal {
			return Decimal.ZERO
		}

		override fun receiveEnergy(howMuch: Decimal, simulate: Boolean): Decimal {
			return node.graph.receiveEnergy(howMuch, simulate, node, side)
		}

		override var batteryLevel: Decimal
			get() = Decimal.ZERO
			set(value) {}

		override val maxBatteryLevel: Decimal get() = Decimal.POSITIVE_INFINITY
		override var energyFlow: FlowDirection = FlowDirection.INPUT
			private set

		override val canSetBatteryLevel: Boolean
			get() = false

		override val missingPower: Decimal
			get() = Decimal.POSITIVE_INFINITY
	}

	private val isPoweredState by countingLazy(blockStateChangesCounter) {
		this.blockState[BlockStateProperties.POWERED]
	}

	inner class Node : GraphNode<Node, EnergyCableGraph>(::EnergyCableGraph) {
		@Deprecated("internal property")
		var _segment = EnergyCableGraph.Segment(this)

		var segment: EnergyCableGraph.Segment
			get() { return _segment }
			set(value) {
				if (_segment === value) return
				_segment.remove(this)
				value.add(this)
				_segment = value
			}

		val sides get() = energySides

		override fun onNeighbour(link: Link) {
			if (link is DirectionLink) {
				updateBlockState(link.direction, true)
			}
		}

		override fun onUnNeighbour(link: Link) {
			if (link is DirectionLink) {
				updateBlockState(link.direction, false)
			}
		}

		private var updateTimer: TickList.Timer? = null
		private var ongoingNewState: Boolean? = null

		fun updatePoweredState(newState: Boolean) {
			if (isRemoved || !SERVER_IS_LIVE) return
			val level = level ?: return

			if (isPoweredState != newState || ongoingNewState != newState) {
				updateTimer?.cancel()

				if (isPoweredState == newState) {
					ongoingNewState = null
					updateTimer?.cancel()
				} else {
					ongoingNewState = newState

					// reduces flickering and block updates pressure
					updateTimer = level.once(4) {
						ongoingNewState = null
						updateTimer = null

						if (!isRemoved) {
							level.setBlock(blockPos, blockState.set(BlockStateProperties.POWERED, newState), Block.UPDATE_CLIENTS)
						}
					}
				}
			}
		}

		val blockEntity get() = this@EnergyCableBlockEntity
		val canTraverse get() = energyThroughput > Decimal.ZERO
		val energyThroughput get() = this@EnergyCableBlockEntity.energyThroughput
		val position: BlockPos get() = this@EnergyCableBlockEntity.blockPos
	}

	private fun updateBlockState(side: Direction, status: Boolean) {
		if (!SERVER_IS_LIVE) return
		val level = level

		level?.once(4) {
			if (!node.isValid) return@once

			val newState = blockState
				.setValue(CableBlock.MAPPING_CONNECTION_PROP[side]!!, status)

			if (newState !== blockState && SERVER_IS_LIVE)
				level.setBlock(blockPos, newState, Block.UPDATE_CLIENTS)
		}
	}

	// whenever this changes, graph#invalidatePathCache() MUST be called
	abstract val energyThroughput: Decimal

	final override val blockRotation: BlockRotation
		get() = BlockRotation.NORTH

	private val energySidesInternal = EnumMap<RelativeSide, CableSide>(RelativeSide::class.java)
	val energySides: Map<RelativeSide, CableSide> = Collections.unmodifiableMap(energySidesInternal)
	val node = Node()

	override fun setLevel(level: Level) {
		super.setLevel(level)

		if (!level.isClientSide)
			node.discover(this, MatteryCapability.ENERGY_CABLE_NODE)
	}

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

	init {
		RelativeSide.entries.forEach { CableSide(it) }
		exposeSideless(MatteryCapability.ENERGY_CABLE_NODE, node)
	}
}

class SimpleEnergyCableBlockEntity(type: BlockEntityType<*>, blockPos: BlockPos, blockState: BlockState, val config: CablesConfig.E) : EnergyCableBlockEntity(type, blockPos, blockState) {
	override val energyThroughput: Decimal
		get() = config.throughput
}
