package ru.dbotthepony.kstarbound

import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.Int2ObjectFunction
import it.unimi.dsi.fastutil.ints.Int2ObjectMap
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.KOptional
import java.util.Collections
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Supplier
import kotlin.collections.set
import kotlin.concurrent.withLock

class Registry<T : Any>(val name: String) {
	private val keysInternal = HashMap<String, Impl>()
	private val idsInternal = Int2ObjectOpenHashMap<Impl>()
	private val keyRefs = HashMap<String, RefImpl>()
	private val idRefs = Int2ObjectOpenHashMap<RefImpl>()
	private val backlog = ConcurrentLinkedQueue<Runnable>()

	// it is much cheaper to queue registry additions rather than locking during high congestion
	fun add(task: Runnable) {
		backlog.add(task)
	}

	fun finishLoad() {
		lock.withLock {
			var next = backlog.poll()

			while (next != null) {
				next.run()
				next = backlog.poll()
			}
		}
	}

	private val lock = ReentrantLock()

	val keys: Map<String, Entry<T>> = Collections.unmodifiableMap(keysInternal)
	val ids: Int2ObjectMap<out Entry<T>> = Int2ObjectMaps.unmodifiable(idsInternal)

	sealed class Ref<T : Any> : Supplier<Entry<T>?> {
		abstract val key: Either<String, Int>
		abstract val entry: Entry<T>?
		abstract val registry: Registry<T>

		val isPresent: Boolean
			get() = value != null

		val isEmpty: Boolean
			get() = value == null

		val value: T?
			get() = entry?.value

		final override fun get(): Entry<T>? {
			return entry
		}
	}

	sealed class Entry<T : Any> : Supplier<T> {
		abstract val key: String
		abstract val id: Int?
		abstract val value: T
		abstract val json: JsonElement
		abstract val file: IStarboundFile?
		abstract val registry: Registry<T>
		@Deprecated("Careful! This might be confused with 'isMeta' of some entries (such as tiles, in such case use entry.isMeta)")
		abstract val isBuiltin: Boolean
		abstract val ref: Ref<T>

		final override fun get(): T {
			return value
		}

		val jsonObject: JsonObject
			get() = json as JsonObject
	}

	private inner class Impl(override val key: String, override var value: T, override var id: Int? = null) : Entry<T>() {
		override var json: JsonElement = JsonNull.INSTANCE
		override var file: IStarboundFile? = null
		override var isBuiltin: Boolean = false

		override val ref: Ref<T> by lazy { ref(key) }

		override fun equals(other: Any?): Boolean {
			return this === other
		}

		override fun hashCode(): Int {
			return key.hashCode()
		}

		override fun toString(): String {
			return "Entry of $name at $key/${id ?: "-"}"
		}

		override val registry: Registry<T>
			get() = this@Registry
	}

	private inner class RefImpl(override val key: Either<String, Int>) : Ref<T>() {
		override var entry: Entry<T>? = null
		var references = 0

		override fun equals(other: Any?): Boolean {
			return this === other || other is Registry<*>.RefImpl && other.key == key && other.registry == registry
		}

		override fun hashCode(): Int {
			return key.hashCode()
		}

		override fun toString(): String {
			return "Ref of $name at $key/${if (entry != null) "bound" else "missing"}"
		}

		override val registry: Registry<T>
			get() = this@Registry
	}

	operator fun get(index: String): Entry<T>? = lock.withLock { keysInternal[index] }
	operator fun get(index: Int): Entry<T>? = lock.withLock { idsInternal[index] }

	fun getOrThrow(index: String): Entry<T> {
		return get(index) ?: throw NoSuchElementException("No such $name: $index")
	}

	fun getOrThrow(index: Int): Entry<T> {
		return get(index) ?: throw NoSuchElementException("No such $name: $index")
	}

	fun ref(index: String): Ref<T> = lock.withLock {
		keyRefs.computeIfAbsent(index, Object2ObjectFunction {
			val ref = RefImpl(Either.left(it as String))
			ref.entry = keysInternal[it]
			ref
		}).also {
			it.references++
		}
	}

	fun ref(index: Int): Ref<T> = lock.withLock {
		idRefs.computeIfAbsent(index, Int2ObjectFunction {
			val ref = RefImpl(Either.right(it))
			ref.entry = idsInternal[it]
			ref
		}).also {
			it.references++
		}
	}

	operator fun contains(index: String) = lock.withLock { index in keysInternal }
	operator fun contains(index: Int) = lock.withLock { index in idsInternal }

	fun validate(): Boolean {
		var valid = true

		keyRefs.values.forEach {
			if (!it.isPresent && it.key.left() != "") {
				LOGGER.warn("Registry '$name' reference at '${it.key.left()}' is not bound to value, expect problems (referenced ${it.references} times)")
				valid = false
			}
		}

		idRefs.values.forEach {
			if (!it.isPresent) {
				LOGGER.warn("Registry '$name' reference with ID '${it.key.right()}' is not bound to value, expect problems (referenced ${it.references} times)")
				valid = false
			}
		}

		return valid
	}

	fun add(
		key: String,
		value: T,
		json: JsonElement = JsonNull.INSTANCE,
		file: IStarboundFile? = null,
		id: KOptional<Int?> = KOptional(),
		isBuiltin: Boolean = false
	): Entry<T> {
		require(key != "") { "Adding $name with empty name (empty name is reserved)" }

		lock.withLock {
			if (key in keysInternal) {
				LOGGER.warn("Overwriting $name at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: "<code>"})")
			}

			id.ifPresent { id ->
				if (id != null && id in idsInternal) {
					LOGGER.warn("Overwriting $name with ID '$id' (new def originate from $file; old def originate from ${idsInternal[id]?.file ?: "<code>"})")
				}
			}

			val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) })
			check(!entry.isBuiltin || isBuiltin) { "Trying to redefine builtin entry at $key" }

			id.ifPresent {
				entry.id?.let {
					idsInternal.remove(it)
					idRefs[it]?.entry = null
				}

				entry.id = it
			}

			entry.value = value
			entry.json = json
			entry.file = file
			entry.isBuiltin = isBuiltin

			keyRefs[key]?.entry = entry

			id.ifPresent { id ->
				if (id != null) {
					idRefs[id]?.entry = entry
					idsInternal[id] = entry
				}
			}

			return entry
		}
	}

	val emptyRef = ref("")

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