// Animancer // Copyright 2018-2024 Kybernetik //

#if UNITY_EDITOR

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

namespace Animancer.Editor
{
    /// <summary>[Editor-Only]
    /// A window for managing a copy of some serialized data and applying or reverting it.
    /// </summary>
    /// <remarks>
    /// This system assumes the implementation of <see cref="IEquatable{T}"/>
    /// compares the values of all fields in <typeparamref name="TData"/>.
    /// </remarks>
    /// https://kybernetik.com.au/animancer/api/Animancer.Editor/SerializedDataEditorWindow_2
    public abstract class SerializedDataEditorWindow<TObject, TData> : EditorWindow
        where TObject : Object
        where TData : class, ICopyable<TData>, IEquatable<TData>, new()
    {
        /************************************************************************************************************************/

        [SerializeField]
        private TObject _SourceObject;

        /// <summary>The object which contains the data this class manages.</summary>
        /// <remarks><see cref="SetAndCaptureSource"/> should generally be used instead of setting this property directly.</remarks>
        public virtual TObject SourceObject
        {
            get => _SourceObject;
            protected set => _SourceObject = value;
        }

        /************************************************************************************************************************/

        /// <summary>The <see cref="Data"/> field of the <see cref="SourceObject"/>.</summary>
        public abstract TData SourceData { get; set; }

        /************************************************************************************************************************/

        [SerializeField]
        private TData _Data;

        /// <summary>A copy of the <see cref="SourceData"/> being managed by this window.</summary>
        public ref TData Data
            => ref _Data;

        /************************************************************************************************************************/

        /// <summary>Is the <see cref="Data"/> managed by this window different to the <see cref="SourceData"/>.</summary>
        public bool HasDataChanged
        {
            get
            {
                try
                {
                    return _Data != null && !_Data.Equals(SourceData);
                }
                catch (Exception exception)
                {
                    Debug.LogException(exception);
                    return false;
                }
            }
        }

        /************************************************************************************************************************/

        /// <summary>Initializes this window.</summary>
        protected virtual void OnEnable()
        {
            EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
            EditorApplication.wantsToQuit += OnTryCloseEditor;
            Undo.undoRedoPerformed += Repaint;
        }

        /// <summary>Cleans up this window.</summary>
        protected virtual void OnDisable()
        {
            EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
            EditorApplication.wantsToQuit -= OnTryCloseEditor;
            Undo.undoRedoPerformed -= Repaint;
        }

        /************************************************************************************************************************/

        /// <summary>
        /// Prompts the user to <see cref="Apply"/> or <see cref="Revert"/>
        /// if there are changes in the <see cref="Data"/> when this window is closed.
        /// </summary>
        protected virtual void OnDestroy()
        {
            var sourceObject = SourceObject;
            if (sourceObject == null ||
                !HasDataChanged ||
                titleContent == null)
                return;

            if (EditorUtility.DisplayDialog(
                titleContent.text,
                $"Apply unsaved changes to '{sourceObject.name}'?",
                "Apply",
                "Revert"))
            {
                Apply();
            }
        }

        /************************************************************************************************************************/

        /// <summary>Called before closing the Unity Editor to confirm that un-saved data is applied.</summary>
        private bool OnTryCloseEditor()
        {
            var sourceObject = SourceObject;
            if (sourceObject == null ||
                !HasDataChanged ||
                titleContent == null)
                return true;

            var option = EditorUtility.DisplayDialogComplex(
                titleContent.text,
                $"Apply unsaved changes to '{sourceObject.name}'?",
                "Apply",
                "Cancel",
                "Revert");

            switch (option)
            {
                case 0:// Apply.
                    Apply();
                    return true;

                case 2:// Revert.
                    Revert();
                    return true;

                case 1:// Cancel.
                default:
                    return false;
            }
        }

        /************************************************************************************************************************/

        /// <summary>
        /// Sets the <see cref="SourceObject"/> and captures the <see cref="Data"/>
        /// as a copy of its <see cref="SourceData"/>.
        /// </summary>
        protected void SetAndCaptureSource(TObject sourceObject)
        {
            _SourceObject = sourceObject;
            CaptureData();
            Repaint();
        }

        /************************************************************************************************************************/

        /// <summary>
        /// Override this to return <c>true</c> if the <see cref="SourceObject"/> could be part of a prefab
        /// to ensure that modifications are serialized properly.
        /// </summary>
        public virtual bool SourceObjectMightBePrefab
            => false;

        /************************************************************************************************************************/

        /// <summary>Saves the edited <see cref="Data"/> into the <see cref="SourceObject"/>.</summary>
        public virtual void Apply()
        {
            var sourceObject = SourceObject;
            if (sourceObject == null)
                return;

            using (new ModifySerializedField(sourceObject, name, SourceObjectMightBePrefab))
            {
                SourceData = _Data.CopyableClone();

                if (EditorUtility.IsPersistent(SourceObject))
                {
                    var objects = SetPool.Acquire<Object>();
                    GatherObjectReferences(sourceObject, objects);

                    foreach (var obj in objects)
                        if (!EditorUtility.IsPersistent(obj))
                            AssetDatabase.AddObjectToAsset(obj, SourceObject);

                    SetPool.Release(objects);
                }
            }

            Repaint();
            AssetDatabase.SaveAssets();
        }

        /************************************************************************************************************************/

        /// <summary>Gathers all objects referenced by the `root`.</summary>
        public static void GatherObjectReferences(Object root, HashSet<Object> objects)
        {
            using var serializedObject = new SerializedObject(root);
            var property = serializedObject.GetIterator();
            while (property.Next(true))
            {
                if (property.propertyType == SerializedPropertyType.ObjectReference)
                {
                    var value = property.objectReferenceValue;
                    if (value != null)
                        objects.Add(value);
                }
            }
        }

        /************************************************************************************************************************/

        /// <summary>Restores the <see cref="Data"/> to the original values from the <see cref="SourceData"/>.</summary>
        public virtual void Revert()
        {
            RecordUndo();
            CaptureData();
        }

        /************************************************************************************************************************/

        /// <summary>Stores a copy of the <see cref="SourceData"/> in the <see cref="Data"/>.</summary>
        protected virtual void CaptureData()
        {
            _Data = SourceData?.CopyableClone() ?? new();
            AnimancerReflection.TryInvoke(_Data, "OnValidate");
        }

        /************************************************************************************************************************/

        /// <summary>Records the current state of this window so it can be undone later.</summary>
        public TData RecordUndo()
            => RecordUndo(titleContent.text);

        /// <summary>Records the current state of this window so it can be undone later.</summary>
        public virtual TData RecordUndo(string name)
        {
            Undo.RecordObject(this, name);
            Repaint();
            return _Data;
        }

        /************************************************************************************************************************/

        /// <summary>
        /// Opens a new <typeparamref name="TWindow"/> for the `sourceObject`
        /// or gives focus to an existing window that was already displaying it.
        /// </summary>
        public static TWindow Open<TWindow>(
            TObject sourceObject,
            bool onlyOneWindow = false,
            params Type[] desiredDockNextTo)
            where TWindow : SerializedDataEditorWindow<TObject, TData>
        {
            if (!onlyOneWindow)
            {
                foreach (var window in Resources.FindObjectsOfTypeAll<TWindow>())
                {
                    if (window.SourceObject == sourceObject)
                    {
                        window.Show();
                        window.SetAndCaptureSource(sourceObject);
                        window.Focus();
                        return window;
                    }
                }
            }

            var newWindow = onlyOneWindow
                ? GetWindow<TWindow>(desiredDockNextTo ?? Type.EmptyTypes)
                : CreateInstance<TWindow>();
            newWindow.Show();
            newWindow.SetAndCaptureSource(sourceObject);
            return newWindow;
        }

        /************************************************************************************************************************/
        #region Auto Apply
        /************************************************************************************************************************/

        /// <summary>The <see cref="EditorPrefs"/> key for <see cref="AutoApply"/>.</summary>
        protected virtual string AutoApplyPref
            => $"{titleContent.text}.{nameof(AutoApply)}";

        /************************************************************************************************************************/

        private bool _HasLoadedAutoApply;
        private bool _AutoApply;
        private bool _EnabledAutoApplyInPlayMode;

        /// <summary>Is the "Auto Apply" toggle currently enabled?</summary>
        public bool AutoApply
        {
            get
            {
                if (!_HasLoadedAutoApply)
                {
                    _HasLoadedAutoApply = true;
                    _AutoApply = EditorPrefs.GetBool(AutoApplyPref);
                }

                return _AutoApply;
            }
            set
            {
                _HasLoadedAutoApply = true;
                _AutoApply = value;
                _EnabledAutoApplyInPlayMode = _AutoApply && EditorApplication.isPlayingOrWillChangePlaymode;
                EditorPrefs.SetBool(AutoApplyPref, value);
            }
        }

        /************************************************************************************************************************/

        /// <summary>Handles entering and exiting Play Mode.</summary>
        protected virtual void OnPlayModeStateChanged(PlayModeStateChange change)
        {
            switch (change)
            {
                case PlayModeStateChange.EnteredPlayMode:
                    if (HasDataChanged && focusedWindow != null)
                        focusedWindow.ShowNotification(new($"{titleContent.text} window has un-applied changes"));
                    break;

                case PlayModeStateChange.ExitingPlayMode:
                    if (_EnabledAutoApplyInPlayMode)
                        AutoApply = false;
                    break;
            }
        }

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
        #region GUI
        /************************************************************************************************************************/

        private static readonly GUIContent
            RevertLabel = new(
                "Revert",
                "Undo all changes made in this window"),
            ApplyLabel = new(
                "Apply",
                "Apply all changes made in this window to the source object"),
            AutoApplyLabel = new(
                "Auto",
                "Immediately apply all changes made in this window to the source object?" +
                "\n\nIf enabled in Play Mode, this toggle will be disabled when returning to Edit Mode.");

        /************************************************************************************************************************/

        /// <summary>
        /// Calculates the pixel width required for
        /// <see cref="DoApplyRevertGUI(Rect, Rect, Rect, ButtonGroupStyles)"/>.
        /// </summary>
        public float CalculateApplyRevertWidth(ButtonGroupStyles styles = default)
        {
            styles.CopyMissingStyles(ButtonGroupStyles.Button);
            return
                styles.left.CalculateWidth(RevertLabel) +
                styles.middle.CalculateWidth(ApplyLabel) +
                styles.right.CalculateWidth(AutoApplyLabel);
        }

        /************************************************************************************************************************/

        /// <summary>Draws GUI controls for <see cref="Revert"/>, <see cref="Apply"/>, and <see cref="AutoApply"/>.</summary>
        public void DoApplyRevertGUI(ButtonGroupStyles styles = default)
        {
            styles.CopyMissingStyles(ButtonGroupStyles.Button);

            GUILayout.BeginHorizontal();

            var leftArea = GUILayoutUtility.GetRect(RevertLabel, styles.left);
            var middleArea = GUILayoutUtility.GetRect(ApplyLabel, styles.middle);
            var rightArea = GUILayoutUtility.GetRect(AutoApplyLabel, styles.right);

            DoApplyRevertGUI(leftArea, middleArea, rightArea, styles);

            GUILayout.EndHorizontal();
        }

        /************************************************************************************************************************/

        /// <summary>Draws GUI controls for <see cref="Revert"/>, <see cref="Apply"/>, and <see cref="AutoApply"/>.</summary>
        public void DoApplyRevertGUI(Rect area, ButtonGroupStyles styles = default)
        {
            styles.CopyMissingStyles(ButtonGroupStyles.Button);

            var leftArea = AnimancerGUI.StealFromLeft(ref area, styles.left.CalculateWidth(RevertLabel));
            var middleArea = AnimancerGUI.StealFromLeft(ref area, styles.middle.CalculateWidth(ApplyLabel));

            DoApplyRevertGUI(leftArea, middleArea, area, styles);
        }

        /************************************************************************************************************************/

        /// <summary>Draws GUI controls for <see cref="Revert"/>, <see cref="Apply"/>, and <see cref="AutoApply"/>.</summary>
        public void DoApplyRevertGUI(
            Rect leftArea,
            Rect middleArea,
            Rect rightArea,
            ButtonGroupStyles styles = default)
        {
            styles.CopyMissingStyles(ButtonGroupStyles.Button);

            var enabled = GUI.enabled;
            GUI.enabled = SourceObject != null && HasDataChanged;

            // Revert.
            if (GUI.Button(leftArea, RevertLabel, styles.left))
                Revert();

            // Apply.
            if (GUI.Button(middleArea, ApplyLabel, styles.middle))
                Apply();

            // Auto Apply.
            var autoApply = AutoApply;
            if (autoApply && GUI.enabled)
                Apply();

            GUI.enabled = enabled;

            if (autoApply != GUI.Toggle(rightArea, autoApply, AutoApplyLabel, styles.right))
                AutoApply = !autoApply;
        }

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
    }
}

#endif
