package ru.dbotthepony.mc.otm

import it.unimi.dsi.fastutil.ints.IntArrayList
import net.minecraft.resources.ResourceLocation
import net.minecraftforge.common.ForgeConfigSpec
import net.minecraftforge.registries.IForgeRegistry
import ru.dbotthepony.mc.otm.config.getValue
import java.util.LinkedList

abstract class ObservedConfigList<V : Any>(val parent: ForgeConfigSpec.ConfigValue<MutableList<String>>, private val allowNulls: Boolean = false) : AbstractMutableList<V>(), RandomAccess {
	private val rawValue: MutableList<String> by parent
	private val observedValue = LinkedList<String>()
	private val parsedView: ArrayList<V> = ArrayList()

	protected abstract fun toString(value: V): Pair<String, V>?
	protected abstract fun fromString(value: String): V?

	private fun load() {
		parsedView.clear()
		observedValue.clear()
		val removeIndexes = IntArrayList()

		for ((i, value) in rawValue.withIndex()) {
			val parsedValue = fromString(value)

			if (parsedValue != null || allowNulls) {
				observedValue.add(value)
			}

			if (parsedValue != null) {
				parsedView.add(parsedValue)
			} else if (!allowNulls) {
				removeIndexes.add(i)
			}
		}

		for (i in removeIndexes.size - 1 downTo 0) {
			rawValue.removeAt(removeIndexes.getInt(i))
		}

		if (removeIndexes.isNotEmpty()) {
			parent.save()
		}
	}

	private fun observe() {
		val observedValue = observedValue
		val rawValue = rawValue

		if (observedValue.size != rawValue.size) {
			load()
		} else {
			val iter1 = rawValue.iterator()
			val iter2 = observedValue.iterator()

			while (iter1.hasNext() && iter2.hasNext()) {
				if (iter1.next() != iter2.next()) {
					load()
					return
				}
			}

			if (iter1.hasNext() || iter2.hasNext()) {
				load()
			}
		}
	}

	final override fun add(index: Int, element: V) {
		observe()

		val (serialized, newObject) = toString(element) ?: return

		rawValue.add(index, serialized)
		parsedView.add(index, newObject)
		observedValue.add(index, serialized)

		parent.save()
	}

	final override fun removeAt(index: Int): V {
		observe()

		val old = parsedView[index]
		val (serialized, _) = toString(old) ?: throw RuntimeException("Serializer did not accept element already present in list")
		val oldStr = rawValue.getOrNull(index)

		if (oldStr == serialized) {
			rawValue.removeAt(index)
			observedValue.removeAt(index)
		} else {
			val serializedIndex = rawValue.indexOf(serialized)

			if (serializedIndex == -1) {
				throw NoSuchElementException("Element $old is present in parsed list at index $index with serialized form of '$serialized', but it is not found in Forge's config list")
			}

			rawValue.removeAt(serializedIndex)
			observedValue.removeAt(serializedIndex)
		}

		parsedView.removeAt(index)

		parent.save()

		return old
	}

	final override fun set(index: Int, element: V): V {
		observe()

		val old = parsedView[index]

		val (serializedNew, newElement) = toString(element) ?: return old

		if (old === newElement) {
			return old
		}

		val (serialized, _) = toString(old) ?: throw RuntimeException("Serializer did not accept element already present in list")

		check(rawValue[index] == serialized) { "Old value serialized representation $serialized does not match already stored representation ${rawValue[index]} at index $index" }

		parsedView[index] = newElement
		observedValue[index] = serializedNew
		rawValue[index] = serializedNew

		parent.save()

		return old
	}

	final override val size: Int get() {
		observe()
		return parsedView.size
	}

	override fun get(index: Int): V {
		observe()
		return parsedView[index]
	}
}

fun <T : Any> ForgeConfigSpec.Builder.defineObjectList(
	name: String,
	defaultValues: () -> List<T>,
	write: (value: T) -> Pair<String, T>?,
	read: (value: String) -> T?
): ObservedConfigList<T> {
	val parent = defineListAllowEmpty(name.split('.'), { defaultValues.invoke().mapNotNull { write.invoke(it)?.first }.toMutableList()}, { it is String }) as ForgeConfigSpec.ConfigValue<MutableList<String>>

	return object : ObservedConfigList<T>(parent) {
		override fun toString(value: T): Pair<String, T>? {
			return write.invoke(value)
		}

		override fun fromString(value: String): T? {
			return read.invoke(value)
		}
	}
}

@Deprecated("Obviously better use tags")
fun <T : Any> ForgeConfigSpec.Builder.defineForgeObjectList(
	name: String,
	defaultValues: () -> List<T>,
	registry: IForgeRegistry<T>
): ObservedConfigList<T> {
	return defineObjectList(name, defaultValues,
		write = { (registry.getKey(it)?.toString() ?: return@defineObjectList null) to it },
		read = { registry.getValue(ResourceLocation(it)) })
}
