using System.Collections.Generic;
using UnityEngine;

public class CharacterAnimation : MonoBehaviour
{
	public float walkSwing = 45;
	public float walkSpeed = 1.0f;
	public float rollTuck = 45;
	public float rollRadius = 0.25f;
	public float swanDiveHeight = 1.5f;
	public float swanDiveRecoverDuration = 0.5f;
	public float rollBlendInTime = 1.0f;
	public float rollBlendInBounce = 0.9f;
	public float rollBlendOutTime = 1.0f;
	public float rollBlendOutBounce = 0.9f;
	public float wobbleTime = 1.0f;
	public float wobbleBounce = 0.5f;

	const float pi2 = Mathf.PI*2;

	struct Bone
	{
		public float x, y, z, scale;
	}

	struct Pose
	{
		public Bone lfoot, rfoot;
		public Bone lhand, rhand;
		public Bone hips, chest, head;
		public Bone carrying;
	}

	class Bind
	{
		public Transform tf;
		public Quaternion initialRotation;

		public void Set(Bone bone) {
			Quaternion q = Quaternion.Euler(bone.x, bone.y, bone.z);
			tf.localRotation = q * initialRotation;
		}
	}

	Bind tfRoot;
	Bind tfHips, tfChest;
	Bind tfFootL, tfFootR;
	Bind tfHandL, tfHandR;
	Bind tfHead;
	Bind tfCarrying;
	Entity entity;
	Rigidbody body;
	PlayerControl control;
	Vector3 lastPos = Vector3.zero;

	enum RollState
	{
		None, Rolling, SwanDive, ExitSwanDive, CleanFinish
	}

	float walkCycle = 0;
	float groundSpeed = 0;
	SprungValue rollAngle, rollBlend, pivotHeight;
	RollState _roll = RollState.None;
	float rollStateTime = 0;

	Vector3 wobblePos, wobbleVel;
	DampedSpring wobbleSpring;

	RollState roll {
		get { return _roll; }
		set { _roll = value; rollStateTime = 0; }
	}

	public bool didCleanlyExitRoll {
		get {
			return roll == RollState.CleanFinish && rollAngle.value >= 0;
		}
	}

	public bool isRollingBackward {
		get {
			return Vector3.Dot(body.velocity, transform.forward) < 0;
		}
	}

	void OnEnable() {
		entity = GetComponent<Entity>();
		body = GetComponent<Rigidbody>();
		control = GetComponent<PlayerControl>();

		tfRoot = Find("Root");
		tfHead = Find("Head");
		tfHips = Find("Hips");
		tfChest = Find("Chest");
		tfFootL = Find("Foot_L");
		tfFootR = Find("Foot_R");
		tfHandL = Find("Hand_L");
		tfHandR = Find("Hand_R");
		tfCarrying = new Bind { tf=null, initialRotation=Quaternion.identity };

		wobblePos = transform.position + Vector3.up;
		wobbleVel = Vector3.zero;

		OnValidate();
	}

	Pose CalcWalk() {
		Pose pose = Empty();

		float ratio = Mathf.Clamp01(groundSpeed / entity.walkSpeed);
		float angle = Mathf.Sin(walkCycle) * walkSwing * ratio;

		pose.lhand.x = +angle;
		pose.rhand.x = -angle;
		pose.lfoot.x = -angle;
		pose.rfoot.x = +angle;

		pose.carrying.z = 180;

		Item item = entity.carrying;
		if (item && item.carried == Item.CarryMode.OneHandUpsideDown)
			pose.carrying.x = 180;

		return pose;
	}

	Pose CalcRoll() {
		Pose pose = Empty();

		pose.head.x = rollTuck;
		pose.lfoot.x = pose.rfoot.x = -rollTuck;

		return pose;
	}

	Pose CalcCharge() {
		Pose pose = Empty();

		pose.chest.x = -45;
		pose.rhand.x = -140;
		pose.carrying.x = 180+45;
		pose.carrying.z = -180;

		return pose;
	}

	bool CanCleanlyExitRoll() {
		return
			entity.onGround
			&& entity.groundTime > entity.rollMaxTime
			&& entity.rollTime > entity.rollMaxTime
			&& !isRollingBackward
			&& rollAngle.value > -Mathf.PI/2 && rollAngle.value <= 0;
	}

	void UpdateRollState(float dt) {
		switch (roll) {
			case RollState.None:
				// Bounce back to upright. (pivot at feet)
				rollBlend.goalOut = 0.0f;
				rollAngle.goalOut = (rollAngle.value > pi2*0.75f) ? pi2 : 0.0f;
				pivotHeight.goalOut = 0.0f;

				if (entity.isRolling)
					roll = (entity.groundDist > swanDiveHeight) ? RollState.SwanDive : RollState.Rolling;
				break;
			case RollState.SwanDive:
				// High up, aim for a swan dive. (pivot at center of mass)
				Vector3 localVel = transform.InverseTransformVector(body.velocity);
				rollAngle.goal = Mathf.Atan2(localVel.z, localVel.y);
				rollBlend.goal = 0.0f;
				pivotHeight.goal = body.centerOfMass.y;

				if (entity.groundDist < 0.2f || !entity.isRolling)
					roll = RollState.ExitSwanDive;
				break;
			case RollState.ExitSwanDive:
			case RollState.Rolling:
				// Near floor, start rolling. (pivot at roll axel)
				float target = groundSpeed/rollRadius;
				float max = entity.rollSpeed/rollRadius;

				// Roll backwards if that's where we're going.
				if (isRollingBackward)
					target = -target;

				if (roll == RollState.Rolling || rollStateTime > swanDiveRecoverDuration)
					rollAngle.velocity = Mathf.MoveTowards(rollAngle.velocity, target, dt*max/swanDiveRecoverDuration);

				rollAngle.Integrate();
				rollBlend.goal = 1.0f;
				pivotHeight.goal = rollRadius;

				if (groundSpeed < entity.rollExitSpeed)
					roll = RollState.None;
				else if (CanCleanlyExitRoll())
					roll = RollState.CleanFinish;
				break;
			case RollState.CleanFinish:
				// Exit the roll cleanly.
				rollAngle.velocity = entity.rollSpeed / rollRadius;
				rollAngle.Integrate();
				rollBlend.goalOut = 0.0f;
				pivotHeight.goalOut = 0.0f;
				if (!entity.isRolling)
					roll = RollState.None;
				break;
		}

		// Wrap angle within -180...+180
		rollAngle.value = Mathf.Repeat(rollAngle.value+Mathf.PI, pi2)-Mathf.PI;
	}

	void FixedUpdate() {
		float dt = Time.deltaTime;

		Vector3 flatVel = Vector3.ProjectOnPlane(body.velocity, Vector3.up);
		groundSpeed = flatVel.magnitude;

		walkCycle += dt * walkSpeed;
		walkCycle = Mathf.Repeat(walkCycle, pi2);

		rollStateTime += dt;
		UpdateRollState(dt);
	}

	void Update() {
		// Handle teleporting.
		if (Vector3.Distance(lastPos, transform.position) > 0.5f)
			wobblePos = transform.position+Vector3.up;
		lastPos = transform.position;

		// Track wobble spring.
		wobbleSpring = new DampedSpring(Time.deltaTime, wobbleTime, wobbleBounce);
		wobbleSpring.Apply(transform.position+Vector3.up, ref wobblePos, ref wobbleVel);
		Vector3 wobLocal = transform.InverseTransformPoint(wobblePos);
		Quaternion wobbleRotation = Quaternion.FromToRotation(Vector3.up, wobLocal);
	
		PlayerControl pc = GetComponent<PlayerControl>();

		// Blend poses.
		Pose walk = CalcWalk();
		Pose roll = CalcRoll();
		Pose charge = CalcCharge();
		Pose pose = Blend(walk, roll, rollBlend.value);
		if (pc)
			pose = Blend(pose, charge, pc.chargeAmount);
		Apply(pose);

		// Apply roll.
		Quaternion rot = Quaternion.Euler(rollAngle.value*Mathf.Rad2Deg, 0.0f, 0.0f);
		Vector3 pivot = new Vector3(0, pivotHeight.value, 0);
		Vector3 pos = pivot - rot * pivot;

		// Apply wobble.
		tfRoot.tf.localRotation = wobbleRotation * rot;
		tfRoot.tf.localPosition = wobbleRotation * pos;
	}

	Pose Empty() {
		Bone i = new Bone { x=0, y=0, z=0, scale=1 };
		return new Pose {
			lfoot = i,
			rfoot = i,
			lhand = i,
			rhand = i,
			hips = i,
			chest = i,
			head = i,
			carrying = i,
		};
	}

	Bone Blend(Bone a, Bone b, float t) {
		return new Bone {
			x = Mathf.LerpUnclamped(a.x, b.x, t),
			y = Mathf.LerpUnclamped(a.y, b.y, t),
			z = Mathf.LerpUnclamped(a.z, b.z, t),
			scale = Mathf.LerpUnclamped(a.scale, b.scale, t)
		};
	}

	Pose Blend(Pose a, Pose b, float t) {
		return new Pose {
			lfoot = Blend(a.lfoot, b.lfoot, t),
			rfoot = Blend(a.rfoot, b.rfoot, t),
			lhand = Blend(a.lhand, b.lhand, t),
			rhand = Blend(a.rhand, b.rhand, t),
			hips = Blend(a.hips, b.hips, t),
			chest = Blend(a.chest, b.chest, t),
			head = Blend(a.head, b.head, t),
			carrying = Blend(a.carrying, b.carrying, t),
		};
	}

	void Apply(Pose pose) {
		tfHandL.Set(pose.lhand);
		tfHandR.Set(pose.rhand);
		tfFootL.Set(pose.lfoot);
		tfFootR.Set(pose.rfoot);
		tfHead.Set(pose.head);
		tfHips.Set(pose.hips);
		tfChest.Set(pose.chest);

		Item item = entity.carrying;
		if (item) {
			tfCarrying.tf = item.transform;
			tfCarrying.tf.localPosition = Vector3.zero;
			tfCarrying.Set(pose.carrying);
		}
	}

	Bind Find(string name) {
		Transform tf = Utils.Find(transform, name);
		if (tf)
			return new Bind { tf=tf, initialRotation=tf.localRotation };

		Debug.LogError($"Missing bone {name}.");
		return null;
	}

	void OnValidate() {
		rollBlend.Configure(rollBlendInTime, rollBlendInBounce, rollBlendOutTime, rollBlendOutBounce);
		pivotHeight.Configure(rollBlendInTime, rollBlendInBounce);
		rollAngle.Configure(rollBlendInTime, rollBlendInBounce);
	}
}
