package Entity
import public TimerUtils
import public ClosureEvents
import public DupletListModule
import public TerrainUtils
import public PhysicsConstants
import public LinkedList
import public MapBounds
import initlater DebugInfo
import initlater Json
import public FText
import public ErrorHandling
import public UnitIndexer
import public Frentity
import Heightmap

public constant KEY_X = "x"
public constant KEY_Y = "y"
public constant KEY_Z = "z"
public constant KEY_TYPE = "t"

public vec3 serializeOrigin = ZERO3

UnitEntity array entities

public function unit.getEntity() returns UnitEntity
	return entities[this.getIndex()]

public function unit.setEntity(UnitEntity e)
	entities[this.getIndex()] = e

// Item entities still use userdata as they aren't indexed
public function item.getEntity() returns Entity
	return this.getUserData() castTo Entity

public function item.setEntity(Entity e)
	this.setUserData(e castTo int)

public interface Serializable
	function serialize() returns Json
	function deserialize(Json json)

	function debugPrint() returns string
		let s = new BigString()
		let json = serialize()
		json.addToBigString(s)
		destroy json
		let st =  s.getString(0, BIG_SUBSTRING_LEN)
		destroy s
		return st

/** Base Entity Class  */
public abstract class Entity implements Serializable
	use DupletListModule
	/** 3d position */
	protected vec3 pos
	/** 3d velocity */
	protected vec3 vel

	/** gravity */
	real gravity = defaultGravity
	/** radius */
	real radius
	/** squared radius to speed up distance checks */
	real radius2
	/** speed factor (multiplied to velocity) */
	real speedFactor = 1.

	/** owning player */
	player owner

	/** sleeping or active? */
	var flying 		= false
	var done 		= false
	var sleeps 		= true
	var idleSeconds	= 0

	construct()
		setupEntity(ZERO3, ZERO3, DUMMY_PLAYER, 0.)

	construct(player owner)
		setupEntity(ZERO3, ZERO3, owner, 0.)

	construct(player owner, vec2 pos)
		setupEntity(pos.toVec3(), ZERO3, owner, 0.)

	construct(player owner, vec3 pos)
		setupEntity(pos, ZERO3, owner, 0.)

	construct(player owner, vec3 pos, real radius)
		setupEntity(pos, ZERO3, owner, radius)

	construct(player owner, vec3 pos, real radius, vec3 vel)
		setupEntity(pos, vel, owner, radius)

	function setupEntity(vec3 pos, vec3 vel, player owner, real radius)
		this.pos = pos
		this.vel = vel
		this.owner = owner
		this.radius = radius
		this.radius2 = radius * radius
		if pos.z > (DYNAMIC_Z ? pos.getHeightMap() : 0)
			setFlying(true)
		allocationsPerSecond++

	function getPos() returns vec3
		return pos

	function setXY(vec3 pos)
		this.pos = pos

	function setPos(vec3 pos)
		this.pos = pos
		if pos.z > pos.getHeightMap() and gravity != 0
			activate()

	function addPos(vec3 pos)
		this.pos += pos
		if pos.z > pos.getHeightMap() and gravity != 0
			activate()

	function getVel() returns vec3
		return vel

	function setVel(vec3 vel)
		this.vel = vel
		activate()

	function addVel(vec3 vel)
		this.vel += vel
		activate()

	function scaleVel(real factor)
		this.vel *= factor

	function setTarget(vec3 tpos, real speed)
		var t = pos.distanceTo2d(tpos) / speed
		let tangle = pos.angleTo2d(tpos)
		var e = 0.
		if DYNAMIC_Z
			e = tpos.getHeightMap()

		if t < 1.
			t = 1./speed

		let startZVelocity = ((-gravity * t) / 2 - pos.z/t + e/t)
		setVel(tangle.toVec(speed).withZ(startZVelocity))

	function setFlying(boolean flag)
		flying = flag

	/** This function is called every ANIMATION_PERIOD tick if the Entity is active */
	function update()
		if sleeps and not flying and vel.lengthSquared() < 1.5
			deactivate()
		else
			let tz = DYNAMIC_Z ? pos.getHeightMap() : 0
			if pos.z >= tz
				vel.z += gravity
				setFlying(true)
			else
				pos.z = tz - 0.01
				setFlying(false)

			setXY(pos + vel * speedFactor)

	function slowUpdate()
		idleSeconds++

	function terminate()
		if not done
			done = true
			destroy this

	override function serialize() returns Json
		let json = new Json()
		getPos()
		json.addProperty(new Property(KEY_X, (pos.x-serializeOrigin.x).toInt().toString()))
		json.addProperty(new Property(KEY_Y, (pos.y-serializeOrigin.y).toInt().toString()))
		json.addProperty(new Property(KEY_Z, (pos.z-serializeOrigin.z).toInt().toString()))
		return json

	override function deserialize(Json json)
		// pos = vec3(json.getReal(KEY_X)+serializeOrigin.x, json.getReal(KEY_Y)+serializeOrigin.y, json.getReal(KEY_Z)+serializeOrigin.z)


function unit.getDynamicPos() returns vec3
	if DYNAMIC_Z
		return this.getPos().withHeightMap() + vec3(0, 0, this.getFlyHeight())
	return this.getPos().withZ(0)

abstract public class UnitEntity extends Entity
	unit actor = null

	construct(unit actor)
		super(actor.getOwner(), actor.getDynamicPos(), 0)
		setupUnitEntity(actor)

	construct(unit actor, vec3 pos)
		super(actor.getOwner(), pos, 0)
		setupUnitEntity(actor)

	construct(unit actor, vec3 pos, real radius)
		super(actor.getOwner(), pos, radius)
		setupUnitEntity(actor)

	function setupUnitEntity(unit actor)
		this.actor = actor
		getPos()
		if not actor.isAlive()
			terminate()
			error("Trying to assign entity to unit that is not alive")
		else if actor.getEntity() != null
			terminate()
			error("Trying to assign entity to unit that already has an entity")

		actor..setEntity(this)..addAbility(HEIGHT_ENABLER)..removeAbility(HEIGHT_ENABLER)
		if DYNAMIC_Z
			pos = actor.getDynamicPos()
			actor.setXYZReal(pos)
		else
			pos = actor.getPos3Fly()
			actor.setXYZ(pos)

	override function update()
		pos = actor.getPos3with(pos.z)
		super.update()

	function setNewActor(unit u)
		if u == null or not u.isAlive() or u.getEntity() != null
			error("invalid actor replacement")
		actor.setEntity(null)
		actor.remove()
		actor = u
		actor.setEntity(this)

	override function setXY(vec3 tpos)
		if tpos.inPlayable()
			pos = tpos
			if DYNAMIC_Z
				actor.setXYZReal(pos)
			else
				actor.setXYZ(pos)
		else
			Log.error("out of bounds: " + tpos.toString())
			setPos(ZERO3)

	override function setPos(vec3 tpos)
		super.setPos(tpos)
		if DYNAMIC_Z
			actor.setPosReal(pos)
		else
			actor.setPos(pos.toVec2())

	override function getPos() returns vec3
		if not active
			if DYNAMIC_Z
				pos = actor.getDynamicPos()
			else
				pos = actor.getPos3Fly()
		return pos

	ondestroy
		if actor.getEntity() == this
			actor.setEntity(null)
			if actor.isAlive()
				if KILL_ACTORS
					actor.kill()
				else
					actor.remove()
		actor = null

abstract public class ItemEntity extends Entity
	item actor = null

	construct(item actor)
		super(DUMMY_PLAYER, actor.getPos())
		this.actor = actor
		actor.setEntity(this)

	construct(item actor, player owner)
		super(owner, actor.getPos())
		this.actor = actor
		actor.setEntity(this)

	abstract function onPickup(UnitEntity entity)

	abstract function onDrop(UnitEntity entity)

	abstract function onUse(UnitEntity entity)

	function setNewActor(item i)
		if i == null or not i.isAlive() or i.getEntity() != null
			error("invalid actor replacement")
		actor.setEntity(null)
		actor.remove()
		actor = i
		actor.setEntity(this)
		actor.setPos(pos.toVec2())

	override function setXY(vec3 tpos)
		if tpos.inPlayable()
			pos = tpos
			actor.setPos(pos.toVec2())
		else
			terminate()

	override function setPos(vec3 tpos)
		super.setPos(tpos)
		actor.setPos(pos.toVec2())

	override function getPos() returns vec3
		if not active
			pos = actor.getPos().withHeightMap()
		return pos

	ondestroy
		if actor.getEntity() == this
			actor.setEntity(null)
			if actor.isAlive()
				actor.remove()
		actor = null

function onItemUse()
	let idata = GetManipulatedItem().getEntity()
	let edata = GetManipulatingUnit().getEntity()
	if idata != null and not idata.done
		if edata != null and not edata.done
			(idata castTo ItemEntity).onUse(edata)

function onItemPickup()
	let idata = GetManipulatedItem().getEntity()
	let edata = GetManipulatingUnit().getEntity()
	if idata != null and not idata.done
		if edata != null and not edata.done
			(idata castTo ItemEntity).onPickup(edata)

function onItemDrop()
	let idata = GetManipulatedItem().getEntity()
	let edata = GetManipulatingUnit().getEntity()
	if idata != null and not idata.done
		if edata != null and not edata.done
			(idata castTo ItemEntity).onDrop(edata)

init
	EventListener.add(EVENT_PLAYER_UNIT_PICKUP_ITEM, () -> onItemPickup())
	EventListener.add(EVENT_PLAYER_UNIT_DROP_ITEM, () -> onItemDrop())
	EventListener.add(EVENT_PLAYER_UNIT_USE_ITEM, () -> onItemUse())