package ru.dbotthepony.kstarbound.world.entities

import com.google.gson.JsonArray
import com.google.gson.JsonElement
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.koptional
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.client.world.ClientWorld
import ru.dbotthepony.kstarbound.defs.EntityDamageTeam
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.InteractAction
import ru.dbotthepony.kstarbound.defs.InteractRequest
import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec
import ru.dbotthepony.kstarbound.network.syncher.MasterElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
import ru.dbotthepony.kstarbound.network.syncher.networkedData
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.world.LightCalculator
import ru.dbotthepony.kstarbound.world.EntityIndex
import ru.dbotthepony.kstarbound.world.World
import java.io.DataOutputStream
import java.util.function.Consumer

abstract class AbstractEntity : Comparable<AbstractEntity> {
	abstract val position: Vector2d

	var entityID: Int = 0
		set(value) {
			if (field != 0)
				throw IllegalStateException("Already has Entity ID set (to $field)")

			field = value

			if (value < 0) {
				connectionID = (-value - 1) / 65536 + 1
			}
		}

	var connectionID: Int = 0
		private set

	final override fun compareTo(other: AbstractEntity): Int {
		return entityID.compareTo(other.entityID)
	}

	private val exceptionLogger = Consumer<Throwable> {
		LOGGER.error("Error while executing queued task on $this", it)
	}

	var mailbox = MailboxExecutorService().also { it.exceptionHandler = exceptionLogger }
		private set

	private var innerWorld: World<*, *>? = null

	val world: World<*, *>
		get() = innerWorld ?: throw IllegalStateException("Not in world")

	inline val clientWorld get() = world as ClientWorld
	inline val serverWorld get() = world as ServerWorld

	val isInWorld: Boolean
		get() = innerWorld != null

	abstract val type: EntityType

	/**
	 * If set, then the entity will be discoverable by its unique id and will be
	 * indexed in the stored world.  Unique ids must be different across all
	 * entities in a single world.
	 */
	val uniqueID = networkedData(KOptional(), InternedStringCodec.koptional())

	var description = ""

	/**
	 * Whenever this entity should be removed when chunk containing it is being unloaded
	 *
	 * Returning false will also stop entity from being saved to disk, and render entity orphaned
	 * when chunk containing it will get unloaded
	 */
	open val isPersistent: Boolean
		get() = true

	val team = networkedData(EntityDamageTeam(), EntityDamageTeam.CODEC, EntityDamageTeam.LEGACY_CODEC)

	enum class RemovalReason(val removal: Boolean, val dying: Boolean, val remote: Boolean) {
		UNLOADED(false, false, false),      // Being saved to disk
		REMOVED(true, false, false),        // Got removed from world
		DYING(true, true, false),           // Same as REMOVED, but indicates that entity has died

		REMOTE_REMOVAL(true, false, true),  // We were client for that entity,
											// and other side removed that entity

		REMOTE_DYING(true, true, true);     // REMOTE_REMOVAL + DYING
	}

	protected open fun onJoinWorld(world: World<*, *>) { }
	protected open fun onRemove(world: World<*, *>, reason: RemovalReason) { }

	val networkGroup = MasterElement(NetworkedGroup())
	abstract fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean)

	protected var spatialEntry: EntityIndex.Entry? = null
		private set

	/**
	 * Used for spatial index
	 */
	abstract val metaBoundingBox: AABB

	open val collisionArea: AABB
		get() = NEVER

	open fun onNetworkUpdate() {

	}

	open fun receiveMessage(name: String, arguments: JsonArray): JsonElement? {
		return null
	}

	fun joinWorld(world: World<*, *>) {
		if (innerWorld != null)
			throw IllegalStateException("Already spawned (in world $innerWorld)")

		if (entityID == 0)
			entityID = world.nextEntityID.incrementAndGet()

		world.eventLoop.ensureSameThread()

		check(!world.entities.containsKey(entityID)) { "Duplicate entity ID: $entityID" }

		if (mailbox.isShutdown)
			mailbox = MailboxExecutorService().also { it.exceptionHandler = exceptionLogger }

		innerWorld = world
		world.entities[entityID] = this
		world.entityList.add(this)
		spatialEntry = world.entityIndex.Entry(this)
		onJoinWorld(world)
	}

	fun remove(reason: RemovalReason) {
		val world = innerWorld ?: throw IllegalStateException("Not in world")
		world.eventLoop.ensureSameThread()

		mailbox.shutdownNow()
		check(world.entities.remove(entityID) == this) { "Tried to remove $this from $world, but removed something else!" }
		world.entityList.remove(this)

		try {
			onRemove(world, reason)
		} catch (err: Throwable) {
			LOGGER.error("Exception while removing $this from $world", err)
		}

		spatialEntry?.remove()
		spatialEntry = null
		innerWorld = null

		if (world is ServerWorld) {
			world.clients.forEach {
				it.forget(this, reason)
			}
		}
	}

	open fun interact(request: InteractRequest): InteractAction {
		return InteractAction.NONE
	}

	var isRemote: Boolean = false

	open fun tick(delta: Double) {
		mailbox.executeQueuedTasks()

		if (networkGroup.upstream.isInterpolating) {
			networkGroup.upstream.tickInterpolation(delta)
		}
	}

	open fun render(client: StarboundClient, layers: LayeredRenderer) {

	}

	open fun addLights(lightCalculator: LightCalculator, xOffset: Int, yOffset: Int) {

	}

	companion object {
		private val LOGGER = LogManager.getLogger()
		private val NEVER = AABB(Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY), Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY))
	}
}
