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

import com.google.gson.JsonObject
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
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.item.ItemStack
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.util.random.nextNormalDouble
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import kotlin.math.PI

/**
 * (Hopefully) Static world entities (Plants, Objects, etc), which reside on cell grid
 */
abstract class TileEntity : AbstractEntity() {
	override fun deserialize(data: JsonObject) {
		super.deserialize(data)
		tilePosition = data.get("tilePosition", vectors)
	}

	override fun serialize(data: JsonObject) {
		super.serialize(data)
		data["tilePosition"] = vectors.toJsonTree(tilePosition)
	}

	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
	}

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

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

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

	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() }
					}
				}
			}

			// remove roots
			if (!updateRoots(listOf())) {
				// we were unable to remove all roots...
				// create a task to try not to leave world in inconsistent state!
				val currentRoots = currentRoots
				val tilePosition = tilePosition

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

					try {
						currentRoots.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 currentRoots) {
							val cell = world.getCell(space).mutable()

							if (cell.rootSource == tilePosition) {
								cell.rootSource = null
							}

							if (!world.setCell(space, cell)) {
								LOGGER.warn("Unable to clear root source at ${space}, 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
	}

	protected fun updateRoots(desired: Collection<Vector2i>): Boolean {
		val toRemove = ArrayList<Vector2i>()
		val toPlace = ArrayList<Vector2i>()

		for (current in currentRoots) {
			if (current !in desired) {
				toRemove.add(current)
			}
		}

		for (pos in desired) {
			if (pos !in currentRoots) {
				toPlace.add(pos)
			}
		}

		var clear = true

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

			if (cell.rootSource == tilePosition) {
				cell.rootSource = null

				if (world.setCell(pos, cell)) {
					currentRoots.remove(pos)
				} else {
					clear = false
				}
			} else {
				currentRoots.remove(pos)
			}
		}

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

			if (cell.rootSource == null) {
				cell.rootSource = tilePosition

				if (world.setCell(pos, cell)) {
					currentRoots.add(pos)
				} else {
					clear = false
				}
			} else {
				// bugger, someone rooted onto our tile!
				// will try again...
				// TODO: however, shouldn't we just ignore this and move on?
				clear = false
			}
		}

		return clear
	}

	fun updateMaterialSpacesNow() {
		needToUpdateSpaces = false

		if (world.isServer) {
			needToUpdateSpaces = !updateMaterialSpaces(materialSpaces)
		}
	}

	fun updateRootsNow() {
		needToUpdateRoots = false

		if (world.isServer) {
			needToUpdateRoots = !updateRoots(roots)
		}
	}

	fun spewItem(item: ItemStack, maxVelocity: Double = 8.0) {
		val entity = ItemDropEntity(item)

		if (occupySpaces.isEmpty()) {
			entity.position = position
		} else {
			val rand = occupySpaces.random(world.random)

			entity.position = Vector2d(
				rand.x + world.random.nextDouble(),
				rand.y + world.random.nextDouble(),
			)
		}

		// push items out of hypothetical center
		val center = if (occupySpaces.isNotEmpty()) AABB.ofPoints(occupySpaces).centre else position
		val angle = world.random.nextNormalDouble(0.5, (entity.position - center).toAngle())
		entity.movement.velocity += Vector2d.angle(angle, world.random.nextDouble(0.0, maxVelocity))

		entity.joinWorld(world)
	}

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

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

		if (world.isServer && needToUpdateRoots) {
			updateRootsNow()
		}
	}

	companion object {
		private val LOGGER = LogManager.getLogger()
		private val vectors by lazy { Starbound.gson.getAdapter(Vector2i::class.java) }
	}
}
