using System.Collections;
using System.Collections.Generic;
using UnityEngine;

using Opcode = ScriptData.Opcode;

public class ScriptVM
{
	public enum Status { Waiting, Ended };

	public class Thread
	{
		public ScriptData.Function func;
		public int pc = 0;
		public EntityBehaviour action;
		public bool dead = false;
		public bool background = false;
		public Stack<object> stack = new Stack<object>();

		public T WaitFor<T>(GameObject owner) where T : EntityBehaviour {
			System.Type type = typeof(T);
			Entity ent = owner.GetComponent<Entity>();
			if (!ent)
				Debug.LogError($"{owner.name} is not an entity.", owner);

			T comp = ent.StartAction<T>();
			action = comp;
			return comp;
		}

		public void Kill() {
			if (action) {
				action.StopAction();
				action = null;
			}
			dead = true;
		}

		public byte Byte() {
			return func.bytes[pc++];
		}

		public ushort Word() {
			byte lo = Byte();
			byte hi = Byte();
			return (ushort)((hi << 8) + lo);
		}

		public int Int() {
			byte lo = Byte();
			byte m1 = Byte();
			byte m2 = Byte();
			byte hi = Byte();
			return (int)((hi << 24) + (m2 << 16) + (m1 << 8) + lo);
		}

		public float Number() {
			return Int() / 65536.0f;
		}
	}

	ScriptData data;
	object[] globals;
	List<Thread> threads = new List<Thread>();
	Dictionary<GameObject, ushort> globalLookup = new Dictionary<GameObject, ushort>();
	GameObject[] prefabs;

	public Thread Spawn(ScriptData.Function func) {
		Thread thread = new Thread {
			func = func
		};

		return thread;
	}

	public void KillAll(ScriptData.Function func) {
		foreach (Thread thread in threads)
			if (thread.func == func)
				thread.Kill();
	}

	void SetGlobalObject(ushort i, GameObject value) {
		globals[i] = value;
		globalLookup[value] = i;
	}

	void ClearGlobalObject(ushort i) {
		GameObject g = globals[i] as GameObject;
		globalLookup.Remove(g);
		globals[i] = null;
	}

	public void Bind(GameObject oldObj, GameObject newObj) {
		if (!globalLookup.ContainsKey(oldObj))
			Debug.LogError($"No object '{oldObj.name}' to re-bind!", oldObj);
		ushort i = globalLookup[oldObj];
		globals[i] = newObj;
		globalLookup[newObj] = (ushort)i;
	}

	public ScriptVM(ScriptData data) {
		this.data = data;
		globals = new object[data.symbols.Count];
		prefabs = new GameObject[data.symbols.Count];

		for (int i = 0; i<data.symbols.Count; i++) {
			ScriptData.Symbol sym = data.symbols[i];

			if (sym.storage.HasFlag(ScriptData.StorageFlags.Dynamic)) {
				GameObject prefab = Resources.Load<GameObject>(sym.name);
				if (!prefab)
					Debug.LogError($"No prefab '{sym.name}' found in project.");
				prefabs[i] = prefab;
			}

			if (sym.storage.HasFlag(ScriptData.StorageFlags.External)) {
				switch (sym.type) {
					case "GameObject":
						GameObject g = GameObject.Find(sym.name);
						if (g)
							SetGlobalObject((ushort)i, g);
						else if (!prefabs[i])
							Debug.LogError($"No entity '{sym.name}' found in scene.");
						break;
					case "Variable":
						if (!SaveGame.current.vars.ContainsKey(sym.name))
							Debug.LogError($"No saved variable '{sym.name}' was declared.");
						break;
					case "Item":
						Item item = Game.game.inventoryDatabase.Find(sym.name);
						if (item == null)
							Debug.LogError($"No item '{sym.name}' found in project.");
						globals[i] = item;
						break;
					default:
						Debug.LogError($"Unknown symbol type '{sym.type}'.");
						break;
				}
			}
		}
	}

	public Thread MatchCondition(Opcode verb, GameObject subject, GameObject obj = null) {
		ushort id_subj = 0, id_obj = 0;
		if (subject)
			globalLookup.TryGetValue(subject, out id_subj);
		if (obj)
			globalLookup.TryGetValue(obj, out id_obj);
		foreach (ScriptData.Condition cond in data.conditions) {
			if (cond.verb == verb && cond.subject == id_subj && cond.obj == id_obj) {
				Thread thread = Spawn(data.functions[cond.function]);
				if (Run(thread) != Status.Ended)
					return thread;
				else
					return null;
			}
		}
		return null;
	}

	public void RunBackground() {
		foreach (Thread thread in threads)
			Run(thread);

		// Clean up the dead.
		for (int i = 0; i<threads.Count;) {
			if (threads[i].dead)
				threads.RemoveAt(i);
			else
				i++;
		}
	}

	public Status Run(Thread thread) {
		GameObject who, where;
		Item item;
		ScriptData.Function func;
		string text, name;
		ushort a, b;
		float value;
		bool flag;
		ScriptData.DlgFlags flags;

		if (thread.dead)
			return Status.Ended;

		// If we're doing an action already, wait for it to finish.
		if (thread.action) {
			if (!thread.action.finished)
				return Status.Waiting;
			thread.action = null;
		}

		int timeout = 1000;
		do {
			Opcode op = (Opcode)thread.Byte();
			switch (op) {
				default:
					throw new System.Exception($"Invalid opcode '{op}'.");

				case Opcode.DefFn:
					a = thread.Word();
					b = thread.Word();
					object obj = data.functions[b];
					globals[a] = obj;
					break;

				case Opcode.DefSaved:
					a = thread.Word();
					name = data.symbols[a].name;
					if (!SaveGame.current.vars.ContainsKey(name))
						SaveGame.current.vars[name] = 0.0f;
					break;

				case Opcode.DefLocal:
					a = thread.Word();
					globals[a] = 0.0f;
					break;

				case Opcode.SetVar:
					a = thread.Word();
					name = data.symbols[a].name;
					if (globals[a] is null) {
						SaveGame.current.vars[name] = 1.0f;
						Debug.Log($"Global '{name}' became set.");
					} else {
						globals[a] = 1.0f;
						Debug.Log($"Local '{name}' became set.");
					}
					break;

				case Opcode.ClearVar:
					a = thread.Word();
					name = data.symbols[a].name;
					if (globals[a] is null) {
						SaveGame.current.vars[name] = 0.0f;
						Debug.Log($"Global '{name}' was cleared.");
					} else {
						globals[a] = 0.0f;
						Debug.Log($"Local '{name}' was cleared.");
					}
					break;

				case Opcode.PushVar:
					a = thread.Word();
					if (globals[a] is null) {
						name = data.symbols[a].name;
						thread.stack.Push(SaveGame.current.vars[name]);
					} else {
						thread.stack.Push(globals[a]);
					}
					break;

				case Opcode.Return:
					thread.dead = true;
					return Status.Ended;

				case Opcode.Jump:
					thread.pc = thread.Word();
					break;

				case Opcode.JFalse:
					value = (float)thread.stack.Pop();
					a = thread.Word();
					if (value == 0)
						thread.pc = a;
					break;

				case Opcode.Not:
					value = (float)thread.stack.Pop();
					thread.stack.Push(value == 0 ? 1.0f : 0.0f);
					break;

				case Opcode.Says:
					who = globals[thread.Word()] as GameObject;
					text = data.strings[thread.Word()];
					flags = (ScriptData.DlgFlags)thread.Byte();
					if (flags != ScriptData.DlgFlags.None)
						Debug.Log($"{who.name} says <{flags}>: {text}");
					else
						Debug.Log($"{who.name} says: {text}");
					bool caption = flags.HasFlag(ScriptData.DlgFlags.Caption) || thread.background;
					Game.SayDialogue(who, text, caption);
					if (caption)
						break;
					return Status.Waiting;

				case Opcode.WalksTo:
				case Opcode.HeadsFor:
					who = globals[thread.Word()] as GameObject;
					where = globals[thread.Word()] as GameObject;
					Debug.Log($"{who.name} walks to: {where.name}");
					WalkToPoint walk = thread.WaitFor<WalkToPoint>(who);
					walk.target = where.transform.position;
					if (op == Opcode.HeadsFor) {
						thread.action = null;
						break;
					} else {
						return Status.Waiting;
					}

				case Opcode.Waits:
					who = globals[thread.Word()] as GameObject;
					value = thread.Number();
					Debug.Log($"{who.name} waits {value}");
					WaitDuration wait = thread.WaitFor<WaitDuration>(who);
					wait.timer = value;
					return Status.Waiting;

				case Opcode.Exits:
					a = thread.Word();
					who = globals[a] as GameObject;
					Debug.Log($"{who.name} exits");
					GameObject.Destroy(who);
					ClearGlobalObject(a);
					break;

				case Opcode.Drops:
					who = globals[thread.Word()] as GameObject;
					value = thread.Number();
					item = globals[thread.Word()] as Item;
					Game.DropObject(who, item.gameObject, Mathf.RoundToInt(value));
					break;

				case Opcode.Receives:
					who = globals[thread.Word()] as GameObject;
					value = thread.Number();
					item = globals[thread.Word()] as Item;
					Game.GiveObject(who, item, Mathf.RoundToInt(value));
					break;

				case Opcode.Starts:
					func = globals[thread.Word()] as ScriptData.Function;
					Thread th = Spawn(func);
					th.background = true;
					threads.Add(th);
					break;

				case Opcode.Stops:
					func = globals[thread.Word()] as ScriptData.Function;
					KillAll(func);
					break;

				case Opcode.StaysFocused:
					who = globals[thread.Word()] as GameObject;
					Game.FollowEntity(who, true);
					break;

				case Opcode.LosesFocus:
					who = globals[thread.Word()] as GameObject;
					Game.FollowEntity(who, false);
					break;

				case Opcode.EntersVia:
					a = thread.Word();
					where = globals[thread.Word()] as GameObject;
					Debug.Log($"{data.symbols[a].name} enters via {where.name}.");
					SetGlobalObject(a, Game.Spawn(prefabs[a], where));
					break;

				case Opcode.Enters:
					a = thread.Word();
					name = (SaveGame.current.checkPoint == "") ? "DevSpawn" : SaveGame.current.checkPoint;
					where = GameObject.Find(name);
					if (!where)
						Debug.LogError($"No such entrance '{name}' defined in scene.");
					Debug.Log($"{data.symbols[a].name} enters via {where.name}.");
					SetGlobalObject(a, Game.Spawn(prefabs[a], where));
					break;

				case Opcode.Opens:
					who = globals[thread.Word()] as GameObject;
					Debug.Log($"{who.name} opens.");
					Game.ChangeDoor(who, true);
					break;

				case Opcode.Closes:
					who = globals[thread.Word()] as GameObject;
					Debug.Log($"{who.name} closes.");
					Game.ChangeDoor(who, false);
					break;

				case Opcode.Has:
					who = thread.stack.Pop() as GameObject;
					value = thread.Number();
					item = globals[thread.Word()] as Item;
					flag = Game.CheckInventory(who, item, Mathf.RoundToInt(value));
					thread.stack.Push(flag ? 1.0f : 0.0f);
					break;
			}
		} while (--timeout > 0);

		throw new System.Exception("Script timeout.");
	}
}
