#########################################################################
# 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: speedwalk.py,v 1.8 2007/07/24 00:39:03 willhelm Exp $
#########################################################################
"""
This module defines the speedwalking code.  Speedwalking is highly
configurable and it's actually less like speedwalking in other mudclients
and more like alias shorthand that allows you to quickly do things
a number of times.

First you want to create maps from a character or characters to
an expansion using #swdir.  Then every instance of this character/characters
gets expanded to the expansion.

For example::

  #swdir {n} {north}
  #swdir {s} {south}

"nnsss" will expand to "north;north;south;south;south".

Similarly, "2n3s" will expand to "north;north;south;sout;south".

To handle instances where certain combinations will get expanded, but we
really don't want them to be, we use #swexclude::

  #swexclude {news}
"""

# Originally written 2002 by Sebastian John

import re, string
from lyntin import manager, utils, exported
from lyntin.modules import modutils

class SpeedwalkHash:
  def __init__(self):
    self._dirs = {}
    self.compileRegexp()
    self._excludes = []
  
  def __copy__(self):
    sm = SpeedwalkManager()
    for mem in list(self._dirs.keys()):
      sm.addDir(mem, self._dirs[mem])

    for mem in self._excludes:
      sm.addExclude(mem)
    return sm

  def clearDirs(self):
    """
    Clears all stored speedwalking dirs from the manager.
    """
    self._dirs = {}
    self.compileRegexp()
  
  def addDir(self, alias, dir):
    """
    Adds a speedwalking direction alias to the manager.
    
    @param alias: the speedwalking alias
    @type  alias: string

    @param dir: the expansion for the speedwalking alias
    @type  dir: string

    @raises ValueError: if there exists another alias where the dir is a
        substring.  for example if dir was "n" and there was an alias "ln",
        that would raise a ValueError.
    """
    for mem in list(self._dirs.keys()):
      if mem.find(dir) != -1:
        raise ValueError("possible ambiguity")
    self._dirs[alias] = dir
    self.compileRegexp()
  
  def removeDirs(self, alias):
    """
    Removes the speedwalking alias and only this one (no wildcard patterns
    are possible).
    
    @param alias: speedwalking aliases that match the alias will be removed
    @type  alias: string

    @returns: a list of (alias, dir) tuples of removed speedwalking aliases.
    @rtype: list of (string, string)
    """
    try:
      dir = self._dirs[alias]
      del self._dirs[alias]
    except KeyError:
      return []
    else:
      self.compileRegexp()
      return [(alias, dir)]
  
  def getDirs(self):
    """
    Returns a list of all the speedwalking aliases currently defined.
    
    @returns: the list of (alias, dir) tuples consisting of all the
        speedwalking aliases we're managing
    @rtype: list of (string, string)
    """
    dirs = list(self._dirs.items())
    dirs.sort()
    return dirs
  
  def getDirsInfo(self, text=""):
    """
    Returns information about the speedwalking aliases in here.
    
    This is used by #swdir to tell all the speedwalking aliases involved as
    well as #write which takes this information and dumps it to the file.
    
    @param text: the text to expand on to find aliases that the user is
        interested in
    @type  text: string
    
    @returns: list of strings where each string represents a speedwalk alias
    @rtype: list of strings
    """
    listing = list(self._dirs.keys())
    if text:
      listing = utils.expand_text(text, listing)
    
    listing = ["swdir {%s} {%s}" % (mem, self._dirs[mem]) for mem in listing]
    
    return listing

  def getDirsInfoMappings(self):
    l = []
    for mem in list(self._dirs.keys()):
      l.append( { "alias": mem, "dir": self._dirs[mem] } )

    return l
  
  def getDirStatus(self):
    """
    Returns a one liner about dirs.

    @returns: a one-line about the dirs managed.
    @rtype: string
    """
    return "%d dir(s)." % len(self._dirs)
  
  def compileRegexp(self):
    """
    Compiles the actual speedwalking pattern.
    Also maintains self._aliases the default excludes.
    """
    if self._dirs:
      keys = "|".join(list(self._dirs.keys()))
      regexp = "^(\\d*(%s))+$" % (keys)
      self._regexp = re.compile(regexp)
      self._aliases = list(self._dirs.values())
      self._dirs_available = list(self._dirs.keys())
    else:
      self._regexp = None
      self._aliases = []
      self._dirs_available = []
  
  def clearExcludes(self):
    """
    Clears the list of excludes (things we don't want to expand speedwalking
    on).
    """
    self._excludes = []
  
  def addExclude(self, exclude):
    """
    Adds a speedwalking exclude to the manager.
    
    @param exclude: the exclude to add
    @type  exclude: string
    """
    if exclude not in self._excludes:
      self._excludes.append(exclude)
  
  def removeExcludes(self, exclude):
    """
    Removes a speedwalking exclude (and only one, no wildcards or the like)
    from the manager.
    
    @param exclude: the exclude to remove (we don't accept wildcards here)
    @type  exclude: string

    @returns: list of the excludes removed
    @rtype: list of strings
    """
    badexcludes = utils.expand_text(exclude, self._excludes)

    for mem in badexcludes:
      self._excludes.remove(mem)

    return badexcludes
  
  def getExcludes(self):
    """
    Returns the exclude list we are managing.
    
    @return: the sorted list of excludes being managed
    @rtype: list of strings
    """
    self._excludes.sort()
    return self._excludes
  
  def getExcludesInfo(self, text=""):
    """
    Returns information about the speedwalking excludes in here.
    
    This is used by #swexcl to tell all the excludes involved as well as
    #write which takes this information and dumps it to the file.
    
    @param text: the text to expand on to find excludes that the user
        is interested in
    @type  text: string

    @returns: list of strings where each string represents an exclude
    @rtype: list of strings
    """
    listing = self._excludes
    if text:
      listing = utils.expand_text(text, listing)
    
    listing = ["swexclude {%s}" % mem for mem in listing]
    
    return listing
  
  def getExcludesInfoMappings(self):
    l = []
    for mem in self._excludes:
      l.append( { "exclude": mem } )
    return l

  def getExcludeStatus(self):
    """
    Returns a one-line string describing how many excludes we have.

    @returns: a one liner describing how many excludes we have
    @rtype: string
    """
    return "%d exclude(s)." % len(self._excludes)

  def clear(self):
    """
    Clears both speedwalking dir aliases and excludes.
    """
    self.clearDirs()
    self.clearExcludes()
  

class SpeedwalkManager(manager.Manager):
  def __init__(self):
    self._hashes = {}

  def clearDirs(self, ses):
    if ses in self._hashes:
      self._hashes[ses].clearDirs()

  def addDir(self, ses, alias, dir):
    if ses not in self._hashes:
      self._hashes[ses] = SpeedwalkHash()
    self._hashes[ses].addDir(alias, dir)

  def removeDirs(self, ses, alias):
    if ses in self._hashes:
      return self._hashes[ses].removeDirs(alias)
    return []

  def getDirs(self, ses):
    if ses in self._hashes:
      return self._hashes[ses].getDirs()
    return []

  def getDirsInfo(self, ses, text=""):
    if ses in self._hashes:
      return self._hashes[ses].getDirsInfo(text)
    return []

  def getStatus(self, ses):
    if ses in self._hashes:
      sdata = self._hashes[ses]
      return "%s %s" % (sdata.getDirStatus(), sdata.getExcludeStatus())
    return "0 dir(s). 0 exclude(s)."

  def addExclude(self, ses, exclude):
    if ses not in self._hashes:
      self._hashes[ses] = SpeedwalkHash()
    self._hashes[ses].addExclude(exclude)

  def removeExcludes(self, ses, exclude):
    if ses in self._hashes:
      return self._hashes[ses].removeExcludes(exclude)
    return []

  def getExcludesInfo(self, ses):
    if ses in self._hashes:
      return self._hashes[ses].getExcludesInfo()
    return []

  def getItems(self):
    return ["swdir", "swexclude"]

  def getParameters(self, item):
    if item == "swdir":
      return [ ("alias", "The speedwalk alias."),
               ("dir", "The speedwalk direction expansion.") ]
    if item == "swexclude":
      return [ ("exclude", "The word to exclude from swdir expansion.") ]

    raise ValueError("%s is not a valid item for this manager." % item)

  def getInfoMappints(self, item, ses):
    if item not in ["swexclude", "swdir"]:
      raise ValueError("%s is not a valid item for this manager." % item)

    if ses not in self._hashes:
      return []

    if item == "swdir":
      return self._hashes.getDirsInfoMappings()
    
    return self._hashes.getExcludesInfoMappings()

  def getInfo(self, ses):
    if ses in self._hashes:
      myhash = self._hashes[ses]
      return myhash.getDirsInfo() + myhash.getExcludesInfo() 
    return []

  def clear(self, ses):
    if ses in self._hashes:
      self._hashes[ses].clear()
  
  def addSession(self, newsession, basesession=None):
    if basesession:
      if basesession in self._hashes:
        sdata = self._hashes[basesession]
        for mem in list(sdata._dirs.keys()):
          self.addDir(newsession, mem, sdata._dirs[mem])

        for mem in sdata._excludes:
          self.addExclude(newsession, mem)

  def removeSession(self, ses):
    if ses in self._hashes:
      del self._hashes[ses]

  def persist(self, args):
    """
    write_hook function for persisting the state of our session.
    """
    ses = args["session"]
    quiet = args["quiet"]

    data = self.getInfo(ses)
    if quiet:
      data = [m + " quiet={true}" for m in data]

    return data

  def userfilter(self, args):
    """
    user_filter_hook function to check for speedwalking expansion.
    """
    ses = args["session"]
    internal = args["internal"]
    verbatim = args["verbatim"]
    text = args["dataadj"]
    
    if ses not in self._hashes or exported.get_config("speedwalk", ses) == 0 or verbatim == 1:
      return text

    sdata = self._hashes[ses]

    if not sdata._dirs or not sdata._regexp or text in sdata._excludes \
        or text in sdata._aliases or not sdata._regexp.match(text):
      return text
    
    swdirs = []
    dir = num = ""
    for char in text:
      if char.isdigit():
        num = num + char
      else:
        dir = dir + char
        if dir in sdata._dirs_available:
          if num: count = int(num)
          else: count = 1
          for i in range(count):
            swdirs.append(sdata._dirs[dir])
          dir = num = ""
    
    output = ";".join(swdirs)
    if output == text:
      return text
    else:
      # anything that gets recursed on should be recursed internally
      exported.lyntin_command(output, 1, ses)
      return None


commands_dict = {}

def swdir_cmd(ses, args, input):
  """
  This adds speedwalking aliases and tells you the current speedwalking dirs
  already registered.

  examples:
    #swdir {n} {north}
    #swdir {s} {south}
    #swdir {e} {east}
    #swdir {w} {west}
    #swdir {NE} {northeast}
    #swdir {l} {look}
    ...

  This allows you to string characters together to speedwalk:

    4e2sNE

  which using the above swdirs gets expanded to 
  "east;east;east;east;south;south;northeast" and who wants to type all 
  that?

  see also: swexclude

  category: commands
  """
  # originally written by Sebastian John
  alias = args["alias"]
  dir = args["dir"]
  quiet = args["quiet"]

  # they typed '#swdir dd*' and are looking for matching speedwalking dirs
  if not dir:
    data = exported.get_manager("speedwalk").getDirsInfo(ses, alias)
    if not data:
      data = ["swdir: no speedwalking dirs defined."]

    exported.write_message("swdirs:\n" + "\n".join(data), ses)
    return

  try:
    exported.get_manager("speedwalk").addDir(ses, alias, dir)
    if not quiet:
      exported.write_message("swdir: {%s} {%s} added." % (alias, dir), ses)
  except ValueError as e:
    exported.write_error("swdir: cannot add alias '%s': %s." % (alias, e), ses)

commands_dict["swdir"] = (swdir_cmd, "alias= dir= quiet:boolean=false")


def swexclude_cmd(ses, args, input):
  """
  Adds words that should be excluded from speedwalk expansion as well
  as tells you which words are currently being excluded.

  If you had swdirs "n", "e", "s", and "w", you might want to create
  excludes for the words "sense", "news", "sew", ...  Which are real
  words that you most likely don't want to be expanded.

  examples:
    #swexclude {end}
    #swexclude {news}

  see also: swdir

  category: commands
  """
  # originally written by Sebastian John
  excludes = args["exclude"]
  quiet = args["quiet"]

  # they typed '#swexclude'--print out all current speedwalking excludes
  if len(excludes) == 0:
    data = exported.get_manager("speedwalk").getExcludesInfo(ses)
    if not data:
      data = ["swexcl: no speedwalking excludes defined."]

    exported.write_message("swexcludes:\n" + "\n".join(data), ses)
    return

  for exclude in excludes:
    exported.get_manager("speedwalk").addExclude(ses, exclude)
    if not quiet:
      exported.write_message("swexclude: {%s} added." % exclude, ses)

commands_dict["swexclude"] = (swexclude_cmd, "exclude* quiet:boolean=false")


def unswdir_cmd(ses, args, input):
  """
  Allows you to remove swdirs.

  category: commands
  """
  func = exported.get_manager("speedwalk").removeDirs
  modutils.unsomething_helper(args, func, ses, "swdir", "swdirs")

commands_dict["unswdir"] = (unswdir_cmd, "str= quiet:boolean=false")


def unswexclude_cmd(ses, args, input):
  """
  Allows you to remove swexcludes.

  category: commands
  """
  func = exported.get_manager("speedwalk").removeExcludes
  modutils.unsomething_helper(args, func, ses, "swexclude", "swexcludes")

commands_dict["unswexclude"] = (unswexclude_cmd, "str= quiet:boolean=false")


sm = None

def load():
  """ Initializes the module by binding all the commands."""
  global sm
  modutils.load_commands(commands_dict)
  sm = SpeedwalkManager()
  exported.add_manager("speedwalk", sm)

  exported.hook_register("user_filter_hook", sm.userfilter, 80)
  exported.hook_register("write_hook", sm.persist)

  from lyntin import config
  for mem in exported.get_active_sessions():
    tc = config.BoolConfig("speedwalk", 1, 1,
         "Allows you to turn on and turn off speedwalk handling.")
    exported.add_config("speedwalk", tc, mem)

def unload():
  """ Unloads the module by calling any unload/unbind functions."""
  global sm
  modutils.unload_commands(list(commands_dict.keys()))
  exported.remove_manager("speedwalk")

  exported.hook_unregister("user_filter_hook", sm.userfilter)
  exported.hook_unregister("write_hook", sm.persist)

  # remove configuration items for every session involved
  for mem in exported.get_active_sessions():
    exported.remove_config("speedwalk", mem)

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