package ru.dbotthepony.mc.otm.capability

import com.google.common.collect.Streams
import earth.terrarium.botarium.common.registry.fluid.FluidSounds
import net.minecraft.ChatFormatting
import net.minecraft.core.Direction
import net.minecraft.network.chat.Component
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraftforge.common.capabilities.ForgeCapabilities
import net.minecraftforge.common.capabilities.ICapabilityProvider
import net.minecraftforge.common.util.LazyOptional
import net.minecraftforge.energy.IEnergyStorage
import net.minecraftforge.fluids.FluidStack
import net.minecraftforge.fluids.capability.IFluidHandler
import net.minecraftforge.fml.ModList
import net.minecraftforge.items.IItemHandler
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.mc.otm.capability.energy.IMatteryEnergyStorage
import ru.dbotthepony.mc.otm.client.ShiftPressedCond
import ru.dbotthepony.mc.otm.compat.cos.cosmeticArmorAwareStream
import ru.dbotthepony.mc.otm.compat.cos.cosmeticArmorStream
import ru.dbotthepony.mc.otm.compat.cos.isCosmeticArmorLoaded
import ru.dbotthepony.mc.otm.compat.curios.curiosAwareStream
import ru.dbotthepony.mc.otm.compat.curios.curiosStream
import ru.dbotthepony.mc.otm.compat.curios.isCuriosLoaded
import ru.dbotthepony.mc.otm.compat.mekanism.getMekanismEnergySided
import ru.dbotthepony.mc.otm.compat.mekanism.mekanismEnergy
import ru.dbotthepony.mc.otm.container.awareStream
import ru.dbotthepony.mc.otm.container.iterator
import ru.dbotthepony.mc.otm.container.stream
import ru.dbotthepony.mc.otm.core.collect.AwareItemStack
import ru.dbotthepony.mc.otm.core.collect.ContainerItemStackEntry
import ru.dbotthepony.mc.otm.core.isNotEmpty
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.orNull
import ru.dbotthepony.mc.otm.core.util.formatFluidLevel
import java.util.IdentityHashMap
import java.util.LinkedList
import java.util.stream.Stream

private val LOGGER = LogManager.getLogger()

val ICapabilityProvider.matteryPlayer: MatteryPlayerCapability? get() = getCapability(MatteryCapability.MATTERY_PLAYER).orNull()

/**
 * Does a checked energy receive, calls [IMatteryEnergyStorage.receiveEnergyChecked] if possible
 */
fun IEnergyStorage.receiveEnergy(amount: Decimal, simulate: Boolean): Decimal {
	if (this is IMatteryEnergyStorage)
		return receiveEnergyChecked(amount, simulate)

	if (amount.isNegative)
		throw IllegalArgumentException("Negative energy: $amount")

	if (!canReceive())
		return Decimal.ZERO

	if (amount >= Decimal.INT_MAX_VALUE)
		return Decimal.valueOf(receiveEnergy(Int.MAX_VALUE, simulate))

	return Decimal.valueOf(receiveEnergy(amount.toInt(), simulate))
}

fun IEnergyStorage.transcieveEnergy(amount: Decimal, isReceiving: Boolean, simulate: Boolean): Decimal {
	if (isReceiving)
		return receiveEnergy(amount, simulate)
	else
		return extractEnergy(amount, simulate)
}

/**
 * Does a checked energy extraction, calls [IMatteryEnergyStorage.extractEnergyChecked] if possible
 */
fun IEnergyStorage.extractEnergy(amount: Decimal, simulate: Boolean): Decimal {
	if (this is IMatteryEnergyStorage)
		return extractEnergyChecked(amount, simulate)

	if (amount.isNegative)
		throw IllegalArgumentException("Negative energy: $amount")

	if (!canExtract())
		return Decimal.ZERO

	if (amount >= Decimal.INT_MAX_VALUE)
		return Decimal.valueOf(extractEnergy(Int.MAX_VALUE, simulate))

	return Decimal.valueOf(extractEnergy(amount.toInt(), simulate))
}

val IEnergyStorage.maxEnergyStoredMattery: Decimal
	get() {
	if (this is IMatteryEnergyStorage) {
		return maxBatteryLevel
	}

	return Decimal.valueOf(maxEnergyStored)
}

var IEnergyStorage.energyStoredMattery: Decimal
	get() {
		if (this is IMatteryEnergyStorage) {
			return batteryLevel
		}

		return Decimal.valueOf(energyStored)
	}
	set(value) {
		if (this is IMatteryEnergyStorage) {
			batteryLevel = value
			return
		}

		throw UnsupportedOperationException("Can't set stored energy on ${this::class.qualifiedName}")
	}

val IEnergyStorage.canSetBatteryMattery: Boolean get() {
	if (this is IMatteryEnergyStorage) {
		return canSetBatteryLevel
	}

	return false
}

val IEnergyStorage.chargeRatio: Float get() {
	if (this is IMatteryEnergyStorage) {
		return (batteryLevel / maxBatteryLevel).toFloat()
	}

	return energyStored.toFloat() / maxEnergyStored.toFloat()
}

val isMekanismLoaded by lazy { ModList.get().isLoaded("mekanism") }

/**
 * Shortcut for getting [IEnergyStorage], including wrappers for it
 */
val ICapabilityProvider.energy: IEnergyStorage? get() {
	val mattery = getCapability(MatteryCapability.ENERGY)

	if (mattery.isPresent) {
		return mattery.orNull()
	}

	if (isMekanismLoaded) {
		val mekanismEnergy = mekanismEnergy

		if (mekanismEnergy != null) {
			return mekanismEnergy
		}
	}

	return getCapability(ForgeCapabilities.ENERGY).orNull()
}

/**
 * Shortcut for getting sideless [IMatteryEnergyStorage], including wrappers for it
 */
val ICapabilityProvider.matteryEnergy: IMatteryEnergyStorage? get() {
	val mattery = getCapability(MatteryCapability.ENERGY)

	if (mattery.isPresent) {
		return mattery.orNull()
	}

	if (isMekanismLoaded) {
		val mekanismEnergy = mekanismEnergy

		if (mekanismEnergy != null) {
			return mekanismEnergy
		}
	}

	return null
}

/**
 * Shortcut for getting [LazyOptional] with [IEnergyStorage], including wrappers for it
 */
fun ICapabilityProvider.getEnergySided(side: Direction? = null): LazyOptional<out IEnergyStorage> {
	val mattery = getCapability(MatteryCapability.ENERGY, side)

	if (mattery.isPresent) {
		return mattery
	}

	if (isMekanismLoaded) {
		val mekanismEnergy = getMekanismEnergySided(side)

		if (mekanismEnergy.isPresent) {
			return mekanismEnergy
		}
	}

	return getCapability(ForgeCapabilities.ENERGY, side)
}

/**
 * Shortcut for getting [LazyOptional] with [IMatteryEnergyStorage], including wrappers for it
 */
fun ICapabilityProvider.getMatteryEnergySided(side: Direction? = null): LazyOptional<out IMatteryEnergyStorage> {
	val mattery = getCapability(MatteryCapability.ENERGY, side)

	if (mattery.isPresent) {
		return mattery
	}

	if (isMekanismLoaded) {
		val mekanismEnergy = getMekanismEnergySided(side)

		if (mekanismEnergy.isPresent) {
			return mekanismEnergy
		}
	}

	return LazyOptional.empty()
}

/**
 * DO NOT modify returned ItemStacks!
 *
 * Contains all items that player might carry
 */
fun Player.itemsStream(includeCosmetics: Boolean = true): Stream<out ItemStack> {
	val streams = ArrayList<Stream<out ItemStack>>()
	streams.add(inventory.stream())

	matteryPlayer?.let {
		if (it.hasExoPack) {
			streams.add(it.exoPackContainer.stream())
		}
	}

	if (isCuriosLoaded) {
		streams.add(curiosStream(includeCosmetics))
	}

	if (includeCosmetics && isCosmeticArmorLoaded) {
		streams.add(cosmeticArmorStream())
	}

	return streams.stream().flatMap { it }
}

/**
 * DO NOT modify returned ItemStacks!
 *
 * Contains all items that player might see/access ([itemsStream] + open container's items)
 */
fun Player.allItemsStream(includeCosmetics: Boolean = true): Stream<out ItemStack> {
	if (containerMenu == inventoryMenu || containerMenu == matteryPlayer?.exoPackMenu) {
		return itemsStream(includeCosmetics)
	}

	val seen = IdentityHashMap<ItemStack, Unit>(containerMenu.slots.size)

	for (slot in containerMenu.slots) {
		seen[slot.item] = Unit
	}

	return Streams.concat(
		itemsStream(includeCosmetics).filter { it.isEmpty || it !in seen },
		containerMenu.slots.stream().map { it.item })
}

/**
 * Modify returned [ItemStack]s through [AwareItemStack.extract]
 *
 * Contains all items that player might carry
 */
fun Player.awareItemsStream(includeCosmetics: Boolean = false): Stream<out AwareItemStack> {
	val streams = ArrayList<Stream<out AwareItemStack>>()
	streams.add(inventory.awareStream())

	matteryPlayer?.let {
		if (it.hasExoPack) {
			streams.add(it.exoPackContainer.awareStream())
		}
	}

	if (isCuriosLoaded) {
		streams.add(curiosAwareStream(includeCosmetics))
	}

	if (includeCosmetics && isCosmeticArmorLoaded) {
		streams.add(cosmeticArmorAwareStream())
	}

	return streams.stream().flatMap { it }
}

/**
 * Modify returned [ItemStack]s through [AwareItemStack.extract]
 *
 * Contains all items that player might see/access ([itemsStream] + open container's items)
 */
fun Player.awareAllItemsStream(includeCosmetics: Boolean = false): Stream<out AwareItemStack> {
	if (containerMenu == inventoryMenu || containerMenu == matteryPlayer?.exoPackMenu) {
		return awareItemsStream(includeCosmetics)
	}

	val seen = HashSet<AwareItemStack>(containerMenu.slots.size)

	for (slot in containerMenu.slots) {
		seen.add(ContainerItemStackEntry(slot.slotIndex, slot.container))
	}

	return Streams.concat(
		awareItemsStream(includeCosmetics).filter { it !in seen },
		seen.stream())
}

/**
 * Attempts to safely exchange/move item between slots of two handlers
 *
 * @return pair of new (advanced) [sourceSlot] and [destinationSlot]
 */
fun moveBetweenSlots(source: IItemHandler, sourceSlot: Int, destination: IItemHandler, destinationSlot: Int): Pair<Int, Int> {
	val getItem = source.extractItem(sourceSlot, Int.MAX_VALUE, true)

	if (getItem.isEmpty) {
		return sourceSlot + 1 to destinationSlot
	} else {
		val leftover = destination.insertItem(destinationSlot, getItem, true)

		if (leftover.count == getItem.count) {
			return sourceSlot to destinationSlot + 1
		} else {
			val getItem2 = source.extractItem(sourceSlot, getItem.count - leftover.count, true)

			if (getItem2.isEmpty) {
				return sourceSlot + 1 to destinationSlot
			} else {
				val leftover2 = destination.insertItem(destinationSlot, getItem2, true)

				if (leftover2.isEmpty) {
					source.extractItem(sourceSlot, getItem2.count, false)
					destination.insertItem(destinationSlot, getItem2, false)

					if (getItem2.count == getItem.count) {
						return sourceSlot + 1 to destinationSlot
					} else {
						return sourceSlot to destinationSlot
					}
				}
			}
		}
	}

	return sourceSlot to destinationSlot
}

@Suppress("name_shadowing")
fun moveEnergy(source: IEnergyStorage, destination: IEnergyStorage, amount: Decimal = Decimal.LONG_MAX_VALUE.coerceAtLeast(source.energyStoredMattery), simulate: Boolean, ignoreFlowRestrictions: Boolean = false): Decimal {
	val extracted = if (ignoreFlowRestrictions && source is IMatteryEnergyStorage) source.extractEnergy(amount, true) else source.extractEnergy(amount, true)

	if (extracted.isPositive) {
		val received = destination.receiveEnergy(extracted, true)

		if (received.isPositive) {
			val extracted = if (ignoreFlowRestrictions && source is IMatteryEnergyStorage) source.extractEnergy(received, true) else source.extractEnergy(received, true)

			if (extracted.isPositive) {
				if (simulate) {
					return extracted
				}

				val extracted = if (ignoreFlowRestrictions && source is IMatteryEnergyStorage) source.extractEnergy(received, false) else source.extractEnergy(received, false)
				return destination.receiveEnergy(extracted, false)
			}
		}
	}

	return Decimal.ZERO
}

internal fun IFluidHandler.fluidLevel(tooltips: MutableList<Component>) {
	val fluid = getFluidInTank(0)

	if (fluid.isEmpty) {
		tooltips.add(formatFluidLevel(0, getTankCapacity(0), formatAsReadable = ShiftPressedCond).withStyle(ChatFormatting.GRAY))
	} else {
		tooltips.add(formatFluidLevel(fluid.amount, getTankCapacity(0), fluid.displayName, formatAsReadable = ShiftPressedCond).withStyle(ChatFormatting.GRAY))
	}
}

private fun actuallyMoveFluid(drained: FluidStack, source: IFluidHandler, destination: IFluidHandler, limit: Int, actuallyDrain: Boolean, actuallyFill: Boolean): FluidStack {
	if (drained.isEmpty) return FluidStack.EMPTY

	val filled = destination.fill(drained, IFluidHandler.FluidAction.SIMULATE)
	if (filled == 0) return FluidStack.EMPTY

	val drained2 = source.drain(FluidStack(drained, filled), IFluidHandler.FluidAction.SIMULATE)
	if (drained2.amount != filled) return FluidStack.EMPTY

	val filled2 = destination.fill(drained2, IFluidHandler.FluidAction.SIMULATE)
	if (filled2 != drained2.amount) return FluidStack.EMPTY

	if (!actuallyDrain && !actuallyFill) return FluidStack(drained2, filled2)

	val drained3: FluidStack

	if (actuallyDrain) {
		drained3 = source.drain(FluidStack(drained2, filled2), IFluidHandler.FluidAction.EXECUTE)

		if (drained3.amount != filled2) {
			LOGGER.warn("Inconsistency of fluid extraction from $source between simulate and execute modes (simulated $drained2; extracted $drained3); This can lead to duping!!!")
		}
	} else {
		drained3 = drained2
	}

	val filled3: Int

	if (actuallyFill) {
		filled3 = destination.fill(drained3, IFluidHandler.FluidAction.EXECUTE)

		if (filled3 != drained3.amount) {
			LOGGER.warn("Inconsistency of fluid insertion to $destination between simulate and execute modes (simulated $filled2; inserted $filled3); This can lead to duping!!!")
		}
	} else {
		filled3 = filled2
	}

	return FluidStack(drained3, filled3)
}

fun moveFluid(source: IFluidHandler, sourceTank: Int? = null, destination: IFluidHandler, limit: Int = Int.MAX_VALUE, actuallyDrain: Boolean = true, actuallyFill: Boolean = true): FluidStack {
	if (sourceTank == null) {
		for (drained in destination.iterator()) {
			if (drained.isNotEmpty) {
				val moved = actuallyMoveFluid(drained, source, destination, limit, actuallyDrain, actuallyFill)
				if (moved.isNotEmpty) return moved
			}
		}

		for (drained in source.iterator()) {
			if (drained.isNotEmpty) {
				val moved = actuallyMoveFluid(drained, source, destination, limit, actuallyDrain, actuallyFill)
				if (moved.isNotEmpty) return moved
			}
		}

		return FluidStack.EMPTY
	} else {
		return actuallyMoveFluid(source.drain(source.getFluidInTank(sourceTank), IFluidHandler.FluidAction.SIMULATE), source, destination, limit, actuallyDrain, actuallyFill)
	}
}

val IFluidHandler.isEmpty: Boolean get() = stream().allMatch { it.isEmpty }
val IFluidHandler.isNotEmpty: Boolean get() = stream().anyMatch { it.isNotEmpty }
val IFluidHandler.isNotFull: Boolean get() {
	for ((i, fluid) in iterator().withIndex())
		if (fluid.isEmpty || fluid.amount < getTankCapacity(i))
			return true

	return false
}
