package ru.dbotthepony.kstarbound.json.builder

import com.google.common.collect.ImmutableMap
import com.github.benmanes.caffeine.cache.Interner
import com.google.gson.Gson
import com.google.gson.JsonObject
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.internal.bind.TypeAdapters
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.objects.ObjectLinkedOpenHashSet
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.defs.util.flattenJsonElement
import kotlin.properties.Delegates
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.declaredMembers
import kotlin.reflect.jvm.isAccessible

/**
 * [TypeAdapter] для классов, которые создаются "без всего", а после наполняются данными (паттерн builder).
 */
class BuilderAdapter<T : Any> private constructor(
	/**
	 * Завод по созданию объектов типа [T]
	 */
	val factory: () -> T,

	/**
	 * Свойства объекта [T], которые можно выставлять
	 */
	val properties: ImmutableMap<String, ReferencedMutableProperty<T, *>>,

	val stringInterner: Interner<String> = Interner { it },
) : TypeAdapter<T>() {
	private val loggedMisses = ObjectOpenHashSet<String>()
	private val flatProperties = properties.values.filter { it.isFlat }

	override fun write(writer: JsonWriter, value: T) {
		TODO("Not yet implemented")
	}

	override fun read(reader: JsonReader): T {
		@Suppress("name_shadowing")
		var reader = reader

		// загружаем указатели на стек
		val properties = properties

		val missing = ObjectOpenHashSet<ReferencedMutableProperty<T, *>>()
		missing.addAll(properties.values)

		val instance = factory.invoke()
		var json: JsonObject by Delegates.notNull()

		if (instance is IJsonHolder || flatProperties.isNotEmpty()) {
			json = TypeAdapters.JSON_ELEMENT.read(reader) as JsonObject
			reader = JsonTreeReader(json)

			if (instance is INativeJsonHolder) {
				instance.acceptJson(flattenJsonElement(json, stringInterner::intern))
			} else if (instance is IJsonHolder) {
				instance.acceptJson(json)
			}
		}

		// загружаем указатели на стек

		reader.beginObject()

		while (reader.hasNext()) {
			val name = reader.nextName()
			val property = properties[name]

			if (property != null) {
				try {
					val peek = reader.peek()

					if (!property.isMarkedNullable && peek === JsonToken.NULL) {
						throw NullPointerException("Property ${property.name} of ${instance::class.qualifiedName} does not accept nulls (JSON contains null)")
					} else if (peek === JsonToken.NULL) {
						property.set(instance, null)
						reader.nextNull()
					} else {
						val readValue = property.adapter.read(reader)

						if (!property.isMarkedNullable && readValue == null)
							throw JsonSyntaxException("Property ${property.name} of ${instance::class.qualifiedName} does not accept nulls (Type provider returned null)")

						property.set(instance, readValue)
						check(missing.remove(property))
					}
				} catch(err: Throwable) {
					throw JsonSyntaxException("Reading property ${property.name} of ${instance::class.qualifiedName} near ${reader.path}", err)
				}
			} else {
				if (loggedMisses.add(name)) {
					LOGGER.warn("${instance::class.qualifiedName} has no property for storing $name")
				}

				reader.skipValue()
			}
		}

		reader.endObject()

		for (property in flatProperties) {
			if (!missing.remove(property))
				continue

			try {
				val read = property.adapter.read(JsonTreeReader(json))

				if (!property.isMarkedNullable && read == null) {
					throw NullPointerException("Property ${property.name} of ${instance::class.qualifiedName} does not accept nulls (flat property adapter returned NULL)")
				} else if (read == null) {
					property.set(instance, null)
				} else {
					property.set(instance, read)
				}
			} catch(err: Throwable) {
				throw JsonSyntaxException("Reading flat property ${property.name} of ${instance::class.qualifiedName} near ${reader.path}", err)
			}
		}

		for (property in missing) {
			if (property.mustBePresent == true) {
				throw JsonSyntaxException("${instance::class.qualifiedName} demands for ${property.name} to be present, however, it is missing from JSON structure")
			} else if (property.mustBePresent == null) {
				if (property.isMarkedNullable) {
					continue
				}

				try {
					property.property.get(instance)
				} catch (err: Throwable) {
					throw JsonSyntaxException("${property.name} in ${instance::class.qualifiedName} does not like it being missing from JSON structure", err)
				}
			}
		}

		return instance
	}

	class Builder<T : Any>(val factory: () -> T, vararg fields: KMutableProperty1<T, *>) : TypeAdapterFactory {
		private val properties = ArrayList<ReferencedMutableProperty<T, *>>()
		private val factoryReturnType by lazy { factory.invoke()::class.java }

		var stringInterner: Interner<String> = Interner { it }

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

			return null
		}

		fun build(gson: Gson): BuilderAdapter<T> {
			val map = ImmutableMap.Builder<String, ReferencedMutableProperty<T, *>>()

			for (property in properties)
				map.put(property.property.name, property.also { it.resolve(gson) })

			return BuilderAdapter(
				factory = factory,
				properties = map.build(),
				stringInterner = stringInterner,
			)
		}

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

		fun <V> add(property: KMutableProperty1<T, V>, isFlat: Boolean = false, mustBePresent: Boolean? = null): Builder<T> {
			if (properties.any { it.property == property }) {
				throw IllegalArgumentException("Property $property is defined twice")
			}

			properties.add(ReferencedMutableProperty(
				property = property,
				mustBePresent = mustBePresent,
				isFlat = isFlat,
			))

			return this
		}
	}

	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(JsonBuilder::class.java)) {
				val kclass = raw.kotlin
				val builder = Builder(kclass.constructors.first { it.parameters.isEmpty() } as () -> T)

				builder.stringInterner = stringInterner

				val declarations = LinkedHashMap<String, KMutableProperty1<*, *>>()
				collectDecl(kclass, declarations)

				for (decl in declarations.values) {
					if (decl.annotations.none { it is JsonIgnore }) {
						builder.add(
							decl as KMutableProperty1<T, *>,
							isFlat = decl.annotations.any { it.annotationClass == JsonFlat::class },
							mustBePresent = if (decl.annotations.any { it.annotationClass == JsonRequired::class }) true else if (decl.annotations.any { it.annotationClass == JsonOptional::class }) false else null
						)
					}
				}

				return builder.build(gson)
			}

			return null
		}
	}

	companion object {
		private val LOGGER = LogManager.getLogger()
		private val declCache = Reference2ObjectOpenHashMap<KClass<*>, ArrayList<KMutableProperty1<*, *>>>()

		private fun collectDecl(input: KClass<*>, output: MutableMap<String, KMutableProperty1<*, *>>) {
			for (decl in collectDeclImpl(input)) {
				output.putIfAbsent(decl.name, decl)
			}
		}

		private fun collectDeclImpl(input: KClass<*>): ArrayList<KMutableProperty1<*, *>> {
			val getValues = synchronized(declCache) { declCache[input] }

			if (getValues != null) {
				return getValues
			}

			val values = ObjectLinkedOpenHashSet<KMutableProperty1<*, *>>()

			for (decl in input.declaredMembers) {
				if (decl is KMutableProperty1<*, *>) {
					decl.isAccessible = true
					values.add(decl)
				}
			}

			for (parent in input.supertypes) {
				val classifier = parent.classifier

				if (classifier is KClass<*> && !classifier.java.isInterface) {
					values.addAll(collectDeclImpl(classifier))
				}
			}

			val list = ArrayList(values)
			synchronized(declCache) { declCache.put(input, list) }
			return list
		}
	}
}
