package ru.dbotthepony.kstarbound.world.entities

import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.nullable
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.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.DamageData
import ru.dbotthepony.kstarbound.defs.DamageNotification
import ru.dbotthepony.kstarbound.defs.DamageSource
import ru.dbotthepony.kstarbound.defs.DamageType
import ru.dbotthepony.kstarbound.defs.EntityDamageTeam
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.HitType
import ru.dbotthepony.kstarbound.defs.InteractAction
import ru.dbotthepony.kstarbound.defs.InteractRequest
import ru.dbotthepony.kstarbound.network.packets.DamageNotificationPacket
import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket
import ru.dbotthepony.kstarbound.network.packets.EntityDestroyPacket
import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket
import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket
import ru.dbotthepony.kstarbound.network.packets.EntityMessagePacket
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.world.LightCalculator
import ru.dbotthepony.kstarbound.world.EntityIndex
import ru.dbotthepony.kstarbound.world.TileRayFilter
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.castRay
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataOutputStream
import java.util.PriorityQueue
import java.util.UUID
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
import java.util.function.Predicate

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

	open fun deserialize(data: JsonObject) {

	}

	open fun serialize(data: JsonObject) {

	}

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

	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

	// in original engine call this "masterOnly" and condition is inverted
	/**
	 * Whenever this entity should be networked to remote(s) (client -> server or server -> clients)
	 *
	 * If this value is changed once entity has been spawned in world
	 * entity tracker behavior is undefined
	 */
	open val visibleToRemotes: Boolean
		get() = true

	var isEphemeral: Boolean = false
		protected set

	/**
	 * 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(null, InternedStringCodec.nullable())

	init {
		uniqueID.addListener { new, old ->
			if (isInWorld) {
				if (new != null && new in world.uniqueEntities && world.uniqueEntities[new] != this) {
					uniqueID.accept(old) // rollback the change to avoid bad things from happening
					throw IllegalArgumentException("Duplicate unique entity ID: $new")
				}

				if (old != null) {
					if (world.uniqueEntities[old] == this) {
						world.uniqueEntities.remove(old)
					}
				}

				if (new != null) world.uniqueEntities[new] = this
			}
		}
	}

	var description = ""

	/**
	 * Whenever this entity should be saved to disk when chunk containing it is being 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)

	fun writeNetwork(isLegacy: Boolean): ByteArrayList {
		val stream = FastByteArrayOutputStream()
		writeNetwork(DataOutputStream(stream), isLegacy)
		return ByteArrayList.wrap(stream.array, stream.length)
	}

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

	/**
	 * Used for spatial index, AABB in world coordinates
	 */
	abstract val metaBoundingBox: AABB

	open val collisionArea: AABB
		get() = AABB.NEVER

	/**
	 * in local coordinates because shoddy-ness (plural shoddies)
	 *
	 * in all seriousness though, these might get created by Lua code and they
	 * want damage source to be "pinned" to location where it was created
	 */
	open val damageSources: Collection<DamageSource>
		get() = listOf()

	/**
	 * Hitbox utilized to hitscan damage lines against
	 */
	open val damageHitbox: Collection<Poly>
		get() = listOf()

	open fun onNetworkUpdate() {

	}

	var removalReason: RemovalReason? = null
		private set

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

		removalReason = null

		if (entityID == 0) {
			if (world is ClientWorld) {
				entityID = world.client.activeConnection?.nextEntityID() ?: world.nextEntityID.incrementAndGet()
			} else {
				entityID = world.nextEntityID.incrementAndGet()
			}
		}

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

		uniqueID.get()?.let {
			check(it !in world.uniqueEntities) { "Duplicate unique entity ID: $it" }
			world.uniqueEntities[it] = this
		}

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

		if (visibleToRemotes) {
			if (world is ClientWorld && !isRemote) {
				val connection = world.client.activeConnection
				// TODO: incomplete
				connection?.send(EntityCreatePacket(type, writeNetwork(connection.isLegacy), networkGroup.write(0L, connection.isLegacy).first, entityID))
			} else if (world is ServerWorld) {
				world.clients.forEach { it.evaluateShouldTrack(this) }
			}
		}
	}

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

		removalReason = reason

		scheduledTasks.forEach { it.cancel(false) }
		scheduledTasks.clear()

		uniqueID.get()?.let {
			if (world.uniqueEntities[it] == this)
				world.uniqueEntities.remove(it)
		}

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

		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)
			}
		} else if (world is ClientWorld && !isRemote) {
			val connection = world.client.activeConnection

			if (connection != null) {
				connection.send(EntityDestroyPacket(entityID, writeNetwork(connection.isLegacy), reason.dying))
				connection.freeEntityID(entityID)
			}
		}
	}

	// This is necessary to cancel tasks when we are removed, so we don't reference ourselves
	// in event loop after we have been removed
	private val scheduledTasks = ObjectArrayList<ScheduledFuture<*>>()

	protected fun scheduleInTicks(ticks: Int, action: Runnable) {
		var task: ScheduledFuture<*>? = null

		task = world.eventLoop.schedule(Runnable {
			scheduledTasks.remove(task!!)
			action.run()
		}, ticks * Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS)

		scheduledTasks.add(task)
	}

	protected fun schedule(time: Long, unit: TimeUnit, action: Runnable) {
		var task: ScheduledFuture<*>? = null

		task = world.eventLoop.schedule(Runnable {
			scheduledTasks.remove(task!!)
			action.run()
		}, time, unit)

		scheduledTasks.add(task)
	}

	var isRemote: Boolean = false

	open val mouthPosition: Vector2d
		get() = position

	private fun isDamageAuthoritative(target: AbstractEntity): Boolean {
		// Damage manager is authoritative if either one of the entities is
		// masterOnly, OR the manager is server-side and both entities are
		// server-side master entities, OR the damage manager is server-side and both
		// entities are different clients, OR if the manager is client-side and the
		// source is client-side master and the target is server-side master, OR if
		// the manager is client-side and the target is client-side master.
		//
		// This means that PvE and EvP are both decided on the player doing the
		// hitting or getting hit, and PvP is decided on the server, except for
		// master-only entities whose interactions are always decided on the machine
		// they are residing on.

		val causeClient = connectionID
		val targetClient = target.connectionID
		val thisID = world.connectionID

		if (!visibleToRemotes || !target.visibleToRemotes) {
			return true
		} else if (causeClient == 0 && targetClient == 0) {
			return thisID == 0
		} else if (causeClient != 0 && targetClient != 0 && causeClient != targetClient) {
			return thisID == 0
		} else if (targetClient == 0) {
			return causeClient == thisID
		} else {
			return targetClient == thisID
		}
	}

	/**
	 * Called in narrow phase when determining which entities can be hit
	 */
	fun canBeHit(source: DamageSource, attacker: AbstractEntity? = null, inflictor: AbstractEntity? = null): Boolean {
		if (source.rayCheck) {
			val damageSource = inflictor ?: attacker ?: return true // nothing to check against

			// Original ray check is utterly broken, because it checks for line of sight between this entity and attacker entity,
			//  and not the inflictor. What this means, is that if you throw a grenade, and grenade is sourced to Player, then
			//  when grenade hits someone, rayCheck will check for line of sight between victim and Player (attacker), and not between victim and grenade (inflictor)
			if (source.damageArea.isLeft) {
				val poly = source.damageArea.left()
				val overlap = damageSource.metaBoundingBox.overlap(poly.aabb)

				// TODO: this should check for tile physics geometry, and not cell spaces
				//  Also, this seems to be flawed, since it casts ray from overlap center to damage source position
				//  So it is possible, in some setups, to attack through 1 tile wide walls
				if (!overlap.isEmpty && world.castRay(overlap.centre, damageSource.position, TileRayFilter.Solid).hitTile != null) {
					return false
				}
			} else {
				val line = source.damageArea.right()

				for (poly in damageHitbox) {
					val intersect = poly.intersect(line)

					// TODO: this should check for tile physics geometry, and not cell spaces
					if (intersect != null && intersect.second.point != null && world.castRay(line.p0, intersect.second.point!!, TileRayFilter.Solid).hitTile != null) {
						return false
					}
				}
			}
		}

		return true
	}

	/**
	 * Called in broad phase when determining which entities can be hit
	 */
	open fun potentiallyCanBeHit(source: DamageSource, attacker: AbstractEntity? = null, inflictor: AbstractEntity? = null): Boolean {
		return false
	}

	/**
	 * Called in narrow phase when entity is determined to be hittable by [potentiallyCanBeHit] and [canBeHit]
	 */
	open fun queryHit(source: DamageSource, attacker: AbstractEntity? = null, inflictor: AbstractEntity? = null): HitType? {
		return null
	}

	private data class DamageEvent(val group: Either<String, Int>, val expiresAt: Double)
	private val damageEvents = ObjectArrayList<DamageEvent>()

	/**
	 * Called on local entities (isRemote = false) when getting hit.
	 *
	 * Returns list of damage notifications, which are used for both displaying damage
	 * numbers and notifying entities they have damaged something
	 */
	open fun experienceDamage(damage: DamageRequestPacket): List<DamageNotification> {
		return listOf()
	}

	/**
	 * Called on local entities (isRemote = false) when this entity has hit something (possibly oneself)
	 *
	 * Called only for inflictors
	 */
	open fun hitOther(damage: HitRequestPacket) {}

	/**
	 * Called on local entities (isRemote = false) when this entity has damaged something
	 * (possibly oneself, when entity has generated [DamageNotification])
	 *
	 * Called only for attackers.
	 */
	open fun damagedOther(notification: DamageNotificationPacket) {}

	open fun tick(delta: Double) {
		if (networkGroup.upstream.isInterpolating) {
			networkGroup.upstream.tickInterpolation(delta)
		}

		damageEvents.removeIf { it.expiresAt <= world.sky.time }

		val world = world

		// It took a good amount of pondering around original code base, but here is how it works:
		// A - Player
		// B - Arrow
		// C - Target
		// When A fires B, B registers its damage source with sourceEntityId = A
		// When B reaches C, B receives hitOther() event, while C receives applyDamage() event
		// if C is vulnerable to damage specified, it will generate one, or more DamageNotifications
		// which, in turn, will trigger A's damagedOther() event
		//
		// This presents us with next problems:
		// * A will be completely unaware when hitting invulnerable entities
		//   (such as hitting NPCs with shield raised, and damage fully negated)
		// * B will be unaware, if it actually damaged C, B only knows it has tried to damage C.
		//   But we also must keep in mind that "projectiles" and similar entities remove themselves
		//   once they hit something, so they won't live to see applyDamage() to be called on remote entities
		for (bsource in damageSources) {
			var source = bsource

			if (source.trackSourceEntity) {
				source += position
			}

			val attacker = world.entities[source.sourceEntityId] ?: this

			val predicate = Predicate<AbstractEntity> {
				it !== this && isDamageAuthoritative(it) && source.team.canDamage(it.team.get(), attacker === it) && it.potentiallyCanBeHit(source, attacker, this) && it.canBeHit(source, attacker, this)
			}

			val hitEntities = source.damageArea.map({ world.entityIndex.query(it.aabb, predicate) }, { world.entityIndex.query(it, predicate) })

			for (entity in hitEntities) {
				val hitType = entity.queryHit(source, attacker, this) ?: return
				val eventGroup: Either<String, Int>

				if (source.damageRepeatGroup != null) {
					eventGroup = Either.left(source.damageRepeatGroup!!)
				} else {
					// Original engine checks against attacker and not against inflictor
					// TODO: allow mods to specify whenever invulnerability frames should be per-inflictor (new behavior) or per-attacker (old behavior)
					// If both attacker and inflictor are nulls, then it means damage comes from environment
					// but shouldn't it be impossible? Example - lava doesn't damage directly, "on fire" is what damages entities
					eventGroup = Either.right(entityID)
				}

				if (entity.damageEvents.any { it.group == eventGroup }) {
					return
				}

				val invulnerabilityFrames = source.damageRepeatTimeout ?: 1.0
				entity.damageEvents.add(DamageEvent(eventGroup, world.sky.time + invulnerabilityFrames))

				val data = DamageData(
					hitType,
					source.damageType,
					source.damage,
					source.knockbackMomentum(world.geometry, position),
					source.sourceEntityId,
					entityID,
					source.damageSourceKind,
					source.statusEffects
				)

				// I tried my best to make something more sane than what is
				// in original sources, but after 20 hours of tinkering
				// (trying to make it meaningful AND not deviate from original behavior), I gave up.
				//
				// At least we won't be as unoptimized as original code
				world.addHitRequest(HitRequestPacket(entityID, entity.entityID, data))

				if (data.damageType != DamageType.NO_DAMAGE)
					world.addDamageRequest(DamageRequestPacket(entityID, entity.entityID, data))
			}
		}
	}

	/**
	 * Entities can send other entities potentially remote messages and get
	 * responses back from them, and should implement this to receive and respond
	 * to messages.  If the message is NOT handled, should return Nothing,
	 * otherwise should return some Json value.
	 * This will only ever be called on master entities.
	 */
	protected open fun handleMessage(connection: Int, message: String, arguments: JsonArray): JsonElement? {
		return null
	}

	fun tryHandleMessage(sourceConnection: Int, message: String, arguments: JsonArray): CompletableFuture<JsonElement> {
		val response = try {
			handleMessage(sourceConnection, message, arguments)
		} catch (err: Throwable) {
			return CompletableFuture.failedFuture(err)
		}

		if (response == null) {
			return CompletableFuture.failedFuture(World.MessageCallException("Message '$message' was not handled"))
		} else {
			return CompletableFuture.completedFuture(response)
		}
	}

	fun dispatchMessage(sourceConnection: Int, message: String, arguments: JsonArray): CompletableFuture<JsonElement> {
		if (!isRemote) {
			return tryHandleMessage(sourceConnection, message, arguments)
		}

		return world.dispatchEntityMessage(sourceConnection, entityID, message, arguments)
	}

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

	}

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

	}

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