// ---------------------------------------------------------------------------
// - Terminal.cpp                                                            -
// - afnix standard library - terminal class implementation                  -
// ---------------------------------------------------------------------------
// - This program is free software;  you can redistribute it  and/or  modify -
// - it provided that this copyright notice is kept intact.                  -
// -                                                                         -
// - This program  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.  In no event shall -
// - the copyright holder be liable for any  direct, indirect, incidental or -
// - special damages arising in any way out of the use of this software.     -
// ---------------------------------------------------------------------------
// - copyright (c) 1999-2008 amaury darsch                                   -
// ---------------------------------------------------------------------------

#include "Vector.hpp"
#include "Boolean.hpp"
#include "Terminal.hpp"
#include "Character.hpp"
#include "QuarkZone.hpp"
#include "Exception.hpp"

namespace afnix {

  // -------------------------------------------------------------------------
  // - private section                                                       -
  // -------------------------------------------------------------------------

  // default prompts
  static const char* DEFAULT_PROMPT1 = "(axi) ";
  static const char* DEFAULT_PROMPT2 = "(...) ";

  // -------------------------------------------------------------------------
  // - class section                                                         -
  // -------------------------------------------------------------------------

  // create a default terminal

  Terminal::Terminal (void) {
    // prompt initialize
    d_prt1 = DEFAULT_PROMPT1;
    d_prt2 = DEFAULT_PROMPT2;
    // save the input stream state
    save ();
  }

  // destroy this terminal

  Terminal::~Terminal (void) {
    // restore terminal state
    restore ();
  }

  // return the class name

  String Terminal::repr (void) const {
    return "Terminal";
  }

  // insert a character in this terminal

  void Terminal::insert (const t_quad c) {
    wrlock ();
    try {
      // check if we are at the eol
      if (d_lbuf.iseol () == true) {
	// insert the character in the buffer
	d_lbuf.add (c);
	// insert the character in the terminal
	OutputTerm::insert (c);
	// check if we need to move the cursor to the next line
	long cols = getcols ();
	long cpos = d_lbuf.getabs ();
	if ((cols != 0) && ((cpos % cols) == 0)) {
	  if (OutputTerm::mvbol () == false) OutputTerm::movel (cols);
	  OutputTerm::moved (1);
	}
	unlock ();
	return;
      }
      // if we are not in insert mode - just replace the character
      if (d_insert == false) {
	d_lbuf.add (c);
	OutputTerm::insert (c);
	// check if we need to move the cursor to the next line
	long cols = getcols ();
	long cpos = d_lbuf.getabs ();
	if ((cols != 0) && ((cpos % cols) == 0)) {
	  if (OutputTerm::mvbol () == false) OutputTerm::movel (cols);
	  OutputTerm::moved (1);
	}
	unlock ();
	return;
      }
      // here we are in the middle of the line - start by killing the line at
      // the cursor position - then insert the character and refresh the rest
      // of the line - we also need to restore the original cursor position
      // first save the context
      long   crem = d_lbuf.getrem ();
      String sstr = d_lbuf.substr ();
      // kill at the cursor position
      kbchr ();
      // insert the character, then the line
      insert (c);
      OutputTerm::insert (sstr);
      // restore the cursor position
      for (long i = 0; i < crem; i++) {
	if (movel () == false) break;
      }
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // delete a character in place

  void Terminal::rmchr (void) {
    wrlock ();
    try {
      // check if we operate at the eol
      if (d_lbuf.iseol () ==  true) {
	unlock ();
	return;
      }
      // check if we are at the last character
      if (d_lbuf.islst () ==  true) {
	// delete the buffer character
	if (d_lbuf.chdel () == true) {
	  // delete the terminal character
	  OutputTerm::chdel ();
	}
	unlock ();
	return;
      }
      // here we are in the middle of the line - start by saving the buffer
      // in the next position, kill the line at the cursor and restore
      // the remaining buffer
      if (d_lbuf.isdel () == true) {
	// save the buffer contents
	String sstr = d_lbuf.delstr ();
	// kill at the cursor position
	kbchr ();
	// insert the line
	OutputTerm::insert (sstr);
	// restore the cursor position
	long slen = sstr.length ();
	for (long i = 0; i < slen; i++) {
	  if (movel () == false) break;
	}
      }
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // delete a character in backspace mode

  void Terminal::bkchr (void) {
    wrlock ();
    try {
      // move to the left and delete
      if (movel () == true) rmchr ();
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // move to the beginning of the line

  bool Terminal::mvbol (void) {
    wrlock ();
    try {
      // get the number of move
      long num  = d_lbuf.getrel ();
      // loop for the move
      for (long i = 0; i < num; i++) {
	if (movel () == false) {
	  unlock ();
	  return false;
	}
      }
      unlock ();
      return true;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // move to the end of the line

  bool Terminal::mveol (void) {
    wrlock ();
    try {
      // get the number of moves
      long num  = d_lbuf.getrem ();
      // loop for the move
      for (long i = 0; i < num; i++) {
	if (mover () == false) {
	  unlock ();
	  return false;
	}
      }
      unlock ();
      return true;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // move the terminal cursor to the left

  bool Terminal::movel (void) {
    wrlock ();
    try {
      // move the line cursor
      if (d_lbuf.movel () == false) {
	unlock ();
	return false;
      }
      // update the terminal 
      long cols = getcols ();
      long cpos = d_lbuf.getabs () + 1;
      if ((cols != 0) && ((cpos % cols) == 0)) {
	OutputTerm::moveu (1);
	OutputTerm::mover (cols);
      } else {
	OutputTerm::movel (1);
      }
      unlock ();
      return true;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // move the terminal cursor to the right

  bool Terminal::mover (void) {
    wrlock ();
    try {
      // update the line buffer
      if (d_lbuf.mover () ==  false) {
	unlock ();
	return false;
      }
      // update the terminal cursor
      long cols = getcols ();
      long cpos = d_lbuf.getabs () - 1;
      if ((cols > 0) && ((cpos % cols) == (cols - 1))) {
	OutputTerm::movel (cols-1);
	OutputTerm::moved (1);
      } else {
	OutputTerm::mover (1);
      }
      unlock ();
      return true;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // kill all characters from the current position

  void Terminal::kbchr (void) {
    wrlock ();
    try {
      // get the remaining characters
      long num = d_lbuf.getrem ();
      // move to the eol
      mveol ();
      // remove the characters
      for (long i = 0; i < num; i++) bkchr ();
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // kill all characters in the buffer

  void Terminal::kachr (void) {
    wrlock ();
    try {
      // move to the eol
      mveol ();
      // get the length
      long num = d_lbuf.length ();
      // remove the characters
      for (long i = 0; i < num; i++) bkchr ();
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // replace a buffer line with another one

  void Terminal::rline (const String& line) {
    wrlock ();
    try {
      // kill existing buffer
      kachr ();
      // reset buffer
      d_lbuf.clear ();
      // update the terminal
      OutputTerm::insert (line);
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // refresh the terminal

  void Terminal::rfrsh (void) {
    wrlock ();
    try {
      if (OutputTerm::clear () == true) {
	// get the string buffer
	String line = d_lbuf.tostring ();
	// clear the buffer
	d_lbuf.clear ();
	// write the prompt
	Output::write (d_lbuf.getprt ());
	// insert the line
	OutputTerm::insert (line);
      }
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // set the insert mode

  void Terminal::setim (void) {
    wrlock ();
    d_insert = !d_insert;
    d_lbuf.setim (d_insert);
    unlock ();
  }

  // read a line and return a string

  String Terminal::readline (bool pflag) {
    // reset the readline buffer
    d_lbuf.reset ();
    
    // check for the eof
    if (d_eof == true) return eofc;
    
    // turn off canonical mode and reset error mode
    bool canon = nocanon ();
    temode (false);

    // ok, print the prompt
    if (pflag == true) {
      Output::write (d_prt1);
      d_lbuf.setprt (d_prt1);
    } else {
      Output::write (d_prt2);
      d_lbuf.setprt (d_prt2);
    }

    // loop until we have an eol/eof
    while (true) {
      t_quad w = wread ();

      // check for backspace
      if ((canon == true ) && isbs (w) == true) {
	bkchr ();
	continue;
      }
      
      // check for delete
      if ((canon == true)  && isdel (w) == true) {
	rmchr ();
	continue;
      }
      
      // check for insert
      if ((canon == true) && (isins (w) == true)) {
	setim ();
	continue;
      }
      
      // check for move to begin line
      if ((canon == true) && (w == WIDE_STDBL)) {
	mvbol ();
	continue;
      }
      
      // check for move to end of line
      if ((canon == true) && (w == WIDE_STDEL)) {
	mveol ();
	continue;
      }
      
      // check for kill everything
      if ((canon == true) && (w == WIDE_STDKB)) {
	kachr ();
	continue;
      }
      
      // check for kill to eol
      if ((canon == true) && (w == WIDE_STDKL)) {
	kbchr ();
	continue;
      }

      // check for refresh
      if ((canon == true) && (w == WIDE_STDRF)) {
	rfrsh ();
        continue;
      }
      
      // check for move to left
      if ((canon == true) && (isle (w) == true)) {
	movel ();
	continue;
      }
      
      // check for move to right
      if ((canon == true)  && (isri (w) == true)) {
	mover ();
	continue;
      }

      // check for a move down
      if ((canon == true) && (isdo (w) == true)) {
	if (d_cilo.istop () == true) {
	  kachr ();
	  continue;
	}
	String* data = dynamic_cast <String*> (d_cilo.getup ());
	if (data == nilp)
	  kachr ();
	else
	  rline (*data);
	continue;
      }
      
      // check for a move up
      if ((canon == true) && (isup (w) == true)) {
	if (d_cilo.isbottom () == true) continue;
	String* data = dynamic_cast <String*> (d_cilo.getdown ());
	if (data == nilp)
	  kachr ();
	else
	  rline (*data);
	continue;
      }

      // make sure the cursor is reset to the end for a eol/eof
      if (w == WIDE_STDNL) d_lbuf.setce ();
      if (w == WIDE_STDEF) d_lbuf.setce ();
      
      // check for printable - if not we reject - we accept eol/eof as well
      if (ispr (w) == false) continue;

      // process normal character
      if ((w == WIDE_STDNL) || (w == WIDE_STDEF)) {
	if (d_lbuf.length () > 0) {
	  d_cilo.add (new String (d_lbuf.tostring()));
	}
	d_lbuf.add (w);
	write (eolc);
	break;
      }
      // insert the character in the terminal
      insert (w);
    }
    // this is it - return the string buffer
    if (canon == true) restore ();
    return d_lbuf.tostring ();
  }

  // set the primary prompt

  void Terminal::setpp (const String& value) {
    wrlock ();
    d_prt1 = value;
    unlock ();
  }

  // set the seconday prompt

  void Terminal::setsp (const String& value) {
    wrlock ();
    d_prt2 = value;
    unlock ();
  }

  // get the primary prompt

  String Terminal::getpp (void) const {
    rdlock ();
    String result = d_prt1;
    unlock ();
    return result;
  }

  // get the secondary prompt

  String Terminal::getsp (void) const {
    rdlock ();
    String result = d_prt2;
    unlock ();
    return result;
  }

  // -------------------------------------------------------------------------
  // - object section                                                        -
  // -------------------------------------------------------------------------

  // the quark zone
  static const long QUARK_ZONE_LENGTH = 5;
  static QuarkZone  zone (QUARK_ZONE_LENGTH);

  // the object supported quarks
  static const long QUARK_GETPP    = zone.intern ("get-primary-prompt");
  static const long QUARK_GETSP    = zone.intern ("get-secondary-prompt");
  static const long QUARK_SETPP    = zone.intern ("set-primary-prompt");
  static const long QUARK_SETSP    = zone.intern ("set-secondary-prompt");
  static const long QUARK_READLINE = zone.intern ("read-line");

  // create a new object in a generic way

  Object* Terminal::mknew (Vector* argv) {
    long argc = (argv == nilp) ? 0 : argv->length ();
    if (argc != 0) 
      throw Exception ("argument-error", "invalid arguments with terminal"); 
    return new Terminal;
  }

  // return true if the given quark is defined

  bool Terminal::isquark (const long quark, const bool hflg) const {
    rdlock ();
    if (zone.exists (quark) == true) {
      unlock ();
      return true;
    }
    bool result = hflg ? InputTerm::isquark (quark, hflg) : false;
    if (result == false) {
      result = hflg ? OutputTerm::isquark (quark, hflg) : false;
    }
    unlock ();
    return result;
  }

  // apply this object with a set of arguments and a quark

  Object* Terminal::apply (Runnable* robj, Nameset* nset, const long quark,
			   Vector* argv) {
    // get the number of arguments
    long argc = (argv == nilp) ? 0 : argv->length ();

    // dispatch 0 argument
    if (argc == 0) {
      if (quark == QUARK_READLINE) return new String    (readline (true));
      if (quark == QUARK_GETPP)    return new String    (getpp ());
      if (quark == QUARK_GETSP)    return new String    (getsp ());
    }

    // dispatch 1 argument
    if (argc == 1) {
      if (quark == QUARK_READLINE) {
	bool flag = argv->getbool (0);
	String* result = new String (readline (flag));
	return result;
      }
      if (quark == QUARK_SETPP) {
	String val = argv->getstring (0);
	setpp (val);
	return nilp;
      }
      if (quark == QUARK_SETSP) {
	String val = argv->getstring (0);
	setsp (val);
	return nilp;
      }
    }

    // check the input terminal stream
    if (InputTerm::isquark (quark, true) == true)
      return InputTerm::apply (robj, nset, quark, argv);
    // default to the output terminal stream
    return OutputTerm::apply(robj, nset, quark, argv);
  }
}
