package ru.dbotthepony.mc.otm.core.util

import com.google.common.collect.ImmutableList
import com.mojang.serialization.Codec
import net.minecraft.nbt.ByteTag
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.DoubleTag
import net.minecraft.nbt.FloatTag
import net.minecraft.nbt.IntTag
import net.minecraft.nbt.ListTag
import net.minecraft.nbt.LongTag
import net.minecraft.nbt.NbtOps
import net.minecraft.nbt.NumericTag
import net.minecraft.nbt.StringTag
import net.minecraft.nbt.Tag
import net.minecraft.resources.ResourceLocation
import net.minecraftforge.common.util.INBTSerializable
import ru.dbotthepony.mc.otm.core.GetterSetter
import ru.dbotthepony.mc.otm.core.asGetterSetter
import ru.dbotthepony.mc.otm.core.immutableList
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.math.Vector
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.data.getOrNull
import java.util.function.Supplier
import kotlin.reflect.KMutableProperty0
import kotlin.reflect.KProperty0

/**
 * Utility class to manage serialization and deserialization of properties
 * of mutable class
 */
class Savetables : INBTSerializable<CompoundTag?> {
	private val entries = ArrayList<Entry<*, *>>()

	sealed interface Entry<V : Any?, T : Tag?> : INBTSerializable<T?> {
		val name: String
		val type: Class<T>
		fun validate()
	}

	inline fun <V : INBTSerializable<T?>, reified T : Tag> stateful(getter: Supplier<V>, name: String): Stateful<V, T> {
		return stateful(getter, name, T::class.java)
	}

	inline fun <V : INBTSerializable<T?>, reified T : Tag> stateful(getter: V, name: String): Stateful<V, T> {
		return stateful(getter, name, T::class.java)
	}

	inline fun <V : INBTSerializable<T?>, reified T : Tag> stateful(getter: KProperty0<V>, name: String = getter.name): Stateful<V, T> {
		return stateful(getter, name, T::class.java)
	}

	inline fun <V : INBTSerializable<T?>, reified T : Tag> stateful(values: List<V>, name: String): ImmutableList<Stateful<V, T>> {
		return immutableList(values.size) {
			stateful(values[it], "${name}_$it", T::class.java)
		}
	}

	fun <V : INBTSerializable<T?>, T : Tag> stateful(getter: Supplier<V>, name: String, type: Class<T>): Stateful<V, T> {
		return Stateful(getter, name, type)
			.withSerializer { it.serializeNBT() }
			.withDeserializer { v, t -> v.deserializeNBT(t) }
	}

	fun <V : INBTSerializable<T?>, T : Tag> stateful(getter: V, name: String, type: Class<T>): Stateful<V, T> {
		return Stateful({ getter }, name, type)
			.withSerializer { it.serializeNBT() }
			.withDeserializer { v, t -> v.deserializeNBT(t) }
	}

	fun <V : INBTSerializable<T?>, T : Tag> stateful(getter: KProperty0<V>, name: String = getter.name, type: Class<T>): Stateful<V, T> {
		return Stateful(getter, name, type)
			.withSerializer { it.serializeNBT() }
			.withDeserializer { v, t -> v.deserializeNBT(t) }
	}

	fun decimal(prop: GetterSetter<Decimal>, name: String, default: Decimal = Decimal.ZERO): Stateless<Decimal, Tag> {
		return Stateless(prop, name, Tag::class.java)
			.withDeserializer { Decimal.deserializeNBT(it) }
			.withSerializer { it.serializeNBT() }
			.withDefault { default }
	}

	fun decimalNullable(prop: GetterSetter<Decimal?>, name: String, default: Decimal = Decimal.ZERO): Stateless<Decimal?, Tag> {
		return Stateless(prop, name, Tag::class.java)
			.withDeserializer { Decimal.deserializeNBT(it) }
			.withSerializer { it?.serializeNBT() }
			.withDefault { default }
	}

	fun decimal(prop: KMutableProperty0<Decimal>, name: String = prop.name, default: Decimal = Decimal.ZERO): Stateless<Decimal, Tag> {
		return Stateless(prop, name, Tag::class.java)
			.withDeserializer { Decimal.deserializeNBT(it) }
			.withSerializer { it.serializeNBT() }
			.withDefault { default }
	}

	fun decimalNullable(prop: KMutableProperty0<Decimal?>, name: String = prop.name, default: Decimal = Decimal.ZERO): Stateless<Decimal?, Tag> {
		return Stateless(prop, name, Tag::class.java)
			.withDeserializer { Decimal.deserializeNBT(it) }
			.withSerializer { it?.serializeNBT() }
			.withDefault { default }
	}

	fun float(prop: GetterSetter<Float>, name: String): Stateless<Float, NumericTag> {
		return Stateless(prop, name, NumericTag::class.java)
			.withSerializer { FloatTag.valueOf(it) }
			.withDeserializer { it.asFloat }
	}

	fun float(prop: KMutableProperty0<Float>, name: String = prop.name): Stateless<Float, NumericTag> {
		return Stateless(prop, name, NumericTag::class.java)
			.withSerializer { FloatTag.valueOf(it) }
			.withDeserializer { it.asFloat }
	}

	fun double(prop: GetterSetter<Double>, name: String): Stateless<Double, NumericTag> {
		return Stateless(prop, name, NumericTag::class.java)
			.withSerializer { DoubleTag.valueOf(it) }
			.withDeserializer { it.asDouble }
	}

	fun double(prop: KMutableProperty0<Double>, name: String = prop.name): Stateless<Double, NumericTag> {
		return Stateless(prop, name, NumericTag::class.java)
			.withSerializer { DoubleTag.valueOf(it) }
			.withDeserializer { it.asDouble }
	}

	fun int(prop: GetterSetter<Int>, name: String): Stateless<Int, NumericTag> {
		return Stateless(prop, name, NumericTag::class.java)
			.withSerializer { IntTag.valueOf(it) }
			.withDeserializer { it.asInt }
	}

	fun int(prop: KMutableProperty0<Int>, name: String = prop.name): Stateless<Int, NumericTag> {
		return Stateless(prop, name, NumericTag::class.java)
			.withSerializer { IntTag.valueOf(it) }
			.withDeserializer { it.asInt }
	}

	fun long(prop: GetterSetter<Long>, name: String): Stateless<Long, NumericTag> {
		return Stateless(prop, name, NumericTag::class.java)
			.withSerializer { LongTag.valueOf(it) }
			.withDeserializer { it.asLong }
	}

	fun long(prop: KMutableProperty0<Long>, name: String = prop.name): Stateless<Long, NumericTag> {
		return Stateless(prop, name, NumericTag::class.java)
			.withSerializer { LongTag.valueOf(it) }
			.withDeserializer { it.asLong }
	}

	fun bool(prop: GetterSetter<Boolean>, name: String): Stateless<Boolean, NumericTag> {
		return Stateless(prop, name, NumericTag::class.java)
			.withSerializer { ByteTag.valueOf(it) }
			.withDeserializer { it.asByte > 0 }
	}

	fun bool(prop: KMutableProperty0<Boolean>, name: String = prop.name): Stateless<Boolean, NumericTag> {
		return Stateless(prop, name, NumericTag::class.java)
			.withSerializer { ByteTag.valueOf(it) }
			.withDeserializer { it.asByte > 0 }
	}

	fun string(prop: GetterSetter<String>, name: String): Stateless<String, StringTag> {
		return Stateless(prop, name, StringTag::class.java)
			.withSerializer { StringTag.valueOf(it) }
			.withDeserializer { it.asString }
	}

	fun string(prop: KMutableProperty0<String>, name: String = prop.name): Stateless<String, StringTag> {
		return Stateless(prop, name, StringTag::class.java)
			.withSerializer { StringTag.valueOf(it) }
			.withDeserializer { it.asString }
	}

	fun <E : Enum<E>> enum(prop: GetterSetter<E>, name: String, map: (String) -> E): Stateless<E, StringTag> {
		return Stateless(prop, name, StringTag::class.java)
			.withSerializer { StringTag.valueOf(it.name) }
			.withDeserializer { map.invoke(it.asString) }
	}

	fun <E : Enum<E>> enum(prop: KMutableProperty0<E>, name: String = prop.name, map: (String) -> E): Stateless<E, StringTag> {
		return Stateless(prop, name, StringTag::class.java)
			.withSerializer { StringTag.valueOf(it.name) }
			.withDeserializer { map.invoke(it.asString) }
	}

	fun <T : Any> codecNullable(prop: GetterSetter<T?>, codec: Codec<T>, name: String): Stateless<T?, Tag> {
		return Stateless(prop, name, Tag::class.java)
			.withSerializer { prop.get()?.let { codec.encode(it, NbtOps.INSTANCE, NbtOps.INSTANCE.empty()).getOrThrow(false) { throw IllegalStateException("Failed to write NBT data for $name: $it") } } }
			.withDeserializer { codec.decode(NbtOps.INSTANCE, it).getOrThrow(false) { throw IllegalStateException("Failed to read NBT data for $name: $it") }.first }
	}

	fun <T : Any> codecNullable(prop: KMutableProperty0<T?>, codec: Codec<T>, name: String = prop.name): Stateless<T?, Tag> {
		return codecNullable(prop.asGetterSetter(), codec, name)
	}

	fun <T : Any> codec(prop: GetterSetter<T>, codec: Codec<T>, name: String): Stateless<T, Tag> {
		return Stateless(prop, name, Tag::class.java)
			.withSerializer { codec.encode(prop.get(), NbtOps.INSTANCE, NbtOps.INSTANCE.empty()).getOrThrow(false) { throw IllegalStateException("Failed to write NBT data for $name: $it") } }
			.withDeserializer { codec.decode(NbtOps.INSTANCE, it).getOrThrow(false) { throw IllegalStateException("Failed to read NBT data for $name: $it") }.first }
	}

	fun <T : Any> codec(prop: KMutableProperty0<T>, codec: Codec<T>, name: String = prop.name): Stateless<T, Tag> {
		return codec(prop.asGetterSetter(), codec, name)
	}

	fun vector(prop: GetterSetter<Vector>, name: String, default: Vector = Vector.ZERO): Stateless<Vector, ListTag> {
		return Stateless(prop, name, ListTag::class.java)
			.withSerializer { ListTag().also { l ->
				l.add(DoubleTag.valueOf(it.x))
				l.add(DoubleTag.valueOf(it.y))
				l.add(DoubleTag.valueOf(it.z))
			} }
			.withDeserializer {
				if (it.size < 3) {
					default
				} else {
					val x = it[0] as? NumericTag ?: return@withDeserializer default
					val y = it[1] as? NumericTag ?: return@withDeserializer default
					val z = it[2] as? NumericTag ?: return@withDeserializer default
					Vector(x.asDouble, y.asDouble, z.asDouble)
				}
			}
	}

	fun vector(prop: KMutableProperty0<Vector>, name: String = prop.name, default: Vector = Vector.ZERO): Stateless<Vector, ListTag> {
		return vector(prop.asGetterSetter(), name, default)
	}

	fun location(prop: GetterSetter<ResourceLocation>, name: String): Stateless<ResourceLocation, StringTag> {
		return Stateless(prop, name, StringTag::class.java)
			.withSerializer { StringTag.valueOf(it.toString()) }
			.withDeserializer { ResourceLocation(it.asString) }
	}

	fun location(prop: KMutableProperty0<ResourceLocation>, name: String = prop.name): Stateless<ResourceLocation, StringTag> {
		return Stateless(prop, name, StringTag::class.java)
			.withSerializer { StringTag.valueOf(it.toString()) }
			.withDeserializer { ResourceLocation(it.asString) }
	}

	override fun serializeNBT(): CompoundTag {
		return CompoundTag().also(::serializeNBT)
	}

	private var validated = false

	fun validate() {
		if (validated) return

		for (entry in entries)
			entry.validate()

		validated = true
	}

	override fun deserializeNBT(nbt: CompoundTag?) {
		validate()

		if (nbt == null) {
			for (entry in entries) {
				entry.deserializeNBT(null)
			}
		} else {
			for (entry in entries) {
				val value = nbt[entry.name]

				if (value != null && entry.type.isAssignableFrom(value.javaClass)) {
					(entry as INBTSerializable<Tag>).deserializeNBT(value)
				} else {
					entry.deserializeNBT(null)
				}
			}
		}
	}

	fun serializeNBT(nbt: CompoundTag) {
		validate()

		for (entry in entries) {
			val value = entry.serializeNBT()

			if (value != null)
				nbt[entry.name] = value
		}
	}

	inner class Stateful<V : Any?, T : Tag>(private val prop: Supplier<V>, override val name: String, override val type: Class<T>) : Entry<V, T> {
		constructor(field: KProperty0<V>, name: String = field.name, type: Class<T>) : this(field::get, name, type)

		init {
			check(!entries.any { it.name == name }) { "Already has entry with name $name!" }
			entries.add(this)
			validated = false
		}

		private var serializer: ((V) -> T?)? = null
		private var deserializer: ((V, T) -> Unit)? = null
		private var resetter: ((V) -> Unit)? = null

		fun withSerializer(serializer: (V) -> T?): Stateful<V, T> {
			this.serializer = serializer
			return this
		}

		fun withDeserializer(deserializer: (V, T) -> Unit): Stateful<V, T> {
			this.deserializer = deserializer
			return this
		}

		fun withResetter(resetter: (V) -> Unit): Stateful<V, T> {
			this.resetter = resetter
			return this
		}

		override fun serializeNBT(): T? {
			return checkNotNull(serializer) { "No serializer specified for $name" }.invoke(prop.get())
		}

		override fun deserializeNBT(nbt: T?) {
			if (nbt == null) {
				resetter?.invoke(prop.get())
			} else {
				checkNotNull(deserializer) { "No deserializer specified for $name" }.invoke(prop.get(), nbt)
			}
		}

		override fun validate() {
			checkNotNull(serializer) { "No serializer specified for $name" }
			checkNotNull(deserializer) { "No deserializer specified for $name" }
		}
	}

	inner class Stateless<V : Any?, T : Tag>(private val prop: GetterSetter<V>, override val name: String, override val type: Class<T>) : Entry<V, T> {
		constructor(field: KMutableProperty0<V>, name: String = field.name, type: Class<T>) : this(field.asGetterSetter(), name, type)

		init {
			check(!entries.any { it.name == name }) { "Already has entry with name $name!" }
			entries.add(this)
			validated = false
		}

		private var serializer: ((V) -> T?)? = null
		private var deserializer: ((T) -> V)? = null
		private var default: (() -> V)? = null

		override fun validate() {
			checkNotNull(serializer) { "No serializer specified for $name" }
			checkNotNull(deserializer) { "No deserializer specified for $name" }
		}

		fun withSerializer(serializer: (V) -> T?): Stateless<V, T> {
			this.serializer = serializer
			return this
		}

		fun withDeserializer(deserializer: (T) -> V): Stateless<V, T> {
			this.deserializer = deserializer
			return this
		}

		fun withDefault(default: () -> V): Stateless<V, T> {
			this.default = default
			return this
		}

		override fun serializeNBT(): T? {
			return checkNotNull(serializer) { "No serializer specified for $name" }.invoke(prop.get())
		}

		override fun deserializeNBT(nbt: T?) {
			if (nbt == null) {
				if (default != null) {
					prop.accept(default!!.invoke())
				}
			} else {
				prop.accept(checkNotNull(deserializer) { "No deserializer specified for $name" }.invoke(nbt))
			}
		}
	}
}
