// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Design;
using System.Globalization;
using System.Windows.Forms.Layout;

namespace System.Windows.Forms;

/// <summary>
///  Represents a Windows up-down control that displays string values.
/// </summary>
[DefaultProperty(nameof(Items))]
[DefaultEvent(nameof(SelectedItemChanged))]
[DefaultBindingProperty(nameof(SelectedItem))]
[SRDescription(nameof(SR.DescriptionDomainUpDown))]
public partial class DomainUpDown : UpDownBase
{
    private static readonly string s_defaultValue = string.Empty;

    /// <summary>
    ///  Allowable strings for the domain updown.
    /// </summary>
    private DomainUpDownItemCollection? _domainItems;

    private string _stringValue = s_defaultValue;      // Current string value
    private int _domainIndex = -1;                    // Index in the domain list
    private bool _sorted;                 // Sort the domain values

    private EventHandler? _onSelectedItemChanged;

    private bool _inSort;

    /// <summary>
    ///  Initializes a new instance of the <see cref="DomainUpDown"/> class.
    /// </summary>
    public DomainUpDown() : base()
    {
        // this class overrides GetPreferredSizeCore, let Control automatically cache the result
        SetExtendedState(ExtendedStates.UserPreferredSizeCache, true);
        Text = string.Empty;
    }

    // Properties

    /// <summary>
    ///  Gets the collection of objects assigned to the
    ///  up-down control.
    /// </summary>
    [SRCategory(nameof(SR.CatData))]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
    [SRDescription(nameof(SR.DomainUpDownItemsDescr))]
    [Localizable(true)]
    [Editor($"System.Windows.Forms.Design.StringCollectionEditor, {AssemblyRef.SystemDesign}", typeof(UITypeEditor))]
    public DomainUpDownItemCollection Items => _domainItems ??= new DomainUpDownItemCollection(this);

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public new Padding Padding
    {
        get => base.Padding;
        set => base.Padding = value;
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new event EventHandler? PaddingChanged
    {
        add => base.PaddingChanged += value;
        remove => base.PaddingChanged -= value;
    }

    /// <summary>
    ///  Gets or sets the index value of the selected item.
    /// </summary>
    [Browsable(false)]
    [DefaultValue(-1)]
    [SRCategory(nameof(SR.CatAppearance))]
    [SRDescription(nameof(SR.DomainUpDownSelectedIndexDescr))]
    public int SelectedIndex
    {
        get
        {
            if (UserEdit)
            {
                return -1;
            }
            else
            {
                return _domainIndex;
            }
        }

        set
        {
            ArgumentOutOfRangeException.ThrowIfLessThan(value, -1);
            ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(value, Items.Count);

            if (value != SelectedIndex)
            {
                SelectIndex(value);
            }
        }
    }

    /// <summary>
    ///  Gets or sets the selected item based on the index value
    ///  of the selected item in the
    ///  collection.
    /// </summary>
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.DomainUpDownSelectedItemDescr))]
    public object? SelectedItem
    {
        get
        {
            int index = SelectedIndex;
            return (index == -1) ? null : Items[index];
        }
        set
        {
            // Treat null as selecting no item
            if (value is null)
            {
                SelectedIndex = -1;
            }
            else
            {
                // Attempt to find the given item in the list of items
                for (int i = 0; i < Items.Count; i++)
                {
                    if (value.Equals(Items[i]))
                    {
                        SelectedIndex = i;
                        break;
                    }
                }
            }
        }
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the item collection is sorted.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.DomainUpDownSortedDescr))]
    public bool Sorted
    {
        get
        {
            return _sorted;
        }

        set
        {
            _sorted = value;
            if (_sorted)
            {
                SortDomainItems();
            }
        }
    }

    internal override bool SupportsUiaProviders => true;

    /// <summary>
    ///  Gets or sets a value indicating whether the collection of items continues to
    ///  the first or last item if the user continues past the end of the list.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [Localizable(true)]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.DomainUpDownWrapDescr))]
    public bool Wrap { get; set; }

    //////////////////////////////////////////////////////////////
    // Methods
    //
    //////////////////////////////////////////////////////////////
    /// <summary>
    ///  Occurs when the <see cref="SelectedItem"/> property has
    ///  been changed.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.DomainUpDownOnSelectedItemChangedDescr))]
    public event EventHandler? SelectedItemChanged
    {
        add => _onSelectedItemChanged += value;
        remove => _onSelectedItemChanged -= value;
    }

    /// <summary>
    ///  Displays the next item in the object collection.
    /// </summary>
    public override void DownButton()
    {
        // Make sure domain values exist, and there are >0 items
        if (_domainItems is null)
        {
            return;
        }

        if (_domainItems.Count <= 0)
        {
            return;
        }

        // If the user has entered text, attempt to match it to the domain list
        //
        int matchIndex = -1;
        if (UserEdit)
        {
            matchIndex = MatchIndex(Text, false, _domainIndex);
        }

        if (matchIndex != -1)
        {
            // Found a match, so select this value
            _domainIndex = matchIndex;
            SelectIndex(matchIndex);
        }
        else
        {
            // Otherwise, get the next string in the domain list
            if (_domainIndex < _domainItems.Count - 1)
            {
                SelectIndex(_domainIndex + 1);
            }
            else if (Wrap)
            {
                SelectIndex(0);
            }
        }
    }

    /// <summary>
    ///  Tries to find a match of the supplied text in the domain list.
    ///  If complete is true, a complete match is required for success
    ///  (i.e. the supplied text is the same length as the matched domain value)
    ///  Returns the index in the domain list if the match is successful,
    ///  returns -1 otherwise.
    /// </summary>
    internal int MatchIndex(string text, bool complete)
    {
        return MatchIndex(text, complete, _domainIndex);
    }

    internal int MatchIndex(string text, bool complete, int startPosition)
    {
        // Make sure domain values exist
        if (_domainItems is null)
        {
            return -1;
        }

        // Sanity check of parameters
        if (text.Length < 1)
        {
            return -1;
        }

        if (_domainItems.Count <= 0)
        {
            return -1;
        }

        if (startPosition < 0)
        {
            startPosition = _domainItems.Count - 1;
        }

        if (startPosition >= _domainItems.Count)
        {
            startPosition = 0;
        }

        // Attempt to match the supplied string text with
        // the domain list. Returns the index in the list if successful,
        // otherwise returns -1.
        int index = startPosition;
        int matchIndex = -1;
        bool found;

        if (!complete)
        {
            text = text.ToUpper(CultureInfo.InvariantCulture);
        }

        // Attempt to match the string with Items[index]
        do
        {
            found = complete
                ? Items[index]!.ToString()!.Equals(text)
                : Items[index]!.ToString()!.ToUpper(CultureInfo.InvariantCulture)
                    .StartsWith(text, StringComparison.Ordinal);

            if (found)
            {
                matchIndex = index;
            }

            // Calculate the next index to attempt to match
            index++;
            if (index >= _domainItems.Count)
            {
                index = 0;
            }
        }
        while (!found && index != startPosition);

        return matchIndex;
    }

    /// <summary>
    ///  In the case of a DomainUpDown, the handler for changing
    ///  values is called OnSelectedItemChanged - so just forward it to that
    ///  function.
    /// </summary>
    protected override void OnChanged(object? source, EventArgs e)
    {
        OnSelectedItemChanged(source, e);
    }

    /// <summary>
    ///  Handles the <see cref="Control.KeyPress"/>
    ///  event, using the input character to find the next matching item in our
    ///  item collection.
    /// </summary>
    protected override void OnTextBoxKeyPress(object? source, KeyPressEventArgs e)
    {
        if (ReadOnly)
        {
            char[] character = [e.KeyChar];
            UnicodeCategory uc = char.GetUnicodeCategory(character[0]);

            if (uc is UnicodeCategory.LetterNumber
                or UnicodeCategory.LowercaseLetter
                or UnicodeCategory.DecimalDigitNumber
                or UnicodeCategory.MathSymbol
                or UnicodeCategory.OtherLetter
                or UnicodeCategory.OtherNumber
                or UnicodeCategory.UppercaseLetter)
            {
                // Attempt to match the character to a domain item
                int matchIndex = MatchIndex(new string(character), false, _domainIndex + 1);
                if (matchIndex != -1)
                {
                    // Select the matching domain item
                    SelectIndex(matchIndex);
                }

                e.Handled = true;
            }
        }

        base.OnTextBoxKeyPress(source, e);
    }

    /// <summary>
    ///  Raises the <see cref="SelectedItemChanged"/> event.
    /// </summary>
    protected void OnSelectedItemChanged(object? source, EventArgs e)
    {
        // Call the event handler
        _onSelectedItemChanged?.Invoke(this, e);
    }

    /// <summary>
    ///  Selects the item in the domain list at the given index
    /// </summary>
    private void SelectIndex(int index)
    {
        // Sanity check index

        Debug.Assert(_domainItems is not null, "Domain values array is null");
        Debug.Assert(index < _domainItems.Count && index >= -1, "SelectValue: index out of range");
        if (_domainItems is null || index < -1 || index >= _domainItems.Count)
        {
            return;
        }

        // If the selected index has changed, update the text
        _domainIndex = index;
        if (_domainIndex >= 0)
        {
            _stringValue = _domainItems[_domainIndex]!.ToString()!;
            UserEdit = false;
            UpdateEditText();
        }
        else
        {
            UserEdit = true;
        }

        Debug.Assert(_domainIndex >= 0 || UserEdit, $"UserEdit should be true when domainIndex < 0 {UserEdit}");
    }

    /// <summary>
    ///  Sorts the domain values
    /// </summary>
    private void SortDomainItems()
    {
        if (_inSort)
        {
            return;
        }

        _inSort = true;
        try
        {
            // Sanity check
            Debug.Assert(_sorted, "Sorted == false");

            if (_domainItems is not null)
            {
                // Sort the domain values
                ArrayList.Adapter(_domainItems).Sort(new DomainUpDownItemCompare());

                // Update the domain index
                if (!UserEdit)
                {
                    int newIndex = MatchIndex(_stringValue, true);
                    if (newIndex != -1)
                    {
                        SelectIndex(newIndex);
                    }
                }
            }
        }
        finally
        {
            _inSort = false;
        }
    }

    /// <summary>
    ///  Provides some interesting info about this control in String form.
    /// </summary>
    public override string ToString()
    {
        string s = base.ToString();

        if (Items is not null)
        {
            s = $"{s}, Items.Count: {Items.Count}, SelectedIndex: {SelectedIndex}";
        }

        return s;
    }

    /// <summary>
    ///  Displays the previous item in the collection.
    /// </summary>
    public override void UpButton()
    {
        // Make sure domain values exist, and there are >0 items
        if (_domainItems is null)
        {
            return;
        }

        if (_domainItems.Count <= 0)
        {
            return;
        }

        // If the user has entered text, attempt to match it to the domain list
        int matchIndex = -1;
        if (UserEdit)
        {
            matchIndex = MatchIndex(Text, false, _domainIndex);
        }

        if (matchIndex != -1)
        {
            // Found a match, so set the domain index accordingly
            _domainIndex = matchIndex;
            SelectIndex(matchIndex);
        }
        else
        {
            // Otherwise, get the previous string in the domain list
            if (_domainIndex > 0)
            {
                SelectIndex(_domainIndex - 1);
            }
            else if (Wrap)
            {
                SelectIndex(_domainItems.Count - 1);
            }
        }
    }

    /// <summary>
    ///  Updates the text in the up-down control to display the selected item.
    /// </summary>
    protected override void UpdateEditText()
    {
        UserEdit = false;
        ChangingText = true;
        Text = _stringValue;
    }

    // This is not a breaking change -- Even though this control previously autosized to height,
    // it didn't actually have an AutoSize property. The new AutoSize property enables the
    // smarter behavior.
    internal override Size GetPreferredSizeCore(Size proposedConstraints)
    {
        int height = PreferredHeight;
        int width = LayoutUtils.OldGetLargestStringSizeInCollection(Font, Items).Width;

        // AdjustWindowRect with our border, since textbox is borderless.
        width = SizeFromClientSizeInternal(new(width, height)).Width + _upDownButtons.Width;
        return new Size(width, height) + Padding.Size;
    }
}
