#########################################################################
# 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: tintincmds.py,v 1.26 2007/07/24 00:39:03 willhelm Exp $
#########################################################################

import os, os.path
from lyntin import net, utils, engine, constants, config, exported, event
from lyntin.modules import modutils

"""
This module holds commands that are derived from Tintin, but don't involve
a manager.  Tintin commands that involve a manager (alias and unalias,
action and unaction, variable and unvariable...) are in their respective
modules along with their manager and any helper functions involved.
"""
commands_dict = {}


def bell_cmd(ses, words, input):
  """
  Kicks off the bell for a given session.  Anything registered
  with the bell_hook will get tickled.

  category: commands
  """
  exported.hook_spam("bell_hook", {"session": ses})

commands_dict["bell"] = (bell_cmd, "")


def clear_cmd(ses, words, input):
  """
  This command clears a session of all session data (except the actual 
  connection).  This covers gags, subs, actions, aliases...

  category: commands
  """
  try:
    ses.clear()
    exported.write_message("clear: session %s cleared." % ses.getName(), ses)
  except Exception as e:
    exported.write_traceback("clear: error in clearing session %s" % ses)

commands_dict["clear"] = (clear_cmd, "")
  

def cr_cmd(ses, args, input):
  """
  This sends a carriage return to the mud.  This is useful in aliases 
  and actions that require a carriage return.

  category: commands
  """
  ses.writeSocket("\n")

commands_dict["^cr"] = (cr_cmd, "")


def end_cmd(ses, args, input):
  """
  Closes all sessions and quits out of Lyntin.

  Note: on most muds this will leave your character in a state of 
  linkdeath--it does not sell all your stuff, return you to town, 
  save your character, tell your friends goodbye, or anything of 
  that nature.

  category: commands
  """
  exported.write_message("end: you'll be back...")
  event.ShutdownEvent().enqueue()

commands_dict["^end"] = (end_cmd, "")


def help_cmd(ses, args, input):
  """
  With no arguments, shows all the help files available.
  With an argument, shows that specific help file or lists the contents
  of that category of help files.

  examples:
    #help                      - lists all help files in the root
    #help help                 - shows help for the help command
    #help commands.substitute  - shows help for the substitute command
    #help commands             - shows help for the commands category

  Items that have a number in parentheses after them are a category.
  The number is how many help topics are below that category.

  example:
    > #help
    lyntin: ::Lyntin Help::
    lyntin:
    lyntin: category: root
    lyntin:
    lyntin:    commands(55)  readme(13)
    lyntin:    textui
    >

  category: commands
  """
  item = args["item"]

  keys = item.split(".")
  data = "::Lyntin Help::\n\n"

  error, breadcrumbs, text = exported.get_help(item)

  if error:
    data += "%s\n\n" % error
  if breadcrumbs:
    data += "category: %s\n\n" % breadcrumbs

  data += text
  exported.write_message(data)

commands_dict["help"] = (help_cmd, "item=")


def history_cmd(ses, args, input):
  """
  #history prints the current history buffer.

  ! will call an item in the history indexed by the number after
  the !.  You can also do replacements via the sub=repl syntax.

  examples:
    #history [count=30]
        prints the last count entries in the history buffer
    !
        executes the last thing you did
    !4
        executes the fourth to last thing you did
    !4 3k=gk
        executes the fourth to last thing you did after replacing
        3k with gk in it

  category: commands
  """
  count = args["count"]
  
  historylist = exported.get_history(count)
  for i in range(0, len(historylist)):
    historylist[i] = "%d %s" % ((i+1), historylist[i])
  historylist.reverse()
  exported.write_message("History:\n" + "\n".join(historylist))

commands_dict["history"] = (history_cmd, "count:int=30")


def if_cmd(ses, args, input):
  """
  Allows you to do some boolean logic based on Lyntin variables
  or any Python expression.  If this expression returns a non-false
  value, then the action will be performed otherwise the elseaction
  (if there is one) will be peformed.

  examples:
    #if {$myhpvar < 100} {#showme PANIC!}
    #if {$myhpvar < 100 and $myspvar < 100} {#showme PANIC!}
    #if {'$name' == 'Joe'} {#showme That joe is a jerk.}

  When you're comparing variable values with other strings, make sure 
  to put them in quotes becuase variable expansion happens before
  the if command is evaluated.

  examples:
    WRONG: #if {$name == Joe} {#showme Joe is a jerk.}
    RIGHT: #if {'$name' == 'Joe'} {#showme Joe is a jerk.}

  category: commands
  """
  # original if_cmd code contributed by Sebastian John

  expr = args["expr"]
  action = args["action"]
  elseaction = args["elseaction"]

  try:
    if eval(expr):
      exported.lyntin_command(action, 1, ses)
    elif elseaction:
      exported.lyntin_command(elseaction, 1, ses)
  except SyntaxError:
    exported.write_error("if: invalid syntax / syntax error.", ses)
  except Exception as e:
    exported.write_error("if: exception: %s" % e, ses)

commands_dict["if"] = (if_cmd, "expr action elseaction=")


def info_cmd(ses, args, input):
  """
  Prints all the information about the active session: 
  actions, aliases, gags, highlights, variables, ticker, verbose, 
  speedwalking, and other various things.

  category: commands
  """
  data = exported.myengine.getStatus(ses)
  data = "\n".join(data)
  exported.write_message(data, ses)

commands_dict["info"] = (info_cmd, "")


def killall_cmd(ses, args, input):
  """
  Clears all sessions of session oriented stuff: aliases,
  substitutions, gags, variables, so on so forth.

  category: commands
  """
  for mem in exported.get_active_sessions():
    mem.clear()
    exported.write_message("killall: session %s cleared." % mem.getName())

commands_dict["^killall"] = (killall_cmd, "")


def loop_cmd(ses, args, input):
  """
  Executes a given command replacing %0 in the command with
  the range of numbers specified in <from> and <to>.

  example:
    #loop {1,4} {reclaim %0.corpse}

  will execute:
    reclaim 1.corpse
    reclaim 2.corpse
    reclaim 3.corpse
    reclaim 4.corpse

  Additionally, it can iterate over a comma-separated string of items:

  example:
    #loop {joe,harry,fred,pete} {say hi, %0.} range=no

  will execute:

    say hi, joe.
    say hi, harry.
    say hi, fred.
    say hi, pete.

  A better way to execute a command a number of times without regard
  to an index, would be:

    #4 {reclaim corpse}

  which will send "reclaim corpse" to the mud 5 times.

  category: commands
  """
  loop = args["fromto"]
  command = args["comm"]
  r = args["range"]

  # split it into parts
  loopitems = loop.split(',')

  if r:
    if len(loopitems) != 2:
      exported.write_error("syntax: #loop <from,to> <command>", ses)
      return

    # remove trailing and leading whitespace and convert to ints
    # so we can use them in a range function
    ifrom = int(loopitems[0].strip())
    ito = int(loopitems[1].strip())

    # we need to handle backwards and forwards using the step
    # and need to adjust ito so the range is correctly bounded.
    if ifrom > ito:
      step = -1
    else:
      step = 1

    if ito > 0:
      ito = ito + step
    else:
      ito = ito - step

    loopitems = list(range(ifrom, ito, step))
    loopitems = [repr(m) for m in loopitems]
    # for i in range(ifrom, ito, step):
    #   loopcommand = command.replace("%0", repr(i))
    #   exported.lyntin_command(loopcommand, internal=1, session=ses)

  for mem in loopitems:
    mem = mem.strip()
    loopcommand = command.replace("%0", mem)
    exported.lyntin_command(loopcommand, internal=1, session=ses)

commands_dict["loop"] = (loop_cmd, "fromto comm range:boolean=true")


def math_cmd(ses, args, input):
  """
  Implements the #math command which allows you to manipulate
  variables above and beyond setting them.

  examples:
    #math {hps} {$hps + 5}

  category: commands
  """
  var = args["var"]
  ops = args["operation"]
  quiet = args["quiet"]

  try:
    rvalue = eval(ops)
    varman = exported.get_manager("variable")
    if varman:
      varman.addVariable(ses,var, str(rvalue))
    if not quiet:
      exported.write_message("math: %s = %s = %s." % (var, ops, str(rvalue)), ses)
  except Exception as e:
    exported.write_error("math: exception: %s\n%s" % (ops, e), ses)

commands_dict["math"] = (math_cmd, "var operation quiet:boolean=false")


def nop_cmd(ses, args, input):
  """
  nop stands for "no operation".  So anything after a #nop
  and before a ; (unless it's escaped) will be ignored.

  This was quite possibly the easiest command to program ever.

  category: commands
  """
  return

commands_dict["nop"] = (nop_cmd, "input=", "limitparsing=0")


def read_cmd(ses, args, input):
  """
  Reads in a file running each line as a Lyntin command.  This is the
  opposite of #write which allows you to save session settings and
  restore them using #read.

  You can also read in via the commandline when you start Lyntin:

    lyntin --read 3k

  And read can handle HTTP urls:

    lyntin --read http://lyntin.sourceforge.net/lyntinrc

    #read http://lyntin.sourceforge.net/lyntinrc

  Note: the first non-whitespace char is used to set the Lyntin
  command character.  If you use non Lyntin commands in your file,
  make sure the first one is a command char.  If not, use #nop .
  It will skip blank lines.

  If you don't specify a directory, Lyntin will look for the file
  in your datadir.

  category: commands
  """
  filename = args.get("filename")

  if os.sep not in filename and not filename.startswith("http://"):

    if config.options["datadir"] is None:
      raise ValueError("Error: config.options['datadir'] is not set!")

    if filename is None:
      raise ValueError("Error: filename is None!")
    filename = config.options["datadir"] + filename
    #TODO i have a file, just nor parsing it properly!
    #filename = os.path.join(config.options["datadir"], filename)

  if filename.startswith("~"):
    filename = os.path.expanduser(filename)

  try:
    # http reading contributed by Sebastian John
    if filename.startswith("http://"):
      contents = utils.http_get(filename).split("\n")
    else:
      f = open(filename, "r")
      contents = f.readlines()
      f.close()
  except Exception as e:
    exported.write_error("read: file %s cannot be opened.\n%s" % (filename, e), ses)
    return

  contents = [m for m in contents if len(m.strip()) > 0]

  if len(contents) == 0:
    exported.write_message("read: %s had no data." % filename, ses)
    return

  c = exported.get_config("commandchar")
  if not contents[0].startswith(c):
    exported.lyntin_command("%sconfig commandchar %s" % (c, contents[0][0]), internal=1, session=ses)

  command = ""
  continued = 0
  # FIXME - should this be a config setting?
  esc = "\\"

  for mem in contents:
    mem = mem.strip()
    if len(mem) > 0:
      # handle multi-line commands
      if mem.endswith(esc):
        mem = mem.rstrip(esc)
        continued = 1
      else:
        continued = 0

    command = command + mem
    if not continued:
      exported.lyntin_command(command, internal=1, session=ses)
      command = ""

  exported.write_message("read: file %s read." % filename, ses)

commands_dict["read"] = (read_cmd, "filename")
def session_cmd(ses, args, input):
  """
  This command creates a connection to a specific mud. When you create
  a session, that session becomes the active Lyntin session.

  To create a session to 3k.org named "3k":

  #session 3k www.3k.org 5000

  To create a session and initialize it with commands from a specific
  file:

  #session 3k www.3k.org 5000 /home/david/3k/3k.lyntin

  Then to create another session to another mud:

  #session eto gytje.pvv.unit.no 4000

  Then if 3k was your active session, you could do things on the eto
  session by prepending your command with "#eto ":

  #eto say hello

  or switch to the eto session by typing just "#eto".

  category: commands
  """
  name = args["sessionname"]
  host = args["host"]
  port = args["port"]
  filename = args["filename"]

  if not name and not host and (not port or port == -1):
    data = "Sessions available:\n"
    for mem in exported.get_active_sessions():
      data += f"   {mem.getName()}: {mem._socket!r}\n"

    exported.write_message(data[:-1])
    return

  if not name or not host or port == -1:
    exported.write_error("syntax: #session <sesname> <host> <port>")
    return

  if name.isdigit():
    exported.write_error("session: session name cannot be all numbers.")
    return

  e = exported.myengine
  ses = e.getSession(name)

  preexistingsession = ses is not None

  if preexistingsession and ses.isConnected():
    exported.write_error("session: session of that name already exists.")
    return

  try:
    # create and register a session for this connection....
    if ses is None:
      ses = e.createSession(name)

    sock = net.SocketCommunicator(e, ses, host, port)
    ses.setSocketCommunicator(sock)

    ses._host = host
    ses._port = port

    e.changeSession(name)

    # connect to the mud...
    # this might take a while--we block here until this is done.
    sock.connect(host, port, name)

    # start the network thread
    e.startthread("network", sock.run)

  except Exception as e:
    exported.write_traceback(f"session: had problems creating the session. {str(e)}")
    ses.setSocketCommunicator(None)

    if not preexistingsession:
      try:
        e.unregisterSession(ses)
      except Exception:
        pass

      try:
        e.closeSession(name)
      except Exception:
        pass

  # populate the session using the specified file
  if filename:
    read_cmd(ses, args, '')


commands_dict["session"] = (session_cmd, "sessionname= host= port:int=-1 filename=")


def showme_cmd(ses, args, input):
  """
  Will display {text} on your screen.  Doesn't get sent to the mud--
  just your screen.

  examples:
    #action {^%0 annihilates you!} {#showme {EJECT! EJECT! EJECT!}}

  category: commands
  """
  input = args["input"]
  if not input:
    exported.write_error("syntax: requires a message.", ses)
    return

  input = input.replace("\\;", ";")
  input = input.replace("\\$", "$")
  input = input.replace("\\%", "%")

  exported.write_message(input, ses)
     
commands_dict["showme"] = (showme_cmd, "input=", "limitparsing=0")

def wshowme_cmd(ses, args, input):
  """
  Writes the text into the named window, if the current ui supports named window
s.
  If named windows are unsupported, writes the text into the main window.

  examples:
    #action {^%0 annihilates you!} {#wshowme Alert {EJECT! EJECT! EJECT!}}

  category: commands
  """
  exported.write_message(args["text"], ses, window=args["window"])

commands_dict['wshowme'] = ( wshowme_cmd, "window= text=" )


def textin_cmd(ses, args, input):
  """
  Takes the contents of the file and outputs it directly to the mud
  without processing it (like #read does).

  If you don't specify a directory, Lyntin will look for the file in
  the datadir.

  category: commands
  """
  if (ses.getName() == "common"):
    exported.write_error("textin cannot be applied to common session.", ses)
    return

  filename = args["file"]

  if os.sep not in filename:
    #filename = config.options["datadir"] + filename
    filename = os.path.join(config.options["datadir"], filename)
   
  try:
    f = open(filename, "r")
    contents = f.readlines()
    f.close()
    for mem in contents:
      mem = utils.chomp(mem)
      ses.getSocketCommunicator().write(mem + "\n")
    exported.write_message("textin: file %s read and sent to mud." % filename, ses)

  except OSError:
    exported.write_error("textin: file %s is not readable." % filename, ses)
  except Exception as e:
    exported.write_error("textin: exception thrown %s." % e, ses)

commands_dict["textin"] = (textin_cmd, "file")


def version_cmd(ses, args, input):
  """
  Displays the version number, contact information, and web-site for
  Lyntin.

  category: commands
  """
  exported.write_message(constants.VERSION)

commands_dict["version"] = (version_cmd, "")


def write_cmd(ses, args, input):
  """
  Writes all aliases, actions, gags, etc to the file specified.
  You can then use the #read command to read this file in and
  restore your session settings.

  The quiet argument lets you specify whether you want command data
  to be written to the file so that when you read it back in with #read,
  the commands are executed quietly.

  If you don't specify a directory, it will be written to your datadir.

  Note: Windows users should either use two \\'s or use / to separate
  directory names.

  category: commands
  """
  filename = args["file"]
  quiet = args["quiet"]

  f = None

  c = exported.myengine.getConfigManager().get("commandchar")

  if os.sep not in filename:
    filename = config.options["datadir"] + filename

  data = exported.get_write_data(ses, quiet)

  if data:
    try:
      f = open(filename, "w")
      f.write(c + ("\n" + c).join(data))
      f.close()
      exported.write_message("write: file %s has been written for session %s." % 
                             (filename, ses.getName()), ses)
    except Exception:
      exported.write_traceback("write: error writing to file %s." % filename, ses)
      try:
        f.close()
      except:
        pass
    return

  exported.write_message("write: no data to write.")

commands_dict["write"] = (write_cmd, "file quiet:boolean=false")


def zap_cmd(ses, args, input):
  """
  This disconnects from the mud and closes the session.  If no
  session is specified, it will close the current session.

  category: commands
  """
  sesname = args["session"]
  if sesname:
    ses = exported.myengine.getSession(sesname)
    if ses is None:
      exported.write_error("zap: session %s does not exist." % sesname)
      return

  if exported.myengine.closeSession(ses):
    exported.write_message("zap: session %s zapped!" % ses.getName())
  else:
    exported.write_message("zap: session %s cannot be zapped!" % ses.getName())

commands_dict["zap"] = (zap_cmd, "session=")


def load():
  """ Initializes the module by binding all the commands."""
  modutils.load_commands(commands_dict)


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

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