﻿using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;
#endif

[ExecuteInEditMode]
public class LevelEditor : MonoBehaviour {

	[HideInInspector]
	public GameObject dummyPlayer;
	[HideInInspector]
	public bool levelError;
	bool cleared = false;
	string lastErrorMessage = "";

	[BoolToButton("savePrefab")]
	public bool savePrefab = false;
	// Here for spacing rather than actual functionality
	[BoolToButton("")]
	public bool emptyBool = false;

	[BoolToButton("exportLevel")]
	public bool exportLevel = false;
	// Here for spacing rather than actual functionality
	[BoolToButton("")]
	public bool emptyBool2 = false;
	float autoSaveTimer = 0f;
	bool initialSave = false;
	[HideInInspector]
	public bool holdUpdate = true;

	string myLevelName = "";

	Terrain TerrainMain;
	int terrainCount;

	[HideInInspector]
	public List<Transform> objs;
	[HideInInspector]
	public List<Component> coreComponents;

	#if UNITY_EDITOR
	void Awake () {
		initialSave = true;
		exportLevel = false;
		holdUpdate = true;
		HideHierarchyElements ();
	}

	void Start () {
		exportLevel = false;
		if (EditorApplication.isPlayingOrWillChangePlaymode) {
			if (dummyPlayer) {
				dummyPlayer.SetActive (false);
			}
		} else if (dummyPlayer) {
			dummyPlayer.SetActive (true);
			holdUpdate = true;
		}
	}

	void Update () {
		if (EditorApplication.isPlayingOrWillChangePlaymode) {
			/*if (levelError) {
				Debug.LogError ("LEVEL ERROR: The level cannot be tested because there is something wrong with the level format. Check the error log in edit mode to see what's wrong.");
				Debug.DebugBreak ();
				Debug.Break ();
			}*/

			// Not doing anything here for now but not removing the code just yet

		} else {

			// check that all the default objects are as they should be
			if (CheckBasics () != "") {
				//ShowErrorMessage (CheckBasics ());
				return;
			} else {

				// Snap the terrain to the edges of the level
				SnapTerrainEdges ();

				// Snap the level to the origin
				SnapVals ();

				// delay saving and other updates
				if (holdUpdate) {
					holdUpdate = false;
					return;
				}

				// make the objects children of the main prefab
				AdoptChildren ();

				// remove empty transforms
				objs.RemoveAll(Transform => Transform == null);

				// Keep the gameObjects and Prefabs inside the level
				KeepTheKidsInside(transform);

			}

			//if (!levelError && !cleared) {
				//ClearConsole ();
				//cleared = true;
				//lastErrorMessage = "";
			//}
			
			//ErrorStop (levelError);
			//if (levelError)
				//return;

			autoSaveTimer += Time.deltaTime;

			if(autoSaveTimer > 10 || initialSave){
				SavePrefab (true);
				autoSaveTimer = 0f;
			}
			UnityEditor.EditorApplication.delayCall += () => { SavePrefab (savePrefab); };
			UnityEditor.EditorApplication.delayCall += () => { ExportLevelPackage (exportLevel); };
		}
	}

	void AdoptChildren () {
		// create an array of all the transforms in the Scene
		Transform[] allObjs = FindObjectsOfType<Transform> ();
		// cycle through all the transforms and set the Level Prefab as the parent object (not including Canvases and Cameras)
		foreach (Transform obj in allObjs)
			if (obj.parent == null && obj.tag != "Safe")
				obj.SetParent (transform);
	}

	string CheckBasics () {
		/*
		// get and set the components needed
		foreach (Component com in GetComponents<Component>) {
			bool needed = false;
			for (int i = 0; i < coreComponents.Count; i++) {
				if (com == coreComponents [i]) {
					needed = true;
					break;
				}
			}
			if (!needed) {
				UnityEditor.EditorApplication.delayCall += () => { DestroyImmediate (com); };
			}
		}
		*/

		// set start value for number of terrains
		terrainCount = 0;

		// cycle through the objects to find terrain
		foreach (Transform child in transform) {
			if (child.GetComponent<Terrain> ()) {

				// increase the terrain count
				terrainCount++;

				// make the frst terrain the main one
				if (!TerrainMain)
					TerrainMain = child.GetComponent<Terrain> ();

				// remove any additional terrains
				if (TerrainMain && TerrainMain != child.GetComponent<Terrain>()) {
					UnityEditor.EditorApplication.delayCall += () => { DestroyImmediate (child.gameObject); };
					terrainCount--;
				}

				// set the terrain count to 0 if there's no terrain
				if (!TerrainMain)
					terrainCount = 0;
			}

			// remove the object if it is a duplicate of the Level Prefab
			if (child.GetComponent<LevelEditor> ())
				UnityEditor.EditorApplication.delayCall += () => { DestroyImmediate (child.gameObject); };
			
		}

		// add a LootRandomizer if there is none
		if (!GetComponent<LootRandomizer> ())
			UnityEditor.EditorApplication.delayCall += () => { gameObject.AddComponent<LootRandomizer> (); };

		// display terrain error message
		if (terrainCount == 0)
			return "NO TERRAIN: There is no Terrain attached to the gameobject. Please add a unique Terrain from the Level Prefabs folder to the gameobject.";

		return "";
	}

	void KeepTheKidsInside (Transform parent) {
		foreach (Transform kid in parent) {

			// get the index of the object from the list, and if it's not in the list, add it
			int index = GetObjectIndex (kid);

			// determine the object type
			string objectType = CheckObjectComponents (kid);

			// contain all the objects in the level
			ContainLevelObjects (objs [index].GetComponent<Collider> ());

			// update the object based on whether or not it is inside the level
			if (objectType == "Tinker") {
				Bounds tinkerBounds = new Bounds (kid.position, Vector3.zero);
				foreach (Collider tinker in kid.GetComponentsInChildren<Collider>()) {
					
					if (tinker.GetComponent<Tinkercad> ())
						continue;
					
					if (!tinker.name.Contains ("group")) {
						ContainLevelObjects (tinker);
						continue;
					}

					tinkerBounds.Encapsulate (tinker.bounds);
				}
				Vector3 tinkerCenter = tinkerBounds.center - kid.transform.position;
				tinkerBounds.center = tinkerCenter;
				kid.GetComponent<BoxCollider> ().size = new Vector3 (tinkerBounds.size.x / kid.localScale.x, tinkerBounds.size.y / kid.localScale.y, tinkerBounds.size.z / kid.localScale.z);
				kid.GetComponent<BoxCollider> ().center = new Vector3 (tinkerBounds.center.x / kid.localScale.x, tinkerBounds.center.y / kid.localScale.y, tinkerBounds.center.z / kid.localScale.z);
			}

			// if the object is not a special type, cycle through its children
			if (objectType == "" && kid.childCount > 0)
				KeepTheKidsInside (kid);
		}
	}

	void ContainLevelObjects (Collider myCollider) {
		if (!myCollider)
			return;
		Vector3 corner1 = new Vector3 (myCollider.bounds.max.x, myCollider.bounds.max.y, myCollider.bounds.min.z);
		Vector3 corner2 = new Vector3 (myCollider.bounds.max.x, myCollider.bounds.min.y, myCollider.bounds.min.z);
		Vector3 corner3 = new Vector3 (myCollider.bounds.max.x, myCollider.bounds.min.y, myCollider.bounds.max.z);
		Vector3 corner4 = new Vector3 (myCollider.bounds.min.x, myCollider.bounds.max.y, myCollider.bounds.max.z);
		Vector3 corner5 = new Vector3 (myCollider.bounds.min.x, myCollider.bounds.max.y, myCollider.bounds.min.z);
		Vector3 corner6 = new Vector3 (myCollider.bounds.min.x, myCollider.bounds.min.y, myCollider.bounds.max.z);

		Vector3 plusX = (myCollider.bounds.max + corner2) / 2f;
		Vector3 negX = (myCollider.bounds.min + corner4) / 2f;

		Vector3 plusY = (myCollider.bounds.max + corner5) / 2f;
		Vector3 negY = (myCollider.bounds.min + corner3) / 2f;

		Vector3 plusZ = (myCollider.bounds.max + corner6) / 2f;
		Vector3 negZ = (myCollider.bounds.min + corner1) / 2f;

		//DrawDebugBounds (myCollider, corner1, corner2, corner3, corner4, corner5, corner6, plusX, plusY, plusZ, negX, negY, negZ);

		// determine how far the outer bounds is outside the level
		float plusOutsideZ = 190f - plusZ.z;
		float negOutsideZ = -210f - negZ.z;
		float plusOutsideX = 190f - plusX.x;
		float negOutsideX = -210f - negX.x;
		float plusOutsideY = 200f - plusY.y;
		float negOutsideY = -200f - negY.y;

		// make the position inside the level if it is outside
		if (plusOutsideZ < 0f) {
			myCollider.transform.position += new Vector3 (0f, 0f, plusOutsideZ);
		} else if (negOutsideZ > 0f) {
			myCollider.transform.position += new Vector3 (0f, 0f, negOutsideZ);
		}
		if (plusOutsideX < 0f) {
			myCollider.transform.position += new Vector3 (plusOutsideX, 0f, 0f);
		} else if (negOutsideX > 0f) {
			myCollider.transform.position += new Vector3 (negOutsideX, 0f, 0f);
		}
		if (plusOutsideY < 0f) {
			myCollider.transform.position += new Vector3 (0f, plusOutsideY, 0f);
		} else if (negOutsideY > 0f) {
			myCollider.transform.position += new Vector3 (0f, negOutsideY, 0f);
		}

		// determine the largest possible scale for each axis
		Vector3 adjustedScale = new Vector3 (myCollider.transform.localScale.x/myCollider.bounds.size.x,myCollider.transform.localScale.y/myCollider.bounds.size.y, myCollider.transform.localScale.z/myCollider.bounds.size.z);

		// adjust the scale accordingly
		if (myCollider.bounds.size.x > 400f) {
			myCollider.transform.localScale = new Vector3 (adjustedScale.x * 400, myCollider.transform.localScale.y, myCollider.transform.localScale.z);
		}
		if (myCollider.bounds.size.y > 400f) {
			myCollider.transform.localScale = new Vector3 (myCollider.transform.localScale.x, adjustedScale.y * 400, myCollider.transform.localScale.z);
		}
		if (myCollider.bounds.size.z > 400f) {
			myCollider.transform.localScale = new Vector3 (myCollider.transform.localScale.x, myCollider.transform.localScale.y, adjustedScale.z * 400);
		}

	}

	int GetObjectIndex (Transform levelObj) {
		int checkInt = -1;
		if (objs.Count > 0) {
			for (int i = 0; i < objs.Count; i++) {
				if (levelObj == objs[i]) {
					checkInt = i;
					break;
				}
			}
		}
		if (checkInt == -1) {
			objs.Add (levelObj);
			return (GetObjectIndex (levelObj));
		}
		return checkInt;
	}

	// determine the type of object
	string CheckObjectComponents (Transform check) {
		if (check == dummyPlayer.transform)
			return "Empty";
		else if (check.GetComponent<Tinkercad> ()) {
			if (!check.GetComponent<Collider> ()) {
				check.gameObject.AddComponent<BoxCollider> ();
				UnityEditor.EditorApplication.delayCall += () => { check.GetComponent<Collider> ().isTrigger = true; };
			}
			return "Tinker";
		} else if (check.GetComponent<PotionScript> ())
			return "Potion";
		else if (check.GetComponent<LootChestScript> ())
			return "Loot";
		else if (check.GetComponent<DiscoBall> ())
			return "Disco";
		else if (check.GetComponent<Collider> ())
			return "";
		else if (check.GetComponent<MeshRenderer> () && !check.GetComponent<Collider> ()) {
			UnityEditor.EditorApplication.delayCall += () => { check.gameObject.AddComponent<MeshCollider> (); };
			return "";
		} else
			return "Empty";

	}

	bool CanUpdate (int objIndex, string objType) {

		if (objType == "Empty")
			return false;

		// Tinkercad objects have Colliders on all their child game objects
		if (objType == "Tinker") {
			foreach (Transform tinker in objs[objIndex]) {

			}
			return true;

			// Disco Balls only have Colliders on two of their children, the rest can be ignored
		} else if (objType == "Disco") {
			foreach (Transform disco in objs[objIndex]) {
				if (disco.GetComponent<Collider> ()) {

				}
			}
			return true;

			// treat Potions and Loot Chests the same as other objects, they are only labled so they don't loop through their children
		} else if (objType == "Potion" || objType == "Loot" || objType == "") {
			return true;
		}

		return true;
	}

	void SavePrefab (bool save) {
		if (initialSave) {
			myLevelName = gameObject.name;
			initialSave = false;
		}

		if (myLevelName != gameObject.name) {
			gameObject.name = myLevelName;
		}

		if (gameObject.activeInHierarchy && save) {
			Object newLevelPrefab = PrefabUtility.CreateEmptyPrefab ("Assets/Levels/" + gameObject.name + ".prefab");
			PrefabUtility.ReplacePrefab (gameObject, newLevelPrefab, ReplacePrefabOptions.ConnectToPrefab);
			EditorSceneManager.SaveScene (EditorSceneManager.GetActiveScene ());
		}
		savePrefab = false;
		HideHierarchyElements ();
	}

	void ExportLevelPackage (bool export) {
		if (gameObject.activeInHierarchy && export) {
			SavePrefab (true);
			Application.OpenURL ("https://arcade.blackrocket.com/add-an-asset/");
			string filename = gameObject.name.Substring (0, gameObject.name.Length - 7) + ".unitypackage";
			AssetDatabase.ExportPackage ("Assets/Levels/" + gameObject.name + ".prefab", "Asset Packages/" + filename, ExportPackageOptions.IncludeDependencies | ExportPackageOptions.Interactive);
		}
		exportLevel = false;
	}

	void HideHierarchyElements () {
		GameObject canvases = GameObject.FindGameObjectWithTag ("Safe");
		canvases.hideFlags = HideFlags.HideInHierarchy;
	}

	void SnapTerrainEdges () {

		// Get the terrain heightmap width and height
		int xRes = TerrainMain.terrainData.heightmapWidth;
		int yRes = TerrainMain.terrainData.heightmapHeight;

		// GetHeights = gets the heightmap points of the terrain. Store those values in a float array
		float [,] heights = TerrainMain.terrainData.GetHeights(0, 0, xRes, yRes);

		// Manipulate the height data
		for (int i = 0; i < 257; i++) {
			for (int j = 0; j < 11; j++) {
				heights [0 + j, i] = 0.5f;
				heights [256 - j, i] = 0.5f;
				heights [i, 0 + j] = 0.5f;
				heights [i, 256 - j] = 0.5f;
			}
		}

		// SetHeights to change the terrain heightdata
		TerrainMain.terrainData.SetHeights(0, 0, heights);
	}

	/*
	void ShowErrorMessage (string error) {
		if (error != lastErrorMessage) {
			lastErrorMessage = error;
			levelError = true;
			cleared = false;
			Debug.LogError (error);
			Debug.DebugBreak ();
		} else if (error == lastErrorMessage) {
			levelError = true;
			cleared = false;
			Debug.DebugBreak ();
		}
	}
	*/

	void LockAndHide () {
		Tools.lockedLayers = (1 << LayerMask.NameToLayer ("UI")) |
			(1 << LayerMask.NameToLayer ("Camera")) |
			(1 << LayerMask.NameToLayer ("Level Editor")) |
			(1 << LayerMask.NameToLayer ("MiniMap")) |
			(1 << LayerMask.NameToLayer ("UI Cameras")) |
			(1 << LayerMask.NameToLayer ("BigMap")) |
			(1 << LayerMask.NameToLayer ("HideOnCamera")) |
			(1 << LayerMask.NameToLayer ("OuterBounds"));
		Tools.visibleLayers = ~((1 << LayerMask.NameToLayer ("UI")) |
			(1 << LayerMask.NameToLayer ("Camera")) |
			(1 << LayerMask.NameToLayer ("Level Editor")) |
			(1 << LayerMask.NameToLayer ("MiniMap")) |
			(1 << LayerMask.NameToLayer ("UI Cameras")) |
			(1 << LayerMask.NameToLayer ("BigMap")) |
			(1 << LayerMask.NameToLayer ("HideOnCamera")) |
			(1 << LayerMask.NameToLayer ("OuterBounds")));
	}

	void SnapVals () {
		// Level Prefab Defaults
		transform.position = new Vector3(-10, 0, -10);
		transform.eulerAngles = Vector3.zero;
		transform.localScale = Vector3.one;

		// Terrain Defaults
		TerrainMain.transform.localPosition = Vector3.one * -200;
		TerrainMain.transform.eulerAngles = Vector3.zero;
		TerrainMain.transform.localScale = Vector3.one;

		// Dummy Defaults
		dummyPlayer.transform.localPosition = Vector3.zero;
		dummyPlayer.transform.eulerAngles = Vector3.zero;
		dummyPlayer.transform.localScale = Vector3.one;

	}

	void DrawDebugBounds (Collider myCollider,
		Vector3 corner1, Vector3 corner2, Vector3 corner3, Vector3 corner4, Vector3 corner5, Vector3 corner6,
		Vector3 plusX, Vector3 plusY, Vector3 plusZ, Vector3 negX, Vector3 negY, Vector3 negZ) {

		Debug.DrawLine (myCollider.bounds.max, corner1, Color.blue);
		Debug.DrawLine (corner2, corner3, Color.green);
		Debug.DrawLine (myCollider.bounds.max, corner3, Color.blue);
		Debug.DrawLine (corner1, corner2, Color.yellow);

		Debug.DrawLine (corner4, corner5, new Color32 (228, 114, 37, 255));
		Debug.DrawLine (corner3, corner6, Color.green);
		Debug.DrawLine (corner1, corner5, new Color32 (228, 114, 37, 255));
		Debug.DrawLine (corner2, myCollider.bounds.min, Color.red);

		Debug.DrawLine (myCollider.bounds.max, corner4, Color.blue);
		Debug.DrawLine (corner4, corner6, Color.yellow);
		Debug.DrawLine (corner5, myCollider.bounds.min, Color.red);
		Debug.DrawLine (corner6, myCollider.bounds.min, Color.red);

		Debug.DrawLine (plusX, negX, Color.magenta);
		Debug.DrawLine (plusY, negY, Color.magenta);
		Debug.DrawLine (plusZ, negZ, Color.magenta);
	}

	/*
	public static void ClearConsole () {
		var assembly = Assembly.GetAssembly (typeof(SceneView));
		var type = assembly.GetType ("UnityEditor.LogEntries");
		var method = type.GetMethod ("Clear");
		method.Invoke (new object (), null);
	}

	void ErrorStop (bool error) {
		Undo.RecordObject (this, "Set Value");
		this.levelError = error;
		EditorUtility.SetDirty (this);
	}
	*/

	#else 
	void Start () {
		if (dummyPlayer) {
			dummyPlayer.SetActive (false);
		}
	}
	#endif
}
