#!/usr/bin/python
#  Copyright (C) 2012 by Carnegie Mellon University.
#  
#  @OPENSOURCE_HEADER_START@
#  Use of Rayon and related source
#  code is subject to the terms of the following licenses:
#  
#  GNU Public License (GPL) Rights pursuant to Version 2, June 1991
#  Government Purpose License Rights (GPLR) pursuant to DFARS 252.227.7013
#  
#  NO WARRANTY
#  
#  ANY INFORMATION, MATERIALS, SERVICES, INTELLECTUAL PROPERTY OR OTHER 
#  PROPERTY OR RIGHTS GRANTED OR PROVIDED BY CARNEGIE MELLON UNIVERSITY 
#  PURSUANT TO THIS LICENSE (HEREINAFTER THE "DELIVERABLES") ARE ON AN 
#  "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY 
#  KIND, EITHER EXPRESS OR IMPLIED AS TO ANY MATTER INCLUDING, BUT NOT 
#  LIMITED TO, WARRANTY OF FITNESS FOR A PARTICULAR PURPOSE, 
#  MERCHANTABILITY, INFORMATIONAL CONTENT, NONINFRINGEMENT, OR ERROR-FREE 
#  OPERATION. CARNEGIE MELLON UNIVERSITY SHALL NOT BE LIABLE FOR INDIRECT, 
#  SPECIAL OR CONSEQUENTIAL DAMAGES, SUCH AS LOSS OF PROFITS OR INABILITY 
#  TO USE SAID INTELLECTUAL PROPERTY, UNDER THIS LICENSE, REGARDLESS OF 
#  WHETHER SUCH PARTY WAS AWARE OF THE POSSIBILITY OF SUCH DAMAGES. 
#  LICENSEE AGREES THAT IT WILL NOT MAKE ANY WARRANTY ON BEHALF OF 
#  CARNEGIE MELLON UNIVERSITY, EXPRESS OR IMPLIED, TO ANY PERSON 
#  CONCERNING THE APPLICATION OF OR THE RESULTS TO BE OBTAINED WITH THE 
#  DELIVERABLES UNDER THIS LICENSE.
#  
#  Licensee hereby agrees to defend, indemnify, and hold harmless Carnegie 
#  Mellon University, its trustees, officers, employees, and agents from 
#  all claims or demands made against them (and any related losses, 
#  expenses, or attorney's fees) arising out of, or relating to Licensee's 
#  and/or its sub licensees' negligent use or willful misuse of or 
#  negligent conduct or willful misconduct regarding the Software, 
#  facilities, or other rights or assistance granted by Carnegie Mellon 
#  University under this License, including, but not limited to, any 
#  claims of product liability, personal injury, death, damage to 
#  property, or violation of any laws or regulations.
#  
#  Carnegie Mellon University Software Engineering Institute authored 
#  documents are sponsored by the U.S. Department of Defense under 
#  Contract FA8721-05-C-0003. Carnegie Mellon University retains 
#  copyrights in all material produced under this contract. The U.S. 
#  Government retains a non-exclusive, royalty-free license to publish or 
#  reproduce these documents, or allow others to do so, for U.S. 
#  Government purposes only pursuant to the copyright license under the 
#  contract clause at 252.227.7013.
#  @OPENSOURCE_HEADER_END@

from __future__ import division



from rayon.rytools import *
from rayon import (
    backgrounds,
    _hilbert,
    miscutils,
    raster,
    toolbox,
)
from rayon.tickspec_parse import parse_tickspec

import math
import socket
import struct
import sys
import unittest
import itertools

def inet_atoi(addr):
    """
    Returns an integer representation of the given dotted-quad string
    IPv4 address.
    """
    return int(struct.unpack('!L', socket.inet_aton(addr))[0])


def inet_itoa(num):
    """
    Returns a dotted-quad string representation of the given integer
    IPv4 address
    """
    return socket.inet_ntoa(struct.pack('!L', num))


def cstr(spec): return miscutils.parse_color_string(spec)

class IntRange(object):
    def __init__(start, stop):
        self.start = start
        self.stop = stop
    def __contains__(self, i): return self.start <= i <= self.stop


def range_from_network(network, cidr):
    if not (0 <= cidr <= 32):
        raise ValueError("%d is not a valid CIDR mask")

    inv_prefix = ((2**32 - 1) >> cidr) & 0xFFFFFFFF
    prefix = (~inv_prefix & 0xFFFFFFFF)
    min_ip = network & prefix
    max_ip = min_ip | inv_prefix

    return min_ip, max_ip


def range_from_cidr(ipv4_cidr):
    network_str, cidr_str = ipv4_cidr.split("/")
    if "." in network_str:
        network  = inet_atoi(network_str)
    else:
        network = int(network_str)
    cidr = int(cidr_str)

    return range_from_network(network, cidr)

    
# $ rwpmapcat --map-file pmaptest.pmap --delimited --no-titles
# 0.0.0.0/1|$00000000|
# 128.0.0.0/2|$00000000|
# 192.0.0.0/9|$00000000|
# 192.128.0.0/11|$00000000|
# 192.160.0.0/13|$00000000|
# 192.168.0.0/24|$00000000|
# 192.168.1.0/24|$ff0000ff|
# 192.168.2.0/24|$00f000ff|
# 192.168.3.0/24|$0000ffff|
# 192.168.4.0/22|$00000000|
# 192.168.8.0/21|$00000000|
# 192.168.16.0/20|$00000000|
# 192.168.32.0/19|$00000000|
# 192.168.64.0/18|$00000000|
# 192.168.128.0/17|$00000000|
# 192.169.0.0/16|$00000000|
# 192.170.0.0/15|$00000000|
# 192.172.0.0/14|$00000000|
# 192.176.0.0/12|$00000000|
# 192.192.0.0/10|$00000000|
# 193.0.0.0/8|$00000000|
# 194.0.0.0/7|$00000000|
# 196.0.0.0/6|$00000000|
# 200.0.0.0/5|$00000000|
# 208.0.0.0/4|$00000000|
# 224.0.0.0/3|$00000000|



class StreamMungler(object):
    "A file-like interface over functional stream manipulation."
    def __init__(self, stream, mungler):
        self._stream = stream
        self._mungler = mungler
    def readline(self):
        return self._mungler(self._stream.readline())
        if line.endswith('|\n'):
            return line[:-2] + "\n"
        else:
            return line
    def next(self):
        line = self.readline()
        if line == "":
            raise StopIteration
        return line


        
class PipeStripper(object):
    "A stream that strips the final pipe from the rwpmapcat input."
    def __init__(self, stream):
        self._stream = stream
    def readline(self):
        line = self._stream.readline()
        line = line.lstrip()
        if line.endswith('|\n'):
            return line[:-2] + "\n"
        else:
            return line
    def next(self):
        line = self.readline()
        if line == "":
            raise StopIteration
        return line


        
def OLDiter_networks(cidr_netmask):
    step = 2**(32 - cidr_netmask)
    for i in xrange(2**cidr_netmask):
        yield i, i*step, ((i+1) * step) - 1

def iter_networks(cidr_netmask, start=0, end=2**32):
    step = 2**(32 - cidr_netmask)
    range_start, range_end = range_from_network(start, cidr_netmask)
    # Always emit at least one block, even if start and end reside
    # entirely in the range of the first network
    if end - start < range_end - range_start:
        yield 0, range_start, range_end
    else:
        # Return the network in which start resides, and all whole
        # networks up to end (so if the last network doesn't end with
        # end, don't emit it)
        start = range_start
        for i in xrange(2**cidr_netmask):
            idx, network_start, network_end = (
                i,
                start + i*step,
                start + ((i+1) * step) - 1
            )
            if network_end > end:
                break
            yield idx, network_start, network_end
        

class RangeMap(object):
    # Maps an IP address to the IP range it resides in and the color
    # of that range
    def __init__(self, dataset):
        # TODO: assuming the input is already a complete set of
        # non-overlapping ranges, which seems sensible if the input is
        # from rwpmapcat, but that may not be such a wise assumption.
        initial_data = tuple(
            range_from_cidr(row[0]) + (row[1],) for row in dataset)

        coalesced = []
        s1, e1, l1 = initial_data[0]
        idx = 1
        while idx < len(initial_data):
            s2, e2, l2 = initial_data[idx]
            if s2 - e1 > 1:
                # non-overlapping/contiguous ranges. error
                raise RuntimeError("Input must be contiguous and cover all IPs")
            if l2 == l1:
                # coalesce
                e1 = e2
            else:
                # end of last range
                coalesced.append((s1, e1, l1))
                s1, e1, l1 = s2, e2, l2
            idx += 1
        coalesced.append((s1, e1, l1))
        self._data = tuple(coalesced)
        
    def __call__(self, ip):
        "Given an IP address, returns range in which it resides, and color."
        for start, end, label in self._data:
            if ip <= end:
                return start, end, label

    def __iter__(self):
        return iter(self._data)
            
            
            



def runtests():
    suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
    unittest.TextTestRunner(verbosity=2).run(suite)

def main(opts):
    if opts['run_tests']:
        runtests()
        return
    
    tools = toolbox.Toolbox.for_file()

    range_map = RangeMap(tools.new_dataset_from_stream(
        PipeStripper(istream_from_str(opts['input_path']))))

    # Note: in original ryhilbert, this is cidr_netmask // 2. I don't
    # think that's correct for odd values of cidr_netmask; in
    # particular, in would mean that there aren't enough pixels (4 * 4
    # = 16) for a cidr_netmask of 5 (2^5 = 32) or 3 (2 * 2 = 4; 2^3 =
    # 8). The version used here ensures enough pixels, but I can't
    # find verification that it's really the right thing to do.
    num_bits = int(math.ceil(opts['cidr_netmask'] / 2))
    bits_per_side = 2**num_bits
    overlay = raster.Bitmap(bits_per_side, bits_per_side,
                            fmt=raster.RGBA32)

    def iter_ranges():
        last_ip_seen = -1
        last_rgba = None
        idx = 0
        for start, end, color in range_map:
            if color == 'UNKNOWN':
                # Optimization: if the default color is #00000000,
                # it's te same as the initialization for the array and
                # we can skip it.
                if opts['default_color'] == (0, 0, 0, 0):
                    idx += (end - start)
                    continue
                rgba = opts['default_color']
            else:
                rgba = miscutils.parse_color(color)

            nw_iter = iter_networks(opts['cidr_netmask'], start=start, end=end)

            for _, nw_start, nw_end in nw_iter:
                if (nw_start <= last_ip_seen
                    and last_rgba != opts['default_color']):
                    # Overlapping with previous run. We shouldn't ever
                    # overlap by more than one network. (We iterate
                    # over the ranges in increasing order, so the
                    # first network will either be the same as the
                    # previous network -- which was the last in the
                    # last range -- or the next one. Subsequent
                    # networks will be > the last network in the last
                    # range, so they can't overlap.)

                    # Therefore: backtrack the index by one so we
                    # overstrike the last network
                    idx -= 1

                    # Also, mark this pixel as disputed territory
                    yield idx, opts['multiregion_color']
                else:
                    yield idx, rgba
                
                idx += 1
            last_ip_seen = nw_end
            last_rgba = rgba

    for idx, rgba in iter_ranges():
        y, x = _hilbert.i2c(num_bits, 2, idx)
        offset = (y * bits_per_side) + x
        overlay.set_pixel(offset, rgba)
            
        
    page = tools.new_page_from_filename(
        opts['output_path'], opts['width'], opts['height'])

    c = tools.new_chart('square')
    c.clear_padding()
    c.set_chart_background(
        backgrounds.BitmapBackground(overlay, fill="scale"))
    
    page.write(c)
            
    return 0




# Tests


class t_IPRanges(unittest.TestCase):
    def run_range(self, fn_input, expected_start, expected_end):
        actual_start, actual_end = range_from_cidr(fn_input)
        self.assertEqual(inet_itoa(actual_start), expected_start)
        self.assertEqual(inet_itoa(actual_end), expected_end)
    def run_range_exc(self, fn_input, exc_type):
        def func():
            rv = range_from_cidr(fn_input)
            self.fail("range_from_cidr('%s') returned %s, expected '%s'" % (
                    fn_input, str(rv), exc_type.__name__))
        self.assertRaises(exc_type, func)
                      
    def test_range_1(self):
        self.run_range("192.168.1.0/24", "192.168.1.0", "192.168.1.255")
    def test_range_2(self):
        self.run_range("192.168.1.0/27", "192.168.1.0", "192.168.1.31")
    def test_range_3(self):
        self.run_range("0.0.0.0/0", "0.0.0.0", "255.255.255.255")
    def test_range_4(self):
        self.run_range("255.255.255.255/32",
                       "255.255.255.255",
                       "255.255.255.255")
    def test_range_5(self):
        self.run_range("12.34.56.78/32", "12.34.56.78", "12.34.56.78")
    def test_range_6(self):
        self.run_range("12.34.56.78/8", "12.0.0.0", "12.255.255.255")
    def test_range_7(self):
        self.run_range_exc("1.2.3.4/-1", ValueError)
    def test_range_7(self):
        self.run_range_exc("1.2.3.4/33", ValueError)
        

from rayon import data
            
class t_RangeMap(unittest.TestCase):
    def setUp(self):
        ranges_in = data.Dataset.from_rows(
            (('0.0.0.0/1', 'rgba(0, 0, 0, 0)'),
             ('128.0.0.0/2', 'rgba(0, 0, 0, 0)'),
             ('192.0.0.0/9', 'rgba(0, 0, 0, 0)'),
             ('192.128.0.0/11', 'rgba(0, 0, 0, 0)'),
             ('192.160.0.0/13', 'rgba(0, 0, 0, 0)'),
             ('192.168.0.0/24', 'rgba(0, 0, 0, 0)'),
             ('192.168.1.0/24', 'rgba(255, 0, 0, 255)'),
             ('192.168.2.0/24', 'rgba(0, 255, 0, 255)'),
             ('192.168.3.0/24', 'rgba(0, 0, 255, 255)'),
             ('192.168.4.0/22', 'rgba(0, 0, 0, 0)'),
             ('192.168.8.0/21', 'rgba(0, 0, 0, 0)'),
             ('192.168.16.0/20', 'rgba(0, 0, 0, 0)'),
             ('192.168.32.0/19', 'rgba(0, 0, 0, 0)'),
             ('192.168.64.0/18', 'rgba(0, 0, 0, 0)'),
             ('192.168.128.0/17', 'rgba(0, 0, 0, 0)'),
             ('192.169.0.0/16', 'rgba(0, 0, 0, 0)'),
             ('192.170.0.0/15', 'rgba(0, 0, 0, 0)'),
             ('192.172.0.0/14', 'rgba(0, 0, 0, 0)'),
             ('192.176.0.0/12', 'rgba(0, 0, 0, 0)'),
             ('192.192.0.0/10', 'rgba(0, 0, 0, 0)'),
             ('193.0.0.0/8', 'rgba(0, 0, 0, 0)'),
             ('194.0.0.0/7', 'rgba(0, 0, 0, 0)'),
             ('196.0.0.0/6', 'rgba(0, 0, 0, 0)'),
             ('200.0.0.0/5', 'rgba(0, 0, 0, 0)'),
             ('208.0.0.0/4', 'rgba(0, 0, 0, 0)'),
             ('224.0.0.0/3', 'rgba(0, 0, 0, 0)')))
        int_ranges_in = data.Dataset.from_rows(
            (('0/1','rgba(0, 0, 0, 0)'),
             ('2147483648/2','rgba(0, 0, 0, 0)'),
             ('3221225472/9','rgba(0, 0, 0, 0)'),
             ('3229614080/11','rgba(0, 0, 0, 0)'),
             ('3231711232/13','rgba(0, 0, 0, 0)'),
             ('3232235520/24','rgba(0, 0, 0, 0)'),
             ('3232235776/24','rgba(255, 0, 0, 255)'),
             ('3232236032/24','rgba(0, 255, 0, 255)'),
             ('3232236288/24','rgba(0, 0, 255, 255)'),
             ('3232236544/22','rgba(0, 0, 0, 0)'),
             ('3232237568/21','rgba(0, 0, 0, 0)'),
             ('3232239616/20','rgba(0, 0, 0, 0)'),
             ('3232243712/19','rgba(0, 0, 0, 0)'),
             ('3232251904/18','rgba(0, 0, 0, 0)'),
             ('3232268288/17','rgba(0, 0, 0, 0)'),
             ('3232301056/16','rgba(0, 0, 0, 0)'),
             ('3232366592/15','rgba(0, 0, 0, 0)'),
             ('3232497664/14','rgba(0, 0, 0, 0)'),
             ('3232759808/12','rgba(0, 0, 0, 0)'),
             ('3233808384/10','rgba(0, 0, 0, 0)'),
             ('3238002688/8','rgba(0, 0, 0, 0)'),
             ('3254779904/7','rgba(0, 0, 0, 0)'),
             ('3288334336/6','rgba(0, 0, 0, 0)'),
             ('3355443200/5','rgba(0, 0, 0, 0)'),
             ('3489660928/4','rgba(0, 0, 0, 0)'),
             ('3758096384/3','rgba(0, 0, 0, 0)')))
        self.rm = RangeMap(ranges_in)
        self.int_rm = RangeMap(int_ranges_in)

        
    
    def test_range_1(self):
        self.assertEqual(self.rm(inet_atoi("192.168.1.100")),
                         (inet_atoi("192.168.1.0"),
                          inet_atoi("192.168.1.255"),
                          "rgba(255, 0, 0, 255)"))
    def test_range_2(self):
        self.assertEqual(self.rm(inet_atoi("192.168.1.0")),
                         (inet_atoi("192.168.1.0"),
                          inet_atoi("192.168.1.255"),
                          "rgba(255, 0, 0, 255)"))
    def test_range_3(self):
        self.assertEqual(self.rm(inet_atoi("192.168.1.255")),
                         (inet_atoi("192.168.1.0"),
                          inet_atoi("192.168.1.255"),
                          "rgba(255, 0, 0, 255)"))
    def test_range_4(self):
        self.assertEqual(self.rm(inet_atoi("192.167.1.1")),
                         (inet_atoi("0.0.0.0"),
                          inet_atoi("192.168.0.255"),
                          "rgba(0, 0, 0, 0)"))
    def test_range_5(self):
        self.assertEqual(self.rm(inet_atoi("0.0.0.0")),
                         (inet_atoi("0.0.0.0"),
                          inet_atoi("192.168.0.255"),
                          "rgba(0, 0, 0, 0)"))
    def test_range_6(self):
        self.assertEqual(self.rm(inet_atoi("192.168.0.255")),
                         (inet_atoi("0.0.0.0"),
                          inet_atoi("192.168.0.255"),
                          "rgba(0, 0, 0, 0)"))
    def test_range_7(self):
        self.assertEqual(self.rm(inet_atoi("192.168.4.0")),
                         (inet_atoi("192.168.4.0"),
                          inet_atoi("255.255.255.255"),
                          "rgba(0, 0, 0, 0)"))
    def test_range_8(self):
        self.assertEqual(self.rm(inet_atoi("192.168.5.0")),
                         (inet_atoi("192.168.4.0"),
                          inet_atoi("255.255.255.255"),
                          "rgba(0, 0, 0, 0)"))
    def test_range_9(self):
        self.assertEqual(self.rm(inet_atoi("255.255.255.255")),
                         (inet_atoi("192.168.4.0"),
                          inet_atoi("255.255.255.255"),
                          "rgba(0, 0, 0, 0)"))
    def test_int_range_1(self):
        self.assertEqual(self.int_rm(inet_atoi("192.168.1.100")),
                         (inet_atoi("192.168.1.0"),
                          inet_atoi("192.168.1.255"),
                          "rgba(255, 0, 0, 255)"))
    def test_int_range_2(self):
        self.assertEqual(self.int_rm(inet_atoi("192.168.1.0")),
                         (inet_atoi("192.168.1.0"),
                          inet_atoi("192.168.1.255"),
                          "rgba(255, 0, 0, 255)"))
    def test_int_range_3(self):
        self.assertEqual(self.int_rm(inet_atoi("192.168.1.255")),
                         (inet_atoi("192.168.1.0"),
                          inet_atoi("192.168.1.255"),
                          "rgba(255, 0, 0, 255)"))
    def test_int_range_4(self):
        self.assertEqual(self.int_rm(inet_atoi("192.167.1.1")),
                         (inet_atoi("0.0.0.0"),
                          inet_atoi("192.168.0.255"),
                          "rgba(0, 0, 0, 0)"))
    def test_int_range_5(self):
        self.assertEqual(self.int_rm(inet_atoi("0.0.0.0")),
                         (inet_atoi("0.0.0.0"),
                          inet_atoi("192.168.0.255"),
                          "rgba(0, 0, 0, 0)"))
    def test_int_range_6(self):
        self.assertEqual(self.int_rm(inet_atoi("192.168.0.255")),
                         (inet_atoi("0.0.0.0"),
                          inet_atoi("192.168.0.255"),
                          "rgba(0, 0, 0, 0)"))
    def test_int_range_7(self):
        self.assertEqual(self.int_rm(inet_atoi("192.168.4.0")),
                         (inet_atoi("192.168.4.0"),
                          inet_atoi("255.255.255.255"),
                          "rgba(0, 0, 0, 0)"))
    def test_int_range_8(self):
        self.assertEqual(self.int_rm(inet_atoi("192.168.5.0")),
                         (inet_atoi("192.168.4.0"),
                          inet_atoi("255.255.255.255"),
                          "rgba(0, 0, 0, 0)"))
    def test_int_range_9(self):
        self.assertEqual(self.int_rm(inet_atoi("255.255.255.255")),
                         (inet_atoi("192.168.4.0"),
                          inet_atoi("255.255.255.255"),
                          "rgba(0, 0, 0, 0)"))

    def test_iter_1(self):
        def mktuple(start, end, color):
            return inet_atoi(start), inet_atoi(end), color
        def showtuple(t):
            return inet_itoa(t[0]), inet_itoa(t[1]), t[2]
        expected = [
            mktuple("0.0.0.0", "192.168.0.255", "rgba(0, 0, 0, 0)"),
            mktuple("192.168.1.0", "192.168.1.255", "rgba(255, 0, 0, 255)"),
            mktuple("192.168.2.0", "192.168.2.255", "rgba(0, 255, 0, 255)"),
            mktuple("192.168.3.0", "192.168.3.255", "rgba(0, 0, 255, 255)"),
            mktuple("192.168.4.0", "255.255.255.255", "rgba(0, 0, 0, 0)"),
        ]
        actual = [x for x in self.rm]
        self.assertEqual(len(expected), len(actual))
        for e, a in zip(expected, actual):
            self.assertEqual(e, a, "%s != %s" % (showtuple(e), showtuple(a)))

from pprint import pprint
def print_list_heads(l):
    top_10 = l[:10]
    btm_10 = l[-10:]
    pprint(list(top_10) + ["..."] + list(btm_10))

def pp_range_list(l):
    return [(i, inet_itoa(s), inet_itoa(e)) for i, s, e in l]
    
class t_NetworkIter(unittest.TestCase):
    def test_1(self):
        def iter_networks_8():
            for i in xrange(2**8):
                yield i, i * (2**24), ((i+1) * 2**24) - 1

        expected = list(iter_networks_8())
        actual = list(iter_networks(8))
        self.assertEqual(len(expected), len(actual))
        
        for ((i1, s1, e1), (i2, s2, e2)) in zip(actual, expected):
            self.assertEqual(i1, i2)
            self.assertEqual(inet_itoa(s1), inet_itoa(s2))
            self.assertEqual(inet_itoa(e1), inet_itoa(e2))

    def test_bounded_1(self):
        def mktuple(idx, start, end):
            return (idx, inet_atoi(start), inet_atoi(end))
        
        expected = [
            mktuple(0, "0.0.0.0", "0.255.255.255"),
            mktuple(1, "1.0.0.0", "1.255.255.255"),
            mktuple(2, "2.0.0.0", "2.255.255.255"),
            mktuple(3, "3.0.0.0", "3.255.255.255"),
            mktuple(4, "4.0.0.0", "4.255.255.255"),
        ]

        actual = list(iter_networks(8, end=inet_atoi("4.255.255.255")))
        self.assertEqual(len(expected), len(actual))
        
        for ((i1, s1, e1), (i2, s2, e2)) in zip(actual, expected):
            self.assertEqual(i1, i2)
            self.assertEqual(inet_itoa(s1), inet_itoa(s2))
            self.assertEqual(inet_itoa(e1), inet_itoa(e2))

    def test_bounded_2(self):
        def mktuple(idx, start, end):
            return (idx, inet_atoi(start), inet_atoi(end))
        
        expected = [
            mktuple(0, "4.0.0.0", "4.255.255.255"),
            mktuple(1, "5.0.0.0", "5.255.255.255"),
            mktuple(2, "6.0.0.0", "6.255.255.255"),
            mktuple(3, "7.0.0.0", "7.255.255.255"),
            mktuple(4, "8.0.0.0", "8.255.255.255"),
        ]

        actual = list(iter_networks(8,
                                    start=inet_atoi("4.0.0.0"),
                                    end=inet_atoi("8.255.255.255")))

        self.assertEqual(len(expected), len(actual))
        
        for ((i1, s1, e1), (i2, s2, e2)) in zip(actual, expected):
            self.assertEqual(i1, i2)
            self.assertEqual(inet_itoa(s1), inet_itoa(s2))
            self.assertEqual(inet_itoa(e1), inet_itoa(e2))

    def test_bounded_3(self):
        def mktuple(idx, start, end):
            return (idx, inet_atoi(start), inet_atoi(end))
        
        expected = [
            mktuple(0, "4.0.0.0", "4.255.255.255"),
            mktuple(1, "5.0.0.0", "5.255.255.255"),
            mktuple(2, "6.0.0.0", "6.255.255.255"),
            mktuple(3, "7.0.0.0", "7.255.255.255"),
        ]

        actual = list(iter_networks(8,
                                    start=inet_atoi("4.0.0.0"),
                                    end=inet_atoi("8.255.255.252")))

        self.assertEqual(len(expected), len(actual))
        
        for ((i1, s1, e1), (i2, s2, e2)) in zip(actual, expected):
            self.assertEqual(i1, i2)
            self.assertEqual(inet_itoa(s1), inet_itoa(s2))
            self.assertEqual(inet_itoa(e1), inet_itoa(e2))

    def test_bounded_4(self):
        def mktuple(idx, start, end):
            return (idx, inet_atoi(start), inet_atoi(end))
        
        expected = [
            mktuple(0, "4.0.0.0", "4.255.255.255"),
            mktuple(1, "5.0.0.0", "5.255.255.255"),
            mktuple(2, "6.0.0.0", "6.255.255.255"),
            mktuple(3, "7.0.0.0", "7.255.255.255"),
        ]

        actual = list(iter_networks(8,
                                    start=inet_atoi("4.0.0.10"),
                                    end=inet_atoi("8.255.255.252")))

        self.assertEqual(len(expected), len(actual))
        
        for ((i1, s1, e1), (i2, s2, e2)) in zip(actual, expected):
            self.assertEqual(i1, i2)
            self.assertEqual(inet_itoa(s1), inet_itoa(s2))
            self.assertEqual(inet_itoa(e1), inet_itoa(e2))

    def test_bounded_5(self):
        def mktuple(idx, start, end):
            return (idx, inet_atoi(start), inet_atoi(end))
        
        expected = [
            mktuple(0, "4.0.0.0", "4.255.255.255"),
        ]

        actual = list(iter_networks(8,
                                    start=inet_atoi("4.0.0.10"),
                                    end=inet_atoi("4.0.0.20")))

        self.assertEqual(len(expected), len(actual))
        
        for ((i1, s1, e1), (i2, s2, e2)) in zip(actual, expected):
            self.assertEqual(i1, i2)
            self.assertEqual(inet_itoa(s1), inet_itoa(s2))
            self.assertEqual(inet_itoa(e1), inet_itoa(e2))
        


            
            

# Option parsing and execution


desc = "generate overlay images for use with ryhilbert"

options = [
    Flag('run-tests'),
    IntOption('cidr-netmask', default_val=20, hlp="Size of subnets in vis"),
    ColorOption('multiregion-color',
                default_val=miscutils.parse_color('#0000ffcc'),
                hlp="Color to use when ranges overlap"),
    ColorOption('default-color',
                default_val=miscutils.parse_color('#00000000'),
                hlp="Color representing default (i.e., no color) in pmap"),
    FalseFlag('skip-default',
              hlp="Don't draw default color (unimplemented)")
]

if __name__ == '__main__':
    execute(cmd_func=main, cmd_options=options, cmd_desc=desc)


