"""pipText text driver for a Pip CPU simulation.
See actions list at the end of the file.

The main loop reads input, notes cmd name and remembers remaining tokens,
and then finds the entry in the actions list corresponding to the cmd and calls
the associated function.  The function then generally accesses the tokens
for details of what to do.

Note that the doc string for each such function is printed out if requested
in the help command.

The functions associated with actions are shown before main().
The remaining functions are shown after main.
The actions list that refers to the cmd methods is defined last.

TODO: ?? consistent error recovery in command methods
make tokens not global, pass to all actions
"""
import sys, time
from graphics import *
from pip import Pip
import pip

showAll = True  # if true print whole cpu state instead of log entry
cpu = None      # Pip acted on
fileName = None # last filename used

def textMakeErr(err):
    if err:
        print err
        return True
    return False

makeErr = textMakeErr

def setMakeErr(errPrinter):
    global makeErr
    makeErr = errPrinter

def errOrShow(err):
    isErr = makeErr(err) 
    if not isErr:
        show()
    return isErr

def step(tokens=None):
    """Single step the CPU and show the result.
-step     -- an empty line is sufficient.
"""

    return errOrShow(cpu.step())

def runCPU(tokens=None):
    """Run multiple steps of the simulator with a possible delay.  Versions:
r-un
r-un <steps total (100 max)>
r-un <steps> <seconds delay between steps>

The number of steps is reduced if it is over 100 or the delays will add up to
more than 25 seconds.
"""
    steps = 100
    secDelay = 0
    if tokens:
        if not tokens[0]:
            tokens[0] = steps
        if len(tokens) > 1:
            secDelay = abs(float(tokens[1]))
        if steps * secDelay > 25:
            steps = min(25.0/secDelay, steps)
        steps = int(max(1, min(int(tokens[0]), steps)))
    runText(cpu, steps, secDelay, not showAll)

def help(tokens=None):
    """Prints this help message or help on a particular command.
h-elp
"""
    if tokens:
        if match(tokens[0], "q-uit"):
            print"Quit the simulation."
        else:
            for (s, f) in actions:
                if match(tokens[0], s):
                    print f.__doc__
                    break
            else:
                print "No command matched.  Enter h for a list of commands."

    else:
        print "The possible commands are below,\n"+\
              "where the part before any '-' is required,\n"+\
              "omit any '-' itself, and\n"+\
              "include as much of the part after any '-' as you like.\n"+\
              "For more information on a legal command, enter help cmd,\n"+\
              "for instance 'help run'.   Commands:\n"
        for i, (s, f) in enumerate(actions):
            print s,"",
            if i % 6 == 5:
                print
        print

def setShowAll(doShowCPU):
    global showAll
    showAll = doShowCPU
    
def viewEntry(tokens=None):  # particularly for text version
    """See the latest log entry, and switch to showing brief log entries
after changes rather than the whole CPU state.
e-ntryInLog
"""
    setShowAll(False)
    print cpu.logEntry("", True, False)
    return False

def viewCPU(tokens=None):  # assumed for GUI
    """See the whole CPU state, and switch to showing this whole state
after changes rather than a brief log entry.
c-pu
"""
    setShowAll(True)
    show()

def viewLog(tokens=None):
    """View the entire history retained in the log.
hi-storyInLog
Use wipe to clear this and start over.

    """
    print cpu.getLogLines(0)

def save(tokens=None, showNumLabels = False):
    """Save the CPU state to a file.  Options:
sa-ve             -- save to the last used filename
sa-ve fileName    -- save the specified file

File format given by explicit extension:
   .dat file like those generated by the Pippin applet,
   .bin file text of bytes in binary
   .asm assember - using current label display format
If a name is given with out one of these extensions, an extension is added:
   .dat if the labels match the display in the Pippin applet, otherwise
   .bin if the current display is binary, or otherwise
   .asm assember - using current label display format
"""
    
    global fileName
    file = fileName
    if tokens:
        file = tokens[0]
    if not file:
        return makeErr("A filename or filebase must be given.")
    try:
        fileName = cpu.saveFile(file, True, showNumLabels)
        print "Saved to " + fileName  #?GUI
    except Exception, e:
        return makeErr(str(e))
    return False

def load(tokens=None):
    """Load the CPU state from a file.  Options:
lo-ad             -- load the previous file if there was one, or an empty CPU
lo-ad fileName    -- load the specified file if it is legal.

Legal file are
   .dat file generated by the Pippin applet,
   files of binary bytes (groups of up to eight 0's and 1's, separated by white)
   assembler files with or without symbolic instruction labels or data labels
"""
    
    global fileName
    file = fileName
    if tokens:
        file = tokens[0]
    if not file:
        return makeErr("A filename or filebase must be given.")
    try:
        cpu.loadFile(file)
        fileName = file
    except Exception, e:
         return makeErr(str(e))           
    show()
    return False
    

def ip(tokens=None):
    """
i-pSet <val>  -- choose a value for the IP 

"""
   
    if not tokens:
        return makeErr("New value required.")
    return errOrShow(cpu.ipEdit(tokens[0]))

def accum(tokens=None):
    """
ac-cum <val>  -- choose a value for the accumulator 

"""
    
    if not tokens:
        return makeErr("New value required.")
    return errOrShow(cpu.accEdit(tokens[0]))

def wipe(tokens=None):
    """
w-ipeLog  -- clear the history log
"""
    cpu.clearLog()
    return False

def whichLabels(tokens=None):
    """Specify which labels are displayed in assembler listings:
l-abels                 -- show all symbolic labels available
l-abels n-one           -- show no symbolic labels -- all numeric
l-abels c-ode           -- show symbolic code labels, numeric data labels
l-abels d-ata           -- show numeric code labels, symbolic data labels,
                           like in the applet.

The notation with the '-' is as in the help message -- indicating only the
first letter of none, code or data are needed.
"""

    if not tokens or matchToken(tokens, "a-ll"):
        code = True
        data = True
    elif matchToken(tokens, "n-one"):
        code = False
        data = False
    elif matchToken(tokens, "c-ode"):
        code = True
        data = False
    elif matchToken(tokens, "d-ata"):
        code = False
        data = True
    else:
        return makeErr("Unrecognized label option: %s" % tokens[0])
    setLabels(data, code)
    return False

def maybeShow(oldHeading):
    """Called on formatting change, may trigger display."""
    if showAll:
        show()
    else:
        heading = cpu.logHeading(False)
        if oldHeading != heading: 
            print cpu.logEntry(heading, False, False)

def setLabels(data, code):
    oldHeading = cpu.logHeading(False)
    cpu.useAlphaDataLabels(data) 
    cpu.useAlphaCodeLabels(code)
    cpu.useBinary(False)
    maybeShow(oldHeading)

def binary(tokens=None):
    """Show everything in binary.  Reverse with a-sm.
b-inary
"""
    oldHeading = cpu.logHeading(False)
    cpu.useBinary(True)
    maybeShow(oldHeading)
    return False

def assembler(tokens=None):
    """Show everything in assember. Switch to binary with b-inary.
a-sm
"""
    cpu.useBinary(False)
    show()  # only used in text version
    return False

def editMem(tokens=None):
    """Edit Pip instrution or data
    e-dit <numerical or symbolic code or data address label> <newVal>
    """
    #  For GUI version see Pip's codeEdit and dataEdit
    if not len(tokens) > 1:
        return makeErr("Requires both a memory address and new value.")
    return errOrShow(cpu.memEdit(tokens[0], " ".join(tokens[1:])))


def init(tokens=None):
    """
in-it -- initialize the CPU for a program restart
"""
    cpu.initCPU()
    show()



def cd(tokens=None):
    """Not inplemented.
"""
    print "Not inplemented"

def ls(tokens=None):
    """Not inplemented.
"""
    print "Not inplemented"

def createCPU(args):
    global cpu
    if args:
        if len(args) > 1:
            print "Too many arguments -- ignoring them!"
            cpu = Pip()
        else:
         cpu = Pip(args[0])
    else:
        cpu = Pip()
    return cpu    
    
def mainText(args = None):
    global cpu
    cpu = createCPU(args)
    
    print "Type h for help."
    cmd, tokens = getInput()  
    while not match(cmd, "q-uit"):
        for (s, f) in actions:
            if match(cmd, s):
                f(tokens)
                break
        else:
            print "No command matched.  Enter h for help."
        cmd, tokens = getInput()

    
def match(cmd, s):
    """ Check if cmd has the required parts of s and any of the optional part.
    """
    parts = s.split('-')
    start = parts[0]
    if len(parts) > 1:
        end = parts[1]
    else:
        end = ""
    return cmd.startswith(start) and (start+end).startswith(cmd)

def matchToken(tokens, s):
    """ helper to parse match in whichLabels."""
    for token in tokens:
        if match(token, s):
            return True
    return False
        
def getInput():
    """ return cmd, list of remaining tokens."""
    tokens = raw_input("?: ").split() # tokens object never changes
    if not tokens:
        return "", []
    return tokens[0].lower(), tokens[1:]

def show(logLines = 1):
    """This is generally called after each command that changes the state.
    """
    if showAll:
        showStateText(cpu)
    else:
        print cpu.getLogLines(logLines)

def showStateText(cpu):
    """
    Show all instructions through halt, with
    * after last instruction executed,
    > after next instruction executed
    show data range from first to last referenced in code
    = after last if written (or omit if none written last)
    > after next data to be referenced (or omit if none to be referenced)
    """
    print        
    print "======CODE======"
    for i in range(0, cpu.codeLim, 2):
        marker = ""
        if i == cpu.ip:
            marker +=" >"
        if i == cpu.lastIP:
            marker +=" *"
        print "%-18s%s" % (cpu.lineStr(i), marker)
    if cpu.dataLims:
        dataAddr = cpu.getDataRef(cpu.ip)
        if cpu.getLastDataEffect() == pip.DATA_WRITTEN:
            writtenDataAddr = cpu.getDataRef(cpu.lastIP)
        else:
            writtenDataAddr = None
        print "------DATA------"
        for i in range(*cpu.dataLims):
            marker = ""
            if i == writtenDataAddr:
                marker =" ="
            if i == dataAddr:
                marker +=" >"
            print "%8s: %-8s%s" % \
                  (cpu.labelStr(i), cpu.dataStr(i), marker)
    print "------CPU-------"
    print "      IP: %s" % cpu.ipStr()        
    print "     Acc: %s" % cpu.accStr() 

def runText(cpu, steps = 100, secDelay = 0, seeLog = False, 
            seeLogHeading = False):
    """Run specified number of steps or until halted.
    If seeLog is true
       see the log lines appear, but no whole Pip displays
    Otherwise
       If secDelay is 0, see no Pip display only at the end
       If secDelay > 0 see Pip display after each step.
    """
    
    if cpu.halted:
      return makeErr('Halted -- reset the IP to continue.')
    if seeLogHeading:
        print cpu.logHeading(False)
    for n in range(steps):
        err = cpu.step()
        if makeErr(err):
            return
        if seeLog:
            print cpu.getLogLines()
        if secDelay or n == steps - 1 or cpu.halted:
            if not seeLog:
                showStateText(cpu)
            if n < steps - 1 and not cpu.halted:
                time.sleep(secDelay)
        if cpu.halted:
            return


actions = [("-stepOnce", step),   #single step
           ("h-elp", help),   #view format
           ("e-ntryInLog", viewEntry),   #view format
           ("c-puView", viewCPU),   #view format
           ("hi-storyOfLog", viewLog),   #view format
           ("sa-veFile", save),   #view format
           ("lo-adFile", load),   # file=fileName - load file
           ("i-pSet", ip),   #view format
           ("ac-cumSet", accum),   # # - set accum
           ("r-un", runCPU),   #
           ("l-abelsShown", whichLabels),   #
           ("b-inaryShown", binary),   #
           ("a-smShown", assembler),   #
           ("e-ditMemory", editMem),   #
           ("in-itializeCPU", init),   #
           ("w-ipeLog", wipe),# - wipe log
           ("cd", cd),   # cd not implemented
           ("ls", ls) ]   # ls not implemented


    
if __name__ == "__main__":
    mainText(sys.argv[1:])
