package ru.dbotthepony.kstarbound.defs.dungeon

import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import kotlinx.coroutines.future.await
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.defs.image.Image
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.server.world.ServerChunk
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import java.io.FileNotFoundException
import java.util.stream.Stream
import kotlin.properties.Delegates

@JsonAdapter(DungeonPart.Adapter::class)
class DungeonPart(data: JsonData) {
	@JsonFactory
	data class JsonData(
		val name: String,
		val rules: ImmutableList<DungeonRule>,
		val chance: Double = 1.0,
		val markDungeonId: Boolean = true,
		val overrideAllowAlways: Boolean = false,
		val minimumThreatLevel: Double = Double.NEGATIVE_INFINITY,
		val maximumThreatLevel: Double = Double.POSITIVE_INFINITY,
		val clearAnchoredObjects: Boolean = true,
		val def: JsonArray,
	)

	val name = data.name
	val rules = data.rules
	val chance = data.chance.coerceIn(0.0001, 1.0)
	val markDungeonId = data.markDungeonId
	val overrideAllowAlways = data.overrideAllowAlways
	val minimumThreatLevel = data.minimumThreatLevel
	val maximumThreatLevel = data.maximumThreatLevel
	val clearAnchoredObjects = data.clearAnchoredObjects

	data class JigsawConnector(val part: DungeonPart, val index: String, val forwardOnly: Boolean, val direction: DungeonDirection, val offset: Vector2i) {
		fun connectsTo(other: JigsawConnector): Boolean {
			if (forwardOnly || index != other.index)
				return false

			if (direction == DungeonDirection.ANY || other.direction == DungeonDirection.ANY)
				return true

			return  direction == other.direction.opposite
		}
	}

	var connectors: ImmutableList<JigsawConnector> by Delegates.notNull()
		private set

	var anchor: Vector2i by Delegates.notNull()
		private set

	// should be used only when placing actual dungeons, and not microdungeons
	val placementLevelConstraint: Int by lazy {
		var air = reader.size.y
		var airX = 0
		var ground = 0
		var groundX = 0
		var liquid = 0
		var liquidX = 0

		reader.iterateTiles { x, y, tile ->
			for (rule in tile.rules) {
				if (rule.requiresSolid && y > ground) {
					ground = y
					groundX = x
				}

				if (rule.requiresOpen && y < air) {
					air = y
					airX = x
				}

				if ((rule === DungeonRule.MustContainLiquid || rule === DungeonRule.MustNotContainLiquid) && y > liquid) {
					liquid = y
					liquidX = x
				}
			}
		}

		ground = ground.coerceAtLeast(liquid)

		if (air < ground) {
			throw IllegalArgumentException("Invalid ground vs air constraint, ground at: $groundX,$ground; air at: $airX,$air; liquid at: $liquidX,$liquid for part $name")
		} else {
			air
		}
	}

	val ignoresPartMaximum: Boolean = rules.any { it.ignorePartMaximum }

	var dungeon by Delegates.notNull<DungeonDefinition>()
		private set

	val reader: PartReader

	init {
		if (data.def[0].asString == "image") {
			if (data.def[1].isJsonPrimitive) {
				reader = ImagePartReader(this, ImmutableList.of(Image.get(AssetPathStack.remap(data.def[1].asString)) ?: throw FileNotFoundException("Unable to locate image file ${data.def[1].asString} (${AssetPathStack.remap(data.def[1].asString)}) for dungeon part $name!")))
			} else {
				// assume array of images
				reader = ImagePartReader(
					this,
					data.def[1].asJsonArray
						.stream()
						.map { it.asString }
						.map { Image.get(AssetPathStack.remap(it)) ?: throw FileNotFoundException("Unable to locate image file $it (${AssetPathStack.remap(it)}) for dungeon part $name!") }
						.collect(ImmutableList.toImmutableList())
				)
			}
		} else if (data.def[0].asString == "tmx") {
			if (data.def[1].isJsonPrimitive) {
				reader = TiledPartReader(this, Stream.of(data.def[1].asString))
			} else {
				reader = TiledPartReader(this, data.def[1].asJsonArray.stream().map { it.asString })
			}
		} else {
			throw IllegalArgumentException("Unknown part type ${data.def[0].asString}!")
		}
	}

	fun bind(def: DungeonDefinition) {
		dungeon = def
		reader.bind(def)

		val connectors = ArrayList<JigsawConnector>()

		var cx = 0
		var cy = 0
		var cc = 0

		var lowestAir = reader.size.y
		var highestGround = -1
		var highestLiquid = -1

		reader.iterateTiles { x, y, tile ->
			if (tile.connector != null) {
				var direction = tile.connector.direction

				if (direction == DungeonDirection.UNKNOWN)
					direction = pickByNeighbours(x, y)

				if (direction == DungeonDirection.UNKNOWN)
					direction = pickByEdge(x, y)

				connectors.add(JigsawConnector(this, tile.connector.index, tile.connector.forward, direction, Vector2i(x, y)))
			}

			if (tile.collidesWithPlaces) {
				cx += x
				cy += y
				cc++
			}

			if (tile.requiresOpen && y < lowestAir) {
				lowestAir = y
			}

			if (tile.requiresSolid && y > highestGround) {
				highestGround = y
			}

			if (tile.requiresLiquid && y > highestLiquid) {
				highestLiquid = y
			}
		}

		this.connectors = ImmutableList.copyOf(connectors)

		highestGround = highestGround.coerceAtLeast(highestLiquid)

		if (highestGround == -1)
			highestGround = lowestAir - 1

		if (lowestAir == reader.size.y)
			lowestAir = highestGround + 1

		if (cc == 0) {
			cx = reader.size.x / 2
			cy = reader.size.y / 2
		} else {
			cx /= cc
			cy /= cc
		}

		if (highestGround != -1) {
			cy = highestGround + 1
		}

		anchor = Vector2i(cx, cy)
	}

	fun doesNotConnectTo(other: DungeonPart): Boolean {
		return rules.any { it.doesNotConnectToPart(other.name) } || other.rules.any { it.doesNotConnectToPart(name) }
	}

	fun checkPartCombinationsAllowed(counter: Map<String, Int>): Boolean {
		return rules.all { it.checkPartCombinationsAllowed(counter) }
	}

	fun allowsPlacement(spawnCount: Int): Boolean {
		return rules.all { it.allowSpawnCount(spawnCount) }
	}

	suspend fun canPlace(x: Int, y: Int, world: DungeonWorld): Boolean {
		if (overrideAllowAlways || reader.size.x == 0 || reader.size.y == 0)
			return true

		return world.waitForRegionAndJoin(Vector2i(x, y), reader.size) {
			reader.walkTiles<Boolean> { tx, ty, tile ->
				if (!tile.canPlace(x + tx, y + ty, world)) {
					return@walkTiles KOptional(false)
				}

				return@walkTiles KOptional()
			}
		}.orElse(true)
	}

	fun canPlace(x: Int, y: Int, world: ServerWorld): Boolean {
		if (overrideAllowAlways || reader.size.x == 0 || reader.size.y == 0)
			return true

		return reader.walkTiles<Boolean> { tx, ty, tile ->
			if (!tile.canPlace(x + tx, y + ty, world)) {
				return@walkTiles KOptional(false)
			}

			return@walkTiles KOptional()
		}.orElse(true)
	}

	suspend fun place(pos: Vector2i, world: DungeonWorld, dungeonID: Int) {
		val (x, y) = pos
		val markForRemoval = HashSet<Vector2i>()

		reader.iterateTiles { tx, ty, tile ->
			if (tile.hasBrushes) {
				markForRemoval.add(world.geometry.wrap(Vector2i(x + tx, y + ty)))
			}
		}

		// Mark entities for removal, and remove them when dungeon is actually placed in world
		world.waitForRegionAndJoin(Vector2i(x, y), reader.size) {
			val entities = world.parent.entityIndex.query(AABBi(Vector2i(x, y), Vector2i(x, y) + reader.size).toDoubleAABB())

			for (entity in entities) {
				if (entity !is TileEntity)
					continue

				var markForDeath = false

				if (entity.occupySpaces.any { it in markForRemoval }) {
					markForDeath = true
				} else if (clearAnchoredObjects) {
					if (entity.roots.any { it in markForRemoval }) {
						markForDeath = true
					} else if (entity is WorldObject && entity.anchorPositions.any { it in markForRemoval }) {
						markForDeath = true
					}
				}

				if (markForDeath) {
					// mark all spaces for removal so "is tile empty" placement rules properly account
					// for marked-for-death tiles occupied by existing entities
					entity.occupySpaces.forEach { world.clearTileEntityAt(it) }
					world.clearTileEntity(entity)
				}
			}
		}

		if (markDungeonId) {
			world.setDungeonID(dungeonID)
		} else {
			world.setDungeonID()
		}

		for (phase in DungeonBrush.Phase.entries) {
			reader.iterateTiles { tx, ty, tile ->
				if (tile.usesPlaces || !world.hasPlacement(x + tx, y + ty)) {
					tile.place(x + tx, y + ty, phase, world)
				}
			}
		}

		reader.iterateTiles { tx, ty, tile ->
			if (tile.usesPlaces) {
				world.touchPlacement(x + tx, y + ty)
			}

			if (tile.hasBrushes) {
				world.touch(x + tx, y + ty)
			}
		}

		world.finishPart()
	}

	fun tileUsesPlaces(x: Int, y: Int): Boolean {
		return reader.walkTilesAt(x, y) { _, _, tile -> if (tile.usesPlaces) KOptional(true) else KOptional() }.orElse(false)
	}

	fun collidesWithPlaces(x: Int, y: Int, world: DungeonWorld): Boolean {
		return reader.walkTiles { tx, ty, tile -> if (tile.collidesWithPlaces && world.hasPlacement(tx + x, ty + y)) KOptional(true) else KOptional() }.orElse(false)
	}

	fun pickByNeighbours(x: Int, y: Int): DungeonDirection {
		// if on a border use that, corners use the left/right direction
		if (x == 0)
			return DungeonDirection.LEFT
		else if (x == reader.size.x - 1)
			return DungeonDirection.RIGHT
		else if (y == 0)
			return DungeonDirection.DOWN
		else if (y == reader.size.y - 1)
			return DungeonDirection.UP

		// scans around the connector, the direction where it finds a solid is where
		// it assume the connection comes from
		if (tileUsesPlaces(x + 1, y) && !tileUsesPlaces(x - 1, y))
			return DungeonDirection.LEFT

		if (tileUsesPlaces(x - 1, y) && !tileUsesPlaces(x + 1, y))
			return DungeonDirection.RIGHT

		if (tileUsesPlaces(x, y + 1) && !tileUsesPlaces(x, y - 1))
			return DungeonDirection.DOWN

		if (tileUsesPlaces(x, y - 1) && !tileUsesPlaces(x, y + 1))
			return DungeonDirection.UP

		return DungeonDirection.UNKNOWN
	}

	fun pickByEdge(x: Int, y: Int): DungeonDirection {
		val dxb = reader.size.x - x
		val dyb = reader.size.y - y

		return when (x.coerceAtMost(dxb).coerceAtMost(y).coerceAtMost(dyb)) {
			x -> DungeonDirection.LEFT
			dxb -> DungeonDirection.RIGHT
			y -> DungeonDirection.DOWN
			dyb -> DungeonDirection.UP
			else -> throw RuntimeException()
		}
	}

	class Adapter(gson: Gson) : TypeAdapter<DungeonPart>() {
		private val data = gson.getAdapter(JsonData::class.java)

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

		override fun read(`in`: JsonReader): DungeonPart {
			return DungeonPart(data.read(`in`))
		}
	}

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