package ru.dbotthepony.kstarbound.json.builder

import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.github.benmanes.caffeine.cache.Interner
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonParseException
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.internal.bind.JsonTreeReader
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.util.enrollList
import ru.dbotthepony.kstarbound.defs.util.enrollMap
import ru.dbotthepony.kstarbound.defs.util.flattenJsonElement
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.value
import java.lang.reflect.Constructor
import java.util.Collections
import java.util.function.Function
import kotlin.jvm.internal.DefaultConstructorMarker
import kotlin.properties.Delegates
import kotlin.reflect.*
import kotlin.reflect.full.declaredMembers
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.isSupertypeOf
import kotlin.reflect.full.primaryConstructor

/**
 * [TypeAdapter] для классов, которые имеют все свои свойства в главном конструкторе.
 */
class FactoryAdapter<T : Any> private constructor(
	val clazz: KClass<T>,
	val types: ImmutableList<ReferencedProperty<T, *>>,
	aliases: Map<String, List<String>>,
	val asJsonArray: Boolean,
	val stringInterner: Interner<String>,
	val logMisses: Boolean,
	private val elements: TypeAdapter<JsonElement>
) : TypeAdapter<T>() {
	private val name2index = Object2ObjectArrayMap<String, IntArrayList>()
	private val loggedMisses = Collections.synchronizedSet(ObjectArraySet<String>())

	init {
		if (asJsonArray && types.any { it.isFlat }) {
			throw IllegalArgumentException("Can't have both flat properties and input be as json array")
		}

		for ((i, pair) in types.withIndex()) {
			name2index.computeIfAbsent(pair.property.name, Function { IntArrayList() }).add(i)

			aliases.entries.forEach {
				it.value.forEach { target ->
					if (target == pair.property.name) {
						name2index.computeIfAbsent(it.key, Function { IntArrayList() }).add(i)
					}
				}
			}
		}
	}

	/**
	 * Обычный конструктор класса (без флагов "значения по умолчанию")
	 */
	private val regularFactory: KFunction<T> = clazz.constructors.firstOrNull first@{
		val requiredSize = types.size

		if (it.parameters.size == requiredSize) {
			val iterator = types.iterator()
			val factoryIterator = it.parameters.iterator()

			while (factoryIterator.hasNext() && iterator.hasNext()) {
				val param = factoryIterator.next()
				val nextParam = iterator.next()

				val a = param.type
				val b = nextParam.type

				if (!a.isSupertypeOf(b) || a.isMarkedNullable != b.isMarkedNullable) {
					return@first false
				}
			}

			return@first true
		}

		return@first false
	} ?: throw NoSuchElementException("Unable to determine constructor for ${clazz.qualifiedName} matching (${types.joinToString(", ")})")

	/**
	 * Синтетический конструктор класса, который создаётся Kotlin'ном, для создания классов со значениями по умолчанию
	 */
	private val syntheticFactory: Constructor<T>? = try {
		val typelist = types.map { (it.type.classifier as KClass<*>).java }.toMutableList()

		for (i in 0 until (if (types.size % 31 == 0) types.size / 31 else types.size / 31 + 1))
			typelist.add(Int::class.java)

		typelist.add(DefaultConstructorMarker::class.java)

		clazz.java.getDeclaredConstructor(*typelist.toTypedArray())
	} catch(_: NoSuchMethodException) {
		null
	}

	private val syntheticPrimitives = Int2ObjectAVLTreeMap<Any>()

	init {
		if (syntheticFactory != null) {
			for ((i, param) in syntheticFactory.parameters.withIndex()) {
				val type = param.parameterizedType as? Class<*> ?: continue

				if (type.isPrimitive) {
					syntheticPrimitives[i] = when (type) {
						java.lang.Boolean.TYPE -> false
						java.lang.Integer.TYPE -> 0
						java.lang.Long.TYPE -> 0L
						java.lang.Short.TYPE -> (0).toShort()
						java.lang.Byte.TYPE -> (0).toByte()
						java.lang.Character.TYPE -> (0).toChar()
						java.lang.Float.TYPE -> 0f
						java.lang.Double.TYPE -> 0.0
						else -> throw IllegalArgumentException("mamma mia: ${type.name}")
					}
				}
			}
		}
	}

	override fun write(out: JsonWriter, value: T?) {
		if (value == null) {
			out.nullValue()
			return
		}

		if (asJsonArray)
			out.beginArray()
		else
			out.beginObject()

		for (type in types) {
			if (type.isFlat) {
				check(!asJsonArray)

				val (field, adapter) = type
				val result = (adapter as TypeAdapter<Any>).toJsonTree((field as KProperty1<T, Any>).get(value))

				if (result != null && result != JsonNull.INSTANCE) {
					if (result !is JsonObject) {
						throw JsonSyntaxException("Expected JsonObject from adapter of ${type.name}, but got ${result::class.qualifiedName}")
					}

					for ((k, v) in result.entrySet()) {
						out.name(k)
						out.value(v)
					}
				}
			} else {
				val (field, adapter) = type

				if (!asJsonArray)
					out.name(field.name)

				@Suppress("unchecked_cast")
				(adapter as TypeAdapter<Any>).write(out, (field as KProperty1<T, Any>).get(value))
			}
		}

		if (asJsonArray)
			out.endArray()
		else
			out.endObject()
	}

	override fun read(reader: JsonReader): T? {
		if (reader.consumeNull())
			return null

		// таблица присутствия значений (если значение true то на i было значение внутри json)
		val presentValues = BooleanArray(types.size)
		var readValues = arrayOfNulls<Any>(types.size)

		@Suppress("name_shadowing")
		var reader = reader

		// Если нам необходимо читать объект как набор данных массива, то давай
		if (asJsonArray) {
			reader.beginArray()
			val iterator = types.iterator()
			var fieldId = 0

			while (reader.peek() != JsonToken.END_ARRAY) {
				if (!iterator.hasNext()) {
					val name = fieldId.toString()

					if (logMisses && loggedMisses.add(name)) {
						LOGGER.warn("${clazz.qualifiedName} has no property for storing $name")
					}

					reader.skipValue()
					fieldId++

					continue
				}

				val (field, adapter) = iterator.next()

				try {
					readValues[fieldId] = adapter.read(reader)
					presentValues[fieldId] = true
				} catch(err: Throwable) {
					throw JsonSyntaxException("Reading field \"${field.name}\" near ${reader.path} for ${clazz.qualifiedName}", err)
				}

				fieldId++
			}
		// иначе - читаем как json object
		} else {
			var json: JsonObject by Delegates.notNull()

			if (types.any { it.isFlat }) {
				val readMap = elements.read(reader)

				if (readMap !is JsonObject)
					throw JsonParseException("Expected JSON element to be a Map, ${readMap::class.qualifiedName} given")

				json = readMap
				reader = JsonTreeReader(readMap)
			}

			reader.beginObject()

			while (reader.peek() != JsonToken.END_OBJECT) {
				val name = reader.nextName()
				val fields = name2index[name]

				if (fields == null || fields.size == 0) {
					if (logMisses && loggedMisses.add(name)) {
						LOGGER.warn("${clazz.qualifiedName} has no property for storing $name")
					}

					reader.skipValue()
				} else {
					val localReader: Either<JsonElement, JsonReader>

					if (fields.size == 1) {
						localReader = Either.right(reader)
					} else {
						val readValue = elements.read(reader)
						localReader = Either.left(readValue)
					}

					val itr = fields.intIterator()

					while (itr.hasNext()) {
						val fieldId = itr.nextInt()

						@Suppress("NAME_SHADOWING")
						val reader = localReader.map({ JsonTreeReader(it) }, { it })
						val tuple = types[fieldId]

						if (tuple.isFlat) {
							reader.skipValue()
							continue
						}

						if (tuple.isIgnored) {
							if (loggedMisses.add(name)) {
								LOGGER.warn("${clazz.qualifiedName} can not load $name from JSON")
							}

							reader.skipValue()
							continue
						}

						val (field, adapter) = tuple

						try {
							readValues[fieldId] = adapter.read(reader)
							presentValues[fieldId] = true
						} catch(err: Throwable) {
							throw JsonSyntaxException("Reading field \"${field.name}\" near ${reader.path} for ${clazz.qualifiedName}", err)
						}
					}
				}
			}

			for ((i, property) in types.withIndex()) {
				if (property.isFlat) {
					try {
						val read = property.adapter.read(JsonTreeReader(json))

						if (read != null) {
							presentValues[i] = true
							readValues[i] = read
						}
					} catch(err: Throwable) {
						throw JsonSyntaxException("Reading flat field \"${property.property.name}\" for ${clazz.qualifiedName}", err)
					}
				}
			}
		}

		if (asJsonArray) {
			reader.endArray()
		} else {
			reader.endObject()
		}

		// если у нас есть все значения для конструктора (все значения присутствуют в исходном json объекте)
		// или у нас нет синтетического конструктора, то делаем вызов "обычного" конструктора
		if (syntheticFactory == null || presentValues.all { it }) {
			for ((i, tuple) in types.withIndex()) {
				val (field) = tuple

				if (readValues[i] == null) {
					if (!tuple.isMarkedNullable) {
						throw JsonSyntaxException("Field ${field.name} of ${clazz.qualifiedName} does not accept nulls")
					} else if (!regularFactory.parameters[i].isOptional && !presentValues[i]) {
						throw JsonSyntaxException("Field ${field.name} of ${clazz.qualifiedName} must be defined")
					}
				}
			}

			@Suppress("unchecked_cast")
			return regularFactory.call(*readValues as Array<out Any>)
		// иначе - в бой вступает синтетический конструктор, с флагами "значения по умолчанию"
		} else {
			var argumentFlagCount = presentValues.size / 31 + 1 /* DefaultConstructorMarker */
			if (presentValues.size % 31 != 0) argumentFlagCount++
			readValues = readValues.copyOf(readValues.size + argumentFlagCount)

			for ((i, field) in types.withIndex()) {
				val param = regularFactory.parameters[i]

				if (readValues[i] == null && param.isOptional && !param.type.isMarkedNullable) {
					// while this makes whole shit way more lenient, at least it avoids silly errors
					// caused by quirks in original engine serialization process
					presentValues[i] = false
				}
			}

			var flagIndex = readValues.size - argumentFlagCount
			var flags = 0
			var flagBit = 0

			for (isPresent in presentValues) {
				if (!isPresent) flags = flags.or(1.shl(flagBit))
				flagBit++

				if (flagBit >= 32) {
					readValues[flagIndex++] = flags
					flags = 0
					flagBit = 0
				}
			}

			if (flagBit != 0) {
				readValues[flagIndex] = flags
			}

			for ((i, field) in types.withIndex()) {
				if (readValues[i] != null) continue
				val param = regularFactory.parameters[i]

				if (param.isOptional && (!presentValues[i] || i in syntheticPrimitives)) {
					readValues[i] = syntheticPrimitives[i]
				} else if (!param.isOptional) {
					if (!presentValues[i]) throw JsonSyntaxException("Field ${field.name} of ${clazz.qualifiedName} is missing")
					if (!param.type.isMarkedNullable) throw JsonSyntaxException("Field ${field.name} of ${clazz.qualifiedName} does not accept nulls")
				}
			}

			try {
				return syntheticFactory.newInstance(*readValues)
			} catch (err: Throwable) {
				throw JsonSyntaxException("Failed to instance $clazz", err)
			}
		}
	}

	/**
	 * Позволяет построить класс [FactoryAdapter] на основе заданных параметров
	 */
	class Builder<T : Any>(val clazz: KClass<T>, vararg fields: KProperty1<T, *>) : TypeAdapterFactory {
		private var asList = false
		private var storesJson = false
		private val types = ArrayList<ReferencedProperty<T, *>>()
		private val aliases = Object2ObjectArrayMap<String, ArrayList<String>>()
		var stringInterner: Interner<String> = Interner { it }
		var logMisses = true

		init {
			for (field in fields) {
				add(field)
			}
		}

		override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
			if (type.rawType == clazz.java) {
				return build(gson) as TypeAdapter<T>
			}

			return null
		}

		/**
		 * Собирает этот [FactoryAdapter] с указанным GSON объектом
		 */
		fun build(gson: Gson): TypeAdapter<T> {
			check(!asList || types.none { it.isFlat }) { "Can't have both flat properties and json data array layout" }

			return FactoryAdapter(
				clazz = clazz,
				types = ImmutableList.copyOf(types.also { it.forEach{ it.resolve(gson) } }),
				asJsonArray = asList,
				stringInterner = stringInterner,
				aliases = aliases,
				logMisses = logMisses,
				elements = Starbound.ELEMENTS_ADAPTER
			)
		}

		fun <V> add(field: KProperty1<T, V>, isFlat: Boolean = false, isMarkedNullable: Boolean? = null, isIgnored: Boolean = false): Builder<T> {
			types.add(ReferencedProperty(field, isFlat = isFlat, isMarkedNullable = isMarkedNullable, isIgnored = isIgnored))
			return this
		}

		fun alias(alias: String, canonical: String): Builder<T> {
			aliases.computeIfAbsent(alias, Function { ArrayList() }).add(canonical)
			return this
		}

		/**
		 * При выставлении данного флага в качестве исходной структуры будет приниматься Json объект:
		 *
		 * ```json
		 * {
		 * 		"thingname": "a",
		 * 		"count": 4,
		 * 		...
		 * }
		 * ```
		 *
		 * Данный режим используется по умолчанию
		 *
		 * @see inputAsList
		 */
		fun inputAsMap(): Builder<T> {
			asList = false
			return this
		}

		/**
		 * При выставлении данного флага в качестве исходной структуры будет приниматься Json массив:
		 *
		 * ```json
		 * ["a", 4, 100.2, true]
		 * ```
		 *
		 * @see inputAsMap
		 */
		fun inputAsList(): Builder<T> {
			asList = true
			return this
		}
	}

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

		fun <T : Any> createFor(kclass: KClass<T>, gson: Gson, config: JsonFactory = JsonFactory(), stringInterner: Interner<String> = Starbound.STRINGS): TypeAdapter<T> {
			val builder = Builder(kclass)
			val properties = kclass.declaredMembers.filterIsInstance<KProperty1<T, *>>()

			if (config.asList) {
				builder.inputAsList()
			}

			builder.stringInterner = stringInterner
			builder.logMisses = config.logMisses

			if (properties.isEmpty()) {
				throw IllegalArgumentException("${kclass.qualifiedName} has no valid members")
			}

			val foundConstructor = kclass.primaryConstructor ?: throw NoSuchElementException("Can't determine primary constructor for ${kclass.qualifiedName}")

			val params = foundConstructor.parameters
			val lastIndex = params.size

			for (i in 0 until lastIndex) {
				val argument = params[i]
				val property = properties.first { it.name == argument.name && it.returnType.isSupertypeOf(argument.type) }

				builder.add(
					property,
				    isFlat = property.annotations.any { it.annotationClass == JsonFlat::class },
				    isIgnored = property.annotations.any { it.annotationClass == JsonIgnore::class },
				    isMarkedNullable = if (property.annotations.any { it.annotationClass == JsonNotNull::class }) false else null,
				)

				property.annotations.firstOrNull { it.annotationClass == JsonAlias::class }?.let {
					it as JsonAlias

					for (name in it.aliases) {
						builder.alias(name, property.name)
					}
				}
			}

			return builder.build(gson)
		}
	}

	class Factory(val stringInterner: Interner<String> = Interner { it }) : TypeAdapterFactory {
		override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
			val raw = type.rawType

			if (raw.isAnnotationPresent(JsonFactory::class.java)) {
				val first = raw.getAnnotationsByType(JsonFactory::class.java)
				require(first.size == 1) { "Multiple JsonFactory defined: ${first.joinToString(", ")}" }

				val bconfig = first[0] as JsonFactory
				val kclass = raw.kotlin as KClass<T>
				return createFor(kclass, gson, bconfig, stringInterner)
			}

			return null
		}
	}
}
