// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

//
// Description: CompositeCollection holds the list of items that constitute the content of a ItemsControl.
//
// See specs at ItemsControl.mht
//

using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;

using System.Windows;
using System.Windows.Markup;

using MS.Internal;              // Invariant.Assert
using MS.Internal.Data;
using MS.Utility;

using System;

namespace System.Windows.Data
{
    /// <summary>
    /// CompositeCollection will contain items shaped as strings, objects, xml nodes,
    /// elements as well as other collections.
    /// A <seealso cref="System.Windows.Controls.ItemsControl"/> uses the data
    /// in the CompositeCollection to generate its content according to its ItemTemplate.
    /// </summary>

    [Localizability(LocalizationCategory.Ignore)]
    public class CompositeCollection : IList, INotifyCollectionChanged, ICollectionViewFactory, IWeakEventListener
    {
        //------------------------------------------------------
        //
        //  Constructors
        //
        //------------------------------------------------------

        #region Constructors

        /// <summary>
        /// Initializes a new instance of CompositeCollection that is empty and has default initial capacity.
        /// </summary>
        public CompositeCollection()
        {
            Initialize(new ArrayList());
        }

        /// <summary>
        /// Initializes a new instance of CompositeCollection that is empty and has specified initial capacity.
        /// </summary>
        /// <param name="capacity">The number of items that the new list is initially capable of storing</param>
        /// <remarks>
        /// Some ItemsControl implementations have better idea how many items to anticipate,
        /// capacity parameter lets them tailor the initial size.
        /// </remarks>
        public CompositeCollection(int capacity)
        {
            Initialize(new ArrayList(capacity));
        }

        #endregion Constructors


        //------------------------------------------------------
        //
        //  Public Methods
        //
        //------------------------------------------------------

        #region Public Methods
        /// <summary>
        ///     Returns an enumerator object for this CompositeCollection
        /// </summary>
        /// <returns>
        ///     Enumerator object for this CompositeCollection
        /// </returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            // Enumerator from the underlying ArrayList
            return InternalList.GetEnumerator();
        }

        /// <summary>
        ///     Makes a shallow copy of object references from this
        ///     CompositeCollection to the given target array
        /// </summary>
        /// <param name="array">
        ///     Target of the copy operation
        /// </param>
        /// <param name="index">
        ///     Zero-based index at which the copy begins
        /// </param>
        public void CopyTo(Array array, int index)
        {
            // Forward call to internal list.
            InternalList.CopyTo(array, index);
        }

        /// <summary>
        ///     Add an item to this collection.
        /// </summary>
        /// <param name="newItem">
        ///     New item to be added to collection
        /// </param>
        /// <returns>
        ///     Zero-based index where the new item is added.
        /// </returns>
        /// <exception cref="ArgumentException">
        ///     CompositeCollection can only accept CollectionContainers it doesn't already have.
        /// </exception>
        public int Add(object newItem)
        {
            CollectionContainer cc = newItem as CollectionContainer;
            if (cc != null)
            {
                AddCollectionContainer(cc);
            }

            int addedIndex = InternalList.Add(newItem);

            OnCollectionChanged(NotifyCollectionChangedAction.Add, newItem, addedIndex);
            return addedIndex;
        }

        /// <summary>
        ///     Clears the collection.  Releases the references on all items
        /// currently in the collection.
        /// </summary>
        public void Clear()
        {
            // unhook contained collections
            for (int k=0, n=InternalList.Count;  k < n;  ++k)
            {
                CollectionContainer cc = this[k] as CollectionContainer;
                if (cc != null)
                {
                    RemoveCollectionContainer(cc);
                }
            }

            InternalList.Clear();
            OnCollectionChanged(NotifyCollectionChangedAction.Reset);
        }

        /// <summary>
        ///     Checks to see if a given item is in this collection
        /// </summary>
        /// <param name="containItem">
        ///     The item whose membership in this collection is to be checked.
        /// </param>
        /// <returns>
        ///     True if the collection contains the given item
        /// </returns>
        public bool Contains(object containItem)
        {
            return InternalList.Contains(containItem);
        }

        /// <summary>
        ///     Finds the index in this collection where the given item is found.
        /// </summary>
        /// <param name="indexItem">
        ///     The item whose index in this collection is to be retrieved.
        /// </param>
        /// <returns>
        ///     Zero-based index into the collection where the given item can be
        /// found.  Otherwise, -1
        /// </returns>
        public int IndexOf(object indexItem)
        {
            return InternalList.IndexOf(indexItem);
        }

        /// <summary>
        ///     Insert an item in the collection at a given index.  All items
        /// after the given position are moved down by one.
        /// </summary>
        /// <param name="insertIndex">
        ///     The index at which to inser the item
        /// </param>
        /// <param name="insertItem">
        ///     The item reference to be added to the collection
        /// </param>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Thrown if index is out of range
        /// </exception>
        public void Insert(int insertIndex, object insertItem)
        {
            CollectionContainer cc = insertItem as CollectionContainer;
            if (cc != null)
            {
                AddCollectionContainer(cc);
            }

            // ArrayList implementation checks index and will throw out of range exception
            InternalList.Insert(insertIndex, insertItem);

            OnCollectionChanged(NotifyCollectionChangedAction.Add, insertItem, insertIndex);
        }

        /// <summary>
        ///     Removes the given item reference from the collection.  All
        /// remaining items move up by one.
        /// </summary>
        /// <param name="removeItem">
        ///     The item to be removed.
        /// </param>
        public void Remove(object removeItem)
        {
            int index = InternalList.IndexOf(removeItem);
            if (index >= 0)
            {
                // to ensure model parent is cleared and the CollectionChange notification is raised,
                // call this.RemoveAt, not the aggregated ArrayList
                this.RemoveAt(index);
            }
        }

        /// <summary>
        ///     Removes an item from the collection at the given index.  All
        /// remaining items move up by one.
        /// </summary>
        /// <param name="removeIndex">
        ///     The index at which to remove an item.
        /// </param>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Thrown if index is out of range
        /// </exception>
        public void RemoveAt(int removeIndex)
        {
            if ((0 <= removeIndex) && (removeIndex < Count))
            {
                object removedItem = this[removeIndex];

                CollectionContainer cc = removedItem as CollectionContainer;
                if (cc != null)
                {
                    RemoveCollectionContainer(cc);
                }

                InternalList.RemoveAt(removeIndex);

                OnCollectionChanged(NotifyCollectionChangedAction.Remove, removedItem, removeIndex);
            }
            else
            {
                throw new ArgumentOutOfRangeException("removeIndex",
                            SR.ItemCollectionRemoveArgumentOutOfRange);
            }
        }


        /// <summary>
        /// Create a new view on this collection [Do not call directly].
        /// </summary>
        /// <remarks>
        /// Normally this method is only called by the platform's view manager,
        /// not by user code.
        /// </remarks>
        ICollectionView ICollectionViewFactory.CreateView()
        {
            return new CompositeCollectionView(this);
        }

        #endregion Public Methods


        //------------------------------------------------------
        //
        //  Public Properties
        //
        //------------------------------------------------------

        #region Public Properties

        /// <summary>
        ///     Read-only property for the number of items stored in this collection of objects
        /// </summary>
        /// <remarks>
        ///     CollectionContainers each count as 1 item.
        ///     When in ItemsSource mode, Count always equals 1.
        /// </remarks>
        public int Count
        {
            get
            {
                // Return value from the underlying ArrayList.
                return InternalList.Count;
            }
        }

        /// <summary>
        ///     Indexer property to retrieve or replace the item at the given
        /// zero-based offset into the collection.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Thrown if index is out of range
        /// </exception>
        public object this[int itemIndex]
        {
            get
            {
                // ArrayList implementation checks index and will throw out of range exception
                return InternalList[itemIndex];
            }
            set
            {
                // ArrayList implementation checks index and will throw out of range exception
                object originalItem = InternalList[itemIndex];

                // unhook the old, hook the new
                CollectionContainer cc;
                if ((cc = originalItem as CollectionContainer) != null)
                {
                    RemoveCollectionContainer(cc);
                }
                if ((cc = value as CollectionContainer) != null)
                {
                    AddCollectionContainer(cc);
                }

                // make the change
                InternalList[itemIndex] = value;

                OnCollectionChanged(NotifyCollectionChangedAction.Replace, originalItem, value, itemIndex);
            }
        }

        /// <summary>
        ///     Gets a value indicating whether access to the CompositeCollection is synchronized (thread-safe).
        /// </summary>
        bool ICollection.IsSynchronized
        {
            get
            {
                // Return value from the underlying ArrayList.
                return InternalList.IsSynchronized;
            }
        }

        /// <summary>
        ///     Returns an object to be used in thread synchronization.
        /// </summary>
        object ICollection.SyncRoot
        {
            get
            {
                // Return the SyncRoot object of the underlying ArrayList
                return InternalList.SyncRoot;
            }
        }

        /// <summary>
        ///     Gets a value indicating whether the IList has a fixed size.
        ///     An CompositeCollection can usually grow dynamically,
        ///     this call will commonly return FixedSize = False.
        ///     In ItemsSource mode, this call will return IsFixedSize = True.
        /// </summary>
        bool IList.IsFixedSize
        {
            get
            {
                return InternalList.IsFixedSize;
            }
        }

        /// <summary>
        ///     Gets a value indicating whether the IList is read-only.
        ///     An CompositeCollection is usually writable,
        ///     this call will commonly return IsReadOnly = False.
        ///     In ItemsSource mode, this call will return IsReadOnly = True.
        /// </summary>
        bool IList.IsReadOnly
        {
            get
            {
                return InternalList.IsReadOnly;
            }
        }

        #endregion Public Properties


        //------------------------------------------------------
        //
        //  Public Events
        //
        //------------------------------------------------------

        #region Public Events

        /// <summary>
        /// Occurs when the collection changes, either by adding or removing an item
        /// <see cref="INotifyCollectionChanged" />
        /// </summary>
        event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged
        {
            add
            {
                CollectionChanged += value;
            }
            remove
            {
                CollectionChanged -= value;
            }
        }

        /// <summary>
        /// Occurs when the collection changes, either by adding or removing an item.
        /// </summary>
        protected event NotifyCollectionChangedEventHandler CollectionChanged;

        #endregion Public Events

        #region IWeakEventListener

        /// <summary>
        /// Handle events from the centralized event table
        /// </summary>
        bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
        {
            return ReceiveWeakEvent(managerType, sender, e);
        }

        /// <summary>
        /// Handle events from the centralized event table
        /// </summary>
        protected virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
        {
            return false;   // this method is no longer used (but must remain, for compat)
        }

        #endregion IWeakEventListener

        //------------------------------------------------------
        //
        //  Internal Events
        //
        //------------------------------------------------------

        #region Internal Events

        internal event NotifyCollectionChangedEventHandler ContainedCollectionChanged;

        private void OnContainedCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (ContainedCollectionChanged != null)
                ContainedCollectionChanged(sender, e);
        }

        #endregion Internal Events

        //------------------------------------------------------
        //
        //  Private Methods
        //
        //------------------------------------------------------

        #region Private Methods

        // common ctor initialization
        private void Initialize(ArrayList internalList)
        {
            _internalList = internalList;
        }

        // ArrayList that holds collection containers as well as single items
        private ArrayList InternalList
        {
            get
            {
                return _internalList;
            }
        }

        // Hook up to a newly-added CollectionContainer
        private void AddCollectionContainer(CollectionContainer cc)
        {
            if (InternalList.Contains(cc))
                throw new ArgumentException(SR.CollectionContainerMustBeUniqueForComposite, "cc");

            CollectionChangedEventManager.AddHandler(cc, OnContainedCollectionChanged);

#if DEBUG
            _hasRepeatedCollectionIsValid = false;
#endif
        }

        // Unhook a newly-deleted CollectionContainer
        private void RemoveCollectionContainer(CollectionContainer cc)
        {
            CollectionChangedEventManager.RemoveHandler(cc, OnContainedCollectionChanged);

#if DEBUG
            _hasRepeatedCollectionIsValid = false;
#endif
        }

        // raise CollectionChanged event to any listeners
        void OnCollectionChanged(NotifyCollectionChangedAction action)
        {
#if DEBUG
            _hasRepeatedCollectionIsValid = false;
#endif

            if (CollectionChanged != null)
            {
                CollectionChanged(this, new NotifyCollectionChangedEventArgs(action));
            }
        }

        // raise CollectionChanged event to any listeners
        void OnCollectionChanged(NotifyCollectionChangedAction action, object item, int index)
        {
            if (CollectionChanged != null)
            {
                CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, item, index));
            }
        }

        /// raise CollectionChanged event to any listeners
        void OnCollectionChanged(NotifyCollectionChangedAction action, object oldItem, object newItem, int index)
        {
            if (CollectionChanged != null)
            {
                CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index));
            }
        }

        #endregion Private Methods


        //------------------------------------------------------
        //
        //  Private Fields
        //
        //------------------------------------------------------

        #region Private Fields

        private ArrayList               _internalList;

        #endregion Private Fields


        //------------------------------------------------------
        //
        //  Debugging Aids
        //
        //------------------------------------------------------

        #region Debugging Aids

#if DEBUG
        internal bool HasRepeatedCollection()
        {
            if (!_hasRepeatedCollectionIsValid)
            {
                _hasRepeatedCollection = FindRepeatedCollection(new ArrayList());
                _hasRepeatedCollectionIsValid = true;
            }
            return _hasRepeatedCollection;
        }

        // recursive depth-first search for repeated collection
        private bool FindRepeatedCollection(ArrayList collections)
        {
            for (int i = 0; i < Count; ++i)
            {
                CollectionContainer cc = this[i] as CollectionContainer;
                if (cc != null && cc.Collection != null)
                {
                    CompositeCollection composite = cc.Collection as CompositeCollection;
                    if (composite != null)
                    {
                        if (composite.FindRepeatedCollection(collections))
                            return true;
                    }
                    else if (collections.IndexOf(cc.Collection) > -1)
                        return true;
                    else
                        collections.Add(cc.Collection);
                }
            }
            return false;
        }

        private bool _hasRepeatedCollection = false;
        private bool _hasRepeatedCollectionIsValid = false;
#endif

        internal void GetCollectionChangedSources(int level, Action<int, object, bool?, List<string>> format, List<string> sources)
        {
            format(level, this, false, sources);
            foreach (object o in InternalList)
            {
                CollectionContainer cc = o as CollectionContainer;
                if (cc != null)
                {
                    cc.GetCollectionChangedSources(level+1, format, sources);
                }
            }
        }

        #endregion Debugging Aids
    }
}

