using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using UnityEngine.UIElements;
using System.Reflection;
using UnityEditor.UIElements;
namespace Unity.Cinemachine.Editor
{
    /// 
    /// Helpers for drawing CinemachineCamera inspectors.
    /// 
    static class CmCameraInspectorUtility
    {
        struct PipelineStageItem
        {
            public CinemachineCore.Stage Stage;
            public DropdownField Dropdown;
            public Label WarningIcon;
        }
        static bool IsPrefab(UnityEngine.Object target)
        {
            var t = target as CinemachineVirtualCameraBase;
            return t != null && PrefabUtility.IsPartOfPrefabAsset(t);
        }
        static Color s_NormalColor = Color.black;
        static Color s_NormalBkgColor = Color.black;
        
        /// Add the camera status controls and indicators in the inspector
        public static void AddCameraStatus(this UnityEditor.Editor editor, VisualElement ux)
        {
            // No status and Solo for prefabs or multi-select
            if (Selection.objects.Length > 1 || IsPrefab(editor.target))
                return;
            
            var cameraParentingMessage = ux.AddChild(new HelpBox(
                $"Setup error: {editor.target.GetType().Name} should not be a child "
                + "of CinemachineCamera or CinemachineBrain.\n\n"
                + "Best practice is to have CinemachineCamera, CinemachineBrain, and camera targets as "
                + "separate objects, not parented to each other.", 
                HelpBoxMessageType.Error));
            var noBrainMessage = ux.AddChild(InspectorUtility.HelpBoxWithButton(
                "A CinemachineBrain is required in the scene.", 
                HelpBoxMessageType.Warning, "Add Brain", () => CinemachineMenu.GetOrCreateBrain()));
            var navelGazeMessage = ux.AddChild(new HelpBox(
                "The camera is trying to look at itself.", 
                HelpBoxMessageType.Warning));
            var row = ux.AddChild(new InspectorUtility.LabeledRow("Status"));
            var statusText = row.Label;
            var soloButton = row.Contents.AddChild(new Button() 
            { 
                text = "Solo", 
                style = { flexGrow = 1, paddingLeft = 0, paddingRight = 0, 
                    marginLeft = 0, marginRight = 0, borderLeftWidth = 1, borderRightWidth = 1 } 
            });
            var updateMode = row.Contents.AddChild(new Label("(Update Mode)") { style = { flexGrow = 0, alignSelf = Align.Center }});
            updateMode.SetEnabled(false);
            updateMode.style.display = DisplayStyle.None;
            var target = editor.target as CinemachineVirtualCameraBase; // capture for lambda
            soloButton.RegisterCallback(_ =>
            {
                var isSolo = CinemachineCore.SoloCamera != (ICinemachineCamera)target;
                CinemachineCore.SoloCamera = isSolo ? target : null;
                InspectorUtility.RepaintGameView();
            });
            ux.TrackAnyUserActivity(() =>
            { 
                if (target == null)
                    return;
                // Is the camera navel-gazing?
                CameraState state = target.State;
                bool isNavelGazing = target.PreviousStateIsValid && state.HasLookAt() &&
                    (state.ReferenceLookAt - state.GetCorrectedPosition()).AlmostZero();
                if (isNavelGazing)
                {
                    var aim = target.GetCinemachineComponent(CinemachineCore.Stage.Aim);
                    if (aim == null || !aim.CameraLooksAtTarget)
                        isNavelGazing = false;
                }
                navelGazeMessage.SetVisible(isNavelGazing);
                // Is the camera parenting incorrect?
                cameraParentingMessage.SetVisible(
                    target.GetComponentInParent() != null 
                    || (target.ParentCamera != null && target.ParentCamera is not CinemachineCameraManagerBase));
                // Is there a Brain?
                noBrainMessage.SetVisible(CinemachineBrain.ActiveBrainCount == 0 && !IsPrefab(target));
            });
            // Capture "normal" colors
            if (s_NormalBkgColor == Color.black)
            {
                ux.OnInitialGeometry(() =>
                {
                    s_NormalColor = statusText.resolvedStyle.color;
                    s_NormalBkgColor = soloButton.resolvedStyle.backgroundColor;
                });
            }
            // Refresh camera state
            ux.ContinuousUpdate(() =>
            { 
                if (target == null)
                    return;
                bool isSolo = CinemachineCore.SoloCamera == (ICinemachineCamera)target;
                var color = isSolo ? Color.Lerp(s_NormalColor, CinemachineCore.SoloGUIColor(), 0.5f) : s_NormalColor;
                bool isLive = CinemachineCore.IsLive(target);
                statusText.text = isLive ? "Status: Live"
                    : target.isActiveAndEnabled ? "Status: Standby" : "Status: Disabled";
                statusText.SetEnabled(isLive);
                statusText.style.color = color;
                if (!Application.isPlaying)
                    updateMode.SetVisible(false);
                else
                {
                    var mode = CameraUpdateManager.GetVcamUpdateStatus(target);
                    updateMode.text = mode == UpdateTracker.UpdateClock.Fixed ? " Fixed Update" : " Late Update";
                    updateMode.SetVisible(true);
                }
                soloButton.style.color = color;
                soloButton.style.backgroundColor = isSolo 
                    ? Color.Lerp(s_NormalBkgColor, CinemachineCore.SoloGUIColor(), 0.2f) : s_NormalBkgColor;
                // Refresh the game view if solo and not playing
                if (isSolo && !Application.isPlaying)
                    InspectorUtility.RepaintGameView();
            });
            // Kill solo when inspector shuts down
            ux.RegisterCallback(_ =>
            {
                if (target != null && CinemachineCore.SoloCamera == (ICinemachineCamera)target)
                {
                    CinemachineCore.SoloCamera = null;
                    InspectorUtility.RepaintGameView();
                }
            });
        }
        public static void AddTransitionsSection(
            this UnityEditor.Editor editor, VisualElement ux, 
            List otherProperties = null)
        {
            var serializedObject = editor.serializedObject;
            var target = editor.target as CinemachineVirtualCameraBase;
            ux.Add(new PropertyField(serializedObject.FindProperty(() => target.Priority)));
            ux.Add(new PropertyField(serializedObject.FindProperty(() => target.OutputChannel)));
            ux.Add(new PropertyField(serializedObject.FindProperty(() => target.StandbyUpdate)));
            for (int i = 0; otherProperties != null && i < otherProperties.Count; ++i)
                ux.Add(new PropertyField(otherProperties[i]));
        }
        /// Add the pipeline control dropdowns in the inspector
        public static void AddPipelineDropdowns(this UnityEditor.Editor editor, VisualElement ux)
        {
            var target = editor.target as CinemachineCamera;
            if (target == null)
                return;
            var targets = editor.targets; // capture for lambda
            // Add a dropdown for each pipeline stage
            var pipelineItems = new List();
            for (int i = 0; i < PipelineStageMenu.s_StageData.Length; ++i)
            {
                // Skip empty categories
                if (PipelineStageMenu.s_StageData[i].Types.Count < 2)
                    continue;
                var stage = i; // capture for lambda
                var row = ux.AddChild(new InspectorUtility.LeftRightRow());
                row.Left.Add(new Label(PipelineStageMenu.s_StageData[stage].Name) 
                { 
                    tooltip = "Will add a Behaviour to implement this stage in the procedural pipeline", 
                    style = { flexGrow = 1, alignSelf = Align.Center }
                });
                var warningIcon = row.Left.AddChild(InspectorUtility.MiniHelpIcon("Component is disabled or has a problem"));
                warningIcon.SetVisible(false);
                int currentSelection = PipelineStageMenu.GetSelectedComponent(
                    i, target.GetCinemachineComponent((CinemachineCore.Stage)i));
                var dropdown = row.Right.AddChild(new DropdownField
                {
                    choices = PipelineStageMenu.s_StageData[stage].Choices,
                    index = currentSelection,
                    style = { flexGrow = 1 }
                });
                dropdown.RegisterValueChangedCallback(evt => 
                {
                    var newType = PipelineStageMenu.s_StageData[stage].Types[GetTypeIndexFromSelection(evt.newValue, stage)];
                    for (int j = 0; j < targets.Length; j++)
                    {
                        var t = targets[j] as CinemachineCamera;
                        if (t == null)
                            continue;
                        var oldComponents = t.GetComponents();
                        CinemachineComponentBase existingComponent = null;
                        for (int k = 0; k < oldComponents.Length; ++k)
                        {
                            if (existingComponent == null && oldComponents[k].GetType() == newType)
                                existingComponent = oldComponents[k];
                            else if (oldComponents[k].Stage == (CinemachineCore.Stage)stage)
                                Undo.DestroyObjectImmediate(oldComponents[k]);
                        }
                        if (newType != null && existingComponent == null)
                            Undo.AddComponent(t.gameObject, newType);
                        t.InvalidatePipelineCache();
                    }
                    static int GetTypeIndexFromSelection(string selection, int stage)
                    {
                        for (var j = 0; j < PipelineStageMenu.s_StageData[stage].Choices.Count; ++j)
                            if (PipelineStageMenu.s_StageData[stage].Choices[j].Equals(selection))
                                return j;
                        return 0;
                    }
                });
                pipelineItems.Add(new PipelineStageItem
                {
                    Stage = (CinemachineCore.Stage)i,
                    Dropdown = dropdown,
                    WarningIcon = warningIcon
                });
            }
            ux.TrackAnyUserActivity(() =>
            {
                if (target == null)
                    return; // deleted
                target.InvalidatePipelineCache();
                for (int i = 0; i < pipelineItems.Count; ++i)
                {
                    var item = pipelineItems[i];
                    var c = target.GetCinemachineComponent(item.Stage);
                    int selection = PipelineStageMenu.GetSelectedComponent((int)item.Stage, c);
                    item.Dropdown.value = PipelineStageMenu.s_StageData[(int)item.Stage].Choices[selection];
                    item.WarningIcon.SetVisible(c != null && !c.IsValid);
                }
            });
        }
        /// Draw the Extensions dropdown in the inspector
        public static void AddExtensionsDropdown(this UnityEditor.Editor editor, VisualElement ux)
        {
            var row = new InspectorUtility.LabeledRow(
                "Add Extension", "Extensions are behaviours that inject themselves into "
                + "the Cinemachine pipeline to alter the camera's behaviour.  "
                + "This dropdown will add the selected extension behaviour.");
            var menu = new ContextualMenuManipulator((evt) => 
            {
                for (int i = 0; i < PipelineStageMenu.s_ExtensionTypes.Count; ++i)
                {
                    var type = PipelineStageMenu.s_ExtensionTypes[i];
                    if (type == null)
                        continue;
                    var name = PipelineStageMenu.s_ExtensionNames[i];
                    evt.menu.AppendAction(name, 
                        (action) => 
                        {
                            var target = editor.target as CinemachineVirtualCameraBase;
                            Undo.AddComponent(target.gameObject, type);
                        }, 
                        (status) => 
                        {
                            var target = editor.target as CinemachineVirtualCameraBase;
                            var disable = target == null || target.GetComponent(type) != null;
                            return disable ? DropdownMenuAction.Status.Disabled : DropdownMenuAction.Status.Normal;
                        }
                    );
                }
            });
            var button = row.Contents.AddChild(new Button 
            { 
                text = "(select)", 
                style = 
                { 
                    flexGrow = 1, marginRight = 0, marginLeft = 3, 
                    paddingTop = 0, paddingBottom = 0, paddingLeft = 1,
                    height = InspectorUtility.SingleLineHeight + 2, 
                    unityTextAlign = TextAnchor.MiddleLeft
                }
            });
            menu.activators.Clear();
            menu.activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse });
            button.AddManipulator(menu);
            ux.Add(row);
        }
        
        [InitializeOnLoad]
        static class PipelineStageMenu
        {
            // Pipeline stages
            public struct StageData
            {
                public CinemachineCore.Stage Stage;
                public string Name;
                public List Types;   // first entry is null - this array is synched with PopupOptions
                public List Choices;
            }
            public static StageData[] s_StageData = null;
            
            // Extensions
            public static List s_ExtensionTypes;
            public static List s_ExtensionNames;
            public static int GetSelectedComponent(int stage, CinemachineComponentBase component)
            {
                if (component != null)
                    for (int j = 0; j < s_StageData[stage].Choices.Count; ++j)
                        if (s_StageData[stage].Types[j] == component.GetType())
                            return j;
                return 0;
            }
            // This code dynamically discovers eligible classes and builds the menu
            // data for the various component pipeline stages.
            static PipelineStageMenu()
            {
                s_StageData = new StageData[Enum.GetValues(typeof(CinemachineCore.Stage)).Length];
                for (int i = 0; i < s_StageData.Length; ++i)
                {
                    var stage = (CinemachineCore.Stage)i;
                    s_StageData[i] = new StageData
                    {
                        Stage = stage,
                        Name = stage == CinemachineCore.Stage.Body ? "Position Control" 
                            : stage == CinemachineCore.Stage.Aim ? "Rotation Control"
                            : ObjectNames.NicifyVariableName(stage.ToString()),
                        Types = new List() { null }, // first item is "None"
                        Choices = new List() { "None" }
                    };
                }
                // Get all CinemachineComponentBase
                var allTypes = ReflectionHelpers.GetTypesInAllDependentAssemblies((Type t) => 
                    typeof(CinemachineComponentBase).IsAssignableFrom(t) && !t.IsAbstract 
                    && t.GetCustomAttribute() != null
                    && t.GetCustomAttribute() == null);
                var iter = allTypes.GetEnumerator();
                while (iter.MoveNext())
                {
                    var t = iter.Current;
                    var stage = (int)t.GetCustomAttribute().Stage;
                    s_StageData[stage].Types.Add(t);
                    s_StageData[stage].Choices.Add(InspectorUtility.NicifyClassName(t));
                }
                // Populate the extension list
                s_ExtensionTypes = new List();
                s_ExtensionNames = new List();
                s_ExtensionTypes.Add(null);
                s_ExtensionNames.Add("(select)");
                var allExtensions
                    = ReflectionHelpers.GetTypesInAllDependentAssemblies(
                            (Type t) => typeof(CinemachineExtension).IsAssignableFrom(t) 
                                && !t.IsAbstract && t.GetCustomAttribute() == null);
                var iter2 = allExtensions.GetEnumerator();
                while (iter2.MoveNext())
                {
                    var t = iter2.Current;
                    s_ExtensionTypes.Add(t);
                    s_ExtensionNames.Add(t.Name);
                }
            }
        }
        
        /// Draw the global settings controls in the inspector
        public static void AddGlobalControls(this UnityEditor.Editor editor, VisualElement ux)
        {
            var helpBox = ux.AddChild(new HelpBox("CinemachineCamera settings changes made during Play Mode will be "
                    + "propagated back to the scene when Play Mode is exited.", 
                HelpBoxMessageType.Info));
            helpBox.SetVisible(SaveDuringPlay.Enabled && Application.isPlaying);
            var toggle = ux.AddChild(new Toggle(CinemachineCorePrefs.s_SaveDuringPlayLabel.text) 
            { 
                tooltip = CinemachineCorePrefs.s_SaveDuringPlayLabel.tooltip,
                value = SaveDuringPlay.Enabled
            });
            toggle.AddToClassList(InspectorUtility.kAlignFieldClass);
            toggle.RegisterValueChangedCallback((evt) => 
            {
                SaveDuringPlay.Enabled = evt.newValue;
                helpBox.SetVisible(evt.newValue && Application.isPlaying);
            });
            var choices = new List() { "Disabled", "Passive", "Interactive" };
            int index = CinemachineCorePrefs.ShowInGameGuides.Value 
                ? (CinemachineCorePrefs.DraggableComposerGuides.Value ? 2 : 1) : 0;
            var dropdown = ux.AddChild(new DropdownField("Game View Guides")
            {
                tooltip = CinemachineCorePrefs.s_ShowInGameGuidesLabel.tooltip,
                choices = choices,
                index = index,
                style = { flexGrow = 1 }
            });
            dropdown.AddToClassList(InspectorUtility.kAlignFieldClass);
            dropdown.RegisterValueChangedCallback((evt) => 
            {
                CinemachineCorePrefs.ShowInGameGuides.Value = evt.newValue != choices[0];
                CinemachineCorePrefs.DraggableComposerGuides.Value = evt.newValue == choices[2];
                InspectorUtility.RepaintGameView();
            });
        }
        static List s_componentCache = new ();
        enum SortOrder { None, Camera, Pipeline, Extensions = CinemachineCore.Stage.Finalize + 1, Other };
        /// 
        /// This is only for aesthetics, sort order does not affect camera logic.
        /// Behaviours should be sorted like this:
        /// CinemachineCamera, Body, Aim, Noise, Finalize, Extensions, everything else.
        /// 
        public static void SortComponents(CinemachineVirtualCameraBase target)
        {
            if (target == null || PrefabUtility.IsPartOfNonAssetPrefabInstance(target))
                return; // target was deleted or is part of a prefab instance
            SortOrder lastItem = SortOrder.None;
            bool sortNeeded = false;
            target.gameObject.GetComponents(s_componentCache);
            for (int i = 0; i < s_componentCache.Count && !sortNeeded; ++i)
            {
                var current = GetSortOrderForComponent(s_componentCache[i]);
                if (current < lastItem)
                    sortNeeded = true;
                lastItem = current;
            }
            if (sortNeeded)
            {
                // This is painful, but it won't happen too often
                var pos = 0;
                if (MoveComponentToPosition(pos, SortOrder.Camera, s_componentCache)) ++pos;
                if (MoveComponentToPosition(pos, SortOrder.Pipeline + (int)CinemachineCore.Stage.Body, s_componentCache)) ++pos;
                if (MoveComponentToPosition(pos, SortOrder.Pipeline + (int)CinemachineCore.Stage.Aim, s_componentCache)) ++pos;
                if (MoveComponentToPosition(pos, SortOrder.Pipeline + (int)CinemachineCore.Stage.Noise, s_componentCache)) ++pos;
                MoveComponentToPosition(pos, SortOrder.Pipeline + (int)CinemachineCore.Stage.Finalize, s_componentCache);
                // leave everything else where it is
            }
            SortOrder GetSortOrderForComponent(MonoBehaviour component)
            {
                if (component is CinemachineVirtualCameraBase)
                    return SortOrder.Camera;
                if (component is CinemachineExtension)
                    return SortOrder.Extensions;
                if (component is CinemachineComponentBase)
                    return SortOrder.Pipeline + (int)(component as CinemachineComponentBase).Stage;
                return SortOrder.Other;
            }
        
            // Returns true if item exists.  Will re-sort components if something changed.
            bool MoveComponentToPosition(int pos, SortOrder item, List components)
            {
                for (int i = pos; i < components.Count; ++i)
                {
                    var component = components[i];
                    if (GetSortOrderForComponent(component) == item)
                    {
                        for (int j = i; j > pos; --j)
                            UnityEditorInternal.ComponentUtility.MoveComponentUp(component);
                        if (i > pos)
                            component.gameObject.GetComponents(components);
                        return true;
                    }
                }
                return false;
            }
        }
        /// 
        /// Use this delegate to control the display of warning icons next to the child cameras
        /// 
        public delegate string GetChildWarningMessageDelegate(object childObject);
        /// If camera is a CinemachineCameraManagerBase, draw the Child camera list
        public static void AddChildCameras(
            this UnityEditor.Editor editor, VisualElement ux, 
            GetChildWarningMessageDelegate getChildWarning)
        {
            var vcam = editor.target as CinemachineCameraManagerBase;
            if (vcam == null)
                return;
            var floatFieldWidth = EditorGUIUtility.singleLineHeight * 2.5f;
            var helpBox = ux.AddChild(new HelpBox(
                "Child Cameras cannot be displayed when multiple objects are selected.", 
                HelpBoxMessageType.Info));
            var container = ux.AddChild(new VisualElement());
            
            var header = container.AddChild(new VisualElement { style = { flexDirection = FlexDirection.Row, marginBottom = -2 } });
            header.AddToClassList("unity-collection-view--with-border");
            header.AddChild(new Label("Child Cameras") { style = { marginLeft = 3, flexGrow = 1, flexBasis = 10  }});
            header.AddChild(new Label("Priority") 
                { style = { marginRight = 4, flexGrow = 1, flexBasis = floatFieldWidth, unityTextAlign = TextAnchor.MiddleRight }});
            var list = container.AddChild(new ListView()
            {
                reorderable = false,
                showAddRemoveFooter = true,
                showBorder = true,
                showBoundCollectionSize = false,
                showFoldoutHeader = false,
                style = { borderTopWidth = 0 },
            });
            list.itemsSource = vcam.ChildCameras;
            list.makeItem = () => new VisualElement { style = { flexDirection = FlexDirection.Row }};
            list.bindItem = (row, index) =>
            {
                // Remove children - items seem to get recycled
                for (int i = row.childCount - 1; i >= 0; --i)
                    row.RemoveAt(i);
                var element = list.itemsSource[index] as CinemachineVirtualCameraBase;
                row.AddChild(new ObjectField 
                { 
                    value = element,
                    objectType = typeof(CinemachineVirtualCameraBase),
                    style = { flexBasis = 20, flexGrow = 1 }
                }).SetEnabled(false);
                if (element == null)
                    return;
                var warningIcon = row.AddChild(InspectorUtility.MiniHelpIcon("Item is null"));
                var warningText = getChildWarning == null ? string.Empty : getChildWarning(element);
                warningIcon.tooltip = warningText;
                warningIcon.SetVisible(!string.IsNullOrEmpty(warningText));
                var dragger = row.AddChild(new Label(" "));
                dragger.AddToClassList("unity-base-field__label--with-dragger");
                var so = new SerializedObject(element);
                var prop = so.FindProperty("Priority");
                var enabledProp = prop.FindPropertyRelative("Enabled");
                var priorityProp = prop.FindPropertyRelative("m_Value");
                var priorityField = row.AddChild(new IntegerField
                {
                    value = enabledProp.boolValue ? priorityProp.intValue : 0,
                    style = { flexBasis = floatFieldWidth, flexGrow = 0, marginRight = 4 }
                });
                new FieldMouseDragger(priorityField).SetDragZone(dragger);
                priorityField.RegisterValueChangedCallback((evt) =>
                {
                    if (evt.newValue != 0)
                        enabledProp.boolValue = true;
                    priorityProp.intValue = evt.newValue;
                    so.ApplyModifiedProperties();
                });
                priorityField.TrackPropertyValue(priorityProp, (p) => priorityField.value = p.intValue);
                priorityField.TrackPropertyValue(enabledProp, (p) => priorityField.value = p.boolValue ? priorityProp.intValue : 0);
            };
            list.itemsAdded += (added) =>
            {
                var selected = list.selectedIndex;
                var selectedCam = (selected >= 0 && selected < list.itemsSource.Count) 
                    ? list.itemsSource[selected] as CinemachineVirtualCameraBase : null;
                var name = selectedCam != null ? selectedCam.Name : "Child";
                var iter = added.GetEnumerator();
                while (iter.MoveNext())
                    CinemachineMenu.CreatePassiveCmCamera(name, vcam.gameObject);
                Selection.activeObject = vcam;
                vcam.InvalidateCameraCache();
            };
            list.itemsRemoved += (removed) =>
            {
                var iter = removed.GetEnumerator();
                while (iter.MoveNext())
                {
                    var child = list.itemsSource[iter.Current] as CinemachineVirtualCameraBase;
                    if (child != null)
                        Undo.DestroyObjectImmediate(child.gameObject);
                }
                vcam.InvalidateCameraCache();
            };
            container.TrackAnyUserActivity(() =>
            {
                if (editor == null || editor.target == null)
                    return; // object deleted
                var isMultiSelect = editor.targets.Length > 1;
                helpBox.SetVisible(isMultiSelect);
                container.SetVisible(!isMultiSelect);
                // Update child list
                if (!isMultiSelect)
                {
                    list.itemsSource = vcam.ChildCameras;
                    list.Rebuild();
                }
            });
        }
    }
}