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

[ExecuteInEditMode]
public class LevelEditor : MonoBehaviour {

	[HideInInspector]
	public GameObject dummyPlayer;
	[HideInInspector]
	public bool hideToggle = true;
	[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;
	float autoSaveTimer = 0f;
	bool initialSave = false;

	const string randomChars = "BbCcDdFfGgHhJjKkLlMmNnPpQqRrSsTtVvWwXxYyZz0123456789";
	string additionalChars = "";

	Terrain TerrainMain;
	BoxCollider cube, topEdge, bottomEdge, leftEdge, rightEdge, frontEdge, backEdge;
	List<BoxCollider> edges;
	List<Transform> objs;

	#if UNITY_EDITOR
	void Awake () {
		initialSave = true;
	}

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

		if (hideToggle) {
			LockAndHide ();
		}
	}

	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 ();
			}
		} else {

			Transform[] allObjs = FindObjectsOfType<Transform> ();
			foreach (Transform obj in allObjs) {
				if (obj.parent == null && obj.tag != "Safe") {
					obj.SetParent (transform);
				}
			}
			if (GetEverything () != "") {
				ShowErrorMessage (GetEverything ());
				return;
			} else {
				// Hide the cube bounds colliders
				HideBounds ();

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

				// Find all the collider objects in the scene and assign them
				objs = new List<Transform>();
				objs.Clear();
				if (CheckObjectLocation (transform))
					CheckLevelBounds ();
				else
					return;

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

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

			autoSaveTimer += Time.deltaTime;

			if(autoSaveTimer > 10 || initialSave){
				SavePrefab (true);
				autoSaveTimer = 0f;
			}
			SavePrefab (savePrefab);
		}
	}

	void SavePrefab (bool save) {

		if (initialSave) {
			for (int i = 0; i < 6; i++) {
				additionalChars += randomChars [Random.Range (0, randomChars.Length)];
			}
			initialSave = false;
		}

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

	string GetEverything () {
		
		// Get the terrain and bounds box (cube)
		int terrainCount = 0;
		int boundaryCount = 0;
		foreach (Transform child in transform) {
			if (child.GetComponent<Terrain> ()) {
				terrainCount++;
				if (terrainCount > 1)
					return "TOO MANY TERRAINS: There are multiple Terrains attached to the gameobject. Please remove any additional terrain objects so that there is only one terrain.";
				
				TerrainMain = child.GetComponent<Terrain> ();
			}
			if (child.tag == "Boundary") {
				boundaryCount++;
				if (boundaryCount > 1)
					return "BOUNDS BOX ERROR: A child of this gameobject appears to have the tag 'Boundary'. Please remove this object or change its tag.";
				
				cube = child.GetComponent<BoxCollider> ();
			}
		}
		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.";

		if (boundaryCount == 0)
			return "NO BOUNDS BOX: There is no boundary box attached to the gameobject. Please revert the prefab to its original state by pressing the Revert button at the top of the Inspector for this object.";

		// Get the level edge boundaries
		edges = new List<BoxCollider>();
		edges.Clear ();
		for (int i = 0; i < cube.transform.childCount; i++) {
			edges.Add (cube.transform.GetChild (i).GetComponent<BoxCollider> ());
		}
		topEdge = cube.transform.GetChild (0).GetComponent<BoxCollider> ();
		bottomEdge = cube.transform.GetChild (1).GetComponent<BoxCollider> ();
		leftEdge = cube.transform.GetChild (2).GetComponent<BoxCollider> ();
		rightEdge = cube.transform.GetChild (3).GetComponent<BoxCollider> ();
		frontEdge = cube.transform.GetChild (4).GetComponent<BoxCollider> ();
		backEdge = cube.transform.GetChild (5).GetComponent<BoxCollider> ();
		if (!topEdge || !bottomEdge || !leftEdge || !rightEdge || !frontEdge || !backEdge)
			return "NO EDGES: There are missing boundary edges attached to the gameobject. Please revert the prefab to its original state by pressing the Revert button at the top of the Inspector for this object.";

		// Check that the Level Prefab has a LootRandomizer script attached to it
		if(!GetComponent<LootRandomizer>())
			return "NO LOOT RANDOMIZER SCRIPT: There is no LootRandomizer script attached the the gameobject. Please attach a LootRandomizer script to the object using the Add Component button in the Inspector";

		// Everything was assigned propperly
		return "";
	}

	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);
	}

	bool CheckObjectLocation (Transform main) {
		// loop through all the children of a transform and its children's children
		for (int i = 0; i < main.childCount; i++) {
			if (main.GetChild (i) == cube.transform || main.tag == "Potion") {
				continue;
			} else if (!main.GetChild (i).GetComponent<Collider> ()) {
				if (main.GetChild (i).GetComponent<MeshRenderer> ()) {
					ShowErrorMessage ("NO COLLIDER: The " + main.GetChild (i).name + " does not have a collider attached to it. Please attach a Mesh Collider to the object using the Add Component button in the Inspector");
					return false;
				}
			}

			if (main.GetChild (i).childCount > 0) {
				if (main.GetChild (i).GetComponent<Collider> ())
					objs.Add (main.GetChild (i));
				if (!CheckObjectLocation (main.transform.GetChild (i)))
					return false;
			} else {
				if (main.GetChild (i).GetComponent<Collider> ())
					objs.Add (main.GetChild (i));
			}
		}
		levelError = false;
		return true;
	}

	void CheckLevelBounds () {
		// Check for objects outside the bounds of the level.
		foreach (Transform obj in objs) {
			string objName = obj.name;
			if (!cube.bounds.Contains(obj.position)) {
				if (obj.parent) {
					objName = (obj.parent.GetComponent<Tinkercad> ()) ? obj.parent.name : obj.name;
				}
				ShowErrorMessage ("NOT CONTAINED: The " + objName + " is outside the build area for the level. Please move the object inside the build area.");
				return;
			} else {
				levelError = false;
			}
		}

		foreach (Collider edge in edges) {
			foreach (Transform obj in objs) {
				string objName = obj.name;
				if (obj.parent) {
					objName = (obj.parent.GetComponent<Tinkercad> ()) ? obj.parent.name : obj.name;
				}
				if ((edge.bounds.Intersects (obj.GetComponent<Collider>().bounds) && cube.transform != obj && !obj.GetComponent<Terrain>())) {
					ShowErrorMessage ("INTERSECTING EDGE: The " + objName + " is extending outside the build area for the level and intersecting with the " + edge.name + ". Please move the object inside the build area.");
					return;
				} else {
					levelError = false;
				}
			}
		}
	}

	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 OnValidate () {
		if (hideToggle) {
			LockAndHide ();
		}
	}

	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;
	}

	void HideBounds () {
		if (hideToggle) {
			cube.gameObject.hideFlags = HideFlags.HideInHierarchy;
		} else {
			cube.gameObject.hideFlags = HideFlags.None;
		}
	}

	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
}
