// 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.Collections.Specialized;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Design;
using System.Drawing.Imaging;

namespace System.Windows.Forms;

public sealed partial class ImageList
{
    // Everything other than set_All, Add, and Clear will force handle creation.
    [Editor($"System.Windows.Forms.Design.ImageCollectionEditor, {Assemblies.SystemDesign}", typeof(UITypeEditor))]
    public sealed partial class ImageCollection : IList
    {
        private readonly ImageList _owner;
        private readonly List<ImageInfo> _imageInfoCollection = [];

        /// A caching mechanism for key accessor
        /// We use an index here rather than control so that we don't have lifetime
        /// issues by holding on to extra references.
        private int _lastAccessedIndex = -1;

        // Indicates whether images are added in a batch.
        private bool _isBatchAdd;

        /// <summary>
        ///  Returns the keys in the image list - images without keys return String.Empty.
        /// </summary>
        public StringCollection Keys
        {
            get
            {
                // pass back a copy of the current state.
                StringCollection keysCollection = [];

                for (int i = 0; i < _imageInfoCollection.Count; i++)
                {
                    if ((_imageInfoCollection[i] is ImageInfo image) && (image.Name is not null) && (image.Name.Length != 0))
                    {
                        keysCollection.Add(image.Name);
                    }
                    else
                    {
                        keysCollection.Add(string.Empty);
                    }
                }

                return keysCollection;
            }
        }

        internal ImageCollection(ImageList owner)
        {
            _owner = owner;
        }

        internal void ResetKeys()
        {
            _imageInfoCollection.Clear();

            for (int i = 0; i < Count; i++)
            {
                _imageInfoCollection.Add(new ImageInfo());
            }
        }

        [Conditional("DEBUG")]
        private void AssertInvariant()
        {
            Debug.Assert(_owner is not null, "ImageCollection has no owner (ImageList)");
            Debug.Assert((_owner._originals is null) == (_owner.HandleCreated), " Either we should have the original images, or the handle should be created");
        }

        [Browsable(false)]
        public int Count
        {
            get
            {
                Debug.Assert(_owner is not null, "ImageCollection has no owner (ImageList)");

                if (_owner.HandleCreated)
                {
                    return PInvoke.ImageList.GetImageCount(_owner);
                }
                else
                {
                    if (_owner._originals is null)
                    {
                        return 0;
                    }

                    int count = 0;
                    foreach (Original original in _owner._originals)
                    {
                        if (original is not null)
                        {
                            count += original._nImages;
                        }
                    }

                    return count;
                }
            }
        }

        object ICollection.SyncRoot => this;

        bool ICollection.IsSynchronized => false;

        bool IList.IsFixedSize => false;

        public bool IsReadOnly => false;

        /// <summary>
        ///  Determines if the ImageList has any images, without forcing a handle creation.
        /// </summary>
        public bool Empty => Count == 0;

        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public Image this[int index]
        {
            get
            {
                ArgumentOutOfRangeException.ThrowIfNegative(index);
                ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count);

                return _owner.GetBitmap(index);
            }
            set
            {
                ArgumentOutOfRangeException.ThrowIfNegative(index);
                ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count);
                ArgumentNullException.ThrowIfNull(value);

                if (value is not Bitmap bitmap)
                {
                    throw new ArgumentException(SR.ImageListBitmap);
                }

                AssertInvariant();
                bool ownsImage = false;
                if (_owner.UseTransparentColor && bitmap.RawFormat.Guid != ImageFormat.Icon.Guid)
                {
                    // Since there's no ImageList_ReplaceMasked, we need to generate
                    // a transparent bitmap
                    bitmap = (Bitmap)bitmap.Clone();
                    bitmap.MakeTransparent(_owner.TransparentColor);
                    ownsImage = true;
                }

                try
                {
                    HBITMAP hMask = (HBITMAP)ControlPaint.CreateHBitmapTransparencyMask(bitmap);
                    HBITMAP hBitmap = (HBITMAP)ControlPaint.CreateHBitmapColorMask(bitmap, hMask);
                    bool ok;
                    try
                    {
                        ok = PInvoke.ImageList.Replace(_owner, index, hBitmap, hMask);
                    }
                    finally
                    {
                        PInvokeCore.DeleteObject((HGDIOBJ)hBitmap);
                        PInvokeCore.DeleteObject((HGDIOBJ)hMask);
                    }

                    if (!ok)
                    {
                        throw new InvalidOperationException(SR.ImageListReplaceFailed);
                    }
                }
                finally
                {
                    if (ownsImage)
                    {
                        bitmap.Dispose();
                    }
                }
            }
        }

        object? IList.this[int index]
        {
            get => this[index];
            set
            {
                if (value is not Image image)
                {
                    throw new ArgumentException(SR.ImageListBadImage, nameof(value));
                }

                this[index] = image;
            }
        }

        /// <summary>
        ///  Retrieves the child control with the specified key.
        /// </summary>
        public Image? this[string key]
        {
            get
            {
                // We do not support null and empty string as valid keys.
                if (string.IsNullOrEmpty(key))
                {
                    return null;
                }

                // Search for the key in our collection
                int index = IndexOfKey(key);
                if (!IsValidIndex(index))
                {
                    return null;
                }

                return this[index];
            }
        }

        /// <summary>
        ///  Adds an image to the end of the image list with a key accessor.
        /// </summary>
        public void Add(string key, Image image)
        {
            Debug.Assert((Count == _imageInfoCollection.Count), "The count of these two collections should be equal.");

            // Store off the name.
            ImageInfo imageInfo = new()
            {
                Name = key
            };

            // Add the image to the IList
            Original original = new(image, OriginalOptions.Default);
            Add(original, imageInfo);
        }

        /// <summary>
        ///  Adds an icon to the end of the image list with a key accessor.
        /// </summary>
        public void Add(string key, Icon icon)
        {
            Debug.Assert((Count == _imageInfoCollection.Count), "The count of these two collections should be equal.");

            // Store off the name.
            ImageInfo imageInfo = new()
            {
                Name = key
            };

            // Add the image to the IList
            Original original = new(icon, OriginalOptions.Default);
            Add(original, imageInfo);
        }

        int IList.Add(object? value)
        {
            if (value is not Image image)
            {
                throw new ArgumentException(SR.ImageListBadImage, nameof(value));
            }

            Add(image);
            return Count - 1;
        }

        public void Add(Icon value)
        {
            ArgumentNullException.ThrowIfNull(value);

            // Don't clone it now is a breaking change, so we have to keep track of this specific icon and dispose that
            Add(new Original(value.Clone(), OriginalOptions.OwnsImage), null);
        }

        /// <summary>
        ///  Add the given image to the ImageList.
        /// </summary>
        public void Add(Image value)
        {
            ArgumentNullException.ThrowIfNull(value);

            Original original = new(value, OriginalOptions.Default);
            Add(original, null);
        }

        /// <summary>
        ///  Add the given image to the ImageList, using the given color
        ///  to generate the mask. The number of images to add is inferred from
        ///  the width of the given image.
        /// </summary>
        public int Add(Image value, Color transparentColor)
        {
            ArgumentNullException.ThrowIfNull(value);

            Original original = new(value, OriginalOptions.CustomTransparentColor, transparentColor);
            return Add(original, null);
        }

        private int Add(Original original, ImageInfo? imageInfo)
        {
            ArgumentNullException.ThrowIfNull(original);
            ArgumentNullException.ThrowIfNull(original._image, nameof(original));

            int index = -1;

            AssertInvariant();

            if (original._image is Bitmap)
            {
                if (_owner._originals is not null)
                {
                    index = ((IList)_owner._originals).Add(original);
                }

                if (_owner.HandleCreated)
                {
                    Bitmap bitmapValue = _owner.CreateBitmap(original, out bool ownsBitmap);
                    index = _owner.AddToHandle(bitmapValue);
                    if (ownsBitmap)
                    {
                        bitmapValue.Dispose();
                    }
                }
            }
            else if (original._image is Icon originalIcon)
            {
                if (_owner._originals is not null)
                {
                    index = ((IList)_owner._originals).Add(original);
                }

                if (_owner.HandleCreated)
                {
                    index = _owner.AddIconToHandle(original, originalIcon);
                    // NOTE: if we own the icon (it's been created by us) this WILL dispose the icon to avoid a GDI leak
                    // **** original.image is NOT LONGER VALID AFTER THIS POINT ***
                }
            }
            else
            {
                throw new ArgumentException(SR.ImageListBitmap);
            }

            // update the imageInfoCollection
            // support AddStrip
            if ((original._options & OriginalOptions.ImageStrip) != 0)
            {
                for (int i = 0; i < original._nImages; i++)
                {
                    _imageInfoCollection.Add(new ImageInfo());
                }
            }
            else
            {
                imageInfo ??= new ImageInfo();
                _imageInfoCollection.Add(imageInfo);
            }

            if (!_isBatchAdd)
            {
                _owner.OnChangeHandle(EventArgs.Empty);
            }

            return index;
        }

        public void AddRange(params Image[] images)
        {
            ArgumentNullException.ThrowIfNull(images);

            _isBatchAdd = true;
            foreach (Image image in images)
            {
                Add(image);
            }

            _isBatchAdd = false;
            _owner.OnChangeHandle(EventArgs.Empty);
        }

        /// <summary>
        ///  Add an image strip the given image to the ImageList. A strip is a single Image
        ///  which is treated as multiple images arranged side-by-side.
        /// </summary>
        public int AddStrip(Image value)
        {
            ArgumentNullException.ThrowIfNull(value);

            // strip width must be a positive multiple of image list width
            if (value.Width == 0 || (value.Width % _owner.ImageSize.Width) != 0)
            {
                throw new ArgumentException(SR.ImageListStripBadWidth, nameof(value));
            }

            if (value.Height != _owner.ImageSize.Height)
            {
                throw new ArgumentException(SR.ImageListImageTooShort, nameof(value));
            }

            int nImages = value.Width / _owner.ImageSize.Width;

            Original original = new(value, OriginalOptions.ImageStrip, nImages);

            return Add(original, null);
        }

        /// <summary>
        ///  Remove all images and masks from the ImageList.
        /// </summary>
        public void Clear()
        {
            AssertInvariant();
            _owner._originals?.Clear();

            _imageInfoCollection.Clear();

            if (_owner.HandleCreated)
            {
                PInvoke.ImageList.Remove(_owner, -1);
            }

            _owner.OnChangeHandle(EventArgs.Empty);
        }

        [EditorBrowsable(EditorBrowsableState.Never)]
        public bool Contains(Image image) => throw new NotSupportedException();

        bool IList.Contains(object? value)
        {
            if (value is not Image image)
            {
                return false;
            }

            return Contains(image);
        }

        /// <summary>
        ///  Returns true if the collection contains an item with the specified key, false otherwise.
        /// </summary>
        public bool ContainsKey(string key) => IsValidIndex(IndexOfKey(key));

        [EditorBrowsable(EditorBrowsableState.Never)]
        public int IndexOf(Image image) => throw new NotSupportedException();

        int IList.IndexOf(object? value)
        {
            if (value is not Image image)
            {
                return -1;
            }

            return IndexOf(image);
        }

        /// <summary>
        ///  The zero-based index of the first occurrence of value within the entire CollectionBase,
        ///  if found; otherwise, -1.
        /// </summary>
        public int IndexOfKey(string key)
        {
            if (string.IsNullOrEmpty(key))
            {
                // We don't support empty or null keys.
                return -1;
            }

            // Check the last cached item
            int i = _lastAccessedIndex;
            if (IsValidIndex(i))
            {
                if ((_imageInfoCollection[i] is not null) &&
                    (WindowsFormsUtils.SafeCompareStrings(_imageInfoCollection[i].Name, key, ignoreCase: true)))
                {
                    return i;
                }
            }

            // Search for the item
            for (i = 0; i < Count; i++)
            {
                if ((_imageInfoCollection[i] is not null) &&
                        (WindowsFormsUtils.SafeCompareStrings(_imageInfoCollection[i].Name, key, ignoreCase: true)))
                {
                    _lastAccessedIndex = i;
                    return i;
                }
            }

            // We didn't find it. Invalidate the last accessed index and return -1.
            _lastAccessedIndex = -1;
            return -1;
        }

        void IList.Insert(int index, object? value) => throw new NotSupportedException();

        /// <summary>
        ///  Determines if the index is valid for the collection.
        /// </summary>
        private bool IsValidIndex(int index) => index >= 0 && index < Count;

        void ICollection.CopyTo(Array dest, int index)
        {
            AssertInvariant();
            for (int i = 0; i < Count; ++i)
            {
                dest.SetValue(_owner.GetBitmap(i), index++);
            }
        }

        public IEnumerator GetEnumerator()
        {
            // Forces handle creation

            AssertInvariant();
            Image[] images = new Image[Count];
            for (int i = 0; i < images.Length; ++i)
            {
                images[i] = _owner.GetBitmap(i);
            }

            return images.GetEnumerator();
        }

        [EditorBrowsable(EditorBrowsableState.Never)]
        public void Remove(Image image) => throw new NotSupportedException();

        void IList.Remove(object? value)
        {
            if (value is Image image)
            {
                Remove(image);

                _owner.OnChangeHandle(EventArgs.Empty);
            }
        }

        public void RemoveAt(int index)
        {
            ArgumentOutOfRangeException.ThrowIfNegative(index);
            ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count);

            AssertInvariant();
            if (!PInvoke.ImageList.Remove(_owner, index))
            {
                throw new InvalidOperationException(SR.ImageListRemoveFailed);
            }

            if ((_imageInfoCollection is not null) && (index >= 0 && index < _imageInfoCollection.Count))
            {
                _imageInfoCollection.RemoveAt(index);

                _owner.OnChangeHandle(EventArgs.Empty);
            }
        }

        /// <summary>
        ///  Removes the child control with the specified key.
        /// </summary>
        public void RemoveByKey(string key)
        {
            int index = IndexOfKey(key);
            if (IsValidIndex(index))
            {
                RemoveAt(index);
            }
        }

        /// <summary>
        ///  Sets/Resets the key accessor for an image already in the image list.
        /// </summary>
        public void SetKeyName(int index, string name)
        {
            if (!IsValidIndex(index))
            {
                throw new IndexOutOfRangeException();
            }

            if (_imageInfoCollection[index] is null)
            {
                _imageInfoCollection[index] = new ImageInfo();
            }

            _imageInfoCollection[index].Name = name;
        }
    }
}
