/**
	Use the `Knockback3.add(..)` functions to apply 3D knockback to units.
	Using angles:
		import Knockback3
		...
		let u = ...
		Knockback3.add(u, 1000., 0 .fromDeg(), 45 .fromDeg())

	Using a target position:
		let caster = GetSpellAbilityUnit()
		let target = getSpellTargetPos()

		Knockback3.add(caster, target, 500.)

	Configure this system by overriding the @configurable globals and setting
	the static class members:

		Knockback3.gravity = 25.

	By default, this package allows units to collide with destructables and
	destroy them. To disable this feature, make the global
	destroyDestructableSpeedThreshold a very high value:

		Knockback3.destroyDestructableSpeedThreshold = 999999999.

	This value defaults to 300.
*/
package Knockback3
import TerrainUtils
import MapBounds
import LinkedListModule
import HashMap
import Objects

/** A vec3 -> real function. */
public interface TerrainZProvider
	function get(vec3 where) returns real

/** A unit -> bool function. */
public interface UnitFilter
	function get(unit which) returns bool

/** A vec3 -> vec3 closure. */
public interface WallHitTransform
	function apply(vec3 vel) returns vec3

/**
If configured, provides an alternative source for getting terrain z-height.
This can be useful for avoiding a desync in some cases.
*/
@configurable TerrainZProvider HEIGHTMAP_PROVIDER = (vec3 w) -> w.getTerrainZ()

/**
Controls the behavior when hitting a wall at low altitude. Default behavior is to bounce/reverse off the wall, applying
the same restitution as in bouncing off the ground.

Override this value if you want to prevent bouncing, reflect on planes, etc.
*/
@configurable WallHitTransform WALLHIT_TRANSFORM = (vec3 vel) -> (
	vel.rotate(vec3(0., 0., 1.), PI) * Knockback3.restitutionCoefficientGround
)

/** The square rect size used for finding destructables. */
@configurable let DESTRUCTABLE_ENUM_SIZE = 130.

/**
If enabled, units have their move-speed changed while airborne.
Warning: this is not a lock-safe form of crowd control.
*/
@configurable let USE_MOVE_SPEED_MODIFIERS = true

/**
If enabled, units have their prop window changed while airborne.
Warning: this is not a lock-safe form of crowd control.
*/
@configurable let USE_PROP_WINDOW_MODIFIERS = true

/** Units must match this filter, or Knockback3 takes no effect. */
@configurable UnitFilter UNIT_FILTER = (unit u) -> not u.isType(UNIT_TYPE_FLYING)


public class Knockback3
	use LinkedListModule

	/** Fraction of velocity retained after colliding with ground/destructable. */
	static var restitutionCoefficientGround = .2
	static var restitutionCoefficientDestructable = .3

	/** Ratio. */
	static var frictionCoefficientGround = .15

	/** In units per second squared. */
	static var gravity = 90.

	/**
	If a unit is not falling faster than this, it will not bounce. Units
	per second.
	*/
	static var elasticityThreshold = -300.

	/**
	If a unit's vertical component is not greater than this, it will not
	become airborne. Units per second.
	*/
	static var airborneThreshold = 150.

	/**
	For the purposes of friction, distinguish a unit which is airborne
	versus one which is sliding. Height in units.
	*/
	static var isAirborneThreshold = 5.

	/** Below this speed, sliding units will stop. */
	static var minimumSlideSpeed = 30.

	/** Above this speed, sliding units will spawn a dust effect. */
	static var frictionFxThreshold = 180.
	static var frictionFxPath = Objects.impaleTargetDust

	/** Above this speed, destructables hit will be destroyed. */
	static var destroyDestructableSpeedThreshold = 300.

	/** Below this height, destructables hit may be destroyed. */
	static var destroyDestructableHeightThreshold = 150.

	/**
	 	Apply a knockback vector to unit u. `velocity` is initial speed in units
	 	per second. `groundAngle` and `airAngle` are the direction and trajectory
	 	parameters, respectively. Example:
	 */
	static function add(unit u, real velocity, angle groundAngle, angle airAngle)
		if not UNIT_FILTER.get(u)
			return

		let instVel = velocity * ANIMATION_PERIOD
		let v = ZERO3.polarProject(instVel, groundAngle, airAngle)
		if unitNodes.has(u)
			unitNodes.get(u).del += v
		else
			let knockback = new Knockback3()
			unitNodes.put(u, knockback)
			knockback.u = u
			knockback.del = v
			if size == 1
				clock.startPeriodic(ANIMATION_PERIOD, function tick)

			if knockback.u.addAbility(HEIGHT_ENABLER)
				knockback.u.removeAbility(HEIGHT_ENABLER)


	/**
		Apply a knockback vector to unit u. If the unit is stopped, this vector will
		throw the unit to the position at vec2 `target`. Along the XY-plane, the
		unit will move `groundSpeed` units per second. Note that the more vertical
		angle will always be used from the pair of possible trajectories. Example:
	*/
	static function add(unit u, vec2 target, real groundSpeed)
		let t = (target - u.getPos()).length() / groundSpeed
		let theta = u.getPos().angleTo(target)
		let velZ = gravity * t / 2. / ANIMATION_PERIOD
		let vel = theta.toVec(groundSpeed).withZ(velZ)
		let speed = vel.length()

		add(u, speed, theta, vec2(groundSpeed, vel.z).getAngle())


	/**
		Setter for the knockback vector on unit u. If the unit is not already tracked, this has the same behavior as
		`add`.
	*/
	static function setVel(unit u, real velocity, angle groundAngle, angle airAngle)
		if unitNodes.has(u)
			let knockback = unitNodes.get(u)
			let instVel = velocity * ANIMATION_PERIOD

			knockback.del = ZERO3.polarProject(instVel, groundAngle, airAngle)
		else
			add(u, velocity, groundAngle, airAngle)


	/**
		Getter for the knockback vector on unit u.  If the unit is not already tracked, returns (0, 0, 0).
	*/
	static function getVel(unit u) returns vec3
		return unitNodes.has(u) ? unitNodes.get(u).del : ZERO3


	/**
		Stop tracking unit u. If the unit is middair it will simply stop moving. If the unit is already untracked,
		nothing happens.
	*/
	static function forget(unit u)
		if unitNodes.has(u)
			destroy unitNodes.get(u)


	// Instance Variables
	private unit u
	private vec3 del


	private static function tickNearGround(Knockback3 knockback, vec3 newPos3, vec3 pos3, real velXySquared)
		if (
				newPos3.toVec2().isTerrainWalkable()
				or (newPos3.z > HEIGHTMAP_PROVIDER.get(newPos3) and knockback.del.z > 0.)
				or (not pos3.toVec2().isTerrainWalkable())
		)
			knockback.u.setXY(newPos3)

			if knockback.del.z <= isAirborneThreshold
				knockback.del *= (1. - frictionCoefficientGround)

				if velXySquared > frictionFxThreshold * frictionFxThreshold * ANIMATION_PERIOD
					addEffect(frictionFxPath, pos3).destr()

			if USE_MOVE_SPEED_MODIFIERS
				knockback.u.setMoveSpeed(knockback.u.getDefaultMovespeed())

			if USE_PROP_WINDOW_MODIFIERS
				knockback.u.setPropWindow(knockback.u.getDefaultPropWindow() * bj_DEGTORAD)

		else
			knockback.del = WALLHIT_TRANSFORM.apply(knockback.del)

		if knockback.del.z < elasticityThreshold * ANIMATION_PERIOD
			knockback.del.z = knockback.del.z*-1.*restitutionCoefficientGround

		if knockback.del.z > airborneThreshold * ANIMATION_PERIOD
			knockback.u.setFlyHeight(pos3.z + knockback.del.z, 0.)
			knockback.del.z = knockback.del.z - gravity * ANIMATION_PERIOD


	private static function tickAboveGround(Knockback3 knockback, vec3 newPos3, vec3 pos3)
		knockback.del.z -= gravity * ANIMATION_PERIOD
		let heightDifference = HEIGHTMAP_PROVIDER.get(newPos3) - HEIGHTMAP_PROVIDER.get(pos3)

		knockback.u..setFlyHeight(newPos3.z - heightDifference, 0.)
			..setXY(newPos3)

		if USE_MOVE_SPEED_MODIFIERS
			knockback.u.setMoveSpeed(0.)

		if USE_PROP_WINDOW_MODIFIERS
			knockback.u.setPropWindow(angle(0.))


	private static bool hitDestructable
	private static let destructableRect = Rect(-1 * DESTRUCTABLE_ENUM_SIZE, -1 * DESTRUCTABLE_ENUM_SIZE, DESTRUCTABLE_ENUM_SIZE, DESTRUCTABLE_ENUM_SIZE)

	private static function tickTryDestructable(Knockback3 knockback, vec3 newPos3)
		hitDestructable = false
		MoveRectTo(destructableRect, newPos3.x, newPos3.y)
		EnumDestructablesInRect(destructableRect, null) ->
			let des = GetEnumDestructable()
			if des.getLife() > 0.
				des.kill()
				hitDestructable = true

		if hitDestructable
			knockback.del *= restitutionCoefficientDestructable


	private static constant unitNodes = new HashMap<unit, Knockback3>()

	ondestroy
		unitNodes.remove(this.u)

		if USE_MOVE_SPEED_MODIFIERS
			this.u.setMoveSpeed(this.u.getDefaultMovespeed())

		if USE_PROP_WINDOW_MODIFIERS
			this.u.setPropWindow(this.u.getDefaultPropWindow() * bj_DEGTORAD)


	private static let clock = CreateTimer()

	private static function tick()
		for knockback from staticItr()
			let pos3 = knockback.u.getPos3Fly()
			var newPos3 = pos3 + knockback.del

			if not newPos3.inPlayable()
				newPos3.x = pos3.x
				newPos3.y = pos3.y
				knockback.del = ZERO2.withZ(knockback.del.z)

			let velXySquared = knockback.del.toVec2().lengthSq()

			if pos3.z < isAirborneThreshold
				tickNearGround(knockback, newPos3, pos3, velXySquared)
			else
				tickAboveGround(knockback, newPos3, pos3)

			if velXySquared > destroyDestructableSpeedThreshold * ANIMATION_PERIOD and pos3.z < destroyDestructableHeightThreshold
				tickTryDestructable(knockback, newPos3)

			let isVelxyLow = velXySquared < (minimumSlideSpeed * minimumSlideSpeed) * ANIMATION_PERIOD
			let isVelzLow = knockback.del.z.abs() < -1. * elasticityThreshold * ANIMATION_PERIOD
			let isAirborne = pos3.z > isAirborneThreshold
			if isVelxyLow and isVelzLow and not isAirborne
				knockback.u.setFlyHeight(0., 0.)
				destroy knockback

		if size == 0
			clock.pause()