package ru.dbotthepony.kstarbound.world.entities

import ru.dbotthepony.kommons.io.koptional
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.defs.JumpProfile
import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.defs.actor.player.ActorMovementModifiers
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
import ru.dbotthepony.kstarbound.network.syncher.networkedData
import ru.dbotthepony.kstarbound.network.syncher.networkedEnum
import ru.dbotthepony.kstarbound.util.GameTimer
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.physics.CollisionType
import kotlin.math.PI
import kotlin.math.absoluteValue
import kotlin.math.sign

class ActorMovementController() : MovementController() {
	var controlRun: Boolean = false
	var controlCrouch: Boolean = false
	var controlDown: Boolean = false
	var lastControlDown: Boolean = false
	var controlFly: Vector2d? = null
	var controlFace: Direction? = null

	var isWalking: Boolean by networkGroup.add(networkedBoolean())
		private set
	var isRunning: Boolean by networkGroup.add(networkedBoolean())
		private set

	var movingDirection: Direction by networkGroup.add(networkedEnum(Direction.RIGHT))
		private set
	var facingDirection: Direction by networkGroup.add(networkedEnum(Direction.RIGHT))
		private set

	var isCrouching: Boolean by networkGroup.add(networkedBoolean())
		private set

	var isFlying: Boolean by networkGroup.add(networkedBoolean())
		private set
	var isFalling: Boolean by networkGroup.add(networkedBoolean())
		private set

	var canJump: Boolean by networkGroup.add(networkedBoolean())
		private set
	var isJumping: Boolean by networkGroup.add(networkedBoolean())
		private set

	var isGroundMovement: Boolean by networkGroup.add(networkedBoolean())
		private set
	var isLiquidMovement: Boolean by networkGroup.add(networkedBoolean())
		private set

	var anchorState by networkGroup.add(networkedData(KOptional(), AnchorState.CODEC.koptional(), AnchorState.LEGACY_CODEC.koptional()))
		private set

	var controlJump: Boolean = false
	var controlJumpAnyway: Boolean = false

	var lastControlJump: Boolean = false
		private set
	var lastControlCrouch: Boolean = false
		private set

	var controlRotationRate: Double = 0.0
	var controlAcceleration: Vector2d = Vector2d.ZERO
	var controlForce: Vector2d = Vector2d.ZERO

	var fallThroughSustain: Int = 0
		private set

	// Target horizontal velocity for walking / running
	var targetHorizontalAmbulatingVelocity: Double = 0.0
		private set

	var controlPathMove: Pair<Vector2d, Boolean>? = null
	var pathMoveResult: Pair<Vector2d, Boolean>? = null

	var controlMove: Direction? = null

	var actorMovementParameters: ActorMovementParameters = ActorMovementParameters.EMPTY
	var movementModifiers: ActorMovementModifiers = ActorMovementModifiers.EMPTY

	var controlActorMovementParameters: ActorMovementParameters = ActorMovementParameters.EMPTY
	var controlMovementModifiers: ActorMovementModifiers = ActorMovementModifiers.EMPTY

	val approachVelocities = ArrayList<ApproachVelocityCommand>()
	val approachVelocityAngles = ArrayList<ApproachVelocityAngleCommand>()

	var anchorEntity: DynamicEntity? = null

	var pathController: PathController? = null
	var groundMovementSustainTimer: GameTimer = GameTimer(0.0)
	var reJumpTimer: GameTimer = GameTimer(0.0)
	var jumpHoldTimer: GameTimer? = null

	data class ApproachVelocityCommand(
		val target: Vector2d,
		val maxControlForce: Double
	)

	data class ApproachVelocityAngleCommand(
		val alongAngle: Double,
		val targetVelocity: Double,
		val maxControlForce: Double,
		val positiveOnly: Boolean
	)

	fun calculateMovementParameters(base: ActorMovementParameters): MovementParameters {
		val mass = base.mass
		val gravityMultiplier = base.gravityMultiplier

		val liquidBuoyancy = base.liquidBuoyancy
		val airBuoyancy = base.airBuoyancy
		val bounceFactor = base.bounceFactor
		val stopOnFirstBounce = base.stopOnFirstBounce
		val enableSurfaceSlopeCorrection = base.enableSurfaceSlopeCorrection
		val slopeSlidingFactor = base.slopeSlidingFactor
		val maxMovementPerStep = base.maxMovementPerStep

		val collisionPoly = if (isCrouching) base.crouchingPoly else base.standingPoly

		val stickyCollision = base.stickyCollision
		val stickyForce = base.stickyForce

		val airFriction = base.airFriction
		val liquidFriction = base.liquidFriction

		// If we are traveling in the correct direction while in a movement mode that
		// requires contact with the ground (ambulating i.e. walking or running), and
		// not traveling faster than our target horizontal movement, then apply the
		// special 'ambulatingGroundFriction'.
		val relativeXVelocity = velocity.x - surfaceVelocity.x
		val useAmbulatingGroundFriction = (isWalking || isRunning) &&
			targetHorizontalAmbulatingVelocity.sign == relativeXVelocity.sign &&
			relativeXVelocity.absoluteValue <= targetHorizontalAmbulatingVelocity.absoluteValue

		val groundFriction = if (useAmbulatingGroundFriction) base.ambulatingGroundFriction else base.normalGroundFriction

		val ignorePlatformCollision = fallThroughSustain > 0 || controlFly != null || controlDown

		val collisionEnabled = base.collisionEnabled
		val frictionEnabled = base.frictionEnabled
		val gravityEnabled = base.gravityEnabled

		val maximumPlatformCorrection = base.maximumPlatformCorrection
		val maximumPlatformCorrectionVelocityFactor = base.maximumPlatformCorrectionVelocityFactor

		val physicsEffectCategories = base.physicsEffectCategories

		val maximumCorrection = base.maximumCorrection
		val speedLimit = base.speedLimit

		return MovementParameters(
			mass = mass,
			gravityMultiplier = gravityMultiplier,
			liquidBuoyancy = liquidBuoyancy,
			airBuoyancy = airBuoyancy,
			bounceFactor = bounceFactor,
			stopOnFirstBounce = stopOnFirstBounce,
			enableSurfaceSlopeCorrection = enableSurfaceSlopeCorrection,
			slopeSlidingFactor = slopeSlidingFactor,
			maxMovementPerStep = maxMovementPerStep,
			maximumCorrection = maximumCorrection,
			speedLimit = speedLimit,
			stickyCollision = stickyCollision,
			stickyForce = stickyForce,
			airFriction = airFriction,
			liquidFriction = liquidFriction,
			groundFriction = groundFriction,
			collisionEnabled = collisionEnabled,
			frictionEnabled = frictionEnabled,
			gravityEnabled = gravityEnabled,
			maximumPlatformCorrection = maximumPlatformCorrection,
			maximumPlatformCorrectionVelocityFactor = maximumPlatformCorrectionVelocityFactor,
			physicsEffectCategories = physicsEffectCategories,
			ignorePlatformCollision = ignorePlatformCollision,
			collisionPoly = collisionPoly,
		)
	}

	fun updateMovementParameters(base: ActorMovementParameters): MovementParameters {
		val params = calculateMovementParameters(base)
		movementParameters = params
		return params
	}

	fun clearControls() {
		controlRotationRate = 0.0
		controlAcceleration = Vector2d.ZERO
		controlForce = Vector2d.ZERO
		controlRun = false
		controlCrouch = false
		controlJump = false
		controlJumpAnyway = false
		controlFly = null
		controlPathMove = null
		controlActorMovementParameters = ActorMovementParameters.EMPTY
		controlMovementModifiers = ActorMovementModifiers.EMPTY
	}

	override fun move(delta: Double) {
		// TODO: anchor entity

		if (anchorEntity?.isInWorld != true)
			anchorEntity = null

		val anchorEntity = anchorEntity

		if (anchorEntity != null) {
			controlRun = false
			controlCrouch = false
			isFlying = false
			isFalling = false
			canJump = false
			controlJump = false
			isGroundMovement = false
			isLiquidMovement = false

			velocity = (anchorEntity.position - position) / delta
			super.move(delta)
			position = anchorEntity.position
		} else {
			val movementParameters = actorMovementParameters.merge(controlActorMovementParameters)
			val movementModifiers = movementModifiers.combine(controlMovementModifiers)

			if (movementModifiers.movementSuppressed) {
				controlMove = null
				controlRun = false
				controlCrouch = false
				controlDown = false
				controlJump = false
				controlPathMove = null
			}

			// controlling any other movement overrides the pathing
			if (controlMove != null || controlCrouch || controlDown || controlJump || controlFly != null || approachVelocities.isNotEmpty() || approachVelocityAngles.isNotEmpty()) {
				controlPathMove = null
			}

			if (controlPathMove != null && pathMoveResult == null) {
				if (appliedForceRegion) {
					pathController?.reset()
				} else if (!pathController!!.isPathfinding) {
					// TODO: path move code
				} else {

				}
			} else {
				pathController = null
			}

			if (controlFly != null)
				controlMove = null

			if ((controlDown && !lastControlDown) || controlFly != null)
				fallThroughSustain = movementParameters.fallThroughSustainFrames ?: 0
			else if (fallThroughSustain > 0)
				fallThroughSustain--

			updateMovementParameters(movementParameters)

			targetHorizontalAmbulatingVelocity = 0.0

			rotation = (rotation + controlRotationRate * delta) % (PI * 2.0)
			velocity += controlAcceleration * delta + controlForce / mass * delta

			approachVelocities.forEach {
				approachVelocity(it.target, it.maxControlForce, delta)
			}

			approachVelocityAngles.forEach {
				approachVelocityAlongAngle(it.alongAngle, it.targetVelocity, it.maxControlForce, delta, it.positiveOnly)
			}

			isLiquidMovement = liquidPercentage >= (actorMovementParameters.minimumLiquidPercentage ?: 0.0)
			val liquidImpedance = liquidPercentage * (actorMovementParameters.liquidImpedance ?: 0.0)

			var updatedMovingDirection: Direction? = null
			val isRunning = controlRun && !movementModifiers.runningSuppressed

			if (controlFly != null) {
				var flyVelocity = controlFly!!

				if (flyVelocity.lengthSquared != 0.0)
					flyVelocity = flyVelocity.unitVector * (actorMovementParameters.flySpeed ?: 0.0)

				if (isLiquidMovement)
					approachVelocity(flyVelocity * (1.0 - liquidImpedance) * movementModifiers.speedModifier, (movementParameters.liquidForce ?: 0.0) * movementModifiers.liquidMovementModifier, delta)
				else
					approachVelocity(flyVelocity * movementModifiers.speedModifier, movementParameters.airForce ?: 0.0, delta)

				if (flyVelocity.x > 0.0)
					updatedMovingDirection = Direction.RIGHT
				else if (flyVelocity.x < 0.0)
					updatedMovingDirection = Direction.LEFT

				groundMovementSustainTimer = GameTimer(0.0)
			} else {
				val jumpModifier: Double
				val jumpProfile: JumpProfile

				if (isLiquidMovement) {
					jumpModifier = movementModifiers.liquidJumpModifier
					jumpProfile = movementParameters.liquidJumpProfile.copy(jumpSpeed = (movementParameters.liquidJumpProfile.jumpSpeed ?: 0.0) * (1.0 - liquidImpedance))
				} else {
					jumpModifier = movementModifiers.airJumpModifier
					jumpProfile = movementParameters.airJumpProfile
				}

				var startJump = false
				var holdJump = false

				// If we are on the ground, then reset the ground movement sustain timer
				// to the maximum.  If we are not on the ground or near the ground
				// according to the nearGroundCheckDistance, and we are past the minimum
				// sustain time, then go ahead and immediately clear the ground movement
				// sustain timer.
				val minGroundSustain = movementParameters.groundMovementMinimumSustain ?: 0.0
				val maxGroundSustain = movementParameters.groundMovementMaximumSustain ?: 0.0
				val groundCheckDistance = movementParameters.groundMovementCheckDistance ?: 0.0

				groundMovementSustainTimer.tick(delta)

				if (isOnGround) {
					groundMovementSustainTimer = GameTimer(maxGroundSustain)
				} else if (!groundMovementSustainTimer.hasFinished && groundCheckDistance > 0.0 && maxGroundSustain - groundMovementSustainTimer.timer > minGroundSustain) {
					val collideAny = computeLocalHitboxes()
						.any { world.polyIntersects(it + Vector2d(0.0, -groundCheckDistance), { it.type >= CollisionType.PLATFORM }) }

					if (collideAny)
						groundMovementSustainTimer = GameTimer(0.0)
				}

				val standingJumpable = !groundMovementSustainTimer.hasFinished
				val controlJump = this.controlJump && (!movementModifiers.jumpingSuppressed || controlJumpAnyway)

				// We are doing a jump if m_reJumpTimer has run out and there has been a
				// new m_controlJump command which was just recently triggered.  If
				// jumpProfile.autoJump is set, then we don't care whether it is a new
				// m_controlJump command, m_controlJump can be held.
				if (reJumpTimer.hasFinished && controlJump && (jumpProfile.autoJump == true || !lastControlJump)) {
					if (standingJumpable || jumpProfile.multiJump == true || controlJumpAnyway)
						startJump = true
				} else if (isJumping && controlJump && jumpHoldTimer?.hasFinished != true) {
					if (jumpProfile.collisionCancelled != true || collisionCorrection.y >= 0.0)
						holdJump = true
				}

				if (startJump) {
					isJumping = true
					reJumpTimer = GameTimer(jumpProfile.reJumpDelay ?: 0.0)

					jumpHoldTimer = if ((jumpProfile.jumpHoldTime ?: 0.0) >= 0.0) {
						GameTimer(jumpProfile.jumpHoldTime ?: 0.0)
					} else {
						null
					}

					velocity = velocity.copy(y = velocity.y + (jumpProfile.jumpSpeed ?: 0.0) * (jumpProfile.jumpInitialPercentage ?: 0.0) * jumpModifier)
					groundMovementSustainTimer = GameTimer(0.0)
				} else if (holdJump) {
					reJumpTimer.tick(delta)
					jumpHoldTimer?.tick(delta)

					approachYVelocity((jumpProfile.jumpSpeed ?: 0.0) * jumpModifier, (jumpProfile.jumpControlForce ?: 0.0) * jumpModifier, delta)
				} else {
					isJumping = false
					reJumpTimer.tick(delta)
				}

				if (controlMove == Direction.LEFT) {
					updatedMovingDirection = Direction.LEFT
					targetHorizontalAmbulatingVelocity = -1.0 * (if (isRunning) movementParameters.runSpeed ?: 0.0 else movementParameters.walkSpeed ?: 0.0) * movementModifiers.speedModifier
				} else if (controlMove == Direction.RIGHT) {
					updatedMovingDirection = Direction.RIGHT
					targetHorizontalAmbulatingVelocity = 1.0 * (if (isRunning) movementParameters.runSpeed ?: 0.0 else movementParameters.walkSpeed ?: 0.0) * movementModifiers.speedModifier
				}

				if (isLiquidMovement)
					targetHorizontalAmbulatingVelocity *= 1.0 - liquidImpedance

				// don't ambulate if we're already moving faster than the target velocity in the direction of ambulation
				val ambulationWouldAccelerate = (targetHorizontalAmbulatingVelocity + surfaceVelocity.x).absoluteValue > velocity.x.absoluteValue ||
					targetHorizontalAmbulatingVelocity < 0 != velocity.x < 0

				if (targetHorizontalAmbulatingVelocity != 0.0 && ambulationWouldAccelerate) {
					val ambulatingAccelerate = if (isOnGround)
						(movementParameters.groundForce ?: 0.0) * movementModifiers.groundMovementModifier
					else if (isLiquidMovement)
						(movementParameters.liquidForce ?: 0.0) * movementModifiers.liquidMovementModifier
					else
						movementParameters.airForce ?: 0.0

					approachXVelocity(targetHorizontalAmbulatingVelocity + surfaceVelocity.x, ambulatingAccelerate, delta)
				}
			}

			if (updatedMovingDirection != null)
				movingDirection = updatedMovingDirection

			if (!movementModifiers.facingSuppressed) {
				if (controlFace != null)
					facingDirection = controlFace!!
				else if (updatedMovingDirection != null)
					facingDirection = updatedMovingDirection
				else if (controlPathMove != null && pathController?.controlFace != null)
					facingDirection = pathController?.controlFace!!
			}

			isGroundMovement = !groundMovementSustainTimer.hasFinished

			if (isGroundMovement) {
				this.isRunning = isRunning && controlMove != null
				this.isWalking = !isRunning && controlMove != null
				this.isCrouching = controlCrouch && controlMove != null
			}

			isFlying = controlFly != null
			isFalling = (velocity.y < (movementParameters.fallStatusSpeedMin ?: 0.0)) && !isGroundMovement

			super.move(delta)

			lastControlDown = controlDown
			lastControlJump = controlJump

			if (isLiquidMovement)
				canJump = reJumpTimer.hasFinished && (!groundMovementSustainTimer.hasFinished || movementParameters.liquidJumpProfile.multiJump == true)
			else
				canJump = reJumpTimer.hasFinished && (!groundMovementSustainTimer.hasFinished || movementParameters.airJumpProfile.multiJump == true)
		}

		clearControls()
	}
}
