// Unity C# reference source
// Copyright (c) Unity Technologies. For terms of use, see
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor.IMGUI.Controls;
using UnityEditor.Utils;
using UnityEngine;
using UnityEditorInternal;
using UnityEditor.Experimental;

namespace UnityEditor
{
    internal class PackageImportTreeView
    {
        TreeViewController m_TreeView;
        List<PackageImportTreeViewItem> m_Selection = new List<PackageImportTreeViewItem>();
        static readonly bool s_UseFoldouts = true;
        public enum EnabledState
        {
            NotSet = -1,
            None = 0,
            All = 1,
            Mixed = 2
        }

        private PackageImport m_PackageImport;

        public ImportPackageItem[] packageItems { get { return m_PackageImport.packageItems; } }


        public PackageImportTreeView(PackageImport packageImport, TreeViewState treeViewState, Rect startRect)
        {
            m_PackageImport = packageImport;

            m_TreeView = new TreeViewController(m_PackageImport, treeViewState);
            var dataSource = new PackageImportTreeViewDataSource(m_TreeView, this);
            var gui        = new PackageImportTreeViewGUI(m_TreeView, this);

            m_TreeView.Init(startRect, dataSource, gui, null);
            m_TreeView.ReloadData();
            m_TreeView.selectionChangedCallback += SelectionChanged;
            gui.itemWasToggled += ItemWasToggled;

            ComputeEnabledStateForFolders();
        }

        void ComputeEnabledStateForFolders()
        {
            var root = m_TreeView.data.root as PackageImportTreeViewItem;
            var done = new HashSet<PackageImportTreeViewItem>();
            done.Add(root); // Dont compute for root: mark it as done
            RecursiveComputeEnabledStateForFolders(root, done);
        }

        void RecursiveComputeEnabledStateForFolders(PackageImportTreeViewItem pitem, HashSet<PackageImportTreeViewItem> done)
        {
            if (pitem.item != null && !pitem.item.isFolder)
                return;

            // Depth first recursion to allow parent folders be dependant on child folders

            // Recurse
            if (pitem.hasChildren)
            {
                foreach (var child in pitem.children)
                {
                    RecursiveComputeEnabledStateForFolders(child as PackageImportTreeViewItem, done);
                }
            }

            // Now do logic
            if (!done.Contains(pitem))
            {
                EnabledState amount = GetFolderChildrenEnabledState(pitem);
                pitem.enableState = amount;

                // If 'item' is mixed then all of its parents will also be mixed
                if (amount == EnabledState.Mixed)
                {
                    done.Add(pitem);
                    var current = pitem.parent as PackageImportTreeViewItem;
                    while (current != null)
                    {
                        if (!done.Contains(current))
                        {
                            current.enableState = EnabledState.Mixed;
                            done.Add(current);
                        }
                        current = current.parent as PackageImportTreeViewItem;
                    }
                }
            }
        }

        bool ItemShouldBeConsideredForEnabledCheck(PackageImportTreeViewItem pitem)
        {
            // Not even an item
            if (pitem == null)
                return false;

            // item was a folder that had to be created
            // in this treeview.
            if (pitem.item == null)
                return true;

            var item = pitem.item;
            // Its a package asset or its changed
            if (item.projectAsset || !(item.isFolder || item.assetChanged))
                return false;

            return true;
        }

        EnabledState GetFolderChildrenEnabledState(PackageImportTreeViewItem folder)
        {
            if (folder.item != null && !folder.item.isFolder)
                Debug.LogError("Should be a folder item!");

            if (!folder.hasChildren)
                return EnabledState.None;

            EnabledState amount = EnabledState.NotSet;

            int i = 0;
            for (; i < folder.children.Count; ++i)
            {
                // We dont want to consider project assets in this calculation as they are
                // ignored
                var firstValidChild = folder.children[i] as PackageImportTreeViewItem;
                if (ItemShouldBeConsideredForEnabledCheck(firstValidChild))
                {
                    amount = firstValidChild.enableState;
                    break;
                }
            }

            ++i;
            for (; i < folder.children.Count; ++i)
            {
                // We dont want to consider project assets in this calculation as they are
                // ignored
                var childItem = folder.children[i] as PackageImportTreeViewItem;
                if (ItemShouldBeConsideredForEnabledCheck(childItem))
                {
                    if (amount != childItem.enableState)
                    {
                        amount = EnabledState.Mixed;
                        break;
                    }
                }
            }

            if (amount == EnabledState.NotSet)
                return EnabledState.None;

            return amount;
        }

        void SelectionChanged(int[] selectedIDs)
        {
            // Cache selected tree view items (from ids)
            m_Selection = new List<PackageImportTreeViewItem>();
            var visibleItems = m_TreeView.data.GetRows();
            foreach (var visibleItem in visibleItems)
            {
                if (selectedIDs.Contains(visibleItem.id))
                {
                    var pitem = visibleItem as PackageImportTreeViewItem;
                    if (pitem != null)
                        m_Selection.Add(pitem);
                }
            }

            // Show preview on selection
            var selectedItem = m_Selection[0].item;
            if (m_Selection.Count == 1 && selectedItem != null && !string.IsNullOrEmpty(selectedItem.previewPath))
            {
                var gui = m_TreeView.gui as PackageImportTreeViewGUI;
                gui.showPreviewForID = m_Selection[0].id;
            }
            else
            {
                PopupWindowWithoutFocus.Hide();
            }
        }

        public void OnGUI(Rect rect)
        {
            // Remove preview popup on mouse scroll wheel events
            if (Event.current.type == EventType.ScrollWheel)
                PopupWindowWithoutFocus.Hide();

            int keyboardControlID = GUIUtility.GetControlID(FocusType.Keyboard);
            m_TreeView.OnGUI(rect, keyboardControlID);

            // Keyboard space toggles selection enabledness
            if (Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Space &&
                m_Selection != null && m_Selection.Count > 0 && GUIUtility.keyboardControl == keyboardControlID)
            {
                var pitem = m_Selection[0];
                if (pitem != null)
                {
                    EnabledState newEnabled = (pitem.enableState == EnabledState.None) ? EnabledState.All : EnabledState.None;
                    pitem.enableState = newEnabled;
                    ItemWasToggled(m_Selection[0]);
                }

                Event.current.Use();
            }
        }

        public void SetAllEnabled(EnabledState state)
        {
            EnableChildrenRecursive(m_TreeView.data.root, state);
            ComputeEnabledStateForFolders();
        }

        void ItemWasToggled(PackageImportTreeViewItem pitem)
        {
            if (m_Selection.Count <= 1)
            {
                EnableChildrenRecursive(pitem, pitem.enableState);
            }
            else
            {
                foreach (var childPItem in m_Selection)
                {
                    childPItem.enableState = pitem.enableState;
                }
            }

            ComputeEnabledStateForFolders();
        }

        void EnableChildrenRecursive(TreeViewItem parentItem, EnabledState state)
        {
            if (!parentItem.hasChildren)
                return;

            foreach (TreeViewItem tvitem in parentItem.children)
            {
                var pitem = tvitem as PackageImportTreeViewItem;
                pitem.enableState = state;

                EnableChildrenRecursive(pitem, state);
            }
        }

        // Item

        private class PackageImportTreeViewItem : TreeViewItem
        {
            public ImportPackageItem item { get; set; }

            public EnabledState enableState
            {
                get { return m_EnableState; }
                set
                {
                    // We only want to set the enabled state if the item
                    // is not a project asset.
                    if (item == null || !item.projectAsset)
                    {
                        m_EnableState = value;
                        if (item != null)
                            item.enabledStatus = (int)value;
                    }
                }
            }

            public PackageImportTreeViewItem(ImportPackageItem itemIn, int id, int depth, TreeViewItem parent, string displayName)
                : base(id, depth, parent, displayName)
            {
                item = itemIn;

                if (item == null)
                    m_EnableState = EnabledState.All;
                else
                    m_EnableState = (EnabledState)item.enabledStatus;
            }

            private EnabledState m_EnableState;
        }

        // Gui

        private class PackageImportTreeViewGUI : TreeViewGUI
        {
            internal static class Constants
            {
                public static Texture2D folderIcon = EditorGUIUtility.FindTexture(EditorResources.folderIconName);
                public static GUIContent badgeNew    = EditorGUIUtility.TrIconContent("PackageBadgeNew", "This is a new Asset");
                public static GUIContent badgeDelete = EditorGUIUtility.TrIconContent("PackageBadgeDelete", "These files will be deleted!");
                public static GUIContent badgeWarn   = EditorGUIUtility.TrIconContent("console.warnicon", "Warning: File exists in project, but with different GUID. Will override existing asset which may be undesired.");
                public static GUIContent badgeChange = EditorGUIUtility.TrIconContent("playLoopOff", "This file is new or has changed.");

                public static GUIStyle   paddinglessStyle;

                static Constants()
                {
                    paddinglessStyle = new GUIStyle();
                    paddinglessStyle.padding = new RectOffset(0, 0, 0, 0);
                }
            }

            public Action<PackageImportTreeViewItem> itemWasToggled;
            public int showPreviewForID { get; set; }

            private PackageImportTreeView m_PackageImportView;
            protected float k_FoldoutWidth = 12f;

            public PackageImportTreeViewGUI(TreeViewController treeView, PackageImportTreeView view)
                : base(treeView)
            {
                m_PackageImportView = view;

                k_BaseIndent = 4f;
                if (!s_UseFoldouts)
                    k_FoldoutWidth = 0f;
            }

            override public void OnRowGUI(Rect rowRect, TreeViewItem tvItem, int row, bool selected, bool focused)
            {
                k_IndentWidth = 18;
                k_FoldoutWidth = 18;
                const float k_ToggleWidth = 18f;

                var pitem = tvItem as PackageImportTreeViewItem;
                var item = pitem.item;

                bool repainting = Event.current.type == EventType.Repaint;

                // 0. Selection row rect
                if (selected && repainting)
                    selectionStyle.Draw(rowRect, false, false, true, focused);

                bool validItem    = (item != null);
                bool isFolder     = (item != null) ? item.isFolder     : true;
                bool assetChanged = (item != null) ? item.assetChanged : false;
                bool pathConflict = (item != null) ? item.pathConflict : false;
                bool exists       = (item != null) ? item.exists       : true;
                bool projectAsset = (item != null) ? item.projectAsset : false;

                // 1. Foldout
                if (m_TreeView.data.IsExpandable(tvItem))
                    DoFoldout(rowRect, tvItem, row);

                // 2. Toggle only for items that are actually in the package.
                Rect toggleRect = new Rect(k_BaseIndent + tvItem.depth * indentWidth + k_FoldoutWidth, rowRect.y, k_ToggleWidth, rowRect.height);

                if ((isFolder && !projectAsset) || (validItem && !projectAsset && assetChanged))
                    DoToggle(pitem, toggleRect);

                using (new EditorGUI.DisabledScope(!validItem || projectAsset))
                {
                    // 3. Icon & Text
                    Rect contentRect = new Rect(toggleRect.xMax, rowRect.y, rowRect.width, rowRect.height);
                    DoIconAndText(tvItem, contentRect, selected, focused);

                    // 4. Preview popup
                    DoPreviewPopup(pitem, rowRect);

                    // 4.5 Warning about file clashing.
                    if (repainting && validItem && pathConflict)
                    {
                        Rect labelRect = new Rect(rowRect.xMax - 58, rowRect.y, rowRect.height, rowRect.height);
                        EditorGUIUtility.SetIconSize(new Vector2(rowRect.height, rowRect.height));
                        GUI.Label(labelRect, Constants.badgeWarn, Constants.paddinglessStyle);
                        EditorGUIUtility.SetIconSize(Vector2.zero);
                    }


                    // 5. Optional badge ("New")
                    if (repainting && validItem && !(exists || pathConflict))
                    {
                        // FIXME: Need to enable tooltips here.
                        Texture badge = Constants.badgeNew.image;
                        Rect labelRect = new Rect(rowRect.xMax - badge.width - 6, rowRect.y + (rowRect.height - badge.height) / 2, badge.width, badge.height);
                        GUI.Label(labelRect, Constants.badgeNew, Constants.paddinglessStyle);
                    }

                    // 7. Show what stuff has changed
                    if (repainting && validItem && (exists || pathConflict) && assetChanged)
                    {
                        Texture badge = Constants.badgeChange.image;
                        Rect labelRect = new Rect(rowRect.xMax - badge.width - 6, rowRect.y, rowRect.height, rowRect.height);
                        GUI.Label(labelRect, Constants.badgeChange, Constants.paddinglessStyle);
                    }
                }
            }

            static void Toggle(ImportPackageItem[] items, PackageImportTreeViewItem pitem, Rect toggleRect)
            {
                bool enabled = (int)pitem.enableState > 0;
                bool isFolder = (pitem.item == null) || pitem.item.isFolder;

                GUIStyle style = EditorStyles.toggle;
                bool setMixed = isFolder && (pitem.enableState == EnabledState.Mixed);
                if (setMixed)
                    style = EditorStyles.toggleMixed;

                bool newEnabled =  GUI.Toggle(toggleRect, enabled, GUIContent.none, style);
                if (newEnabled != enabled)
                    pitem.enableState = newEnabled ? EnabledState.All : EnabledState.None;
            }

            void DoToggle(PackageImportTreeViewItem pitem, Rect toggleRect)
            {
                // Toggle on/off
                EditorGUI.BeginChangeCheck();
                Toggle(m_PackageImportView.packageItems, pitem, toggleRect);
                if (EditorGUI.EndChangeCheck())
                {
                    // Only change selection if we already have single selection (Keep multi-selection when toggling)
                    if (m_TreeView.GetSelection().Length <= 1 || !m_TreeView.GetSelection().Contains(pitem.id))
                    {
                        m_TreeView.SetSelection(new int[] { pitem.id }, false);
                        m_TreeView.NotifyListenersThatSelectionChanged();
                    }
                    if (itemWasToggled != null)
                        itemWasToggled(pitem);
                    Event.current.Use();
                }
            }

            void DoPreviewPopup(PackageImportTreeViewItem pitem, Rect rowRect)
            {
                var item = pitem.item;

                if (item != null)
                {
                    // Ensure preview is shown when clicking on an already selected item (the preview might have been closed)
                    if (Event.current.type == EventType.MouseDown && rowRect.Contains(Event.current.mousePosition) && !PopupWindowWithoutFocus.IsVisible())
                        showPreviewForID = pitem.id;

                    // Show preview
                    if (pitem.id == showPreviewForID && Event.current.type != EventType.Layout)
                    {
                        showPreviewForID = 0;
                        if (!string.IsNullOrEmpty(item.previewPath))
                        {
                            Texture2D preview = PackageImport.GetPreview(item.previewPath);
                            Rect buttonRect = rowRect;
                            buttonRect.width = EditorGUIUtility.currentViewWidth;
                            PopupWindowWithoutFocus.Show(buttonRect, new PreviewPopup(preview), new[] { PopupLocation.Right, PopupLocation.Left, PopupLocation.Below });
                        }
                    }
                }
            }

            void DoIconAndText(TreeViewItem item, Rect contentRect, bool selected, bool focused)
            {
                EditorGUIUtility.SetIconSize(new Vector2(k_IconWidth, k_IconWidth)); // If not set we see icons scaling down if text is being cropped
                lineStyle = Styles.lineStyle;
                lineStyle.padding.left = 0; // padding could have been set by other tree views
                contentRect.height += 5; // with the default row height, underscore and lower parts of characters like g, p, etc. were not visible
                if (Event.current.type == EventType.Repaint)
                    lineStyle.Draw(contentRect, GUIContent.Temp(item.displayName, GetIconForItem(item)), false, false, selected, focused);
                EditorGUIUtility.SetIconSize(Vector2.zero);
            }

            protected override Texture GetIconForItem(TreeViewItem tvItem)
            {
                var ourItem         = tvItem as PackageImportTreeViewItem;
                var item            = ourItem.item;

                // Indefined items are always folders.
                if (item == null || item.isFolder)
                {
                    return Constants.folderIcon;
                }

                // We are using this TreeViewGUI when importing and exporting a package, so handle both situations:

                // Exporting a package can use cached icons (icons we generate on import)
                Texture cachedIcon = AssetDatabase.GetCachedIcon(item.destinationAssetPath);
                if (cachedIcon != null)
                    return cachedIcon;

                // Importing a package have to use icons based on file extension
                return InternalEditorUtility.GetIconForFile(item.destinationAssetPath);
            }

            protected override void RenameEnded()
            {
            }
        }

        // Datasource

        private class PackageImportTreeViewDataSource : TreeViewDataSource
        {
            private PackageImportTreeView m_PackageImportView;

            public PackageImportTreeViewDataSource(TreeViewController treeView, PackageImportTreeView view)
                : base(treeView)
            {
                m_PackageImportView = view;
                rootIsCollapsable = false;
                showRootItem = false;
            }

            public override bool IsRenamingItemAllowed(TreeViewItem item)
            {
                return false;
            }

            public override bool IsExpandable(TreeViewItem item)
            {
                if (!s_UseFoldouts)
                    return false;

                return base.IsExpandable(item);
            }

            public override void FetchData()
            {
                int rootDepth = -1; // -1 so its children will have 0 depth
                m_RootItem = new PackageImportTreeViewItem(null, "Assets".GetHashCode(), rootDepth, null, "InvisibleAssetsFolder");

                bool initExpandedState = true;
                if (initExpandedState)
                    m_TreeView.state.expandedIDs.Add(m_RootItem.id);

                ImportPackageItem[] items = m_PackageImportView.packageItems;

                Dictionary<string, TreeViewItem> treeViewFolders = new Dictionary<string, TreeViewItem>();
                for (int i = 0; i < items.Length; i++)
                {
                    var item = items[i];

                    if (PackageImport.HasInvalidCharInFilePath(item.destinationAssetPath))
                        continue; // Do not add invalid paths (we already warn the user with a dialog in PackageImport.cs)

                    string filename   = Path.GetFileName(item.destinationAssetPath).ConvertSeparatorsToUnity();
                    string folderPath = Path.GetDirectoryName(item.destinationAssetPath).ConvertSeparatorsToUnity();

                    // Ensure folders. This is for when installed packages have been moved to other folders.

                    TreeViewItem targetFolder;
                    treeViewFolders.TryGetValue(folderPath, out targetFolder);

                    if (targetFolder == null)
                    {
                        targetFolder = EnsureFolderPath(folderPath, treeViewFolders, initExpandedState);
                    }

                    // Add file to folder
                    if (targetFolder != null)
                    {
                        int id = item.destinationAssetPath.GetHashCode();
                        var newItem = new PackageImportTreeViewItem(item, id, targetFolder.depth + 1, targetFolder, filename);
                        targetFolder.AddChild(newItem);

                        if (initExpandedState)
                            m_TreeView.state.expandedIDs.Add(id);

                        // We need to ensure that the folder is available for
                        // EnsureFolderPath on subsequent iterations.
                        if (item.isFolder)
                            treeViewFolders[item.destinationAssetPath] = newItem;
                    }
                }

                if (initExpandedState)
                    m_TreeView.state.expandedIDs.Sort();
            }

            TreeViewItem EnsureFolderPath(string folderPath, Dictionary<string, TreeViewItem> treeViewFolders, bool initExpandedState)
            {
                //We're in the root folder, so just return the root item as the parent.
                if (folderPath == "")
                    return m_RootItem;

                // Does folder path exist?
                int id = folderPath.GetHashCode();
                TreeViewItem item = TreeViewUtility.FindItem(id, m_RootItem);

                if (item != null)
                {
                    return item;
                }

                // Add folders as needed
                string[] splitPath = folderPath.Split('/');
                string currentPath = "";
                TreeViewItem currentItem = m_RootItem;
                int folderDepth = -1; // Will be incremented to the right depth in the loop.

                for (int depth = 0; depth < splitPath.Length; ++depth)
                {
                    string folder = splitPath[depth];
                    if (currentPath != "")
                        currentPath += '/';

                    currentPath += folder;

                    // Dont create a 'Assets' folder (we already have that as a hidden root)
                    if (depth == 0 && currentPath == "Assets")
                        continue;

                    // Only increment the folder depth if we are past the root "Assets" folder.
                    ++folderDepth;

                    id = currentPath.GetHashCode();

                    TreeViewItem foundItem;
                    if (treeViewFolders.TryGetValue(currentPath, out foundItem))
                    {
                        currentItem = foundItem;
                    }
                    else
                    {
                        // If we do not have a tree view item for this folder we create one
                        var folderItem = new PackageImportTreeViewItem(null, id, folderDepth, currentItem, folder);

                        // Add to children array of the parent
                        currentItem.AddChild(folderItem);
                        currentItem = folderItem;

                        // Auto expand all folder items
                        if (initExpandedState)
                            m_TreeView.state.expandedIDs.Add(id);

                        // For faster finding of folders
                        treeViewFolders[currentPath] = folderItem;
                    }
                }

                return currentItem;
            }
        }


        class PreviewPopup : PopupWindowContent
        {
            readonly Texture2D m_Preview;
            readonly Vector2 kPreviewSize = new Vector2(128f, 128f);

            public PreviewPopup(Texture2D preview)
            {
                m_Preview = preview;
            }

            public override void OnGUI(Rect rect)
            {
                PackageImport.DrawTexture(rect, m_Preview, false);
            }

            public override Vector2 GetWindowSize()
            {
                return kPreviewSize;
            }
        }
    }
}
