# SPDX-License-Identifier: MIT
from enum import Enum
import threading, traceback, bisect, copy, heapq, importlib, sys, itertools, time, os, functools, struct, re, signal
from construct import Adapter, Int64ul, Int32ul, Int16ul, Int8ul, ExprAdapter, GreedyRange, ListContainer, StopFieldError, ExplicitError, StreamError

__all__ = ["FourCC"]

def align_up(v, a=16384):
    return (v + a - 1) & ~(a - 1)

align = align_up

def align_down(v, a=16384):
    return v & ~(a - 1)

def align_pot(v):
    out = 1
    while out < v:
        out *= 2
    return out

def hexdump(s, sep=" "):
    return sep.join(["%02x"%x for x in s])

def hexdump32(s, sep=" "):
    vals = struct.unpack("<%dI" % (len(s)//4), s)
    return sep.join(["%08x"%x for x in vals])

def _ascii(s):
    s2 = ""
    for c in s:
        if c < 0x20 or c > 0x7e:
            s2 += "."
        else:
            s2 += chr(c)
    return s2

def chexdump(s, st=0, abbreviate=True, stride=16, indent="", print_fn=print):
    last = None
    skip = False
    for i in range(0,len(s),stride):
        val = s[i:i+stride]
        if val == last and abbreviate:
            if not skip:
                print_fn(indent+"%08x  *" % (i + st))
                skip = True
        else:
            print_fn(indent+"%08x  %s  |%s|" % (
                i + st,
                "  ".join(hexdump(val[i:i+8], ' ').ljust(23)
                          for i in range(0, stride, 8)),
                _ascii(val).ljust(stride)))
            last = val
            skip = False

def chexdiff32(prev, cur, ascii=True, offset=0, offset2=None):
    assert len(cur) % 4 == 0
    count = len(cur) // 4
    words = struct.unpack("<%dI" % count, cur)

    if prev is None:
        last = None
    else:
        assert len(prev) == len(cur)
        last = struct.unpack("<%dI" % count, prev)

    row = 8
    skipping = False
    out = []
    for i in range(0, count, row):
        off_text = f"{offset + i * 4:016x}"
        if offset2 is not None:
            off_text += f"/{offset2 + i * 4:08x}"
        if not last:
            if i != 0 and words[i:i+row] == words[i-row:i]:
                if not skipping:
                    out.append(f"{off_text} *\n")
                skipping = True
            else:
                out.append(f"{off_text} ")
                for new in words[i:i+row]:
                    out.append("%08x " % new)
                if ascii:
                    out.append("| " + _ascii(cur[4*i:4*(i+row)]))
                out.append("\n")
                skipping = False
        elif last[i:i+row] != words[i:i+row]:
            out.append(f"{off_text} ")
            for old, new in zip(last[i:i+row], words[i:i+row]):
                so = "%08x" % old
                sn = s = "%08x" % new
                if old != new:
                    s = "\x1b[32m"
                    ld = False
                    for a,b in zip(so, sn):
                        d = a != b
                        if ld != d:
                            s += "\x1b[31;1;4m" if d else "\x1b[32m"
                            ld = d
                        s += b
                    s += "\x1b[m"
                out.append(s + " ")
            if ascii:
                out.append("| " + _ascii(cur[4*i:4*(i+row)]))
            out.append("\n")
    return "".join(out)

def chexundump(dump, base=0):
    if type(dump) is bytes:
        dump = dump.decode("ascii")
    elif type(dump) is str:
        pass
    else:
        dump = dump.read()

    decoded = bytearray()
    for line in dump.strip().splitlines():
        if not line:
            continue
        try:
            cropped = line.strip().split("|", 2)[0]
            mark, data = cropped.split(" ", 1)
            if data.strip() == "*":
                continue
            offset = int(mark, 16)
            data = data.replace(" ", "")
            if len(data) % 2 != 0:
                raise ValueError("odd sized data")
            if offset > len(decoded):
                decoded.extend([0] * (offset - len(decoded) - base))
            decoded.extend([int(data[i:i+2], 16) for i \
                            in range(0, len(data), 2)])
        except (ValueError, TypeError) as exc:
            raise ValueError(f"can't decode line: {line:r}") from exc

    return decoded

_extascii_table_low = [
    "▪", "☺", "☻", "♥", "♦", "♣", "♠", "•",
    "◘", "○", "◙", "♂", "♀", "♪", "♫", "☼",
    "►", "◄", "↕", "‼", "¶", "§", "▬", "↨",
    "↑", "↓", "→", "←", "∟", "↔", "▲", "▼"]

_extascii_table_high = [
    "⌂",
    "█", "⡀", "⢀", "⣀", "⠠", "⡠", "⢠", "⣠",
    "⠄", "⡄", "⢄", "⣄", "⠤", "⡤", "⢤", "⣤",
    "⠁", "⡁", "⢁", "⣁", "⠡", "⡡", "⢡", "⣡",
    "⠅", "⡅", "⢅", "⣅", "⠥", "⡥", "⢥", "⣥",
    "⠃", "⡃", "⢃", "⣃", "⠣", "⡣", "⢣", "⣣",
    "⠇", "⡇", "⢇", "⣇", "⠧", "⡧", "⢧", "⣧",
    "⠉", "⡉", "⢉", "⣉", "⠩", "⡩", "⢩", "⣩",
    "⠍", "⡍", "⢍", "⣍", "⠭", "⡭", "⢭", "⣭",
    "⠊", "⡊", "⢊", "⣊", "⠪", "⡪", "⢪", "⣪",
    "⠎", "⡎", "⢎", "⣎", "⠮", "⡮", "⢮", "⣮",
    "⠑", "⡑", "⢑", "⣑", "⠱", "⡱", "⢱", "⣱",
    "⠕", "⡕", "⢕", "⣕", "⠵", "⡵", "⢵", "⣵",
    "⠚", "⡚", "⢚", "⣚", "⠺", "⡺", "⢺", "⣺",
    "⠞", "⡞", "⢞", "⣞", "⠾", "⡾", "⢾", "⣾",
    "⠛", "⡛", "⢛", "⣛", "⠻", "⡻", "⢻", "⣻",
    "⠟", "⡟", "⢟", "⣟", "⠿", "⡿", "⢿", "⣿"]

def _extascii(s):
    s2 = ""
    for c in s:
        if c < 0x20:
            s2 += _extascii_table_low[c]
        elif c > 0x7e:
            s2 += _extascii_table_high[c-0x7f]
        else:
            s2 += chr(c)
    return s2

def ehexdump(s, st=0, abbreviate=True, indent="", print_fn=print):
    last = None
    skip = False
    for i in range(0,len(s),16):
        val = s[i:i+16]
        if val == last and abbreviate:
            if not skip:
                print_fn(indent+"%08x  *" % (i + st))
                skip = True
        else:
            print_fn(indent+"%08x  %s  %s  |%s|" % (
                  i + st,
                  hexdump(val[:8], ' ').ljust(23),
                  hexdump(val[8:], ' ').ljust(23),
                  _extascii(val).ljust(16)))
            last = val
            skip = False

def chexdump32(s, st=0, abbreviate=True, print_fn=print):
    last = None
    skip = False
    for i in range(0,len(s),32):
        val = s[i:i+32]
        if val == last and abbreviate:
            if not skip:
                print_fn("%08x  *" % (i + st))
                skip = True
        else:
            print_fn("%08x  %s" % (
                i + st,
                hexdump32(val, ' ')))
            last = val
            skip = False

def unhex(s):
    s = re.sub(r"/\*.*?\*/", "", s)
    return bytes.fromhex(s.replace(" ", "").replace("\n", ""))

def dumpstacks(signal, frame):
    id2name = dict([(th.ident, th.name) for th in threading.enumerate()])
    code = []
    for threadId, stack in sys._current_frames().items():
        code.append("\n# Thread: %s(%d)" % (id2name.get(threadId,""), threadId))
        for filename, lineno, name, line in traceback.extract_stack(stack):
            code.append('File: "%s", line %d, in %s' % (filename, lineno, name))
            if line:
                code.append("  %s" % (line.strip()))
    print("\n".join(code))
    sys.exit(1)

def set_sigquit_stackdump_handler():
    signal.signal(signal.SIGQUIT, dumpstacks)

def parse_indexlist(s):
    items = set()
    for i in s.split(","):
        if "-" in i:
            a, b = map(int, i.split("-", 1))
            for i in range(a, b + 1):
                items.add(i)
        else:
            items.add(int(i))
    return items

FourCC = ExprAdapter(Int32ul,
                     lambda d, ctx: d.to_bytes(4, "big").decode("latin-1"),
                     lambda d, ctx: int.from_bytes(d.encode("latin-1"), "big"))

class SafeGreedyRange(GreedyRange):
    def __init__(self, subcon, discard=False):
        super().__init__(subcon)
        self.discard = discard

    def _parse(self, stream, context, path):
        discard = self.discard
        obj = ListContainer()
        try:
            for i in itertools.count():
                context._index = i
                e = self.subcon._parsereport(stream, context, path)
                if not discard:
                    obj.append(e)
        except StreamError:
            pass
        return obj

class ReloadableMeta(type):
    def __new__(cls, name, bases, dct):
        m = super().__new__(cls, name, bases, dct)
        m._load_time = time.time()
        return m

class Reloadable(metaclass=ReloadableMeta):
    @classmethod
    def _reloadcls(cls, force=False):
        mods = []
        for c in cls.mro():
            mod = sys.modules[c.__module__]
            cur_cls = getattr(mod, c.__name__)
            mods.append((cur_cls, mod))
            if c.__name__ == "Reloadable":
                break

        reloaded = set()
        newest = 0
        for pcls, mod in mods[::-1]:
            source = getattr(mod, "__file__", None)
            if not source:
                continue
            newest = max(newest, os.stat(source).st_mtime, pcls._load_time)
            if (force or reloaded or pcls._load_time < newest) and mod.__name__ not in reloaded:
                print(f"Reload: {mod.__name__}")
                mod = importlib.reload(mod)
                reloaded.add(mod.__name__)

        return getattr(mods[0][1], cls.__name__)

    def _reloadme(self):
        self.__class__ = self._reloadcls()

class Constant:
    def __init__(self, value):
        self.value = value

    def __call__(self, v):
        assert v == self.value
        return v

class RegisterMeta(ReloadableMeta):
    def __new__(cls, name, bases, dct):
        m = super().__new__(cls, name, bases, dct)

        f = {}

        if bases and bases[0] is not Reloadable:
            for cls in bases[0].mro():
                if cls is Reloadable:
                    break
                f.update({k: None for k,v in cls.__dict__.items()
                          if not k.startswith("_") and isinstance(v, (int, tuple))})

        f.update({k: None for k, v in dct.items()
                 if not k.startswith("_") and isinstance(v, (int, tuple))})

        m._fields_list = list(f.keys())
        m._fields = set(f.keys())

        return m

class Register(Reloadable, metaclass=RegisterMeta):
    _Constant = Constant
    def __init__(self, v=None, **kwargs):
        if v is not None:
            self._value = v
            for k in self._fields_list:
                getattr(self, k) # validate
        else:
            self._value = 0
            for k in self._fields_list:
                field = getattr(self.__class__, k)
                if isinstance(field, tuple) and len(field) >= 3 and isinstance(field[2], self._Constant):
                    setattr(self, k, field[2].value)

        for k,v in kwargs.items():
            setattr(self, k, v)

    def __getattribute__(self, attr):
        if attr.startswith("_") or attr not in self._fields:
            return object.__getattribute__(self, attr)

        field = getattr(self.__class__, attr)
        value = self._value

        if isinstance(field, int):
            return (value >> field) & 1
        elif isinstance(field, tuple):
            if len(field) == 2:
                msb, lsb = field
                ftype = int
            else:
                msb, lsb, ftype = field
            return ftype((value >> lsb) & ((1 << ((msb + 1) - lsb)) - 1))
        else:
            raise AttributeError(f"Invalid field definition {attr} = {field!r}")

    def __setattr__(self, attr, fvalue):
        if attr.startswith("_"):
            self.__dict__[attr] = fvalue
            return

        field = getattr(self.__class__, attr)

        value = self._value

        if isinstance(field, int):
            self._value = (value & ~(1 << field)) | ((fvalue & 1) << field)
        elif isinstance(field, tuple):
            if len(field) == 2:
                msb, lsb = field
            else:
                msb, lsb, ftype = field
            mask = ((1 << ((msb + 1) - lsb)) - 1)
            self._value = (value & ~(mask << lsb)) | ((fvalue & mask) << lsb)
        else:
            raise AttributeError(f"Invalid field definition {attr} = {field!r}")

    def __int__(self):
        return self._value

    def _field_val(self, field_name, as_repr=False):
        field = getattr(self.__class__, field_name)
        val = getattr(self, field_name)
        if isinstance(val, Enum):
            if as_repr:
                return str(val)
            else:
                msb, lsb = field[:2]
                if (msb - lsb + 1) > 3:
                    return f"0x{val.value:x}({val.name})"
                else:
                    return f"{val.value}({val.name})"
        elif not isinstance(val, int):
            return val
        elif isinstance(field, int):
            return val
        elif isinstance(field, tuple):
            msb, lsb = field[:2]
            if (msb - lsb + 1) > 3:
                return f"0x{val:x}"

        return val

    @property
    def fields(self):
        return {k: getattr(self, k) for k in self._fields_list}

    def str_fields(self):
        return ', '.join(f'{k}={self._field_val(k)}' for k in self._fields_list)

    def __str__(self):
        return f"0x{self._value:x} ({self.str_fields()})"

    def __repr__(self):
        return f"{type(self).__name__}({', '.join(f'{k}={self._field_val(k, True)}' for k in self._fields_list)})"

    def copy(self):
        return type(self)(self._value)

    @property
    def value(self):
        return self._value
    @value.setter
    def value(self, val):
        self._value = val

class Register8(Register):
    __WIDTH__ = 8

class Register16(Register):
    __WIDTH__ = 16

class Register32(Register):
    __WIDTH__ = 32

class Register64(Register):
    __WIDTH__ = 64

class RegAdapter(Adapter):
    def __init__(self, register):
        if register.__WIDTH__ == 64:
            subcon = Int64ul
        elif register.__WIDTH__ == 32:
            subcon = Int32ul
        elif register.__WIDTH__ == 16:
            subcon = Int16ul
        elif register.__WIDTH__ == 8:
            subcon = Int8ul
        else:
            raise ValueError("Invalid reg width")

        self.reg = register
        super().__init__(subcon)

    def _decode(self, obj, context, path):
        return self.reg(obj)

    def _encode(self, obj, context, path):
        return obj.value

class RangeMap(Reloadable):
    def __init__(self):
        self.__start = []
        self.__end = []
        self.__value = []

    def clone(self):
        r = type(self)()
        r.__start = list(self.__start)
        r.__end = list(self.__end)
        r.__value = [copy.copy(i) for i in self.__value]
        return r

    def __len__(self):
        return len(self.__start)

    def __nonzero__(self):
        return bool(self.__start)

    def __contains(self, pos, addr):
        if pos < 0 or pos >= len(self.__start):
            return False

        return self.__start[pos] <= addr and addr <= self.__end[pos]

    def __split(self, pos, addr):
        self.__start.insert(pos + 1, addr)
        self.__end.insert(pos, addr - 1)
        self.__value.insert(pos + 1, copy.copy(self.__value[pos]))

    def __zone(self, zone):
        if isinstance(zone, slice):
            zone = range(zone.start if zone.start is not None else 0,
                         zone.stop if zone.stop is not None else 1 << 64)
        elif isinstance(zone, int):
            zone = range(zone, zone + 1)

        return zone

    def lookup(self, addr, default=None):
        addr = int(addr)

        pos = bisect.bisect_left(self.__end, addr)
        if self.__contains(pos, addr):
            return self.__value[pos]
        else:
            return default

    def __iter__(self):
        return self.ranges()

    def ranges(self):
        return (range(s, e + 1) for s, e in zip(self.__start, self.__end))

    def items(self):
        return ((range(s, e + 1), v) for s, e, v in zip(self.__start, self.__end, self.__value))

    def _overlap_range(self, zone, split=False):
        zone = self.__zone(zone)
        if not zone:
            return 0, 0

        start = bisect.bisect_left(self.__end, zone.start)

        if split:
            # Handle left-side overlap
            if self.__contains(start, zone.start) and self.__start[start] != zone.start:
                self.__split(start, zone.start)
                start += 1
                assert self.__start[start] == zone.start

        for pos in range(start, len(self.__start)):
            if self.__start[pos] >= zone.stop:
                return start, pos
            if split and (self.__end[pos] + 1) > zone.stop:
                self.__split(pos, zone.stop)
                return start, pos + 1

        return start, len(self.__start)

    def populate(self, zone, default=[]):
        zone = self.__zone(zone)
        if len(zone) == 0:
            return

        start, stop = zone.start, zone.stop

        # Starting insertion point, overlap inclusive
        pos = bisect.bisect_left(self.__end, zone.start)

        # Handle left-side overlap
        if self.__contains(pos, zone.start) and self.__start[pos] != zone.start:
            self.__split(pos, zone.start)
            pos += 1
            assert self.__start[pos] == zone.start

        # Iterate through overlapping ranges
        while start < stop:
            if pos == len(self.__start):
                # Append to end
                val = copy.copy(default)
                self.__start.append(start)
                self.__end.append(stop - 1)
                self.__value.append(val)
                yield range(start, stop), val
                break

            assert self.__start[pos] >= start
            if self.__start[pos] > start:
                # Insert new range
                boundary = stop
                if pos < len(self.__start):
                    boundary = min(stop, self.__start[pos])
                val = copy.copy(default)
                self.__start.insert(pos, start)
                self.__end.insert(pos, boundary - 1)
                self.__value.insert(pos, val)
                yield range(start, boundary), val
                start = boundary
            else:
                # Handle right-side overlap
                if self.__end[pos] > stop - 1:
                    self.__split(pos, stop)
                # Add to existing range
                yield range(self.__start[pos], self.__end[pos] + 1), self.__value[pos]
                start = self.__end[pos] + 1

            pos += 1
        else:
            assert start == stop

    def overlaps(self, zone, split=False):
        start, stop = self._overlap_range(zone, split)
        for pos in range(start, stop):
            yield range(self.__start[pos], self.__end[pos] + 1), self.__value[pos]

    def replace(self, zone, val):
        zone = self.__zone(zone)
        if zone.start == zone.stop:
            return
        start, stop = self._overlap_range(zone, True)
        self.__start = self.__start[:start] + [zone.start] + self.__start[stop:]
        self.__end = self.__end[:start] + [zone.stop - 1] + self.__end[stop:]
        self.__value = self.__value[:start] + [val] + self.__value[stop:]

    def clear(self, zone=None):
        if zone is None:
            self.__start = []
            self.__end = []
            self.__value = []
        else:
            zone = self.__zone(zone)
            if zone.start == zone.stop:
                return
            start, stop = self._overlap_range(zone, True)
            self.__start = self.__start[:start] + self.__start[stop:]
            self.__end = self.__end[:start] + self.__end[stop:]
            self.__value = self.__value[:start] + self.__value[stop:]

    def compact(self, equal=lambda a, b: a == b, empty=lambda a: not a):
        if len(self) == 0:
            return

        new_s, new_e, new_v = [], [], []

        for pos in range(len(self)):
            s, e, v = self.__start[pos], self.__end[pos], self.__value[pos]
            if empty(v):
                continue
            if new_v and equal(last, v) and s == new_e[-1] + 1:
                new_e[-1] = e
            else:
                new_s.append(s)
                new_e.append(e)
                new_v.append(v)
                last = v

        self.__start, self.__end, self.__value = new_s, new_e, new_v

    def _assert(self, expect, val=lambda a:a):
        state = []
        for i, j, v in zip(self.__start, self.__end, self.__value):
            state.append((i, j, val(v)))
        if state != expect:
            print(f"Expected: {expect}")
            print(f"Got:      {state}")

class AddrLookup(RangeMap):
    def __str__(self):
        b = [""]
        for zone, values in self.items():
            b.append(f"{zone.start:#11x} - {zone.stop - 1:#11x}")
            if len(values) == 0:
                b.append(f" (empty range)")
            elif len(values) == 1:
                b.append(f" : {values[0][0]}\n")
            if len(values) > 1:
                b.append(f" ({len(values):d} devices)\n")
                for value, r in sorted(values, key=lambda r: r[1].start):
                    b.append(f"      {r.start:#10x} - {r.stop - 1:#8x} : {value}\n")

        return "".join(b)

    def add(self, zone, value):
        for r, values in self.populate(zone):
            values.append((value, zone))

    def remove(self, zone, value):
        for r, values in self.overlaps(zone):
            try:
                values.remove((value, zone))
            except:
                pass

    def lookup(self, addr, default='unknown'):
        maps = super().lookup(addr)
        return maps[0] if maps else (default, range(0, 1 << 64))

    def lookup_all(self, addr):
        return super().lookup(addr, [])

    def _assert(self, expect, val=lambda a:a):
        super()._assert(expect, lambda v: [i[0] for i in v])

class ScalarRangeMap(RangeMap):
    def get(self, addr, default=None):
        return self.lookup(addr, default)

    def __setitem__(self, zone, value):
        self.replace(zone, value)

    def __delitem__(self, zone):
        self.clear(zone)

    def __getitem__(self, addr):
        value = self.lookup(addr, default=KeyError)
        if value is KeyError:
            raise KeyError(f"Address {addr:#x} has no value")
        return value

class BoolRangeMap(RangeMap):
    def set(self, zone):
        self.replace(zone, True)

    def __delitem__(self, zone):
        self.clear(zone)

    def __getitem__(self, addr):
        return self.lookup(addr, False)

class DictRangeMap(RangeMap):
    def __setitem__(self, k, value):
        if not isinstance(k, tuple):
            self.replace(k, dict(value))
        else:
            zone, key = k
            for r, values in self.populate(zone, {}):
                values[key] = value

    def __delitem__(self, k):
        if not isinstance(k, tuple):
            self.clear(k)
        else:
            zone, key = k
            for r, values in self.overlaps(zone, True):
                values.pop(key, None)

    def __getitem__(self, k):
        if isinstance(k, tuple):
            addr, k = k
            values = self.lookup(addr)
            return values.get(k, None) if values else None
        else:
            values = self.lookup(k)
            return values or {}

class SetRangeMap(RangeMap):
    def add(self, zone, key):
        for r, values in self.populate(zone, set()):
            values.add(key)

    def discard(self, zone, key):
        for r, values in self.overlaps(zone, split=True):
            if values:
                values.discard(key)
    remove = discard

    def __setitem__(self, k, value):
        self.replace(k, set(value))

    def __delitem__(self, k):
        self.clear(k)

    def __getitem__(self, addr):
        values = super().lookup(addr)
        return frozenset(values) if values else frozenset()

class NdRange:
    def __init__(self, rng, min_step=1):
        if isinstance(rng, range):
            self.ranges = [rng]
        else:
            self.ranges = list(rng)
        least_step = self.ranges[0].step
        for i, rng in enumerate(self.ranges):
            if rng.step == 1:
                self.ranges[i] = range(rng.start, rng.stop, min_step)
                least_step = min_step
            else:
                assert rng.step >= min_step
                least_step = min(least_step, rng.step)
        self.start = sum(rng[0] for rng in self.ranges)
        self.stop = sum(rng[-1] for rng in self.ranges) + least_step
        self.rev = {}
        for i in itertools.product(*map(enumerate, self.ranges)):
            index = tuple(j[0] for j in i)
            addr = sum(j[1] for j in i)
            if len(self.ranges) == 1:
                index = index[0]
            self.rev[addr] = index

    def index(self, item):
        return self.rev[item]

    def __len__(self):
        return self.stop - self.start

    def __contains__(self, item):
        return item in self.rev

    def __getitem__(self, item):
        if not isinstance(item, tuple):
            assert len(self.ranges) == 1
            return self.ranges[0][item]

        assert len(self.ranges) == len(item)
        if all(isinstance(i, int) for i in item):
            return sum((i[j] for i, j in zip(self.ranges, item)))
        else:
            iters = (i[j] for i, j in zip(self.ranges, item))
            return map(sum, itertools.product(*(([i] if isinstance(i, int) else i) for i in iters)))

class RegMapMeta(ReloadableMeta):
    def __new__(cls, name, bases, dct):
        m = super().__new__(cls, name, bases, dct)
        if getattr(m, "_addrmap", None) is None:
            m._addrmap = {}
            m._rngmap = SetRangeMap()
            m._namemap = {}
        else:
            m._addrmap = dict(m._addrmap)
            m._rngmap = m._rngmap.clone()
            m._namemap = dict(m._namemap)

        for k, v in dct.items():
            if k.startswith("_") or not isinstance(v, tuple):
                continue
            addr, rtype = v

            if isinstance(addr, int):
                m._addrmap[addr] = k, rtype
            else:
                addr = NdRange(addr, rtype.__WIDTH__ // 8)
                m._rngmap.add(addr, (addr, k, rtype))

            m._namemap[k] = addr, rtype

            def prop(k):
                def getter(self):
                    return self._accessor[k]
                def setter(self, val):
                    self._accessor[k].val = val
                return property(getter, setter)

            setattr(m, k, prop(k))

        return m

class RegAccessor(Reloadable):
    def __init__(self, cls, rd, wr, addr):
        self.cls = cls
        self.rd = rd
        self.wr = wr
        self.addr = addr

    def __int__(self):
        return self.rd(self.addr)

    @property
    def val(self):
        return self.rd(self.addr)

    @val.setter
    def val(self, value):
        self.wr(self.addr, int(value))

    @property
    def reg(self):
        val = self.val
        if val is None:
            return None
        return self.cls(val)

    @reg.setter
    def reg(self, value):
        self.wr(self.addr, int(value))

    def set(self, **kwargs):
        r = self.reg
        for k, v in kwargs.items():
            setattr(r, k, v)
        self.wr(self.addr, int(r))

    def __str__(self):
        return str(self.reg)

class RegArrayAccessor(Reloadable):
    def __init__(self, range, cls, rd, wr, addr):
        self.range = range
        self.cls = cls
        self.rd = rd
        self.wr = wr
        self.addr = addr

    def __getitem__(self, item):
        off = self.range[item]
        if isinstance(off, int):
            return RegAccessor(self.cls, self.rd, self.wr, self.addr + off)
        else:
            return [RegAccessor(self.cls, self.rd, self.wr, self.addr + i) for i in off]

class BaseRegMap(Reloadable):
    def __init__(self, backend, base):
        self._base = base
        self._backend = backend
        self._accessor = {}

        for name, (addr, rcls) in self._namemap.items():
            width = rcls.__WIDTH__
            rd = functools.partial(backend.read, width=width)
            wr = functools.partial(backend.write, width=width)
            if type(addr).__name__ == "NdRange":
                self._accessor[name] = RegArrayAccessor(addr, rcls, rd, wr, base)
            else:
                self._accessor[name] = RegAccessor(rcls, rd, wr, base + addr)

    def _lookup_offset(cls, offset):
        reg = cls._addrmap.get(offset, None)
        if reg is not None:
            name, rcls = reg
            return name, None, rcls
        ret = cls._rngmap[offset]
        if ret:
            for rng, name, rcls in ret:
                if offset in rng:
                    return name, rng.index(offset), rcls
        return None, None, None
    lookup_offset = classmethod(_lookup_offset)

    def lookup_addr(self, addr):
        return self.lookup_offset(addr - self._base)

    def get_name(self, addr):
        name, index, rcls = self.lookup_addr(addr)
        if index is not None:
            return f"{name}[{index}]"
        else:
            return name

    def _lookup_name(cls, name):
        return cls._namemap.get(name, None)
    lookup_name = classmethod(_lookup_name)

    def _scalar_regs(self):
        for addr, (name, rtype) in self._addrmap.items():
            yield addr, name, self._accessor[name], rtype

    def _array_reg(self, zone, map):
        addrs, name, rtype = map
        def index(addr):
            idx = addrs.index(addr)
            if isinstance(idx, tuple):
                idx = str(idx)[1:-1]
            return idx
        reg = ((addr, f"{name}[{index(addr)}]", self._accessor[name][addrs.index(addr)], rtype)
                     for addr in zone if addr in addrs)
        return reg

    def _array_regs(self):
        for zone, maps in self._rngmap.items():
            yield from heapq.merge(*(self._array_reg(zone, map) for map in maps))

    def dump_regs(self):
        for addr, name, acc, rtype in heapq.merge(sorted(self._scalar_regs()), self._array_regs()):
            print(f"{self._base:#x}+{addr:06x} {name} = {acc.reg}")

class RegMap(BaseRegMap, metaclass=RegMapMeta):
    pass

def irange(start, count, step=1):
    return range(start, start + count * step, step)

# Table generated by:
#
# tbl = [0] * 256
# crc = 1
# for i in [2**x for x in irange(7, 0, -1)]:
#    if crc & 1:
#         crc = (crc >> 1) ^ 0xA001
#     else:
#         crc = crc >> 1
#     for j in range(0, 255, 2*i):
#         tbl[i + j] = crc ^ tbl[j]
#
# for i in range(0, 255, 8):
#     print(f"{tbl[i]:#06x}, {tbl[i+1]:#06x}, {tbl[i+2]:#06x}, {tbl[i+3]:#06x}, {tbl[i+4]:#06x}, {tbl[i+5]:#06x}, {tbl[i+6]:#06x}, {tbl[i+7]:#06x}, ")

_crc16_table = [
    0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
    0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
    0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
    0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
    0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
    0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
    0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
    0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
    0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
    0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
    0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
    0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
    0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
    0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
    0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
    0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
    0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
    0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
    0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
    0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
    0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
    0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
    0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
    0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
    0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
    0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
    0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
    0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
    0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
    0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
    0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
    0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040
]

def crc16USB(crc, data):
    for x in data:
        crc = (crc >> 8) ^ _crc16_table[(crc ^ x) & 0xff]
    return crc

__all__.extend(k for k, v in globals().items()
               if (callable(v) or isinstance(v, type)) and v.__module__ == __name__)

if __name__ == "__main__":
    # AddrLookup test
    a = AddrLookup()
    a.add(range(0, 10), 0)
    a._assert([
        (0, 9, [0])
    ])
    a.add(range(10, 20), 1)
    a._assert([
        (0, 9, [0]), (10, 19, [1])
    ])
    a.add(range(20, 25), 2)
    a._assert([
        (0, 9, [0]), (10, 19, [1]), (20, 24, [2])
    ])
    a.add(range(30, 40), 3)
    a._assert([
        (0, 9, [0]), (10, 19, [1]), (20, 24, [2]), (30, 39, [3])
    ])
    a.add(range(0, 15), 4)
    a._assert([
        (0, 9, [0, 4]), (10, 14, [1, 4]), (15, 19, [1]), (20, 24, [2]), (30, 39, [3])
    ])
    a.add(range(0, 15), 5)
    a._assert([
        (0, 9, [0, 4, 5]), (10, 14, [1, 4, 5]), (15, 19, [1]), (20, 24, [2]), (30, 39, [3])
    ])
    a.add(range(21, 44), 6)
    a._assert([
        (0, 9, [0, 4, 5]), (10, 14, [1, 4, 5]), (15, 19, [1]), (20, 20, [2]), (21, 24, [2, 6]),
        (25, 29, [6]), (30, 39, [3, 6]), (40, 43, [6])
    ])
    a.add(range(70, 80), 7)
    a._assert([
        (0, 9, [0, 4, 5]), (10, 14, [1, 4, 5]), (15, 19, [1]), (20, 20, [2]), (21, 24, [2, 6]),
        (25, 29, [6]), (30, 39, [3, 6]), (40, 43, [6]), (70, 79, [7])
    ])
    a.add(range(0, 100), 8)
    a._assert([
        (0, 9, [0, 4, 5, 8]), (10, 14, [1, 4, 5, 8]), (15, 19, [1, 8]), (20, 20, [2, 8]),
        (21, 24, [2, 6, 8]), (25, 29, [6, 8]), (30, 39, [3, 6, 8]), (40, 43, [6, 8]),
        (44, 69, [8]), (70, 79, [7, 8]), (80, 99, [8])
    ])
    a.remove(range(21, 44), 6)
    a._assert([
        (0, 9, [0, 4, 5, 8]), (10, 14, [1, 4, 5, 8]), (15, 19, [1, 8]), (20, 20, [2, 8]),
        (21, 24, [2, 8]), (25, 29, [8]), (30, 39, [3, 8]), (40, 43, [8]),
        (44, 69, [8]), (70, 79, [7, 8]), (80, 99, [8])
    ])
    a.compact()
    a._assert([
        (0, 9, [0, 4, 5, 8]), (10, 14, [1, 4, 5, 8]), (15, 19, [1, 8]), (20, 24, [2, 8]),
        (25, 29, [8]), (30, 39, [3, 8]), (40, 69, [8]), (70, 79, [7, 8]),
        (80, 99, [8])
    ])
    a.remove(range(0, 100), 8)
    a._assert([
        (0, 9, [0, 4, 5]), (10, 14, [1, 4, 5]), (15, 19, [1]), (20, 24, [2]), (25, 29, []),
        (30, 39, [3]), (40, 69, []), (70, 79, [7]), (80, 99, [])
    ])
    a.compact()
    a._assert([
        (0, 9, [0, 4, 5]), (10, 14, [1, 4, 5]), (15, 19, [1]), (20, 24, [2]), (30, 39, [3]),
        (70, 79, [7])
    ])
    a.clear(range(12, 21))
    a._assert([
        (0, 9, [0, 4, 5]), (10, 11, [1, 4, 5]), (21, 24, [2]), (30, 39, [3]),
        (70, 79, [7])
    ])

    # ScalarRangeMap test
    a = ScalarRangeMap()
    a[0:5] = 1
    a[5:10] = 2
    a[4:8] = 3
    del a[2:4]
    expect = [1, 1, None, None, 3, 3, 3, 3, 2, 2, None]
    for i,j in enumerate(expect):
        assert a.get(i) == j
        if j is not None:
            assert a[i] == j
    try:
        a[10]
    except KeyError:
        pass
    else:
        assert False

    # DictRangeMap test
    a = DictRangeMap()
    a[0:5, 0] = 10
    a[5:8, 1] = 11
    a[4:6, 2] = 12
    del a[2:4]
    expect = [{0: 10}, {0: 10}, {}, {}, {0: 10, 2: 12}, {1: 11, 2: 12}, {1: 11}, {1: 11}, {}]
    for i,j in enumerate(expect):
        assert a[i] == j
        for k, v in j.items():
            assert a[i, k] == v

    # SetRangeMap test
    a = SetRangeMap()
    a[0:2] = {1,}
    a[2:7] = {2,}
    a.add(range(1, 4), 3)
    a.discard(0, -1)
    a.discard(3, 2)
    del a[4]
    expect = [{1,}, {1,3}, {2,3}, {3,}, set(), {2,}, {2,}, set()]
    for i,j in enumerate(expect):
        assert a[i] == j

    # BoolRangeMap test
    a = BoolRangeMap()
    a.set(range(0, 2))
    a.set(range(4, 6))
    a.set(range(5, 5))
    a.clear(range(3, 5))
    expect = [True, True, False, False, False, True, False]
    for i,j in enumerate(expect):
        assert a[i] == j
