using System;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;

namespace Znyc.Admin.Commons.Net
{
    /// <summary>
    /// IP v4 and v6 range helper by jsakamoto
    /// Fork from https://github.com/jsakamoto/ipaddressrange
    /// </summary>
    /// <example>
    /// "192.168.0.0/24"
    /// "fe80::/10"
    /// "192.168.0.0/255.255.255.0"
    /// "192.168.0.0-192.168.0.255"
    /// </example>
    public class IpAddressRange
    {
        public IPAddress Begin { get; set; }

        public IPAddress End { get; set; }

        public IpAddressRange()
        {
            Begin = new IPAddress(0L);
            End = new IPAddress(0L);
        }

        public IpAddressRange(string ipRangeString)
        {
            // remove all spaces.
            ipRangeString = ipRangeString.Replace(" ", "");

            // Pattern 1. CIDR range: "192.168.0.0/24", "fe80::/10"
            Match m1 = Regex.Match(ipRangeString, @"^(?<adr>[\da-f\.:]+)/(?<maskLen>\d+)$", RegexOptions.IgnoreCase);
            if (m1.Success)
            {
                byte[] baseAdrBytes = IPAddress.Parse(m1.Groups["adr"].Value).GetAddressBytes();
                byte[] maskBytes = Bits.GetBitMask(baseAdrBytes.Length, int.Parse(m1.Groups["maskLen"].Value));
                baseAdrBytes = Bits.And(baseAdrBytes, maskBytes);
                Begin = new IPAddress(baseAdrBytes);
                End = new IPAddress(Bits.Or(baseAdrBytes, Bits.Not(maskBytes)));
                return;
            }

            // Pattern 2. Uni address: "127.0.0.1", ":;1"
            Match m2 = Regex.Match(ipRangeString, @"^(?<adr>[\da-f\.:]+)$", RegexOptions.IgnoreCase);
            if (m2.Success)
            {
                Begin = End = IPAddress.Parse(ipRangeString);
                return;
            }

            // Pattern 3. Begin end range: "169.258.0.0-169.258.0.255"
            Match m3 = Regex.Match(ipRangeString, @"^(?<begin>[\da-f\.:]+)-(?<end>[\da-f\.:]+)$",
                RegexOptions.IgnoreCase);
            if (m3.Success)
            {
                Begin = IPAddress.Parse(m3.Groups["begin"].Value);
                End = IPAddress.Parse(m3.Groups["end"].Value);
                return;
            }

            // Pattern 4. Bit mask range: "192.168.0.0/255.255.255.0"
            Match m4 = Regex.Match(ipRangeString, @"^(?<adr>[\da-f\.:]+)/(?<bitmask>[\da-f\.:]+)$",
                RegexOptions.IgnoreCase);
            if (m4.Success)
            {
                byte[] baseAdrBytes = IPAddress.Parse(m4.Groups["adr"].Value).GetAddressBytes();
                byte[] maskBytes = IPAddress.Parse(m4.Groups["bitmask"].Value).GetAddressBytes();
                baseAdrBytes = Bits.And(baseAdrBytes, maskBytes);
                Begin = new IPAddress(baseAdrBytes);
                End = new IPAddress(Bits.Or(baseAdrBytes, Bits.Not(maskBytes)));
                return;
            }

            throw new FormatException("Unknown IP range string.");
        }

        public bool Contains(IPAddress ipaddress)
        {
            if (ipaddress.AddressFamily != Begin.AddressFamily)
            {
                return false;
            }

            byte[] adrBytes = ipaddress.GetAddressBytes();
            return Bits.GE(Begin.GetAddressBytes(), adrBytes) && Bits.LE(End.GetAddressBytes(), adrBytes);
        }
    }

    internal static class Bits
    {
        internal static byte[] Not(byte[] bytes)
        {
            return bytes.Select(b => (byte)~b).ToArray();
        }

        internal static byte[] And(byte[] A, byte[] B)
        {
            return A.Zip(B, (a, b) => (byte)(a & b)).ToArray();
        }

        internal static byte[] Or(byte[] A, byte[] B)
        {
            return A.Zip(B, (a, b) => (byte)(a | b)).ToArray();
        }

        internal static bool GE(byte[] A, byte[] B)
        {
            return A.Zip(B, (a, b) => a == b ? 0 : a < b ? 1 : -1)
                .SkipWhile(c => c == 0)
                .FirstOrDefault() >= 0;
        }

        internal static bool LE(byte[] A, byte[] B)
        {
            return A.Zip(B, (a, b) => a == b ? 0 : a < b ? 1 : -1)
                .SkipWhile(c => c == 0)
                .FirstOrDefault() <= 0;
        }

        internal static byte[] GetBitMask(int sizeOfBuff, int bitLen)
        {
            byte[] maskBytes = new byte[sizeOfBuff];
            int bytesLen = bitLen / 8;
            int bitsLen = bitLen % 8;
            for (int i = 0; i < bytesLen; i++)
            {
                maskBytes[i] = 0xff;
            }

            if (bitsLen > 0)
            {
                maskBytes[bytesLen] = (byte)~Enumerable.Range(1, 8 - bitsLen).Select(n => 1 << (n - 1))
                    .Aggregate((a, b) => a | b);
            }

            return maskBytes;
        }
    }
}