package ru.dbotthepony.kstarbound.defs.quest

import com.google.common.collect.ImmutableList
import com.google.gson.Gson
import com.google.gson.JsonElement
import com.google.gson.JsonObject
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.gson.set
import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kommons.io.readCollection
import ru.dbotthepony.kommons.io.readVector2d
import ru.dbotthepony.kommons.io.readVector2f
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeCollection
import ru.dbotthepony.kommons.io.writeStruct2d
import ru.dbotthepony.kommons.io.writeStruct2f
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.actor.Gender
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.json.builder.DispatchingAdapter
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonSingleton
import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.readJsonObject
import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.json.writeJsonObject
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import ru.dbotthepony.kstarbound.util.asStringOrNull
import ru.dbotthepony.kstarbound.util.coalesceNull
import ru.dbotthepony.kstarbound.world.UniversePos
import java.io.DataInputStream
import java.io.DataOutputStream

class QuestParameter(
	val detail: Detail,
	val name: String? = null,
	val portrait: JsonElement? = null,
	val indicator: String? = null
) {
	constructor(stream: DataInputStream, isLegacy: Boolean) : this(
		readDetail(stream, isLegacy),
		if (stream.readBoolean()) stream.readInternedString() else null,
		if (stream.readBoolean()) stream.readJsonElement() else null,
		if (stream.readBoolean()) stream.readInternedString() else null,
	)

	enum class Type(override val jsonName: String, val token: TypeToken<out Detail>) : IStringSerializable {
		NO_DETAIL("noDetail", TypeToken.get(Empty::class.java)),
		ITEM("item", TypeToken.get(Item::class.java)),
		ITEM_TAG("itemTag", TypeToken.get(ItemTag::class.java)),
		ITEM_LIST("itemList", TypeToken.get(ItemList::class.java)),
		ENTITY("entity", TypeToken.get(Entity::class.java)),
		LOCATION("location", TypeToken.get(Location::class.java)),
		MONSTER_TYPE("monsterType", TypeToken.get(MonsterType::class.java)),
		NPC_TYPE("npcType", TypeToken.get(NpcType::class.java)),
		COORDINATE("coordinate", TypeToken.get(Coordinate::class.java)),
		JSON("json", TypeToken.get(JsonData::class.java));
	}

	fun write(stream: DataOutputStream, isLegacy: Boolean) {
		detail.write(stream, isLegacy)
		stream.writeBoolean(name != null)
		if (name != null) stream.writeBinaryString(name)
		stream.writeBoolean(portrait != null)
		if (portrait != null) stream.writeJsonElement(portrait)
		stream.writeBoolean(indicator != null)
		if (indicator != null) stream.writeBinaryString(indicator)
	}

	override fun hashCode(): Int {
		return name.hashCode() * 31 * 31 + portrait.hashCode() * 31 + indicator.hashCode()
	}

	override fun equals(other: Any?): Boolean {
		return this === other || other is QuestParameter && name == other.name && portrait == other.portrait && indicator == other.indicator
	}
	
	sealed class Detail {
		abstract val type: Type
		abstract fun write(stream: DataOutputStream, isLegacy: Boolean)
	}

	@JsonSingleton
	object Empty : Detail() {
		override val type: Type
			get() = Type.NO_DETAIL

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(type.ordinal)
		}
	}

	// Item name - always one single item. QuestItem and QuestItemList are
	// distinct due to how the surrounding text interacts with the parameter
	// in the quest text. For a single item we might want to say "the <bandage>" or
	// "any <bandage>", whereas the text for QuestItemList is always a list, e.g.
	// "<1 bandage, 3 apple>."
	@JsonFactory
	data class Item(val item: ItemDescriptor) : Detail() {
		constructor(stream: DataInputStream, isLegacy: Boolean) : this(
			if (isLegacy) ItemDescriptor(stream.readInternedString(), 1L, stream.readJsonElement() as JsonObject) else ItemDescriptor(stream))

		override val type: Type
			get() = Type.ITEM

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(type.ordinal)

			if (isLegacy) {
				stream.writeBinaryString(item.name)
				stream.writeJsonElement(item.parameters)
			} else {
				item.write(stream)
			}
		}
	}

	// An item itemTag, indicating a set of possible items
	@JsonFactory
	data class ItemTag(val tag: String) : Detail() {
		constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInternedString())

		override val type: Type
			get() = Type.ITEM_TAG

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(type.ordinal)
			stream.writeBinaryString(tag)
		}
	}

	// A collection of items
	@JsonFactory
	data class ItemList(val items: ImmutableList<ItemDescriptor>) : Detail() {
		constructor(stream: DataInputStream, isLegacy: Boolean) : this(ImmutableList.copyOf(stream.readCollection { ItemDescriptor(this) }))

		override val type: Type
			get() = Type.ITEM_LIST

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(type.ordinal)
			stream.writeCollection(items) { it.write(this) }
		}
	}

	// The uniqueId of a specific entity
	@JsonFactory
	data class Entity(val uniqueId: String? = null, val species: String? = null, val gender: Gender? = null) : Detail() {
		constructor(stream: DataInputStream, isLegacy: Boolean) : this(
			if (stream.readBoolean()) stream.readInternedString() else null,
			if (stream.readBoolean()) stream.readInternedString() else null,
			if (stream.readBoolean()) Gender.entries[stream.readUnsignedByte()] else null,
		)

		override val type: Type
			get() = Type.ENTITY

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(type.ordinal)

			stream.writeBoolean(uniqueId != null)
			if (uniqueId != null) stream.writeBinaryString(uniqueId)

			stream.writeBoolean(species != null)
			if (species != null) stream.writeBinaryString(species)

			stream.writeBoolean(gender != null)
			if (gender != null) stream.writeByte(gender.ordinal)
		}
	}

	// A location within the world, which could represent a spawn point or a dungeon
	@JsonFactory
	data class Location(val uniqueId: String? = null, val region: Vector2d) : Detail() {
		constructor(stream: DataInputStream, isLegacy: Boolean) : this(
			if (stream.readBoolean()) stream.readInternedString() else null,
			if (isLegacy) stream.readVector2f().toDoubleVector() else stream.readVector2d(),
		)

		override val type: Type
			get() = Type.LOCATION

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(type.ordinal)
			stream.writeBoolean(uniqueId != null)
			if (uniqueId != null)
				stream.writeBinaryString(uniqueId)

			if (isLegacy)
				stream.writeStruct2f(region.toFloatVector())
			else
				stream.writeStruct2d(region)
		}
	}

	@JsonFactory
	data class MonsterType(val typeName: String, val parameters: JsonObject = JsonObject()) : Detail() {
		constructor(stream: DataInputStream, isLegacy: Boolean) : this(
			stream.readInternedString(),
			stream.readJsonObject(),
		)

		override val type: Type
			get() = Type.MONSTER_TYPE

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(type.ordinal)
			stream.writeBinaryString(typeName)
			stream.writeJsonObject(parameters)
		}
	}

	@JsonFactory
	data class NpcType(val species: String, val typeName: String, val parameters: JsonObject = JsonObject(), val seed: Long? = null) : Detail() {
		constructor(stream: DataInputStream, isLegacy: Boolean) : this(
			stream.readInternedString(),
			stream.readInternedString(),
			stream.readJsonObject(),
			if (stream.readBoolean()) stream.readLong() else null,
		)

		override val type: Type
			get() = Type.NPC_TYPE

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(type.ordinal)
			stream.writeBinaryString(species)
			stream.writeBinaryString(typeName)
			stream.writeJsonObject(parameters)
			stream.writeBoolean(seed != null)
			if (seed != null) stream.writeLong(seed)
		}
	}

	@JsonFactory
	data class Coordinate(val coordinate: UniversePos) : Detail() {
		constructor(stream: DataInputStream, isLegacy: Boolean) : this(UniversePos(stream, isLegacy))

		override val type: Type
			get() = Type.COORDINATE

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(type.ordinal)
			coordinate.write(stream, isLegacy)
		}
	}

	class JsonData(val json: JsonObject) : Detail() {
		override val type: Type
			get() = Type.JSON

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeJsonElement(json)
		}
	}

	class Adapter(gson: Gson) : TypeAdapter<QuestParameter>() {
		private val details = DETAIL_ADAPTER.create(gson, TypeToken.get(Detail::class.java))!!

		override fun write(out: JsonWriter, value: QuestParameter?) {
			if (value == null)
				out.nullValue()
			else {
				val base = if (value.detail is JsonData) {
					value.detail.json.deepCopy()
				} else {
					details.toJsonTree(value.detail) as JsonObject
				}

				if (value.name != null) base["name"] = value.name
				if (value.portrait != null) base["portrait"] = value.portrait
				if (value.indicator != null) base["indicator"] = value.indicator
				out.value(base)
			}
		}

		override fun read(`in`: JsonReader): QuestParameter? {
			if (`in`.consumeNull())
				return null

			val read = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)

			val detail = if (read["type"]?.asString == "json") {
				JsonData(read)
			} else {
				details.fromJsonTree(read)
			}

			val name = read["name"]?.asStringOrNull
			val portrait = read["portrait"]?.coalesceNull
			val indicator = read["indicator"]?.asStringOrNull
			return QuestParameter(detail, name, portrait, indicator)
		}
	}

	companion object {
		val CODEC = nativeCodec(::QuestParameter, QuestParameter::write)
		val LEGACY_CODEC = legacyCodec(::QuestParameter, QuestParameter::write)
		private val DETAIL_ADAPTER = DispatchingAdapter("type", { type }, { token }, Type.entries)

		fun readDetail(stream: DataInputStream, isLegacy: Boolean): Detail {
			return when (Type.entries[stream.readUnsignedByte()]) {
				Type.NO_DETAIL -> Empty
				Type.ITEM -> Item(stream, isLegacy)
				Type.ITEM_TAG -> ItemTag(stream, isLegacy)
				Type.ITEM_LIST -> ItemList(stream, isLegacy)
				Type.ENTITY -> Entity(stream, isLegacy)
				Type.LOCATION -> Location(stream, isLegacy)
				Type.MONSTER_TYPE -> MonsterType(stream, isLegacy)
				Type.NPC_TYPE -> NpcType(stream, isLegacy)
				Type.COORDINATE -> Coordinate(stream, isLegacy)
				Type.JSON -> JsonData(stream.readJsonElement() as JsonObject)
			}
		}
	}
}
