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

public class Game : MonoBehaviour
{
	public ScriptData script;
	public InventoryDatabase inventoryDatabase;
	public bool outdoors = false;
	[Range(0, 360)] public float indoorAngle = 0;
	public SaveGame save;

	public static Game game { get; private set; }
	public static new GameCamera camera { get; private set; }
	public static bool inCutscene => running != null;
	public static float physInterp { get; private set; }
	public static bool saveRequested { get; set; }
	public static GameObject speaker => ui.dialogueBox.speaker;

	static ScriptVM vm;
	static ScriptVM.Thread running;
	static UIWrangler ui;

	static List<GameObject> toActivate = new List<GameObject>();

	Rigidbody dummyBody;
	bool dummyToggle;
	Coroutine leaving;

	[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
	static void Startup() {
		SceneManager.sceneLoaded += OnSceneLoaded;

		StickyKeys.Allow(false);
		Application.quitting += () => { StickyKeys.Allow(true); };

		// TODO: menu system for managing saves.
		int slot = 0;
		if (SaveGame.Exists(slot)) {
			SaveGame.current = SaveGame.Load(slot);
		} else {
			SaveGame.current = SaveGame.New(slot);
			SaveGame.current.scenePath = "Assets/Scenes/Tulip/Florin's House.unity";
			saveRequested = true;
		}

#if UNITY_EDITOR
		// We're not going to the scene in the savegame, we're
		// going to whatever scene we had loaded in the editor.
		// So clear out any checkpoints we might have been
		// thinking about because they won't apply here.
		SaveGame.current.checkPoint = "";
#else
		// Start loading whatever scene we need.
		Debug.Log($"Loading initial scene {SaveGame.current.scenePath}.");
		SceneManager.LoadScene(SaveGame.current.scenePath);
#endif
	}

	static void OnSceneLoaded(Scene scene, LoadSceneMode mode) {
		// See if they had the master scene open in the editor.
		bool isMasterSceneLoading = false;
		for (int i = 0; i<SceneManager.sceneCount; i++) {
			Scene other = SceneManager.GetSceneAt(i);
			if (other.name == "Master")
				isMasterSceneLoading = true;
		}

		Scene master = SceneManager.GetSceneByName("Master");
		if (master.isLoaded) {
			// Master scene is ready, enable everything we paused.
			foreach (GameObject g in toActivate)
				g.SetActive(true);
			toActivate.Clear();
		} else {
			// Disable everything until the master scene's loaded too.
			foreach (GameObject g in scene.GetRootGameObjects()) {
				if (g.activeSelf) {
					g.SetActive(false);
					toActivate.Add(g);
				}
			}

			// Get the master scene loading.
			if (!isMasterSceneLoading)
				SceneManager.LoadScene("Master", LoadSceneMode.Additive);
		}
	}

	void Awake() {
		UnityEngine.Assertions.Assert.IsNull(game, "Game is already created!");
		game = this;
	}

	void Start() {
		Debug.Log($"Game startup ({gameObject.scene.name}).");
		save = SaveGame.current;
		
		// Fixup scene path in case of launching from editor etc.
		save.scenePath = SceneManager.GetActiveScene().path;

		// Clear out all per-scene items.
		foreach (Item item in Game.game.inventoryDatabase.items) {
			if (item.sceneLocal)
				save.inventory.Remove(item.name);
		}

		// Give them the default initial items if starting anew.
		if (save.inventory.Count == 0) {
			foreach (Item item in Game.game.inventoryDatabase.items) {
				if (item.startFull)
					save.inventory[item.name] = new Inventory { count = item.defaultLimit, capacity = item.defaultLimit };
			}
		}

		// Deactivate all cameras to stop them rendering stuff.
		foreach (Camera old in GameObject.FindObjectsOfType<Camera>()) {
			if (old.gameObject.layer != LayerMask.NameToLayer("UI"))
				old.enabled = false;
		}

		string cameraName = outdoors ? "Outdoor Camera" : "Indoor Camera";

		// Create a camera.
		GameObject cameraPrefab = Resources.Load<GameObject>(cameraName);
		GameObject newcam = GameObject.Instantiate(cameraPrefab);
		newcam.name = "Game Camera";
		newcam.tag = "MainCamera";
		newcam.AddComponent<AudioListener>();
		camera = newcam.GetComponent<GameCamera>();

		// Locate our UI objects.
		ui = UIWrangler.ui;

		// Add the dummy rigid body.
		GameObject tmp = new GameObject("Dummy");
		dummyBody = tmp.AddComponent<Rigidbody>();
		dummyBody.interpolation = RigidbodyInterpolation.Interpolate;
		dummyBody.useGravity = false;
		dummyBody.isKinematic = true;
		dummyBody.drag = 0;
		dummyBody.angularDrag = 0;
		dummyBody.detectCollisions = false;
		dummyBody.freezeRotation = true;
		dummyToggle = false;

		UIWrangler.ui.fadeToBlack.CrossFadeAlpha(0.0f, 1.0f, true);

		// Kick off the startup script.
		vm = new ScriptVM(script);
		running = vm.Spawn(script.functions[0]);
		vm.Run(running);
	}

	void FixedUpdate() {
		if (dummyToggle)
			dummyBody.MovePosition(new Vector3(0, 0, 0));
		else
			dummyBody.MovePosition(new Vector3(1, 0, 0));
		dummyToggle = !dummyToggle;
	}

	void Update()
    {
		// Read off the physics timestep interpolation value.
		// This is (AFAIK) the only way to do this reliably.
		physInterp = dummyToggle ? dummyBody.transform.position.x : 1 - dummyBody.transform.position.x;

		ui.cinematicBars.shown = inCutscene;
		ui.hud.shown = !inCutscene && !ui.gamePaused;

		// If the UI's up, pause everything else.
		if (ui.gamePaused) {
			if (ui.inGameMenu.shown && Input.GetButtonDown("Menu"))
				ui.inGameMenu.shown = false;

			Time.timeScale = 0.0f;
			return;
		}

		if (Input.GetButtonDown("Menu"))
			ui.inGameMenu.shown = true;
			
		if (running != null) {
			if (vm.Run(running) == ScriptVM.Status.Ended)
				running = null;
		}

		vm.RunBackground();

		// Save once any script's finished.
		if (saveRequested && running == null) {
			SaveGame.current.Save();
			saveRequested = false;
		}

		// Restore the timescale.
		// We do this after running the script in case
		// the script executes new dialogue right after
		// finishing the old one. (avoids seeing the
		// game move forward for one frame)
		if (!ui.gamePaused)
			Time.timeScale = 1.0f;
	}

	public static void UseEvent(GameObject who, GameObject what) {
		if (running == null)
			running = vm.MatchCondition(ScriptData.Opcode.Uses, who, what);
	}

	public static void SeeEvent(GameObject who, GameObject what) {
		if (running == null)
			running = vm.MatchCondition(ScriptData.Opcode.Sees, who, what);
	}

	public static void TootEvent(GameObject who, GameObject what) {
		if (running == null)
			running = vm.MatchCondition(ScriptData.Opcode.Toots, who, what);
	}

	public static void ExitEvent(GameObject who) {
		if (running == null)
			running = vm.MatchCondition(ScriptData.Opcode.Exits, who, null);
	}

	public static void SayDialogue(GameObject who, string text, bool background) {
		if (background) {
			Entity ent = who.GetComponent<Entity>();
			if (!ent)
				Debug.LogError($"Object {who.name} is not an entity.");
			ent.Say(text);
		} else {
			// TODO: use owner in some way? color? display name/icon?
			ui.dialogueBox.Say(text, who);
		}
	}

	public static void FollowEntity(GameObject who, bool follow) {
		Entity ent = who.GetComponent<Entity>();
		if (!ent)
			Debug.LogError($"Object {who.name} is not an entity.");
		ent.cameraTarget = follow;
	}

	public static bool CheckInventory(GameObject who, Item item, int required) {
		Entity entity = who.GetComponent<Entity>();
		if (!entity)
			Debug.LogError($"Object '{who.name}' is not an entity and cannot hold items.");
		int has = entity.GetItemCount(item);
		return has >= required;
	}

	public static void GiveObject(GameObject who, Item item, int value) {
		Entity entity = who.GetComponent<Entity>();
		if (!entity)
			Debug.LogError($"Object '{who.name}' is not an entity and cannot hold items.");
		entity.Award(item, value);
	}

	public static GameObject DropObject(GameObject where, GameObject what, int value=0) {
		// Picks a suitable place vaguely near the top of the object.
		Collider coll = where.GetComponentInChildren<Collider>();
		Bounds bounds = new Bounds(where.transform.position + Vector3.one, Vector3.zero);
		if (coll)
			bounds = coll.bounds;

		Vector3 pos = new Vector3(
			bounds.center.x,
			Mathf.Lerp(bounds.min.y, bounds.max.y, 0.7f),
			bounds.center.z);

		GameObject obj = GameObject.Instantiate(what);
		obj.name = what.name;
		obj.transform.position = pos;
		if (value > 0) {
			Pickup pickup = obj.GetComponent<Pickup>();
			if (!pickup)
				Debug.LogError($"Object '{what.name}' is not a pickup.");
			pickup.Spawn(value);
		}
		return obj;
	}

	public static void ChangeDoor(GameObject who, bool state) {
		Door door = who.GetComponent<Door>();
		if (!door)
			Debug.LogError($"Object '{who.name}' is not a door.");
		door.open = state;
	}

	public static GameObject Spawn(GameObject prefab, GameObject where) {
		Vector3 pos = where.transform.position;

		// Spawn them just outside of any door they came through.
		PlayerControl pc = prefab.GetComponent<PlayerControl>();
		if (pc && pc.enabled) {
			if (where.GetComponent<ExitPortal>())
				pos += where.transform.forward;
		}

		GameObject g = GameObject.Instantiate(prefab, pos, where.transform.rotation);
		g.name = prefab.name;
		return g;
	}

	public static void Bind(GameObject oldObj, GameObject newObj) {
		vm.Bind(oldObj, newObj);
	}

	public static void GotoScene(string scenePath, string door) {
		if (game.leaving == null)
			game.leaving = game.StartCoroutine(game.LeavingScene(scenePath, door));
	}

	IEnumerator LeavingScene(string scenePath, string checkPoint) {
		float fadeOut = 0.5f;
		UIWrangler.ui.fadeToBlack.CrossFadeAlpha(1.0f, fadeOut, true);
		yield return new WaitForSecondsRealtime(fadeOut);

		save.scenePath = scenePath;
		save.checkPoint = checkPoint;
		save.Save();

		Debug.Log($"Player exited for {scenePath} arriving via {checkPoint}.");
		SceneManager.LoadScene(scenePath);
	}
}
