package ru.dbotthepony.mc.otm.core.chart

import com.mojang.serialization.Codec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.core.HolderLookup
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtOps
import net.minecraft.network.RegistryFriendlyByteBuf
import net.neoforged.neoforge.common.util.INBTSerializable
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.mc.otm.network.MatteryStreamCodec
import ru.dbotthepony.mc.otm.network.syncher.IRemoteState
import ru.dbotthepony.mc.otm.network.syncher.ISynchable
import java.util.concurrent.CopyOnWriteArrayList

abstract class AbstractHistoryChart<V : Any>(
	final override val resolution: Int,
	final override val width: Int,
) : IHistoryChart<V>, ISynchable, INBTSerializable<CompoundTag> {
	constructor(ticks: Int) : this(1, ticks)

	init {
		require(resolution >= 1) { "Invalid resolution: $resolution" }
		require(width >= 1) { "Invalid width: $width" }
	}

	// can't believe i've become lazy enough to use codecs for this
	private data class SData<V : Any>(val accumulated: List<V>, val values: List<V>)

	protected abstract fun calculateAverage(input: List<V>): V
	protected abstract fun sum(a: V, b: V): V
	protected abstract fun sum(input: List<V>): V
	protected abstract fun identity(): V

	fun calculateAverage(width: Int): V {
		require(width >= 0) { "Invalid time frame: $width" }

		if (width == 0 || this.values.isEmpty())
			return identity()

		return calculateAverage(values.subList(0, width.coerceAtMost(values.size)))
	}

	fun calculateSum(width: Int = this.width): V {
		require(width >= 0) { "Invalid time frame: $width" }

		if (width == 0 || this.values.isEmpty())
			return identity()

		return sum(this.values.subList(0, width.coerceAtMost(this.values.size)))
	}

	abstract val codec: Codec<V>
	abstract val streamCodec: MatteryStreamCodec<RegistryFriendlyByteBuf, V>

	private val remotes = CopyOnWriteArrayList<RemoteState>()
	final override val hasRemotes: Boolean
		get() = remotes.isNotEmpty()

	final override var isRemote: Boolean = false
		private set

	private inner class RemoteState(val listener: Runnable) : IRemoteState {
		init {
			remotes.add(this)
		}

		private var fresh = true
		private var currentIndex = values.size

		override fun write(stream: RegistryFriendlyByteBuf) {
			stream.writeBoolean(fresh)
			stream.writeVarInt(currentIndex)

			for (i in currentIndex - 1 downTo 0) {
				streamCodec.encode(stream, values[i])
			}

			currentIndex = 0
			fresh = false
		}

		fun push() {
			if (currentIndex >= width) return
			currentIndex++
			listener.run()
		}

		override fun invalidate() {
			currentIndex = values.size
			fresh = true
		}

		override fun close() {
			remotes.remove(this)
		}
	}

	final override fun read(stream: RegistryFriendlyByteBuf) {
		isRemote = true

		if (stream.readBoolean()) {
			values.clear()
		}

		for (i in 0 until stream.readVarInt()) {
			values.addFirst(streamCodec.decode(stream))
		}

		while (values.size > width) values.removeLast()
	}

	final override fun createRemoteState(listener: Runnable): IRemoteState {
		return RemoteState(listener)
	}

	final override fun calculateAverage(): V {
		if (values.isEmpty()) {
			return identity()
		}

		return calculateAverage(values)
	}

	private val accumulator = ArrayList<V>(this.resolution)
	private val values = ArrayDeque<V>()

	/**
	 * Calculates sum of values provided by calls to [add] and writes it to graph history or graph value accumulator
	 */
	fun add(value: V) {
		accumulator.add(value)

		if (accumulator.size >= resolution) {
			val calc = calculateAverage(accumulator)
			values.addFirst(calc)
			remotes.forEach { it.push() }
			accumulator.clear()
			while (values.size > width) values.removeLast()
		}
	}

	val size: Int
		get() = values.size

	final override operator fun get(index: Int): V {
		return values.getOrNull(index) ?: identity()
	}

	fun getRaw(index: Int): V {
		return values[index]
	}

	private fun codec(): Codec<SData<V>> {
		val list = Codec.list(codec)

		return RecordCodecBuilder.create {
			it.group(
				list.fieldOf("accumulator").forGetter { it.accumulated },
				list.fieldOf("values").forGetter { it.values },
			).apply(it) { a, b -> SData(a, b) }
		}
	}

	override fun serializeNBT(registry: HolderLookup.Provider): CompoundTag {
		return codec().encodeStart(registry.createSerializationContext(NbtOps.INSTANCE), SData(accumulator, values)).orThrow as CompoundTag
	}

	override fun deserializeNBT(registry: HolderLookup.Provider, nbt: CompoundTag) {
		this.accumulator.clear()
		this.values.clear()

		val (accumulator, values) = codec().decode(registry.createSerializationContext(NbtOps.INSTANCE), nbt)
			.map { it.first }
			.resultOrPartial { LOGGER.error("Errors during deserializing history graph: $it") }
			.orElseGet { SData(listOf(), listOf()) }

		this.accumulator.addAll(accumulator)
		this.values.addAll(values)
		this.remotes.forEach { it.invalidate() }
	}

	fun clear() {
		accumulator.clear()
		values.fill(identity())
	}

	final override fun iterator(): Iterator<V> {
		return object : Iterator<V> {
			private var index = 0

			override fun hasNext(): Boolean {
				return index < width
			}

			override fun next(): V {
				return this@AbstractHistoryChart[index++]
			}
		}
	}

	companion object {
		private val LOGGER = LogManager.getLogger()
	}
}
