using System;
using System.Collections.ObjectModel;
using System.Net;

namespace MediaBrowser.Common.Net
{
    /// <summary>
    /// Defines the <see cref="NetworkExtensions" />.
    /// </summary>
    public static class NetworkExtensions
    {
        /// <summary>
        /// Add an address to the collection.
        /// </summary>
        /// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
        /// <param name="ip">Item to add.</param>
        public static void AddItem(this Collection<IPObject> source, IPAddress ip)
        {
            if (!source.ContainsAddress(ip))
            {
                source.Add(new IPNetAddress(ip, 32));
            }
        }

        /// <summary>
        /// Adds a network to the collection.
        /// </summary>
        /// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
        /// <param name="item">Item to add.</param>
        /// <param name="itemsAreNetworks">If <c>true</c> the values are treated as subnets.
        /// If <b>false</b> items are addresses.</param>
        public static void AddItem(this Collection<IPObject> source, IPObject item, bool itemsAreNetworks = true)
        {
            if (!source.ContainsAddress(item) || !itemsAreNetworks)
            {
                source.Add(item);
            }
        }

        /// <summary>
        /// Converts this object to a string.
        /// </summary>
        /// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
        /// <returns>Returns a string representation of this object.</returns>
        public static string AsString(this Collection<IPObject> source)
        {
            return $"[{string.Join(',', source)}]";
        }

        /// <summary>
        /// Returns true if the collection contains an item with the ip address,
        /// or the ip address falls within any of the collection's network ranges.
        /// </summary>
        /// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
        /// <param name="item">The item to look for.</param>
        /// <returns>True if the collection contains the item.</returns>
        public static bool ContainsAddress(this Collection<IPObject> source, IPAddress item)
        {
            if (source.Count == 0)
            {
                return false;
            }

            if (item == null)
            {
                throw new ArgumentNullException(nameof(item));
            }

            if (item.IsIPv4MappedToIPv6)
            {
                item = item.MapToIPv4();
            }

            foreach (var i in source)
            {
                if (i.Contains(item))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Returns true if the collection contains an item with the ip address,
        /// or the ip address falls within any of the collection's network ranges.
        /// </summary>
        /// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
        /// <param name="item">The item to look for.</param>
        /// <returns>True if the collection contains the item.</returns>
        public static bool ContainsAddress(this Collection<IPObject> source, IPObject item)
        {
            if (source.Count == 0)
            {
                return false;
            }

            if (item == null)
            {
                throw new ArgumentNullException(nameof(item));
            }

            foreach (var i in source)
            {
                if (i.Contains(item))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Compares two Collection{IPObject} objects. The order is ignored.
        /// </summary>
        /// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
        /// <param name="dest">Item to compare to.</param>
        /// <returns>True if both are equal.</returns>
        public static bool Compare(this Collection<IPObject> source, Collection<IPObject> dest)
        {
            if (dest == null || source.Count != dest.Count)
            {
                return false;
            }

            foreach (var sourceItem in source)
            {
                bool found = false;
                foreach (var destItem in dest)
                {
                    if (sourceItem.Equals(destItem))
                    {
                        found = true;
                        break;
                    }
                }

                if (!found)
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Returns a collection containing the subnets of this collection given.
        /// </summary>
        /// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
        /// <returns>Collection{IPObject} object containing the subnets.</returns>
        public static Collection<IPObject> AsNetworks(this Collection<IPObject> source)
        {
            if (source == null)
            {
                throw new ArgumentNullException(nameof(source));
            }

            Collection<IPObject> res = new Collection<IPObject>();

            foreach (IPObject i in source)
            {
                if (i is IPNetAddress nw)
                {
                    // Add the subnet calculated from the interface address/mask.
                    var na = nw.NetworkAddress;
                    na.Tag = i.Tag;
                    res.AddItem(na);
                }
                else if (i is IPHost ipHost)
                {
                    // Flatten out IPHost and add all its ip addresses.
                    foreach (var addr in ipHost.GetAddresses())
                    {
                        IPNetAddress host = new IPNetAddress(addr)
                        {
                            Tag = i.Tag
                        };

                        res.AddItem(host);
                    }
                }
            }

            return res;
        }

        /// <summary>
        /// Excludes all the items from this list that are found in excludeList.
        /// </summary>
        /// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
        /// <param name="excludeList">Items to exclude.</param>
        /// <param name="isNetwork">Collection is a network collection.</param>
        /// <returns>A new collection, with the items excluded.</returns>
        public static Collection<IPObject> Exclude(this Collection<IPObject> source, Collection<IPObject> excludeList, bool isNetwork)
        {
            if (source.Count == 0 || excludeList == null)
            {
                return new Collection<IPObject>(source);
            }

            Collection<IPObject> results = new Collection<IPObject>();

            bool found;
            foreach (var outer in source)
            {
                found = false;

                foreach (var inner in excludeList)
                {
                    if (outer.Equals(inner))
                    {
                        found = true;
                        break;
                    }
                }

                if (!found)
                {
                    results.AddItem(outer, isNetwork);
                }
            }

            return results;
        }

        /// <summary>
        /// Returns all items that co-exist in this object and target.
        /// </summary>
        /// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
        /// <param name="target">Collection to compare with.</param>
        /// <returns>A collection containing all the matches.</returns>
        public static Collection<IPObject> ThatAreContainedInNetworks(this Collection<IPObject> source, Collection<IPObject> target)
        {
            if (source.Count == 0)
            {
                return new Collection<IPObject>();
            }

            if (target == null)
            {
                throw new ArgumentNullException(nameof(target));
            }

            Collection<IPObject> nc = new Collection<IPObject>();

            foreach (IPObject i in source)
            {
                if (target.ContainsAddress(i))
                {
                    nc.AddItem(i);
                }
            }

            return nc;
        }
    }
}