/*
 * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved.
 *
 * This software is distributable under the BSD license. See the terms of the
 * BSD license in the documentation provided with this software.
 */

package org.jboss.forge.shell.console.jline.console;

import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionListener;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;

import org.fusesource.jansi.AnsiOutputStream;
import org.jboss.forge.shell.Shell;
import org.jboss.forge.shell.integration.BufferManager;
import org.jboss.forge.shell.integration.KeyListener;

/**
 * A reader for console applications. It supports custom tab-completion, saveable command history, and command line
 * editing. On some platforms, platform-specific commands will need to be issued before the reader will function
 * properly. See {@link org.jboss.forge.shell.console.jline.Terminal#init} for convenience methods for issuing
 * platform-specific setup commands.
 *
 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
 * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
 */
public class ConsoleReader
{
   public static final String JLINE_NOBELL = "jline.nobell";

   public static final char BACKSPACE = '\b';

   public static final char RESET_LINE = '\r';

   public static final char KEYBOARD_BELL = '\07';

   public static final char NULL_MASK = 0;

   public static final int TAB_WIDTH = 4;

   private static final ResourceBundle resources = ResourceBundle
            .getBundle(org.jboss.forge.shell.console.jline.console.completer.CandidateListCompletionHandler.class
                     .getName());

   private final org.jboss.forge.shell.console.jline.Terminal terminal;

   private InputStream in;

   private final Shell shell;

   // private final Writer out;

   private final CursorBuffer buf = new CursorBuffer();

   private String prompt;

   private boolean bellEnabled = true;

   private Character mask;

   private char echoCharacter;

   private StringBuffer searchTerm = null;

   private String previousSearchTerm = "";

   private int searchIndex = -1;

   private final List<KeyListener> keyListeners = new ArrayList<KeyListener>();

   public ConsoleReader(final InputStream in, final Shell shell, final InputStream bindings,
            final org.jboss.forge.shell.console.jline.Terminal term) throws
            IOException
   {
      this.in = in;
      this.shell = shell;
      this.terminal = term;
      this.keyBindings = loadKeyBindings(bindings);

      setBellEnabled(!org.jboss.forge.shell.console.jline.internal.Configuration.getBoolean(JLINE_NOBELL, false));
   }

   public ConsoleReader(final InputStream in, final Shell shell, final org.jboss.forge.shell.console.jline.Terminal term)
            throws IOException
   {
      this(in, shell, null, term);
   }

   // FIXME: Only used for tests
   void setInput(final InputStream in)
   {
      this.in = in;
   }

   public void registerKeyListener(final KeyListener keyListener)
   {
      keyListeners.add(keyListener);
   }

   public InputStream getInput()
   {
      return in;
   }

   public Shell getShell()
   {
      return shell;
   }

   public org.jboss.forge.shell.console.jline.Terminal getTerminal()
   {
      return terminal;
   }

   public CursorBuffer getCursorBuffer()
   {
      return buf;
   }

   public void setBellEnabled(final boolean enabled)
   {
      this.bellEnabled = enabled;
   }

   public boolean isBellEnabled()
   {
      return bellEnabled;
   }

   public void setPrompt(final String prompt)
   {
      this.prompt = prompt;
   }

   public String getPrompt()
   {
      return prompt;
   }

   /**
    * Set the echo character. For example, to have "*" entered when a password is typed:
    * <p/>
    *
    * <pre>
    * myConsoleReader.setEchoCharacter(new Character('*'));
    * </pre>
    * <p/>
    * Setting the character to
    * <p/>
    *
    * <pre>
    * null
    * </pre>
    * <p/>
    * will restore normal character echoing. Setting the character to
    * <p/>
    *
    * <pre>
    * new Character(0)
    * </pre>
    * <p/>
    * will cause nothing to be echoed.
    *
    * @param c the character to echo to the console in place of the typed character.
    */
   public void setEchoCharacter(final Character c)
   {
      this.echoCharacter = c;
   }

   /**
    * Returns the echo character.
    */
   public Character getEchoCharacter()
   {
      return echoCharacter;
   }

   /**
    * Erase the current line.
    *
    * @return false if we failed (e.g., the buffer was empty)
    */
   final boolean resetLine() throws IOException
   {
      if (buf.cursor == 0)
      {
         return false;
      }

      backspaceAll();

      return true;
   }

   int getCursorPosition()
   {
      // FIXME: does not handle anything but a line with a prompt absolute position
      String prompt = getPrompt();
      return ((prompt == null) ? 0 : stripAnsi(lastLine(prompt)).length()) + buf.cursor;
   }

   /**
    * Returns the text after the last '\n'. prompt is returned if no '\n' characters are present. null is returned if
    * prompt is null.
    */
   private String lastLine(final String str)
   {
      if (str == null)
         return "";
      int last = str.lastIndexOf("\n");

      if (last >= 0)
      {
         return str.substring(last + 1, str.length());
      }

      return str;
   }

   private String stripAnsi(final String str)
   {
      if (str == null)
         return "";
      try
      {
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         AnsiOutputStream aos = new AnsiOutputStream(baos);
         aos.write(str.getBytes());
         aos.flush();
         aos.close();
         return baos.toString();
      }
      catch (IOException e)
      {
         return str;
      }
   }

   /**
    * Move the cursor position to the specified absolute index.
    */
   public final boolean setCursorPosition(final int position) throws IOException
   {
      return moveCursor(position - buf.cursor) != 0;
   }

   /**
    * Set the current buffer's content to the specified {@link String}. The visual console will be modified to show the
    * current buffer.
    *
    * @param buffer the new contents of the buffer.
    */
   private void setBuffer(final String buffer) throws IOException
   {
      // don't bother modifying it if it is unchanged
      if (buffer.equals(buf.buffer.toString()))
      {
         return;
      }

      // obtain the difference between the current buffer and the new one
      int sameIndex = 0;

      for (int i = 0, l1 = buffer.length(), l2 = buf.buffer.length(); (i < l1)
               && (i < l2); i++)
      {
         if (buffer.charAt(i) == buf.buffer.charAt(i))
         {
            sameIndex++;
         }
         else
         {
            break;
         }
      }

      int diff = buf.cursor - sameIndex;
      if (diff < 0)
      { // we can't backspace here so try from the end of the buffer
         moveToEnd();
         diff = buf.buffer.length() - sameIndex;
      }

      backspace(diff); // go back for the differences
      killLine(); // clear to the end of the line
      buf.buffer.setLength(sameIndex); // the new length
      putString(buffer.substring(sameIndex)); // append the differences
   }

   private void setBuffer(final CharSequence buffer) throws IOException
   {
      setBuffer(String.valueOf(buffer));
   }

   /**
    * Output put the prompt + the current buffer
    */
   public final void drawLine() throws IOException
   {
      String prompt = getPrompt();
      if (prompt != null)
      {
         print(prompt);
      }

      print(buf.buffer.toString());

      if (buf.length() != buf.cursor)
      { // not at end of line
         back(buf.length() - buf.cursor - 1);
      }
   }

   /**
    * Clear the line and redraw it.
    */
   public final void redrawLine() throws IOException
   {
      print(RESET_LINE);
      // flush();
      drawLine();
   }

   /**
    * Clear the buffer and add its contents to the history.
    *
    * @return the former contents of the buffer.
    */
   final String finishBuffer() throws IOException
   { // FIXME: Package protected because used by tests
      String str = buf.buffer.toString();

      str = expandEvents(str);

      // we only add it to the history if the buffer is not empty
      // and if mask is null, since having a mask typically means
      // the string was a password. We clear the mask after this call
      if (str.length() > 0)
      {
         if ((mask == null) && isHistoryEnabled())
         {
            history.add(str);
         }
         else
         {
            mask = null;
         }
      }

      history.moveToEnd();

      buf.buffer.setLength(0);
      buf.cursor = 0;

      return str;
   }

   /**
    * Expand event designator such as !!, !#, !3, etc... See
    * http://www.gnu.org/software/bash/manual/html_node/Event-Designators.html
    *
    * @param str
    * @return
    */
   final String expandEvents(final String str) throws IOException
   {
      StringBuilder sb = new StringBuilder();
      for (int i = 0; i < str.length(); i++)
      {
         char c = str.charAt(i);
         switch (c)
         {
         case '!':
            if ((i + 1) < str.length())
            {
               c = str.charAt(++i);
               boolean neg = false;
               String rep = null;
               int i1, idx;
               switch (c)
               {
               case '!':
                  if (history.size() == 0)
                  {
                     throw new IllegalArgumentException("!!: event not found");
                  }
                  rep = history.get(history.index() - 1).toString();
                  break;
               case '#':
                  sb.append(sb.toString());
                  break;
               case '?':
                  i1 = str.indexOf('?', i + 1);
                  if (i1 < 0)
                  {
                     i1 = str.length();
                  }
                  String sc = str.substring(i + 1, i1);
                  i = i1;
                  idx = searchBackwards(sc);
                  if (idx < 0)
                  {
                     throw new IllegalArgumentException("!?" + sc + ": event not found");
                  }
                  else
                  {
                     rep = history.get(idx).toString();
                  }
                  break;
               case ' ':
               case '\t':
                  sb.append('!');
                  sb.append(c);
                  break;
               case '-':
                  neg = true;
                  i++;
                  // fall through
               case '0':
               case '1':
               case '2':
               case '3':
               case '4':
               case '5':
               case '6':
               case '7':
               case '8':
               case '9':
                  i1 = i;
                  for (; i < str.length(); i++)
                  {
                     c = str.charAt(i);
                     if ((c < '0') || (c > '9'))
                     {
                        break;
                     }
                  }
                  idx = 0;
                  try
                  {
                     idx = Integer.parseInt(str.substring(i1, i));
                  }
                  catch (NumberFormatException e)
                  {
                     throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i) + ": event not found");
                  }
                  if (neg)
                  {
                     if (idx < history.size())
                     {
                        rep = (history.get(history.index() - idx)).toString();
                     }
                     else
                     {
                        throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i)
                                 + ": event not found");
                     }
                  }
                  else
                  {
                     if ((idx >= (history.index() - history.size())) && (idx < history.index()))
                     {
                        rep = (history.get(idx)).toString();
                     }
                     else
                     {
                        throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i)
                                 + ": event not found");
                     }
                  }
                  break;
               default:
                  String ss = str.substring(i);
                  i = str.length();
                  idx = searchBackwards(ss, history.index(), true);
                  if (idx < 0)
                  {
                     throw new IllegalArgumentException("!" + ss + ": event not found");
                  }
                  else
                  {
                     rep = history.get(idx).toString();
                  }
                  break;
               }
               if (rep != null)
               {
                  sb.append(rep);
               }
            }
            else
            {
               sb.append(c);
            }
            break;
         case '^':
            if (i == 0)
            {
               int i1 = str.indexOf('^', i + 1);
               int i2 = str.indexOf('^', i1 + 1);
               if (i2 < 0)
               {
                  i2 = str.length();
               }
               if ((i1 > 0) && (i2 > 0))
               {
                  String s1 = str.substring(i + 1, i1);
                  String s2 = str.substring(i1 + 1, i2);
                  String s = history.get(history.index() - 1).toString().replace(s1, s2);
                  sb.append(s);
                  i = i2 + 1;
                  break;
               }
            }
            sb.append(c);
            break;
         default:
            sb.append(c);
            break;
         }
      }
      String result = sb.toString();
      if (!str.equals(result))
      {
         print(result);
         println();
         flush();
      }
      return result;

   }

   /*
    * Handle case where terminal does not move cursor to the next line when a character is inserted at the width of the
    * terminal. This also fixes backspace issue, where it assumes that the terminal is doing this.
    */
   private final void newlineAtWrap() throws IOException
   {
      int width = getTerminal().getWidth();

      if (((getCursorPosition() % width) == 0) && (getCurrentPosition() >= width))
         println();
   }

   /**
    * Write out the specified string to the buffer and the output stream.
    */
   public final void putString(final CharSequence str) throws IOException
   {
      buf.write(str);
      print(str);
      drawBuffer();
      newlineAtWrap();
   }

   /**
    * Output the specified character, both to the buffer and the output stream.
    */
   private void putChar(final int c, final boolean print) throws IOException
   {
      buf.write((char) c);

      if (print)
      {
         if (mask == null)
         {
            // no masking
            print(c);
         }
         else if (mask == NULL_MASK)
         {
            // Don't print anything
         }
         else
         {
            print(mask);
         }

         drawBuffer();
         newlineAtWrap();
      }
   }

   /**
    * Redraw the rest of the buffer from the cursor onwards. This is necessary for inserting text into the buffer.
    *
    * @param clear the number of characters to clear after the end of the buffer
    */
   private void drawBuffer(final int clear) throws IOException
   {
      // debug ("drawBuffer: " + clear);
      if ((buf.cursor == buf.length()) && (clear == 0))
      {
         return;
      }
      byte[] chars = buf.buffer.substring(buf.cursor).getBytes();
      if (mask != null)
      {
         Arrays.fill(chars, (byte) mask.charValue());
      }

      print(chars);
      clearAhead(clear);
      if (terminal.isAnsiSupported())
      {
         if (chars.length > 0)
         {
            // don't ask, it works
            back(Math.max(chars.length - 1, 1));
         }
      }
      else
      {
         back(chars.length);
      }
      // flush();
   }

   /**
    * Redraw the rest of the buffer from the cursor onwards. This is necessary for inserting text into the buffer.
    */
   private void drawBuffer() throws IOException
   {
      drawBuffer(0);
   }

   /**
    * Clear ahead the specified number of characters without moving the cursor.
    */
   private void clearAhead(final int num) throws IOException
   {
      if (num == 0)
      {
         return;
      }

      if (terminal.isAnsiSupported())
      {
         printAnsiSequence("K");
         return;
      }

      // print blank extra characters
      print((byte) ' ', num);

      // we need to flush here so a "clever" console doesn't just ignore the redundancy
      // of a space followed by a backspace.
      // flush();

      // reset the visual cursor
      back(num);

      // flush();
   }

   /**
    * Move the visual cursor backwards without modifying the buffer cursor.
    */
   private void back(final int num) throws IOException
   {
      if (num == 0)
         return;
      if (terminal.isAnsiSupported())
      {
         int width = getTerminal().getWidth();
         int cursor = getCursorPosition();
         // debug("back: " + cursor + " + " + num + " on " + width);
         int currRow = (cursor + num) / width;
         int newRow = cursor / width;
         int newCol = (cursor % width) + 1;
         // debug("    old row: " + currRow + " new row: " + newRow);
         if (newRow < currRow)
         {
            printAnsiSequence((currRow - newRow) + "A");
         }
         printAnsiSequence(newCol + "G");
         return;
      }
      print(BACKSPACE, num);
      // flush();
   }

   /**
    * Flush the console output stream. This is important for printout out single characters (like a backspace or
    * keyboard) that we want the console to handle immediately.
    */
   public void flush() throws IOException
   {
      shell.flush();
   }

   private int backspaceAll() throws IOException
   {
      return backspace(Integer.MAX_VALUE);
   }

   /**
    * Issue <em>num</em> backspaces.
    *
    * @return the number of characters backed up
    */
   private int backspace(final int num) throws IOException
   {
      if (buf.cursor == 0)
      {
         return 0;
      }

      int count = 0;

      int termwidth = getTerminal().getWidth();
      int lines = getCursorPosition() / termwidth;
      count = moveCursor(-1 * num) * -1;
      buf.buffer.delete(buf.cursor, buf.cursor + count);
      if ((getCursorPosition() / termwidth) != lines)
      {
         if (terminal.isAnsiSupported())
         {
            // debug("doing backspace redraw: " + getCursorPosition() + " on " + termwidth + ": " + lines);
            printAnsiSequence("K");
         }
      }
      drawBuffer(count);

      return count;
   }

   /**
    * Issue a backspace.
    *
    * @return true if successful
    */
   public boolean backspace() throws IOException
   {
      return backspace(1) == 1;
   }

   private boolean moveToEnd() throws IOException
   {
      return moveCursor(buf.length() - buf.cursor) > 0;
   }

   /**
    * Delete the character at the current position and redraw the remainder of the buffer.
    */
   private boolean deleteCurrentCharacter() throws IOException
   {
      if ((buf.length() == 0) || (buf.cursor == buf.length()))
      {
         return false;
      }

      buf.buffer.deleteCharAt(buf.cursor);
      drawBuffer(1);
      return true;
   }

   private boolean previousWord() throws IOException
   {
      while (isDelimiter(buf.current()) && (moveCursor(-1) != 0))
      {
         // nothing
      }

      while (!isDelimiter(buf.current()) && (moveCursor(-1) != 0))
      {
         // nothing
      }

      return true;
   }

   private boolean nextWord() throws IOException
   {
      while (isDelimiter(buf.current()) && (moveCursor(1) != 0))
      {
         // nothing
      }

      while (!isDelimiter(buf.current()) && (moveCursor(1) != 0))
      {
         // nothing
      }

      return true;
   }

   private boolean deletePreviousWord() throws IOException
   {
      while (isDelimiter(buf.current()) && backspace())
      {
         // nothing
      }

      while (!isDelimiter(buf.current()) && backspace())
      {
         // nothing
      }

      return true;
   }

   /**
    * Move the cursor <i>where</i> characters.
    *
    * @param num If less than 0, move abs(<i>where</i>) to the left, otherwise move <i>where</i> to the right.
    * @return The number of spaces we moved
    */
   public int moveCursor(final int num) throws IOException
   {
      int where = num;

      if ((buf.cursor == 0) && (where <= 0))
      {
         return 0;
      }

      if ((buf.cursor == buf.buffer.length()) && (where >= 0))
      {
         return 0;
      }

      if ((buf.cursor + where) < 0)
      {
         where = -buf.cursor;
      }
      else if ((buf.cursor + where) > buf.buffer.length())
      {
         where = buf.buffer.length() - buf.cursor;
      }

      moveInternal(where);

      return where;
   }

   /**
    * Move the cursor <i>where</i> characters, without checking the current buffer.
    *
    * @param where the number of characters to move to the right or left.
    */
   private void moveInternal(final int where) throws IOException
   {
      // debug ("move cursor " + where + " ("
      // + buf.cursor + " => " + (buf.cursor + where) + ")");
      buf.cursor += where;

      if (terminal.isAnsiSupported())
      {
         if (where < 0)
         {
            back(Math.abs(where));
         }
         else
         {
            int width = getTerminal().getWidth();
            int cursor = getCursorPosition();
            int oldLine = (cursor - where) / width;
            int newLine = cursor / width;
            if (newLine > oldLine)
            {
               printAnsiSequence((newLine - oldLine) + "B");
            }
            printAnsiSequence(1 + (cursor % width) + "G");
         }
         // flush();
         return;
      }

      char c;

      if (where < 0)
      {
         int len = 0;
         for (int i = buf.cursor; i < (buf.cursor - where); i++)
         {
            if (buf.buffer.charAt(i) == '\t')
            {
               len += TAB_WIDTH;
            }
            else
            {
               len++;
            }
         }

         char chars[] = new char[len];
         Arrays.fill(chars, BACKSPACE);
         shell.print(new String(chars));

         return;
      }
      else if (buf.cursor == 0)
      {
         return;
      }
      else if (mask != null)
      {
         c = mask;
      }
      else
      {
         print(buf.buffer.substring(buf.cursor - where, buf.cursor).getBytes());
         return;
      }

      // null character mask: don't output anything
      if (mask == NULL_MASK)
      {
         return;
      }

      print(c, Math.abs(where));
   }

   // FIXME: replace() is not used

   public final boolean replace(final int num, final String replacement)
   {
      buf.buffer.replace(buf.cursor - num, buf.cursor, replacement);
      try
      {
         moveCursor(-num);
         drawBuffer(Math.max(0, num - replacement.length()));
         moveCursor(replacement.length());
      }
      catch (IOException e)
      {
         e.printStackTrace();
         return false;
      }
      return true;
   }

   //
   // Key reading
   //

   /**
    * Read a character from the console.
    *
    * @return the character, or -1 if an EOF is received.
    */
   public final int readVirtualKey() throws IOException
   {
      int c = terminal.readVirtualKey(in);

      org.jboss.forge.shell.console.jline.internal.Log.trace("Keystroke: ", c);

      // clear any echo characters
      clearEcho(c);

      return c;
   }

   /**
    * Clear the echoed characters for the specified character code.
    */
   private int clearEcho(final int c) throws IOException
   {
      // if the terminal is not echoing, then ignore
      if (!terminal.isEchoEnabled())
      {
         return 0;
      }

      // otherwise, clear
      int num = countEchoCharacters((char) c);
      back(num);
      drawBuffer(num);

      return num;
   }

   private int countEchoCharacters(final char c)
   {
      // tabs as special: we need to determine the number of spaces
      // to cancel based on what out current cursor position is
      if (c == 9)
      {
         int tabStop = 8; // will this ever be different?
         int position = getCursorPosition();

         return tabStop - (position % tabStop);
      }

      return getPrintableCharacters(c).length();
   }

   /**
    * Return the number of characters that will be printed when the specified character is echoed to the screen
    * <p/>
    * Adapted from cat by Torbjorn Granlund, as repeated in stty by David MacKenzie.
    */
   private StringBuilder getPrintableCharacters(final char ch)
   {
      StringBuilder sbuff = new StringBuilder();

      if (ch >= 32)
      {
         if (ch < 127)
         {
            sbuff.append(ch);
         }
         else if (ch == 127)
         {
            sbuff.append('^');
            sbuff.append('?');
         }
         else
         {
            sbuff.append('M');
            sbuff.append('-');

            if (ch >= (128 + 32))
            {
               if (ch < (128 + 127))
               {
                  sbuff.append((char) (ch - 128));
               }
               else
               {
                  sbuff.append('^');
                  sbuff.append('?');
               }
            }
            else
            {
               sbuff.append('^');
               sbuff.append((char) ((ch - 128) + 64));
            }
         }
      }
      else
      {
         sbuff.append('^');
         sbuff.append((char) (ch + 64));
      }

      return sbuff;
   }

   public final int readCharacter(final char... allowed) throws IOException
   {
      // if we restrict to a limited set and the current character is not in the set, then try again.
      char c;

      Arrays.sort(allowed); // always need to sort before binarySearch

      while (Arrays.binarySearch(allowed, c = (char) readVirtualKey()) < 0)
      {
         // nothing
      }

      return c;
   }

   //
   // Key Bindings
   //

   public static final String JLINE_COMPLETION_THRESHOLD = "jline.completion.threshold";

   public static final String JLINE_KEYBINDINGS = "jline.keybindings";

   public static final String JLINEBINDINGS_PROPERTIES = ".jlinebindings.properties";

   /**
    * The map for logical operations.
    */
   private final short[] keyBindings;

   private short[] loadKeyBindings(InputStream input) throws IOException
   {
      if (input == null)
      {
         try
         {
            File file = new File(org.jboss.forge.shell.console.jline.internal.Configuration.getUserHome(),
                     JLINEBINDINGS_PROPERTIES);

            String path = org.jboss.forge.shell.console.jline.internal.Configuration.getString(JLINE_KEYBINDINGS);
            if (path != null)
            {
               file = new File(path);
            }

            if (file.isFile())
            {
               org.jboss.forge.shell.console.jline.internal.Log.debug("Loading user bindings from: ", file);
               input = new FileInputStream(file);
            }
         }
         catch (Exception e)
         {
            org.jboss.forge.shell.console.jline.internal.Log.error("Failed to load user bindings", e);
         }
      }

      short[] keyBindings = new short[Character.MAX_VALUE * 2];

      Arrays.fill(keyBindings, org.jboss.forge.shell.console.jline.console.Operation.UNKNOWN.code);

      if (input == null)
      {
         org.jboss.forge.shell.console.jline.internal.Log.debug("Using default bindings");
         ResourceBundle bundle = getTerminal().getDefaultBindings();

         if (bundle == null)
         {
            throw new RuntimeException("failed to load default keybidings");
         }
         loadMappingsFromBundle(keyBindings, bundle);
         return keyBindings;
      }

      // Loads the key bindings. Bindings file is in the format:
      //
      // keycode: operation name

      input = new BufferedInputStream(input);
      Properties p = new Properties();
      p.load(input);
      input.close();

      for (Object key : p.keySet())
      {
         String val = (String) key;

         try
         {
            short code = Short.parseShort(val);
            String name = p.getProperty(val);
            org.jboss.forge.shell.console.jline.console.Operation op = org.jboss.forge.shell.console.jline.console.Operation
                     .valueOf(name);
            keyBindings[code] = op.code;
         }
         catch (NumberFormatException e)
         {
            org.jboss.forge.shell.console.jline.internal.Log.error("Failed to convert binding code: ", val, e);
         }
      }

      // hardwired arrow key bindings
      // keybindings[VK_UP] = PREV_HISTORY;
      // keybindings[VK_DOWN] = NEXT_HISTORY;
      // keybindings[VK_LEFT] = PREV_CHAR;
      // keybindings[VK_RIGHT] = NEXT_CHAR;

      return keyBindings;
   }

   private void loadMappingsFromBundle(final short[] keyBindings, final ResourceBundle bundle)
   {
      Enumeration<String> keys = bundle.getKeys();
      String val;
      while (keys.hasMoreElements())
      {
         val = keys.nextElement();

         try
         {
            short code = Short.parseShort(val);
            String name = bundle.getString(val);
            org.jboss.forge.shell.console.jline.console.Operation op = org.jboss.forge.shell.console.jline.console.Operation
                     .valueOf(name);
            keyBindings[code] = op.code;
         }
         catch (NumberFormatException e)
         {
            org.jboss.forge.shell.console.jline.internal.Log.error("Failed to convert binding code: ", val, e);
         }

      }

   }

   int getKeyForAction(final short logicalAction)
   {
      for (int i = 0; i < keyBindings.length; i++)
      {
         if (keyBindings[i] == logicalAction)
         {
            return i;
         }
      }

      return -1;
   }

   int getKeyForAction(final org.jboss.forge.shell.console.jline.console.Operation op)
   {
      assert op != null;
      return getKeyForAction(op.code);
   }

   /**
    * Reads the console input and returns an array of the form [raw, key binding].
    */
   private int[] readBinding() throws IOException
   {
      int c = readVirtualKey();

      for (KeyListener listener : keyListeners)
      {
         if (listener.keyPress(c))
         {
            return null;
         }
      }

      if (c == -1)
      {
         return null;
      }

      // extract the appropriate key binding
      short code = keyBindings[c];

      org.jboss.forge.shell.console.jline.internal.Log.trace("Translated: ", c, " -> ", code);

      return new int[] { c, code };
   }

   //
   // Line Reading
   //

   /**
    * Read the next line and return the contents of the buffer.
    */
   public String readLine() throws IOException
   {
      return readLine((String) null);
   }

   /**
    * Read the next line with the specified character mask. If null, then characters will be echoed. If 0, then no
    * characters will be echoed.
    */
   public String readLine(final Character mask) throws IOException
   {
      return readLine(null, mask);
   }

   public String readLine(final String prompt) throws IOException
   {
      return readLine(prompt, null);
   }

   /**
    * Read a line from the <i>in</i> {@link InputStream}, and return the line (without any trailing newlines).
    *
    * @param prompt The prompt to issue to the console, may be null.
    * @return A line that is read from the terminal, or null if there was null input (e.g., <i>CTRL-D</i> was pressed).
    */
   public String readLine(String prompt, final Character mask) throws IOException
   {
      // prompt may be null
      // mask may be null

      // FIXME: This blows, each call to readLine will reset the console's state which doesn't seem very nice.
      this.mask = mask;
      if (prompt != null)
      {
         setPrompt(prompt);
      }
      else
      {
         prompt = getPrompt();
      }

      try
      {
         if (!terminal.isSupported())
         {
            beforeReadLine(prompt, mask);
         }

         if ((prompt != null) && (prompt.length() > 0))
         {
            shell.print(prompt);
            shell.flush();
         }

         // if the terminal is unsupported, just use plain-java reading
         if (!terminal.isSupported())
         {
            return readLine(in);
         }

         String originalPrompt = this.prompt;

         final int NORMAL = 1;
         final int SEARCH = 2;
         int state = NORMAL;

         boolean success = true;

         while (true)
         {
            int[] next = readBinding();

            if (next == null)
            {
               return null;
            }

            int c = next[0];

            // int code = next[1];
            org.jboss.forge.shell.console.jline.console.Operation code = org.jboss.forge.shell.console.jline.console.Operation
                     .valueOf(next[1]);

            if (c == -1)
            {
               return null;
            }

            // Search mode.
            //
            // Note that we have to do this first, because if there is a command
            // not linked to a search command, we leave the search mode and fall
            // through to the normal state.
            if (state == SEARCH)
            {
               int cursorDest = -1;

               switch (code)
               {
               // This doesn't work right now, it seems CTRL-G is not passed
               // down correctly. :(
               case ABORT:
                  state = NORMAL;
                  break;

               case SEARCH_PREV:
                  if (searchTerm.length() == 0)
                  {
                     searchTerm.append(previousSearchTerm);
                  }

                  if (searchIndex == -1)
                  {
                     searchIndex = searchBackwards(searchTerm.toString());
                  }
                  else
                  {
                     searchIndex = searchBackwards(searchTerm.toString(), searchIndex);
                  }
                  break;

               case DELETE_PREV_CHAR:
                  if (searchTerm.length() > 0)
                  {
                     searchTerm.deleteCharAt(searchTerm.length() - 1);
                     searchIndex = searchBackwards(searchTerm.toString());
                  }
                  break;

               case UNKNOWN:
                  searchTerm.appendCodePoint(c);
                  searchIndex = searchBackwards(searchTerm.toString());
                  break;

               default:
                  // Set buffer and cursor position to the found string.
                  if (searchIndex != -1)
                  {
                     history.moveTo(searchIndex);
                     // set cursor position to the found string
                     cursorDest = history.current().toString().indexOf(searchTerm.toString());
                  }
                  state = NORMAL;
                  break;
               }

               // if we're still in search mode, print the search status
               if (state == SEARCH)
               {
                  if (searchTerm.length() == 0)
                  {
                     printSearchStatus("", "");
                     searchIndex = -1;
                  }
                  else
                  {
                     if (searchIndex == -1)
                     {
                        beep();
                     }
                     else
                     {
                        printSearchStatus(searchTerm.toString(), history.get(searchIndex).toString());
                     }
                  }
               }
               // otherwise, restore the line
               else
               {
                  restoreLine(originalPrompt, cursorDest);
               }
            }

            if (state == NORMAL)
            {

               switch (code)
               {
               case EXIT: // ctrl-d
                  if (buf.buffer.length() == 0)
                  {
                     return null;
                  }
                  else
                  {
                     deleteCurrentCharacter();
                  }
                  break;

               case COMPLETE: // tab
                  success = complete();
                  break;

               case MOVE_TO_BEG:
                  success = setCursorPosition(0);
                  break;

               case KILL_LINE: // CTRL-K
                  success = killLine();
                  break;

               case CLEAR_SCREEN: // CTRL-L
                  success = clearScreen();
                  break;

               case KILL_LINE_PREV: // CTRL-U
                  success = resetLine();
                  break;

               case NEWLINE: // enter
                  moveToEnd();
                  // println(); // output newline
                  // flush();
                  return finishBuffer();

               case DELETE_PREV_CHAR: // backspace
                  success = backspace();
                  break;

               case DELETE_NEXT_CHAR: // delete
                  success = deleteCurrentCharacter();
                  break;

               case MOVE_TO_END:
                  success = moveToEnd();
                  break;

               case PREV_CHAR:
                  success = moveCursor(-1) != 0;
                  break;

               case NEXT_CHAR:
                  success = moveCursor(1) != 0;
                  break;

               case NEXT_HISTORY:
                  success = moveHistory(true);
                  break;

               case PREV_HISTORY:
                  success = moveHistory(false);
                  break;

               case ABORT:
               case REDISPLAY:
                  break;

               case PASTE:
                  success = paste();
                  break;

               case DELETE_PREV_WORD:
                  success = deletePreviousWord();
                  break;

               case PREV_WORD:
                  success = previousWord();
                  break;

               case NEXT_WORD:
                  success = nextWord();
                  break;

               case START_OF_HISTORY:
                  success = history.moveToFirst();
                  if (success)
                  {
                     setBuffer(history.current());
                  }
                  break;

               case END_OF_HISTORY:
                  success = history.moveToLast();
                  if (success)
                  {
                     setBuffer(history.current());
                  }
                  break;

               case CLEAR_LINE:
                  moveInternal(-(buf.buffer.length()));
                  killLine();
                  break;

               case INSERT:
                  buf.setOverTyping(!buf.isOverTyping());
                  break;

               case SEARCH_PREV: // CTRL-R
                  if (searchTerm != null)
                  {
                     previousSearchTerm = searchTerm.toString();
                  }
                  searchTerm = new StringBuffer(buf.buffer);
                  state = SEARCH;
                  if (searchTerm.length() > 0)
                  {
                     searchIndex = searchBackwards(searchTerm.toString());
                     if (searchIndex == -1)
                     {
                        beep();
                     }
                     printSearchStatus(searchTerm.toString(),
                              searchIndex > -1 ? history.get(searchIndex).toString() : "");
                  }
                  else
                  {
                     searchIndex = -1;
                     printSearchStatus("", "");
                  }
                  break;

               case UNKNOWN:
               default:
                  if (c != 0)
                  { // ignore null chars
                     ActionListener action = triggeredActions.get((char) c);
                     if (action != null)
                     {
                        action.actionPerformed(null);
                     }
                     else
                     {
                        putChar(c, true);
                     }
                  }
                  else
                  {
                     success = false;
                  }
               }

               if (!success)
               {
                  beep();
               }

               flush();
            }
         }
      }
      finally
      {
         if (!terminal.isSupported())
         {
            afterReadLine();
         }
      }
   }

   /**
    * Read a line for unsupported terminals.
    */
   private String readLine(final InputStream in) throws IOException
   {
      StringBuilder buff = new StringBuilder();

      while (true)
      {
         int i = in.read();

         if ((i == -1) || (i == '\n') || (i == '\r'))
         {
            return buff.toString();
         }

         buff.append((char) i);
      }

      // return new BufferedReader (new InputStreamReader (in)).readLine ();
   }

   //
   // Completion
   //

   private final List<org.jboss.forge.shell.console.jline.console.completer.Completer> completers = new LinkedList<org.jboss.forge.shell.console.jline.console.completer.Completer>();

   private org.jboss.forge.shell.console.jline.console.completer.CompletionHandler completionHandler = new org.jboss.forge.shell.console.jline.console.completer.CandidateListCompletionHandler();

   /**
    * Add the specified {@link org.jboss.forge.shell.console.jline.console.completer.Completer} to the list of handlers
    * for tab-completion.
    *
    * @param completer the {@link org.jboss.forge.shell.console.jline.console.completer.Completer} to add
    * @return true if it was successfully added
    */
   public boolean addCompleter(final org.jboss.forge.shell.console.jline.console.completer.Completer completer)
   {
      return completers.add(completer);
   }

   /**
    * Remove the specified {@link org.jboss.forge.shell.console.jline.console.completer.Completer} from the list of
    * handlers for tab-completion.
    *
    * @param completer The {@link org.jboss.forge.shell.console.jline.console.completer.Completer} to remove
    * @return True if it was successfully removed
    */
   public boolean removeCompleter(final org.jboss.forge.shell.console.jline.console.completer.Completer completer)
   {
      return completers.remove(completer);
   }

   /**
    * Returns an unmodifiable list of all the completers.
    */
   public Collection<org.jboss.forge.shell.console.jline.console.completer.Completer> getCompleters()
   {
      return Collections.unmodifiableList(completers);
   }

   public void setCompletionHandler(
            final org.jboss.forge.shell.console.jline.console.completer.CompletionHandler handler)
   {
      assert handler != null;
      this.completionHandler = handler;
   }

   public org.jboss.forge.shell.console.jline.console.completer.CompletionHandler getCompletionHandler()
   {
      return this.completionHandler;
   }

   /**
    * Use the completers to modify the buffer with the appropriate completions.
    *
    * @return true if successful
    */
   private boolean complete() throws IOException
   {
      // debug ("tab for (" + buf + ")");
      if (completers.size() == 0)
      {
         return false;
      }

      List<CharSequence> candidates = new LinkedList<CharSequence>();
      String bufstr = buf.buffer.toString();
      int cursor = buf.cursor;

      int position = -1;

      for (org.jboss.forge.shell.console.jline.console.completer.Completer comp : completers)
      {
         if ((position = comp.complete(bufstr, cursor, candidates)) != -1)
         {
            break;
         }
      }

      return (candidates.size() != 0) && getCompletionHandler().complete(this, candidates, position);
   }

   /**
    * The number of tab-completion candidates above which a warning will be prompted before showing all the candidates.
    */
   private int autoprintThreshold = Integer.getInteger(JLINE_COMPLETION_THRESHOLD, 100); // same default as bash

   /**
    * @param threshold the number of candidates to print without issuing a warning.
    */
   public void setAutoprintThreshold(final int threshold)
   {
      this.autoprintThreshold = threshold;
   }

   /**
    * @return the number of candidates to print without issuing a warning.
    */
   public int getAutoprintThreshold()
   {
      return autoprintThreshold;
   }

   private boolean paginationEnabled;

   /**
    * Whether to use pagination when the number of rows of candidates exceeds the height of the terminal.
    */
   public void setPaginationEnabled(final boolean enabled)
   {
      this.paginationEnabled = enabled;
   }

   /**
    * Whether to use pagination when the number of rows of candidates exceeds the height of the terminal.
    */
   public boolean isPaginationEnabled()
   {
      return paginationEnabled;
   }

   //
   // History
   //

   private org.jboss.forge.shell.console.jline.console.history.History history = new org.jboss.forge.shell.console.jline.console.history.MemoryHistory();

   public void setHistory(final org.jboss.forge.shell.console.jline.console.history.History history)
   {
      this.history = history;
   }

   public org.jboss.forge.shell.console.jline.console.history.History getHistory()
   {
      return history;
   }

   private boolean historyEnabled = true;

   /**
    * Whether or not to add new commands to the history buffer.
    */
   public void setHistoryEnabled(final boolean enabled)
   {
      this.historyEnabled = enabled;
   }

   /**
    * Whether or not to add new commands to the history buffer.
    */
   public boolean isHistoryEnabled()
   {
      return historyEnabled;
   }

   /**
    * Move up or down the history tree.
    */
   private boolean moveHistory(final boolean next) throws IOException
   {
      if (next && !history.next())
      {
         return false;
      }
      else if (!next && !history.previous())
      {
         return false;
      }

      setBuffer(history.current());

      return true;
   }

   //
   // Printing
   //

   public static final String CR = System.getProperty("line.separator");

   /**
    * Output the specified character to the output stream without manipulating the current buffer.
    */
   private void print(final int c) throws IOException
   {
      if (c == '\t')
      {
         byte[] chars = new byte[TAB_WIDTH];
         Arrays.fill(chars, (byte) ' ');
         shell.write(chars);
         return;
      }

      shell.write(c);
   }

   private void print(final char c) throws IOException
   {
      print((int) c);
   }

   private void print(final char c, final int i) throws IOException
   {
      if (i == 1)
      {
         print(c);
      }
      else
      {
         byte[] chars = new byte[i];
         Arrays.fill(chars, (byte) c);
         print(chars);
      }
   }

   /**
    * Output the specified characters to the output stream without manipulating the current buffer.
    */
   private void print(final byte... buff) throws IOException
   {
      int len = 0;
      for (byte c : buff)
      {
         if (c == '\t')
         {
            len += TAB_WIDTH;
         }
         else
         {
            len++;
         }
      }

      byte[] chars;
      if (len == buff.length)
      {
         chars = buff;
      }
      else
      {
         chars = new byte[len];
         int pos = 0;
         for (byte c : buff)
         {
            if (c == '\t')
            {
               Arrays.fill(chars, pos, pos + TAB_WIDTH, (byte) ' ');
               pos += TAB_WIDTH;
            }
            else
            {
               chars[pos] = c;
               pos++;
            }
         }
      }

      shell.write(chars);
   }

   private void print(final byte c, final int num) throws IOException
   {
      if (num == 1)
      {
         print(c);
      }
      else
      {
         byte[] chars = new byte[num];
         Arrays.fill(chars, c);
         print(chars);
      }
   }

   /**
    * Output the specified string to the output stream (but not the buffer).
    */
   public final void print(final CharSequence s) throws IOException
   {
      assert s != null;
      print(s.toString().getBytes());
   }

   public final void println(final CharSequence s) throws IOException
   {
      assert s != null;
      print(s.toString().getBytes());
      println();
   }

   /**
    * Output a platform-dependant newline.
    */
   public final void println() throws IOException
   {
      print(CR);
      // flush();
   }

   //
   // Actions
   //

   /**
    * Issue a delete.
    *
    * @return true if successful
    */
   public final boolean delete() throws IOException
   {
      return delete(1) == 1;
   }

   // FIXME: delete(int) only used by above + the return is always 1 and num is ignored

   /**
    * Issue <em>num</em> deletes.
    *
    * @return the number of characters backed up
    */
   private int delete(final int num) throws IOException
   {
      // TODO: Try to use jansi for this

      /*
       * Commented out because of DWA-2949: if (buf.cursor == 0) { return 0; }
       */

      buf.buffer.delete(buf.cursor, buf.cursor + 1);
      drawBuffer(1);

      return 1;
   }

   /**
    * Kill the buffer ahead of the current cursor position.
    *
    * @return true if successful
    */
   public boolean killLine() throws IOException
   {
      int cp = buf.cursor;
      int len = buf.buffer.length();

      if (cp >= len)
      {
         return false;
      }

      int num = buf.buffer.length() - cp;
      clearAhead(num);

      for (int i = 0; i < num; i++)
      {
         buf.buffer.deleteCharAt(len - i - 1);
      }

      return true;
   }

   /**
    * Clear the screen by issuing the ANSI "clear screen" code.
    */
   public boolean clearScreen() throws IOException
   {
      if (!terminal.isAnsiSupported())
      {
         return false;
      }

      // send the ANSI code to clear the screen
      printAnsiSequence("2J");

      // then send the ANSI code to go to position 1,1
      printAnsiSequence("1;1H");

      redrawLine();

      return true;
   }

   /**
    * Issue an audible keyboard bell, if {@link #isBellEnabled} return true.
    */
   public void beep() throws IOException
   {
      if (isBellEnabled())
      {
         print(KEYBOARD_BELL);
         // need to flush so the console actually beeps
         flush();
      }
   }

   /**
    * Paste the contents of the clipboard into the console buffer
    *
    * @return true if clipboard contents pasted
    */
   public boolean paste() throws IOException
   {
      Clipboard clipboard;
      try
      { // May throw ugly exception on system without X
         clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
      }
      catch (Exception e)
      {
         return false;
      }

      if (clipboard == null)
      {
         return false;
      }

      Transferable transferable = clipboard.getContents(null);

      if (transferable == null)
      {
         return false;
      }

      try
      {
         @SuppressWarnings("deprecation")
         Object content = transferable.getTransferData(DataFlavor.plainTextFlavor);

         // This fix was suggested in bug #1060649 at
         // http://sourceforge.net/tracker/index.php?func=detail&aid=1060649&group_id=64033&atid=506056
         // to get around the deprecated DataFlavor.plainTextFlavor, but it
         // raises a UnsupportedFlavorException on Mac OS X

         if (content == null)
         {
            try
            {
               content = new DataFlavor().getReaderForText(transferable);
            }
            catch (Exception e)
            {
               // ignore
            }
         }

         if (content == null)
         {
            return false;
         }

         String value;

         if (content instanceof Reader)
         {
            // TODO: we might want instead connect to the input stream
            // so we can interpret individual lines
            value = "";
            String line;

            BufferedReader read = new BufferedReader((Reader) content);
            while ((line = read.readLine()) != null)
            {
               if (value.length() > 0)
               {
                  value += "\n";
               }

               value += line;
            }
         }
         else
         {
            value = content.toString();
         }

         if (value == null)
         {
            return true;
         }

         putString(value);

         return true;
      }
      catch (UnsupportedFlavorException e)
      {
         org.jboss.forge.shell.console.jline.internal.Log.error("Paste failed: ", e);

         return false;
      }
   }

   //
   // Triggered Actions
   //

   private final Map<Character, ActionListener> triggeredActions = new HashMap<Character, ActionListener>();

   /**
    * Adding a triggered Action allows to give another curse of action if a character passed the pre-processing.
    * <p/>
    * Say you want to close the application if the user enter q. addTriggerAction('q', new ActionListener(){
    * System.exit(0); }); would do the trick.
    */
   public void addTriggeredAction(final char c, final ActionListener listener)
   {
      triggeredActions.put(c, listener);
   }

   //
   // Formatted Output
   //

   /**
    * Output the specified {@link Collection} in proper columns.
    */
   public void printColumns(final Collection<? extends CharSequence> items) throws IOException
   {
      if ((items == null) || items.isEmpty())
      {
         return;
      }

      int width = getTerminal().getWidth();
      int height = getTerminal().getHeight();

      int maxWidth = 0;
      for (CharSequence item : items)
      {
         maxWidth = Math.max(maxWidth, item.length());
      }
      org.jboss.forge.shell.console.jline.internal.Log.debug("Max width: ", maxWidth);

      int showLines;
      if (isPaginationEnabled())
      {
         showLines = height - 1; // page limit
      }
      else
      {
         showLines = Integer.MAX_VALUE;
      }

      StringBuilder buff = new StringBuilder();
      for (CharSequence item : items)
      {
         if ((buff.length() + maxWidth) > width)
         {
            println(buff);
            buff.setLength(0);

            if (--showLines == 0)
            {
               // Overflow
               print(resources.getString("display-more"));
               flush();
               int c = readVirtualKey();
               if ((c == '\r') || (c == '\n'))
               {
                  // one step forward
                  showLines = 1;
               }
               else if (c != 'q')
               {
                  // page forward
                  showLines = height - 1;
               }

               back(resources.getString("display-more").length());
               if (c == 'q')
               {
                  // cancel
                  break;
               }
            }
         }

         // NOTE: toString() is important here due to AnsiString being retarded
         buff.append(item.toString());
         for (int i = 0; i < ((maxWidth + 3) - item.length()); i++)
         {
            buff.append(' ');
         }
      }

      if (buff.length() > 0)
      {
         println(buff);
      }
   }

   //
   // Non-supported Terminal Support
   //

   private Thread maskThread;

   private void beforeReadLine(final String prompt, final Character mask)
   {
      if ((mask != null) && (maskThread == null))
      {
         final String fullPrompt = "\r" + prompt
                  + "                 "
                  + "                 "
                  + "                 "
                  + "\r" + prompt;

         maskThread = new Thread()
         {
            @Override
            public void run()
            {
               while (!interrupted())
               {
                  try
                  {
                     BufferManager out = getShell().getBufferManager();
                     out.write(fullPrompt);
                     out.flushBuffer();
                     sleep(3);
                  }

                  catch (InterruptedException e)
                  {
                     return;
                  }
               }
            }
         };

         maskThread.setPriority(Thread.MAX_PRIORITY);
         maskThread.setDaemon(true);
         maskThread.start();
      }
   }

   private void afterReadLine()
   {
      if ((maskThread != null) && maskThread.isAlive())
      {
         maskThread.interrupt();
      }

      maskThread = null;
   }

   /**
    * Erases the current line with the existing prompt, then redraws the line with the provided prompt and buffer
    *
    * @param prompt the new prompt
    * @param buffer the buffer to be drawn
    * @param cursorDest where you want the cursor set when the line has been drawn. -1 for end of line.
    */
   public void resetPromptLine(final String prompt, final String buffer, int cursorDest) throws IOException
   {
      // move cursor to end of line
      moveToEnd();

      // backspace all text, including prompt
      buf.buffer.append(this.prompt);
      buf.cursor += this.prompt.length();
      this.prompt = "";
      backspaceAll();

      this.prompt = prompt;
      redrawLine();
      setBuffer(buffer);

      // move cursor to destination (-1 will move to end of line)
      if (cursorDest < 0)
         cursorDest = buffer.length();
      setCursorPosition(cursorDest);

      flush();
   }

   public void printSearchStatus(final String searchTerm, final String match) throws IOException
   {
      String prompt = "(reverse-i-search)`" + searchTerm + "': ";
      String buffer = match;
      int cursorDest = match.indexOf(searchTerm);
      resetPromptLine(prompt, buffer, cursorDest);
   }

   public void restoreLine(final String originalPrompt, final int cursorDest) throws IOException
   {
      // TODO move cursor to matched string
      String prompt = lastLine(originalPrompt);
      String buffer = buf.buffer.toString();
      resetPromptLine(prompt, buffer, cursorDest);
   }

   //
   // History search
   //

   /**
    * Search backward in history from a given position.
    *
    * @param searchTerm substring to search for.
    * @param startIndex the index from which on to search
    * @return index where this substring has been found, or -1 else.
    */
   public int searchBackwards(final String searchTerm, final int startIndex)
   {
      return searchBackwards(searchTerm, startIndex, false);
   }

   /**
    * Search backwards in history from the current position.
    *
    * @param searchTerm substring to search for.
    * @return index where the substring has been found, or -1 else.
    */
   public int searchBackwards(final String searchTerm)
   {
      return searchBackwards(searchTerm, history.index());
   }

   public int searchBackwards(final String searchTerm, final int startIndex, final boolean startsWith)
   {
      ListIterator<org.jboss.forge.shell.console.jline.console.history.History.Entry> it = history.entries(startIndex);
      while (it.hasPrevious())
      {
         org.jboss.forge.shell.console.jline.console.history.History.Entry e = it.previous();
         if (startsWith)
         {
            if (e.value().toString().startsWith(searchTerm))
            {
               return e.index();
            }
         }
         else
         {
            if (e.value().toString().contains(searchTerm))
            {
               return e.index();
            }
         }
      }
      return -1;
   }

   //
   // Helpers
   //

   /**
    * Checks to see if the specified character is a delimiter. We consider a character a delimiter if it is anything but
    * a letter or digit.
    *
    * @param c The character to test
    * @return True if it is a delimiter
    */
   private boolean isDelimiter(final char c)
   {
      return !Character.isLetterOrDigit(c);
   }

   private static final String ESCAPE_STR = new String(new char[] { 27, '[' });

   private void printAnsiSequence(final String sequence) throws IOException
   {
      print(ESCAPE_STR + sequence);
   }

   // return column position, reported by the terminal
   private int getCurrentPosition()
   {
      // check for forge.debug.no_auto_init_streams system property to disable for unit tests
      if (terminal.isAnsiSupported() && !Boolean.getBoolean("forge.debug.no_auto_init_streams"))
      {
         try
         {
            printAnsiSequence("6n");
            flush();
            StringBuffer b = new StringBuffer(8);
            // position is sent as <ESC>[{ROW};{COLUMN}R
            int r;
            while (((r = in.read()) > -1) && (r != 'R'))
            {
               if ((r != 27) && (r != '['))
               {
                  b.append((char) r);
               }
            }
            String[] pos = b.toString().split(";");
            return Integer.parseInt(pos[1]);
         }
         catch (Exception x)
         {
            // no luck
         }
      }

      return -1; // TODO: throw exception instead?
   }

}
