package ru.dbotthepony.kstarbound.defs.dungeon

import com.github.benmanes.caffeine.cache.Interner
import com.google.common.collect.ImmutableList
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.vector.Vector4i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.getAdapter
import ru.dbotthepony.kstarbound.server.world.ServerWorld

@JsonAdapter(DungeonTile.Adapter::class)
data class DungeonTile(
	val brushes: ImmutableList<DungeonBrush>,
	val rules: ImmutableList<DungeonRule>,
	val index: Int,
	val connector: JigsawConnector?,
) {
	data class JigsawConnector(
		val index: String,
		val forward: Boolean,
		val direction: DungeonDirection
	)

	@JsonFactory
	data class BasicData(
		val brush: ImmutableList<DungeonBrush> = ImmutableList.of(),
		val rules: ImmutableList<DungeonRule> = ImmutableList.of(),
		val direction: DungeonDirection = DungeonDirection.UNKNOWN,

		// original engine supports specifying pixels as strings
		// but I don't see it being used anywhere in original game assets
		// /shrug lets support anyway
		val value: Either<Vector4i, String>,
		val connector: Boolean = false,
		val connectForwardOnly: Boolean = false,
	)

	// whenever this tile CAN BE overdrawn by other tile
	val allowOverdrawing: Boolean = rules.any { it.allowOverdrawing }

	// "modifyPlaces" in original code
	val hasBrushes: Boolean get() = brushes.isNotEmpty()
	val hasRules: Boolean get() = rules.isNotEmpty()
	val usesPlaces: Boolean get() = hasBrushes && !allowOverdrawing
	val collidesWithPlaces: Boolean get() = usesPlaces

	val requiresOpen: Boolean get() = rules.any { it.requiresOpen }
	val requiresLiquid: Boolean get() = rules.any { it.requiresLiquid }
	val requiresSolid: Boolean get() = rules.any { it.requiresSolid }

	// empty tiles can be cut off from canPlace and place loops
	// (since they don't do anything other than padding region coordinates)
	val isEmpty: Boolean get() = !hasBrushes && !hasRules

	// sucks that we have to join world thread when doing this check
	// this hinders parallelized computations by noticeable margin,
	// and also makes dungeon placement in world completely serial
	// (one dungeon can be in process of generating at time)
	// TODO: find a way around this, to make dungeons less restricted by this
	// but thats also not a priority, since this check happens quite quickly
	// to have any noticeable impact on world's performance
	fun canPlace(x: Int, y: Int, world: DungeonWorld): Boolean {
		val cell = world.parent.getCell(x, y)

		if (cell.dungeonId != NO_DUNGEON_ID)
			return false

		if (!world.geometry.x.isValidCellIndex(x) || !world.geometry.y.isValidCellIndex(y))
			return false

		return rules.none { !it.checkTileCanPlace(x, y, world) }
	}

	fun canPlace(x: Int, y: Int, world: ServerWorld): Boolean {
		val cell = world.getCell(x, y)

		if (cell.dungeonId != NO_DUNGEON_ID)
			return false

		if (!world.geometry.x.isValidCellIndex(x) || !world.geometry.y.isValidCellIndex(y))
			return false

		return rules.none { !it.checkTileCanPlace(x, y, world) }
	}

	fun place(x: Int, y: Int, phase: DungeonBrush.Phase, world: DungeonWorld) {
		brushes.forEach { it.execute(x, y, phase, world) }
	}

	// weird custom parsing rules but ok
	class Adapter(gson: Gson) : TypeAdapter<DungeonTile>() {
		private val data = gson.getAdapter(BasicData::class.java)
		private val values = gson.getAdapter<Either<Vector4i, String>>()

		private fun parseIndex(value: String): Int {
			val split = value.split(',')

			require(split.size == 4) { "Invalid color string: $value" }
			var index = 0

			for (v in split.map { it.toInt() }.asReversed())
				index = index.shl(8) or v.and(0xFF)

			return index
		}

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

		override fun read(`in`: JsonReader): DungeonTile {
			val read = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)
			val (brushes, rules, direction, rawIndex, connector, connectForwardOnly) = data.fromJsonTree(read)
			var connectorIndex = rawIndex

			if ("connector-value" in read) {
				connectorIndex = values.fromJsonTree(read["connector-value"])
			}

			val index = rawIndex.map({ it.w.and(0xFF).shl(24) or it.z.and(0xFF).shl(16) or it.y.and(0xFF).shl(8) or it.x.and(0xFF) }, { parseIndex(it) })

			if (connector) {
				return INTERNER.intern(DungeonTile(brushes, rules, index, JigsawConnector(
					index = connectorIndex.map({ it.w.and(0xFF).shl(24) or it.z.and(0xFF).shl(16) or it.y.and(0xFF).shl(8) or it.x.and(0xFF) }, { parseIndex(it) }).toString(),
					forward = connectForwardOnly,
					direction = direction
				)))
			} else {
				return INTERNER.intern(DungeonTile(brushes, rules, index, null))
			}
		}
	}

	companion object {
		val INTERNER: Interner<DungeonTile> = if (Starbound.DEDUP_CELL_STATES) Starbound.interner(5) else Interner { it }
		val EMPTY: DungeonTile = INTERNER.intern(DungeonTile(ImmutableList.of(), ImmutableList.of(), 0, null))
		val CLEAR: DungeonTile = INTERNER.intern(DungeonTile(ImmutableList.of(DungeonBrush.Clear), ImmutableList.of(), 0, null))
	}
}

