package ru.dbotthepony.kstarbound.defs

import com.google.gson.Gson
import com.google.gson.JsonElement
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.objects.Object2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.sbIntern
import java.lang.reflect.ParameterizedType
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import kotlin.collections.HashMap

class AssetReference<V> {
	constructor(value: V?) {
		lazy = lazy { value }
		path = null
		fullPath = null
		json = null
	}

	constructor(value: () -> V?) {
		lazy = lazy { value() }
		path = null
		fullPath = null
		json = null
	}

	constructor(path: String?, fullPath: String?, value: V?, json: JsonElement?) {
		this.path = path
		this.fullPath = fullPath
		this.lazy = lazy { value }
		this.json = json
	}

	val path: String?
	val fullPath: String?
	val json: JsonElement?

	val value: V?
		get() = lazy.value

	private val lazy: Lazy<V?>

	companion object : TypeAdapterFactory {
		val EMPTY = AssetReference(null, null, null, null)

		fun <V> empty() = EMPTY as AssetReference<V>

		override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
			if (type.rawType == AssetReference::class.java) {
				val param = type.type as? ParameterizedType ?: return null

				return object : TypeAdapter<AssetReference<T>>() {
					private val cache = Collections.synchronizedMap(HashMap<String, Pair<T, JsonElement>>())
					private val adapter = gson.getAdapter(TypeToken.get(param.actualTypeArguments[0])) as TypeAdapter<T>
					private val strings = gson.getAdapter(String::class.java)
					private val missing = Collections.synchronizedSet(ObjectOpenHashSet<String>())
					private val logger = LogManager.getLogger()

					override fun write(out: JsonWriter, value: AssetReference<T>?) {
						if (value == null)
							out.nullValue()
						else
							out.value(value.fullPath)
					}

					override fun read(`in`: JsonReader): AssetReference<T>? {
						if (`in`.consumeNull()) {
							return null
						} else if (`in`.peek() == JsonToken.STRING) {
							val path = strings.read(`in`)!!
							val fullPath = AssetPathStack.remap(path)
							val get = cache[fullPath]

							if (get != null)
								return AssetReference(path.sbIntern(), fullPath.sbIntern(), get.first, get.second)

							if (fullPath in missing)
								return null

							val json = Starbound.loadJsonAsset(fullPath)

							if (json == null) {
								if (missing.add(fullPath))
									logger.error("JSON asset does not exist: $fullPath")

								return AssetReference(path.sbIntern(), fullPath.sbIntern(), null, null)
							}

							val value = AssetPathStack(fullPath.substringBefore(':').substringBeforeLast('/')) {
								adapter.fromJsonTree(json)
							}

							if (value == null) {
								missing.add(fullPath)
								return AssetReference(path.sbIntern(), fullPath.sbIntern(), null, json)
							}

							cache[fullPath] = value to json
							return AssetReference(path.sbIntern(), fullPath.sbIntern(), value, json)
						} else {
							val json = Starbound.ELEMENTS_ADAPTER.read(`in`)
							val value = adapter.read(JsonTreeReader(json)) ?: return null
							return AssetReference(null, null, value, json)
						}
					}
				} as TypeAdapter<T>
			}

			return null
		}
	}
}
