package ru.dbotthepony.kstarbound.defs.world

import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kstarbound.math.AABBi
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.json.NativeLegacy
import ru.dbotthepony.kstarbound.json.builder.JsonAlias
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonIgnore
import ru.dbotthepony.kstarbound.world.Direction

@JsonFactory
data class WorldStructure(
	val region: AABBi = AABBi(Vector2i.ZERO, Vector2i.ZERO),
	val anchorPosition: Vector2i = Vector2i.ZERO,
	val config: JsonObject = JsonObject(),
	val backgroundOverlays: ImmutableList<Overlay> = ImmutableList.of(),
	val foregroundOverlays: ImmutableList<Overlay> = ImmutableList.of(),

	val blockKey: AssetReference<ImmutableList<BlockKey>> = AssetReference.empty(),
	val blockImage: SpriteReference? = null,

	val backgroundBlocks: ImmutableList<Block> = ImmutableList.of(),
	val foregroundBlocks: ImmutableList<Block> = ImmutableList.of(),
	val objects: ImmutableList<Obj> = ImmutableList.of(),
	val flaggedBlocks: ImmutableMap<String, ImmutableSet<Vector2i>> = ImmutableMap.of(),
) {
	@JsonFactory
	data class Overlay(
		@JsonAlias("position")
		val min: Vector2d,
		val image: AssetPath,
		val fullbright: Boolean = false)

	@JsonFactory
	data class Block(val position: Vector2i, val materialId: NativeLegacy.Tile, val residual: Boolean)

	@JsonFactory
	data class Obj(
		val position: Vector2i = Vector2i.ZERO,
		val name: Registry.Ref<ObjectDefinition>,
		val direction: Direction = Direction.LEFT,
		val parameters: JsonObject = JsonObject(),
		val residual: Boolean = false,
	)

	@JsonFactory
	data class BlockKey(
		val value: RGBAColor,
		val foregroundBlock: Boolean = false,
		val backgroundBlock: Boolean = false,
		val foregroundMat: Registry.Ref<TileDefinition> = BuiltinMetaMaterials.STRUCTURE.ref,
		val backgroundMat: Registry.Ref<TileDefinition> = BuiltinMetaMaterials.STRUCTURE.ref,
		val foregroundResidual: Boolean = false,
		val backgroundResidual: Boolean = false,
		val `object`: Registry.Ref<ObjectDefinition> = Registries.worldObjects.emptyRef,
		val objectDirection: Direction = Direction.LEFT,
		val objectParameters: JsonObject = JsonObject(),
		val objectResidual: Boolean = false,
		val flags: ImmutableSet<String> = ImmutableSet.of(),
		val anchor: Boolean = false,
	)

	fun resolve(position: Vector2i): WorldStructure {
		val blockKey = blockKey.value.get() ?: return copy(
			anchorPosition = position,
			backgroundBlocks = ImmutableList.of(),
			foregroundBlocks = ImmutableList.of(),
			objects = ImmutableList.of(),
			flaggedBlocks = ImmutableMap.of(),
		)

		val sprite = blockImage?.sprite ?: return copy(
			anchorPosition = position,
			backgroundBlocks = ImmutableList.of(),
			foregroundBlocks = ImmutableList.of(),
			objects = ImmutableList.of(),
			flaggedBlocks = ImmutableMap.of(),
		)

		var anchorPosition: Vector2i? = null

		val backgroundBlocks = ArrayList<Block>()
		val foregroundBlocks = ArrayList<Block>()
		val objects = ArrayList<Obj>()
		val flaggedBlocks = HashMap<String, HashSet<Vector2i>>()

		val mapped = Int2ObjectOpenHashMap<BlockKey>()

		for (v in blockKey) {
			mapped[v.value.toRGBA()] = v
		}

		for (x in 0 until sprite.width) {
			for (y in sprite.height - 1 downTo  0) { // reverses order of objects
				// flip image so image visible top maps to world's top
				val color = sprite[x, sprite.height - y - 1]
				val block = mapped[color]

				if (block == null) {
					LOGGER.error("No such block with color index $color at $x, $y in $sprite")
					continue
				}

				val pos = Vector2i(x, y)

				if (block.foregroundBlock) {
					foregroundBlocks.add(Block(pos, NativeLegacy.Tile(block.foregroundMat), block.foregroundResidual))
				}

				if (block.backgroundBlock) {
					backgroundBlocks.add(Block(pos, NativeLegacy.Tile(block.backgroundMat), block.backgroundResidual))
				}

				if (block.`object`.isPresent) {
					objects.add(Obj(
						pos,
						block.`object`,
						block.objectDirection,
						block.objectParameters,
						block.objectResidual
					))
				}

				if (block.anchor) {
					check(anchorPosition == null) { "Duplicate anchor position (previous was at ${anchorPosition!! - position}, new at $x, $y)" }
					anchorPosition = Vector2i(x, y)
				}

				for (flag in block.flags)
					flaggedBlocks.computeIfAbsent(flag) { HashSet() }.add(pos)
			}
		}

		val diff = position - (anchorPosition ?: position)

		return copy(
			anchorPosition = (anchorPosition ?: position) + diff,

			backgroundBlocks = backgroundBlocks.stream().map { it.copy(position = it.position + diff) }.collect(ImmutableList.toImmutableList()),
			foregroundBlocks = foregroundBlocks.stream().map { it.copy(position = it.position + diff) }.collect(ImmutableList.toImmutableList()),

			backgroundOverlays = backgroundOverlays.stream().map { it.copy(min = it.min + diff) }.collect(ImmutableList.toImmutableList()),
			foregroundOverlays = foregroundOverlays.stream().map { it.copy(min = it.min + diff) }.collect(ImmutableList.toImmutableList()),

			objects = objects.stream().map { it.copy(position = it.position + diff) }.collect(ImmutableList.toImmutableList()),

			flaggedBlocks = flaggedBlocks.entries
				.stream()
				.map { it.key to it.value.stream().map { it + diff }.collect(ImmutableSet.toImmutableSet()) }
				.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })),
		)
	}

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