package ru.dbotthepony.mc.otm.compat.mekanism

import mekanism.api.Action
import mekanism.api.energy.IStrictEnergyHandler
import mekanism.api.math.FloatingLong
import mekanism.common.config.MekanismConfig
import net.minecraft.core.Direction
import net.minecraftforge.common.capabilities.ForgeCapabilities
import net.minecraftforge.common.capabilities.ICapabilityProvider
import net.minecraftforge.common.util.LazyOptional
import net.minecraftforge.energy.IEnergyStorage
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.mc.otm.capability.FlowDirection
import ru.dbotthepony.mc.otm.capability.energy.IMatteryEnergyStorage
import ru.dbotthepony.mc.otm.capability.MatteryCapability
import ru.dbotthepony.mc.otm.capability.isMekanismLoaded
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.orNull
import java.lang.ref.WeakReference
import java.util.WeakHashMap

private val LOGGER = LogManager.getLogger()

private class LazyCache<O, T>(private val provider: () -> O, private val computer: (O) -> T) : Lazy<T> {
	private var cache: T? = null
	private var observed: O? = null

	override fun isInitialized(): Boolean {
		return cache != null
	}

	override val value: T get() {
		if (cache == null || observed == null || observed != provider.invoke()) {
			observed = provider.invoke()
			cache = computer.invoke(observed ?: throw ConcurrentModificationException())
		}

		return cache ?: throw ConcurrentModificationException()
	}
}

private class DoubleLazy<T>(onion: () -> Lazy<T>) : Lazy<T> {
	private val onion = lazy(onion)
	override val value: T
		get() = onion.value.value

	override fun isInitialized(): Boolean {
		return onion.isInitialized() && onion.value.isInitialized()
	}
}

class Mekanism2MatteryEnergyWrapper(private val power: IStrictEnergyHandler, private val forgePower: IEnergyStorage? = null) : IMatteryEnergyStorage {
	companion object {
		val mekanism2MtJ by DoubleLazy lazy@{
			try {
				val conf = MekanismConfig.general
				return@lazy LazyCache(conf.forgeConversionRate::get) { Decimal.ONE / it.toDecimal() }
			} catch(err: Throwable) {
				LOGGER.error("Unable to get Forge Energy to Mekanism Joules's conversion rate! Expect issues", err)
			}

			return@lazy lazyOf(Decimal.ONE)
		}
	}

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

	override fun receiveEnergyChecked(howMuch: Decimal, simulate: Boolean): Decimal {
		return receiveEnergy(howMuch, simulate)
	}

	override fun extractEnergy(howMuch: Decimal, simulate: Boolean): Decimal {
		val action = when (simulate) {
			true -> Action.SIMULATE
			false -> Action.EXECUTE
		}

		return power.extractEnergy((howMuch * Mattery2MekanismEnergyWrapper.mtj2Mekanism).toFloatingLong(), action).toDecimal() * mekanism2MtJ
	}

	override fun receiveEnergy(howMuch: Decimal, simulate: Boolean): Decimal {
		val action = when (simulate) {
			true -> Action.SIMULATE
			false -> Action.EXECUTE
		}

		return howMuch - power.insertEnergy((howMuch * Mattery2MekanismEnergyWrapper.mtj2Mekanism).toFloatingLong(), action).toDecimal() * mekanism2MtJ
	}

	override val canSetBatteryLevel: Boolean
		get() = power.energyContainerCount == 1

	override var batteryLevel: Decimal
		get() {
			var sum = Decimal.ZERO

			for (i in 0 until power.energyContainerCount) {
				sum += power.getEnergy(i).toDecimal()
			}

			return sum * mekanism2MtJ
		}
		set(value) {
			if (power.energyContainerCount != 1)
				throw UnsupportedOperationException("Can set power only when we have 1 energy container, ${power.energyContainerCount} present")

			power.setEnergy(0, (value * Mattery2MekanismEnergyWrapper.mtj2Mekanism).toFloatingLong())
		}

	override val maxBatteryLevel: Decimal
		get() {
			var sum = Decimal.ZERO

			for (i in 0 until power.energyContainerCount) {
				sum += power.getMaxEnergy(i).toDecimal()
			}

			return sum * mekanism2MtJ
		}

	override val energyFlow: FlowDirection
		get() = FlowDirection.of(output = forgePower?.canExtract() ?: power.extractEnergy(FloatingLong.MAX_VALUE, Action.SIMULATE).greaterThan(FloatingLong.ZERO), input = forgePower?.canReceive() ?: power.insertEnergy(FloatingLong.MAX_VALUE, Action.SIMULATE).greaterThan(FloatingLong.ZERO))
}

class Mattery2MekanismEnergyWrapper(private val power: IMatteryEnergyStorage) : IStrictEnergyHandler {
	companion object {
		val mtj2Mekanism by DoubleLazy lazy@{
			try {
				val conf = MekanismConfig.general
				return@lazy LazyCache(conf.forgeConversionRate::get) { it.toDecimal() }
			} catch(err: Throwable) {
				LOGGER.error("Unable to get Mekanism Joules's to Forge Energy conversion rate! Expect issues", err)
			}

			return@lazy lazyOf(Decimal.ONE)
		}
	}

	override fun getEnergyContainerCount(): Int = 1

	override fun getEnergy(container: Int): FloatingLong {
		if (container != 0) {
			return FloatingLong.ZERO
		}

		return (power.batteryLevel * mtj2Mekanism).toFloatingLong()
	}

	override fun setEnergy(container: Int, value: FloatingLong) {
		// no op
	}

	override fun getMaxEnergy(container: Int): FloatingLong {
		if (container != 0) {
			return FloatingLong.ZERO
		}

		return (power.maxBatteryLevel * mtj2Mekanism).toFloatingLong()
	}

	override fun getNeededEnergy(container: Int): FloatingLong {
		if (container != 0) {
			return FloatingLong.ZERO
		}

		return (power.missingPower * mtj2Mekanism).toFloatingLong()
	}

	override fun insertEnergy(container: Int, howMuch: FloatingLong, action: Action): FloatingLong {
		val copy = howMuch.copy()
		return copy.minusEqual((power.receiveEnergyChecked(howMuch.toDecimal() * Mekanism2MatteryEnergyWrapper.mekanism2MtJ, action.simulate()) * mtj2Mekanism).toFloatingLong())
	}

	override fun extractEnergy(container: Int, howMuch: FloatingLong, action: Action): FloatingLong {
		return (power.extractEnergyChecked(howMuch.toDecimal() * Mekanism2MatteryEnergyWrapper.mekanism2MtJ, action.simulate()) * mtj2Mekanism).toFloatingLong()
	}
}

val ICapabilityProvider.mekanismEnergy: IMatteryEnergyStorage? get() {
	if (!isMekanismLoaded) {
		return null
	}

	val capability = getCapability(MatteryCapability.MEKANISM_ENERGY)

	if (!capability.isPresent) {
		return null
	}

	return Mekanism2MatteryEnergyWrapper(capability.orElseThrow(::IllegalStateException), getCapability(ForgeCapabilities.ENERGY).orNull())
}

private val lazyCache by lazy { WeakHashMap<LazyOptional<IStrictEnergyHandler>, WeakReference<LazyOptional<Mekanism2MatteryEnergyWrapper>>>() }
private val lazyCacheDown by lazy { WeakHashMap<LazyOptional<IStrictEnergyHandler>, WeakReference<LazyOptional<Mekanism2MatteryEnergyWrapper>>>() }
private val lazyCacheUp by lazy { WeakHashMap<LazyOptional<IStrictEnergyHandler>, WeakReference<LazyOptional<Mekanism2MatteryEnergyWrapper>>>() }
private val lazyCacheSouth by lazy { WeakHashMap<LazyOptional<IStrictEnergyHandler>, WeakReference<LazyOptional<Mekanism2MatteryEnergyWrapper>>>() }
private val lazyCacheNorth by lazy { WeakHashMap<LazyOptional<IStrictEnergyHandler>, WeakReference<LazyOptional<Mekanism2MatteryEnergyWrapper>>>() }
private val lazyCacheEast by lazy { WeakHashMap<LazyOptional<IStrictEnergyHandler>, WeakReference<LazyOptional<Mekanism2MatteryEnergyWrapper>>>() }
private val lazyCacheWest by lazy { WeakHashMap<LazyOptional<IStrictEnergyHandler>, WeakReference<LazyOptional<Mekanism2MatteryEnergyWrapper>>>() }

fun ICapabilityProvider.getMekanismEnergySided(side: Direction? = null): LazyOptional<IMatteryEnergyStorage> {
	if (!isMekanismLoaded) return LazyOptional.empty()
	val lazyOptional = getCapability(MatteryCapability.MEKANISM_ENERGY, side)
	if (!lazyOptional.isPresent) return LazyOptional.empty()

	val cache = when (side) {
		Direction.DOWN -> lazyCacheDown
		Direction.UP -> lazyCacheUp
		Direction.NORTH -> lazyCacheNorth
		Direction.SOUTH -> lazyCacheSouth
		Direction.WEST -> lazyCacheWest
		Direction.EAST -> lazyCacheEast
		null -> lazyCache
	}

	val cached = cache[lazyOptional]?.get()
	if (cached != null) return cached.cast()

	val forgeEnergy = getCapability(ForgeCapabilities.ENERGY, side)

	val resolver = LazyOptional.of {
		Mekanism2MatteryEnergyWrapper(lazyOptional.orElseThrow(::IllegalStateException), forgeEnergy.orNull())
	}

	val ref = WeakReference(resolver)
	cache[lazyOptional] = ref

	lazyOptional.addListener {
		ref.get()?.invalidate()
	}

	return resolver.cast()
}

