package ru.dbotthepony.mc.otm.capability

import com.google.common.collect.ImmutableList
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.ListTag
import net.minecraft.nbt.NumericTag
import net.minecraftforge.common.util.INBTSerializable
import ru.dbotthepony.mc.otm.core.forValidRefs
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.nbt.map
import ru.dbotthepony.mc.otm.core.nbt.set
import java.lang.ref.WeakReference
import java.util.*
import kotlin.collections.ArrayList

abstract class AbstractProfiledStorage<out P>(val parent: P) : INBTSerializable<CompoundTag?> {
	init {
		storages.add(WeakReference(this))
	}

	private val historyReceiveInternal = ArrayList<Decimal>(HISTORY_SIZE)
	private val historyTransferInternal = ArrayList<Decimal>(HISTORY_SIZE)

	val historyReceive: List<Decimal> = Collections.unmodifiableList(historyReceiveInternal)
	val historyTransfer: List<Decimal> = Collections.unmodifiableList(historyTransferInternal)

	val lastTickReceive
		get() = historyReceiveInternal[tick]

	val lastTickTransfer
		get() = historyTransferInternal[tick]

	private var thisTickReceive = Decimal.ZERO
	private var thisTickTransfer = Decimal.ZERO

	var tick = 0
		private set

	init {
		for (i in 0 until HISTORY_SIZE) {
			historyReceiveInternal.add(Decimal.ZERO)
			historyTransferInternal.add(Decimal.ZERO)
		}
	}

	protected fun recordTransfer(value: Decimal, simulate: Boolean): Decimal {
		if (!simulate) {
			thisTickTransfer += value
		}

		return value
	}

	protected fun recordReceive(value: Decimal, simulate: Boolean): Decimal {
		if (!simulate) {
			thisTickReceive += value
		}

		return value
	}

	private fun tick() {
		tick = (tick + 1) % HISTORY_SIZE

		historyReceiveInternal[tick] = thisTickReceive
		historyTransferInternal[tick] = thisTickTransfer

		thisTickReceive = Decimal.ZERO
		thisTickTransfer = Decimal.ZERO
	}

	val savedata: INBTSerializable<CompoundTag?> = object : INBTSerializable<CompoundTag?> {
		override fun serializeNBT(): CompoundTag {
			return CompoundTag().also { tag ->
				tag["historyReceive"] = ListTag().also {
					for (value in historyReceiveInternal) {
						it.add(value.serializeNBT())
					}
				}

				tag["historyTransfer"] = ListTag().also {
					for (value in historyTransferInternal) {
						it.add(value.serializeNBT())
					}
				}

				tag["historyTick"] = tick
			}
		}

		override fun deserializeNBT(nbt: CompoundTag?) {
			tick = 0
			thisTickReceive = Decimal.ZERO
			thisTickTransfer = Decimal.ZERO

			for (i in 0 until HISTORY_SIZE) {
				historyReceiveInternal[i] = Decimal.ZERO
				historyTransferInternal[i] = Decimal.ZERO
			}

			nbt ?: return

			nbt.map("historyTick") { it: NumericTag ->
				tick = it.asInt
			}

			nbt.map("historyReceive") { it: ListTag ->
				for (i in 0 until HISTORY_SIZE.coerceAtMost(it.size)) {
					historyReceiveInternal[i] = Decimal.deserializeNBT(it[i])
				}
			}

			nbt.map("historyTransfer") { it: ListTag ->
				for (i in 0 until HISTORY_SIZE.coerceAtMost(it.size)) {
					historyTransferInternal[i] = Decimal.deserializeNBT(it[i])
				}
			}
		}
	}

	override fun serializeNBT(): CompoundTag {
		val tag: CompoundTag

		if (parent is INBTSerializable<*>) {
			tag = (parent as INBTSerializable<CompoundTag?>).serializeNBT() ?: CompoundTag()
		} else {
			tag = CompoundTag()
		}

		val tag2 = savedata.serializeNBT()!!

		for (k in tag2.allKeys) {
			tag[k] = tag2[k]!!
		}

		return tag
	}

	val weightedTransfer: Decimal get() {
		return calcWeightedAverage(historyTransfer, tick)
	}

	val weightedReceive: Decimal get() {
		return calcWeightedAverage(historyReceive, tick)
	}

	override fun deserializeNBT(nbt: CompoundTag?) {
		if (parent is INBTSerializable<*>) {
			(parent as INBTSerializable<CompoundTag?>).deserializeNBT(nbt)
		}

		savedata.deserializeNBT(nbt)
	}

	companion object {
		const val HISTORY_SIZE = 20
		private val storages = ArrayList<WeakReference<AbstractProfiledStorage<*>>>()

		val HISTORY_WEIGHTERS: ImmutableList<Decimal> = ImmutableList.of(
			Decimal("0.313335967"),
			Decimal("0.146176397"),
			Decimal("0.09357867"),
			Decimal("0.068193701"),
			Decimal("0.053351084"),
			Decimal("0.043655993"),
			Decimal("0.036847023"),
			Decimal("0.031813486"),
			Decimal("0.027947534"),
			Decimal("0.024889161"),
			Decimal("0.02241188"),
			Decimal("0.020366241"),
			Decimal("0.018649731"),
			Decimal("0.017189744"),
			Decimal("0.015933452"),
			Decimal("0.014841516"),
			Decimal("0.013884059"),
			Decimal("0.013037985"),
			Decimal("0.012285173"),
			Decimal("0.011611204"),
		)

		fun calcWeightedAverage(inputs: List<Decimal>, pointer: Int): Decimal {
			require(inputs.size == HISTORY_WEIGHTERS.size) { "Expected list of size ${HISTORY_WEIGHTERS.size}, got ${inputs.size}" }
			require(pointer in 0 until HISTORY_WEIGHTERS.size) { "Invalid pointer position: $pointer" }

			var result = Decimal.ZERO
			var i = 0

			for (i2 in pointer downTo 0)
				result += inputs[i2] * HISTORY_WEIGHTERS[i++]

			for (i2 in HISTORY_WEIGHTERS.size - 1 downTo pointer + 1)
				result += inputs[i2] * HISTORY_WEIGHTERS[i++]

			return result
		}

		// после раздумий, наиболее корректное место для обновления состояния профилированных
		// хранилищ энергии и материи будет после всех остальных хуков на тики после тика сервера
		// разумеется, такое решение может несколько снизить производительность сервера,
		// но ничего страшного произойти не должно
		internal fun onServerPostTick() {
			storages.forValidRefs { it.tick() }
		}
	}
}
