#########################################################################
# 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: exported.py,v 1.18 2007/07/24 00:39:03 drexl Exp $
#########################################################################
"""
This is the X{API} for lyntin internals and is guaranteed to change 
very rarely even though we might change Lyntin's internals.  If
it does change it'll be between major Lyntin versions.

X{write_hook}::

   The write_hook allows you to persist data.  Functions that register
   with this hook should return a list of strings.

   For example, the alias plugin would register with this hook and if
   I had an alias vv, it would return:

      [ "alias {vv} {this is my alias}" ]

   as its data.  If quiet == 1, then it would return:

      [ "alias {vv} {this is my alias} quiet={true}" ]

   because "quiet" is an argument for the alias command which quells
   output when it's called (assuming the output isn't error output).

   Arg mapping: {"session": Session, "quiet": boolean }

   session - the session in question

   quiet - whether (1) or not (0) to append an argument that causes
           the command to quell input when it is later read in
"""
import sys, traceback
from lyntin import constants
from lyntin.ui import message

LAST = 99
FIRST = 1

myengine = None

def get_engine():
  """
  This is a helper function provided for backwards compatibility with
  older modules.
  """
  return myengine

class StopSpammingException(Exception):
  """
  This is what you raise when you're implementing a handler hook
  and you've handled the data.  This will cause the hook to stop
  iterating through the rest of the hook functions.
  """
  pass

class DoneSpammingException(Exception):
  def __init__(self, output):
    self.output = output

def lyntin_command(text, internal=0, session=None):
  """
  The best way of executing a Lyntin command as if the user had typed it.

  This executes "#help", doesn't create an entry in the history, and
  doesn't spam the from_user_hook::

    from lyntin.exported import lyntin_command
    lyntin_command("#help", internal=1, session=None)

  This executes "#action {killing blow} {reclaim}", creates an entry
  in the history, spams the from_user_hook, and does it in the session
  named "a"::

    from lyntin.exported import lyntin_command, get_session
    lyntin_command("#action {killing blow} {reclaim}", internal=0, session=get_session("a"))


  @param text: the command to execute.  ex. "#help"
  @type  text: string

  @param internal: whether (1) or not (0) to execute the line internally 
      suppressing history and the spamhook.
  @type  internal: boolean

  @param session: the session instance to execute this command in
      (defaults to the current session)
  @type  session: Session
  """
  if session is not None:
    myengine.handleUserData(text, internal, session)
  else:
    myengine.handleUserData(text, internal)


def add_command(cmd, func, arguments=None, argoptions=None, helptext=""):
  """
  This function allows you to add additional commands to Lyntin.
  Note, if you add a command that has the same name as an existing
  command, we'll first remove the existing command and then add
  the new command (this is done in the CommandManager).

  If you add a E{^} to the beginning of the command name, then the user
  has to type the entire command name for it to kick off.  For
  example, if I specified "E{^}end", then the user has to type "#end"
  to execute that command.  If I had specified "end", then the user
  could type "e", "en", or "end".

  If you don't specify helptext and the function has a doc_string,
  then we'll pull the helptext from the function's doc_string.

  This creates a basic command that has no arguments or argoptions
  and adds it to the CommandManager::

    import os
    from lyntin.exported import add_command

    ht = "This command does an os.system("who") to let you see " + \
         "who's online\\nright now."

    def who_cmd(ses, args, input):
      os.system("who")

    add_command("who", who_cmd, "", None, ht)


  Same thing, but the help text is in the doc_string of the command::

    import os
    from lyntin.exported import add_command

    def who_cmd(ses, args, input):
      \"\"\"
      This command does an os.system("who") to let you see who's
      online right now.
      \"\"\"
      os.system("who")

    add_command("who", who_cmd)


  @param cmd: the command name to add.  ex. "help"
  @type  cmd: string

  @param func: the function to call to handle the command
  @type  func: function

  @param arguments: the argument spec for building the ArgumentParser
  @type  arguments: string

  @param argoptions: the options to give the ArgumentParser to tell it
      how to parse the arguments
  @type  argoptions: string

  @param helptext: the helptext associated with this command to give to
      the HelpManager
  @type  helptext: string
  """
  get_manager("command").addCommand(cmd, func, arguments, argoptions, helptext)

def remove_command(text):
  """
  Removes a command from Lyntin.

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

  @return: 0 if no command was found, 1 if the command was removed
      successfully
  @rtype: boolean
  """
  return get_manager("command").removeCommand(text)

def get_commands():
  """
  Returns a list of the existing commands in Lyntin.

  @return: the list of commands currently registered with Lyntin
  @rtype: list of strings
  """
  return get_manager("command").getCommands()

def add_manager(name, mgr):
  """
  Registers a manager with the engine.

  example of usage::

     from lyntin.exported import add_manager
     from lyntin import manager

     class MyManager(manager.Manager):
       def __init__(self):
         pass

     add_manager("mymanager", MyManager)


  Managers are pretty straightforward especially considering that
  Lyntin makes great use of them so there are lots of examples.

  @param name: the name of the manager to register with Lyntin
  @type  name: string

  @param mgr: the manager instance being registered
  @type  mgr: manager.Manager
  """
  myengine.addManager(name, mgr)

def remove_manager(name):
  """
  Removes a manager from the engine.

  @param name: the name of the manager to remove from Lyntin
  @type  name: string

  @return: 0 if nothing happened, 1 if the manager was removed
  @rtype: boolean
  """
  return myengine.removeManager(name)

def get_manager(name):
  """
  Retrieves a manager from the engine.

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

  @return: the manager instance
  @rtype: manager.Manager
  """
  return myengine.getManager(name)

def get_config(name, ses=None, defaultvalue=constants.NODEFAULTVALUE):
  """
  Gets a value for a config item.  If the default value is
  not specified, then it will raise a ValueError.

  This gets the snoopdefault config value.  Since we're not
  specifying a session, it'll get the global one::

     from lyntin.exported import get_config

     snoopdefault = get_config("snoopdefault", defaultvalue=0)


  This gets the ignoreactions setting for the session named "a"::

     from lyntin.exported import get_config, get_session

     ia = get_config("ignoreactions", get_session("a"), defaultvalue=0)


  @param name: the name of the item to retrieve the value of
  @type  name: string

  @param ses: the session (or None if this is not session-scoped)
  @type  ses: Session
   
  @param defaultvalue: the value to return if there is no config
      item of that name.  if you don't specify this, then we'll
      raise a ValueError.
  @type  defaultvalue: varies
  """
  return myengine.getConfigManager().get(name, ses, defaultvalue)

def add_config(name, configitem, ses=None):
  """
  Adds a new configuration item.  Configuration items allow you to
  present the user with options that they can change which control
  the behavior of your module.  Examples of this abound in Lyntin.

  Here we create a boolean config item to control whether we're
  ignoring actions or not for the session named "a"::

    from lyntin import config
    from lyntin.exported import add_config

    tc = config.BoolConfig("ignoreactions", 0, 1, "Allows you to turn off action handling")
    add_config("ignoreactions", tc, get_session("a"))
   

  @param name: the name of the item
  @type  name: string

  @param configitem: the configuration item to add
  @type  configitem: ConfigBase

  @param ses: if this item is session based, then this is the session
      to associate the item with
  @type  ses: Session

  @raises ValueError: if there is already an item with that name for
      that session
  """
  myengine.getConfigManager().add(name, configitem, ses)

def remove_config(name, ses=None):
  """
  Allows you to remove a configuration item from the system.

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

  @param ses: the session from which to remove the item (None if
      it's a general Lyntin item)
  @type  ses: Session

  @raises ValueError: if the item does not exist
  """
  myengine.getConfigManager().remove(name, ses)

def add_help(fqn, helptext):
  """
  Adds a help topic to the structure.  See the helpmanager documentation
  for more details as to what the helptext should look like.

  Note: If you're building commands, the add_command function allows you
  to pass in help text for the command.  If you don't pass in help text
  and the command function has a doc_string, then we'll try to 
  extract the help text from the doc_string.

  @param fqn: a . delmited string of categories ending
      with a help name
  @type  fqn: string

  @param helptext: the help text
  @type  helptext: string

  @return: the fqn of where the help topic was stored (you can place
      category overrides in the helptext)
  @rtype: string
  """
  return get_manager("help").addHelp(fqn, helptext)

def remove_help(fqn):
  """
  Removes a help topic from Lyntin.

  @param fqn: a . delmited string of categories ending
      with a help name
  @type  fqn: string
  """
  get_manager("help").removeHelp(fqn)

def get_help(fqn):
  """
  Retrieves a help topic via a fully qualified name.

  @param fqn: a . delimited string of categories ending
      with a help name
  @type  fqn: string

  @return: the help topic of that name or None if the topic
      doesn't exist
  @rtype: string
  """
  return get_manager("help").getHelp(fqn)

def get_version():
  """
  Returns Lyntin's version number as a tuple.  For example, if
  this were Lyntin version 4.0, the tuple would be (4, 0, 0).
  If this were Lyntin version 55.3.2 the tuple would be
  (55, 3, 2).

  @return: the version number of this installation of Lyntin
  @rtype: tuple of (int, int, int)
  """
  import lyntin.__init__
  return lyntin.__init__.__version_tuple__

def expand_ses_vars(text, ses):
  """
  Grabs the variable manager (which we're hoping is using the
  same expand_vars as what's registered--only time will tell)
  and expands variables using the variable manager and its
  varmap.

  @param text: the text to expand variables in
  @type  text: string

  @param ses: the session object to pass to the VariableManager
  @type  ses: session.Session

  @return: the text with variables expanded
  @rtype: string
  """
  vm = get_manager("variable")
  if vm:
    return vm.expand(ses, text)
  return text

def get_session(name):
  """
  Returns a named session or None if the session doesn't exist.

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

  @return: the session instance or None
  @rtype: session.Session
  """
  return myengine.getSession(name)

def get_active_sessions():
  """
  Returns a list of the active sessions including the common one.

  @return: the list of active sessions
  @rtype: list of session.Session's
  """
  return list(myengine._sessions.values())

def get_current_session():
  """
  Returns the current session.

  @return: the current session
  @rtype: session.Session
  """
  return myengine._current_session

def set_current_session(ses):
  """
  Changes the current session to another session.

  @param ses: the session instance to set the current session to
  @type  ses: session.Session
  """
  myengine.set_current_session(ses)
    
def get_num_errors():
  """
  Returns the total number of errors Lyntin has had thus far.

  @return: the number of unhandled errors Lyntin has encountered so far
  @rtype: int
  """
  return myengine._errorcount
 
def set_num_errors(num):
  """
  Sets the number of errors Lyntin has had thus far.  Do be careful
  when setting this because Lyntin keeps track of errors for the 
  purposes of shutting down the client in case we get into a runaway
  exception loop.

  @param num: the number of errors to set
  @type  num: int
  """
  myengine._errorcount = num

def write_ui(text):
  """
  Calls engine.myengine.writeUI which writes a message to the ui.
  If there is no engine instance available, it prints it to sysout.

  @param text: the message to write to the ui
  @type  text: string or ui.Message
  """
  if myengine:
    myengine.writeUI(text)
  else:
    print(text)

def write_message(text, ses=None, **hints):
  """
  Calls engine.myengine.writeMessage which writes LTDATA message.
  If there is no engine instance available, it prints it to sysout.

  @param text: the message to send
  @type  text: string

  @param ses: the session instance the error data is associated with
  @type  ses: session.Session
  """
  text = str(text)
  if myengine:
    myengine.writeUI(message.Message(text + "\n", message.LTDATA, ses, **hints))
  else:
    print(("message:", text))

def write_error(text, ses=None):
  """
  Calls engine.myengine.writeError which writes ERROR message.
  If there is no engine instance available, it prints it to sysout.

  @param text: the message to send
  @type  text: string

  @param ses: the session instance the error data is associated with
  @type  ses: session.Session
  """
  text = str(text)
  if myengine:
    myengine.writeUI(message.Message(text + "\n", message.ERROR, ses))
  else:
    print(("error:", text))

def write_user_data(text, ses=None):
  """
  Calls engine.myengine.writeUserData which writes a USERDATA message.
  If there is no engine instance available, it prints it to sysout.

  @param text: the message to send
  @type  text: string

  @param ses: the session instance the user data is associated with
  @type  ses: session.Session
  """
  text = str(text)
  if myengine:
    myengine.writeUI(message.Message(text + "\n", message.USERDATA, ses))
  else:
    print(("userdata:", text))

def write_mud_data(text, ses=None):
  """
  Calls engine.myengine.writeMudData which writes a MUDDATA message.
  If there is no engine instance available, it prints it to sysout.

  @param text: the message to send
  @type  text: string

  @param ses: the session instance the mud data is associated with
  @type  ses: session.Session
  """
  text = str(text)
  if myengine:
    myengine.writeUI(message.Message(text, message.MUDDATA, ses))
  else:
    print(("muddata:", text))

def write_traceback(message="", ses=None):
  """
  Convenience method for grabbing the traceback, formatting it, 
  piping it through write_error, with a message for the user.

  @param message: any message you want to pass to the user--this
      gets printed first
  @type  message: string

  @param ses: the session instance the mud data is associated with
  @type  ses: session.Session
  """
  exc = "".join(traceback.format_exception(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2]))

  if message:
    message = message + "\n" + exc
  else:
    message = exc
  write_error(message, ses)


def get_history(count=30):
  """
  Retrieves the history as a oldest to youngest list of strings.

  @param count: the number of lines to return
  @type  count: int

  @return: the history oldest to youngest
  @rtype: list of strings
  """
  return get_manager("history").getHistory(count)

def tally_error():
  """
  This adds one to the current error count and checks to see
  if we're over our limit.  If we are, it enqueues a shutdown
  event which will shutdown Lyntin.
  """
  myengine.tallyError()

def get_hook(hookname):
  """
  If the hook exists, returns the hook.  Otherwise it creates
  a new hook and returns that.

  @param hookname: the name of the hook to retrieve
  @type  hookname: string

  @returns: the Hook by the name of hookname
  @rtype: Hook
  """
  return myengine.getHook(hookname)

def hook_register(hookname, func, place=constants.LAST):
  """
  Registers a function with a hook.

  @param hookname: the name of the hook
  @type  hookname: string

  @param func: the function to register with the hook
  @type  func: function

  @param place: the function will get this place in the call
      order.  functions with the same place specified will get
      arbitrary ordering.  defaults to constants.LAST.
  @type  place: int
  """
  myengine.hookRegister(hookname, func, place)

def hook_unregister(hookname, func):
  """
  If the hook exists, unregisters the func from the hook.

  @param hookname: the name of the hook
  @type  hookname: string

  @param func: the function to remove from the hook
  @type  func: function
  """
  if hookname in myengine._hooks:
    myengine._hooks[hookname].remove(func)

def hook_spam(hookname, argmap={}, mappingfunc=lambda x,y:x, 
      emptyfunc=lambda x:x, donefunc=lambda x:x):
  """
  Sends out input to all the registrants of a hook.

  @param hookname: the name of the hook to spam
  @type  hookname: string

  @param argmap: the map of arguments that gets passed to
      each function in the hook.  the actual arguments differs
      from hook to hook.
  @type  argmap: dict of arguments

  @param mappingfunc: function whose output will be passed to the next
      function in the hook.  Must take two arguments: the previous 
      arglist and the return from the previous function.
  @type  mappingfunc: function

  @param emptyfunc: Function to be called with arglist if there are no
      objects registered with this hook.  Must take 1 argument, the arglist
      tuple, and return what spamhook should return.
  @type  emptyfunc: function

  @param donefunc: Function to be called when spamming finishes normally.
      Should take 1 argument and return what spamhook should return.
  @type  donefunc: function
        
  @return: argmap
  @rtype:  map of output arguments
  """
  hooklist = get_hook(hookname).getList()
  try:
    if hooklist:
      for mem in hooklist:
        output = mem(argmap)
        argmap = mappingfunc(argmap, output)
    else:
      argmap = emptyfunc(argmap)
  except StopSpammingException as e:
    return e
  except DoneSpammingException as d:
    return d.output

  return donefunc(argmap)

def filter_mapper_hook_spam(hookname, argmap=None, emptyfunc=lambda x:x,
    donefunc=lambda x:x):
  """
  This is a slightly optimized filter_mapper hook because it's used so
  often in the system.  It incorporates the filter_mapper, but skips
  any exception handling.

  Arguments correspond to hook_spam.
  """
  if argmap is None:
    argmap = {}

  hooklist = get_hook(hookname).getList()
  if hooklist:
    for mem in hooklist:
      output = mem(argmap)
      if output is None:
        return None

      argmap["dataadj"] = output
  else:
    argmap = emptyfunc(argmap)

  return donefunc(argmap)

def filter_mapper(x, y):
  """
  This is the mapping function to use for filter-style hooks.  
  Spamhook should be called as:

    1. spamargs = {"session": ses, "data": data, "dataadj": data... }
    2. spamargs = exported.hook_spam(... spamargs ...)
    3. output = spamargs["dataadj"]

  Each filter function will get a map with at least the following keys when 
  it is called:

    - session - the session
    - data - the original data
    - dataadj - the adjusted data
  """
  if y is not None:
    x["dataadj"] = y
    return x
  else:
    raise StopSpammingException

def query_mapper(x, y):
  """
  This is the mapping function to be used for query-style hooks.
  Spamhook should be called as:

    1. output = hook.spamhook( arguments )

  Each hook function will be called with the arguments until one function
  returns non-None.  That non-None value will be returned from spamhook
  """
  if y is not None:
    raise DoneSpammingException(y)
  else:
    return x

def query_done(x):
  """
  This is the done hook function to go with the query mapper for proper 
  behaviour.
  """
  return None


def get_write_data(ses, quiet=0):
  """
  Calls the write_hook and retrieves data from all the functions that
  have registered with the hook.  It passes in the session involved,
  and also the "quiet" argument.

  For example, the alias module might have one alias for session a.  It
  would return::

    [ "alias {vv %1} {evoke %1}" ]

  If quiet was 1, then it would return::

    [ "alias {vv %1} {evoke %1} quiet={true}" ]

  Then this method would take all those lists of strings and generate
  one list of all the strings and return it.

  Exceptions kicked up by poorly written plugins (and bad Lyntin code)
  will get percolated upwards.  i.e. if exceptions are raised, then you
  won't get any return data.

  @param ses: the session to save the data for
  @type  ses: Session

  @param quiet: whether or not to be quiet--this is for when the data
      that is written is eventually read in.  0 if not quiet, 1 if quiet.
  @type  quiet: boolean

  @returns: data as a list of strings
  @rtype: list of strings
  """
  data = []
  def write_mapper(x, y):
    """
    Takes the data from x and sticks it into y so that we continue
    it all the way through.
    """
    data.append(y)
    return x

  hook_spam("write_hook", {"session": ses, "quiet": quiet}, mappingfunc=write_mapper)

  listing = []
  if data:
    for mem in data:
      listing = listing + mem

  return listing


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