﻿using System.Collections.Generic;

using UnityEngine;
using UnityEngine.SceneManagement;

using Photon.Pun;

using BR.BattleRoyale.Networking;
using BR.BattleRoyale.Items;
using UPG.Extensions;
using UPG.Debugging;
using UnityEngine.Events;

namespace BR.BattleRoyale
{
	[ExecuteInEditMode]
	[RequireComponent(typeof(PhotonView))]
	public class Level : MonoBehaviour
	{
		public static UnityEvent OnLevelLoaded = new UnityEvent();

        #region ENUMS
        public enum DebugShape
		{
			Ray,
			Sphere,
			Quad
		}
        #endregion

        #region STRUCTS
        public struct TerrainNormal
		{
			public Vector3 Position { get; private set; }
			public Vector3 Normal { get; private set; }

			public TerrainNormal(RaycastHit _hit) =>
				(Position, Normal) = (_hit.point, _hit.normal);
		}
        #endregion

        #region INSPECTOR METHODS
        [Header("Level Prefab Components")]
		[SerializeField] [ReadOnly] string m_levelName = "";
		[ReadOnly] public Terrain PrimaryTerrain;

		[Header("Debug")]
		[SerializeField] DebugChannel m_debugChannel = null;
		#endregion

		#region PRIVATE FIELDS
		bool m_allowNameEnforcement = true;
		bool m_enforceHierarchy = true;
		bool m_showLevelBounds = false;
		bool m_showGridNodes = false;

#if UNITY_EDITOR
		bool m_initialSave = false;

		List<Transform> m_levelObjects = new List<Transform>();
		List<TerrainNormal> m_hitResults = new List<TerrainNormal>();
		List<GameObject> m_addingComponentList = new List<GameObject>();

		GameObject m_selectedObject;
		MeshRenderer m_selectedRenderer;
		MeshCollider m_selectedCollider;
		
#endif
		Mesh m_sizeComparisonMesh;
		#endregion

		#region PUBLIC FIELDS
		public bool AllowNameEnforcement
        {
			get => m_allowNameEnforcement;
			set
            {
				if (value == m_allowNameEnforcement)
					return;

				m_allowNameEnforcement = value;
            }
        }
        public string LevelName
        {
			get => m_levelName;
			set
            {
				if (value == m_levelName)
					return;

				m_levelName = value;

				m_debugChannel?.Raise(this, "Name set to " + value);
			}
        }

		[HideInInspector] public DebugShape DisplayShape = DebugShape.Ray;
		[HideInInspector] public float CheckInterval = 5;
		[HideInInspector] public float DisplayRadius = 80;
		[HideInInspector] public bool ShowLevelGround = false;
		[HideInInspector] public bool ShowSteepGround = false;
		[HideInInspector] public bool ShowUnwalkableGround = false;
		#endregion

		#region READABLES
		bool TerrainIsSetUpCorrectly
		{
            get
            {
				if (GetComponent<ColorChanger>())
					return true;

#if UNITY_EDITOR
				if (SceneManager.GetActiveScene().name == "GameIsland")
					return true;
#endif
				Terrain[] _terrainsInGameObject = GetComponentsInChildren<Terrain>();

				if (_terrainsInGameObject.Length > 0 && !PrimaryTerrain && PrimaryTerrain != _terrainsInGameObject[0])
					PrimaryTerrain = _terrainsInGameObject[0];

				for (int i = 1; i < _terrainsInGameObject.Length; i++)
					if (_terrainsInGameObject[i] != PrimaryTerrain)
					{
						_terrainsInGameObject[i].GetComponent<Collider>().DestroyOnValidate();
						_terrainsInGameObject[i].DestroyOnValidate();
					}
#if UNITY_EDITOR
				if (UnityEditor.EditorApplication.isPlaying)
					return true;
#endif
				if (!PrimaryTerrain)
					m_debugChannel?.Raise(this,
						new string[]{
							"There is no Terrain attached to the Level Prefab!",
							"Please add Terrain of the same name from the Do Not Edit OR Delete/Terrains folder to the Level Prefab."
						},
						DebugChannel.Severity.Error);

				return PrimaryTerrain;
            }
		}

#if UNITY_EDITOR
        Mesh SizeComparisonMesh =>
			m_sizeComparisonMesh ?? (m_sizeComparisonMesh = Resources.Load<Mesh>("PlayerMesh"));
#endif
		#endregion

		#region CONSTANTS
		static readonly System.Type[] NonEnvironmentalTypes =
			new System.Type[] {
				typeof(Item),
				typeof(RectTransform),
				typeof(Camera)
			};

		readonly Gradient m_gradient = 
			new Gradient(){
				colorKeys = new GradientColorKey[] {
					new GradientColorKey(Color.green, 0),
					new GradientColorKey(Color.red, 1)
				}
			};

		readonly AnimationCurve m_terrainMaxHeightCurve = new AnimationCurve(
			new Keyframe[] {
				new Keyframe(0, 0, 0, 0),
				new Keyframe(1, 1, 0, 0)
			});
#endregion

#if UNITY_EDITOR
		#region DEFAULT EDITOR METHODS
		void OnValidate()
		{
			if (m_levelName == "" && gameObject.name != "")
				m_levelName = gameObject.name;

			if (m_levelName == "" || !m_allowNameEnforcement)
				return;

			if (PrimaryTerrain)
				if (PrimaryTerrain.name != m_levelName + " Terrain")
				{
					PrimaryTerrain.name = m_levelName + " Terrain";
				
					m_debugChannel?.Raise(this,
						new string[] {
							"Enforced terrain name.",
							"Terrain name must match level prefab in order to export properly."
						});
				}

			if (gameObject.name != m_levelName)
			{
				gameObject.name = m_levelName;

				m_debugChannel?.Raise(this,
						new string[] {
							"Enforced level name.",
							"Terrain name must match level prefab in order to export properly."
						});
			}
		}
		void Awake()
		{
			// Safe
			if (!UnityEditor.EditorApplication.isPlaying && !gameObject.GetIsPrefab())
				Instantiate(Resources.Load<GameObject>("Level Setup/Level Post-Processing Volume"), transform.position, transform.rotation);

			MarkChildrenAsEnvironment(transform);

			OnLevelLoaded?.Invoke();
		}
		void Start()
		{
			// Safe
			if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
			{
				if (!TerrainIsSetUpCorrectly)
				{
					UnityEditor.EditorUtility.DisplayDialog("Level Setup 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.", "Ok");
					Debug.DebugBreak();
					Debug.Break();

					return;
				}

				if (!UnityEditor.EditorApplication.isPlaying)
					return;

				if (SceneManager.GetActiveScene().name == name)
					Instantiate(Resources.Load<GameObject>("Player"),
						SceneManager.GetActiveScene().name == "Tutorial Island" ? new Vector3(-146, 0, -193) : new Vector3(0, 200, 0),
						Quaternion.identity);
			}
		}
		void Update()
		{
			if (Application.isPlaying || gameObject.scene.name != name)
				return;

			foreach (Player _player in FindObjectsOfType<Player>())
				DestroyImmediate(_player.gameObject);

			if (!TerrainIsSetUpCorrectly)
				return;

			SnapLevelElements();

			AdoptChildren();

			KeepTheKidsInside(transform);

			MarkChildrenAsEnvironment(transform);
		}
		void OnDrawGizmos()
		{
			if (m_showGridNodes)
			{
				// Safe
				Vector3 _testPosition = UnityEditor.SceneView.lastActiveSceneView.camera.transform.position + UnityEditor.SceneView.lastActiveSceneView.camera.transform.forward * Constants.GridSize;
				Vector3Int _coordinateOfNode = Building.MagicWall.ToCoordinate(_testPosition);
				Vector3 _positionOfGridNode = Building.MagicWall.ToPosition(_coordinateOfNode);

				m_debugChannel?.DrawText(
					new string[] {
						"Node Coordinate: " + _coordinateOfNode,
						"Node Position: " + _positionOfGridNode
					},
					_positionOfGridNode, Color.cyan, Vector2.one);

				m_debugChannel?.DrawText(
					"Test Position: " + _testPosition, 
					_testPosition, 
					Color.white, 
					Vector2.one);

				Vector3 _innerCorner = _positionOfGridNode - Vector3.one * (Constants.GridSize / 2);
				Vector3 _outerCorner = _positionOfGridNode + Vector3.one * (Constants.GridSize / 2);

				m_debugChannel?.DrawLine(
					new Vector3(_innerCorner.x, _testPosition.y, _testPosition.z), 
					new Vector3(_outerCorner.x, _testPosition.y, _testPosition.z), 
					Color.red);

				m_debugChannel?.DrawLine(
					new Vector3(_testPosition.x, _innerCorner.y, _testPosition.z), 
					new Vector3(_testPosition.x, _outerCorner.y, _testPosition.z), 
					Color.red);

				m_debugChannel?.DrawLine(
					new Vector3(_testPosition.x, _testPosition.y, _innerCorner.z), 
					new Vector3(_testPosition.x, _testPosition.y, _outerCorner.z), 
					Color.red);

				m_debugChannel?.DrawSphere(
					_positionOfGridNode,
					Color.cyan,
					0.4f);

				m_debugChannel?.DrawSphere(
					_testPosition,
					Color.red,
					0.2f);

				m_debugChannel?.DrawRay(
					_testPosition, 
					Building.MagicWall.CardinalForward(UnityEditor.SceneView.lastActiveSceneView.camera.transform), 
					Color.red);

				m_debugChannel?.DrawCube(
					_testPosition + Building.MagicWall.CardinalForward(UnityEditor.SceneView.lastActiveSceneView.camera.transform.forward), 
					Vector3.one / 3,
					Color.blue);

				m_debugChannel?.DrawWireCube(
					_positionOfGridNode + Vector3.up * Constants.GridSize / 2,
					Vector3.one * Constants.GridSize,
					Color.green);
			}

			// Safe
			if (Physics.Raycast(
				UnityEditor.SceneView.lastActiveSceneView.camera.transform.position,
				UnityEditor.SceneView.lastActiveSceneView.camera.transform.forward, out RaycastHit _viewportHit, 500))
			{
				TerrainNormal[] _localHits = 
					m_hitResults.FindAll(x => _viewportHit.point.DistanceTo(x.Position) < DisplayRadius).ToArray();

				if (_localHits.Length > 0)
				{
					m_debugChannel?.DrawWireSphere(_viewportHit.point, Color.cyan, DisplayRadius);

					int _level = 0;
					int _steep = 0;
					int _unwalkable = 0;

					foreach (TerrainNormal _hit in _localHits)
					{
						float _angle = Mathf.Abs(Vector3.Angle(Vector3.up, _hit.Normal));

						if (_angle < 30)
						{
							_level++;

							if (!ShowLevelGround)
								continue;
						}
						else if (_angle < 60)
						{
							_steep++;

							if (!ShowSteepGround)
								continue;
						}
						else
						{
							_unwalkable++;

							if (!ShowUnwalkableGround)
								continue;
						}

						Color _color = m_gradient.Evaluate((_angle - 10) / 80);

						switch (DisplayShape)
						{
							case DebugShape.Ray:
								m_debugChannel?.DrawRay(_hit.Position, _hit.Normal, _color, 1.5f);
								break;
							case DebugShape.Sphere:
								m_debugChannel?.DrawSphere(_hit.Position, _color, 1);
								break;
							case DebugShape.Quad:
								m_debugChannel?.DrawQuad(
									_hit.Position + Vector3.up * 0.1f, 
									Quaternion.FromToRotation(Vector3.up, _hit.Normal) * Quaternion.Euler(90, 0, 0),
									CheckInterval * 0.95f,
									_color);
								break;
						}
					}

					int _totalPoints = _level + _steep + _unwalkable;

					List<string> _message = new List<string>();

					if (ShowLevelGround)
						_message.Add((100 * ((float)_level / _totalPoints)).RoundTo(1) + "% Level");

					if (ShowSteepGround)
						_message.Add((100 * ((float)_steep / _totalPoints)).RoundTo(1) + "% Steep");

					if (ShowUnwalkableGround)
						_message.Add((100 * ((float)_unwalkable / _totalPoints)).RoundTo(1) + "% Unwalkable");

					m_debugChannel?.DrawText(
						_message.ToArray(), 
						_viewportHit.point + Vector3.up * DisplayRadius, 
						Color.cyan, 
						Vector2.up);
				}
			}

			if (!SizeComparisonMesh)
				return;

			// Safe
			if (m_selectedObject != UnityEditor.Selection.activeGameObject)
			{
				if (!UnityEditor.Selection.activeGameObject || !UnityEditor.Selection.activeGameObject.transform.IsChildOf(transform))
				{
					m_selectedObject = null;
					m_selectedRenderer = null;
					m_selectedCollider = null;

					return;
				}

				m_selectedObject = UnityEditor.Selection.activeGameObject;
				m_selectedRenderer = m_selectedObject.GetComponent<MeshRenderer>();
				m_selectedCollider = m_selectedObject.GetComponent<MeshCollider>();
			}

			if (!m_selectedObject || (!m_selectedRenderer && !m_selectedCollider))
				return;

			Gizmos.color = Color.green;

			float _height = 0;

			Vector3 _bounds = m_selectedRenderer ? m_selectedRenderer.bounds.size : m_selectedCollider.bounds.size;
			float _distance = m_selectedRenderer ? m_selectedRenderer.bounds.size.y : m_selectedCollider.bounds.size.y;

			if (Physics.Raycast(m_selectedObject.transform.position +
					Quaternion.Euler(0, m_selectedObject.transform.rotation.eulerAngles.y, 0) *
					new Vector3(_bounds.x / 2 + 3, 0, 0) + Vector3.up * _distance, Vector3.down, out RaycastHit hit, _distance))
				_height = _distance - hit.distance;

			Gizmos.DrawMesh(SizeComparisonMesh,
				m_selectedObject.transform.position + Vector3.up * _height + 
				Quaternion.Euler(0, m_selectedObject.transform.rotation.eulerAngles.y, 0) * 
				new Vector3(_bounds.x / 2 + 3, 0, 0),
				Quaternion.Euler(-90, m_selectedObject.transform.rotation.eulerAngles.y, 0), Vector3.one * 100);
		}
		#endregion

		#region EDITOR METHODS
		void AdoptChildren()
		{
			Transform[] _allTransformsInScene = FindObjectsOfType<Transform>();

			foreach (Transform obj in _allTransformsInScene)
				if (obj.gameObject.scene == gameObject.scene)
					if (obj.parent == null && obj.tag != "Safe")
					{
						obj.SetParent(transform);

						if (!obj.name.Contains(m_levelName))
							m_debugChannel?.Raise(this,
								new string[]{
									"Added " + obj.name + " as a child of level prefab.",
									"Use the Save Scene & Level utility to finalize your changes."
								});
					}
		}
		void KeepTheKidsInside(Transform parent)
		{
			m_levelObjects.RemoveAll(Transform => Transform == null);

			if (!m_enforceHierarchy)
				return;

			foreach (Transform kid in parent)
			{
				if (!m_levelObjects.Contains(kid))
					m_levelObjects.Add(kid);

				string objectType = CheckObjectComponents(kid);

				MoveColliderWithinBounds(kid.GetComponent<Collider>());

				if (objectType == "" && kid.childCount > 0)
					KeepTheKidsInside(kid);
			}
		}
		void MoveColliderWithinBounds(Collider _collider)
		{
			if (!_collider)
				return;

			Vector3 corner1 = new Vector3(_collider.bounds.max.x, _collider.bounds.max.y, _collider.bounds.min.z);
			Vector3 corner2 = new Vector3(_collider.bounds.max.x, _collider.bounds.min.y, _collider.bounds.min.z);
			Vector3 corner3 = new Vector3(_collider.bounds.max.x, _collider.bounds.min.y, _collider.bounds.max.z);
			Vector3 corner4 = new Vector3(_collider.bounds.min.x, _collider.bounds.max.y, _collider.bounds.max.z);
			Vector3 corner5 = new Vector3(_collider.bounds.min.x, _collider.bounds.max.y, _collider.bounds.min.z);
			Vector3 corner6 = new Vector3(_collider.bounds.min.x, _collider.bounds.min.y, _collider.bounds.max.z);

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

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

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

			if (m_showLevelBounds) { DrawDebugBounds(_collider, 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)
			{
				_collider.transform.position += new Vector3(0f, 0f, plusOutsideZ);
			}
			else if (negOutsideZ > 0f)
			{
				_collider.transform.position += new Vector3(0f, 0f, negOutsideZ);
			}
			if (plusOutsideX < 0f)
			{
				_collider.transform.position += new Vector3(plusOutsideX, 0f, 0f);
			}
			else if (negOutsideX > 0f)
			{
				_collider.transform.position += new Vector3(negOutsideX, 0f, 0f);
			}
			if (plusOutsideY < 0f)
			{
				_collider.transform.position += new Vector3(0f, plusOutsideY, 0f);
			}
			else if (negOutsideY > 0f)
			{
				_collider.transform.position += new Vector3(0f, negOutsideY, 0f);
			}

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

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

		}
		string CheckObjectComponents(Transform check)
		{
			if (check.GetComponent<LODGroup>())
				return "LOD";
			else if (check.GetComponent<Wand>())
				return "Wand";
			else if (check.GetComponent<Spells.Spell>())
				return "Spell";
			else if (check.GetComponent<Potion>())
				return "Potion";
			else if (check.GetComponent<LootChest>())
				return "Loot";
			else if (check.GetComponent<DiscoBall>())
				return "Disco";
			else if (check.GetComponent<Collider>())
				return "";
			else if (!m_addingComponentList.Contains(check.gameObject) &&  check.GetComponent<MeshRenderer>() && !check.GetComponent<Collider>() && check.tag != "LOD")
			{
				m_addingComponentList.Add(check.gameObject);
				
				UnityEditor.EditorApplication.delayCall += () => { 
					check.gameObject.AddComponent<MeshCollider>();
					m_addingComponentList.Remove(check.gameObject);
				};

				return "";
			}
			else
				return "Empty";
		}
		void SnapLevelElements()
		{
			transform.position = new Vector3(-10, 0, -10);
			transform.eulerAngles = Vector3.zero;
			transform.localScale = Vector3.one;

			if (!PrimaryTerrain)
				return;
            
			PrimaryTerrain.transform.localPosition = Vector3.one * -200;
			PrimaryTerrain.transform.eulerAngles = Vector3.zero;
			PrimaryTerrain.transform.localScale = Vector3.one;

			PrimaryTerrain.gameObject.layer = 17;

			SnapTerrainEdges();
		}
		void SnapTerrainEdges()
		{
			if (!PrimaryTerrain || !PrimaryTerrain.terrainData)
				return;

			int _xResolution = PrimaryTerrain.terrainData.heightmapWidth;
			int _yResolution = PrimaryTerrain.terrainData.heightmapHeight;

			float[,] _terrainHeights = PrimaryTerrain.terrainData.GetHeights(0, 0, _xResolution, _yResolution);

			int _borderClampWidth = 64;

			for (int j = 0; j < _borderClampWidth; j++)
			{
				float _percFromEdge = (float)j / _borderClampWidth;
				float _maxHeight = 0.5f + 0.5f * m_terrainMaxHeightCurve.Evaluate(_percFromEdge);

				for (int i = 0 + j; i < 257 - j; i++)
				{
					float _currentHeight = _terrainHeights[i, j];

					if (Mathf.Abs(_currentHeight) > _maxHeight || Mathf.Abs(_currentHeight) < 1 - _maxHeight)
						_terrainHeights[i, j] = Mathf.Clamp(_terrainHeights[i, j], 1 - _maxHeight, _maxHeight);

					_currentHeight = _terrainHeights[i, 256 - j];

					if (Mathf.Abs(_currentHeight) > _maxHeight || Mathf.Abs(_currentHeight) < 1 - _maxHeight)
						_terrainHeights[i, 256 - j] = Mathf.Clamp(_terrainHeights[i, 256 - j], 1 - _maxHeight, _maxHeight);

					_currentHeight = _terrainHeights[j, i];

					if (Mathf.Abs(_currentHeight) > _maxHeight || Mathf.Abs(_currentHeight) < 1 - _maxHeight)
						_terrainHeights[j, i] = Mathf.Clamp(_terrainHeights[j, i], 1 - _maxHeight, _maxHeight);

					_currentHeight = _terrainHeights[256 - j, i];

					if (Mathf.Abs(_currentHeight) > _maxHeight || Mathf.Abs(_currentHeight) < 1 - _maxHeight)
						_terrainHeights[256 - j, i] = Mathf.Clamp(_terrainHeights[256 - j, i], 1 - _maxHeight, _maxHeight);
				}
			}

			PrimaryTerrain.terrainData.SetHeights(0, 0, _terrainHeights);
		}
		#endregion

		#region STEEPNESS METHODS
		public void CalculateSteepnessVisuals()
		{
			//if (!EditorUtility.DisplayDialog(
			//			"Calculate Terrain Slopes",
			//			"Showing terrain normals can cause intense slowdown on low-spec devices. Are you sure you want to proceed?",
			//			"Let's go, I can take it!",
			//			"No thanks! I like my frames!"))
			//	return;

			RaycastHit[] _hits;

			List<TerrainNormal> _normals = new List<TerrainNormal>();

			for (float x = -210; x < 210; x += CheckInterval)
				for (float y = -210; y < 210; y += CheckInterval)
				{
					Ray _ray = new Ray(new Vector3(x, 200, y), Vector3.down);

					_hits = Physics.RaycastAll(_ray, 500, LayerMask.GetMask("Default", "Environment"), QueryTriggerInteraction.Ignore);

					if (_hits.Length > 0)
						foreach (RaycastHit _hit in _hits)
							_normals.Add(new TerrainNormal(_hit));
				}

			m_hitResults = _normals;

			//m_debugChannel?.Raise(this, "Calculated " + m_hitResults.Count + " contact points");
		}
		public void ClearSteepnessData()
		{
			m_hitResults.Clear();

			m_debugChannel?.Raise(this, "Terrain slope gizmos reset.");
		}
		#endregion

		#region CONTEXT METHODS
		[ContextMenu("Toggle Level Bounds Visuals")]
		void ToggleLevelBoundsGizmos()
		{
			m_showLevelBounds = !m_showLevelBounds;

			m_debugChannel?.Raise(this, "Level bounds visuals " + (m_showLevelBounds ? "enabled" : "disabled") + "!");
		}
		[ContextMenu("Toggle Name Enforcement")]
		void ToggleNameEnforcement()
		{
			m_allowNameEnforcement = !m_allowNameEnforcement;

			m_debugChannel?.Raise(this, "Name enforcment " + (m_allowNameEnforcement ? "enabled" : "disabled") + "!");
		}
		[ContextMenu("Toggle Hierarchy Enforcement")]
		void ToggleHierarchyEnforcement()
		{
			m_enforceHierarchy = !m_enforceHierarchy;

			m_debugChannel?.Raise(this, "Hierarchy enforcment " + (m_enforceHierarchy ? "enabled" : "disabled") + "!");
		}
		[ContextMenu("Toggle Grid Node Visuals")]
		void ToggleGridNodeVisuals()
		{
			m_showGridNodes = !m_showGridNodes;

			m_debugChannel?.Raise(this, "Grid node visuals " + (m_showGridNodes ? "enabled" : "disabled") + "!");
		}
		public void SavePrefab()
		{
			if (!gameObject.activeInHierarchy)
			{
				DebugChannel.Raise("Editor", this,
					"Cannot save an inactive level!");

				return;
			}

			// Safe
			Transform selectedTransform = UnityEditor.Selection.activeTransform;

			if (m_initialSave)
			{
				m_initialSave = false;

				UnityEditor.EditorUtility.DisplayProgressBar("Save Level as Prefab", "Executing mandatory save...", 0.2f);
			}
			else
				UnityEditor.EditorUtility.DisplayProgressBar("Save Level as Prefab", "Saving Scene & Level...", 0.2f);


			foreach (LODGroup _group in GetComponentsInChildren<LODGroup>())
				_group.RecalculateBounds();

			GameObject currentPrefab = (GameObject)UnityEditor.AssetDatabase.LoadAssetAtPath("Assets/Resources/Levels/" + gameObject.name + ".prefab", typeof(GameObject));

			if (!currentPrefab)
			{
				UnityEditor.EditorUtility.DisplayProgressBar("Save Level as Prefab", "Creating new level prefab...", 0.4f);

				UnityEditor.PrefabUtility.SaveAsPrefabAsset(gameObject, "Assets/Resources/Levels/" + gameObject.name + ".prefab");

				DebugChannel.Raise("Editor", this,
					"A new prefab has been created at Assets/Resources/Levels/" + gameObject.name + ".prefab");
			}
			else
			{
				UnityEditor.EditorUtility.DisplayProgressBar("Save Level as Prefab", "Overwriting existing level prefab...", 0.4f);

				UnityEditor.PrefabUtility.SaveAsPrefabAssetAndConnect(gameObject, "Assets/Resources/Levels/" + gameObject.name + ".prefab", UnityEditor.InteractionMode.UserAction);

				DebugChannel.Raise("Editor", this,
					"Assets/Resources/Levels/" + gameObject.name + ".prefab was overwritten.");
			}

			if (UnityEditor.PrefabUtility.IsPartOfPrefabInstance(gameObject))
			{
				UnityEditor.EditorUtility.DisplayProgressBar("Save Level as Prefab", "Unpacking prefab instance...", 0.6f);

				UnityEditor.PrefabUtility.UnpackPrefabInstance(gameObject, UnityEditor.PrefabUnpackMode.Completely, UnityEditor.InteractionMode.AutomatedAction);
			}

			UnityEditor.EditorUtility.DisplayProgressBar("Save Level as Prefab", "Saving scene...", 0.8f);

			UnityEditor.SceneManagement.EditorSceneManager.SaveScene(UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene());

			DebugChannel.Raise("Editor", this,
				new string[] {
						"Saved scene as " + SceneManager.GetActiveScene().name,
						"Thank you for your patience."});

			UnityEditor.EditorUtility.ClearProgressBar();

			UnityEditor.Selection.activeTransform = selectedTransform;
		}
		[ContextMenu("Set Visible/Locked Layers")]
		void LockAndHide()
		{
			UnityEditor.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"));
			UnityEditor.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")));
		}
		// Safe
		[ContextMenu("Bake Lighting #l")]
		void BakeLighting() =>
			UnityEditor.Lightmapping.BakeAsync();
		#endregion

		#region DEBUG METHODS
		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);
		}
		#endregion
#else
		#region DEFAULT METHODS
		void Awake()
		{
			if (Application.platform == RuntimePlatform.WebGLPlayer)
			{
				SearchAndDestroyParticles(transform);
				//ToggleAllGPUInstancing(transform, false);
			}
			//else
			//	ToggleAllGPUInstancing(transform, true);
			
			MarkChildrenAsEnvironment(transform);

			OnLevelLoaded?.Invoke();
		}
		#endregion
#endif

		#region GRAPHICS METHODS
		void SearchAndDestroyParticles(Transform parent)
		{
			foreach (Transform child in parent)
			{
				if (child.TryGetComponent(out ParticleSystem _particles))
					Destroy(_particles);
				
				if (child.TryGetComponent(out ParticleSystemRenderer _renderer))
					Destroy(_renderer);

				SearchAndDestroyParticles(child);
			}
		}
		#endregion

		void MarkChildrenAsEnvironment(Transform parent)
		{
			foreach (Transform child in parent)
			{
				if (child.tag == "Safe")
                {
					child.parent = null;
					continue;
                }

				if (child.gameObject.HasAnyComponent(NonEnvironmentalTypes))
					continue;

				if (child.tag != "Environment")
				{
					child.tag = "Environment";

					m_debugChannel?.Raise(this, child.name + " tagged as Environment");
				}

				MarkChildrenAsEnvironment(child);
			}
		}
	}
}