/****************************************************************************
 * apps/system/vi/vi.c
 *
 *   Copyright (C) 2014 Gregory Nutt. All rights reserved.
 *   Author: Gregory Nutt <gnutt@nuttx.org>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 * 3. Neither the name NuttX nor the names of its contributors may be
 *    used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>

#include <sys/stat.h>

#include <stdarg.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <ctype.h>
#include <syslog.h>
#include <errno.h>
#include <debug.h>

#include <nuttx/ascii.h>
#include <nuttx/vt100.h>

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

#ifndef CONFIG_SYSTEM_VI_ROWS
#  define CONFIG_SYSTEM_VI_ROWS 16
#endif

#ifndef CONFIG_SYSTEM_VI_COLS
#  define CONFIG_SYSTEM_VI_COLS 64
#endif

/* Some environments may return CR as end-of-line, others LF, and others
 * both.  If not specified, the logic here assumes either (but not both) as
 * the default.
 */

#if defined(CONFIG_EOL_IS_CR)
#  undef  CONFIG_EOL_IS_LF
#  undef  CONFIG_EOL_IS_BOTH_CRLF
#  undef  CONFIG_EOL_IS_EITHER_CRLF
#elif defined(CONFIG_EOL_IS_LF)
#  undef  CONFIG_EOL_IS_CR
#  undef  CONFIG_EOL_IS_BOTH_CRLF
#  undef  CONFIG_EOL_IS_EITHER_CRLF
#elif defined(CONFIG_EOL_IS_BOTH_CRLF)
#  undef  CONFIG_EOL_IS_CR
#  undef  CONFIG_EOL_IS_LF
#  undef  CONFIG_EOL_IS_EITHER_CRLF
#elif defined(CONFIG_EOL_IS_EITHER_CRLF)
#  undef  CONFIG_EOL_IS_CR
#  undef  CONFIG_EOL_IS_LF
#  undef  CONFIG_EOL_IS_BOTH_CRLF
#else
#  undef  CONFIG_EOL_IS_CR
#  undef  CONFIG_EOL_IS_LF
#  undef  CONFIG_EOL_IS_BOTH_CRLF
#  define CONFIG_EOL_IS_EITHER_CRLF 1
#endif

/* Control characters */

#undef  CTRL
#define CTRL(a)         ((a) & 0x1f)

#define VI_BEL(vi)      vi_putch(vi,CTRL('G'))

/* Sizes of things */

#define MAX_STRING      64   /* The maximum size of a filename or search string */
#define SCRATCH_BUFSIZE 128  /* The maximum size of the scratch buffer */

#define TEXT_GULP_SIZE  512  /* Text buffer allocations are managed with this unit */
#define TEXT_GULP_MASK  511  /* Mask for aligning buffer allocation sizes */
#define ALIGN_GULP(x)   (((x) + TEXT_GULP_MASK) & ~TEXT_GULP_MASK)

#define TABSIZE         8    /* A TAB is eight characters */
#define TABMASK         7    /* Mask for TAB alignment */
#define NEXT_TAB(p)     (((p) + TABSIZE) & ~TABMASK)

/* Parsed command action bits */

#define CMD_READ        (1 << 0) /* Bit 0: Read */
#define CMD_WRITE_MASK  (3 << 1) /* Bits 1-2: x1=Write operation */
#  define CMD_WRITE     (1 << 1) /*           01=Write (without overwriting) */
#  define CMD_OWRITE    (3 << 1) /*           11=Overwrite */
#define CMD_QUIT_MASK   (3 << 3) /* Bits 3-4: x1=Quite operation */
#  define CMD_QUIT      (1 << 3) /*           01=Quit if saved */
#  define CMD_DISCARD   (3 << 3) /*           11=Quit without saving */

#define CMD_NONE        (0)                     /* No command */
#define CMD_WRITE_QUIT  (CMD_WRITE | CMD_QUIT)  /* Write file and quit command */
#define CMD_OWRITE_QUIT (CMD_OWRITE | CMD_QUIT) /* Overwrite file and quit command */

#define IS_READ(a)      (((uint8_t)(a) & CMD_READ) != 0)
#define IS_WRITE(a)     (((uint8_t)(a) & CMD_WRITE) != 0)
#define IS_OWRITE(a)    (((uint8_t)(a) & CMD_WRITE_MASK) == CMD_OWRITE)
#define IS_NOWRITE(a)   (((uint8_t)(a) & CMD_WRITE_MASK) == CMD_WRITE)
#define IS_QUIT(a)      (((uint8_t)(a) & CMD_QUIT) != 0)
#define IS_DISCARD(a)   (((uint8_t)(a) & CMD_QUIT_MASK) == CMD_DISCARD)
#define IS_NDISCARD(a)  (((uint8_t)(a) & CMD_QUIT_MASK) == CMD_QUIT)

#define CMD_FILE_MASK   (CMD_READ | CMD_WRITE)
#define USES_FILE(a)    (((uint8_t)(a) & CMD_FILE_MASK) != 0)

/* Debug */

#ifndef CONFIG_SYSTEM_VI_DEBUGLEVEL
#  define CONFIG_SYSTEM_VI_DEBUGLEVEL 0
#endif

#ifdef CONFIG_CPP_HAVE_VARARGS
#  if CONFIG_SYSTEM_VI_DEBUGLEVEL > 0
#    define vidbg(format, ...) \
       syslog(LOG_DEBUG, EXTRA_FMT format EXTRA_ARG, ##__VA_ARGS__)
#    define vvidbg(format, ap) \
       vsyslog(LOG_DEBUG, format, ap)
#  else
#    define vidbg(x...)
#    define vvidbg(x...)
#  endif

#  if CONFIG_SYSTEM_VI_DEBUGLEVEL > 1
#    define viinfo(format, ...) \
       syslog(LOG_DEBUG, EXTRA_FMT format EXTRA_ARG, ##__VA_ARGS__)
#  else
#    define viinfo(x...)
#  endif
#else
#  if CONFIG_SYSTEM_VI_DEBUGLEVEL > 0
#    define vidbg  vi_debug
#    define vvidbg vi_vdebug
#  else
#    define vidbg  (void)
#    define vvidbg (void)
#  endif

#  if CONFIG_SYSTEM_VI_DEBUGLEVEL > 1
#    define viinfo vi_debug
#  else
#    define viinfo (void)
#  endif
#endif

/****************************************************************************
 * Private Types
 ****************************************************************************/
/* VI Key Bindings */

enum vi_cmdmode_key_e
{
  KEY_CMDMODE_BEGINLINE   = '0',  /* Move cursor to start of current line */
  KEY_CMDMODE_APPEND      = 'a',  /* Enter insertion mode after current character */
  KEY_CMDMODE_DEL_LINE    = 'd',  /* "dd" deletes a lines */
  KEY_CMDMODE_LEFT        = 'h',  /* Move left one character */
  KEY_CMDMODE_INSERT      = 'i',  /* Enter insertion mode before current character */
  KEY_CMDMODE_DOWN        = 'j',  /* Move down one line */
  KEY_CMDMODE_UP          = 'k',  /* Move up one line */
  KEY_CMDMODE_RIGHT       = 'l',  /* Move right one character */
  KEY_CMDMODE_MARK        = 'm',  /* Place a mark beginning at the current cursor position */
  KEY_CMDMODE_OPENBELOW   = 'o',  /* Enter insertion mode in new line below current */
  KEY_CMDMODE_PASTE       = 'p',  /* Paste line(s) from into text after current line */
  KEY_CMDMODE_REPLACECH   = 'r',  /* Replace character(s) under cursor */
  KEY_CMDMODE_YANK        = 'y',  /* "yy" yanks the current line(s) into the buffer */
  KEY_CMDMODE_DEL         = 'x',  /* Delete a single character */

  KEY_CMDMODE_APPENDEND   = 'A',  /* Enter insertion mode at the end of the current line */
  KEY_CMDMODE_GOTO        = 'G',  /* Got to line */
  KEY_CMDMODE_INSBEGIN    = 'I',  /* Enter insertion mode at the beginning of the current */
  KEY_CMDMODE_OPENABOVE   = 'O',  /* Enter insertion mode in new line above current line */
  KEY_CMDMODE_REPLACE     = 'R',  /* Replace character(s) under cursor until ESC */

  KEY_CMDMODE_COLONMODE   = ':',  /* The next character command prefaced with a colon */
  KEY_CMDMODE_FINDMODE    = '/',  /* Enter forward search string */
  KEY_CMDMODE_ENDLINE     = '$',  /* Move cursor to end of current line */

  KEY_CMDMODE_PAGEUP      = CTRL('b'), /* Move backward one screen */
  KEY_CMDMODE_HALFDOWN    = CTRL('d'), /* Move down (forward) one half screen */
  KEY_CMDMODE_PAGEDOWN    = CTRL('f'), /* Move forward one screen */
  KEY_CMDMODE_REDRAW      = CTRL('l'), /* Redraws the screen */
  KEY_CMDMODE_REDRAW2     = CTRL('r'), /* Redraws the screen, removing deleted lines */
  KEY_CMDMODE_HALFUP      = CTRL('u')  /* Move up (back) one half screen */
};

enum vi_insmode_key_e
{
  KEY_INSMODE_QUOTE       = '\\', /* The next character is quote (use literal value) */
};

enum vi_colonmode_key_e
{
  KEY_COLMODE_READ        = 'r',  /* Read file */
  KEY_COLMODE_QUIT        = 'q',  /* Quit vi */
  KEY_COLMODE_WRITE       = 'w',  /* Write file */
  KEY_COLMODE_FORCE       = '!',  /* Force operation */
  KEY_COLMODE_QUOTE       = '\\'  /* The next character is quote (use literal value) */
};

enum vi_findmode_key_e
{
  KEY_FINDMODE_QUOTE      = '\\'  /* The next character is quote (use literal value) */
};

/* VI modes */

enum vi_mode_s
{
  MODE_COMMAND            = 0,    /* ESC         Command mode */
  SUBMODE_COLON,                  /* :           Command sub-mode */
  SUBMODE_FIND,                   /* /           Search sub-mode */
  SUBMODE_REPLACECH,              /* r           Replace sub-mode 1 */
  MODE_INSERT,                    /* i,I,a,A,o,O Insert mode */
  MODE_REPLACE                    /* R           Replace sub-mode 2 */
};

/* This structure represents a cursor position */

struct vi_pos_s
{
  uint16_t row;
  uint16_t column;
};

/* This structure describes the overall state of the editor */

struct vi_s
{
  struct vi_pos_s cursor;   /* Current cursor position */
  struct vi_pos_s cursave;  /* Saved cursor position */
  struct vi_pos_s display;  /* Display size */
  off_t curpos;             /* The current cursor offset into the text buffer */
  off_t textsize;           /* The size of the text buffer */
  off_t winpos;             /* Offset corresponding to the start of the display */
  off_t prevpos;            /* Previous display position */
  uint16_t hscroll;         /* Horizontal display offset */
  uint16_t value;           /* Numeric value entered prior to a command */
  uint8_t mode;             /* See enum vi_mode_s */
  uint8_t cmdlen;           /* Length of the command in the scratch[] buffer */
  bool modified;            /* True: The file has modified */
  bool error;               /* True: There is an error message on the last line */
  bool delarm;              /* One more 'd' and the line(s) will be deleted */
  bool yankarm;             /* One more 'y' and the line(s) will be yanked */

  /* Buffers */

  FAR char *text;           /* Dynamically allocated text buffer */
  size_t txtalloc;          /* Current allocated size of the text buffer */
  FAR char *yank;           /* Dynamically allocated yank buffer */
  size_t yankalloc;         /* Current allocated size of the yank buffer */

  char filename[MAX_STRING];     /* Holds the currently selected filename */
  char findstr[MAX_STRING];      /* Holds the current search string */
  char scratch[SCRATCH_BUFSIZE]; /* For general, scratch usage */
};

/****************************************************************************
 * Private Function Prototypes
 ****************************************************************************/

/* Low-level display and data entry functions */

static void     vi_write(FAR struct vi_s *vi, FAR const char *buffer,
                  size_t buflen);
static void     vi_putch(FAR struct vi_s *vi, char ch);
static char     vi_getch(FAR struct vi_s *vi);
#if 0 /* Not used */
static void     vi_blinkon(FAR struct vi_s *vi);
#endif
static void     vi_boldon(FAR struct vi_s *vi);
static void     vi_reverseon(FAR struct vi_s *vi);
static void     vi_attriboff(FAR struct vi_s *vi);
static void     vi_cursoron(FAR struct vi_s *vi);
static void     vi_cursoroff(FAR struct vi_s *vi);
#if 0 /* Not used */
static void     vi_cursorhome(FAR struct vi_s *vi);
#endif
static void     vi_setcursor(FAR struct vi_s *vi, uint16_t row,
                  uint16_t column);
static void     vi_clrtoeol(FAR struct vi_s *vi);
#if 0 /* Not used */
static void     vi_clrscreen(FAR struct vi_s *vi);
#endif

/* Error display */

static void     vi_error(FAR struct vi_s *vi, FAR const char *fmt, ...);

/* Line positioning */

static off_t    vi_linebegin(FAR struct vi_s *vi, off_t pos);
static off_t    vi_prevline(FAR struct vi_s *vi, off_t pos);
static off_t    vi_lineend(FAR struct vi_s *vi, off_t pos);
static off_t    vi_nextline(FAR struct vi_s *vi, off_t pos);

/* Text buffer management */

static bool     vi_extendtext(FAR struct vi_s *vi, off_t pos,
                  size_t increment);
static void     vi_shrinkpos(off_t delpos, size_t delsize, FAR off_t *pos);
static void     vi_shrinktext(FAR struct vi_s *vi, off_t pos, size_t size);

/* File access */

static bool     vi_insertfile(FAR struct vi_s *vi, off_t pos,
                  FAR const char *filename);
static bool     vi_savetext(FAR struct vi_s *vi, FAR const char *filename,
                  off_t pos, size_t size);
static bool     vi_checkfile(FAR struct vi_s *vi, FAR const char *filename);

/* Mode management */

static void     vi_setmode(FAR struct vi_s *vi, uint8_t mode, long value);
static void     vi_setsubmode(FAR struct vi_s *vi, uint8_t mode,
                  char prompt, long value);
static void     vi_exitsubmode(FAR struct vi_s *vi, uint8_t mode);

/* Display management */

static void     vi_windowpos(FAR struct vi_s *vi, off_t start, off_t end,
                  uint16_t *pcolumn, off_t *ppos);
static void     vi_scrollcheck(FAR struct vi_s *vi);
static void     vi_showtext(FAR struct vi_s *vi);

/* Command mode */

static void     vi_cusorup(FAR struct vi_s *vi, int nlines);
static void     vi_cursordown(FAR struct vi_s *vi, int nlines);
static off_t    vi_cursorleft(FAR struct vi_s *vi, off_t curpos,
                  int ncolumns);
static off_t    vi_cursorright(FAR struct vi_s *vi, off_t curpos,
                  int ncolumns);
static void     vi_delforward(FAR struct vi_s *vi);
static void     vi_delbackward(FAR struct vi_s *vi);
static void     vi_linerange(FAR struct vi_s *vi, off_t *start, off_t *end);
static void     vi_delline(FAR struct vi_s *vi);
static void     vi_yank(FAR struct vi_s *vi);
static void     vi_paste(FAR struct vi_s *vi);
static void     vi_gotoline(FAR struct vi_s *vi);
static void     vi_cmd_mode(FAR struct vi_s *vi);

/* Command sub-modes */

static void     vi_cmdch(FAR struct vi_s *vi, char ch);
static void     vi_cmdbackspace(FAR struct vi_s *vi);

static void     vi_parsecolon(FAR struct vi_s *vi);
static void     vi_cmd_submode(FAR struct vi_s *vi);

static bool     vi_findstring(FAR struct vi_s *vi);
static void     vi_parsefind(FAR struct vi_s *vi);
static void     vi_find_submode(FAR struct vi_s *vi);

static void     vi_replacech(FAR struct vi_s *vi, char ch);
static void     vi_replacech_submode(FAR struct vi_s *vi);

/* Insert and replace modes */

static void     vi_insertch(FAR struct vi_s *vi, char ch);
static void     vi_insert_mode(FAR struct vi_s *vi);

static void     vi_replace_mode(FAR struct vi_s *vi);

/* Command line processing */

static void     vi_release(FAR struct vi_s *vi);
static void     vi_showusage(FAR struct vi_s *vi, FAR const char *progname,
                  int exitcode);

/****************************************************************************
 * Private Data
 ****************************************************************************/

/* VT100 escape sequences */

static const char g_cursoron[]      = VT100_CURSORON;
static const char g_cursoroff[]     = VT100_CURSOROFF;
#if 0 /* Not used */
static const char g_cursorhome[]    = VT100_CURSORHOME;
#endif
static const char g_erasetoeol[]    = VT100_CLEAREOL;
#if 0 /* Not used */
static const char g_clrscreen[]     = VT100_CLEARSCREEN;
#endif
static const char g_index[]         = VT100_INDEX;
static const char g_revindex[]      = VT100_REVINDEX;
static const char g_attriboff[]     = VT100_MODESOFF;
static const char g_boldon[]        = VT100_BOLD;
static const char g_reverseon[]     = VT100_REVERSE;
#if 0 /* Not used */
static const char g_blinkon[]       = VT100_BLINK;
static const char g_boldoff[]       = VT100_BOLDOFF;
static const char g_reverseoff[]    = VT100_REVERSEOFF;
static const char g_blinkoff[]      = VT100_BLINKOFF;
#endif

static const char g_fmtcursorpos[]  = VT100_FMT_CURSORPOS;

/* Error format strings */

static const char g_fmtallocfail[]  = "Failed to allocate memory";
static const char g_fmtcmdfail[]    = "%s failed: %d";
static const char g_fmtnotfile[]    = "%s is not a regular file";
static const char g_fmtfileexists[] = "File exists (add ! to override)";
static const char g_fmtmodified[]   = "No write since last change (add ! to override)";
static const char g_fmtnotvalid[]   = "Command not valid";
static const char g_fmtnotcmd[]     = "Not an editor command: %s";

/****************************************************************************
 * Private Functions
 ****************************************************************************/

/****************************************************************************
 * Name: vi_vdebug and vi_debug
 *
 * Description:
 *   Print a debug message to the syslog
 *
 ****************************************************************************/

#if !defined(CONFIG_CPP_HAVE_VARARGS) && CONFIG_SYSTEM_VI_DEBUGLEVEL > 0
static inline int vi_vdebug(FAR const char *fmt, va_list ap)
{
  return vsyslog(LOG_DEBUG, fmt, ap);
}

static int vi_debug(FAR const char *fmt, ...)
{
  va_list ap;
  int ret;

  /* Let vsyslog do the real work */

  va_start(ap, fmt);
  ret = vsyslog(LOG_DEBUG, fmt, ap);
  va_end(ap);
  return ret;
}
#endif

/****************************************************************************
 * Low-level display and data entry functions
 ****************************************************************************/

/****************************************************************************
 * Name: vi_write
 *
 * Description:
 *   Write a sequence of bytes to the console device (stdout, fd = 1).
 *
 ****************************************************************************/

static void vi_write(FAR struct vi_s *vi, FAR const char *buffer,
                     size_t buflen)
{
  ssize_t nwritten;
  size_t  nremaining = buflen;

  //viinfo("buffer=%p buflen=%d\n", buffer, (int)buflen);

  /* Loop until all bytes have been successfully written (or until a
   * unrecoverable error is encountered)
   */

  do
    {
      /* Take the next gulp */

      nwritten = write(1, buffer, buflen);

      /* Handle write errors.  write() should neve return 0. */

      if (nwritten <= 0)
        {
          /* EINTR is not really an error; it simply means that a signal was
           * received while waiting for write.
           */

          int errcode = errno;
          if (nwritten == 0 || errcode != EINTR)
            {
              fprintf(stderr, "ERROR: write to stdout failed: %d\n",
                      errcode);
              exit(EXIT_FAILURE);
            }
        }

      /* Decrement the count of bytes remaining to be sent (to handle the
       * case of a partial write)
       */

      else
        {
          nremaining -= nwritten;
        }
    }
  while (nremaining > 0);
}

/****************************************************************************
 * Name: vi_putch
 *
 * Description:
 *   Write a single character to the console device.
 *
 ****************************************************************************/

static void vi_putch(FAR struct vi_s *vi, char ch)
{
  vi_write(vi, &ch, 1);
}

/****************************************************************************
 * Name: vi_getch
 *
 * Description:
 *   Get a single character from the console device (stdin, fd = 0).
 *
 ****************************************************************************/

static char vi_getch(FAR struct vi_s *vi)
{
  char buffer;
  ssize_t nread;

  /* Loop until we successfully read a character (or until an unexpected
   * error occurs).
   */

  do
    {
      /* Read one character from the incoming stream */

      nread = read(0, &buffer, 1);

      /* Check for error or end-of-file. */

      if (nread <= 0)
        {
          /* EINTR is not really an error; it simply means that a signal we
           * received while waiting for input.
           */

          int errcode = errno;
          if (nread == 0 || errcode != EINTR)
            {
              fprintf(stderr, "ERROR: read from stdin failed: %d\n",
                      errcode);
              exit(EXIT_FAILURE);
            }
        }
    }
  while (nread < 1);

  /* On success, return the character that was read */

  viinfo("Returning: %c[%02x]\n", isprint(buffer) ? buffer : '.', buffer);
  return buffer;
}

/****************************************************************************
 * Name: vi_blinkon
 *
 * Description:
 *   Enable the blinking attribute at the current cursor location
 *
 ****************************************************************************/

#if 0 /* Not used */
static void vi_blinkon(FAR struct vi_s *vi)
{
  /* Send the VT100 BLINKON command */

  vi_write(vi, g_blinkon, sizeof(g_blinkon));
}
#endif

/****************************************************************************
 * Name: vi_boldon
 *
 * Description:
 *   Enable the blinking attribute at the current cursor location
 *
 ****************************************************************************/

static void vi_boldon(FAR struct vi_s *vi)
{
  /* Send the VT100 BOLDON command */

  vi_write(vi, g_boldon, sizeof(g_boldon));
}

/****************************************************************************
 * Name: vi_reverseon
 *
 * Description:
 *   Enable the blinking attribute at the current cursor location
 *
 ****************************************************************************/

static void vi_reverseon(FAR struct vi_s *vi)
{
  /* Send the VT100 REVERSON command */

  vi_write(vi, g_reverseon, sizeof(g_reverseon));
}

/****************************************************************************
 * Name:
 *
 * Description:
 *   Disable all previously selected attributes.
 *
 ****************************************************************************/

static void vi_attriboff(FAR struct vi_s *vi)
{
  /* Send the VT100 ATTRIBOFF command */

  vi_write(vi, g_attriboff, sizeof(g_attriboff));
}

/****************************************************************************
 * Name: vi_cursoron
 *
 * Description:
 *   Turn on the cursor
 *
 ****************************************************************************/

static void vi_cursoron(FAR struct vi_s *vi)
{
  /* Send the VT100 CURSORON command */

  vi_write(vi, g_cursoron, sizeof(g_cursoron));
}

/****************************************************************************
 * Name: vi_cursoroff
 *
 * Description:
 *   Turn off the cursor
 *
 ****************************************************************************/

static void vi_cursoroff(FAR struct vi_s *vi)
{
  /* Send the VT100 CURSOROFF command */

  vi_write(vi, g_cursoroff, sizeof(g_cursoroff));
}

/****************************************************************************
 * Name: vi_cursorhome
 *
 * Description:
 *   Move the current cursor to the upper left hand corner of the display
 *
 ****************************************************************************/

#if 0 /* Not used */
static void vi_cursorhome(FAR struct vi_s *vi)
{
  /* Send the VT100 CURSORHOME command */

  vi_write(vi, g_cursorhome, sizeof(g_cursorhome));
}
#endif

/****************************************************************************
 * Name: vi_setcursor
 *
 * Description:
 *   Move the current cursor position to position (row,col)
 *
 ****************************************************************************/

static void vi_setcursor(FAR struct vi_s *vi, uint16_t row, uint16_t column)
{
  char buffer[16];
  int len;

  viinfo("row=%d column=%d\n", row, column);

  /* Format the cursor position command.  The origin is (1,1). */

  len = snprintf(buffer, 16, g_fmtcursorpos, row + 1, column + 1);

  /* Send the VT100 CURSORPOS command */

  vi_write(vi, buffer, len);
}

/****************************************************************************
 * Name: vi_clrtoeol
 *
 * Description:
 *   Clear the display from the current cursor position to the end of the
 *   current line.
 *
 ****************************************************************************/

static void vi_clrtoeol(FAR struct vi_s *vi)
{
  /* Send the VT100 ERASETOEOL command */

  vi_write(vi, g_erasetoeol, sizeof(g_erasetoeol));
}

/****************************************************************************
 * Name: vi_clrscreen
 *
 * Description:
 *   Clear the entire display
 *
 ****************************************************************************/

#if 0 /* Not used */
static void vi_clrscreen(FAR struct vi_s *vi)
{
  /* Send the VT100 CLRSCREEN command */

  vi_write(vi, g_clrscreen, sizeof(g_clrscreen));
}
#endif

/****************************************************************************
 * Name: vi_scrollup
 *
 * Description:
 *   Scroll the display up 'nlines' by sending the VT100 INDEX command.
 *
 ****************************************************************************/

static void vi_scrollup(FAR struct vi_s *vi, uint16_t nlines)
{
  viinfo("nlines=%d\n", nlines);

  /* Scroll for the specified number of lines */

  for (; nlines; nlines--)
    {
      /* Send the VT100 INDEX command */

      vi_write(vi, g_index, sizeof(g_index));
    }
}

/****************************************************************************
 * Name: vi_scrolldown
 *
 * Description:
 *   Scroll the display down 'nlines' by sending the VT100 REVINDEX command.
 *
 ****************************************************************************/

static void vi_scrolldown(FAR struct vi_s *vi, uint16_t nlines)
{
  viinfo("nlines=%d\n", nlines);

  /* Scroll for the specified number of lines */

  for (; nlines; nlines--)
    {
      /* Send the VT100 REVINDEX command */

      vi_write(vi, g_revindex, sizeof(g_revindex));
    }
}

/****************************************************************************
 * Name: vi_error
 *
 * Description:
 *   Show a highlighted error message at the final line of the display.
 *
 ****************************************************************************/

static void vi_error(FAR struct vi_s *vi, FAR const char *fmt, ...)
{
  struct vi_pos_s cursor;
  va_list ap;
  int len;

  /* Save the current cursor position */

  cursor.row    = vi->cursor.row;
  cursor.column = vi->cursor.column;

  /* Set up for a reverse text message on the final line */

  vi_setcursor(vi, vi->display.row - 1, 0);
  vi_reverseon(vi);

  /* Expand the error message in the scratch buffer */

  len  = snprintf(vi->scratch, SCRATCH_BUFSIZE, "ERROR: ");

  va_start(ap, fmt);
  len += vsnprintf(vi->scratch + len, SCRATCH_BUFSIZE - len, fmt, ap);
  vvidbg(fmt, ap);
  va_end(ap);

  /* Write the error message to the display in reverse text */

  vi_write(vi, vi->scratch, len);

  /* Restore normal attributes */

  vi_attriboff(vi);

  /* Reposition the cursor */

  vi_setcursor(vi, cursor.row, cursor.column);

  /* Remember that there is an error message on the last line of the display.
   * When the display is refreshed, the last line will not be altered until
   * the error is cleared.
   */

  vi->error = true;
  VI_BEL(vi);
}

/****************************************************************************
 * Line positioning
 ****************************************************************************/

/****************************************************************************
 * Name: vi_linebegin
 *
 * Description:
 *   Search backward for the beginning of the current line
 *
 ****************************************************************************/

static off_t vi_linebegin(FAR struct vi_s *vi, off_t pos)
{
  /* Search backward to find the previous newline character (or, possibly,
   * the beginning of the text buffer).
   */

  while (pos && vi->text[pos - 1] != '\n')
    {
      pos--;
    }

  viinfo("Return pos=%ld\n", (long)pos);
  return pos;
}

/****************************************************************************
 * Name: vi_prevline
 *
 * Description:
 *   Search backward for the beginning of the previous line
 *
 ****************************************************************************/

static off_t vi_prevline(FAR struct vi_s *vi, off_t pos)
{
  /* Find the beginning the of current line */

  pos = vi_linebegin(vi, pos);

  /* If this not the first line, then back up one more character to position
   * at the last byte of the previous line.
   */

  if (pos > 0)
    {
      pos = vi_linebegin(vi, pos - 1);
    }

  viinfo("Return pos=%ld\n", (long)pos);
  return pos;
}

/****************************************************************************
 * Name: vi_lineend
 *
 * Description:
 *   Search forward for the end of the current line
 *
 ****************************************************************************/

static off_t vi_lineend(FAR struct vi_s *vi, off_t pos)
{
  /* Search forward to find the next newline character. (or, possibly,
   * the end of the text buffer).
   */

  while (pos < vi->textsize && vi->text[pos] != '\n')
    {
      pos++;
    }

  viinfo("Return pos=%ld\n", (long)pos);
  return pos;
}

/****************************************************************************
 * Name: vi_nextline
 *
 * Description:
 *   Search backward for the start of the next line
 *
 ****************************************************************************/

static off_t vi_nextline(FAR struct vi_s *vi, off_t pos)
{
  /* Position at the end of the current line */

  pos = vi_lineend(vi, pos);

  /* If this is not the last byte in the buffer, then increment by one
   * for position of the first byte of the next line.
   */

  if (pos < vi->textsize)
    {
      pos++;
    }

  viinfo("Return pos=%ld\n", (long)pos);
  return pos;
}

/****************************************************************************
 * Text buffer management
 ****************************************************************************/

/****************************************************************************
 * Name: vi_extendtext
 *
 * Description:
 *   Reallocate the in-memory file memory by (at least) 'increment' and make
 *   space for new text of size 'increment' at the specified cursor position.
 *
 ****************************************************************************/

static bool vi_extendtext(FAR struct vi_s *vi, off_t pos, size_t increment)
{
  FAR char *alloc;
  int i;

  viinfo("pos=%ld increment=%ld\n", (long)pos, (long)increment);

  /* Check if we need to reallocate */

  if (!vi->text || vi->textsize + increment > vi->txtalloc)
    {
      /* Allocate in chunksize so that we do not have to reallocate so
       * often.
       */

      size_t allocsize = ALIGN_GULP(vi->textsize + increment);
      alloc = realloc(vi->text, allocsize);
      if (!alloc)
        {
          /* Reallocation failed */

          vi_error(vi, g_fmtallocfail);
          return false;
        }

      /* Save the new buffer information */

      vi->text    = alloc;
      vi->txtalloc = allocsize;
    }

  /* Move text to make space for new text of size 'increment' at the current
   * cursor position
   */

  for (i = vi->textsize - 1; i >= pos; i--)
    {
      vi->text[i + increment] = vi->text[i];
    }

  /* Adjust end of file position */

  vi->textsize += increment;
  vi->modified  = true;
  return true;
}

/****************************************************************************
 * Name: vi_shrinkpos
 *
 * Description:
 *   This is really part of vi_shrinktext.  When any text is deleted, any
 *   positions lying beyond the deleted region in the text buffer must be
 *   adjusted.
 *
 * Input Parameters:
 *   delpos  The position where text was deleted
 *   delsize The number of bytes deleted.
 *   pos     A pointer to a position that may need to be adjusted.
 *
 ****************************************************************************/

static void vi_shrinkpos(off_t delpos, size_t delsize, FAR off_t *pos)
{
  viinfo("delpos=%ld delsize=%ld pos=%ld\n",
         (long)delpos, (long)delsize, (long)*pos);

  /* Check if the position is beyond the deleted region */

  if (*pos > delpos + delsize)
    {
      /* Yes... just subtract the size of the deleted region */

      *pos -= delsize;
    }

  /* What if the position is within the deleted region?  Set it to the
   * beginning of the deleted region.
   */

  else if (*pos > delpos)
    {
      *pos = delpos;
    }
}

/****************************************************************************
 * Name: vi_shrinktext
 *
 * Description:
 *   Delete a region in the text buffer by copying the end of the text buffer
 *   over the deleted region and adjusting the size of the region.  The text
 *   region may be reallocated in order to recover the unused memory.
 *
 ****************************************************************************/

static void vi_shrinktext(FAR struct vi_s *vi, off_t pos, size_t size)
{
  FAR char *alloc;
  size_t allocsize;
  int i;

  viinfo("pos=%ld size=%ld\n", (long)pos, (long)size);

  /* Close up the gap to remove 'size' characters at 'pos' */

  for (i = pos + size; i < vi->textsize; i++)
    {
      vi->text[i - size] = vi->text[i];
    }

  /* Adjust sizes and positions */

  vi->textsize -= size;
  vi->modified  = true;
  vi_shrinkpos(pos, size, &vi->curpos);
  vi_shrinkpos(pos, size, &vi->winpos);
  vi_shrinkpos(pos, size, &vi->prevpos);

  /* Reallocate the buffer to free up memory no longer in use */

  allocsize = ALIGN_GULP(vi->textsize);
  if (allocsize < vi->txtalloc)
    {
      alloc = realloc(vi->text, allocsize);
      if (!alloc)
        {
          vi_error(vi, g_fmtallocfail);
          return;
        }

      /* Save the new buffer information */

      vi->text    = alloc;
      vi->txtalloc = allocsize;
    }
}

/****************************************************************************
 * File access
 ****************************************************************************/

/****************************************************************************
 * Name: vi_insertfile
 *
 * Description:
 *   Insert the contents of a file into the text buffer
 *
 ****************************************************************************/

static bool vi_insertfile(FAR struct vi_s *vi, off_t pos,
                          FAR const char *filename)
{
  struct stat buf;
  FILE *stream;
  off_t filesize;
  size_t nread;
  int result;
  bool ret;

  viinfo("pos=%ld filename=\"%s\"\n", (long)pos, filename);

  /* Get the size of the file  */

  result = stat(filename, &buf);
  if (result < 0)
    {
      vi_error(vi, g_fmtcmdfail, "stat", errno);
      return false;
    }

  /* Check for zero-length file */

  filesize = buf.st_size;
  if (filesize < 1)
    {
      return false;
    }

  /* Open the file for reading*/

  stream = fopen(filename, "r");
  if (!stream)
    {
      vi_error(vi, g_fmtcmdfail, "open", errno);
      return false;
    }

  /* [Re]allocate the text buffer to hold the file contents at the current
   * cursor position.
   */

  ret = false;
  if (vi_extendtext(vi, pos, filesize))
    {
      /* Read the contents of the file into the text buffer at the
       * current cursor position.
       */

      nread = fread(vi->text + pos, 1, filesize, stream);
      if (nread < filesize)
        {
          /* Report the error (or partial read), EINTR is not handled */

          vi_error(vi, g_fmtcmdfail, "fread", errno);
          vi_shrinktext(vi, pos, filesize);
        }
      else
        {
          ret = true;
        }
    }

  (void)fclose(stream);
  return ret;
}

/****************************************************************************
 * Name: vi_savetext
 *
 * Description:
 *   Save a region of the text buffer to 'filename'
 *
 ****************************************************************************/

static bool vi_savetext(FAR struct vi_s *vi, FAR const char *filename,
                        off_t pos, size_t size)
{
  FILE *stream;
  size_t nwritten;

  viinfo("filename=\"%s\" pos=%ld size=%ld\n",
         filename, (long)pos, (long)size);

  /* Open the file for writing */

  stream = fopen(filename, "w");
  if (!stream)
    {
      vi_error(vi, g_fmtcmdfail, "fopen", errno);
      return false;
    }

  /* Write the region of the text buffer beginning at pos and extending
   * through pos + size -1.
   */

  nwritten = fwrite(vi->text + pos, 1, size, stream);
  if (nwritten < size)
    {
      /* Report the error (or partial write).  EINTR is not handled. */

      vi_error(vi, g_fmtcmdfail, "fwrite", errno);
      (void)fclose(stream);
      return false;
    }

  (void)fclose(stream);
  return true;
}

/****************************************************************************
 * Name: vi_checkfile
 *
 * Description:
 *   Check if a file by this name already exists.
 *
 ****************************************************************************/

static bool vi_checkfile(FAR struct vi_s *vi, FAR const char *filename)
{
  struct stat buf;
  int ret;

  viinfo("filename=\"%s\"\n", filename);

  /* Get the size of the file  */

  ret = stat(filename, &buf);
  if (ret < 0)
    {
      /* The file does not exist */

      return false;
    }

  /* It exists, but is it a regular file */

  if (!S_ISREG(buf.st_mode))
    {
      /* Report the error... there is really no good return value in
       * this case.
       */

      vi_error(vi, g_fmtnotfile, filename);
    }

  return true;
}

/****************************************************************************
 * Mode Management Functions
 ****************************************************************************/

/****************************************************************************
 * Name: vi_setmode
 *
 * Description:
 *   Set the new mode (or command sub-mode) and reset all other common state
 *   variables.  NOTE that a numeric value may be passed to the new mode in
 *   the value field.
 *
 ****************************************************************************/

static void vi_setmode(FAR struct vi_s *vi, uint8_t mode, long value)
{
  viinfo("mode=%d value=%ld\n", mode, value);

  /* Set the mode and clear mode-dependent states that are not preserved
   * across mode changes.
   */

  vi->mode           = mode;
  vi->delarm         = false;
  vi->yankarm        = false;
//vi->error          = false; need to preserve until vi_showtext is called
  vi->value          = value;
  vi->cmdlen         = 0;
}

/****************************************************************************
 * Name: vi_setsubmode
 *
 * Description:
 *   Set up one of the data entry sub-modes of the command mode.  These are
 *   modes in which commands or search data will be entered on the final line
 *   of the display.
 *
 ****************************************************************************/

static void vi_setsubmode(FAR struct vi_s *vi, uint8_t mode, char prompt,
                          long value)
{
  viinfo("mode=%d prompt='%c' value=%ld\n", mode, prompt, value);

  /* Set up the new mode */

  vi_setmode(vi, mode, value);

  /* Save the previous cursor position (not required by all modes) */

  vi->cursave.row    = vi->cursor.row;
  vi->cursave.column = vi->cursor.column;

  /* Set up for data entry on the final line */

  vi->cursor.row     = vi->display.row - 1;
  vi->cursor.column  = 0;
  vi_setcursor(vi, vi->cursor.row, vi->cursor.column);

  /* Output the prompt character in bold text */

  vi_boldon(vi);
  vi_putch(vi, prompt);
  vi_attriboff(vi);

  /* Clear to the end of the line */

  vi_clrtoeol(vi);

  /* Update the cursor position */

  vi->cursor.column  = 1;
}

/****************************************************************************
 * Name:  vi_exitsubmode
 *
 * Description:
 *   Exit the data entry sub-mode and return to normal command mode.
 *
 ****************************************************************************/

static void vi_exitsubmode(FAR struct vi_s *vi, uint8_t mode)
{
  viinfo("mode=%d\n", mode);

  /* Set up the new mode */

  vi_setmode(vi, mode, 0);

  /* Restore the saved cursor position */

  vi->cursor.row    = vi->cursave.row;
  vi->cursor.column = vi->cursave.column;
}

/****************************************************************************
 * Display Management
 ****************************************************************************/

/****************************************************************************
 * Name: vi_windowpos
 *
 * Description:
 *   Based on the position of the cursor in the text buffer, determine the
 *   horizontal display cursor position, performing TAB expansion as
 *   necessary.
 *
 ****************************************************************************/

static void vi_windowpos(FAR struct vi_s *vi, off_t start, off_t end,
                         uint16_t *pcolumn, off_t *ppos)
{
  uint16_t column;
  off_t pos;

  viinfo("start=%ld end=%ld\n", (long)start, (long)end);

  /* Make sure that the end position is not beyond the end of the text. We
   * assume that the start position is okay.
   */

  if (end > vi->textsize)
    {
      end = vi->textsize;
    }

  /* Loop incrementing the text buffer position while text buffer position
   * is within range.
   */

  for (pos = start, column = 0; pos < end; pos++)
    {
      /* Is there a newline terminator at this position? */

      if (vi->text[pos] == '\n')
        {
          /* Yes... break out of the loop return the cursor column */

          break;
        }

      /* No... Is there a TAB at this position? */

      else if (vi->text[pos] == '\t')
        {
          /* Yes.. expand the TAB */

          column = NEXT_TAB(column);
        }

      /* No, then just increment the cursor column by one character */

      else
        {
          column++;
        }
    }

  /* Now return the requested values */

  if (ppos)
    {
      *ppos = pos;
    }

  if (pcolumn)
    {
      *pcolumn = column;
    }
}

/****************************************************************************
 * Name: vi_scrollcheck
 *
 * Description:
 *   Check if any operations will require that we scroll the display.
 *
 ****************************************************************************/

static void vi_scrollcheck(FAR struct vi_s *vi)
{
  off_t curline;
  off_t pos;
  uint16_t tmp;
  int column;
  int nlines;

  /* Get the text buffer offset to the beginning of the current line */

  curline = vi_linebegin(vi, vi->curpos);

  /* Check if the current line is above the first line on the display */

  while (curline < vi->winpos)
    {
      /* Yes.. move the window position up to the beginning of the previous
       * line line and check again */

      vi->winpos = vi_prevline(vi, vi->winpos);
    }

  /* Reset the cursor row position so that it is relative to the
   * top of the display.
   */

  vi->cursor.row = 0;
  for (pos = vi->winpos; pos < curline; pos = vi_nextline(vi, pos))
    {
      vi->cursor.row++;
    }

  /* Check if the cursor row position is below the bottom of the display */

  for (; vi->cursor.row >= vi->display.row; vi->cursor.row--)
    {
      /* Yes.. move the window position down by one line and check again */

      vi->winpos = vi_nextline(vi, vi->winpos);
    }

  /* Check if the cursor column is on the display.  vi_windowpos returns the
   * unrestricted column number of cursor.  hscroll is the horizontal offset
   * in characters.
   */

  vi_windowpos(vi, curline, vi->curpos, &tmp, NULL);
  column = (int)tmp - (int)vi->hscroll;

  /* Force the cursor column to lie on the display.  First check if the
   * column lies to the left of the horizontal scrolling position.  If it
   * does, move the scroll position to the left by tabs until the cursor
   * lies on the display.
   */

  while (column < 0)
    {
      column      += TABSIZE;
      vi->hscroll -= TABSIZE;
    }

  /* If the cursor column lies to the right of the display, then adjust
   * the horizontal scrolling position so that the cursor position does
   * lie on the display.
   */

  while (column >= vi->display.column)
    {
      column      -= TABSIZE;
      vi->hscroll += TABSIZE;
    }

  /* That final adjusted position is the display cursor column */

  vi->cursor.column = column;

  /* Check if new window position is below the previous position.
   * In this case, we will need to scroll up until the new window
   * position is at the top of the display.
   */

  if (vi->winpos > vi->prevpos)
    {
      /* We will need to scroll up.  Count how many lines we
       * need to scroll.
       */

      for (nlines = 0, pos = vi->prevpos;
           pos != vi->winpos && nlines < vi->display.row;
           nlines++)
        {
          pos = vi_nextline(vi, pos);
        }

      /* Then scroll up that number of lines */

      if (nlines < vi->display.row)
        {
          vi_scrollup(vi, nlines);
        }
    }

  /* Check if new window position is above the previous position.
   * In this case, we will need to scroll down until the new window
   * position is at the top of the display.
   */

  else if (vi->winpos < vi->prevpos)
    {

      for (nlines = 0, pos = vi->prevpos;
           pos != vi->winpos && nlines < vi->display.row;
           nlines++)
        {
          pos = vi_prevline(vi, pos);
        }

      /* Then scroll down that number of lines */

      if (nlines < vi->display.row)
        {
          vi_scrolldown(vi, nlines);
        }
    }

  /* Save the previous top-of-display position for next time around.
   * This can be modified asynchronously by text deletion operations.
   */

  vi->prevpos = vi->winpos;
  viinfo("winpos=%ld hscroll=%d\n",
         (long)vi->winpos, (long)vi->hscroll);
}

/****************************************************************************
 * Name: vi_showtext
 *
 * Description:
 *   Update the display based on the last operation.  This function is
 *   called at the beginning of the processing loop in Command and Insert
 *   modes (and also in the continuous replace mode).
 *
 ****************************************************************************/

static void vi_showtext(FAR struct vi_s *vi)
{
  off_t pos;
  uint16_t row;
  uint16_t endrow;
  uint16_t column;
  uint16_t endcol;
  uint16_t tabcol;

  /* Check if any of the preceding operations will cause the display to
   * scroll.
   */

  vi_scrollcheck(vi);

  /* If there is an error message at the bottom of the display, then
   * do not update the last line.
   */

  endrow = vi->display.row;
  if (vi->error)
    {
      endrow--;
    }

  /* Make sure that all character attributes are disabled; Turn off the
   * cursor during the update.
   */

  vi_attriboff(vi);
  vi_cursoroff(vi);

  /* Write each line to the display, handling horizontal scrolling and
   * tab expansion.
   */

  for (pos = vi->winpos, row = 0;
       pos < vi->textsize && row < endrow;
       row++)
    {
      /* Get the last column on this row.  Avoid writing into the last byte
       * on the screen which may trigger a scroll.
       */

      endcol = vi->display.column;
      if (row >= vi->display.row - 1)
        {
         endcol--;
        }

      /* Get the position into this line corresponding to display column 0,
       * accounting for horizontal scrolling and tab expansion.  Add that to
       * the line start offset to get the first offset to consider for
       * display.
       */

      vi_windowpos(vi, pos, pos + vi->hscroll, NULL, &pos);

      /* Set the cursor position to the beginning of this row and clear to
       * the end of the line.
       */

      vi_setcursor(vi, row, 0);
      vi_clrtoeol(vi);

      /* Loop for each column */

      for (column = 0; pos < vi->textsize && column < endcol; pos++)
        {
          /* Break out of the loop if we encounter the newline before the
           * last column is encountered.
           */

          if (vi->text[pos] == '\n')
            {
              break;
            }

          /* Perform TAB expansion */

          else if (vi->text[pos] == '\t')
            {
              tabcol = NEXT_TAB(column);
              if (tabcol < endcol)
                {
                  for (; column < tabcol; column++)
                    {
                      vi_putch(vi, ' ');
                    }
                }
              else
               {
                 /* Break out of the loop... there is nothing left on the
                  * line but whitespace.
                  */

                 break;
               }
            }

          /* Add the normal character to the display */

          else
            {
              vi_putch(vi, vi->text[pos]);
              column++;
            }
        }

      /* Skip to the beginning of the next line */

      pos = vi_nextline(vi, pos);
    }

  /* If there was not enough text to fill the display, clear the
   * remaining lines (except for any possible error line at the
   * bottom of the display).
   */

  for (; row < endrow; row++)
    {
      /* Set the cursor position to the beginning of the row and clear to
       * the end of the line.
       */

      vi_setcursor(vi, row, 0);
      vi_clrtoeol(vi);
    }

  /* Turn the cursor back on */

  vi_cursoron(vi);
}

/****************************************************************************
 * Command Mode Functions
 ****************************************************************************/

/****************************************************************************
 * Name: vi_cusorup
 *
 * Description:
 *   Move the cursor up one line in the text buffer.
 *
 ****************************************************************************/

static void vi_cusorup(FAR struct vi_s *vi, int nlines)
{
  int remaining;
  off_t start;
  off_t end;

  viinfo("nlines=%d\n", nlines);

  /* How many lines do we need to move?  Zero means 1 (so does 1) */

  remaining = (nlines < 1 ? 1 : nlines);

  /* Get the offset to the start of the current line */

  start = vi_linebegin(vi, vi->curpos);

  /* Now move the cursor position back the correct number of lines */

  for (; remaining > 0; remaining--)
    {
      /* Get the start position of the previous line */

      start = vi_prevline(vi, start);

      /* Find the cursor position on the next line corresponding to the
       * cursor position on the current line.
       */

      end = start + vi->cursor.column + vi->hscroll;
      vi_windowpos(vi, start, end, NULL, &vi->curpos);
    }
}

/****************************************************************************
 * Name: vi_cursordown
 *
 * Description:
 *   Move the cursor down one line in the text buffer
 *
 ****************************************************************************/

static void vi_cursordown(FAR struct vi_s *vi, int nlines)
{
  int remaining;
  off_t start;
  off_t end;

  viinfo("nlines=%d\n", nlines);

  /* How many lines do we need to move?  Zero means 1 (so does 1) */

  remaining = (nlines < 1 ? 1 : nlines);

  /* Get the offset to the start of the current line */

  start = vi_linebegin(vi, vi->curpos);

  /* Now move the cursor position forward the correct number of lines */

  for (; remaining > 0; remaining--)
    {
      /* Get the start of the next line. */

      start = vi_nextline(vi, start);

      /* Find the cursor position on the next line corresponding to the
       * cursor position on the current line.
       */

      end = start + vi->cursor.column + vi->hscroll;
      vi_windowpos(vi, start, end, NULL, &vi->curpos);
    }
}

/****************************************************************************
 * Name: vi_cursorleft
 *
 * Description:
 *   Move the cursor left 'ncolumns' columns in the text buffer (without moving
 *   to the preceding line).  Note that a repetition count of 0 means to
 *   perform the movement once.
 *
 ****************************************************************************/

static off_t vi_cursorleft(FAR struct vi_s *vi, off_t curpos, int ncolumns)
{
  int remaining;

  viinfo("curpos=%ld ncolumns=%d\n", curpos, ncolumns);

  /* Loop decrementing the cursor position for each repetition count.  Break
   * out early if we hit either the beginning of the text buffer, or the end
   * of the previous line.
   */

  for (remaining = (ncolumns < 1 ? 1 : ncolumns);
       curpos > 0 && remaining > 0 && vi->text[curpos - 1] != '\n';
       curpos--, remaining--);

  return curpos;
}

/****************************************************************************
 * Name: vi_cursorright
 *
 * Description:
 *   Move the cursor right 'ncolumns' columns in the text buffer (without
 *   moving to the next line).  Note that a repetition count of 0 means to
 *   perform the movement once.
 *
 ****************************************************************************/

static off_t vi_cursorright(FAR struct vi_s *vi, off_t curpos, int ncolumns)
{
  int remaining;

  viinfo("curpos=%ld ncolumns=%d\n", curpos, ncolumns);

  /* Loop incrementing the cursor position for each repetition count.  Break
   * out early if we hit either the end of the text buffer, or the end of the line.
   */

  for (remaining = (ncolumns < 1 ? 1 : ncolumns);
       curpos < vi->textsize && remaining > 0 && vi->text[curpos] != '\n';
       curpos++, remaining--);

  return curpos;
}

/****************************************************************************
 * Name: vi_delforward
 *
 * Description:
 *   Delete characters from the current cursor position forward
 *
 ****************************************************************************/

static void vi_delforward(FAR struct vi_s *vi)
{
  off_t end;
  size_t size;

  viinfo("curpos=%ld value=%ld\n", (long)vi->curpos, vi->value);

  /* Get the cursor position as if we would have move the cursor right N
   * times (which might be <N characters).
   */

  end  = vi_cursorright(vi, vi->curpos, vi->value);

  /* The difference from the current position then is the number of
   * characters to be deleted.
   */

  size = end - vi->curpos;
  vi_shrinktext(vi, vi->curpos, size);
}

/****************************************************************************
 * Name: vi_delbackward
 *
 * Description:
 *   Delete characters before the current cursor position
 *
 ****************************************************************************/

static void vi_delbackward(FAR struct vi_s *vi)
{
  off_t start;
  off_t end;
  size_t size;

  viinfo("curpos=%ld value=%ld\n", (long)vi->curpos, vi->value);

  /* Back up one character.  This is where the deletion will end */

  end = vi_cursorleft(vi, vi->curpos, 1);

  /* Get the cursor position as if we would have move the cursor left N
   * times (which might be <N characters).
   */

  start = vi_cursorleft(vi, end, vi->value);

  /* The difference from the current position then is the number of
   * characters to be deleted.
   */

  size = end - start;
  vi_shrinktext(vi, start, size);
}

/****************************************************************************
 * Name: vi_linerange
 *
 * Description:
 *   Return the start and end positions for N lines in the text buffer,
 *   beginning at the current line.  This is logic common to yanking and
 *   deleting lines.
 *
 ****************************************************************************/

static void vi_linerange(FAR struct vi_s *vi, off_t *start, off_t *end)
{
  off_t next;
  int nlines;

  /* Get the offset in the text buffer to the beginning of the current line */

  *start = vi_linebegin(vi, vi->curpos);

  /* Move one line unless a repetition count was provided */

  nlines = (vi->value > 0 ? vi->value : 1);

  /* Search ahead to find the end of the last line to yank */

  for (next = *start; nlines > 1; nlines--)
    {
      next = vi_nextline(vi, next);
    }

  *end = vi_lineend(vi, next);
}

/****************************************************************************
 * Name: vi_delline
 *
 * Description:
 *   Delete N lines from the text buffer, beginning at the current line.
 *
 ****************************************************************************/

static void vi_delline(FAR struct vi_s *vi)
{
  off_t delsize;
  off_t start;
  off_t end;

  /* Get the offset in the text buffer corresponding to the range of lines to
   * be deleted
   */

  vi_linerange(vi, &start, &end);
  viinfo("start=%ld end=%ld\n", (long)start, (long)end);

  /* Remove the text from the text buffer */

  delsize =  end - start + 1;
  vi_shrinktext(vi, start, delsize);
}

/****************************************************************************
 * Name: vi_yank
 *
 * Description:
 *   Remove N lines from the text buffer, beginning at the current line and
 *   copy the lines to an allocated yank buffer.
 *
 ****************************************************************************/

static void vi_yank(FAR struct vi_s *vi)
{
  off_t start;
  off_t end;

  /* Get the offset in the text buffer corresponding to the range of lines to
   * be yanked
   */

  vi_linerange(vi, &start, &end);
  viinfo("start=%ld end=%ld\n", (long)start, (long)end);

  /* Free any previously yanked lines */

  if (vi->yank)
    {
      free(vi->yank);
    }

  /* Allocate a yank buffer biggest enough to hold the lines */

  vi->yankalloc = end - start + 1;
  vi->yank     = (FAR char *)malloc(vi->yankalloc);

  if (!vi->yank)
    {
      vi_error(vi, g_fmtallocfail);
      vi->yankalloc = 0;
      return;
    }

  /* Copy the block from the text buffer to the yank buffer */

  memcpy(vi->yank, &vi->text[start], vi->yankalloc);

  /* Remove the yanked text from the text buffer */

  vi_shrinktext(vi, start, vi->yankalloc);
}

/****************************************************************************
 * Name: vi_paste
 *
 * Description:
 *   Copy line(s) from the yank buffer, and past them after the current line.
 *   The contents of the yank buffer are released.
 *
 ****************************************************************************/

static void vi_paste(FAR struct vi_s *vi)
{
  off_t start;

  viinfo("curpos=%ld yankalloc=%d\n", (long)vi->curpos, (long)vi->yankalloc);

  /* Make sure there is something to be yanked */

  if (!vi->yank || vi->yankalloc <= 0)
    {
      return;
    }

  /* Paste at the beginning of the next line */

  start = vi_nextline(vi, vi->curpos);

  /* Reallocate the text buffer to hold the yank buffer contents at the
   * the beginning of the next line.
   */

  if (vi_extendtext(vi, start, vi->yankalloc))
    {
      /* Copy the contents of the yank buffer into the text buffer at the
       * position where the start of the next line was.
       */

      memcpy(&vi->text[start], vi->yank, vi->yankalloc);
    }

  /* Free the yank buffer in any event */

  free(vi->yank);

  vi->yank    = NULL;
  vi->yankalloc = 0;
}

/****************************************************************************
 * Name: vi_gotoline
 *
 * Description:
 *   Position the cursor at the line specified by vi->value.  If
 *   vi->value is zero, then the cursor is position at the end of the text
 *   buffer.
 *
 ****************************************************************************/

static void vi_gotoline(FAR struct vi_s *vi)
{
  viinfo("curpos=%ld value=%ld\n", (long)vi->curpos, vi->value);

  /* Special case the first line */

  if (vi->value == 1)
    {
      vi->curpos = 0;
    }

  /* Work harder to position to lines in the middle */

  else if (vi->value > 0)
    {
      uint32_t line;

      /* Got to the line == value */

      for (line = vi->value, vi->curpos = 0;
          --line > 0 && vi->curpos < vi->textsize;
          )
        {
          vi->curpos = vi_nextline(vi, vi->curpos);
        }
    }

  /* No value means to go to beginning of the last line */

  else
    {
      /* Get the beginning of the last line */

      vi->curpos = vi_linebegin(vi, vi->textsize);
    }
}

/****************************************************************************
 * Name: vi_cmd_mode
 *
 * Description:
 *   Command mode loop
 *
 ****************************************************************************/

static void vi_cmd_mode(FAR struct vi_s *vi)
{
  viinfo("Enter command mode\n");

  /* Loop while we are in command mode */

  while (vi->mode == MODE_COMMAND)
    {
      bool preserve;
      int ch;

      /* Make sure that the display reflects the current state */

      vi_showtext(vi);
      vi_setcursor(vi, vi->cursor.row, vi->cursor.column);

      /* Get the next character from the input */

      ch = vi_getch(vi);

      /* Anything other than 'd' disarms line deletion */

      if (ch != 'd')
        {
          vi->delarm = false;
        }

      /* Anything other than 'y' disarms line yanking */

      if (ch != 'y')
        {
          vi->yankarm = false;
        }

      /* Any key press clears the error message */

      vi->error = false;

      /* Handle numeric input.  Zero (0) with no preceding value is a
       * special case: It means to go to the beginning o the line.
       */

      if (isdigit(ch) && (vi->value > 0 || ch != '0'))
        {
          uint32_t tmp = 10 * vi->value + (ch - '0');
          if (tmp > UINT16_MAX)
            {
              tmp = UINT16_MAX;
            }

          /* Update the command repetition count */

          vi->value = tmp;

          viinfo("Value=%ld\n", vi->value);
          continue;
        }

      /* Then handle the non-numeric character.  Normally the accumulated
       * value will be reset after processing the command.  There are a few
       * exceptions; 'preserve' will be set to 'true' in those exceptional
       * cases.
       */

      preserve = false;
      switch (ch)
        {
        case KEY_CMDMODE_UP: /* Move the cursor up one line */
          {
            vi_cusorup(vi, vi->value);
          }
          break;

        case KEY_CMDMODE_DOWN: /* Move the cursor down one line */
          {
            vi_cursordown(vi, vi->value);
          }
          break;

        case KEY_CMDMODE_LEFT: /* Move the cursor left N characters */
          {
            vi->curpos = vi_cursorleft(vi, vi->curpos, vi->value);
          }
          break;

        case KEY_CMDMODE_RIGHT: /* Move the cursor right one character */
          {
            vi->curpos = vi_cursorright(vi, vi->curpos, vi->value);
          }
          break;

        case KEY_CMDMODE_BEGINLINE: /* Move cursor to start of current line */
          {
            vi->curpos = vi_linebegin(vi, vi->curpos);
          }
          break;

        case KEY_CMDMODE_ENDLINE: /* Move cursor to end of current line */
          {
            vi->curpos = vi_lineend(vi, vi->curpos);
          }
          break;

        case KEY_CMDMODE_PAGEUP: /* Move up (backward) one screen */
          {
            vi_cusorup(vi, vi->display.row);
          }
          break;

        case KEY_CMDMODE_PAGEDOWN: /* Move down (forward) one screen */
          {
            vi_cursordown(vi, vi->display.row);
          }
          break;

        case KEY_CMDMODE_HALFUP: /* Move up (backward) one screen */
          {
            vi_cusorup(vi, vi->display.row >> 1);
          }
          break;

        case KEY_CMDMODE_HALFDOWN: /*  Move down (forward) one half screen */
          {
            vi_cursordown(vi, vi->display.row >> 1);
          }
          break;

        case KEY_CMDMODE_DEL: /* Delete N characters at the cursor */
        case ASCII_DEL:
          {
            vi_delforward(vi);
          }
          break;

        case ASCII_BS:  /* Delete N characters before the cursor */
          {
            vi_delbackward(vi);
          }
          break;

        case KEY_CMDMODE_DEL_LINE: /* Delete the current line */
          {
            if (vi->delarm)
              {
                vi_delline(vi);
                vi->delarm = false;
              }
            else
              {
                vi->delarm = true;
                preserve = true;
              }
          }
          break;

        case KEY_CMDMODE_YANK:  /* Yank the current line(s) into the buffer */
          {
            if (vi->yankarm)
              {
                vi_yank(vi);
                vi->yankarm = false;
              }
            else
              {
                vi->yankarm = true;
                preserve = true;
              }
          }
          break;

        case KEY_CMDMODE_PASTE: /* Paste line(s) from into text after current line */
          {
            vi_paste(vi);
          }
          break;

        case KEY_CMDMODE_REPLACECH: /* Replace character(s) under cursor */
          {
            vi_setmode(vi, SUBMODE_REPLACECH, vi->value);
            preserve = true;
          }
          break;

        case KEY_CMDMODE_REPLACE: /* Replace character(s) under cursor until ESC */
          {
            vi_setmode(vi, MODE_REPLACE, 0);
          }
          break; /* Not implemented */

        case KEY_CMDMODE_OPENBELOW: /* Enter insertion mode in new line below current */
          {
            /* Go forward to the end of the current line */

            vi->curpos = vi_lineend(vi, vi->curpos);

            /* Insert a newline to break the line.  The cursor now points
             * beginning of the new line.
             */

            vi_insertch(vi, '\n');

            /* Then enter insert mode */

            vi_setmode(vi, MODE_INSERT, 0);
          }
          break;

        case KEY_CMDMODE_OPENABOVE: /* Enter insertion mode in new line above current */
          {
            /* Back up to the beginning of the end of the previous line */

            off_t pos  = vi_prevline(vi, vi->curpos);
            vi->curpos = vi_lineend(vi, pos);

            /* Insert a newline to open the line.  The cursor will now point to the
             * beginning of newly openly line before the current line.
             */

            vi_insertch(vi, '\n');

            /* Then enter insert mode */

            vi_setmode(vi, MODE_INSERT, 0);
          }
          break;

        case KEY_CMDMODE_APPEND: /* Enter insertion mode after the current cursor position */
          {
            vi->curpos = vi_cursorright(vi, vi->curpos, 1);
            vi_setmode(vi, MODE_INSERT, 0);
          }
          break;

        case KEY_CMDMODE_APPENDEND: /* Enter insertion mode at the end of the current line */
          {
            vi->curpos = vi_lineend(vi, vi->curpos);
            vi_setmode(vi, MODE_INSERT, 0);
          }
          break;

        case KEY_CMDMODE_INSBEGIN: /* Enter insertion mode at the beginning of the current line */
          {
            vi->curpos = vi_linebegin(vi, vi->curpos);
          }
          /* Fall through */

        case KEY_CMDMODE_INSERT: /* Enter insertion mode before the current cursor position */
          {
            vi_setmode(vi, MODE_INSERT, 0);
          }
          break;

        case KEY_CMDMODE_COLONMODE: /* Enter : command sub-mode */
          {
            vi_setsubmode(vi, SUBMODE_COLON, ':', 0);
          }
          break;

        case KEY_CMDMODE_FINDMODE: /* Enter / find sub-mode */
          {
            vi_setsubmode(vi, SUBMODE_FIND, '/', 0);
          }
          break;

        case KEY_CMDMODE_GOTO:  /* Go to line specified by the accumulated value */
          {
            vi_gotoline(vi);
          }
          break;

        /* Unimplemented and invalid commands */

        case KEY_CMDMODE_REDRAW:  /* Redraws the screen */
        case KEY_CMDMODE_REDRAW2: /* Redraws the screen, removing deleted lines */
        case KEY_CMDMODE_MARK:    /* Place a mark beginning at the current cursor position */
        default:
          {
            VI_BEL(vi);
          }
          break;
        }

      /* Any non-numeric input will reset the accumulated value (after it has
       * been used).  There are a few exceptions:
       *
       * - For the double character sequences, we need to retain the value
       *   until the next character is entered.
       * - If we are changing modes, then we may need to preserve the 'value'
       *   as well; in some cases settings are passed to the new mode in
       *   'value' (vi_setmode() will have set or cleared 'value'
       *   appropriately).
       */

      if (!preserve)
        {
          vi->value = 0;
        }
    }
}

/****************************************************************************
 * Common Sub-Mode Functions
 ****************************************************************************/

/****************************************************************************
 * Name: vi_cmdch
 *
 * Description:
 *   Insert one character into the data entry line
 *
 ****************************************************************************/

static void vi_cmdch(FAR struct vi_s *vi, char ch)
{
  int index = vi->cmdlen;
  int next  = index + 1;

  viinfo("cmdlen=%d ch=%c[%02x]\n", vi->cmdlen, isprint(ch) ? ch : '.', ch);

  /* Abort gracelessly if the scratch buffer becomes full */

  if (next >= SCRATCH_BUFSIZE)
    {
      vi_exitsubmode(vi, MODE_COMMAND);
      return;
    }

  /* Add the new character to the scratch buffer */

  vi->scratch[index] = ch;
  vi->cmdlen = next;

  /* Update the cursor position */

  vi->cursor.column = next + 1;

  /* And add the new character to the display */

  vi_putch(vi, ch);
}

/****************************************************************************
 * Name:  vi_cmdbackspace
 *
 * Description:
 *   Process a backspace character in the data entry line
 *
 ****************************************************************************/

static void vi_cmdbackspace(FAR struct vi_s *vi)
{
  viinfo("cmdlen=%d\n", vi->cmdlen);

  if (vi->cmdlen > 0)
    {
      vi_setcursor(vi, vi->display.row - 1, vi->cmdlen);
      vi_clrtoeol(vi);

      /* Update the command index and cursor position */

      vi->cursor.column = vi->cmdlen;

      /* Decrement the number of characters on in the command */

      vi->cmdlen--;
    }
}

/****************************************************************************
 * Colon Data Entry Sub-Mode Functions
 ****************************************************************************/

/****************************************************************************
 * Name:  vi_parsecolon
 *
 * Description:
 *   Parse the colon command collected in the scratch buffer
 *
 ****************************************************************************/

static void vi_parsecolon(FAR struct vi_s *vi)
{
  FAR const char *filename = NULL;
  uint8_t cmd = CMD_NONE;
  bool done = false;
  bool forced;
  int col;
  int ch;

  viinfo("Parse: \"%s\"\n", vi->scratch);

  /* NUL terminate the command */

  vi->scratch[vi->cmdlen] = '\0';

  /* Then parse the contents of the scratch buffer */

  for (col = 0; col < vi->cmdlen && !done; col++)
    {
      /* Get the next command character from the scratch buffer */

      ch = vi->scratch[col];

      /* Check if the next after that is KEY_COLMODE_FORCE */

      forced = false;
      if (col < vi->cmdlen &&  vi->scratch[col + 1] == KEY_COLMODE_FORCE)
        {
          /* Yes.. the operation is forced */

          forced = true;
          col++;
        }

      /* Then process the command character */

      switch (ch)
        {
          case KEY_COLMODE_READ:
            {
              /* Reading a file should not be forced */

              if (cmd == CMD_NONE && !forced)
                {
                  cmd = CMD_READ;
                }
              else
                {
                  /* The read operation is not compatible with writing or
                   * quitting
                   */

                  goto errout_bad_command;
                }
            }
            break;

          case KEY_COLMODE_WRITE:
            {
              /* Are we just writing?  Or writing then quitting? */

              if (cmd == CMD_NONE)
                {
                  /* Just writing.. do we force overwriting? */

                  cmd = (forced ? CMD_OWRITE : CMD_WRITE);
                }
              else if (cmd == CMD_QUIT)
                {
                  /* Both ... do we force overwriting the file? */

                  cmd = (forced ? CMD_OWRITE_QUIT : CMD_WRITE_QUIT);
                }
              else
                {
                  /* Anything else, including a forced quit is a syntax error */

                  goto errout_bad_command;
                }
            }
            break;

          case KEY_COLMODE_QUIT:
            {
              /* Are we just quitting?  Or writing then quitting? */

              if (cmd == CMD_NONE)
                {
                  /* Just quitting... should we discard any changes? */

                  cmd = (forced ? CMD_DISCARD : CMD_QUIT);
                }

              /* If we are also writing, then it makes no sense to force the
               * quit operation.
               */

              else if (cmd == CMD_WRITE && !forced)
                {
                  cmd = CMD_WRITE_QUIT;
                }
              else if (cmd == CMD_OWRITE && !forced)
                {
                  cmd = CMD_OWRITE_QUIT;
                }
              else
                {
                  /* Quit is not compatible with reading */

                  goto errout_bad_command;
                }
            }
            break;

          default:
            {
              /* Ignore whitespace */

              if (ch != ' ')
                {
                  /* Anything else terminates the loop */

                  done = true;

                  /* If there is anything else on the line, then it must be
                   * a file name.  If we are writing (or reading with an
                   * empty text buffer), then we will need to copy the file
                   * into the filename storage area.
                   */

                  if (ch != '\0')
                    {
                      /* For now, just remember where the file is in the
                       * scratch buffer.
                       */

                      filename = &vi->scratch[col];
                    }
                }
            }
            break;
        }
    }

  /* Did we find any valid command?  A read command requires a filename.
   * A filename where one is not needed is also an error.
   */

  viinfo("cmd=%d filename=\"%s\"\n", cmd, vi->filename);

  if (cmd == CMD_NONE || (IS_READ(cmd) && !filename) ||
      (!USES_FILE(cmd) && filename))
    {
      goto errout_bad_command;
    }

  /* Are we writing to a new filename?  If we are not forcing the write,
   * then we have to check if the file exists.
   */

  if (filename && IS_NOWRITE(cmd))
    {
      /* Check if the file exists */

      if (vi_checkfile(vi, filename))
        {
          /* It does... show an error and exit */

          vi_error(vi, g_fmtfileexists);
          goto errout;
        }
    }

  /* Check if we are trying to quit with un-saved changes.  The user must
   * force quitting in this case.
   */

  if (vi->modified && IS_NDISCARD(cmd))
    {
      /* Show an error and exit */

      vi_error(vi, g_fmtmodified);
      goto errout;
    }

  /* Are we now committed to reading the file? */

  if (IS_READ(cmd))
    {
      /* Was the text buffer empty? */

      bool empty = (vi->textsize == 0);

      /* Yes.. get the cursor position of the beginning of the next line */

      off_t pos = vi_nextline(vi, vi->curpos);

      /* Then read the file into the text buffer at that position. */

      if (vi_insertfile(vi, pos, filename))
        {
          /* Was the text buffer empty before we inserted the file? */

          if (empty)
            {
              /* Yes.. then we want to save the filename and mark the text
               * as unmodified.
               */

              strncpy(vi->filename, filename, MAX_STRING - 1);

             /* Make sure that the (possibly truncated) file name is NUL
              * terminated
              */

              vi->filename[MAX_STRING - 1] = '\0';
              vi->modified = false;
            }
          else
            {
              /* No.. then we want to retain the filename and mark the text
               * as modified.
               */

              vi->modified = true;
            }
        }
    }

  /* Are we now committed to writing the file? */

  if (IS_WRITE(cmd))
    {
      /* If we are writing to a new file, then we need to copy the filename
       * from the scratch buffer to the filename buffer.
       */

      if (filename)
        {
          strncpy(vi->filename, filename, MAX_STRING - 1);

         /* Make sure that the (possibly truncated) file name is NUL
          * terminated
          */

          vi->filename[MAX_STRING - 1] = '\0';
        }

      /* If it is not a new file and if there are no changes to the text
       * buffer, then ignore the write.
       */

      if (filename || vi->modified)
        {
          /* Now, finally, we can save the file */

          if (!vi_savetext(vi, vi->filename, 0, vi->textsize))
            {
              /* An error occurred while saving the file and we are
               * not forcing the quit operation.  So error out without
               * quitting until the user decides what to do about
               * the save failure.
               */

              goto errout;
            }

          /* The text buffer contents are no longer modified */

          vi->modified = false;
        }
    }

  /* Are we committed to exit-ing? */

  if (IS_QUIT(cmd))
    {
      /* Yes... free resources and exit */

      vi_release(vi);
      exit(EXIT_SUCCESS);
    }

  /* Otherwise, revert to command mode */

  vi_exitsubmode(vi, MODE_COMMAND);
  return;

errout_bad_command:
  vi_error(vi, g_fmtnotcmd, vi->scratch);

errout:
  vi_exitsubmode(vi, MODE_COMMAND);
}

/****************************************************************************
 * Name: vi_cmd_submode
 *
 * Description:
 *   Colon command sub-mode of the command mode processing
 *
 ****************************************************************************/

static void vi_cmd_submode(FAR struct vi_s *vi)
{
  int ch;

  viinfo("Enter colon command sub-mode\n");

  /* Loop while we are in colon command mode */

  while (vi->mode == SUBMODE_COLON)
    {
      /* Get the next character from the input */

      ch = vi_getch(vi);

      /* Handle the newly received character */

      switch (ch)
        {
          case KEY_COLMODE_QUOTE: /* Quoted character follows */
            {
              /* Insert the next character unconditionally */

              vi_cmdch(vi, vi_getch(vi));
            }
            break;

          case ASCII_BS: /* Delete the character(s) before the cursor */
            {
              vi_cmdbackspace(vi);
            }
            break;

          case ASCII_ESC: /* Escape exits colon mode */
            {
              vi_exitsubmode(vi, MODE_COMMAND);
            }
            break;

          /* What do we do with carriage returns? line feeds? */

#if defined(CONFIG_EOL_IS_CR)
          case '\r': /* CR terminates line */
            {
              vi_parsecolon(vi);
            }
            break;

#elif defined(CONFIG_EOL_IS_BOTH_CRLF)
          case '\r': /* Wait for the LF */
            break;
#endif

#if defined(CONFIG_EOL_IS_LF) || defined(CONFIG_EOL_IS_BOTH_CRLF)
          case '\n': /* LF terminates line */
            {
              vi_parsecolon(vi);
            }
            break;
#endif

#ifdef CONFIG_EOL_IS_EITHER_CRLF
          case '\r': /* Either CR or LF terminates line */
          case '\n':
            {
              vi_parsecolon(vi);
            }
            break;
#endif

          default:
            {
              /* Ignore all but printable characters */

              if (isprint(ch))
                {
                  /* Insert the filtered character into the scratch buffer */

                  vi_cmdch(vi, ch);
                }
              else
                {
                  VI_BEL(vi);
                }
            }
            break;
        }
    }
}

/****************************************************************************
 * Find Data Entry Sub-Mode Functions
 ****************************************************************************/

/****************************************************************************
 * Name: vi_findstring
 *
 * Description:
 *   Find the string in the findstr buffer by searching for a matching
 *   sub-string in the text buffer, starting at the current cursor position.
 *
 ****************************************************************************/

static bool vi_findstring(FAR struct vi_s *vi)
{
  off_t pos;
  int len;

  viinfo("findstr: \"%s\"\n", vi->findstr);

  /* The search string is in the find buffer */

  len = strlen(vi->findstr);
  if (!len)
    {
      return false;
    }

  /* Search from the current cursor position forward for a
   * matching sub-string.  Stop loo
   */

  for (pos = vi->curpos;
       pos + len <= vi->textsize;
       pos++)
    {
      /* Check for the matching sub-string */

      if (strncmp(vi->text + pos, vi->scratch, len) == 0)
        {
          /* Found it... save the cursor position and
           * return success.
           */

          vi->curpos = pos;
          return true;
        }
    }

  /* If we get here, then the search string was not found anywhere after the
   * current cursor position.
   */

  return false;
}

/****************************************************************************
 * Name:  vi_parsefind
 *
 * Description:
 *   Find the string collected in the scratch buffer.
 *
 ****************************************************************************/

static void vi_parsefind(FAR struct vi_s *vi)
{
  /* Make certain that the scratch buffer contents are NUL terminated */

  vi->scratch[vi->cmdlen] = '\0';

  /* Is there anything in the scratch buffer? If not, then we will use the
   * string from the previous find operation.
   */

  viinfo("scratch: \"%s\"\n", vi->scratch);

  if (vi->cmdlen > 0)
    {
      /* Copy the new search string from the scratch to the find buffer */

      strncpy(vi->findstr, vi->scratch, MAX_STRING - 1);

     /* Make sure that the (possibly truncated) search string is NUL
      * terminated
      */

      vi->findstr[MAX_STRING - 1] = '\0';
    }

  /* Then attempt to find the string */

  (void)vi_findstring(vi);

  /* Exit the sub-mode and revert to command mode */

  vi_exitsubmode(vi, MODE_COMMAND);
}

/****************************************************************************
 * Name: vi_find_submode
 *
 * Description:
 *   Find command sub-mode of the command mode processing
 *
 ****************************************************************************/

static void vi_find_submode(FAR struct vi_s *vi)
{
  int ch;

  viinfo("Enter find sub-mode\n");

  /* Loop while we are in find mode */

  while (vi->mode == SUBMODE_FIND)
    {
      /* Get the next character from the input */

      ch = vi_getch(vi);

      /* Handle the newly received character */

      switch (ch)
        {
          case KEY_FINDMODE_QUOTE: /* Quoted character follows */
            {
              /* Insert the next character unconditionally */

              vi_cmdch(vi, vi_getch(vi));
            }
            break;

          case ASCII_BS: /* Delete the character before the cursor */
            {
              vi_cmdbackspace(vi);
            }
            break;

          case ASCII_ESC: /* Escape exits find mode */
            {
              vi_exitsubmode(vi, MODE_COMMAND);
            }
            break;

          /* What do we do with carriage returns? line feeds? */

#if defined(CONFIG_EOL_IS_CR)
          case '\r': /* CR terminates line */
            {
              vi_parsefind(vi);
            }
            break;

#elif defined(CONFIG_EOL_IS_BOTH_CRLF)
          case '\r': /* Wait for the LF */
            break;
#endif

#if defined(CONFIG_EOL_IS_LF) || defined(CONFIG_EOL_IS_BOTH_CRLF)
          case '\n': /* LF terminates line */
            {
              vi_parsefind(vi);
            }
            break;
#endif

#ifdef CONFIG_EOL_IS_EITHER_CRLF
          case '\r': /* Either CR or LF terminates line */
          case '\n':
            {
              vi_parsefind(vi);
            }
            break;
#endif

          default:
            {
              /* Ignore all but printable characters */

              if (isprint(ch))
                {
                  /* Insert the filtered character into the scratch buffer */

                  vi_cmdch(vi, ch);
                }
              else
                {
                  VI_BEL(vi);
                }
            }
            break;
        }
    }
}

/****************************************************************************
 * Replace Text Sub-Mode Functions
 ****************************************************************************/

/****************************************************************************
 * Name: vi_replacech
 *
 * Description:
 *   Replace the character at the current position.  If the current position
 *   is the end of line, then insert the character.
 *
 ****************************************************************************/

static void vi_replacech(FAR struct vi_s *vi, char ch)
{
  viinfo("curpos=%ld ch=%c[%02x]\n", vi->curpos, isprint(ch) ? ch : '.', ch);

  /* Is there a newline at the current cursor position? */

  if (vi->text[vi->curpos] == '\n')
    {
      /* Yes, then insert the new character before the newline */

      vi_insertch(vi, ch);
    }
  else
    {
      /* No, just replace the character and increment the cursor position */

      vi->text[vi->curpos++] = ch;
    }
}

/****************************************************************************
 * Name: vi_replacech_submode
 *
 * Description:
 *   Replace character command sub-mode of the command mode processing
 *
 ****************************************************************************/

static void vi_replacech_submode(FAR struct vi_s *vi)
{
  off_t end;
  long nchars;
  bool found = false;
  int ch = 0;

  /* Get the number of characters to replace */

  nchars = (vi->value > 0 ? vi->value : 1);

  viinfo("Enter replaces character(s) sub-mode: nchars=%d\n", nchars);

  /* Are there that many characters left on the line to be replaced? */

  end = vi_lineend(vi, vi->curpos);
  if (vi->curpos + nchars > end)
    {
      vi_error(vi, g_fmtnotvalid);
      vi_setmode(vi, MODE_COMMAND, 0);
    }

  /* Loop until we get the replacement character */

  while (vi->mode == SUBMODE_REPLACECH && !found)
    {
      /* Get the next character from the input */

      ch = vi_getch(vi);

      /* Handle the newly received character */

      switch (ch)
        {
          case KEY_FINDMODE_QUOTE: /* Quoted character follows */
            {
              /* Insert the next character unconditionally */

              ch    = vi_getch(vi);
              found = true;
            }
            break;

          case ASCII_ESC: /* Escape exits replace mode */
            {
              vi_setmode(vi, MODE_COMMAND, 0);
            }
            break;

          /* What do we do with carriage returns? line feeds? */

#if defined(CONFIG_EOL_IS_CR)
          case '\r': /* CR terminates line */
            {
              ch = '\n';
              found = true;
            }
            break;

#elif defined(CONFIG_EOL_IS_BOTH_CRLF)
          case '\r': /* Wait for the LF */
            break;
#endif

#if defined(CONFIG_EOL_IS_LF) || defined(CONFIG_EOL_IS_BOTH_CRLF)
          case '\n': /* LF terminates line */
            {
              found = true;
            }
            break;
#endif

#ifdef CONFIG_EOL_IS_EITHER_CRLF
          case '\r': /* Either CR or LF terminates line */
          case '\n':
            {
              ch = '\n';
              found = true;
            }
            break;
#endif

          default:
            {
              /* Ignore all but printable characters and tab */

              if (isprint(ch)  || ch == '\t')
                {
                  found = true;
                }
              else
                {
                  VI_BEL(vi);
                }
            }
            break;
        }
    }

  /* Now replace with the character nchar times */

  for (; nchars > 0; nchars--)
    {
      vi_replacech(vi, ch);
    }

  /* Revert to command mode */

  vi_setmode(vi, MODE_COMMAND, 0);
}

/****************************************************************************
 * Insert and Replace Mode Functions
 ****************************************************************************/

/****************************************************************************
 * Name: vi_insertch
 *
 * Description:
 *   Insert one character into the text buffer
 *
 ****************************************************************************/

static void vi_insertch(FAR struct vi_s *vi, char ch)
{
  viinfo("curpos=%ld ch=%c[%02x]\n", vi->curpos, isprint(ch) ? ch : '.', ch);

  /* Make space in the buffer for the new character */

  if (vi_extendtext(vi, vi->curpos, 1))
    {
      /* Add the new character to the buffer */

      vi->text[vi->curpos++] = ch;
    }
}

/****************************************************************************
 * Name: vi_insert_mode
 *
 * Description:
 *   Insert mode loop
 *
 ****************************************************************************/

static void vi_insert_mode(FAR struct vi_s *vi)
{
  int ch;

  viinfo("Enter insert mode\n");

  /* Loop while we are in insert mode */

  while (vi->mode == MODE_INSERT)
    {
      /* Make sure that the display reflects the current state */

      vi_showtext(vi);
      vi_setcursor(vi, vi->cursor.row, vi->cursor.column);

      /* Get the next character from the input */

      ch = vi_getch(vi);

      /* Any key press clears the error message */

      vi->error = false;

      /* Handle the newly received character */

      switch (ch)
        {
          case KEY_INSMODE_QUOTE: /* Quoted character follows */
            {
              /* Insert the next character unconditionally */

              vi_insertch(vi, vi_getch(vi));
            }
            break;

          case ASCII_DEL:
            {
              if (vi->curpos < vi->textsize)
                {
                  vi_shrinktext(vi, vi->curpos, 1);
                }
            }
            break;

          case ASCII_BS:
            {
              if (vi->curpos > 0)
                {
                  vi_shrinktext(vi, --vi->curpos, 1);
                }
            }
            break;

          case ASCII_ESC: /* Escape exits insert mode */
            {
              vi_setmode(vi, MODE_COMMAND, 0);
            }
            break;

          /* What do we do with carriage returns? */

#if defined(CONFIG_EOL_IS_CR)
          case '\r': /* CR terminates line */
            {
              vi_insertch(vi, '\n');
            }
            break;

#elif defined(CONFIG_EOL_IS_BOTH_CRLF)
         case '\r': /* Wait for the LF */
            break;
#endif

#if defined(CONFIG_EOL_IS_LF) || defined(CONFIG_EOL_IS_BOTH_CRLF)
          case '\n': /* LF terminates line */
            {
              vi_insertch(vi, '\n');
            }
            break;
#endif

#ifdef CONFIG_EOL_IS_EITHER_CRLF
          case '\r': /* Either CR or LF terminates line */
          case '\n':
            {
              vi_insertch(vi, '\n');
            }
            break;
#endif

          default:
            {
              /* Ignore all control characters except for tab and newline */

              if (!iscntrl(ch) || ch == '\t')
                {
                  /* Insert the filtered character into the buffer */

                  vi_insertch(vi, ch);
                }
              else
                {
                  VI_BEL(vi);
                }
            }
            break;
        }
    }
}

/****************************************************************************
 * Name: vi_replace_mode
 *
 * Description:
 *   Replace mode loop
 *
 ****************************************************************************/

static void vi_replace_mode(FAR struct vi_s *vi)
{
  off_t start = vi->curpos;
  int ch;

  viinfo("Enter replace mode\n");

  /* Loop until ESC is pressed */

  while (vi->mode == MODE_REPLACE)
    {
      /* Make sure that the display reflects the current state */

      vi_showtext(vi);
      vi_setcursor(vi, vi->cursor.row, vi->cursor.column);

      /* Get the next character from the input */

      ch = vi_getch(vi);

      /* Any key press clears the error message */

      vi->error = false;

      /* Handle the newly received character */

      switch (ch)
        {
          case KEY_FINDMODE_QUOTE: /* Quoted character follows */
            {
              /* Replace the next character unconditionally */

              vi_replacech(vi, ch);
            }
            break;

          case ASCII_BS: /* Delete the character before the cursor */
            {
              if (vi->curpos > start)
                {
                  vi->curpos = vi_cursorleft(vi, vi->curpos, 1);
                }
            }
            break;

          case ASCII_ESC: /* Escape exits find mode */
            {
              vi_setmode(vi, MODE_COMMAND, 0);
            }
            break;

          /* What do we do with carriage returns? line feeds? */

#if defined(CONFIG_EOL_IS_CR)
          case '\r': /* CR terminates line */
            {
              vi_replacech(vi, '\n');
            }
            break;

#elif defined(CONFIG_EOL_IS_BOTH_CRLF)
          case '\r': /* Wait for the LF */
            break;
#endif

#if defined(CONFIG_EOL_IS_LF) || defined(CONFIG_EOL_IS_BOTH_CRLF)
          case '\n': /* LF terminates line */
            {
              vi_replacech(vi, '\n');
            }
            break;
#endif

#ifdef CONFIG_EOL_IS_EITHER_CRLF
          case '\r': /* Either CR or LF terminates line */
          case '\n':
            {
              vi_replacech(vi, '\n');
            }
            break;
#endif

          default:
            {
              /* Ignore all but printable characters and TABs */

              if (isprint(ch) || ch == '\t')
                {
                  /* Insert the filtered character into the text buffer */

                  vi_replacech(vi, ch);
                }
              else
                {
                  VI_BEL(vi);
                }
            }
            break;
        }
    }
}

/****************************************************************************
 * Command line processing
 ****************************************************************************/

/****************************************************************************
 * Name: vi_showusage
 *
 * Description:
 *   Show command line arguments and exit.
 *
 ****************************************************************************/

static void vi_release(FAR struct vi_s *vi)
{
  if (vi)
    {
      if (vi->text)
        {
          free(vi->text);
        }

      if (vi->yank)
        {
          free(vi->yank);
        }

      free(vi);
    }
}

/****************************************************************************
 * Name: vi_showusage
 *
 * Description:
 *   Show command line arguments and exit.
 *
 ****************************************************************************/

static void vi_showusage(FAR struct vi_s *vi, FAR const char *progname,
                         int exitcode)
{
  fprintf(stderr, "\nUSAGE:\t%s [-c <columns] [-r <rows>] [<filename>]\n",
          progname);
  fprintf(stderr, "\nUSAGE:\t%s -h\n\n",
          progname);
  fprintf(stderr, "Where:\n");
  fprintf(stderr, "\t<filename>:\n");
  fprintf(stderr, "\t\tOptional name of the file to open\n");
  fprintf(stderr, "\t-c <columns>:\n");
  fprintf(stderr, "\t\tOptional width of the display in columns.  Default: %d\n",
          CONFIG_SYSTEM_VI_COLS);
  fprintf(stderr, "\t-r <rows>:\n");
  fprintf(stderr, "\t\tOptional height of the display in rows.  Default: %d\n",
          CONFIG_SYSTEM_VI_ROWS);
  fprintf(stderr, "\t-h:\n");
  fprintf(stderr, "\t\tShows this message and exits.\n");

  /* Release all allocated resources and exit */

  vi_release(vi);
  exit(exitcode);
}

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * Name: vi_main
 *
 * Description:
 *   The main entry point into vi.
 *
 ****************************************************************************/

#ifdef CONFIG_BUILD_KERNEL
int main(int argc, FAR char *argv[])
#else
int vi_main(int argc, char **argv)
#endif
{
  FAR struct vi_s *vi;
  int option;

  /* Allocate a vi state structure */

  vi = (FAR struct vi_s *)zalloc(sizeof(struct vi_s));
  if (!vi)
    {
      vi_error(vi, g_fmtallocfail);
      return EXIT_FAILURE;
    }

  /* Initialize non-zero elements of the vi state structure */

  vi->display.row    = CONFIG_SYSTEM_VI_ROWS;
  vi->display.column = CONFIG_SYSTEM_VI_COLS;

  /* Parse command line arguments */

  while ((option = getopt(argc, argv, ":c:r:h")) != ERROR)
    {
      switch (option)
        {
          case 'c': /* Display width in columns */
            {
              unsigned long value = strtoul(optarg, NULL, 10);
              if (value <= UINT16_MAX)
                {
                  vi->display.column = (uint16_t)value;
                }
              else
                {
                  fprintf(stderr, "ERROR: Column value out of range: %lu\n",
                          value);
                  vi_showusage(vi, argv[0], EXIT_FAILURE);
                }
            }
            break;

          case 'r': /* Display width in columns */
            {
              unsigned long value = strtoul(optarg, NULL, 10);
              if (value <= UINT16_MAX)
                {
                  vi->display.row = (uint16_t)value;
                }
              else
                {
                  fprintf(stderr, "ERROR: Row value out of range: %lu\n",
                          value);
                  vi_showusage(vi, argv[0], EXIT_FAILURE);
                }
            }
            break;

          case 'h':
            {
              vi_showusage(vi, argv[0], EXIT_SUCCESS);
            }
            break;

          case ':':
            {
              fprintf(stderr, "ERROR: Missing parameter argument\n");
              vi_showusage(vi, argv[0], EXIT_FAILURE);
            }
            break;

          case '?':
          default:
            {
              fprintf(stderr, "ERROR: Unrecognized parameter\n");
              vi_showusage(vi, argv[0], EXIT_FAILURE);
            }
            break;
        }
    }

  /* There may be one additional argument on the command line:  The filename */

  if (optind < argc)
    {
      /* Copy the file name into the file name buffer */

      strncpy(vi->filename, argv[optind], MAX_STRING - 1);

      /* Make sure that the (possibly truncated) file name is NUL terminated */

      vi->filename[MAX_STRING - 1] = '\0';

      /* Load the file into memory */

      (void)vi_insertfile(vi, 0, vi->filename);

      /* Skip over the filename argument.  There should nothing after this */

      optind++;
    }

  if (optind != argc)
    {
      fprintf(stderr, "ERROR: Too many arguments\n");
      vi_showusage(vi, argv[0], EXIT_FAILURE);
    }

  /* The editor loop */

  for (;;)
    {
      /* We loop, processing each mode change */

      viinfo("mode=%d\n", vi->mode);

      switch (vi->mode)
        {
          default:
          case MODE_COMMAND:      /* Command mode */
            vi_cmd_mode(vi);
            break;

          case SUBMODE_COLON:     /* Colon data entry in command mode */
            vi_cmd_submode(vi);
            break;

          case SUBMODE_FIND:      /* Find data entry in command mode */
            vi_find_submode(vi);
            break;

          case SUBMODE_REPLACECH: /* Replace characters in command mode */
            vi_replacech_submode(vi);
            break;

          case MODE_INSERT:       /* Insert mode */
            vi_insert_mode(vi);
            break;

          case MODE_REPLACE:      /* Replace character(s) under cursor until ESC */
            vi_replace_mode(vi);
            break;
        }
    }

  /* We won't get here */
}
