using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Collections;
using Robust.Shared.Graphics;
using Robust.Shared.Input;
using Robust.Shared.Maths;
using Timer = Robust.Shared.Timing.Timer;

/// <summary>
/// Represents a scrollable list of items in a user interface.
/// </summary>
namespace Robust.Client.UserInterface.Controls
{
    [Virtual]
    public class ItemList : Control, IList<ItemList.Item>
    {
        private bool _isAtBottom = true;
        /// The size of all the child widgets, in pixels
        private int _totalContentHeight;

        private VScrollBar _scrollBar;
        private readonly List<Item> _itemList = new();
        public event Action<ItemListSelectedEventArgs>? OnItemSelected;
        public event Action<ItemListDeselectedEventArgs>? OnItemDeselected;
        public event Action<ItemListHoverEventArgs>? OnItemHover;

        public const string StylePropertyBackground = "itemlist-background";
        public const string StylePropertyItemBackground = "item-background";
        public const string StylePropertySelectedItemBackground = "selected-item-background";
        public const string StylePropertyDisabledItemBackground = "disabled-item-background";

        /// <summary>
        /// Gets or sets the ItemSeparation of individual list items
        /// </summary>
        public int ItemSeparation { get; set; } = 0; // Default value is 0px
        public int Count => _itemList.Count;
        public bool IsReadOnly => false;

        public bool ScrollFollowing { get; set; } = false;
        public int ButtonDeselectDelay { get; set; } = 100;

        public ItemListSelectMode SelectMode { get; set; } = ItemListSelectMode.Single;

        public ItemList()
        {
            MouseFilter = MouseFilterMode.Pass;

            RectClipContent = true;

            _scrollBar = new VScrollBar
            {
                Name = "_v_scroll",

                HorizontalAlignment = HAlignment.Right
            };
            AddChild(_scrollBar);
            _scrollBar.OnValueChanged += _ => _isAtBottom = _scrollBar.IsAtEnd;
        }

        private void RecalculateContentHeight()
        {
            _totalContentHeight = 0;
            foreach (var item in _itemList)
            {
                var itemHeight = 0f;
                if (item.Icon != null)
                {
                    itemHeight = item.IconSize.Y * item.IconScale;
                }

                itemHeight = Math.Max(itemHeight, ActualFont.GetHeight(UIScale));
                itemHeight += ActualItemBackground.MinimumSize.Y * UIScale;

                _totalContentHeight += (int)Math.Ceiling(itemHeight);
                _totalContentHeight += ItemSeparation;
            }
            //Remove unneeded ItemSeparation on last item.
            _totalContentHeight -= ItemSeparation;
            _scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
            _updateScrollbarVisibility();
        }

        public void Add(IEnumerable<Item> items)
        {
            foreach (var item in items)
            {
                if(item.Owner != this) throw new ArgumentException("Item is owned by another ItemList!");

                _itemList.Add(item);

                item.OnSelected += Select;
                item.OnDeselected += Deselect;
            }

            Recalculate();
        }

        public void Add(Item item)
        {
            if(item.Owner != this) throw new ArgumentException("Item is owned by another ItemList!");

            _itemList.Add(item);

            item.OnSelected += Select;
            item.OnDeselected += Deselect;

            Recalculate();
        }

        public void AddItems(IEnumerable<string> texts, Texture? icon = null, bool selectable = true, object? metadata = null, float iconScale = 1)
        {
            var items = new ValueList<Item>();

            foreach (var text in texts)
            {
                items.Add(new Item(this) {Text = text, Icon = icon, IconScale = iconScale, Selectable = selectable, Metadata = metadata});
            }

            Add(items);
        }

        public Item AddItem(string text, Texture? icon = null, bool selectable = true, object? metadata = null, float iconScale = 1)
        {
            var item = new Item(this) {Text = text, Icon = icon, IconScale = iconScale, Selectable = selectable, Metadata = metadata};
            Add(item);
            return item;
        }

        public void Clear()
        {
            // Handle this manually so we can just clear all at once.
            foreach (var item in _itemList)
            {
                item.OnSelected -= Select;
                item.OnDeselected -= Deselect;
            }

            _itemList.Clear();
            Recalculate();
            _totalContentHeight = 0;
        }

        public bool Contains(Item item)
        {
            return _itemList.Contains(item);
        }

        public void CopyTo(Item[] array, int arrayIndex)
        {
            _itemList.CopyTo(array, arrayIndex);
        }

        private void InternalRemoveAt(int index)
        {
            if (_itemList.Count <= index)
                return;

            // If you modify this then also make sure to update Clear!
            var item = _itemList[index];
            _itemList.RemoveAt(index);

            item.OnSelected -= Select;
            item.OnDeselected -= Deselect;
        }

        public bool Remove(Item item)
        {
            var value =  _itemList.Remove(item);

            item.OnSelected -= Select;
            item.OnDeselected -= Deselect;

            Recalculate();

            return value;
        }

        public void RemoveAt(int index)
        {
            InternalRemoveAt(index);
            Recalculate();
        }

        public IEnumerator<Item> GetEnumerator()
        {
            return _itemList.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public int IndexOf(Item item)
        {
            return _itemList.IndexOf(item);
        }

        private void InternalInsert(int index, Item item)
        {
            if(item.Owner != this) throw new ArgumentException("Item is owned by another ItemList!");

            _itemList.Insert(index, item);

            item.OnSelected += Select;
            item.OnDeselected += Deselect;
        }

        public void Insert(int index, Item item)
        {
            InternalInsert(index, item);
            Recalculate();
        }

        private void Recalculate()
        {
            RecalculateContentHeight();
            if (_isAtBottom && ScrollFollowing)
                _scrollBar.MoveToEnd();
        }

        /// <summary>
        /// Replace the current list of items with the items in newItems.
        /// newItems should be in the order which they should appear in the list,
        /// and items are considered equal if the Item text is equal in each item.
        ///
        /// Provided the existing items have not been re-ordered relative to each
        /// other, any items which already exist in the list are not destroyed,
        /// which maintains consistency of scrollbars, selected items, etc.
        /// </summary>
        /// <param name="newItems">The list of items to update this list to</param>
        public void SetItems(List<Item> newItems)
        {
            SetItems(newItems, (a,b) => string.Compare(a.Text, b.Text));
        }

        /// <summary>
        /// This variant allows for a custom equality operator to compare items, when
        /// comparing the Item text is not desired.
        /// </summary>
        /// <param name="itemCmp">Comparison function to compare existing to new items.</param>
        public void SetItems(List<Item> newItems, Comparison<Item> itemCmp)
        {
            // Walk through the existing items in this list and in newItems
            // in parallel to synchronize our items with those in newItems.
            int i = this.Count - 1;
            int j = newItems.Count - 1;
            while(i >= 0 && j >= 0)
            {
                var cmpResult = itemCmp(this[i], newItems[j]);
                if (cmpResult == 0)
                {
                    // This item exists in both our list and `newItems`. Nothing to do.
                    i--;
                    j--;
                }
                else if (cmpResult > 0)
                {
                    // Item exists in our list, but not in `newItems`. Remove it.
                    InternalRemoveAt(i);
                    i--;
                }
                else if (cmpResult < 0)
                {
                    // A new entry which doesn't exist in our list. Insert it.
                    InternalInsert(i + 1, newItems[j]);
                    j--;
                }
            }

            // Any remaining items in our list don't exist in `newItems` so remove them
            while (i >= 0)
            {
                InternalRemoveAt(i);
                i--;
            }

            // And finally, any remaining items in `newItems` don't exist in our list. Create them.
            while (j >= 0)
            {
                InternalInsert(0, newItems[j]);
                j--;
            }

            Recalculate();
        }

        // Without this attribute, this would compile into a property called "Item", causing problems with the Item class.
        [System.Runtime.CompilerServices.IndexerName("IndexItem")]
        public Item this[int index]
        {
            get => _itemList[index];
            set => _itemList[index] = value;
        }

        public IEnumerable<Item> GetSelected()
        {
            var list = new List<Item>();

            for (var i = 0; i < _itemList.Count; i++)
            {
                var item = _itemList[i];
                if (item.Selected) list.Add(item);
            }

            return list;
        }

        private void Select(int idx)
        {
            if(SelectMode != ItemListSelectMode.Multiple)
                ClearSelected(idx);
            OnItemSelected?.Invoke(new ItemListSelectedEventArgs(idx, this));
        }

        private void Select(Item item)
        {
            var idx = IndexOf(item);
            if (idx != -1)
                Select(idx);
        }

        private void Deselect(int idx)
        {
            OnItemDeselected?.Invoke(new ItemListDeselectedEventArgs(idx, this));
        }

        private void Deselect(Item item)
        {
            var idx = IndexOf(item);
            if (idx == -1) return;
            Deselect(idx);
        }

        public void ClearSelected(int? except = null)
        {
            for (var i = 0; i < _itemList.Count; i++)
            {
                if (i == except)
                    continue;

                var item = _itemList[i];
                item.Selected = false;
            }
        }

        public void SortItemsByText() => Sort((p, q) => string.Compare(p.Text, q.Text, StringComparison.Ordinal));

        public void Sort(Comparison<Item> comparison) => _itemList.Sort(comparison);


        public void EnsureCurrentIsVisible()
        {
            // TODO: Implement this.
        }

        public int GetItemAtPosition(Vector2 position, bool exact = false)
        {
            throw new NotImplementedException();
        }

        public Font ActualFont
        {
            get
            {
                if (TryGetStyleProperty<Font>("font", out var font))
                {
                    return font;
                }

                return UserInterfaceManager.ThemeDefaults.DefaultFont;
            }
        }

        public Color ActualFontColor
        {
            get
            {
                if (TryGetStyleProperty("font-color", out Color fontColor))
                {
                    return fontColor;
                }

                return Color.White;
            }
        }

        public StyleBox ActualBackground
        {
            get
            {
                if (TryGetStyleProperty<StyleBox>(StylePropertyBackground, out var bg))
                {
                    return bg;
                }

                return new StyleBoxFlat();
            }
        }
        public StyleBox ActualItemBackground
        {
            get
            {
                if (TryGetStyleProperty<StyleBox>(StylePropertyItemBackground, out var bg))
                {
                    return bg;
                }

                return new StyleBoxFlat();
            }
        }

        public StyleBox ActualSelectedItemBackground
        {
            get
            {
                if (TryGetStyleProperty<StyleBox>(StylePropertySelectedItemBackground, out var bg))
                {
                    return bg;
                }

                return new StyleBoxFlat();
            }
        }

        public StyleBox ActualDisabledItemBackground
        {
            get
            {
                if (TryGetStyleProperty<StyleBox>(StylePropertyDisabledItemBackground, out var bg))
                {
                    return bg;
                }

                return new StyleBoxFlat();
            }
        }

        public void ScrollToBottom()
        {
            _scrollBar.MoveToEnd();
            _isAtBottom = true;
        }

        protected internal override void Draw(DrawingHandleScreen handle)
        {
            base.Draw(handle);

            var sizeBox = PixelSizeBox;

            var font = ActualFont;
            var listBg = ActualBackground;
            var iconBg = ActualItemBackground;
            var iconSelectedBg = ActualSelectedItemBackground;
            var iconDisabledBg = ActualDisabledItemBackground;

            var offset = -_scrollBar.Value;

            listBg.Draw(handle, PixelSizeBox, UIScale);

            foreach (var item in _itemList)
            {
                var bg = iconBg;

                if (item.Disabled)
                    bg = iconDisabledBg;

                if (item.Selected)
                {
                    bg = iconSelectedBg;
                }

                var itemHeight = 0f;
                if (item.Icon != null)
                {
                    itemHeight = item.IconSize.Y * item.IconScale;
                }

                itemHeight = Math.Max(itemHeight, font.GetHeight(UIScale));
                itemHeight += ActualItemBackground.MinimumSize.Y * UIScale;

                var region = UIBox2.FromDimensions(0, offset, PixelWidth, itemHeight);
                item.Region = region;

                if (region.Intersects(sizeBox))
                {
                    bg.Draw(handle, item.Region.Value, UIScale);

                    var contentBox = bg.GetContentBox(item.Region.Value, UIScale);
                    var drawOffset = contentBox.TopLeft;
                    if (item.Icon != null)
                    {
                        if (item.IconRegion.Size == Vector2.Zero)
                        {
                            handle.DrawTextureRect(item.Icon, UIBox2.FromDimensions(drawOffset, item.Icon.Size * item.IconScale),
                                item.IconModulate);
                        }
                        else
                        {
                            handle.DrawTextureRectRegion(item.Icon, UIBox2.FromDimensions(drawOffset, item.Icon.Size * item.IconScale),
                                item.IconRegion, item.IconModulate);
                        }
                    }

                    if (item.Text != null)
                    {
                        var textBox = new UIBox2(contentBox.Left + item.IconSize.X * item.IconScale, contentBox.Top, contentBox.Right,
                            contentBox.Bottom);
                        DrawTextInternal(handle, item.Text, textBox);
                    }
                }

                offset += itemHeight;

                // Add a ItemSeparation at the bottom of each item.
                offset += ItemSeparation;
            }
        }

        protected void DrawTextInternal(DrawingHandleScreen handle, string text, UIBox2 box)
        {
            var font = ActualFont;

            var color = ActualFontColor;
            var offsetY = (int) (box.Height - font.GetHeight(UIScale)) / 2;
            var baseLine = new Vector2i(5, offsetY + font.GetAscent(UIScale)) + box.TopLeft;

            foreach (var rune in text.EnumerateRunes())
            {
                if (!font.TryGetCharMetrics(rune, UIScale, out var metrics))
                {
                    continue;
                }

                if (!(baseLine.X < box.Left || baseLine.X + metrics.Advance > box.Right))
                {
                    font.DrawChar(handle, rune, baseLine, UIScale, color);
                }

                baseLine += new Vector2(metrics.Advance, 0);
            }
        }

        protected override Vector2 MeasureOverride(Vector2 availableSize)
        {
            var size = Vector2.Zero;
            if (ActualBackground != null)
            {
                size += ActualBackground.MinimumSize;
            }

            return size;
        }

        protected internal override void KeyBindDown(GUIBoundKeyEventArgs args)
        {
            base.KeyBindDown(args);

            if (SelectMode == ItemListSelectMode.None || args.Function != EngineKeyFunctions.UIClick)
            {
                return;
            }

            foreach (var item in _itemList)
            {
                if (item.Region == null)
                    continue;

                if (!item.Region.Value.Contains(args.RelativePixelPosition))
                    continue;

                if (item.Selectable && !item.Disabled)
                {
                    if (item.Selected && SelectMode != ItemListSelectMode.Button)
                    {
                        if(SelectMode != ItemListSelectMode.Multiple)
                            ClearSelected();
                        item.Selected = false;
                        return;
                    }

                    item.Selected = true;
                    if (SelectMode == ItemListSelectMode.Button)
                        Timer.Spawn(ButtonDeselectDelay, () => {  item.Selected = false; } );
                }
                break;
            }
        }

        protected internal override void MouseMove(GUIMouseMoveEventArgs args)
        {
            base.MouseMove(args);

            DefaultCursorShape = CursorShape.Arrow;

            for (var idx = 0; idx < _itemList.Count; idx++)
            {
                var item = _itemList[idx];
                if (item.Region == null) continue;
                if (!item.Region.Value.Contains(args.RelativePosition)) continue;

                if (SelectMode != ItemListSelectMode.None)
                {
                    if (item.Disabled)
                    {
                        DefaultCursorShape = CursorShape.NotAllowed;
                    }
                    else if (item.Selectable)
                    {
                        DefaultCursorShape = CursorShape.Pointer;
                    }
                    else
                    {
                        DefaultCursorShape = CursorShape.Arrow;
                    }
                }

                OnItemHover?.Invoke(new ItemListHoverEventArgs(idx, this));
                break;
            }
        }

        protected internal override void MouseExited()
        {
            base.MouseExited();

            DefaultCursorShape = CursorShape.Arrow;
        }

        protected internal override void MouseWheel(GUIMouseWheelEventArgs args)
        {
            base.MouseWheel(args);

            if (MathHelper.CloseToPercent(0, args.Delta.Y))
            {
                return;
            }

            _scrollBar.ValueTarget -= _getScrollSpeed() * args.Delta.Y;
            _isAtBottom = _scrollBar.IsAtEnd;

            args.Handle();
        }

        [Pure]
        private int _getScrollSpeed()
        {
            var font = ActualFont;
            return font.GetHeight(UIScale) * 2;
        }

        protected override void Resized()
        {
            base.Resized();

            var styleBoxSize = (ActualBackground?.MinimumSize.Y ?? 0) * UIScale;
            _scrollBar.Page = PixelSize.Y - styleBoxSize;
            RecalculateContentHeight();
        }

        protected internal override void UIScaleChanged()
        {
             RecalculateContentHeight();

             base.UIScaleChanged();
        }

        private void _updateScrollbarVisibility()
        {
            _scrollBar.Visible = _totalContentHeight + ActualBackground.MinimumSize.Y > Height;
        }

        public abstract class ItemListEventArgs : EventArgs
        {
            /// <summary>
            ///     The ItemList this event originated from.
            /// </summary>
            public ItemList ItemList { get; }

            protected ItemListEventArgs(ItemList list)
            {
                ItemList = list;
            }
        }

        public sealed class ItemListSelectedEventArgs : ItemListEventArgs
        {
            /// <summary>
            ///     The index of the item that was selected.
            /// </summary>
            public int ItemIndex;

            public ItemListSelectedEventArgs(int itemIndex, ItemList list) : base(list)
            {
                ItemIndex = itemIndex;
            }
        }

        public sealed class ItemListDeselectedEventArgs : ItemListEventArgs
        {
            /// <summary>
            ///     The index of the item that was selected.
            /// </summary>
            public int ItemIndex;

            public ItemListDeselectedEventArgs(int itemIndex, ItemList list) : base(list)
            {
                ItemIndex = itemIndex;
            }
        }

        public sealed class ItemListHoverEventArgs : ItemListEventArgs
        {
            /// <summary>
            ///     The index of the item that was selected.
            /// </summary>
            public int ItemIndex;

            public ItemListHoverEventArgs(int itemIndex, ItemList list) : base(list)
            {
                ItemIndex = itemIndex;
            }
        }

        public enum ItemListSelectMode : byte
        {
            None,
            Single,
            Multiple,
            Button,
        }

        public sealed class Item
        {
            public event Action<Item>? OnSelected;
            public event Action<Item>? OnDeselected;

            private bool _selected = false;
            private bool _disabled = false;

            public ItemList Owner { get; }
            public string? Text { get; set; }
            public string? TooltipText { get; set; }
            public Texture? Icon { get; set; }
            public UIBox2 IconRegion { get; set; }
            public Color IconModulate { get; set; } = Color.White;
            public float IconScale { get; set; } = 1;
            public bool Selectable { get; set; } = true;
            public bool TooltipEnabled { get; set; } = true;
            public UIBox2? Region { get; set; }
            public object? Metadata { get; set; }

            public bool Disabled
            {
                get => _disabled;
                set
                {
                    _disabled = value;
                    if (Selected) Selected = false;
                }
            }
            public bool Selected
            {
                get => _selected;
                set
                {
                    if (!Selectable || _selected == value) return;
                    _selected = value;
                    if(_selected) OnSelected?.Invoke(this);
                    else OnDeselected?.Invoke(this);
                }
            }

            public Vector2 IconSize
            {
                get
                {
                    if (Icon == null)
                        return Vector2.Zero;
                    return IconRegion.Size != Vector2.Zero ? IconRegion.Size : Icon.Size;
                }
            }

            public Item(ItemList owner)
            {
                Owner = owner;
            }
        }
    }
}
