<gh_stars>10-100
#!/usr/bin/env python3
from __future__ import print_function
# Author: <NAME>
# Date: 2019-03-09
# Based on 1801PE2 ROM convertion tool written by Vslav
#    https://github.com/1801BM1/k1801/tree/master/rom/rev16
#
import os
import sys
import argparse
import array
import struct
from collections import namedtuple
from functools import reduce

# config.h
REV16_SIZE = 0x2000


class BF(object):
    # A bit array demo - written for Python 3.0
    # https://wiki.python.org/moin/BitArrays

    def __init__(self, bit_sz, fill=0, offset=0):
        self._offs = offset  # remap addresses for write_byte/insert_ary
        self._bit_sz = bit_sz        # size in bits
        self._map_sz = bit_sz >> 3  # size in bytes
        if bit_sz & 7:
            self._map_sz += 1
        fill = 0xff if fill != 0 else 0
        self._map = array.array('B')  # 'B' = unsigned 8-bit integer
        self._map.extend((0,) * self._map_sz)
        self._data = array.array('B')
        self._data.extend((fill,) * self._bit_sz)

    def size(self):
        return self._bit_sz

    def __iter__(self):
        return self

    def __next__(self):
        # return enumerate(self._data)
        for addr, c in enumerate(self._data):
            if not self.has_byte(addr):
                continue
            yield (addr, c)

    def next(self):
        return self.__next__()

    def write_byte(self, addr, c):
        """
        put single byte into the buffer
        """
        addr -= self._offs
        if addr < 0:
            raise ValueError("Buffer underrun at addr %04Xh (offs %04Xh)" %
                             (addr + self._offs, self._offs))
        if addr + 1 > self.size():
            raise ValueError("Buffer overflow at addr %04Xh (size %04Xh)" %
                             (addr + self._offs, self.size()))
        if self.has_byte(addr):
            raise ValueError("overwrite byte at addr %04Xh" %
                             (addr + self._offs))
        self._data[addr] = c
        self.set_map_bit(addr)

    def insert_ary(self, addr, data):
        """
        Put an array (data) into the buffer
        """
        addr -= self._offs
        if addr < 0:
            raise ValueError("Buffer underrun at addr %04Xh (offs %04Xh)" %
                             (addr + self._offs, self._offs))
        if addr + len(data) > self.size():
            raise ValueError("Buffer overflow at addr %04Xh size %d" %
                             (addr + self._offs, len(data)))
        for i, c in enumerate(data):
            pos = addr + i
            if self.has_byte(pos):
                raise ValueError("overwrite byte at addr %04Xh" % pos)
            self._data[pos] = c
            self.set_map_bit(pos)

    def squeeze_tail(self):
        """
        purpose try to remove trailing bytes
        filled with `fill` value like 0xFF
        """
        pass

    def has_byte(self, bit_idx):
        """
        test that the byte is present in array (by _map)
        return 1 or 0
        """
        byte_idx = bit_idx >> 3
        offset = bit_idx & 7
        mask = 1 << offset
        return(self._map[byte_idx] & mask)

    def set_map_bit(self, bit_num):
        """
        returns an integer with the bit at 'bit_num' set to 1.
        """
        record = bit_num >> 3
        offset = bit_num & 7
        mask = 1 << offset
        self._map[record] |= mask
        return(self._map[record])

    def clear_map_bit(self, bit_num):
        """
        returns an integer with the bit at 'bit_num' cleared.
        """
        record = bit_num >> 3
        offset = bit_num & 7
        mask = ~(1 << offset)
        self._map[record] &= mask
        return(self._map[record])

    def toggle_map_bit(self, bit_num):
        """
        returns an integer with the bit
        at 'bit_num' inverted, 0 -> 1 and 1 -> 0.
        """
        record = bit_num >> 3
        offset = bit_num & 7
        mask = 1 << offset
        self._map[record] ^= mask
        return(self._map[record])


# * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
def do_sum(a, b):
    return a + b


def csum(data):
    return (256 - (reduce(do_sum, data, 0) & 255)) & 255


class IHexDat(namedtuple('IHexDat', ['addr', 'data'])):

    def __str__(self):
        assert type(self.data) == bytes, "data has type %s" % type(self.data)
        l = len(self.data)
        if l > 255:
            raise ValueError("len(data)> 255")
        v = struct.pack('>BHB', l, self.addr, 0)  # header
        v += self.data
        return ':%s%02X' % (bytes(v).hex(), csum(v))


class IHexSSA(namedtuple('IHexSSA', ['cs', 'ip'])):

    def __str__(self):
        v = struct.pack('>BHBHH', 4, 0, 3, self.cs, self.ip)
        return ':%s%02X' % (v.hex(), csum(v))


class IHexEOF(object):

    def __str__(self):
        return ':00000001FF'


# * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
class IHEX(object):
    """
    Read Intel HEX file, convert to bytearray
    https://en.wikipedia.org/wiki/Intel_HEX
    """
    def __init__(self, buf):
        self.buf = buf
        self.lnum = 0
        self._esa = 0  # Extended Segment Address
        self._scs = 0  # Start Segment Address (CS:IP)
        self._sip = 0  # Start Segment Address (CS:IP)
        self._ela = 0  # Extended Linear Address
        self._sla = 0  # Start Linear Address

    def _parse_ihex_rec(self, line):
        self.lnum += 1
        if line[0] == ';':  # is a comment?
            return
        if line[0] != ':':
            raise ValueError("Invalid hex-record found at line %d" %
                             self.lnum)
        # print(line,)
        x = bytearray.fromhex(line[1:].rstrip())
        cs = csum(x)
        if cs & 255 != 0:
            raise ValueError("Invalid checksum %02xh at line %d" %
                             (csum, self.lnum))
        bcnt = x[0]
        addr = (x[1] << 8) + x[2]
        rtype = x[3]
        data = x[4:-1]
        return (bcnt, addr, rtype, data)

    def read_file(self, in_fn):
        in_f = open(in_fn, 'r')
        self.lnum = 0
        for line in in_f.readlines():
            (bcnt, addr, rtype, data) = self._parse_ihex_rec(line)
            if rtype == 0:  # data
                # for 16bit ISA like PDP-11 we ignore ESA
                # use use only addr there
                assert len(data) == bcnt
                self.buf.insert_ary(addr, data)
                print(":DAT [%2d] %04Xh" % (bcnt, addr))
            elif rtype == 1:  # EOF
                # close/flush the file?
                print(":EOF")
            elif rtype == 2:  # extended segment address
                self._esa, = struct.unpack('>H', data)
                self._esa <<= 4
                if (self._esa > self.buf.size()):
                    raise ValueError("ESA(%04Xh) is beyond of buf size at "
                                     "line %d" % (self._esa, self.lnum))
                print(":ESA %04Xh" % (self._esa))

            elif rtype == 3:  # Start Segment Address
                self._scs, self._sip = struct.unpack('>HH', data)
                print(":SSA %04X:%04X" % (self._scs, self._sip))
            elif rtype == 4:  # Extended Linear Address
                # The two data bytes (big endian) specify the upper 16 bits of
                # the 32 bit absolute address for all subsequent type 00
                # records;
                self._ela, = struct.unpack('>H', data)
                print(":ELA %04X0000h" % self._ela)
            elif rtype == 5:  # Start Linear Address
                self._sla = struct.unpack('>L', data)
                print(":SLA %08Xh" % self._sla)


def my_int(val):
    if val.startswith('0x'):
        return int(val, base=16)
    if val.endswith('h'):
        return int(val[:-1], base=16)
    if val.startswith('0'):
        return int(val, base=8)
    return int(val)


def write_rom_file(out_rom_fn, buf):
    with(open(out_rom_fn, 'wb')) as fo:
        fo.write(buf._data)


def do_decode(args):
    """
    do decoding from ROM into HEX format
    """
    with open(args.rom_fn, 'rb') as fi:
        dat = fi.read(REV16_SIZE+2)
        if len(dat) != REV16_SIZE+2:
            raise RuntimeError("Unexpexted ROM file size: %d" % len(dat))
        ibuf = BF(REV16_SIZE, fill=args.fill)
        ibuf.insert_ary(0, dat[:-2])  # w/o chip-code
    obuf = BF(REV16_SIZE, fill=args.fill)
    for addr, c in ibuf.next():
        if addr == REV16_SIZE:
            break
        oaddr = addr ^ (REV16_SIZE-2)
        oc = ((~c) & 255)
        obuf.write_byte(oaddr, oc)
        # print("%04X [%02X] -> %04X [%02X]" % (addr, c, oaddr, oc))
    with open('tmp.rom', 'wb') as fo:
        fo.write(obuf._data[:REV16_SIZE])
    obuf.squeeze_tail()
    # write out IHEX
    with open(args.hex_fn, 'w') as fo:
        #
        # if args.offset:
        addr = 0
        while addr < obuf.size():
            rec = IHexDat(addr + args.offset,
                          bytes(obuf._data[addr:addr+16]))
            fo.write(str(rec))
            fo.write("\r\n")
            addr += 16

        rec = IHexSSA(0, args.start_addr)
        # print("IHexSSA: %s" % str(rec))
        fo.write(str(rec))
        fo.write("\r\n")
        fo.write(str(IHexEOF()))
        fo.write("\r\n")


def do_encode(args):
    """
    do encoding from IHEX into ROM format
    """
    ibuf = BF(REV16_SIZE+2, fill=args.fill, offset=args.offset)
    rom = IHEX(ibuf)
    rom.read_file(args.hex_fn)
    obuf = BF(REV16_SIZE+2, fill=args.fill)
    for addr, c in ibuf.next():
        oaddr = addr ^ (REV16_SIZE-2)
        oc = ((~c) & 255)
        obuf.write_byte(oaddr, oc)
        # print("%04X [%02X] -> %04X [%02X]" % (addr, c, oaddr, oc))
    # 1801PE2 ROM footer
    sig = array.array('B', [0, args.chip_code])
    obuf.insert_ary(REV16_SIZE, sig)
    write_rom_file(args.rom_fn, obuf)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(prog='1801PE2 ROM convereted')
    parser.add_argument('-x', dest='hex_fn', required=True,
                        help='input HEX file')
    parser.add_argument('-b', dest='rom_fn', required=True,
                        help='output ROM file')
    parser.add_argument('-c', dest='chip_code', type=int, default=3,
                        help='ROM chip code')
    parser.add_argument('-f', dest='fill', type=int, default=0xff,
                        help='fill byte')
    parser.add_argument('-o', dest='offset', type=my_int, default=0,
                        help='offset')
    parser.add_argument('-a', dest='start_addr', type=my_int, default=0,
                        help='start address')
    group = parser.add_mutually_exclusive_group()
    group.add_argument('-D', action='store_true')
    group.add_argument('-E', action='store_true')
    args = parser.parse_args()
    if args.E:
        do_encode(args)
    elif args.D:
        do_decode(args)
    else:
        print("Either -D or -E option should be specified", file=sys.stderr)
        sys.exit(os.EX_USAGE)
