#if UNITY_EDITOR
using System;
using UnityEditor;
using UnityEditor.IMGUI.Controls;

////TODO: sync expanded state of SerializedProperties to expanded state of tree (will help preserving expansion in inspector)

////REVIEW: would be great to align all "[device]" parts of binding strings neatly in a column

namespace UnityEngine.InputSystem.Editor
{
    internal abstract class ActionTreeItemBase : TreeViewItem
    {
        public SerializedProperty property { get; }
        public virtual string expectedControlLayout => string.Empty;
        public virtual bool canRename => true;
        public virtual bool serializedDataIncludesChildren => false;
        public abstract GUIStyle colorTagStyle { get; }
        public string name { get; }
        public Guid guid { get; }

        // Action data is generally stored in arrays. Action maps are stored in m_ActionMaps arrays in assets,
        // actions are stored in m_Actions arrays on maps and bindings are stored in m_Bindings arrays on maps.
        public SerializedProperty arrayProperty => property.GetArrayPropertyFromElement();

        // Dynamically look up the array index instead of just taking it from `property`.
        // This makes sure whatever insertions or deletions we perform on the serialized data,
        // we get the right array index from an item.
        public int arrayIndex => InputActionSerializationHelpers.GetIndex(arrayProperty, guid);

        protected ActionTreeItemBase(SerializedProperty property)
        {
            this.property = property;

            // Look up name.
            var nameProperty = property.FindPropertyRelative("m_Name");
            Debug.Assert(nameProperty != null, $"Cannot find m_Name property on {property.propertyPath}");
            name = nameProperty.stringValue;

            // Look up ID.
            var idProperty = property.FindPropertyRelative("m_Id");
            Debug.Assert(idProperty != null, $"Cannot find m_Id property on {property.propertyPath}");
            var idPropertyString = idProperty.stringValue;
            if (string.IsNullOrEmpty(idPropertyString))
            {
                // This is somewhat questionable but we can't operate if we don't have IDs on the data used in the tree.
                // Rather than requiring users of the tree to set this up consistently, we assign IDs
                // on the fly, if necessary.
                guid = Guid.NewGuid();
                idPropertyString = guid.ToString();
                idProperty.stringValue = idPropertyString;
                idProperty.serializedObject.ApplyModifiedPropertiesWithoutUndo();
            }
            else
            {
                guid = new Guid(idPropertyString);
            }

            // All our elements (maps, actions, bindings) carry unique IDs. We use their hash
            // codes as item IDs in the tree. This should result in stable item IDs that keep
            // identifying the right item across all reloads and tree mutations.
            id = guid.GetHashCode();
        }

        public virtual void Rename(string newName)
        {
            Debug.Assert(!canRename, "Item is marked as allowing renames yet does not implement Rename()");
        }

        /// <summary>
        /// Delete serialized data for the tree item and its children.
        /// </summary>
        public abstract void DeleteData();

        public abstract bool AcceptsDrop(ActionTreeItemBase item);

        /// <summary>
        /// Get information about where to drop an item of the given type and (optionally) the given index.
        /// </summary>
        public abstract bool GetDropLocation(Type itemType, int? childIndex, ref SerializedProperty array, ref int arrayIndex);

        protected static class Styles
        {
            private static GUIStyle StyleWithBackground(string fileName)
            {
                return new GUIStyle("Label").WithNormalBackground(AssetDatabase.LoadAssetAtPath<Texture2D>($"{InputActionTreeView.SharedResourcesPath}{fileName}.png"));
            }

            public static readonly GUIStyle yellowRect = StyleWithBackground("yellow");
            public static readonly GUIStyle greenRect = StyleWithBackground("green");
            public static readonly GUIStyle blueRect = StyleWithBackground("blue");
            public static readonly GUIStyle pinkRect = StyleWithBackground("pink");
        }
    }

    /// <summary>
    /// Tree view item for an action map.
    /// </summary>
    /// <seealso cref="InputActionMap"/>
    internal class ActionMapTreeItem : ActionTreeItemBase
    {
        public ActionMapTreeItem(SerializedProperty actionMapProperty)
            : base(actionMapProperty)
        {
        }

        public override GUIStyle colorTagStyle => Styles.yellowRect;
        public SerializedProperty bindingsArrayProperty => property.FindPropertyRelative("m_Bindings");
        public SerializedProperty actionsArrayProperty => property.FindPropertyRelative("m_Actions");
        public override bool serializedDataIncludesChildren => true;

        public override void Rename(string newName)
        {
            InputActionSerializationHelpers.RenameActionMap(property, newName);
        }

        public override void DeleteData()
        {
            var assetObject = property.serializedObject;
            if (!(assetObject.targetObject is InputActionAsset))
                throw new InvalidOperationException(
                    $"Action map must be part of InputActionAsset but is in {assetObject.targetObject} instead");

            InputActionSerializationHelpers.DeleteActionMap(assetObject, guid);
        }

        public override bool AcceptsDrop(ActionTreeItemBase item)
        {
            return item is ActionTreeItem;
        }

        public override bool GetDropLocation(Type itemType, int? childIndex, ref SerializedProperty array, ref int arrayIndex)
        {
            // Drop actions into action array.
            if (itemType == typeof(ActionTreeItem))
            {
                array = actionsArrayProperty;
                arrayIndex = childIndex ?? -1;
                return true;
            }

            // For action maps in assets, drop other action maps next to them.
            if (itemType == typeof(ActionMapTreeItem) && property.serializedObject.targetObject is InputActionAsset)
            {
                array = property.GetArrayPropertyFromElement();
                arrayIndex = this.arrayIndex + 1;
                return true;
            }

            ////REVIEW: would be nice to be able to replace the entire contents of a map in the inspector by dropping in another map

            return false;
        }

        public static ActionMapTreeItem AddTo(TreeViewItem parent, SerializedProperty actionMapProperty)
        {
            var item = new ActionMapTreeItem(actionMapProperty);

            item.depth = parent.depth + 1;
            item.displayName = item.name;
            parent.AddChild(item);

            return item;
        }

        public void AddActionsTo(TreeViewItem parent)
        {
            AddActionsTo(parent, addBindings: false);
        }

        public void AddActionsAndBindingsTo(TreeViewItem parent)
        {
            AddActionsTo(parent, addBindings: true);
        }

        private void AddActionsTo(TreeViewItem parent, bool addBindings)
        {
            var actionsArrayProperty = this.actionsArrayProperty;
            Debug.Assert(actionsArrayProperty != null, $"Cannot find m_Actions in {property}");

            for (var i = 0; i < actionsArrayProperty.arraySize; i++)
            {
                var actionProperty = actionsArrayProperty.GetArrayElementAtIndex(i);
                var actionItem = ActionTreeItem.AddTo(parent, property, actionProperty);

                if (addBindings)
                    actionItem.AddBindingsTo(actionItem);
            }
        }

        public static void AddActionMapsFromAssetTo(TreeViewItem parent, SerializedObject assetObject)
        {
            var actionMapsArrayProperty = assetObject.FindProperty("m_ActionMaps");
            Debug.Assert(actionMapsArrayProperty != null, $"Cannot find m_ActionMaps in {assetObject}");
            Debug.Assert(actionMapsArrayProperty.isArray, $"m_ActionMaps in {assetObject} is not an array");

            var mapCount = actionMapsArrayProperty.arraySize;
            for (var i = 0; i < mapCount; ++i)
            {
                var mapProperty = actionMapsArrayProperty.GetArrayElementAtIndex(i);
                AddTo(parent, mapProperty);
            }
        }
    }

    /// <summary>
    /// Tree view item for an action.
    /// </summary>
    /// <see cref="InputAction"/>
    internal class ActionTreeItem : ActionTreeItemBase
    {
        public ActionTreeItem(SerializedProperty actionMapProperty, SerializedProperty actionProperty)
            : base(actionProperty)
        {
            this.actionMapProperty = actionMapProperty;
        }

        public SerializedProperty actionMapProperty { get; }
        public override GUIStyle colorTagStyle => Styles.greenRect;
        public bool isSingletonAction => actionMapProperty == null;

        public override string expectedControlLayout =>
            property.FindPropertyRelative("m_ExpectedControlType").stringValue;

        public SerializedProperty bindingsArrayProperty => isSingletonAction
        ? property.FindPropertyRelative("m_SingletonActionBindings")
            : actionMapProperty.FindPropertyRelative("m_Bindings");

        // If we're a singleton action (no associated action map property), we include all our bindings in the
        // serialized data.
        public override bool serializedDataIncludesChildren => actionMapProperty == null;

        public override void Rename(string newName)
        {
            InputActionSerializationHelpers.RenameAction(property, actionMapProperty, newName);
        }

        public override void DeleteData()
        {
            InputActionSerializationHelpers.DeleteActionAndBindings(actionMapProperty, guid);
        }

        public override bool AcceptsDrop(ActionTreeItemBase item)
        {
            return item is BindingTreeItem && !(item is PartOfCompositeBindingTreeItem);
        }

        public override bool GetDropLocation(Type itemType, int? childIndex, ref SerializedProperty array, ref int arrayIndex)
        {
            // Drop bindings into binding array.
            if (typeof(BindingTreeItem).IsAssignableFrom(itemType))
            {
                array = bindingsArrayProperty;

                // Indexing by tree items is relative to each action but indexing in
                // binding array is global for all actions in a map. Adjust index accordingly.
                // NOTE: Bindings for any one action need not be stored contiguously in the binding array
                //       so we can't just add something to the index of the first binding to the action.
                arrayIndex =
                    InputActionSerializationHelpers.ConvertBindingIndexOnActionToBindingIndexInArray(
                        array, name, childIndex ?? -1);

                return true;
            }

            // Drop other actions next to us.
            if (itemType == typeof(ActionTreeItem))
            {
                array = arrayProperty;
                arrayIndex = this.arrayIndex + 1;
                return true;
            }

            return false;
        }

        public static ActionTreeItem AddTo(TreeViewItem parent, SerializedProperty actionMapProperty, SerializedProperty actionProperty)
        {
            var item = new ActionTreeItem(actionMapProperty, actionProperty);

            item.depth = parent.depth + 1;
            item.displayName = item.name;
            parent.AddChild(item);

            return item;
        }

        /// <summary>
        /// Add items for the bindings of just this action to the given parent tree item.
        /// </summary>
        public void AddBindingsTo(TreeViewItem parent)
        {
            var isSingleton = actionMapProperty == null;
            var bindingsArrayProperty = isSingleton
                ? property.FindPropertyRelative("m_SingletonActionBindings")
                : actionMapProperty.FindPropertyRelative("m_Bindings");

            var bindingsCountInMap = bindingsArrayProperty.arraySize;
            var currentComposite = (CompositeBindingTreeItem)null;
            for (var i = 0; i < bindingsCountInMap; ++i)
            {
                var bindingProperty = bindingsArrayProperty.GetArrayElementAtIndex(i);

                // Skip if binding is not for action.
                var actionProperty = bindingProperty.FindPropertyRelative("m_Action");
                Debug.Assert(actionProperty != null, $"Could not find m_Action in {bindingProperty}");
                if (!actionProperty.stringValue.Equals(name, StringComparison.InvariantCultureIgnoreCase))
                    continue;

                // See what kind of binding we have.
                var flagsProperty = bindingProperty.FindPropertyRelative("m_Flags");
                Debug.Assert(actionProperty != null, $"Could not find m_Flags in {bindingProperty}");
                var flags = (InputBinding.Flags)flagsProperty.intValue;
                if ((flags & InputBinding.Flags.PartOfComposite) != 0 && currentComposite != null)
                {
                    // Composite part binding.
                    PartOfCompositeBindingTreeItem.AddTo(currentComposite, bindingProperty);
                }
                else if ((flags & InputBinding.Flags.Composite) != 0)
                {
                    // Composite binding.
                    currentComposite = CompositeBindingTreeItem.AddTo(parent, bindingProperty);
                }
                else
                {
                    // "Normal" binding.
                    BindingTreeItem.AddTo(parent, bindingProperty);
                    currentComposite = null;
                }
            }
        }
    }

    /// <summary>
    /// Tree view item for a binding.
    /// </summary>
    /// <seealso cref="InputBinding"/>
    internal class BindingTreeItem : ActionTreeItemBase
    {
        public BindingTreeItem(SerializedProperty bindingProperty)
            : base(bindingProperty)
        {
            path = property.FindPropertyRelative("m_Path").stringValue;
            groups = property.FindPropertyRelative("m_Groups").stringValue;
            action = property.FindPropertyRelative("m_Action").stringValue;
        }

        public string path { get; }
        public string groups { get; }
        public string action { get; }

        public override bool canRename => false;
        public override GUIStyle colorTagStyle => Styles.blueRect;

        public string displayPath =>
            !string.IsNullOrEmpty(path) ? InputControlPath.ToHumanReadableString(path) : "<No Binding>";

        public override string expectedControlLayout
        {
            get
            {
                // Find the action we're under and return its expected control layout.
                for (var item = parent; item != null; item = item.parent)
                {
                    if (item is ActionTreeItem actionItem)
                        return actionItem.expectedControlLayout;
                }
                return string.Empty;
            }
        }

        public override void DeleteData()
        {
            var bindingsArrayProperty = property.GetParentProperty();
            InputActionSerializationHelpers.DeleteBinding(bindingsArrayProperty, guid);
        }

        public override bool AcceptsDrop(ActionTreeItemBase item)
        {
            return false;
        }

        public override bool GetDropLocation(Type itemType, int? childIndex, ref SerializedProperty array, ref int arrayIndex)
        {
            // Drop bindings next to us.
            if (typeof(BindingTreeItem).IsAssignableFrom(itemType))
            {
                array = arrayProperty;
                arrayIndex = this.arrayIndex + 1;
                return true;
            }

            return false;
        }

        public static BindingTreeItem AddTo(TreeViewItem parent, SerializedProperty bindingProperty)
        {
            var item = new BindingTreeItem(bindingProperty);

            item.depth = parent.depth + 1;
            item.displayName = item.displayPath;
            parent.AddChild(item);

            return item;
        }
    }

    /// <summary>
    /// Tree view item for a composite binding.
    /// </summary>
    /// <seealso cref="InputBinding.isComposite"/>
    internal class CompositeBindingTreeItem : BindingTreeItem
    {
        public CompositeBindingTreeItem(SerializedProperty bindingProperty)
            : base(bindingProperty)
        {
        }

        public override GUIStyle colorTagStyle => Styles.blueRect;
        public override bool canRename => true;

        public override void Rename(string newName)
        {
            InputActionSerializationHelpers.RenameComposite(property, newName);
        }

        public override bool AcceptsDrop(ActionTreeItemBase item)
        {
            return item is PartOfCompositeBindingTreeItem;
        }

        public override bool GetDropLocation(Type itemType, int? childIndex, ref SerializedProperty array, ref int arrayIndex)
        {
            // Drop part binding into composite.
            if (itemType == typeof(PartOfCompositeBindingTreeItem))
            {
                array = arrayProperty;

                // Adjust child index by index of composite item itself.
                arrayIndex = childIndex != null
                    ? this.arrayIndex + 1 + childIndex.Value // Dropping at #0 should put as our index plus one.
                    : this.arrayIndex + 1 + InputActionSerializationHelpers.GetCompositePartCount(array, this.arrayIndex);

                return true;
            }

            // Drop other bindings next to us.
            if (typeof(BindingTreeItem).IsAssignableFrom(itemType))
            {
                array = arrayProperty;
                arrayIndex = this.arrayIndex + 1 +
                    InputActionSerializationHelpers.GetCompositePartCount(array, this.arrayIndex);
                return true;
            }

            return false;
        }

        public new static CompositeBindingTreeItem AddTo(TreeViewItem parent, SerializedProperty bindingProperty)
        {
            var item = new CompositeBindingTreeItem(bindingProperty);

            item.depth = parent.depth + 1;
            item.displayName = !string.IsNullOrEmpty(item.name) ? item.name : ObjectNames.NicifyVariableName(item.path);
            parent.AddChild(item);

            return item;
        }
    }

    /// <summary>
    /// Tree view item for bindings that are parts of composites.
    /// </summary>
    /// <see cref="InputBinding.isPartOfComposite"/>
    internal class PartOfCompositeBindingTreeItem : BindingTreeItem
    {
        public PartOfCompositeBindingTreeItem(SerializedProperty bindingProperty)
            : base(bindingProperty)
        {
        }

        public override GUIStyle colorTagStyle => Styles.pinkRect;
        public override bool canRename => false;

        public override string expectedControlLayout
        {
            get
            {
                if (m_ExpectedControlLayout == null)
                {
                    var partName = name;
                    var compositeName = ((CompositeBindingTreeItem)parent).name;
                    var layoutName = InputBindingComposite.GetExpectedControlLayoutName(compositeName, partName);
                    m_ExpectedControlLayout = layoutName ?? "";
                }

                return m_ExpectedControlLayout;
            }
        }

        private string m_ExpectedControlLayout;

        public new static PartOfCompositeBindingTreeItem AddTo(TreeViewItem parent, SerializedProperty bindingProperty)
        {
            var item = new PartOfCompositeBindingTreeItem(bindingProperty);

            item.depth = parent.depth + 1;
            item.displayName = $"{ObjectNames.NicifyVariableName(item.name)}: {item.displayPath}";
            parent.AddChild(item);

            return item;
        }
    }
}
#endif // UNITY_EDITOR
