#if (UNITY_4_3 || UNITY_4_5 || UNITY_4_6 || UNITY_4_7 || UNITY_5_0 || UNITY_5_1 || UNITY_5_2)
#define UNITY_PRE_5_3
#endif

#if (UNITY_4_3 || UNITY_4_5 || UNITY_4_6 || UNITY_4_7)
#define UNITY_PRE_5_0
#endif

#if UNITY_5_3_OR_NEWER || UNITY_5
#define UNITY_5_OR_NEWER
#endif

// Unity 5.1 introduced a new networking library. 
// Unless we define PLAYMAKER_LEGACY_NETWORK old network actions are disabled
#if !(UNITY_4_3 || UNITY_4_5 || UNITY_4_6 || UNITY_4_7 || UNITY_5_0 || PLAYMAKER_LEGACY_NETWORK)
#define UNITY_NEW_NETWORK
#endif

// Some platforms do not support networking (at least the old network library)
#if (UNITY_FLASH || UNITY_NACL || UNITY_METRO || UNITY_WP8 || UNITY_WIIU || UNITY_PSM || UNITY_WEBGL || UNITY_PS3 || UNITY_PS4 || UNITY_XBOXONE)
#define PLATFORM_NOT_SUPPORTED
#endif

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using HutongGames.PlayMaker;
using UnityEditor;
#if !UNITY_PRE_5_3
using UnityEditor.SceneManagement;
#endif
using UnityEngine;
using UnityEngine.SceneManagement;
using Debug = UnityEngine.Debug;

namespace HutongGames.PlayMakerEditor
{
    public class ProjectTools
    {
        // Change MenuRoot to move the Playmaker Menu
        // E.g., MenuRoot = "Plugins/PlayMaker/"
        private const string MenuRoot = "PlayMaker/";

        [MenuItem(MenuRoot + "Tools/Update All Loaded FSMs", false, 25)]
        public static void UpdateAllLoadedFSMs()
        {
            ReSaveAllLoadedFSMs();
        }

        [MenuItem(MenuRoot + "Tools/Update All FSMs in Build", false, 26)]
        public static void UpdateAllFSMsInBuild()
        {
            UpdateScenesInBuild();
        }

        [MenuItem(MenuRoot + "Tools/Preprocess Prefab FSMs in Build", false, 27)]
        public static void PreprocessPrefabFSMs()
        {   
            if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) return;

            var timer = Stopwatch.StartNew();

            var logOutput = DoPreprocessPrefabFSMsInBuild();

            logOutput += "\nElapsed Time: " + timer.Elapsed.Seconds + "s";

            Debug.Log(logOutput);
        }

        [MenuItem(MenuRoot + "Tools/Preprocess All Prefab FSMs", false, 27)]
        public static void PreprocessAllPrefabFSMs()
        {
            var timer = Stopwatch.StartNew();

            var logOutput = DoPreprocessAllPrefabFSMs();

            logOutput += "\nElapsed Time: " + timer.Elapsed.Seconds + "s";

            Debug.Log(logOutput);
        }


        /*WIP
        [MenuItem(MenuRoot + "Tools/Scan Scenes", false, 33)]
        public static void ScanScenesInProject()
        {
            FindAllScenes();
        }
*/

        /// <summary>
        /// Collects all prefabs with FSMs referenced by scenes in the build.
        /// Then preprocess all FSMs on those prefabs.
        /// TODO: check if this handles:
        /// - PlayMakerFSMs referenced in actions
        /// - PlayMakerFSMs in Resources folders
        /// </summary>
        private static string DoPreprocessPrefabFSMsInBuild()
        {
            var loadedScenes = GetLoadedScenes();

            var report = "Preprocess Prefab FSMs in Build:";

            // We open enabled scenes in the build to find prefabs with PlayMakerFSMs
            // We do this first to avoid an AssetDatabase reload for each scene
            // Processing fsms in the scene load loop is much slower

            var prefabs = GetAllPrefabFSMsInBuild();
            report += "\nPrefab FSMs found: " + prefabs.Count;

            if (prefabs.Count == 0) return report;

            // Note: StartAssetEditing/StopAssetEditing doesn't seem to work across scene loading.
            // another reason to collect all the prefabs first.

            AssetDatabase.StartAssetEditing();

            report += DoPreprocessPrefabFSMs(prefabs);

            AssetDatabase.StopAssetEditing();

            LoadScenes(loadedScenes);

            return report;
        }

        /// <summary>
        /// Get the main GameObject for a prefab file.
        /// In 2018.3+ loads the prefab so it can be edited.
        /// </summary>
        public static GameObject GetPrefabGameObject(string prefabPath)
        {
            GameObject go = null;

            // We need to put this in a try block otherwise it can hang Unity editor

            try
            {
#if UNITY_2018_3_OR_NEWER

                go = PrefabUtility.LoadPrefabContents(prefabPath);
#else
                go = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
#endif
            }
            catch (Exception e)
            {
                Debug.LogException(e);
            }

            return go;
        }

        /// <summary>
        /// Get all scenes currently open in the editor
        /// </summary>
        public static List<string> GetLoadedScenes()
        {
            var openScenes = new List<string>();
            for (var i = 0; i < EditorSceneManager.loadedSceneCount; i++)
            {
                openScenes.Add(SceneManager.GetSceneAt(i).path);
            }

            return openScenes;    
        }

        /// <summary>
        /// Get all enabled scenes in Build Settings.
        /// </summary>
        public static List<string> GetScenesInBuild()
        {
            return (from scene in EditorBuildSettings.scenes where scene.enabled select scene.path).ToList();
        }

        /// <summary>
        /// Load multiple scenes in the editor.
        /// </summary>
        /// <param name="scenes"></param>
        public static void LoadScenes(List<string> scenes)
        {
            if (scenes.Count == 0 || string.IsNullOrEmpty(scenes[0])) return;
            
            var loadedScenes = GetLoadedScenes();
            if (loadedScenes.SequenceEqual(scenes)) return;

            EditorSceneManager.OpenScene(scenes[0], OpenSceneMode.Single);
            for (var i = 1; i < scenes.Count; i++)
            {
                EditorSceneManager.OpenScene(scenes[i], OpenSceneMode.Additive);
            }
        }

        /// <summary>
        /// Get all prefab files in build that have PlayMakerFSMs
        /// </summary>
        public static List<string> GetAllPrefabFSMsInBuild()
        {
            var scenes = GetScenesInBuild();
            var loadedScenes = GetLoadedScenes();
            var prefabs = new List<string>();

            // first get prefab FSMs that are already loaded

            GetLoadedPrefabFSMs(prefabs);

            foreach (var scene in loadedScenes)
            {
                scenes.Remove(scene);
            }

            // now iterate through other scenes in build

            float sceneCount = scenes.Count;
            if (sceneCount > 0)
            {
                for (var i = 0; i < scenes.Count; i++)
                {
                    var scene = scenes[i];
                    EditorSceneManager.OpenScene(scene, OpenSceneMode.Single);

                    var cancel = EditorUtility.DisplayCancelableProgressBar("PlayMaker",
                        "Finding prefab FSMs in scene: " + scene, i / sceneCount);
                    if (cancel) return new List<string>();

                    GetLoadedPrefabFSMs(prefabs);
                }

                EditorUtility.ClearProgressBar();
            }
            
            return prefabs;
        }

        /// <summary>
        /// Get loaded prefab files that have PlayMakerFSMs
        /// </summary>
        public static void GetLoadedPrefabFSMs(List<string> prefabs)
        {
            var fsmList = Resources.FindObjectsOfTypeAll<PlayMakerFSM>();
            for (var i = 0; i < fsmList.Length; i++)
            {
                var playMakerFSM = fsmList[i];
                if (playMakerFSM == null || !FsmPrefabs.IsPrefab(playMakerFSM.Fsm)) continue;

                var assetPath = AssetDatabase.GetAssetPath(playMakerFSM);
                if (prefabs.Contains(assetPath)) continue;

                prefabs.Add(assetPath);
            }
        }

        private static string DoPreprocessPrefabFSMs(List<string> prefabs)
        {
            var report = "";
            if (prefabs.Count == 0) return report;

            float prefabCount = prefabs.Count;           
            for (int i = 0; i < prefabCount; i++)
            {
                var prefab = prefabs[i];
                if (prefab == null) continue;

                var cancel = EditorUtility.DisplayCancelableProgressBar("PlayMaker", prefab.Substring(7), i/prefabCount);
                if (cancel) return report + "\nCancelled";
                
                var go = GetPrefabGameObject(prefab);
                if (go == null) continue;

                var prefabFSMs = go.GetComponentsInChildren<PlayMakerFSM>();
                if (prefabFSMs.Length > 0)
                {
                    foreach (var prefabFSM in prefabFSMs)
                    {                

#if UNITY_2018_3_OR_NEWER
                    
                        // nested prefab will be processes as a prefab, so skip here
                        if (PrefabUtility.IsPartOfPrefabInstance(prefabFSM))
                        {
                            //report += "\nSkipping Nested Prefab: " + FsmUtility.GetFullFsmLabel(prefabFSM);
                            continue;
                        }   
#endif

                        //report += "\nPreprocess: " + FsmUtility.GetFullFsmLabel(prefabFSM);

                        prefabFSM.Preprocess();
                    }

                    EditorUtility.SetDirty(go);

#if UNITY_2018_3_OR_NEWER                

                    PrefabUtility.SaveAsPrefabAsset(go, prefab);
                }

                PrefabUtility.UnloadPrefabContents(go);
#else
                }
#endif
            }

            EditorUtility.ClearProgressBar();

            return report;
        }


        /// <summary>
        /// 
        /// </summary>
        private static string DoPreprocessAllPrefabFSMs()
        {
            var report = "Preprocess All Prefab FSMs:";

            AssetDatabase.StartAssetEditing();

            var prefabs = Files.GetPrefabsWithFsmComponent();
            report += "\nPrefabs found: " + prefabs.Count;

            if (prefabs.Count == 0) return report;

            float prefabCount = prefabs.Count;
            
            for (int i = 0; i < prefabCount; i++)
            {
                var prefab = prefabs[i];
                if (prefab == null) continue;

                var cancel = EditorUtility.DisplayCancelableProgressBar("PlayMaker", prefab, i/prefabCount);
                if (cancel) return report + "\nCancelled";
                
                var go = GetPrefabGameObject(prefab);
                if (go == null) continue;

                var prefabFSMs = go.GetComponentsInChildren<PlayMakerFSM>();
                if (prefabFSMs.Length > 0)
                {
                    foreach (var prefabFSM in prefabFSMs)
                    {                

#if UNITY_2018_3_OR_NEWER
                    
                        // nested prefab will be processes as a prefab, so skip here
                        if (PrefabUtility.IsPartOfPrefabInstance(prefabFSM))
                        {
                            //report += "\nSkipping Nested Prefab: " + FsmUtility.GetFullFsmLabel(prefabFSM);
                            continue;
                        }   
#endif

                        //report += "\nPreprocess: " + FsmUtility.GetFullFsmLabel(prefabFSM);

                        prefabFSM.Preprocess();
                    }

                    EditorUtility.SetDirty(go);

#if UNITY_2018_3_OR_NEWER                

                    PrefabUtility.SaveAsPrefabAsset(go, prefab);
                }
                PrefabUtility.UnloadPrefabContents(go);
#else
                }
#endif
            }

            EditorUtility.ClearProgressBar();

            AssetDatabase.StopAssetEditing();

            return report;
        }

        private static List<string> GetAllPrefabFiles()
        {
            var searchDirectory = new DirectoryInfo(Application.dataPath);
            var prefabFiles = searchDirectory.GetFiles("*.prefab", SearchOption.AllDirectories);

            var paths = new List<string>();
            float totalFiles = prefabFiles.Length;

            for (int i = 0; i < prefabFiles.Length; i++)
            {
                var file = prefabFiles[i];
                EditorUtility.DisplayProgressBar("PlayMaker", "Finding prefabs...",
                    i / totalFiles);

                var filePath = file.FullName.Replace('\\', '/').Replace(Application.dataPath, "Assets");
                //Debug.Log(filePath + "\n" + Application.dataPath);

                paths.Add(filePath);
            }

            EditorUtility.ClearProgressBar();

            return paths;
        }

        private static void ReSaveAllLoadedFSMs()
        {
            Debug.Log("Checking loaded FSMs...");
            FsmEditor.RebuildFsmList();
            foreach (var fsm in FsmEditor.FsmList)
            {
                // Re-initialize loads data and forces a dirty check
                // so we can just call this and let it handle dirty etc.

                fsm.Reinitialize();
            }
        }

        private static void UpdateScenesInBuild()
        {
            // Allow the user to save his work!
#if UNITY_PRE_5_3
            if (!EditorApplication.SaveCurrentSceneIfUserWantsTo())
#else
            if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
#endif
            {
                return;
            }

            LoadPrefabsWithPlayMakerFSMComponents();

            foreach (var scene in EditorBuildSettings.scenes)
            {
                Debug.Log("Open Scene: " + scene.path);
#if UNITY_PRE_5_3
                EditorApplication.OpenScene(scene.path);
#else
                EditorSceneManager.OpenScene(scene.path);
#endif

#if UNITY_5_OR_NEWER                
                UpdateGetSetPropertyActions();
#endif

                ReSaveAllLoadedFSMs();

#if UNITY_PRE_5_3
                if (!EditorApplication.SaveScene())
#else
                if (!EditorSceneManager.SaveOpenScenes())
#endif
                {
                    Debug.LogError("Could not save scene!");
                }
            }
        }

        private static void LoadPrefabsWithPlayMakerFSMComponents()
        {
            Debug.Log("Finding Prefabs with PlayMakerFSMs");

            var searchDirectory = new DirectoryInfo(Application.dataPath);
            var prefabFiles = searchDirectory.GetFiles("*.prefab", SearchOption.AllDirectories);

            foreach (var file in prefabFiles)
            {
                var filePath = file.FullName.Replace(@"\", "/").Replace(Application.dataPath, "Assets");
                //Debug.Log(filePath + "\n" + Application.dataPath);

                var dependencies = AssetDatabase.GetDependencies(new[] { filePath });
                foreach (var dependency in dependencies)
                {
                    if (dependency.Contains("/PlayMaker.dll"))
                    {
                        Debug.Log("Found Prefab with FSM: " + filePath);
                        AssetDatabase.LoadAssetAtPath(filePath, typeof(GameObject));
                    }
                }
            }

            FsmEditor.RebuildFsmList();
        }

#if UNITY_5_OR_NEWER

        private static void UpdateGetSetPropertyActions()
        {
            var getPropertyActionType = ActionData.GetActionType("HutongGames.PlayMaker.Actions.GetProperty");
            var setPropertyActionType = ActionData.GetActionType("HutongGames.PlayMaker.Actions.SetProperty");

            FsmEditor.RebuildFsmList();

            foreach (var fsm in FsmEditor.FsmList)
            {
                if (fsm.GameObject == null) continue; // can't update property paths without GameObject

                var upgraded = false;
                foreach (var state in fsm.States)
                {
                    foreach (var action in state.Actions)
                    {
                        var actionType = action.GetType();
                        if (actionType == getPropertyActionType)
                        {
                            var targetPropertyField = getPropertyActionType.GetField("targetProperty", BindingFlags.Public | BindingFlags.Instance);
                            if (targetPropertyField != null)
                            {
                                upgraded |= TryUpgradeFsmProperty(fsm.GameObject, targetPropertyField.GetValue(action) as FsmProperty);
                            }
                        }
                        else if (actionType == setPropertyActionType)
                        {
                            var targetPropertyField = setPropertyActionType.GetField("targetProperty", BindingFlags.Public | BindingFlags.Instance);
                            if (targetPropertyField != null)
                            {
                                upgraded |= TryUpgradeFsmProperty(fsm.GameObject, targetPropertyField.GetValue(action) as FsmProperty);
                            }
                        }
                    }
                }
                if (upgraded)
                {
                    //Undo called in batch operation seems to crash Unity
                    //FsmEditor.SaveActions(fsm);

                    foreach (var state in fsm.States)
				    {
					    state.SaveActions();
				    }

				    FsmEditor.SetFsmDirty(fsm, true);

#if !UNITY_PRE_5_3
                    UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(fsm.Owner.gameObject.scene);
#elif !UNITY_PRE_5_0
                    // Not sure if we need to do this...?
                    UnityEditor.EditorApplication.MarkSceneDirty();
#endif
                }
            }
        }

        private static bool TryUpgradeFsmProperty(GameObject gameObject, FsmProperty fsmProperty)
        {
            if (gameObject == null || fsmProperty == null) return false;
            var propertyPath = fsmProperty.PropertyName;
            if (string.IsNullOrEmpty(propertyPath)) return false;

            var parts = propertyPath.Split('.');
            if (TryFindComponent(gameObject, fsmProperty, parts[0]))
            {
                var oldPath = fsmProperty.PropertyName;
                fsmProperty.PropertyName = string.Join(".", parts, 1, parts.Length-1);
                Debug.Log("Fixed: " + oldPath + "->" + fsmProperty.PropertyName);
                return true;
            }

            return false;
        }

        private static bool TryFindComponent(GameObject gameObject, FsmProperty fsmProperty, string component)
        {
            if (component == "rigidbody") return FixFsmProperty(gameObject, fsmProperty, typeof(Rigidbody));
            if (component == "rigidbody2D") return FixFsmProperty(gameObject, fsmProperty, typeof(Rigidbody2D));
            if (component == "camera") return FixFsmProperty(gameObject, fsmProperty, typeof(Camera));
            if (component == "light") return FixFsmProperty(gameObject, fsmProperty, typeof(Light));
            if (component == "animation") return FixFsmProperty(gameObject, fsmProperty, typeof(Animation));
            if (component == "constantForce") return FixFsmProperty(gameObject, fsmProperty, typeof(ConstantForce));
            if (component == "renderer") return FixFsmProperty(gameObject, fsmProperty, typeof(Renderer));
            if (component == "audio") return FixFsmProperty(gameObject, fsmProperty, typeof(AudioSource));
#if !UNITY_2017_2_OR_NEWER
            if (component == "guiText") return FixFsmProperty(gameObject, fsmProperty, typeof(GUIText));
            if (component == "guiTexture") return FixFsmProperty(gameObject, fsmProperty, typeof(GUITexture));
#endif
            if (component == "collider") return FixFsmProperty(gameObject, fsmProperty, typeof(Collider));
            if (component == "collider2D") return FixFsmProperty(gameObject, fsmProperty, typeof(Collider2D));
            if (component == "hingeJoint") return FixFsmProperty(gameObject, fsmProperty, typeof(HingeJoint));
            if (component == "particleSystem") return FixFsmProperty(gameObject, fsmProperty, typeof(ParticleSystem));

#if !UNITY_5_4_OR_NEWER
            if (component == "particleEmitter") return FixFsmProperty(gameObject, fsmProperty, typeof(ParticleEmitter));
#endif

#if !(PLATFORM_NOT_SUPPORTED || UNITY_NEW_NETWORK || PLAYMAKER_NO_NETWORK)
            if (component == "networkView") return FixFsmProperty(gameObject, fsmProperty, typeof(NetworkView));
#endif
            return false;
        }

        private static bool FixFsmProperty(GameObject gameObject, FsmProperty fsmProperty, Type componentType)
        {
            fsmProperty.TargetObject.Value = gameObject.GetComponent(componentType);
            fsmProperty.TargetObject.ObjectType = componentType;
            return true;
        }
#endif

        /* WIP
        [Localizable(false)]
        private static void FindAllScenes()
        {
            Debug.Log("Finding all scenes...");

            var searchDirectory = new DirectoryInfo(Application.dataPath);
            var assetFiles = searchDirectory.GetFiles("*.unity", SearchOption.AllDirectories);

            foreach (var file in assetFiles)
            {
                var filePath = file.FullName.Replace(@"\", "/").Replace(Application.dataPath, "Assets");
                var obj = AssetDatabase.LoadAssetAtPath(filePath, typeof(Object));
                if (obj == null)
                {
                    //Debug.Log(filePath + ": null!");
                }
                else if (obj.GetType() == typeof(Object))
                {
                    Debug.Log(filePath);// + ": " + obj.GetType().FullName);
                }
                //var obj = AssetDatabase.
            }
        }
         */
    }
}

