using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;

#if UNITY_5_3_OR_NEWER
using UnityEngine.SceneManagement;
#endif

namespace Plugins.Isolationist.Editor
{
	[InitializeOnLoad]
	public static class EditorIsolateCommand
	{
		private const string ISOLATE_KEY_PREF = "IsolationistKey";
		private const string ISOLATE_ALT_PREF = "IsolationistAlt";
		private const string ISOLATE_CTRL_PREF = "IsolationistCtrl";
		private const string ISOLATE_SHIFT_PREF = "IsolationistShift";
		private const string ISOLATE_HIDE_LIGHTS_PREF = "IsolationistHideLights";
		private const string ISOLATE_HIDE_CAMERAS_PREF = "IsolationistHideCameras";
		private static bool _alt;
		private static bool _ctrl;
		private static bool _shift;
		private static bool _ctrlOrShiftPressed;
		private static bool _hideLights;
		private static bool _hideCameras;
		private static KeyCode _hotkey;
		private static GameObject _lastSelection;
		private static int _lastSelectionCount;
		private static string _shortcutDisplay;
		private static List<GameObject> _lastSelectionList;
		private static readonly List<Type> _lightTypeList = new List<Type>();
		private static readonly List<Type> _cameraTypeList = new List<Type>();

		public static void AddLightType(Type type) {
			_lightTypeList.Add(type);
		}

		public static void AddCameraType(Type type) {
			_cameraTypeList.Add(type);
		}

		static EditorIsolateCommand()
		{
			_alt = EditorPrefs.GetBool(ISOLATE_ALT_PREF, false);
			_ctrl = EditorPrefs.GetBool(ISOLATE_CTRL_PREF, false);
			_shift = EditorPrefs.GetBool(ISOLATE_SHIFT_PREF, false);
			_hotkey = (KeyCode) EditorPrefs.GetInt(ISOLATE_KEY_PREF, (int) KeyCode.I);
			_hideLights = EditorPrefs.GetBool(ISOLATE_HIDE_LIGHTS_PREF, true);
			_hideCameras = EditorPrefs.GetBool(ISOLATE_HIDE_CAMERAS_PREF, true);
			EditorApplication.update += Update;
			EditorApplication.playmodeStateChanged += PlaymodeStateChanged;
			EditorApplication.hierarchyWindowItemOnGUI += HierarchyWindowItemOnGUI;
			SceneView.onSceneGUIDelegate += OnSceneGUI;

			AddCameraType(typeof(Camera));
			AddLightType(typeof(Light));
			AddLightType(typeof(ReflectionProbe));
		}

		private static bool IsolateKeyPressed
		{
			get
			{
				if (Event.current == null) return false;
				if (Event.current.type != EventType.KeyUp) return false;
				return Event.current.keyCode == _hotkey && Event.current.alt == _alt && Event.current.control == _ctrl && Event.current.shift == _shift;
			}
		}

		public static string ShortcutDisplay { get { return _shortcutDisplay.IsNullOrEmpty() ? _shortcutDisplay = GetShortcutDisplay() : _shortcutDisplay; } }

		private static void Update()
		{
			if (!IsolateInfo.IsIsolated || _lastSelection == Selection.activeGameObject && _lastSelectionCount == Selection.gameObjects.Length) return;
			var selectionList = Selection.gameObjects.ToList();
			var newItems = _lastSelectionList == null ? selectionList : selectionList.Except(_lastSelectionList).ToList();
			_lastSelection = Selection.activeGameObject;
			_lastSelectionCount = Selection.gameObjects.Length;
			_lastSelectionList = selectionList;
			SelectionChanged(newItems);
		}

		private static void OnSceneGUI(SceneView sceneView) { OnGUI(); }

		private static void HierarchyWindowItemOnGUI(int instanceId, Rect selectionRect)
		{
			if (!GUI.GetNameOfFocusedControl().IsNullOrEmpty()) return;
			OnGUI();
		}

		private static void OnGUI()
		{
			_ctrlOrShiftPressed = Event.current.control || Event.current.shift;
			if (!IsolateKeyPressed) return;
			ToggleIsolate();
			Event.current.Use();
		}

		private static void PlaymodeStateChanged()
		{
			if (EditorApplication.isPlayingOrWillChangePlaymode) IsolateInfo.Show();
			else IsolateInfo.Hide();
		}

		private static void SelectionChanged(List<GameObject> newItems)
		{
			if (WasHidden(Selection.activeTransform) && !_ctrlOrShiftPressed)
			{
				EndIsolation();
				return;
			}

			if (!_ctrlOrShiftPressed) return;

			UpdateIsolation(newItems);
		}

		private static List<GameObject> GetAllGameObjectsToHide() { return IsolateInfo.Instance.FocusObjects.SelectMany<GameObject, GameObject>(GetGameObjectsToHide).Distinct().ToList(); }

		[MenuItem("Tools/Toggle Isolate", true)]
		public static bool CanToggleIsolate() { return Selection.activeGameObject || IsolateInfo.IsIsolated; }

		[MenuItem("Tools/Toggle Isolate")]
		public static void ToggleIsolate()
		{
			if (IsolateInfo.IsIsolated) EndIsolation();
			else StartIsolation();
		}

		private static void StartIsolation()
		{
			if (IsolateInfo.Instance)
			{
				Debug.LogWarning("Isolationist: Found previous isolation info. This shouldn't happen. Ending the previous isolation anyway.");
				EndIsolation();
			}

			if (EditorApplication.isPlayingOrWillChangePlaymode) {
				Debug.LogWarning("Isolationist: Can't isolate while playing. It'll break stuff!");
				return;
			}

			// Create new IsolateInfo object.
			var container = new GameObject("IsolationInfo") {hideFlags = HideFlags.HideInHierarchy};
			Undo.RegisterCreatedObjectUndo(container, "Isolate");
			IsolateInfo.Instance = container.AddComponent<IsolateInfo>();
			var focusList = IsolateInfo.Instance.FocusObjects = Selection.gameObjects.ToList();

			if (!_hideLights) _lightTypeList.ForEach(t => focusList.AddRange(Object.FindObjectsOfType(t).Select<Object, GameObject>(ObjectToGO)));

			if (!_hideCameras) _cameraTypeList.ForEach(t => focusList.AddRange(Object.FindObjectsOfType(t).Select<Object, GameObject>(ObjectToGO)));

			IsolateInfo.Instance.HiddenObjects = GetAllGameObjectsToHide();

			if (!IsolateInfo.Instance.HiddenObjects.Any())
			{
				Object.DestroyImmediate(container);
				Debug.LogWarning("Isolationist: Nothing to isolate.");
				return;
			}

			Undo.RecordObjects(IsolateInfo.Instance.HiddenObjects.Cast<Object>().ToArray(), "Isolate");
			IsolateInfo.Hide();
		}

		private static GameObject ObjectToGO(Object obj)
		{
			Component component = obj as Component;
			return component ? component.gameObject : null;
		}

		private static void UpdateIsolation(List<GameObject> newItems)
		{
			if (!newItems.Any()) return;
			Undo.RecordObject(IsolateInfo.Instance, "Isolate");
			Undo.RecordObjects(IsolateInfo.Instance.HiddenObjects.Cast<Object>().ToArray(), "Isolate");
			IsolateInfo.Show();
			IsolateInfo.Instance.FocusObjects = IsolateInfo.Instance.FocusObjects.Concat(newItems).Distinct().ToList();
			var newHiddenObjects = GetAllGameObjectsToHide();
			Undo.RecordObjects(newHiddenObjects.Except(IsolateInfo.Instance.HiddenObjects).Cast<Object>().ToArray(), "Isolate");
			IsolateInfo.Instance.HiddenObjects = newHiddenObjects;
			IsolateInfo.Hide();
		}

		private static bool WasHidden(Transform t) { return t && !t.gameObject.activeInHierarchy && !t.GetComponent<IsolateInfo>() && !IsolateInfo.Instance.FocusObjects.Any(t.gameObject.IsRelative); }

		private static bool CanHide(Transform t)
		{
			return t && t.gameObject.activeSelf && !t.GetComponent<IsolateInfo>() && !IsolateInfo.Instance.FocusObjects.Any(t.gameObject.IsRelative);
		}

		private static IEnumerable<GameObject> GetGameObjectsToHide(GameObject keeperGo)
		{
			if (!keeperGo) return new List<GameObject>();

			var keeper = keeperGo.transform;
			var transformsToHide = new List<Transform>();

			while (keeper.parent)
			{
				transformsToHide.AddRange(keeper.parent.GetChildren().Where(CanHide));
				keeper = keeper.parent;
			}

			transformsToHide.AddRange(GetRootTransforms().Where(CanHide));
			return transformsToHide.Select(t => t.gameObject);
		}

		[PreferenceItem("Isolationist")]
		public static void PreferencesGUI()
		{
			GUILayout.Label("Shortcut: " + ShortcutDisplay);
			_ctrl = EditorGUILayout.Toggle("Ctrl", _ctrl);
			_alt = EditorGUILayout.Toggle("Alt", _alt);
			_shift = EditorGUILayout.Toggle("Shift", _shift);
			_hotkey = (KeyCode) EditorGUILayout.EnumPopup("Shortcut Key", _hotkey);

			GUILayout.Label("");

			_hideLights = EditorGUILayout.Toggle("Hide Lights", _hideLights);
			_hideCameras = EditorGUILayout.Toggle("Hide Cameras", _hideCameras);


			if (!GUI.changed) return;

			EditorPrefs.SetBool(ISOLATE_CTRL_PREF, _ctrl);
			EditorPrefs.SetBool(ISOLATE_ALT_PREF, _alt);
			EditorPrefs.SetBool(ISOLATE_SHIFT_PREF, _shift);
			EditorPrefs.SetBool(ISOLATE_HIDE_LIGHTS_PREF, _hideLights);
			EditorPrefs.SetBool(ISOLATE_HIDE_CAMERAS_PREF, _hideCameras);
			EditorPrefs.SetInt(ISOLATE_KEY_PREF, (int) _hotkey);
			_shortcutDisplay = GetShortcutDisplay();
		}

		private static void EndIsolation()
		{
			if (!IsolateInfo.Instance) return;

			if (IsolateInfo.Instance.HiddenObjects != null)
			{
				Undo.RecordObjects(IsolateInfo.Instance.HiddenObjects.Cast<Object>().ToArray(), "DeIsolate");
				IsolateInfo.Show();
			}

			Undo.DestroyObjectImmediate(IsolateInfo.Instance.gameObject);
		}

		private static string GetShortcutDisplay()
		{
			var display = "";
			if (_ctrl) display += "Ctrl+";
			if (_alt) display += "Alt+";
			if (_shift) display += "Shift+";
			display += _hotkey;
			return display;
		}


		#region Utils

		private static bool IsParent(this Transform parent, Transform transform)
		{
			while (parent)
			{
				if (parent == transform) return true;
				parent = parent.parent;
			}

			return false;
		}

		private static bool IsParent(this GameObject parent, GameObject go) { return parent && go && IsParent(parent.transform, go.transform); }

		private static bool IsRelative(this GameObject go1, GameObject go2) { return go2.IsParent(go1) || go1.IsParent(go2); }

		private static IEnumerable<Transform> GetChildren(this Transform t)
		{
			var children = new List<Transform>();
			for (var i = 0; i < t.childCount; i++) children.Add(t.GetChild(i));
			return children;
		}

		private static IEnumerable<GameObject> GetRootSceneObjects()
		{
#if UNITY_5_3_OR_NEWER
			return SceneManager.GetActiveScene().GetRootGameObjects();
#else
			var prop = new HierarchyProperty(HierarchyType.GameObjects);
			var expanded = new int[0];
			while (prop.Next(expanded)) yield return prop.pptrValue as GameObject;
#endif
		}

		private static IEnumerable<Transform> GetRootTransforms() { return GetRootSceneObjects().Where(go => go).Select(go => go.transform); }

		private static bool IsNullOrEmpty(this string str) { return string.IsNullOrEmpty(str); }

		#endregion
	}
}