using System;
using System.Collections.Generic;
using System.Linq;

namespace Inspectron.Settings
{
    public class Path<T> : IList<T>, IEquatable<Path<T>>
    {
        /// <summary>
        /// Constructor using single object</summary>
        /// <param name="last">Single object making up the path</param>
        public Path(T last)
        {
            m_path = new T[1];
            m_path[0] = last;
        }

        /// <summary>
        /// Constructor using sequence of objects</summary>
        /// <param name="path">Path, as sequence of objects</param>
        public Path(IEnumerable<T> path)
        {
            m_path = path.ToArray();
        }

        /// <summary>
        /// Constructor using collection of objects</summary>
        /// <param name="path">Path, as collection of objects</param>
        public Path(ICollection<T> path)
        {
            m_path = new T[path.Count];
            path.CopyTo(m_path, 0);
        }

        /// <summary>
        /// Gets or sets the first object in the path</summary>
        public T First
        {
            get { return m_path[0]; }
            set { m_path[0] = value; }
        }

        /// <summary>
        /// Gets or sets the last object in the path</summary>
        public T Last
        {
            get { return m_path[m_path.Length - 1]; }
            set { m_path[m_path.Length - 1] = value; }
        }

        /// <summary>
        /// Obtains a prefix with the specified length</summary>
        /// <param name="length">Prefix length</param>
        /// <returns>Prefix with the specified length</returns>
        public Path<T> Prefix(int length)
        {
            CheckSubPathLength(length);

            T[] path = new T[length];
            Array.Copy(m_path, 0, path, 0, length);

            return new Path<T>(path);
        }

        /// <summary>
        /// Obtains a suffix with the specified length</summary>
        /// <param name="length">Suffix length</param>
        /// <returns>Suffix with the specified length</returns>
        public Path<T> Suffix(int length)
        {
            CheckSubPathLength(length);

            T[] path = new T[length];
            int offset = m_path.Length - length;
            Array.Copy(m_path, offset, path, 0, length);

            return new Path<T>(path);
        }

        /// <summary>
        /// Converts path to a path of another type</summary>
        /// <typeparam name="U">Path type to convert to</typeparam>
        /// <returns>Path of new type</returns>
        public Path<U> Convert<U>()
            where U : class
        {
            U[] converted = new U[m_path.Length];
            for (int i = 0; i < m_path.Length; i++)
                converted[i] = Convert<U>(m_path[i]);
            return new Path<U>(converted);
        }

        /// <summary>
        /// Converts from the path type to another type</summary>
        /// <typeparam name="U">Desired type</typeparam>
        /// <param name="item">Item to convert</param>
        /// <returns>Item, converted to given type, or null</returns>
        protected virtual U Convert<U>(T item)
            where U : class
        {
            U u = item as U;
            return u;
        }

        /// <summary>
        /// Tests path for equality</summary>
        /// <param name="other">Other path</param>
        /// <returns>True iff this path is equivalent to other</returns>
        public bool Equals(Path<T> other)
        {
            if (object.Equals(other, null))
                return false;

            if (m_path.Length != other.m_path.Length)
                return false;

            for (int i = 0; i < m_path.Length; i++)
                if (!m_path[i].Equals(other.m_path[i]))
                    return false;

            return true;
        }

        /// <summary>
        /// Tests object for equality</summary>
        /// <param name="obj">Other object</param>
        /// <returns>True iff this path is equivalent to other object</returns>
        public override bool Equals(object obj)
        {
            Path<T> path = obj as Path<T>;
            if (path != null)
                return Equals(path);

            return false;
        }

        /// <summary>
        /// Obtains hash code</summary>
        /// <returns>Hash code</returns>
        public override int GetHashCode()
        {
            int hash = 0;
            foreach (T obj in m_path)
                hash ^= obj.GetHashCode();

            return hash;
        }

        /// <summary>
        /// Tests paths for equality</summary>
        /// <param name="o1">First path</param>
        /// <param name="o2">Second path</param>
        /// <returns>True iff paths are equivalent</returns>
        public static bool operator ==(Path<T> o1, Path<T> o2)
        {
            if (object.Equals(o1, null))
                return object.Equals(o2, null);
            else
                return o1.Equals(o2);
        }

        /// <summary>
        /// Tests paths for inequality</summary>
        /// <param name="o1">First path</param>
        /// <param name="o2">Second path</param>
        /// <returns>True iff paths are not equivalent</returns>
        public static bool operator !=(Path<T> o1, Path<T> o2)
        {
            if (object.Equals(o1, null))
                return !object.Equals(o2, null);
            else
                return !o1.Equals(o2);
        }

        /// <summary>
        /// Concatenates object with path</summary>
        /// <param name="lhs">Prefix object</param>
        /// <param name="rhs">Optional path, may be null</param>
        /// <returns>Concatenated path, with lhs as first object</returns>
        public static Path<T> operator +(T lhs, Path<T> rhs)
        {
            if (rhs == null)
                return new Path<T>(lhs);
            T[] path = new T[1 + rhs.Count];
            path[0] = lhs;
            Array.Copy(rhs.m_path, 0, path, 1, rhs.Count);
            return new Path<T>(path);
        }

        /// <summary>
        /// Concatenates path with object</summary>
        /// <param name="lhs">Optional path, may be null</param>
        /// <param name="rhs">Suffix object</param>
        /// <returns>Concatenated path, with rhs as last object</returns>
        public static Path<T> operator +(Path<T> lhs, T rhs)
        {
            if (lhs == null)
                return new Path<T>(rhs);
            T[] path = new T[lhs.Count + 1];
            Array.Copy(lhs.m_path, 0, path, 0, lhs.Count);
            path[path.Length - 1] = rhs;
            return new Path<T>(path);
        }

        /// <summary>
        /// Concatenates two paths</summary>
        /// <param name="lhs">First path. Can be null.</param>
        /// <param name="rhs">Second path. Can be null.</param>
        /// <returns>Concatenated path, with rhs as prefix and lhs as suffix. Is null if both lhs and rhs are null.</returns>
        public static Path<T> operator +(Path<T> lhs, Path<T> rhs)
        {
            if (lhs == null)
                return rhs;
            if (rhs == null)
                return lhs;
            T[] path = new T[lhs.Count + rhs.Count];
            Array.Copy(lhs.m_path, 0, path, 0, lhs.Count);
            Array.Copy(rhs.m_path, 0, path, lhs.Count, rhs.Count);
            return new Path<T>(path);
        }

        /// <summary>
        /// Gets the enumeration of each path's last item; i.e., the property 'Last'</summary>
        /// <param name="paths">Enumeration of Path objects, whose Last property is returned</param>
        /// <returns>Last property of each Path in 'paths', in the same order as 'paths'</returns>
        public static IEnumerable<T> GetLastItems(IEnumerable<Path<T>> paths)
        {
            foreach (Path<T> path in paths)
                yield return path.Last;
        }

        #region IList<T> Members

        /// <summary>
        /// Determines the index of a specific item in the <see cref="T:System.Collections.Generic.IList`1"></see></summary>
        /// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.IList`1"></see></param>
        /// <returns>
        /// The index of item if found in the list; otherwise -1
        /// </returns>
        public int IndexOf(T item)
        {
            for (int i = 0; i < m_path.Length; i++)
                if (m_path[i].Equals(item))
                    return i;

            return -1;
        }

        /// <summary>
        /// Inserts an item to the <see cref="T:System.Collections.Generic.IList`1"></see> at the specified index</summary>
        /// <param name="index">The zero-based index at which item should be inserted</param>
        /// <param name="item">The object to insert into the <see cref="T:System.Collections.Generic.IList`1"></see></param>
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.IList`1"></see> is read-only</exception>
        /// <exception cref="T:System.ArgumentOutOfRangeException">index is not a valid index in the <see cref="T:System.Collections.Generic.IList`1"></see></exception>
        public void Insert(int index, T item)
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Removes the <see cref="T:System.Collections.Generic.IList`1"></see> item at the specified index</summary>
        /// <param name="index">The zero-based index of the item to remove</param>
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.IList`1"></see> is read-only</exception>
        /// <exception cref="T:System.ArgumentOutOfRangeException">index is not a valid index in the <see cref="T:System.Collections.Generic.IList`1"></see></exception>
        public void RemoveAt(int index)
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Gets or sets the item at the specified index</summary>
        /// <value>Index at which to set value</value>
        public T this[int index]
        {
            get { return m_path[index]; }
            set { m_path[index] = value; }
        }

        #endregion

        #region ICollection<T> Members

        /// <summary>
        /// Adds an item to the <see cref="T:System.Collections.Generic.ICollection`1"></see></summary>
        /// <param name="item">The object to add to the <see cref="T:System.Collections.Generic.ICollection`1"></see></param>
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.ICollection`1"></see> 
        /// is read-only</exception>
        public void Add(T item)
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Removes all items from the <see cref="T:System.Collections.Generic.ICollection`1"></see></summary>
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.ICollection`1"></see> 
        /// is read-only</exception>
        public void Clear()
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Determines whether the <see cref="T:System.Collections.Generic.ICollection`1"></see> contains a specific value</summary>
        /// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.ICollection`1"></see></param>
        /// <returns>
        /// True iff item is found in the <see cref="T:System.Collections.Generic.ICollection`1"></see>
        /// </returns>
        public bool Contains(T item)
        {
            foreach (T obj in m_path)
                if (obj.Equals(item))
                    return true;

            return false;
        }

        /// <summary>
        /// Copies the elements of the <see cref="T:System.Collections.Generic.ICollection`1"></see> to an <see cref="T:System.Array"></see>, 
        /// starting at a particular <see cref="T:System.Array"></see> index</summary>
        /// <param name="array">The one-dimensional <see cref="T:System.Array"></see> that is the destination of the elements 
        /// copied from <see cref="T:System.Collections.Generic.ICollection`1"></see>. 
        /// The <see cref="T:System.Array"></see> must have zero-based indexing.</param>
        /// <param name="arrayIndex">The zero-based index in array at which copying begins</param>
        /// <exception cref="T:System.ArgumentOutOfRangeException">ArrayIndex is less than 0</exception>
        /// <exception cref="T:System.ArgumentNullException">Array is null</exception>
        /// <exception cref="T:System.ArgumentException">Array is multidimensional.-or-
        /// arrayIndex is equal to or greater than the length of array.-or-
        /// The number of elements in the source <see cref="T:System.Collections.Generic.ICollection`1"></see> is greater than 
        /// the available space from arrayIndex to the end of the destination array.-or-
        /// Type T cannot be cast automatically to the type of the destination array.</exception>
        public void CopyTo(T[] array, int arrayIndex)
        {
            m_path.CopyTo(array, arrayIndex);
        }

        /// <summary>
        /// Gets the number of elements contained in the <see cref="T:System.Collections.Generic.ICollection`1"></see></summary>
        public int Count
        {
            get { return m_path.Length; }
        }

        /// <summary>
        /// Gets whether the <see cref="T:System.Collections.Generic.ICollection`1"></see> is read-only</summary>
        public bool IsReadOnly
        {
            get { return false; }
        }

        /// <summary>
        /// Removes the first occurrence of a specific object from the <see cref="T:System.Collections.Generic.ICollection`1"></see></summary>
        /// <param name="item">The object to remove from the <see cref="T:System.Collections.Generic.ICollection`1"></see></param>
        /// <returns>
        /// True iff item was successfully removed from the <see cref="T:System.Collections.Generic.ICollection`1"></see>. 
        /// This method also returns false if item is not found in the original <see cref="T:System.Collections.Generic.ICollection`1"></see>.
        /// </returns>
        /// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.ICollection`1"></see> is read-only</exception>
        public bool Remove(T item)
        {
            throw new NotSupportedException();
        }

        #endregion

        #region IEnumerable<T> Members

        /// <summary>
        /// Returns an enumerator that iterates through the collection</summary>
        /// <returns>
        /// A <see cref="T:System.Collections.Generic.IEnumerator`1"></see> that can be used to iterate through the collection
        /// </returns>
        public IEnumerator<T> GetEnumerator()
        {
            return ((IEnumerable<T>)m_path).GetEnumerator();
        }

        #endregion

        #region IEnumerable Members

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return m_path.GetEnumerator();
        }

        #endregion

        /// <summary>
        /// Private constructor</summary>
        private Path(T[] path)
        {
            m_path = path;
        }

        private void CheckSubPathLength(int length)
        {
            if (length < 1)
                throw new InvalidOperationException("Length must be > 0");
            if (length > m_path.Length)
                throw new InvalidOperationException("Length greater than path length");
        }

        private readonly T[] m_path;
    }
}