import time
from machine import PDP1170
from kw11 import KW11
from kl11 import KL11
from dc11 import DC11
from rp import RPRM

import breakpoints


def boot_hp(p, /, *, addr=0o10000, deposit_only=False):
    """Deposit, then run, instructions to read first 1KB of drive 0 --> addr.
    RETURN VALUE:  addr if deposit_only else None

    If no 'addr' given, it defaults to something out of the way.

    If not deposit_only (default):
       * The instructions are loaded to 'addr'
       * They are executed.
       * Return value is None.
       * NOTE: The next start address depends on what those instructions do.
       *       TYPICALLY, the next start address will be zero.

    If deposit_only:
       * The instructions are loaded to 'addr'
       * 'addr' is returned.
    """

    # this is the sort of thing that would be keyed in from
    # the console switches (if the machine was not equipped
    # with a boot rom option to hold it instead)
    #
    # It is a minimalist  program, with lots of assumptions, to  read 1K
    # from block zero of drive 0 into location 0. The execution start
    # at zero is done elsewhere.
    #
    # NOTE WELL: THIS ASSUMES THE MACHINE IS IN RESET CONDITION WHICH
    #            MEANS MANY OF THE DEVICE REGISTERS ARE ASSUMED TO BE ZERO
    #
    #      MOV #176704,R0       -- note how used
    #      MOV #177000,-(R0)    -- word count - read 1K though boot really 512
    #      MOV #071,-(R0)       -- go!
    program_insts = (
        0o012700,          # MOV #0176704,R0
        0o176704,
        0o012740,          # MOV #177000,-(R0)
        0o177000,
        0o012740,          # MOV #071, -(R0)
        0o000071,
        0o0,               # HALT
    )

    for o, w in enumerate(program_insts):
        p.physRW(addr + o + o, w)

    p.r[p.PC] = addr
    if not deposit_only:
        p.run()

        # at this point in real life the user would have to set the switches
        # to deposit zero into the PC and then hit start; that takes time
        # and means the bootstrap doesn't have to have code to wait for the
        # drive to complete the read operation. Instead of adding code to
        # the "pretend this was keyed in" program above, this delay works.
        time.sleep(0.25)

    return addr if deposit_only else None


def boot_bin(p, fname, /, *, addr=0, deposit_only=False,
             little_endian=True, skipwords=8):
    """Read a binary file 'fname' into location 'addr' and execute it.
    RETURN VALUE:  addr if deposit_only else None

    NOTE: fname is in the host system, not on an emulated drive.
    If no 'addr' given, it defaults to ZERO.
    If deposit_only=True, the instructions are not executed.

    little_endian (default True) dictates the fname byte order.
    skipwords (default 8 -- a.out header) will seek that many 16-bit
    words into the file before beginning to load.
    """
    with open(fname, 'rb') as f:
        bb = f.read()

        # Two data format cases:
        #   1) little_endian (the default)
        #
        #      The file is truly a binary image of pdp11 format data
        #      and the words (pairs of bytes) in bb are in little endian
        #      order. They will be assembled accordingly and the names
        #      "low" and "hi" make sense.
        #
        #   2) not little_endian
        #
        #      Presumably the file has been byte-swapped already and
        #      the words (pairs of bytes) in it are in big endian order.
        #      They will be assembled accordingly, but the names "low"
        #      and "hi" are backwards.
        #

        xi = iter(bb)
        words = []
        for low in xi:
            hi = next(xi)
            if little_endian:
                words.append((hi << 8) | low)
            else:
                words.append((low << 8) | hi)    # see case 2) above

        for a, w in enumerate(words[skipwords:]):
            p.physmem[a] = w

    p.r[p.PC] = addr
    if not deposit_only:
        p.run()
    return addr if deposit_only else None


def make_unix_machine(*, loglevel='INFO', drivenames=[]):
    p = PDP1170(loglevel=loglevel)

    p.associate_device(KW11(p.ub), 'KW')    # line clock
    p.associate_device(KL11(p.ub), 'KL')    # console
    p.associate_device(RPRM(p.ub, *drivenames), 'RP')    # disk drive
    p.associate_device(DC11(p.ub), 'DC')    # additional serial poirts
    return p


def boot_unix(p, runoptions={}):

    # load, and execute, the key-in bootstrap
    boot_hp(p)

    print("Starting PDP11; this window is NOT THE EMULATED PDP-11 CONSOLE.")
    print("*** In another window, telnet/nc to localhost:1170 to connect.")
    print("    Terminal should be in raw mode. On a mac, this is a good way:")
    print("         (stty raw; nc localhost 1170; stty sane)")
    print("")
    print("There will be no prompt; type 'boot' in your OTHER window")
    print("")
    print("Then, at the ':' prompt, typically type: hp(0,0)unix")

    p.run(pc=0, **runoptions)


# USE:
#    python3 boot.py
#
# to start up unix (or whatever system is on the drive)

if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument('--debug', action='store_true')
    parser.add_argument('--drive', action='append', default=[], dest='drives')
    parser.add_argument('--instlog', action='store_true')
    args = parser.parse_args()

    pdpoptions = {'drivenames': args.drives}
    runoptions = {}
    if args.debug:
        pdpoptions['loglevel'] = 'DEBUG'
    if args.instlog:
        runoptions['breakpoint'] = breakpoints.Logger()

    p = make_unix_machine(**pdpoptions)

    boot_unix(p, runoptions=runoptions)