#######################################################################
# This file is part of Lyntin.
# copyright (c) Free Software Foundation 2001-2007
#
# Lyntin is distributed under the GNU General Public License license.  See the
# file LICENSE for distribution details.
# $Id: textui.py,v 1.10 2007/07/24 00:39:03 willhelm Exp $
#######################################################################
"""
Holds the text ui class.
"""
import re, sys, os, select
from lyntin import ansi, engine, event, utils, exported, config
from lyntin.ui import base, message


HELP_TEXT = """
The textui is the most basic ui you can get.  It works great over
telnet/ssh, but terrible in the Win32 command window.  At the same 
time, because it's so basic, it tends to be a good testing ui.

To enable GNU readline support (if it's available) use:

   --readline on

at the command line.  It'll look for a "readlinerc" file in your
datadir and set readline options from that.

Also, depending on what terminal you're running Lyntin from, it might 
help to set the TERMTYPE at the commandline.  Lyntin returns "lyntin" 
when asked, but doing something like:

   --term xterm

will kick Lyntin into returning "xterm" as the TERMTYPE.
"""

try:
  import termios
except ImportError:
  pass

myui = None

DEFAULT_COLOR = list(ansi.DEFAULT_COLOR)
DEFAULT_ANSI = chr(27) + "[0m"

def get_ui_instance():
  global myui
  if myui == None:
    myui = Textui()
  return myui

class Textui(base.BaseUI):
  """
  This is the text ui.  It's super basic and should run almost
  anywhere.  It lacks several useful functions that the Tkui
  and the Curses ui (eventually) will have.
  """
  def __init__(self):
    """ Initialize the textui."""
    base.BaseUI.__init__(self)
    exported.hook_register("shutdown_hook", self.shutdown)
    exported.hook_register("to_user_hook", self.write)
    exported.hook_register("config_change_hook", self.configChangeHandler)
    exported.hook_register("bell_hook", self.bell)
    self._currcolors = {}
    self._unfinishedcolor = {}

    self._tio = 0
    self._rline = 0
    self._echo = 1

    # grab the stdin stream at once,
    # enabling it's further redirection by another module
    self._stdin = sys.stdin

  def runui(self):
    global HELP_TEXT
    exported.add_help("textui", HELP_TEXT)
    exported.write_message("For textui help, type \"#help textui\".")

    # termios is the module that allows us to change echo for a terminal
    # but only if the module is present
    try:
        import termios
    except ImportError:
        self._tio = 0
    else:
        self._tio = 1
        echonew = termios.tcgetattr(self._stdin.fileno())
        self._onecho_attr = echonew[3]
        self._offecho_attr = echonew[3] & ~termios.ECHO

    if "readline" in config.options:
      try:
        import readline
      except ImportError:
        self._rline = 0
        exported.write_error("Readline not available for your system.")
      else:
        self._rline = 1

        # we do some stuff to grab the readlinerc file if they have one
        # so the user can set some readline oriented things which makes 
        # things a little nicer for the user.
        d = exported.get_config("datadir")

        try:
          readline.read_init_file(d + "readlinerc")
        except FileNotFoundError:
          exported.write_error(f"Note: No readlinerc file available in {d}.")
        except Exception as e:
          exported.write_error(f"Error loading readlinerc file in {d}: {e}")
        
        exported.write_message("Readline enabled.")

    if self._tio == 0 or self._rline == 1:
      exported.write_error("Warming: echo off is unavailable.  " +
                           "Your password will be visible.")

    # go into the main loop
    self.run()
      
  def shutdown(self, args):
    """ Shuts down the textui and makes sure that we're echoing!"""
    if self._echo == 0:
      self.turnonecho()

  def turnonecho(self):
    """ Turns on echo if termios module is present."""
    if self._tio == 0 or self._rline == 1:
      return

    # Ensure stdin is valid and supports fileno()
    if not self._stdin or not hasattr(self._stdin, "fileno"):
      print("Error: stdin is invalid or does not support fileno()")
      return

    # Ensure we're in a real terminal
    if not sys.stdin.isatty():
      print("Error: Not running in a real terminal.")
      return

    try:
      fd = self._stdin.fileno()
      new = termios.tcgetattr(fd)
      new[3] = self._onecho_attr
      termios.tcsetattr(fd, termios.TCSADRAIN, new)
      self._echo = 1
    except termios.error as e:
      exported.write_error(f"textui: unable to turn on echo: {e}")

  def turnoffecho(self):
    """ Turns off echo if termios module is present."""
    if self._tio == 0 or self._rline == 1:
      return

    fd = self._stdin.fileno()
    new = termios.tcgetattr(fd)
    new[3] = self._offecho_attr
    try:
      termios.tcsetattr(fd, termios.TCSADRAIN, new)
      self._echo = 0
    except Exception as e:
      exported.write_error("textui: unable to turn off echo: %s" % e)


  def bell(self, args):
    """ Handles incoming bell characters."""
    sys.stdout.write('\07')

  def configChangeHandler(self, args):
    """ Handles config changes (including mudecho)."""
    name = args["name"]
    newvalue = args["newvalue"]

    if name == "mudecho":
      if newvalue == 0:
        # echo off
        self.turnoffecho()
      else:
        # echo on
        self.turnonecho()

  def _posix_readline_input(self):
    """
    If the os is posix and the readline module is present, then we 
    use input to grab user input.
    """
    try:
      return eval(input())
    except EOFError:
      pass

  def _posix_input(self):
    """
    If the os is posix and there is no readline module, then we
    use sys.stdin.readline().
    """
    readers,w,e = select.select([self._stdin], [], [])
    if readers:
      for mem in readers:
        try:
          return mem.readline()
        except IOError:
          pass

  def _non_posix_input(self):
    return self._stdin.readline()

  def run(self):
    """ This is the poll loop for user input."""
    try:
      while not self.shutdownflag:
        if os.name == 'posix':
          if self._rline == 1:
            data = self._posix_readline_input()
          else:
            data = self._posix_input()
        else:
          data = self._non_posix_input()

        if data != None:
          self.handleinput(data)

          # FIXME - this is just plain icky.  the issue is that
          # we need to know we're ending _before_ we block for
          # the next input.  otherwise Lyntin will shut down except
          # for this thread which will hang around blocking until
          # the user hits the enter key.
          # 
          # any good ideas for dealing with this are more than welcome.
          if data.find("#end") == 0:
            break


    except select.error as e:
      (errno,name) = e
      if errno == 4:
        exported.write_message("system exit: select.error.")
        event.ShutdownEvent().enqueue()
        return

    except SystemExit:
      exported.write_message("system exit: you'll be back...")
      event.ShutdownEvent().enqueue()

    except:
      exported.write_traceback()
      event.ShutdownEvent().enqueue()


  def write(self, args):
    """
    Handles writing information from the mud and/or Lyntin
    to the user.
    """
    msg = args["message"]

    if isinstance(msg, str):
      msg = message.Message(msg, message.LTDATA)

    line = msg.data
    ses = msg.session

    if line == '' or self.showTextForSession(ses) == 0:
      return

    # we prepend the session name to the text if this is not the 
    # current session sending text.
    pretext = ""
    if ses != None and ses != exported.get_current_session():
      pretext = "[" + ses.getName() + "] "

    if msg.type == message.ERROR or msg.type == message.LTDATA:
      if msg.type == message.ERROR:
        pretext = "error: " + pretext
      else:
        pretext = "lyntin: " + pretext

      line = pretext + utils.chomp(line).replace("\n", "\n" + pretext)
      if exported.get_config("ansicolor") == 1:
        line = DEFAULT_ANSI + line
      sys.stdout.write(line + "\n")
      return

    elif msg.type == message.USERDATA:
      # we don't print user data in the textui
      return

    if exported.get_config("ansicolor") == 0:
      if pretext:
        if line.endswith("\n"):
          line = (pretext + line[:-1].replace("\n", "\n" + pretext) + "\n")
        else:
          line = pretext + line.replace("\n", "\n" + pretext)
      sys.stdout.write(line)
      sys.stdout.flush()
      return

    # each session has a saved current color for mud data.  we grab
    # that current color--or user our default if we don't have one
    # for the session yet.
    if ses in self._currcolors:
      color = self._currcolors[ses]
    else:
      # need a copy of the list and not a reference to the list itself.
      color = list(DEFAULT_COLOR)


    # some sessions have an unfinished color as well--in case we
    # got a part of an ansi color code in a mud message, and the other
    # part is in another message.
    if ses in self._unfinishedcolor:
      leftover = self._unfinishedcolor[ses]
    else:
      leftover = ""

    lines = line.splitlines(1)
    if lines:
      for i in range(0, len(lines)):
        mem = lines[i]
        acolor = ansi.convert_tuple_to_ansi(color)

        color, leftover = ansi.figure_color(mem, color, leftover)

        if pretext:
          lines[i] = DEFAULT_ANSI + pretext + acolor + mem
        else:
          lines[i] = DEFAULT_ANSI + acolor + mem

      sys.stdout.write("".join(lines) + DEFAULT_ANSI)
      sys.stdout.flush()

    self._currcolors[ses] = color
    self._unfinishedcolor[ses] = leftover


  def prompt(self):
    """ Prints a prompt to the user."""
    sys.stdout.write("> ")
    sys.stdout.flush()

  def flush(self):
    """ Flushes the stdout.  Not sure we really need this
    but it's here."""
    sys.stdout.flush()

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