#if !NO_UNITY
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using FullSerializer.Internal;
using UnityEditor;
using UnityEngine;

namespace FullSerializer {
	[InitializeOnLoad]
	public static class PlayStateNotifier {
		static PlayStateNotifier() {
			EditorApplication.playModeStateChanged += ModeChanged;
		}

		private static void ModeChanged(PlayModeStateChange stateChange) {
			if (!EditorApplication.isPlayingOrWillChangePlaymode && EditorApplication.isPlaying) {
				Debug.Log("There are " + fsAotCompilationManager.AotCandidateTypes.Count + " candidate types");
				foreach (fsAotConfiguration target in Resources.FindObjectsOfTypeAll<fsAotConfiguration>()) {
					var seen = new HashSet<string>(target.aotTypes.Select(t => t.FullTypeName));
					foreach (Type type in fsAotCompilationManager.AotCandidateTypes) {
						if (seen.Contains(type.FullName) == false) {
							target.aotTypes.Add(new fsAotConfiguration.Entry(type));
							EditorUtility.SetDirty(target);
						}
					}
				}
			}
		}
	}

	[CustomEditor(typeof(fsAotConfiguration))]
	public class fsAotConfigurationEditor : Editor {
		[NonSerialized]
		private List<Type> _allAotTypes;
		private List<Type> allAotTypes {
			get {
				if (_allAotTypes == null)
					_allAotTypes = FindAllAotTypes().ToList();
				return _allAotTypes;
			}
		}

		private string[] options = new string[] { "On", "Off", "[?]" };
		private int GetIndexForState(fsAotConfiguration.AotState state) {
			switch (state) {
				case fsAotConfiguration.AotState.Enabled:
					return 0;
				case fsAotConfiguration.AotState.Disabled:
					return 1;
				case fsAotConfiguration.AotState.Default:
					return 2;
			}

			throw new ArgumentException("state is invalid " + state);
		}
		private fsAotConfiguration.AotState GetStateForIndex(int index) {
			switch (index) {
				case 0: return fsAotConfiguration.AotState.Enabled;
				case 1: return fsAotConfiguration.AotState.Disabled;
				case 2: return fsAotConfiguration.AotState.Default;
			}

			throw new ArgumentException("invalid index " + index);
		}

		private IEnumerable<Type> FindAllAotTypes() {
			foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) {
				foreach (Type t in assembly.GetTypes()) {
					bool performAot = false;

					// check for [fsObject]
					{
						var props = t.GetCustomAttributes(typeof(fsObjectAttribute), true);
						if (props != null && props.Length > 0) performAot = true;
					}

					// check for [fsProperty]
					if (!performAot) {
						foreach (PropertyInfo p in t.GetProperties()) {
							var props = p.GetCustomAttributes(typeof(fsPropertyAttribute), true);
							if (props.Length > 0) {
								performAot = true;
								break;
							}
						}
					}

					if (performAot)
						yield return t;
				}
			}
		}

		private enum OutOfDateResult {
			NoAot,
			Stale,
			Current
		}
		private OutOfDateResult IsOutOfDate(Type type) {
			string converterName = fsAotCompilationManager.GetQualifiedConverterNameForType(type);
			Type converterType = fsTypeCache.GetType(converterName);
			if (converterType == null)
				return OutOfDateResult.NoAot;

			// TODO: We should also verify that the type is contained inside of fsConverterRegistrar as
			//       an additional diagnostic. If it is not, then that means the type will not be used
			//       at runtime.

			object instance_ = null;
			try {
				instance_ = Activator.CreateInstance(converterType);
			} catch (Exception) {}
			if (instance_ is fsIAotConverter == false)
				return OutOfDateResult.NoAot;
			var instance = (fsIAotConverter)instance_;

			var metatype = fsMetaType.Get(new fsConfig(), type);
			if (fsAotCompilationManager.IsAotModelUpToDate(metatype, instance) == false)
				return OutOfDateResult.Stale;

			return OutOfDateResult.Current;
		}

		private void DrawType(fsAotConfiguration.Entry entry, Type resolvedType) {
			var target = (fsAotConfiguration)this.target;

			EditorGUILayout.BeginHorizontal();

			int currentIndex = GetIndexForState(entry.State);
			int newIndex = GUILayout.Toolbar(currentIndex, options, GUILayout.ExpandWidth(false));
			if (currentIndex != newIndex) {
				entry.State = GetStateForIndex(newIndex);
				target.UpdateOrAddEntry(entry);
				EditorUtility.SetDirty(target);
			}

			string displayName = entry.FullTypeName;
			string tooltip = "";
			if (resolvedType != null) {
				displayName = resolvedType.CSharpName();
				tooltip = resolvedType.CSharpName(true);
			}
			GUIContent label = new GUIContent(displayName, tooltip);
			GUILayout.Label(label);

			GUILayout.FlexibleSpace();

			GUIStyle messageStyle = new GUIStyle(EditorStyles.label);
			string message;
			if (resolvedType != null) {
				message = GetAotCompilationMessage(resolvedType);
				if (string.IsNullOrEmpty(message) == false) {
					messageStyle.normal.textColor = Color.red;
				} else {
					switch (IsOutOfDate(resolvedType)) {
						case OutOfDateResult.NoAot:
							message = "No AOT model found";
							break;
						case OutOfDateResult.Stale:
							message = "Stale";
							break;
						case OutOfDateResult.Current:
							message = "\u2713";
							break;
					}
				}
			} else {
				message = "Cannot load type";
			}

			GUILayout.Label(message, messageStyle);

			EditorGUILayout.EndHorizontal();
		}

		private string GetAotCompilationMessage(Type type) {
			try {
				fsMetaType.Get(new fsConfig(), type).EmitAotData(true);
			} catch (Exception e) {
				return e.Message;
			}
			return "";
		}

		private Vector2 _scrollPos;
		public override void OnInspectorGUI() {
			var target = (fsAotConfiguration)this.target;

			if (GUILayout.Button("Compile")) {
				if (Directory.Exists(target.outputDirectory) == false)
					Directory.CreateDirectory(target.outputDirectory);

				foreach (fsAotConfiguration.Entry entry in target.aotTypes) {
					if (entry.State == fsAotConfiguration.AotState.Enabled) {
						Type resolvedType = fsTypeCache.GetType(entry.FullTypeName);
						if (resolvedType == null) {
							Debug.LogError("Cannot find type " + entry.FullTypeName);
							continue;
						}

						try {
							string compilation = fsAotCompilationManager.RunAotCompilationForType(new fsConfig(), resolvedType);
							string path = Path.Combine(target.outputDirectory, "AotConverter_" + resolvedType.CSharpName(true, true) + ".cs");
							File.WriteAllText(path, compilation);
						} catch (Exception e) {
							Debug.LogWarning("AOT compiling " + resolvedType.CSharpName(true) + " failed: " + e.Message);
						}
					}
				}
				AssetDatabase.Refresh();
			}

			target.outputDirectory = EditorGUILayout.TextField("Output Directory", target.outputDirectory);

			EditorGUILayout.BeginHorizontal();
			GUILayout.Label("Set All");
			int newIndex = GUILayout.Toolbar(-1, options, GUILayout.ExpandWidth(false));
			GUILayout.FlexibleSpace();
			EditorGUILayout.EndHorizontal();
			if (newIndex != -1) {
				var newState = fsAotConfiguration.AotState.Default;
				if (newIndex == 0)
					newState = fsAotConfiguration.AotState.Enabled;
				else if (newIndex == 1)
					newState = fsAotConfiguration.AotState.Disabled;

				for (int i = 0; i < target.aotTypes.Count; ++i) {
					var entry = target.aotTypes[i];
					entry.State = newState;
					target.aotTypes[i] = entry;
				}
			}


			_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
			foreach (fsAotConfiguration.Entry entry in target.aotTypes) {
				Type resolvedType = fsTypeCache.GetType(entry.FullTypeName);
				EditorGUI.BeginDisabledGroup(resolvedType == null || string.IsNullOrEmpty(GetAotCompilationMessage(resolvedType)) == false);
				DrawType(entry, resolvedType);
				EditorGUI.EndDisabledGroup();
			}
			EditorGUILayout.EndScrollView();
		}

	}
}
#endif