#!/usr/bin/env python3
import sys, time, random, struct, io, binascii

# Scan regions of memory for changes, and display those changes in real-time.
# It's like My First Temporal Hex Dump. Great for kids!

__all__ = ['watch_scanner', 'watch_tabulator']


def break_up_addresses(device, addrs, block_wordcount):
    """Break up addresses into packet-sized pieces.

    We do this to give all scanning regions about the same probability per
    word. We can also use this opportunity to randomly offset the packet
    boundaries, to reduce correlation with effects of SCSI packet boundaries
    when scanning large blocks.
    
    The returned list contains (address, fn) tuples. When the function is
    called, it returns a string of data starting at 'address'. Length is
    implied by the function results.

    Order of results and offset of blocks are both randomized.
    """

    parts = []
    block_bytecount = block_wordcount * 4

    for addr in addrs:

        if len(addr) == 1:
            # Single word.
            # Looks just like a tiny block, but we read it with peek()

            def fn(device = device,
                   ptr = addr[0]
                   ):
                return struct.pack('<I', device.peek(ptr))

            parts.append(( addr[0], fn ))

        elif len(addr) == 2:
            # Range. First, pad it out to word alignment.

            first, last = addr
            first &= ~3
            last = (last + 3) & ~3

            if last <= first:
                raise ValueError('Empty range ' + repr(addr))

            # The first read address will be randomized to include up to one
            # full block of padding before the range we care about. This will
            # randomize the alignment of packet boundaries with respect to the
            # regions we're scanning.

            block_offset = random.randint(0, block_wordcount) * 4
            ptr = first - block_offset

            while ptr < last:

                def fn(device = device,
                       count = block_wordcount,
                       ptr = ptr,
                       start = block_offset,
                       end = min(block_bytecount, last + 1 - ptr)
                       ):
                    return device.read_block(ptr, count)[start:end]

                parts.append(( ptr + block_offset, fn ))
                block_offset = 0
                ptr += block_bytecount

        else:
            raise ValueError('Unrecognized address format ' + repr(addr))

    random.shuffle(parts)
    return parts


def watch_scanner(d, addrs, verbose = True, block_wordcount = 4, memo_filename = None):
    """Repeatedly scan memory, in randomized order, looking for changes.

    When a change is found, we yield:
       (timestamp, address, new_value, old_value).

    Timestamps are as reported by time.time(). It's the first time we noticed
    this change, timestamped as soon as possible.

    Addresses are a list of things to watch:

       (addr,)
         Single numbers are the address of a word to watch. This guarantees
         that the address is only read as that one single word. We won't read
         anything nearby, and each read will be a single peek.

       (a, b)
         Two elements indicate a range of addresses from a to b, including
         both endpoints. These are broken into randomized sections of exactly