package ru.dbotthepony.kstarbound.json

import com.google.gson.Gson
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import java.lang.reflect.Constructor
import java.lang.reflect.ParameterizedType

/**
 * Creates single point of reference for both native json and legacy json
 * values
 */
abstract class NativeLegacy<NATIVE, LEGACY> {
	protected abstract fun computeLegacy(value: NATIVE): LEGACY
	protected abstract fun computeNative(value: LEGACY): NATIVE

	protected var nativeValue: KOptional<NATIVE> = KOptional()
	protected var legacyValue: KOptional<LEGACY> = KOptional()

	val legacy: LEGACY get() {
		if (legacyValue.isEmpty) {
			val compute = computeLegacy(nativeValue.value)
			legacyValue = KOptional(compute)
			return compute
		}

		return legacyValue.value
	}

	val native: NATIVE get() {
		if (nativeValue.isEmpty) {
			val compute = computeNative(legacyValue.value)
			nativeValue = KOptional(compute)
			return compute
		}

		return nativeValue.value
	}

	class Tile() : NativeLegacy<Registry.Ref<TileDefinition>, Int?>() {
		constructor(tile: Registry.Entry<TileDefinition>?) : this() {
			if (tile == null)
				legacyValue = KOptional(65535)
			else
				nativeValue = KOptional(tile.ref)
		}

		constructor(tile: Registry.Ref<TileDefinition>?) : this() {
			if (tile == null)
				legacyValue = KOptional(65535)
			else
				nativeValue = KOptional(tile)
		}

		override fun computeLegacy(value: Registry.Ref<TileDefinition>): Int {
			return value.value?.materialId ?: 65535
		}

		override fun computeNative(value: Int?): Registry.Ref<TileDefinition> {
			value ?: return BuiltinMetaMaterials.EMPTY.ref
			return Registries.tiles.ref(value)
		}
	}

	class TileMod() : NativeLegacy<Registry.Ref<TileModifierDefinition>, Int?>() {
		constructor(tile: Registry.Entry<TileModifierDefinition>?) : this() {
			if (tile == null)
				legacyValue = KOptional(65535)
			else
				nativeValue = KOptional(tile.ref)
		}

		constructor(tile: Registry.Ref<TileModifierDefinition>?) : this() {
			if (tile == null)
				legacyValue = KOptional(65535)
			else
				nativeValue = KOptional(tile)
		}

		override fun computeLegacy(value: Registry.Ref<TileModifierDefinition>): Int {
			return value.value?.modId ?: 65535
		}

		override fun computeNative(value: Int?): Registry.Ref<TileModifierDefinition> {
			value ?: return BuiltinMetaMaterials.EMPTY_MOD.ref
			return Registries.tileModifiers.ref(value)
		}
	}

	private class Adapter<L, R>(gson: Gson, left: TypeToken<L>, right: TypeToken<R>, private val constructor: Constructor<NativeLegacy<L, R>>) : TypeAdapter<NativeLegacy<L, R>>() {
		private val left = gson.getAdapter(left)
		private val right = gson.getAdapter(right)
		private val either = gson.getAdapter(TypeToken.getParameterized(Either::class.java, left.type, right.type)) as TypeAdapter<Either<L, R>>

		override fun write(out: JsonWriter, value: NativeLegacy<L, R>?) {
			if (value == null)
				out.nullValue()
			else if (Starbound.IS_LEGACY_JSON || value.nativeValue.isEmpty)
				right.write(out, value.legacy)
			else
				left.write(out, value.native)
		}

		override fun read(`in`: JsonReader): NativeLegacy<L, R>? {
			if (`in`.consumeNull())
				return null

			val instance = constructor.newInstance()
			val either = either.read(`in`)
			instance.nativeValue = either.left
			instance.legacyValue = either.right
			return instance
		}
	}

	companion object : TypeAdapterFactory {
		@Suppress("UNCHECKED_CAST")
		override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
			if (NativeLegacy::class.java.isAssignableFrom(type.rawType)) {
				val rawType = type.rawType as? Class<*> ?: throw RuntimeException("bro what the fuck?")
				val superclass = rawType.genericSuperclass as? ParameterizedType ?: throw RuntimeException("bro what the fuck?")

				return Adapter(
					gson,
					TypeToken.get(superclass.actualTypeArguments[0]) as TypeToken<Any>,
					TypeToken.get(superclass.actualTypeArguments[1]) as TypeToken<Any>,
					rawType.getDeclaredConstructor() as Constructor<NativeLegacy<Any, Any>>
				) as TypeAdapter<T>
			}

			return null
		}
	}
}
