/*
 * Copyright (C) 2012-2017 Tobias Brunner
 * HSR Hochschule fuer Technik Rapperswil
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 */

package org.strongswan.android.utils;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import androidx.annotation.NonNull;

/**
 * Class that represents a range of IP addresses. This range could be a proper subnet, but that's
 * not necessarily the case (see {@code getPrefix} and {@code toSubnets}).
 */
public class IPRange implements Comparable<IPRange>
{
	private final byte[] mBitmask = { (byte)0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 };
	private byte[] mFrom;
	private byte[] mTo;
	private Integer mPrefix;

	/**
	 * Determine if the range is a proper subnet and, if so, what the network prefix is.
	 */
	private void determinePrefix()
	{
		boolean matching = true;

		mPrefix = mFrom.length * 8;
		for (int i = 0; i < mFrom.length; i++)
		{
			for (int bit = 0; bit < 8; bit++)
			{
				if (matching)
				{
					if ((mFrom[i] & mBitmask[bit]) != (mTo[i] & mBitmask[bit]))
					{
						mPrefix = (i * 8) + bit;
						matching = false;
					}
				}
				else
				{
					if ((mFrom[i] & mBitmask[bit]) != 0 || (mTo[i] & mBitmask[bit]) == 0)
					{
						mPrefix = null;
						return;
					}
				}
			}
		}
	}

	private IPRange(byte[] from, byte[] to)
	{
		mFrom = from;
		mTo = to;
		determinePrefix();
	}

	public IPRange(String from, String to) throws UnknownHostException
	{
		this(Utils.parseInetAddress(from), Utils.parseInetAddress(to));
	}

	public IPRange(InetAddress from, InetAddress to)
	{
		initializeFromRange(from, to);
	}

	private void initializeFromRange(InetAddress from, InetAddress to)
	{
		byte[] fa = from.getAddress(), ta = to.getAddress();
		if (fa.length != ta.length)
		{
			throw new IllegalArgumentException("Invalid range");
		}
		if (compareAddr(fa, ta) < 0)
		{
			mFrom = fa;
			mTo = ta;
		}
		else
		{
			mTo = fa;
			mFrom = ta;
		}
		determinePrefix();
	}

	public IPRange(String base, int prefix) throws UnknownHostException
	{
		this(Utils.parseInetAddress(base), prefix);
	}

	public IPRange(InetAddress base, int prefix)
	{
		this(base.getAddress(), prefix);
	}

	private IPRange(byte[] from, int prefix)
	{
		initializeFromCIDR(from, prefix);
	}

	private void initializeFromCIDR(byte[] from, int prefix)
	{
		if (from.length != 4 && from.length != 16)
		{
			throw new IllegalArgumentException("Invalid address");
		}
		if (prefix < 0 || prefix > from.length * 8)
		{
			throw new IllegalArgumentException("Invalid prefix");
		}
		byte[] to = from.clone();
		byte mask = (byte)(0xff << (8 - prefix % 8));
		int i = prefix / 8;

		if (i < from.length)
		{
			from[i] = (byte)(from[i] & mask);
			to[i] = (byte)(to[i] | ~mask);
			Arrays.fill(from, i+1, from.length, (byte)0);
			Arrays.fill(to, i+1, to.length, (byte)0xff);
		}
		mFrom = from;
		mTo = to;
		mPrefix = prefix;
	}

	public IPRange(String cidr) throws UnknownHostException
	{
		/* only verify the basic structure */
		if (!cidr.matches("(?i)^(([0-9.]+)|([0-9a-f:]+))(-(([0-9.]+)|([0-9a-f:]+))|(/\\d+))?$"))
		{
			throw new IllegalArgumentException("Invalid CIDR or range notation");
		}
		if (cidr.contains("-"))
		{
			String[] parts = cidr.split("-");
			InetAddress from = InetAddress.getByName(parts[0]);
			InetAddress to = InetAddress.getByName(parts[1]);
			initializeFromRange(from, to);
		}
		else
		{
			String[] parts = cidr.split("/");
			InetAddress addr = InetAddress.getByName(parts[0]);
			byte[] base = addr.getAddress();
			int prefix = base.length * 8;
			if (parts.length > 1)
			{
				prefix = Integer.parseInt(parts[1]);
			}
			initializeFromCIDR(base, prefix);
		}
	}

	/**
	 * Returns the first address of the range. The network ID in case this is a proper subnet.
	 */
	public InetAddress getFrom()
	{
		try
		{
			return InetAddress.getByAddress(mFrom);
		}
		catch (UnknownHostException ignored)
		{
			return null;
		}
	}

	/**
	 * Returns the last address of the range.
	 */
	public InetAddress getTo()
	{
		try
		{
			return InetAddress.getByAddress(mTo);
		}
		catch (UnknownHostException ignored)
		{
			return null;
		}
	}

	/**
	 * If this range is a proper subnet returns its prefix, otherwise returns null.
	 */
	public Integer getPrefix()
	{
		return mPrefix;
	}

	@Override
	public int compareTo(@NonNull IPRange other)
	{
		int cmp = compareAddr(mFrom, other.mFrom);
		if (cmp == 0)
		{	/* smaller ranges first */
			cmp = compareAddr(mTo, other.mTo);
		}
		return cmp;
	}

	@Override
	public boolean equals(Object o)
	{
		if (o == null || !(o instanceof IPRange))
		{
			return false;
		}
		return this == o || compareTo((IPRange)o) == 0;
	}

	@Override
	public String toString()
	{
		try
		{
			if (mPrefix != null)
			{
				return InetAddress.getByAddress(mFrom).getHostAddress() + "/" + mPrefix;
			}
			return InetAddress.getByAddress(mFrom).getHostAddress() + "-" +
				   InetAddress.getByAddress(mTo).getHostAddress();
		}
		catch (UnknownHostException ignored)
		{
			return super.toString();
		}
	}

	private int compareAddr(byte a[], byte b[])
	{
		if (a.length != b.length)
		{
			return (a.length < b.length) ? -1 : 1;
		}
		for (int i = 0; i < a.length; i++)
		{
			if (a[i] != b[i])
			{
				if (((int)a[i] & 0xff) < ((int)b[i] & 0xff))
				{
					return -1;
				}
				else
				{
					return 1;
				}
			}
		}
		return 0;
	}

	/**
	 * Check if this range fully contains the given range.
	 */
	public boolean contains(IPRange range)
	{
		return compareAddr(mFrom, range.mFrom) <= 0 && compareAddr(range.mTo, mTo) <= 0;
	}

	/**
	 * Check if this and the given range overlap.
	 */
	public boolean overlaps(IPRange range)
	{
		return !(compareAddr(mTo, range.mFrom) < 0 || compareAddr(range.mTo, mFrom) < 0);
	}

	private byte[] dec(byte[] addr)
	{
		for (int i = addr.length - 1; i >= 0; i--)
		{
			if (--addr[i] != (byte)0xff)
			{
				break;
			}
		}
		return addr;
	}

	private byte[] inc(byte[] addr)
	{
		for (int i = addr.length - 1; i >= 0; i--)
		{
			if (++addr[i] != 0)
			{
				break;
			}
		}
		return addr;
	}

	/**
	 * Remove the given range from the current range.  Returns a list of resulting ranges (these are
	 * not proper subnets). At most two ranges are returned, in case the given range is contained in
	 * this but does not equal it, which would result in an empty list (which is also the case if
	 * this range is fully contained in the given range).
	 */
	public List<IPRange> remove(IPRange range)
	{
		ArrayList<IPRange> list = new ArrayList<>();
		if (!overlaps(range))
		{	/*           | this | or | this |
		     * | range |                     | range | */
			list.add(this);
		}
		else if (!range.contains(this))
		{	/* we are not completely removed, so none of these cases applies:
			 * | this  | or  | this  |   or   | this  |
		     * | range |     | range   |    |   range | */
			if (compareAddr(mFrom, range.mFrom) < 0 && compareAddr(range.mTo, mTo) < 0)
			{	/* the removed range is completely within our boundaries:
				 * |    this    |
				 *   | range |   */
				list.add(new IPRange(mFrom, dec(range.mFrom.clone())));
				list.add(new IPRange(inc(range.mTo.clone()), mTo));
			}
			else
			{	/* one end is within our boundaries the other at or outside it:
			     * | this     | or    | this     | or | this    |  or  | this    |
			     * | range |       | range |            | range |           | range | */
				byte[] from = compareAddr(mFrom, range.mFrom) < 0 ? mFrom : inc(range.mTo.clone());
				byte[] to = compareAddr(mTo, range.mTo) > 0 ? mTo : dec(range.mFrom.clone());
				list.add(new IPRange(from, to));
			}
		}
		return list;
	}

	private boolean adjacent(IPRange range)
	{
		if (compareAddr(mTo, range.mFrom) < 0)
		{
			byte[] to = inc(mTo.clone());
			return compareAddr(to, range.mFrom) == 0;
		}
		byte[] from = dec(mFrom.clone());
		return compareAddr(from, range.mTo) == 0;
	}

	/**
	 * Merge two adjacent or overlapping ranges, returns null if it's not possible to merge them.
	 */
	public IPRange merge(IPRange range)
	{
		if (overlaps(range))
		{
			if (contains(range))
			{
				return this;
			}
			else if (range.contains(this))
			{
				return range;
			}
		}
		else if (!adjacent(range))
		{
			return null;
		}
		byte[] from = compareAddr(mFrom, range.mFrom) < 0 ? mFrom : range.mFrom;
		byte[] to = compareAddr(mTo, range.mTo) > 0 ? mTo : range.mTo;
		return new IPRange(from, to);
	}

	/**
	 * Split the given range into a sorted list of proper subnets.
	 */
	public List<IPRange> toSubnets()
	{
		ArrayList<IPRange> list = new ArrayList<>();
		if (mPrefix != null)
		{
			list.add(this);
		}
		else
		{
			int i = 0, bit = 0, prefix, netmask, common_byte, common_bit;
			int from_cur, from_prev = 0, to_cur, to_prev = 1;
			boolean from_full = true, to_full = true;

			byte[] from = mFrom.clone();
			byte[] to = mTo.clone();

			/* find a common prefix */
			while (i < from.length && (from[i] & mBitmask[bit]) == (to[i] & mBitmask[bit]))
			{
				if (++bit == 8)
				{
					bit = 0;
					i++;
				}
			}
			prefix = i * 8 + bit;

			/* at this point we know that the addresses are either equal, or that the
			 * current bits in the 'from' and 'to' addresses are 0 and 1, respectively.
			 * we now look at the rest of the bits as two binary trees (0=left, 1=right)
			 * where 'from' and 'to' are both leaf nodes.  all leaf nodes between these
			 * nodes are addresses contained in the range.  to collect them as subnets
			 * we follow the trees from both leaf nodes to their root node and record
			 * all complete subtrees (right for from, left for to) we come across as
			 * subnets.  in that process host bits are zeroed out.  if both addresses
			 * are equal we won't enter the loop below.
			 *      0_____|_____1       for the 'from' address we assume we start on a
			 *   0__|__ 1    0__|__1    left subtree (0) and follow the left edges until
			 *  _|_   _|_   _|_   _|_   we reach the root of this subtree, which is
			 * |   | |   | |   | |   |  either the root of this whole 'from'-subtree
			 * 0   1 0   1 0   1 0   1  (causing us to leave the loop) or the root node
			 * of the right subtree (1) of another node (which actually could be the
			 * leaf node we start from).  that whole subtree gets recorded as subnet.
			 * next we follow the right edges to the root of that subtree which again is
			 * either the 'from'-root or the root node in the left subtree (0) of
			 * another node.  the complete right subtree of that node is the next subnet
			 * we record.  from there we assume that we are in that right subtree and
			 * recursively follow right edges to its root.  for the 'to' address the
			 * procedure is exactly the same but with left and right reversed.
			 */
			if (++bit == 8)
			{
				bit = 0;
				i++;
			}
			common_byte = i;
			common_bit = bit;
			netmask = from.length * 8;
			for (i = from.length - 1; i >= common_byte; i--)
			{
				int bit_min = (i == common_byte) ? common_bit : 0;
				for (bit = 7; bit >= bit_min; bit--)
				{
					byte mask = mBitmask[bit];

					from_cur = from[i] & mask;
					if (from_prev == 0 && from_cur != 0)
					{	/* 0 -> 1: subnet is the whole current (right) subtree */
						list.add(new IPRange(from.clone(), netmask));
						from_full = false;
					}
					else if (from_prev != 0 && from_cur == 0)
					{	/* 1 -> 0: invert bit to switch to right subtree and add it */
						from[i] ^= mask;
						list.add(new IPRange(from.clone(), netmask));
						from_cur = 1;
					}
					/* clear the current bit */
					from[i] &= ~mask;
					from_prev = from_cur;

					to_cur = to[i] & mask;
					if (to_prev != 0 && to_cur == 0)
					{	/* 1 -> 0: subnet is the whole current (left) subtree */
						list.add(new IPRange(to.clone(), netmask));
						to_full = false;
					}
					else if (to_prev == 0 && to_cur != 0)
					{	/* 0 -> 1: invert bit to switch to left subtree and add it */
						to[i] ^= mask;
						list.add(new IPRange(to.clone(), netmask));
						to_cur = 0;
					}
					/* clear the current bit */
					to[i] &= ~mask;
					to_prev = to_cur;
					netmask--;
				}
			}

			if (from_full && to_full)
			{	/* full subnet (from=to or from=0.. and to=1.. after common prefix) - not reachable
				 * due to the shortcut at the top */
				list.add(new IPRange(from.clone(), prefix));
			}
			else if (from_full)
			{	/* full from subnet (from=0.. after prefix) */
				list.add(new IPRange(from.clone(), prefix + 1));
			}
			else if (to_full)
			{	/* full to subnet (to=1.. after prefix) */
				list.add(new IPRange(to.clone(), prefix + 1));
			}
		}
		Collections.sort(list);
		return list;
	}
}
