using UnityEngine;
using System.Collections.Generic;

public class Entity : MonoBehaviour
{
	[Header("Movement")]
	public bool canMove = false;
	public float walkSpeed = 2.0f;
	public float walkHalt = 0.5f;
	[Range(0, 1)] public float turnControl = 0.15f;
	public float turnSpeed = 600;
	public float turnDrag = 30.0f;

	[Header("Rolling")]
	public float rollSpeed = 2.0f;
	public float rollExitSpeed = 1.0f;
	public float rollHalt = 0.5f;
	public float rollMaxTime = 1.0f;
	[Range(0, 1)] public float rollControl = 0.5f;

	[Header("Zones")]
	public Rect vision = new Rect(-2, -2, 4, 4);
	public Rect useRange = new Rect(-1, -1, 2, 2);
	public Rect home = new Rect(-4, -4, 8, 8);
	public float triggerHeight = 4.0f;

	public enum LootDropMode {
		OnUse, OnDeath
	}

	[Header("Loot")]
	public LootDropMode lootDropMode = LootDropMode.OnDeath;
	public int loot = 0;

	[Header("Misc")]
	public GameObject speechPrefab;
	public int initialHealth = 0;
	public bool lookAtPlayer = false;
	public bool cameraTarget = false;

	// Inventory. (can be re-assigned)
	public InventoryDictionary inventory = new InventoryDictionary();

	// Misc state.
	public Item carrying { get; private set; }
	public Entity interest { get; private set; }
	public bool isSpeaking => (Game.speaker == gameObject || speechBubble);
	public bool isDisguised => (undisguised != null);
	[HideInInspector] public Vector3 lastGoodPosition;

	// Ground tracking.
	public bool onGround { get; private set; }
	public float airTime { get; private set; }
	public float groundTime { get; private set; }
	public float groundDist { get; private set; }
	Vector3 groundPos, groundPlane;
	static int groundMask;

	// Rolling.
	enum RollState { None, Rolling, Stopping };
	public bool isRolling { get { return rollState != RollState.None; } }
	public float rollTime { get; private set; }
	RollState rollState;

	// Triggers.
	[HideInInspector]
	public TriggerUtility triggerVision, triggerUsable;

	// Internal.
	float interestLevel, interestCooldown;
	float speechTimer;
	Rigidbody body;
	float accelValue = 0;
	TMPro.TextMeshProUGUI speechBubble;
	Collider coll;
	Squishbobs bobs;
	CharacterAnimation anim;
	PlayerControl pc;
	bool isInCameraList = false;
	Entity undisguised;

	List<EntityBehaviour> actions = new List<EntityBehaviour>();
	List<Entity> usables = new List<Entity>();
	List<Entity> users = new List<Entity>();
	List<Entity> unnoticed = new List<Entity>();

	void Awake() {
		coll = gameObject.GetComponent<Collider>();
		body = gameObject.AddComponent<Rigidbody>();
		bobs = gameObject.GetComponent<Squishbobs>();
		anim = gameObject.GetComponent<CharacterAnimation>();
		pc = gameObject.GetComponent<PlayerControl>();
		body.freezeRotation = true;
		body.interpolation = RigidbodyInterpolation.Interpolate;
		body.useGravity = false;
		body.isKinematic = !canMove;
		body.maxDepenetrationVelocity = 0.2f;

		// Force anything with 'Entity' onto the Entity layer.
		gameObject.layer = LayerMask.NameToLayer("Entity");

		lastGoodPosition = transform.position;
		onGround = false;
		rollState = RollState.None;
		groundDist = 9999;
		airTime = groundTime = rollTime = 0;
		string[] groundLayers = { "Default" };
		groundMask = LayerMask.GetMask(groundLayers);
		triggerVision = AddTrigger("Vision");
		triggerUsable = AddTrigger("Usable");
		triggerUsable.onEnter += OnUseEnter;
		triggerUsable.onExit += OnUseExit;
		triggerVision.onStay += OnVisionStay;
		triggerVision.onEnter += OnVisionEnter;
		triggerVision.onExit += OnVisionExit;

		// Scan for any pre-existing active behaviours and track them.
		// For simple cases, this lets the user easily configure things 
		// in the editor without needing a script to trigger them.
		foreach (EntityBehaviour x in gameObject.GetComponents<EntityBehaviour>()) {
			if (x.enabled) {
				if (actions.Count > 0)
					Debug.LogError($"{gameObject.name} has multiple default behaviours.", gameObject);
				actions.Add(x);
			}
		}

		OnZonesChanged();
	}

	void OnDestroy() {
		Game.camera.targets.Remove(transform);
		if (speechBubble)
			Destroy(speechBubble.gameObject);
		if (carrying)
			Destroy(carrying.gameObject);
	}

	void Start() {
		Disguise disguise = null;

		// Initialize inventory.
		foreach (Item item in Game.game.inventoryDatabase.items) {
			Disguise d = item.GetComponent<Disguise>();
			if (d && GetItemCount(item) > 0)
				disguise = d;
		}
		if (initialHealth != 0)
			health = initialHealth;

		// Auto-disguise whenever we start.
		if (disguise && !IsWearing(disguise))
			Disguise(disguise);
	}

	Bounds rectToBounds(Rect r) {
		return new Bounds(
			new Vector3(r.center.x, 0, r.center.y),
			new Vector3(r.width, triggerHeight, r.height));
	}

	public EntityBehaviour[] GetActions() {
		return actions.ToArray();
	}

	public Bounds visionBounds { get { return rectToBounds(vision); } }
	public Bounds usableBounds { get { return rectToBounds(useRange); } }
	public Bounds homeBounds { get { return rectToBounds(home); } }

	public int health {
		get {
			return inventory["Heart"].count;
		}
		set {
			inventory["Heart"].count = Mathf.Clamp(value, 0, inventory["Heart"].capacity);
		}
	}

	public int GetItemCount(Item item) {
		if (!inventory.ContainsKey(item.name))
			return 0;
		return inventory[item.name].count;
	}

	public int Award(Item item, int count) {
		// If we don't have any yet, add it to our inventory.
		if (!inventory.ContainsKey(item.name)) {
			inventory[item.name] = new Inventory { count = 0, capacity = item.defaultLimit };
		}

		// See how many we can take.
		Inventory inv = inventory[item.name];
		if (inv.capacity > 0)
			count = Mathf.Min(inv.capacity - inv.count, count);

		inventory[item.name].count += count;

		// Save the game if needed.
		if (item.triggersSave && inventory == SaveGame.current.inventory)
			Game.saveRequested = true;

		// If it's a disguise, pop it on.
		Disguise disguise = item.GetComponent<Disguise>();
		if (disguise)
			Disguise(disguise);

		// Announce how much we got.
		// TODO: make this smaller, prettier, and not follow the player around.
		if (count > 0 && !disguise)
			Say(count.ToString());

		return count;
	}

	public bool Pay(Item item, int count) {
		if (!inventory.ContainsKey(item.name))
			return false;

		Inventory inv = inventory[item.name];
		if (inv.count < count)
			return false;

		inv.count -= count;
		if (inv.count == 0)
			RemoveItem(item);

		// TODO: put in HUD?
		Say("-"+count.ToString());
		return true;
	}

	public void RemoveItem(Item item) {
		inventory.Remove(item.name);
		if (UIWrangler.selectedItem && item.name == UIWrangler.selectedItem.name)
			UIWrangler.selectedItem = null;
	}

	public bool IsWearing(Disguise disguise) {
		return gameObject.name == disguise.disguiseAs.name;
	}

	public void Disguise(Disguise disguise) {
		Debug.Log($"{gameObject.name} disguised as {disguise.disguiseAs.name}.");

		// Drop in the new guy and deactivate ourselves.
		GameObject replace = GameObject.Instantiate(disguise.disguiseAs);
		replace.name = disguise.disguiseAs.name;
		replace.transform.SetPositionAndRotation(transform.position, transform.rotation);
		gameObject.SetActive(false);
		replace.GetComponent<Entity>().undisguised = this;
		Game.Bind(gameObject, replace);

		// Force it to be selected.
		UIWrangler.selectedItem = disguise.GetComponent<Item>();
	}

	public Entity Undisguise() {
		Debug.Log($"{gameObject.name} reverted to {undisguised.name}.");

		// Switch back to the old guy.
		Game.Bind(gameObject, undisguised.gameObject);
		undisguised.transform.SetPositionAndRotation(transform.position, transform.rotation);
		undisguised.gameObject.SetActive(true);
		Destroy(gameObject);
		return undisguised;
	}

	public void CarryObject(Item prefab) {
		if (carrying) {
			GameObject.Destroy(carrying.gameObject);
			carrying = null;
		}

		if (!prefab)
			return;

		GameObject g = GameObject.Instantiate(prefab.gameObject);
		g.name = prefab.name;
		carrying = g.GetComponent<Item>();

		// Parent it into the right place.
		Transform where = Utils.Find(transform, "Hand_R_end");
		if (where) {
			g.transform.parent = where;
		} else {
			// No place for it? Then hide it but keep it active
			// at our origin.
			g.transform.parent = transform;
			g.transform.localScale = Vector3.zero;
		}
	}

	// To be called by the user if they change the zones at runtime.
	public void OnZonesChanged() {
		triggerVision.SetBoxBounds(visionBounds);
		triggerUsable.SetBoxBounds(usableBounds);
	}

	// Adds a new trigger as a child.
	TriggerUtility AddTrigger(string name) {
		GameObject g = new GameObject(name, typeof(BoxCollider));
		g.layer = LayerMask.NameToLayer("Entity Trigger");
		g.transform.SetParent(transform, false);

		return g.AddComponent<TriggerUtility>();
	}

	public T StartAction<T>() where T : EntityBehaviour {
		// Disable any current action.
		if (actions.Count > 0)
			actions[actions.Count-1].enabled = false;

		// Reuse any old disabled ones first (avoid GC churn)
		T action = null;
		foreach (T t in gameObject.GetComponents<T>()) {
			if (!actions.Contains(t)) {
				UnityEngine.Assertions.Assert.IsFalse(t.enabled);
				action = t;
				break;
			}
		}

		// If none found to re-use, make a new one and clone
		// the default settings.
		if (!action) {
			// Find an existing instance to clone settings from.
			T old = gameObject.GetComponent<T>();
			if (!old)
				Debug.LogError($"{gameObject.name} has no {typeof(T).Name} configured.");

			action = gameObject.AddComponent<T>();

			// Copy everyting over.
			// There's probably a better way than doing this. It doesn't 
			// really matter as we only do it once. Next time we can just 
			// re-use this component again.
			string tmp = JsonUtility.ToJson(old);
			JsonUtility.FromJsonOverwrite(tmp, action);
		}

		action.enabled = true;
		actions.Add(action);
		return action;
	}

	public bool IsActionFinished(EntityBehaviour action) {
		return !actions.Contains(action);
	}

	public void StopAction(EntityBehaviour action) {
		UnityEngine.Assertions.Assert.IsTrue(actions.Contains(action));
		action.enabled = false;
		actions.Remove(action);

		// Re-enable the next guy down.
		if (actions.Count > 0) {
			EntityBehaviour next = actions[actions.Count-1];
			UnityEngine.Assertions.Assert.IsFalse(next.enabled);
			next.enabled = true;
		}
	}

	public void Move(Vector3 vec) {
		if (isRolling && rollState == RollState.Stopping)
			return;

		// When rolling, limit how much they can fight it.
		if (isRolling)
			vec = (body.velocity.normalized + vec*rollControl).normalized;

		vec.y = 0;
		vec = Quaternion.FromToRotation(Vector3.up, groundPlane) * vec;

		// Slow the acceleration if we're trying to change direction.
		float t = Vector3.Dot(vec.normalized, body.velocity.normalized)*0.5f+0.5f;

		// Compute velocity change this frame.
		Vector3 delta = accelValue*vec * Time.deltaTime;

		body.AddForce(delta, ForceMode.VelocityChange);
	}

	public void Turn(Vector3 dir) {
		if (isRolling) {
			// Roll either forward or backward along our velocity.
			dir = body.velocity;
			if (Vector3.Dot(transform.forward, dir) < 0)
				dir = -dir;

			dir += transform.forward; // hax to soften it
		}

		dir.y = 0;
		dir = dir.normalized;

		float d = Vector3.Dot(dir, transform.forward);
		if (d < 0) {
			if (Vector3.Dot(dir, transform.right) > 0)
				dir = transform.right;
			else
				dir = -transform.right;
		}

		Vector3 turn = -Vector3.Cross(dir, transform.forward);
		body.AddTorque(turn*turnSpeed, ForceMode.Acceleration);
	}

	public bool MoveToTarget(Vector3 target, float closeDistance) {
		Vector3 vec = target - transform.position;
		float dist = vec.magnitude;
		vec.Normalize();

		if (dist < closeDistance)
			return true;

		Move(vec);
		Turn(vec);
		return false;
	}

	void GroundTest() {
		onGround = false;
		groundDist = 9999;
		float gw = 0.0f;
		groundPos = groundPlane = Vector3.zero;

		CapsuleCollider coll = GetComponent<CapsuleCollider>();
		if (coll) {
			float maxDist = coll.height*0.5f + coll.radius;
			Vector3 center = transform.TransformPoint(coll.center);

			// TODO: fire multiple rays and average (or take highest?)
			// Avatar does all 16 per frame. Other entities stratify
			// their rays out, maybe 1 per frame over 16 frames.
			RaycastHit hit;
			if (Physics.Raycast(center, Vector3.down, out hit, 9999, groundMask, QueryTriggerInteraction.Ignore)) {
				groundDist = Mathf.Min(groundDist, transform.position.y - hit.point.y);
				if (hit.collider != coll && hit.normal.y > 0.5f && hit.distance <= maxDist) {
					onGround = true;
					groundPos += hit.point;
					groundPlane += hit.normal;
					gw += 1.0f;
				}
			}
		}

		// Find our final ground plane.
		if (gw > 0) {
			groundPos /= gw;
			groundPlane.Normalize();
		}

		if (onGround) {
			// Don't stick if we're pushing away from it.
			if (Vector3.Dot(body.velocity, groundPlane) >= 0.5f) {
				onGround = false;
				return;
			}

			// If we're grounded, keep them stuck to it.
			if (!isRolling) {
				float floatingAmount = transform.position.y - groundPos.y;
				body.AddForce(-floatingAmount * groundPlane*80, ForceMode.Acceleration);
			}
		}
	}

	static float SolveForDrag(float time) {
		return 2.71f / Mathf.Max(time, 0.001f); // approx
	}

	static float SolveForAccel(float speed, float drag) {
		// Assumes PhysX drag formula: (this is exact)
		//   v' = (v + a*t) * (1 - drag*t)
		return speed * drag / (1 - drag*Time.fixedDeltaTime);
	}

	static float SolveForAirDrag(float gravity, float terminalVelocity) {
		// Inverse of SolveForAccel
		float ga = gravity / Mathf.Max(terminalVelocity, 0.0001f);
		return ga / (1 + ga*Time.fixedDeltaTime);
	}

	void FixedUpdate() {
		if (!canMove)
			return;

		GroundTest();

		if (onGround) {
			airTime = 0;
			groundTime += Time.deltaTime;
		} else {
			groundTime = 0;
			airTime += Time.deltaTime;
		}

		if (isRolling)
			rollTime += Time.deltaTime;
		else
			rollTime = 0;

		// Update physics parameters.
		float walkDrag = SolveForDrag(walkHalt);
		float rollDrag = SolveForDrag(rollHalt);

		if (isRolling) {
			body.drag = onGround ? rollDrag : 0.01f;
			body.angularDrag = turnDrag;
			accelValue = SolveForAccel(rollSpeed, body.drag);
		} else {
			body.drag = onGround ? walkDrag : 0.01f;
			body.angularDrag = turnDrag;
			accelValue = SolveForAccel(walkSpeed, body.drag);
		}

		// Apply our own gravity.
		if (!onGround || isRolling)
			body.AddForce(Physics.gravity, ForceMode.Acceleration);

		if (isRolling)
			UpdateRoll();

		UpdateInterest();

		// Default behaviour if no actions running.
		if (actions.Count == 0) {
			if (isRolling) {
				KeepRolling();
			} else {
				MoveToTarget(lastGoodPosition, 0.5f);
			}
		}
	}

	void UpdateRoll() {
		// If we can, exit the roll cleanly.
		if (rollState == RollState.Rolling && anim && anim.didCleanlyExitRoll) {
			rollState = RollState.None;
		}

		// If they get impeded, bring them to a halt.
		if (rollState == RollState.Rolling
			&& onGround
			&& (body.velocity.magnitude < rollExitSpeed || rollTime > rollMaxTime)
			&& rollTime > 0.1f)
			rollState = RollState.Stopping;

		// See if they've come to a halt yet.
		if (rollState == RollState.Stopping && onGround) {
			if (body.velocity.magnitude < 0.1f) {
				// TODO: Wait for animation to finish.
				rollState = RollState.None;
			}
		}
	}

	public void Roll(Vector3 kick) {
		rollState = RollState.Rolling;
		body.AddForce(kick, ForceMode.VelocityChange);
	}

	public void KeepRolling() {
		// Dummy movement just to keep rolling.
		Move(Vector3.zero);
		Turn(transform.forward);
	}

	void UpdateInterest() {
		interestCooldown = Mathf.Max(0, interestCooldown-Time.deltaTime);

		if (lookAtPlayer && interest && !Game.inCutscene) {
			Turn(interest.transform.position - transform.position);
		}
	}

	public void DropLoot() {
		if (loot > 0) {
			// TODO: possibly a saved cooldown timer to prevent farming?

			// Pick what to spawn.
			// TODO: do this dynamically based on what the player needs more of.
			Item item = Game.game.inventoryDatabase.Find("Coin");
			Game.DropObject(gameObject, item.gameObject, loot);

			loot = 0;
		}
	}

	public void Say(string text) {
		if (text == "") {
			if (speechBubble) {
				GameObject.Destroy(speechBubble.gameObject);
				speechBubble = null;
			}
		} else {
			if (!speechBubble) {
				GameObject g = GameObject.Instantiate(speechPrefab, UIWrangler.ui.worldText.transform);
				g.name = gameObject.name;
				speechBubble = g.GetComponent<TMPro.TextMeshProUGUI>();
			}

			speechBubble.text = text;
			speechTimer = 0.0f;
		}
	}

	public void OnFire() {
		if (carrying) {
			carrying.SendMessage("OnFire", SendMessageOptions.DontRequireReceiver);
		}
	}

	public void Use(Entity byWhom) {
		if (lootDropMode == Entity.LootDropMode.OnUse)
			DropLoot();
		SendMessage("OnUseObject", byWhom, SendMessageOptions.DontRequireReceiver);
		Game.UseEvent(byWhom.gameObject, gameObject);
	}

	void TrackSpeechBubble() {
		const float textTime = 3.0f;
		const float fadeTime = 0.5f;

		if (speechBubble) {
			speechTimer += UIWrangler.deltaTime;
			if (speechTimer < textTime)
				speechBubble.alpha = 1.0f;
			else
				speechBubble.alpha = 1 - Mathf.Clamp01((speechTimer-textTime)/fadeTime);

			Vector3 pos = transform.position;
			pos.y = coll ? coll.bounds.max.y : (transform.position.y + 1.0f);

			Camera cam = Game.camera.GetComponent<Camera>();
			speechBubble.transform.position = cam.WorldToScreenPoint(pos);

			if (speechTimer >= textTime+fadeTime)
				Say("");
		}
	}

	void TrackCamera() {
		GameCamera gc = Game.camera;
		bool wantsCamera = cameraTarget || isSpeaking || (pc && pc.enabled);

		// Don't follow the entities parented under the camera (e.g. narrator)
		if (gc.transform == transform.parent)
			wantsCamera = false;

		if (wantsCamera != isInCameraList) {
			isInCameraList = wantsCamera;

			gc.targets.Remove(transform);
			if (wantsCamera)
				gc.targets.Add(transform);
		}
	}

	void TrackSeen() {
		// Trigger an event once we detect someone.
		for (int i = 0; i<unnoticed.Count; i++) {
			Entity who = unnoticed[i];
			if (who && !who.isDisguised) {
				Debug.Log($"{gameObject.name} saw {who.name}.");
				unnoticed.RemoveAt(i--);
				Game.SeeEvent(gameObject, who.gameObject);
			}
		}
	}

	void Update() {
		TrackSeen();
	}

	void LateUpdate() {
		TrackSpeechBubble();
		TrackCamera();
	}

	public Entity FindUseTarget() {
		// Find the thing we're pointing at.
		Entity nearest = null;
		float bestAngle = 0.1f;
		for (int i = 0; i<usables.Count; i++) {
			Entity thing = usables[i];
			if (!thing) {
				// Skip destroyed entities.
				usables.RemoveAt(i);
				i--;
				continue;
			}

			Vector3 dir = (thing.transform.position - transform.position).normalized;
			float angle = Vector3.Dot(dir, transform.forward);
			if (angle > bestAngle) {
				bestAngle = angle;
				nearest = thing;
			}
		}

		return nearest;
	}	

	public PlayerControl playerApproached {
		get {
			for (int i = 0; i<users.Count; i++) {
				Entity thing = users[i];
				if (!thing) {
					// Skip destroyed entities.
					users.RemoveAt(i);
					i--;
					continue;
				}

				PlayerControl pc = thing.GetComponent<PlayerControl>();
				if (pc && pc.enabled)
					return pc;
			}

			return null;
		}
	}

	void OnUseEnter(Collider coll) {
		Entity target = coll.gameObject.GetComponent<Entity>();
		if (target) {
			target.usables.Add(this);
			users.Add(target);
		}
	}

	void OnUseExit(Collider coll) {
		Entity target = coll.gameObject.GetComponent<Entity>();
		if (target) {
			target.usables.Remove(this);
			users.Remove(target);
		}
	}

	void SetInterest(Entity who, float level) {
		interestCooldown = 1.0f;
		interestLevel = level;
		interest = who;
	}

	void OnVisionStay(Collider coll) {
		// See what it is.
		GameObject g = coll.gameObject;
		Entity target = g.GetComponent<Entity>();
		if (!target)
			return;

		// See how interesting it is.
		float newInterestLevel = 0;
		PlayerControl pc = g.GetComponent<PlayerControl>();
		if (pc && pc.enabled)
			newInterestLevel = 10.0f; // players are very interesting

		// If it's more interesting than our current target, switch.
		if (!interest || newInterestLevel > interestLevel && interestCooldown <= 0)
			SetInterest(target, newInterestLevel);
	}

	void OnVisionEnter(Collider coll) {
		Entity target = coll.gameObject.GetComponent<Entity>();
		if (!target)
			return;

		unnoticed.Add(target);
	}

	void OnVisionExit(Collider coll) {
		Entity target = coll.gameObject.GetComponent<Entity>();
		if (!target)
			return;

		if (interest && coll.gameObject == interest.gameObject)
			SetInterest(null, 0);

		unnoticed.Remove(target);
	}
}
