#!/usr/bin/env python

#Quake-style Command Processor

import sys


class cmdproc:
  """Quake-style Command Processor.  Takes in text into a command buffer, interprets commands line-by-line.
  Make regular frequent calls to cmdproc.cycle().  Due to the semantics of wait(), try to make the timings between calls as consistent as possible.
  Primary buffer methods are append() and prepend().
  Method execute() bypasses the buffer for direct parsing and execution.

Extension commands are added with add_command() and removed with remove_command().
The method add_command() fails if the command name already exists.
The functions that implement the extension commands need to take one argument, the cmdproc object.
One way is to create the extension command's method's parent as a derived class of cmdproc:

class mycomponent (cmdproc):
  def __init__ (self):
    self.add_command("commandname", self.cmd_newcommand)

  def cmd_newcommand (self):
    cmdname = self.argv(0)
    firstarg = self.argv(1)
    secondarg = self.argv(2)
    return 0


Or a standalone command as:

mycmdproc = cmdproc.cmdproc()
def newcommand (cmdproc):
  cmdname = cmdproc.argv(0)
  firstarg = cmdproc.argv(1)
mycmdproc.add_command("mycommand", newcommand)
  
"""
  def __init__ (self):
    self.commands = { "wait": self.cmd_wait, "echo": self.cmd_echo,
      "exec": self.cmd_exec, "cmdlist": self.cmd_cmdlist,
      "crash": self.cmd_crash }
    self.cmdbuf = ""
    self.suspend = 0
    self._argstring = ""
    self._argv = []


  def putstr (self, text):
    """Display to output.  Extend or overload if redirecting/replicating to a special text window or log file.  Output device should recognize '\n' (and '\r') as line terminators (newlines).  Output should not append an implicit newline."""
    sys.stdout.write(text)
    sys.stdout.flush()


  def dump (self):
    """Debug routine."""
    print "Command list:"
    for cmdname in self.commands.keys():
      print " * %s" % cmdname
    print "Command buffer:", self.cmdbuf


  def cycle (self):
    """One command-processor cycle.  Extract one command line from command buffer, parse it, and execute it."""
    if (self.suspend != 0):
      self.suspend = self.suspend - 1
      if (self.suspend < 0):
        self.suspend = 0
      return 0
    cmdline = self.extract()
    if cmdline:
      self.parse(cmdline)
      self.dispatch()
    return 0

  def append (self, text):
    """Append to end of command buffer (process later)."""
    self.cmdbuf = self.cmdbuf + text
    return self.cmdbuf

  def prepend (self, text):
    """Prepend to beginning of command buffer (process sooner)."""
    self.cmdbuf = text + cmdbuf
    return self.cmdbuf

  def execute (self, text):
    """Bypass the command buffer (process now)."""
    self.parse(text)
    return self.dispatch()

  def add_command (self, name, cmd):
    """Register an extension command's function.  Easier to use a method in a class derived from cmdproc, but can also use standalone functions."""
    if (self.commands.has_key(name)):
      return 0
    else:
      self.commands[name] = cmd
    return 1

  def remove_command (self, name):
    """Remove extension command by name."""
    if (self.commands.has_key(name)):
      del self.commands[name]
    return 0

  def argc (self):
    """Argument Count - number of command-processor arguments parsed."""
    return len(self._argv)

  def argv (self, n):
    """Argument Vector - the parsed tokens (0 = command name, 1 = first argument, etc.)"""
    if ((n < 0) or (n > len(self._argv))):
        return None
    return self._argv[n]

  def args (self):
    """Argument String - the unparsed portion of the command after the command name."""
    return self._argstring

  def argj (self, n):
    """Argument Joined - concatenate arguments into one string, separating tokens with a space."""
    if (len(self._argv) > 1):
      return " ".join(self._argv[1:])
    return ""

  def dispatch (self):
    """The jump from dataspace to codespace."""
    cmdname = self.argv(0)
    if (self.commands.has_key(cmdname)):
      cmdfunc = self.commands[cmdname]
#      return apply(cmdfunc, ())
      if cmdfunc.__class__ == self.dispatch.__class__:
        cmdfunc()      #Method
      else:
        cmdfunc(self)  #Standalone command.
    return 0

  def extract (self):
    """Extract one line's worth of command from command buffer."""
    extract = []
    pstate = 'START'
    i = 0
    while (pstate != 'STOP'):
      if (i >= len(self.cmdbuf)):
        c = chr(0)
        pstate = 'STOP'
      else:
        c = self.cmdbuf[i]
      if (pstate == 'START') or (pstate == 'TOKEN'):
        if ((c == '\0') or (c == '\n') or (c == ';')):
          pstate = 'STOP'
        elif (c == '"'):
          pstate = 'QUOTE'
      elif (pstate == 'QUOTE'):
        if ((c == '\0') or (c == '\n')):
          pstate = 'STOP'
        elif (c == '"'):
          pstate = 'START'
      elif ((pstate == 'TOKENSEMICOMMENT') or (pstate == 'STOP')):
        pass
      if (pstate == 'STOP'):
        pass
      else:
        extract.append(c)
      i = i + 1
    retval = "".join(extract)
    self.cmdbuf = self.cmdbuf[i:]
#    print "Extracted: [%s]" % retval
    return retval

  def parse (self, line):
    """Parse line into tokens, stored in _argstring and _argv."""
#    print "Parsing [%s]" % line
    self._argstring = ""
    self._argv = []
    pstate = 'START'  #Parse state.
    start = 0
    stop = 0
    i = 0
    while (pstate != 'STOP'):
      if (i >= len(line)):
        c = chr(0)
      else:
        c = line[i]
#      print "in state [%s] with char %s" % (pstate, c)
      if (pstate == 'START'):
        if ((c == '\0') or (c == '\n') or (c == ';')):
          pstate = 'STOP'
        elif (c == '/'):
          pstate = 'SEMICOMMENT'
        elif (c == '"'):
          start = i + 1
          pstate = 'QUOTE'
        elif (ord(c) > 32):
          start = i
          pstate = 'TOKEN'
      elif (pstate == 'TOKEN'):
        if ((c == '\0') or (c == '\n') or (c == ';')):
          stop = i
          pstate = 'STOP'
        elif (c == '/'):
          pstate = 'TOKENSEMICOMMENT'
        elif (c == '"'):
          stop = i
          pstate = 'START'
          i = i - 1
        elif (ord(c) <= 32):
          stop = i
          pstate = 'START'
          i = i - 1
        #else, still in token.  keep counting.
      elif (pstate == 'QUOTE'):
        if ((c == '\0') or (c == '\n')):
          stop = i
          pstate = 'STOP'
        elif (c == '"'):
          stop = i
          pstate = 'START'
      elif (pstate == 'SEMICOMMENT'):
        if (c == '/'):
          pstate = 'COMMENT'
        else:
          i = i - 1
          pstate = 'TOKEN'
      elif (pstate == 'TOKENSEMICOMMENT'):
        if (c == '/'):
          stop = i - 1
          pstate = 'COMMENT'
        else:
          i = i - 1
          pstate = 'TOKEN'
      else:
        #Unknown parser state
        pass
      if (stop > start):
#        print "Copying %d:%d" % (start, stop)
        if (len(self._argv)):
          self._argstring = line[start:]
        self._argv.append(line[start:stop])
        stop = 0
      i = i + 1
      if (i > len(line)):
        pstate = 'STOP'
#    for i in xrange(0, len(self._argv)):
#      print "[%d] '%s'" % (i, self._argv[i])
    return len(self._argv)


  def cmd_wait (self):
    """Built-in command: wait <n> - suspend command-buffer processing for n cycles."""
    if self.argc() > 1:
      try:
        cycles = int(self.argv(1))
        self.suspend = self.suspend + cycles
      except:
        pass
    return 0

  def cmd_echo (self):
    """Built-in command: echo <s> - echo parameters to output device."""
    text = self.argj(1)
    self.putstr("%s\n" % text)
    return 0

  def cmd_crash (self):
    """Built-in command: crash - induce deliberate process crash (if 'quit' fails)."""
    self.putstr("Crashing...\n")
    sys.exit(0)

  def cmd_cmdlist (self):
    """Built-in command: cmdlist - list known commands."""
    n = 0
    for cmdname in self.commands.keys():
      self.putstr("%s\n" % cmdname)
      n = n + 1
    self.putstr("%d commands\n" % n)
    return n

  def cmd_exec (self):
    """Built-in command: exec <s> - load content of file as commands."""
    filename = self.argv(1)
    for namevariant in [ filename, "%s.cfg" % filename ]:
      try:
        cfgfile = open(namevariant, "rt")
        self.putstr("Execing %s\n" % namevariant)
        self.append("\n")
        for line in cfgfile:
          self.append(line)
        cfgfile.close()
        self.append("\n")  #Just for good measure.
        return 0
      except IOError:
        pass
    self.putstr("Could not open %s\n" % filename)
    return 0



def aliencmd (cmdproc):
  """Example foreign function (standalone form)."""
  cmdproc.putstr("I am a foreign function!\n")
  return 0

if __name__ == "__main__":
  """Test routine."""
  x = cmdproc()

#  n = x.parse('foo bar //baz')
#  print "=> %d" % n
#  for i in xrange(0, x.argc()):
#    print "[%d] = (%s)" % (i, x.argv(i))

#  x.append('foo bar baz; quux "micro lenat"')
#  x.parse(x.extract())
#  x.parse(x.extract())

#  x.execute("crash")
  x.add_command("alien", aliencmd)

  sys.stdout.write("]")
  sys.stdout.flush()
  s = sys.stdin.readline()
  while s:
    x.append(s)
    while (len(x.cmdbuf)):
      x.cycle()
    sys.stdout.write("]")
    sys.stdout.flush()
    s = sys.stdin.readline()
