package ru.dbotthepony.kstarbound.defs.dungeon

import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.set
import java.util.concurrent.ConcurrentHashMap

class TiledTileSet private constructor(
	// plz dont modify :)
	// used for speed
	val front: Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>,
	val back: Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>,
) {
	@JsonFactory
	data class JsonData(
		val properties: JsonObject = JsonObject(),
		val tilecount: Int,
		val tileproperties: JsonObject = JsonObject(), // empty tileset?
	)

	val size: Int
		get() = front.size

	val isEmpty: Boolean
		get() = size == 0

	val isNotEmpty: Boolean
		get() = size != 0

	companion object {
		private val cache = ConcurrentHashMap<String, Either<TiledTileSet, Throwable>>()

		fun makeTile(data: JsonObject): DungeonTile {
			val brushes = DungeonBrushType.entries.mapNotNull { it.readTiled(data) }
			val rules = DungeonRule.Type.entries.mapNotNull { it.readTiled(data) }

			var connector: DungeonTile.JigsawConnector? = null

			if ("connector" in data) {
				val name = data["connector"].asString
				val connectForwardOnly = "connectForwardOnly" in data
				val connectDirection = DungeonDirection.entries.first { it.jsonName == data.get("connectDirection", "any") }
				connector = DungeonTile.JigsawConnector(name, connectForwardOnly, connectDirection)
			}

			return DungeonTile.INTERNER.intern(DungeonTile(ImmutableList.copyOf(brushes), ImmutableList.copyOf(rules), 0, connector))
		}

		private fun load0(location: String): Either<TiledTileSet, Throwable> {
			val locate = Starbound.loadJsonAsset(location)
				?: return Either.right(NoSuchElementException("Tileset at $location does not exist"))

			try {
				val data = Starbound.gson.fromJson(locate, JsonData::class.java)
				val front = Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>(data.tilecount + 40)
				val back = Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>(data.tilecount + 40)

				for ((key, value) in data.tileproperties.entrySet()) {
					if (value !is JsonObject)
						throw JsonSyntaxException("Tile at $key is invalid: $value")

					val index = key.toIntOrNull() ?: throw JsonSyntaxException("Invalid tile ID in $location: $key")
					val merge = mergeJson(data.properties.deepCopy(), value)
					val mergeBack = merge.deepCopy()

					if ("layer" !in merge) {
						merge["layer"] = "front"
					}

					if ("layer" !in mergeBack) {
						mergeBack["layer"] = "back"
					}

					if ("clear" !in merge) {
						// The magic pink tile/brush has the clear property set to "false". All
						// other tiles default to clear="true".
						mergeBack["clear"] = "true"
					}

					front[index] = makeTile(merge) to merge
					back[index] = makeTile(mergeBack) to mergeBack
				}

				return Either.left(TiledTileSet(front, back))
			} catch (err: Throwable) {
				return Either.right(err)
			}
		}

		fun load(location: String): TiledTileSet {
			return cache.computeIfAbsent(location, Companion::load0).map({ it }, { throw it })
		}
	}
}
