package ru.dbotthepony.kstarbound.world.entities.tile

import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.orEmptyTile
import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt
import ru.dbotthepony.kstarbound.server.world.ServerChunk
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity

/**
 * (Hopefully) Static world entities (Plants, Objects, etc), which reside on cell grid
 */
abstract class TileEntity : AbstractEntity() {
	protected val xTilePositionNet = networkedSignedInt()
	protected val yTilePositionNet = networkedSignedInt()

	init {
		xTilePositionNet.addListener(::onPositionUpdated)
		yTilePositionNet.addListener(::onPositionUpdated)
	}

	var xTilePosition: Int
		get() = xTilePositionNet.get()
		set(value) {
			if (isInWorld) {
				xTilePositionNet.accept(world.geometry.x.cell(value))
			} else {
				xTilePositionNet.accept(value)
			}
		}

	var yTilePosition: Int
		get() = yTilePositionNet.get()
		set(value) {
			if (isInWorld) {
				yTilePositionNet.accept(world.geometry.x.cell(value))
			} else {
				yTilePositionNet.accept(value)
			}
		}

	var tilePosition: Vector2i
		get() = Vector2i(xTilePosition, yTilePosition)
		set(value) {
			xTilePosition = value.x
			yTilePosition = value.y
		}

	override val position: Vector2d
		get() = Vector2d(xTilePosition.toDouble(), yTilePosition.toDouble())

	/**
	 * Tile positions this entity occupies in world (in world coordinates, not relative)
	 */
	abstract val occupySpaces: Collection<Vector2i>

	/**
	 * Tile positions this entity physically occupies in world (in world coordinates, not relative)
	 */
	abstract val materialSpaces: Collection<Pair<Vector2i, Registry.Ref<TileDefinition>>>

	/**
	 * Tile positions this entity is rooted in world (in world coordinates, not relative)
	 */
	abstract val roots: Collection<Vector2i>

	abstract fun damage(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean

	private var needToUpdateSpaces = false
	private var needToUpdateRoots = false
	private val currentMaterialSpaces = HashSet<Pair<Vector2i, Registry.Ref<TileDefinition>>>()
	private val currentRoots = HashSet<Vector2i>()

	open fun markSpacesDirty() {
		needToUpdateSpaces = true
	}

	open fun markRootsDirty() {
		needToUpdateRoots = true
	}

	private fun updateSpatialPosition() {
		val spatialEntry = spatialEntry ?: return
		spatialEntry.fixture.move(metaBoundingBox)
	}

	protected open fun onPositionUpdated() {
		updateSpatialPosition()
		markSpacesDirty()
	}

	override fun onJoinWorld(world: World<*, *>) {
		super.onJoinWorld(world)
		updateSpatialPosition()
		markSpacesDirty()
	}

	override fun onRemove(world: World<*, *>, reason: RemovalReason) {
		super.onRemove(world, reason)

		if (world.isServer && (reason.removal || reason.remote)) {
			// remove occupied spaces
			if (!updateMaterialSpaces(listOf())) {
				// we were unable to remove all spaces...
				// create a task to try not to leave world in inconsistent state!
				val currentMaterialSpaces = currentMaterialSpaces

				world.eventLoop.scope.launch {
					world as ServerWorld
					val tickets = ArrayList<ServerChunk.ITicket>()

					try {
						currentMaterialSpaces.forEach { (p) ->
							tickets.add(world.permanentChunkTicket(world.geometry.chunkFromCell(p.x, p.y), ChunkState.EMPTY).await() ?: return@forEach)
						}

						tickets.forEach { it.chunk.await() }

						for (space in currentMaterialSpaces) {
							val cell = world.getCell(space.first).mutable()

							if (cell.foreground.material == space.second.orEmptyTile)
								cell.foreground.empty()

							if (!world.setCell(space.first, cell)) {
								LOGGER.warn("Unable to clear tile entity space at ${space.first}, world left in inconsistent state.")
							}
						}
					} finally {
						tickets.forEach { it.cancel() }
					}
				}
			}
		}
	}

	protected fun updateMaterialSpaces(desired: Collection<Pair<Vector2i, Registry.Ref<TileDefinition>>>): Boolean {
		check(world.isServer) { "Invalid realm" }

		val toRemove = ArrayList<Pair<Vector2i, Registry.Ref<TileDefinition>>>()
		val toPlace = ArrayList<Pair<Vector2i, Registry.Ref<TileDefinition>>>()

		for (space in currentMaterialSpaces) {
			if (desired.any { it == space })
				continue

			// we need to remove this space
			toRemove.add(space)
		}

		for (space in desired) {
			if (currentMaterialSpaces.any { it == space })
				continue

			// we need to put this space
			toPlace.add(space)
		}

		if (toRemove.isEmpty() && toPlace.isEmpty())
			return true // we're clear!

		var clear = true

		for (space in toRemove) {
			val cell = world.getCell(space.first).mutable()

			if (cell.foreground.material == space.second.orEmptyTile)
				cell.foreground.empty()

			if (world.setCell(space.first, cell)) {
				currentMaterialSpaces.remove(space)
			} else {
				clear = false
			}
		}

		for (space in toPlace) {
			val cell = world.getCell(space.first).mutable()

			// already satisfied that placement
			if (cell.foreground.material == space.second.orEmptyTile) {
				currentMaterialSpaces.add(space)
				continue
			}

			cell.foreground.empty()
			cell.foreground.material = space.second.orEmptyTile

			if (world.setCell(space.first, cell)) {
				currentMaterialSpaces.add(space)
			} else {
				clear = false
			}
		}

		return clear
	}

	fun updateMaterialSpacesNow() {
		needToUpdateSpaces = false

		// only server can update entity tiles
		// even if this tile entity is owned by client
		if (world.isServer) {
			needToUpdateSpaces = !updateMaterialSpaces(materialSpaces)
		}
	}

	override fun tick(delta: Double) {
		super.tick(delta)

		if (world.isServer && needToUpdateSpaces) {
			updateMaterialSpacesNow()
		}
	}

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