# -----------------------------------------------------------------------------
#   Copyright (c) 2008 by David P. D. Moss. All rights reserved.
#
#   Released under the BSD license. See the LICENSE file for details.
# -----------------------------------------------------------------------------
"""
Routines for dealing with nmap-style IPv4 address ranges.

Based on nmap's Target Specification :-

    http://nmap.org/book/man-target-specification.html
"""

from netaddr.core import AddrFormatError
from netaddr.ip import IPAddress, IPNetwork


def _nmap_octet_target_values(spec):
    #   Generates sequence of values for an individual octet as defined in the
    #   nmap Target Specification.
    values = set()

    for element in spec.split(','):
        if '-' in element:
            left, right = element.split('-', 1)
            if not left:
                left = 0
            if not right:
                right = 255
            low = int(left)
            high = int(right)
            if not ((0 <= low <= 255) and (0 <= high <= 255)):
                raise ValueError('octet value overflow for spec %s!' % (spec,))
            if low > high:
                raise ValueError('left side of hyphen must be <= right %r' % (element,))
            for octet in range(low, high + 1):
                values.add(octet)
        else:
            octet = int(element)
            if not (0 <= octet <= 255):
                raise ValueError('octet value overflow for spec %s!' % (spec,))
            values.add(octet)

    return sorted(values)


def _generate_nmap_octet_ranges(nmap_target_spec):
    #   Generate 4 lists containing all octets defined by a given nmap Target
    #   specification.
    if not isinstance(nmap_target_spec, str):
        raise TypeError('string expected, not %s' % type(nmap_target_spec))

    if not nmap_target_spec:
        raise ValueError('nmap target specification cannot be blank!')

    tokens = nmap_target_spec.split('.')

    if len(tokens) != 4:
        raise AddrFormatError('invalid nmap range: %s' % (nmap_target_spec,))

    return (
        _nmap_octet_target_values(tokens[0]),
        _nmap_octet_target_values(tokens[1]),
        _nmap_octet_target_values(tokens[2]),
        _nmap_octet_target_values(tokens[3]),
    )


def _parse_nmap_target_spec(target_spec):
    if '/' in target_spec:
        _, prefix = target_spec.split('/', 1)
        if not (0 < int(prefix) < 33):
            raise AddrFormatError('CIDR prefix expected, not %s' % (prefix,))
        net = IPNetwork(target_spec)
        if net.version != 4:
            raise AddrFormatError('CIDR only support for IPv4!')
        for ip in net:
            yield ip
    elif ':' in target_spec:
        #   nmap only currently supports IPv6 addresses without prefixes.
        yield IPAddress(target_spec)
    else:
        octet_ranges = _generate_nmap_octet_ranges(target_spec)
        for w in octet_ranges[0]:
            for x in octet_ranges[1]:
                for y in octet_ranges[2]:
                    for z in octet_ranges[3]:
                        yield IPAddress('%d.%d.%d.%d' % (w, x, y, z), 4)


def valid_nmap_range(target_spec):
    """
    :param target_spec: an nmap-style IP range target specification.

    :return: ``True`` if IP range target spec is valid, ``False`` otherwise.
    """
    try:
        next(_parse_nmap_target_spec(target_spec))
        return True
    except (TypeError, ValueError, AddrFormatError):
        pass
    return False


def iter_nmap_range(*nmap_target_spec):
    """
    An generator that yields IPAddress objects from defined by nmap target
    specifications.

    See https://nmap.org/book/man-target-specification.html for details.

    :param nmap_target_spec: one or more nmap IP range target specification.

    :return: an iterator producing IPAddress objects for each IP in the target spec(s).
    """
    for target_spec in nmap_target_spec:
        for addr in _parse_nmap_target_spec(target_spec):
            yield addr
