#########################################################################
# This file is part of Lyntin.
#
# Lyntin is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Lyntin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# copyright (c) Free Software Foundation 2001-2007
#
# $Id: commandmanager.py,v 1.8 2007/07/24 00:39:03 willhelm Exp $
#########################################################################
"""
Lyntin comes with a series of X{command}s for manipulating aliases, 
actions, highlights, and various other things.  Both the commands and 
the managers that hold the data are all defined in various Lyntin 
modules in the "modules/" subdirectory and are loaded up at Lyntin 
startup.

Lyntin has an extremely powerful argument parser that allows Lyntin
module writers to worry about the functionality of their command without
having to deal with parsing out the arguments, type-checking, 
raising exceptions with command syntax help, and data conversion.

Commands are stored internally in the "CommandManager".  They are
global to Lyntin--there are no commands that only exist in the session
they were declared in.  Commands, however, are executed in a specific
session.  When they are executed, they are given three arguments:
the session object for the session they were executed in, an argument
dictionary from the argument parser, and the exact input the user
typed.

Look at the modules/lyntincmds.py and modules/tintincmds.py modules for 
command examples.  Additionally, check out the Lyntin module repository
on http://lyntin.sourceforge.net/ for more examples.

X{default_resolver_hook}::

   Allows you to fill in values for arguments the user didn't specify 
   on the command line.  

   Arg mapping: {"session": Session, "commandname", string}

   session - the session this command was executed in.

   commandname - the name of the command that was executed

"""
import inspect, re
from lyntin import manager, exported, argparser, utils

class _CommandData:
  """
  Holds data relating to a command.  It's a helper class.
  """
  def __init__(self):
    self._name = ""
    self._func = None
    self._argparser = None
    self._fqn = ""

  def __repr__(self): return self._name
  def __str__(self): return self._name

  def setName(self, name): self._name = name
  def getName(self): return self._name
  def setNameAdjusted(self, name): self._name_adjusted = name
  def getNameAdjusted(self): return self._name_adjusted
  def setFunc(self, func): self._func = func
  def getFunc(self): return self._func
  def setArgParser(self, ap): self._argparser = ap
  def getArgParser(self): return self._argparser
  def setFQN(self, fqn): self._fqn = fqn
  def getFQN(self): return self._fqn

class CommandManager(manager.Manager):
  """ 
  The CommandManager holds a series of _CommandData objects
  and methods to manipulate and use them.  Lyntin developers
  can add their own commands to Lyntin.

  There should be one instance of the CommandManager and
  the engine should have it.  All CommandManager interaction
  should be done through the exported module.
  """
  def __init__(self, e):
    self._commands = {}
    self._engine = e

  def getCommands(self):
    """
    Returns a list of the commands we have registered.

    @return: all the commands that have been registered
    @rtype:  list of strings
    """
    return list(self._commands.keys())

  def addCommand(self, name, func, arguments=None, argoptions=None, helptext=""):
    """
    Registers a command.

    @param name: the name of the command to add
    @type  name: string

    @param func: the function that handles the command
    @type  func: function

    @param arguments: the argument spec to create the argparser
    @type  arguments: string

    @param argoptions: options for how the argument spec should be parsed
    @type  argoptions: string

    @param helptext: the help text for this command for the user
    @type  helptext: string

    @raise ValueError: if the function is uncallable
    @raise Exception: if the argument spec for the command is unparseable
    """
    if not callable(func):
      raise ValueError("%s is uncallable." % name)
    cd = _CommandData()

    syntaxline = ""

    # try to figure out the arguments and syntax line stuff
    if arguments is not None:
      try:
        cd.setName(name)
        cd.setArgParser(argparser.ArgumentParser(arguments, argoptions))
        syntaxline = utils.wrap_text(cd.getArgParser().syntaxline, 60, 6)
      except Exception as e:
        raise Exception("Error with arguments for command %s, (%s)" % (name,e))

    cd.setFunc(func)

    # removeCommand tests to see if the command exists already and will
    # remove it if it does.
    self.removeCommand(name)

    # toss the command thing in the dict
    self._commands[name] = cd

    # deal with the help text
    if not helptext:
      if func.__doc__:
        helptext = inspect.getdoc(func)
      else:
        helptext = "\nThis command has no help."

    if name.startswith("^"):
      cd.setNameAdjusted(name[1:])
    else:
      cd.setNameAdjusted(name)

    if syntaxline:
      commandchar = self._engine.getConfigManager().get("commandchar")
      helptext = ("syntax: %s%s %s\n" % 
             (commandchar, cd.getNameAdjusted(), syntaxline) + helptext)

    fqn = exported.add_help(cd.getNameAdjusted(), helptext)
    cd.setFQN(fqn)
        
  def removeCommand(self, name):
    """
    Removes a command (and the help text) for whatever reasons.

    @param name: the name of the command to remove
    @type  name: string

    @return: 0 if no command was found, 1 if the command was removed
        succesfully.
    @rtype: boolean
    """
    if name in self._commands:
      cd = self._commands[name]
      del self._commands[name]
      try:
        exported.remove_help(cd.getFQN())
      except:
        pass
      return 1
    return 0

  def getCommand(self, name):
    """
    Returns the function for a given command name.

    @param name: the name of the command to retrieve
    @type  name: string

    @return: the function in question or None
    @rtype:  function
    """
    if name in self._commands:
      return self._commands[name].getFunc()

    if ("^" + name) in self._commands:
      return self._commands["^" + name].getFunc()

    # this is kind of a kluge to handle the #@ arbitrary
    # python stuff so that it can be in its own module.
    if name.startswith("@") and "@" in self._commands:
      return self._commands["@"].getFunc()

    return None

  def getArgParser(self, name):
    """
    Returns the arguments parser for a given command name.

    @param name: the name of the command whose arguments should be
        retrieved
    @type  name: string

    @return: argument parsing object to convert incoming arguments
        into a dictionary to pass to the command function
    @rtype: ArgParser instance
    """
    if name in self._commands:
      return self._commands[name].getArgParser()

    return None

  def filter(self, args):
    """
    Takes in user command lines and handles commands that start
    with a Lyntin command character.

    @param args: (session, internal boolean, ..., current input text)
    @type  args: tuple

    @return: None if we handled the input, or the current input text if 
        we didn't
    @rtype:  None or string
    """
    ses = args["session"]
    internal = args["internal"]
    input = args["dataadj"]

    commandchar = self._engine.getConfigManager().get("commandchar")
    if len(input) > 1 and input.startswith(commandchar):
      input = input[1:]

      # splits out the command name from the rest of the command line
      words = input.split(" ",1)

      # We want an empty argument list if there was one, don't want
      # array out-of-bounds issues       
      if len(words) < 2: words.append("")

      # this checks to see if it's a special #@ command.
      if input.startswith("@"):
        self.getCommand("@")(ses, input.split(" "), input)
        if internal==0: ses.prompt()
        return

      # this finds the first matching command and ends there.
      commands = self.getCommands()
      for mem in commands:
        command = None
        if mem.startswith("^"):
          if re.compile(mem).search(words[0]):
            command = self.getCommand(mem)
        else:
          if mem.startswith(words[0]):
            command = self.getCommand(mem)

        if command:
          argumentparser = self.getArgParser(mem)
          if argumentparser is None:
            command(ses, input.split(" "), input)
          else:
            # for printing out the error message, we remove the ^
            # from the command name if it's there.
            fixedmem = mem
            if len(fixedmem) > 0 and fixedmem.startswith("^"):
              fixedmem = fixedmem[1:]

            resolver = exported.hook_spam("default_resolver_hook", 
                                {"session": ses, "commandname": mem}, 
                                mappingfunc=exported.query_mapper, 
                                donefunc=exported.query_done)

            try:
              argdict = argumentparser.parse(words[1], resolver)
              argdict["command"]=mem
              command(ses, argdict, input)
            except ValueError as e:
              exported.write_error("%s: %s\nsyntax: %s%s %s" % 
                                   (fixedmem, e, commandchar, fixedmem,
                                    argumentparser.syntaxline))
            except argparser.ParserException as e:
              exported.write_error("%s: %s\nsyntax: %s%s %s" % 
                                   (fixedmem, e, commandchar, fixedmem,
                                    argumentparser.syntaxline))
          if internal == 0:
            ses.prompt()
          break

      else:
        if internal == 0:
          ses.prompt()
        exported.write_error("Not a valid command: %s" % (words[0]))
      return
    return args["dataadj"]

# Local variables:
# mode:python
# py-indent-offset:2
# tab-width:2
# End:
