<?php
namespace IPTools;

use IPTools\Exception\RangeException;
use ReturnTypeWillChange;

/**
 * @author Safarov Alisher <alisher.safarov@outlook.com>
 * @link https://github.com/S1lentium/IPTools
 */
class Range implements \Iterator, \Countable
{
	use PropertyTrait;

	/**
	 * @var IP
	 */
	private $firstIP;
	/**
	 * @var IP
	 */
	private $lastIP;
	/**
	 * @var int
	 */
	private $position = 0;

	/**
	 * @param IP $firstIP
	 * @param IP $lastIP
	 * @throws RangeException
	 */
	public function __construct(IP $firstIP, IP $lastIP)
	{
		$this->setFirstIP($firstIP);
		$this->setLastIP($lastIP);
	}

	/**
	 * @param string $data
	 * @return Range
	 */
	public static function parse($data)
	{
		if (strpos($data,'/') || strpos($data,' ')) {
			$network = Network::parse($data);
			$firstIP = $network->getFirstIP();
			$lastIP  = $network->getLastIP();
		} elseif (strpos($data, '*') !== false) {
			$firstIP = IP::parse(str_replace('*', '0', $data));
			$lastIP  = IP::parse(str_replace('*', '255', $data));
		} elseif (strpos($data, '-')) {
			list($first, $last) = explode('-', $data, 2);
			$firstIP = IP::parse($first);
			$lastIP  = IP::parse($last);
		} else {
			$firstIP = IP::parse($data);
			$lastIP  = clone $firstIP;
		}

		return new self($firstIP, $lastIP);
	}

	/**
	 * @param IP|Network|Range $find
	 * @return bool
	 * @throws RangeException
	 */
	public function contains($find)
	{
		if ($find instanceof IP) {
			$within = (strcmp($find->inAddr(), $this->firstIP->inAddr()) >= 0)
				&& (strcmp($find->inAddr(), $this->lastIP->inAddr()) <= 0);
		} elseif ($find instanceof Range || $find instanceof Network) {
			/**
			 * @var Network|Range $find
			 */
			$within = (strcmp($find->getFirstIP()->inAddr(), $this->firstIP->inAddr()) >= 0)
				&& (strcmp($find->getLastIP()->inAddr(), $this->lastIP->inAddr()) <= 0);
		} else {
			throw new RangeException('Invalid type');
		}

		return $within;
	}

	/**
	 * @param IP $ip
	 * @throws RangeException
	 */
	public function setFirstIP(IP $ip)
	{
		if ($this->lastIP && strcmp($ip->inAddr(), $this->lastIP->inAddr()) > 0) {
			throw new RangeException('First IP is grater than second');
		}

		$this->firstIP = $ip;
	}

	/**
	 * @param IP $ip
	 * @throws RangeException
	 */
	public function setLastIP(IP $ip)
	{
		if ($this->firstIP && strcmp($ip->inAddr(), $this->firstIP->inAddr()) < 0) {
			throw new RangeException('Last IP is less than first');
		}

		$this->lastIP = $ip;
	}

	/**
	 * @return IP
	 */
	public function getFirstIP()
	{
		return $this->firstIP;
	}

	/**
	 * @return IP
	 */
	public function getLastIP()
	{
		return $this->lastIP;
	}

	/**
	 * @return Network[]
	 */
	public function getNetworks()
	{
		$span = $this->getSpanNetwork();

		$networks = array();

		if ($span->getFirstIP()->inAddr() === $this->firstIP->inAddr()
			&& $span->getLastIP()->inAddr() === $this->lastIP->inAddr()
		) {
			$networks = array($span);
		} else {
			if ($span->getFirstIP()->inAddr() !== $this->firstIP->inAddr()) {
				$excluded = $span->exclude($this->firstIP->prev());
				foreach ($excluded as $network) {
					if (strcmp($network->getFirstIP()->inAddr(), $this->firstIP->inAddr()) >= 0) {
						$networks[] = $network;
					}
				}
			}

			if ($span->getLastIP()->inAddr() !== $this->lastIP->inAddr()) {
				if (!$networks) {
					$excluded = $span->exclude($this->lastIP->next());
				} else {
					$excluded = array_pop($networks);
					$excluded = $excluded->exclude($this->lastIP->next());
				}

				foreach ($excluded as $network) {
					$networks[] = $network;
					if ($network->getLastIP()->inAddr() === $this->lastIP->inAddr()) {
						break;
					}
				}
			}

		}

		return $networks;
	}

	/**
	 * @return Network
	 */
	public function getSpanNetwork()
	{
		$xorIP = IP::parseInAddr($this->getFirstIP()->inAddr() ^ $this->getLastIP()->inAddr());

		preg_match('/^(0*)/', $xorIP->toBin(), $match);

		$prefixLength = strlen($match[1]);

		$ip = IP::parseBin(str_pad(substr($this->getFirstIP()->toBin(), 0, $prefixLength), $xorIP->getMaxPrefixLength(), '0'));

		return new Network($ip, Network::prefix2netmask($prefixLength, $ip->getVersion()));
	}

	/**
	 * @return IP
	 */
	#[ReturnTypeWillChange]
	public function current()
	{
		return $this->firstIP->next($this->position);
	}

	/**
	 * @return int
	 */
	#[ReturnTypeWillChange]
	public function key()
	{
		return $this->position;
	}

    /**
     * @return void
     */
	#[ReturnTypeWillChange]
	public function next()
	{
		++$this->position;
	}

    /**
     * @return void
     */
	#[ReturnTypeWillChange]
	public function rewind()
	{
		$this->position = 0;
	}

	/**
	 * @return bool
	 */
	#[ReturnTypeWillChange]
	public function valid()
	{
		return strcmp($this->firstIP->next($this->position)->inAddr(), $this->lastIP->inAddr()) <= 0;
	}

	/**
	 * @return int
	 */
	#[ReturnTypeWillChange]
	public function count()
	{
		return (integer)bcadd(bcsub($this->lastIP->toLong(), $this->firstIP->toLong()), 1);
	}

}
