//********************************************************************************
//* File       : CsvView.cpp                                                     *
//* Author     : Mahlon R. Smith                                                 *
//*              Copyright (c) 2024-2026 Mahlon R. Smith, The Software Samurai   *
//*                  GNU GPL copyright notice located below                      *
//* Date       : 05-Apr-2026                                                     *
//* Version    : (see AppVersion string)                                         *
//*                                                                              *
//* Description: Read a CSV (Comma-Seperated-Values) file and format it for      *
//* viewing.                                                                     *
//* So far as we can tell, this format has never been formalized; however, the   *
//* W3C (World Wide Web Consortium) has formed a working group, and has issued   *
//* a draft specification, encoding some of the previous sloppy assumptions      *
//* about MIME type and other issues. See also RFC4180 which is the              *
//* Request-For-Comments document created by the Internet Engineering            *
//* Administration, Internet Research Task Force (IRTF)                          *
//*                                                                              *
//* a) Each field in a record is separated by a comma.                           *
//*    The last field of a record _is not_ followed by a comma.                  *
//* b) Each record is on a line terminated by either:                            *
//*    0x0A (newline), or                                                        *
//*    0x0D 0x0A (CRLF) [usually found with UTF16 encoding]                      *
//* c) Format:                                                                   *
//*    UTF8                                                                      *
//*    UTF16 (begins with FE FF (BE) or with FF FE (LE))                         *
//* d) mime type: "text/csv" (MIME database, not embedded as metadata)           *
//*    The MIME type database may be accessed via the 'file' utility.            *
//*        Example: file sample.csv    reports: sample.csv: CSV text             *
//*    Practically speaking, this is completely useless.                         *
//* e) A header record is optional and may or may not be indicated within the    *
//*    mime type. (text/csv;header=present || absent)                            *
//* f) Although commas are considered as standard field delimiters, (duh!),      *
//*    tabs, semicolons or double quotes may optionally used as field delimiters *
//*    in conjunction with commas, but if used, then the field data must not     *
//*    include the delimiter character.                                          *
//*    Example: "field1","field2","field3"CRLF                                   *
//*    The delimiter may be escaped by doubling it (no matter how stupid that    *
//*    would be):  "field""one""","field2"CRLF                                   *
//*    We had hoped that no one was dumb enough to use secondary delimiters in a *
//*    CSV file, but a GMail contacts CSV file does indeed use double quotes     *
//*    as delimiters under some circumstances. This allows them to use newlines  *
//*    and commas as ordinary text within the doubly-delimited fields.           *
//*    The "Notes" field is the primary example.                                 *
//*    This has made the application's text-formatting code more complex.        *
//* g) According to datablist.com, the CSV file may contain an "index column"    *
//*    which gives the index of each record. This is apparently done by having   *
//*    the first field in each record be the index field and the header for      *
//*    field be empty (or blank).                                                *
//*                                                                              *
//*                                                                              *
//* W3C                                                                          *
//*    https://www.w3.org/TR/tabular-data-primer/                                *
//*    https://www.w3.org/2013/csvw/wiki/Main_Page.html                          *
//* FileFormat.com:                                                              *
//*    https://docs.fileformat.com/spreadsheet/csv/                              *
//* The Internet Society                                                         *
//*    https://www.rfc-editor.org/rfc/rfc4180                                    *
//* The Internet Society (2005):                                                 *
//*    https://datatracker.ietf.org/doc/html/rfc4180                             *
//* Wikipedia:                                                                   *
//*   https://en.wikipedia.org/wiki/Comma-separated_values                       *
//*                                                                              *
//*  --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- *
//* NOTE: The 'less' utility recognizes BOM markers, at least for _some_ CSV     *
//*       files.                                                                 *
//*         a) UTF-8 BOMs (EF BB BF) are recognized and not displayed.           *
//*         b) UTF-16LE BOMs (FE FF) are recognized and not displayed.           *
//*         c) UTF-16BE BOMs (FF FE) are recognized and not displayed.           *
//*       However, various UTF-16 CSV files gathered from online or generated    *
//*       locally are seen as "binary" files by 'less', and are therefore        *
//*       displayed as garbage, presumably because under some circumstances      *
//*       'less' "forgets" that UTF-16 encoded data contain null characters.     *
//*       -- If 'less' used the MIME database, it wouldn't get so confused.      *
//*                                                                              *
//* NOTE: Because the source data are often UTF-16, we try to use "uint8_t" for  *
//*       input buffers as much as possible.                                     *
//*       As of C++20, it is possible to specify unsigned char for the input     *
//*       stream methods; however, we do not want to force developers to upgrade *
//*       their compilers just for that. We do require C++17 (gnu++17) support   *
//*       for various file-related issues.                                       *
//*                                                                              *
//*  --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- *
//* Development Tools:                                                           *
//*  GCC 14.3.1 20250808 (Red Hat 14.3.1-3) gnu++17 support required             *
//*  AnsiCmd Library v:0.0.6 or greater by the author.                           *
//*    Note: AnsiCmd Library is built with the API code disabled:                *
//*             INCLUDE_API (0)                                                  *
//*    Note: AnsiCmd Library is built will debugging options disabled:           *
//*             DEBUG_ANSICMD (0)                                                *
//*          and with the the API extensions disabled:                           *
//*             INCLUDE_API (0)                                                  *
//*                                                                              *
//*                                                                              *
//********************************************************************************
//* Version History (most recent first):                                         *
//*                                                                              *
//* v: 0.0.06 02-Oct-2025                                                        *
//*   -- Update file stats retrieval to use the C++17 statx() function and       *
//*      'struct statx' which provides additional information on a target file.  *
//*      -- The primary difference is the redefinition of the C-style structure  *
//*         to C++ object syntax. See"FileStats".                                *
//*      -- The names of the member variables have changed slightly.             *
//*                                                                              *
//* v: 0.0.05 18-Nov-2024                                                        *
//*   -- Make minor modifications to support the new definition of the gString   *
//*      class.                                                                  *
//*   -- Bug Fix: Initialize the 'maxFldWidth' array to zeros after allocating   *
//*      the space. Previously, we foolishly relied on the compiler to do this   *
//*      automatically. Lesson learned!                                          *
//*   -- Replace the hard-coded temporary-file management with an instance of    *
//*      the TFMan class. The AnsiCmd built-in temp-file management methods are  *
//*      not used in this application because they are under the API conditional *
//*      compilation directive.                                                  *
//*   -- Compensate for a potential buffer overrun condition in the 'less'       *
//*      utility. See outputThresholdTest().                                     *
//*                                                                              *
//* v: 0.0.04 04-Mar-2024                                                        *
//*   -- Add the "--capture" command line option. During development, this option*
//*      can be used to isolate records which are causing parsing problems.      *
//*   -- Major documentation update.                                             *
//*                                                                              *
//* v: 0.0.03 18-Feb-2024                                                        *
//*   -- Begin translation of user interface text to Chinese.                    *
//*      The wide vs. narrow output stream is a problem. If the Chinese text is  *
//*      stored as UTF-8 data, i.e. const char* zhText = "....." ;  then it is   *
//*      rendered incorrectly in the terminal window.                            *
//*      -- While we could have simply stored all the Chinese text as wide data, *
//*         i.e. const wchar* zhText = L"....." ;  this would have unnecessarily *
//*         increased the size of the executable.                                *
//*      -- The solution is that we updated the AnsiCmd library's "ttyWrite"     *
//*         group of methods. "ttyWrite" now automagically converts UTF-8 source *
//*         data to UTF-32 which can then be written to the output stream.       *
//*         This is why the narrow I/O streams are not recommended for modern    *
//*         applications.                                                        *
//*         -- Note that the "acWrite" method group routes all input through     *
//*            the gString object, which avoids the wide/narrow problem.         *
//*   -- Limited distribution.                                                   *
//*                                                                              *
//* v: 0.0.02 02-Feb-2024                                                        *
//*   -- Add new command-line options.                                           *
//*   -- Finish implementation of horizontal and vertical formatting of the      *
//*      output. See dfHoriz and dfVert.                                         *
//*   -- Handle secondary delimeter pairs. These are double quote characters     *
//*      in combination with the commas used as field delimeters. If secondary   *
//*      delimeter pairs are present, we capture and convert embedded literal    *
//*      commas and linefeeds (or CR/LF pairs) so that they do not affect parsing*
//*      of the record or formatting of the output.                              *
//*   -- Add functionality for creating CSV files in any of the four supported   *
//*      encodings, using a plain-text file as the source data.                  *
//*      See createCsvFile(), convert2UTF16() and convertNewlines() for          *
//*      additional information.                                                 *
//*   -- Integrate the application into the AnsiCmd distribution package.        *
//*      -- The AnsiCmd library is built with debugging disabled AND with API    *
//*         functionality disabled (directive: INCLUDE_API == 0). This reduces   *
//*         the overall size of the application by approximately 800 Kbytes.     *
//*                                                                              *
//* v: 0.0.01 11-Jan-2024                                                        *
//*   -- Basic application layout and execution flow.                            *
//*   -- Collect sample CSV files.                                               *
//*   -- Compare various OPINIONS about what the Comma-Seperated-Values format   *
//*      actually looks like. (There is no specification.)                       *
//*   -- Adapt low-level file manipulation methods from FileMangler application. *
//*   -- Implement create/delete temporary files.                                *
//*   -- limited distribution                                                    *
//*                                                                              *
//********************************************************************************
//* Copyright Notice:                                                            *
//* This program is free software: you can redistribute it and/or modify it      *
//* under the terms of the GNU General Public License as published by the Free   *
//* Software Foundation, either version 3 of the License, or (at your option)    *
//* any later version.                                                           *
//*                                                                              *
//* This program is distributed in the hope that it will be useful, but          *
//* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY   *
//* or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License     *
//* for more details.                                                            *
//*                                                                              *
//* You should have received a copy of the GNU General Public License along      *
//* with this program.  If not, see <http://www.gnu.org/licenses/>.              *
//*                                                                              *
//*         Full text of the GPL License may be found in the Texinfo             *
//*         documentation for this program under 'Copyright Notice'.             *
//********************************************************************************
//* To Do:                                                                       *
//* - Regenerate all sample files.                                               *
//*   Example: ./csvv --create='csvsource.txt,utf16be,crlf'                      *
//*      FORMAT                     FORMAT                                       *
//*      ---------------------      -----------------------                      *
//*      utf8.csv        DONE       utf16be.csv       DONE                       *
//*      utf8_cr.csv     DONE       utf16be_cr.csv    DONE                       *
//*      utf8bom.csv     DONE       utf16le.csv       DONE                       *
//*      utf8bom_cr.csv  DONE       utf16le_cr.csv    DONE                       *
//*   ** Note that all files contain examples of embedded commas enclosed        *
//*      within field delimiters (double quotes).                                *
//*                                                                              *
//* - Handle embedded newlines as line breaks: DONE                              *
//*   - vertical formatting                                                      *
//*   - horizontal formatting                                                    *
//*   - quick formatting                                                         *
//* - Handle embedded commas as escaped commas: \, DONE                          *
//* - Translate all UI text for Chinese UI option.                               *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//********************************************************************************

//****************
//* Header Files *
//****************
#include "CsvView.hpp"

//***************
//* Definitions *
//***************
static const uint8_t DQ = '"' ;              // ASCII double quotation mark
static const uint8_t SPECIAL_BS = '\\' ;     // escape char for embedded ',' and '\n'
static const uint8_t SPECIAL_BSTRK = '\\' ;  // escape char for embedded ',' and '\n'
static const uint8_t SPECIAL_LFEED = 'n' ;   // code for embedded newline
static const uint8_t SPECIAL_COMMA = 'c' ;   // code for embedded comma
static const wchar_t SPEC_COMMA_SEQ[] = { SPECIAL_BSTRK, SPECIAL_COMMA, L'\0' } ;
static const wchar_t SPEC_LFEED_SEQ[] = { SPECIAL_BSTRK, SPECIAL_LFEED, L'\0' } ;

//***************
//* Prototypes  *
//***************
static short insertSpecialChar ( uint8_t *buff, short cnt, short ip ) ;
static short countSubstrings ( const wchar_t* wptr, short wchars ) ;

//**************
//* Local Data *
//**************


//*************************
//*         main          *
//*************************
//********************************************************************************
//* Program entry point.                                                         *
//*                                                                              *
//* Command-line Usage:  See "DisplayHelp() method.                              *
//*                                                                              *
//*                                                                              *
//********************************************************************************

int main ( int argc, char* argv[], char* argenv[] )
{
   //* User may have specified one or more command-line arguments.           *
   commArgs clArgs( argc, argv, argenv ) ;

   //* Create the application class object and interact with the user.       *
   CsvView* csvptr = new CsvView( clArgs ) ;

   //* Before returning to the system, delete the Keymap object.     *
   //* While the class object and all its resources will be released *
   //* by the system on exit, we want to force execution of the      *
   //* destructor for an orderly termination.                        *
   delete csvptr ;

   exit ( ZERO ) ;

}  //* End main() *


//*************************
//*       ~CsvView        *
//*************************
//********************************************************************************
//* Destructor: Return all resources to the system and return terminal window    *
//*             to initial configuration.                                        *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

CsvView::~CsvView ( void )
{
   delete this->ansi ;                    // delete the AnsiCmd instance

   if ( this->maxFldWidth != NULL )       // release dynamic allocation
      delete [] this->maxFldWidth ;
   if ( this->tfm != NULL )               // if temp file(s), delete them
      delete this->tfm ;

}  //* End ~CsvView() *

//*************************
//*        CsvView        *
//*************************
//********************************************************************************
//* Constructor: Create an instance of the class and initialize all members.     *
//*              Invoke the user interface.                                      *
//*              Shut down when session has completed.                           *
//*                                                                              *
//* Input  : clArgs : (by reference) copy of the captured command-line arguments *
//*                                                                              *
//* Returns: implicitly returns a pointer to object                              *
//********************************************************************************

CsvView::CsvView ( commArgs& clArgs )
{
   gString gsOut ;                        // text formatting

   //* Initialize all class data members.*
   this->reset () ;
   //* Set application's terminal configuration defaults *
   clArgs.tConfig.bufferInput   = false ;
   clArgs.tConfig.echoOption    = noEcho ;
   clArgs.tConfig.bcType        = cchtExit ;
   clArgs.tConfig.foreground    = aesFG_BLUE ;
   clArgs.tConfig.cursorStyle   = csBblock ;

   //* Create a subdirectory on the temp-file path. This will *
   //* contain all temp files created by the application.     *
   this->tfm = new TFMan( "CSV_" ) ;

   //* Parse command-line arguments *
   this->GetCommandLineArgs ( clArgs ) ;
   //* Copy selected attributes to our members *
   this->txtAttr = clArgs.tConfig.foreground ;
   this->bgdAttr = clArgs.tConfig.background ;

   //* Initialize the ANSI terminal attributes for input/output.*
   //* Note that color attributes are set during initialization.*
   //* See notes above about terminal configuration.            *
   this->ansi = new AnsiCmd ( clArgs.tConfig ) ;

   //* If the locale specified by the terminal environment does *
   //* not match the specified user interface language, set the *
   //* local which matches the UI language.                     *
   if ( *clArgs.tConfig.localeName != NULLCHAR )
      this->ansi->acSetLocale ( clArgs.tConfig.localeName ) ;

   //* Get terminal dimensions *
   this->ansi->acGetTermDimensions ( this->termRows, this->termCols ) ;

   //* If pause to view debugging information *
   if ( clArgs.pakFlag && ((clArgs.userOption == clVersion) || 
        (clArgs.userOption == clHelp) || (clArgs.userOption == clHelpless)) )
   { this->pause () ; clArgs.pakFlag = false ; }

   bool done = true ;
   switch ( clArgs.userOption )
   {
      case clVersion:
         this->DisplayVersion () ;
         break ;
      case clQuickHelp:
      case clHelp:
      case clHelpless:
         this->DisplayHelp ( clArgs.userOption ) ;
         break ;
      default:       // valid command(s) (see below)
         done = false ;
         break ;
   }

   if ( ! done )
   {
      if ( clArgs.dimFlag )         // report the size of the terminal window
         this->dumpTermSize () ;
      if ( clArgs.staFlag )         // report sourcefile stats
         this->dumpFileStats ( clArgs.dimFlag ? false : true ) ;
      if ( clArgs.cfgFlag )         // report terminal configuration
         this->dumpTermInfo ( (clArgs.staFlag || clArgs.dimFlag) ? false : true ) ;

      //* If source file specified, scan it now *
      if ( clArgs.userOption == clOpen )
      {
         //* Clear the terminal window and display the app title. *
         if ( ! clArgs.staFlag && ! clArgs.dimFlag && 
              ! clArgs.cfgFlag && ! clArgs.pakFlag )
         {
            this->ComposeTitle ( gsOut, false ) ;
            gsOut.append( L'\n' ) ;
            this->ansi->acEraseArea ( aesERASE_WIN ) ;// clear the terminal window
            this->ansi->acSetCursor ( aesCUR_HOME ) ; // set cursor to origin
            this->ansi->ttyWrite ( gsOut ) ;          // display app title
         }

         //* If preliminary scan not yet performed, do it now. *
         if ( ! this->srcScanned )
            this->SrcScan () ;

         //* If data format verified *
         if ( this->srcCsvFmt )
         {
            //* If a stat request on startup, then prompt *
            //* user for a keypress before continuing.    *
            if ( clArgs.staFlag || clArgs.dimFlag || clArgs.cfgFlag || clArgs.pakFlag )
            { this->pause () ; }

            //* Display the data using the specified layout. *
            //* If specified, write the reformatted data to  *
            //* target file in the current directory.        *
            this->DisplayData () ;
         }
         else     // invalid file format
         { this->ansi->ttyWrite ( L"\nError! Source file is not CSV format.\n\n" ) ; }
      }

      //* If request to generate a file in CSV format *
      else if ( clArgs.userOption == clCreate )
      {
         this->createCsvFile ( this->orgPath, clArgs.trgEncode, clArgs.trgCrlf ) ;
      }

      //* If a request to capture a range of records from the source file. *
      else if ( clArgs.userOption == clCapture )
      {
         this->ComposeTitle ( gsOut, false ) ;
         gsOut.append( L'\n' ) ;
         this->ansi->acEraseArea ( aesERASE_WIN ) ;// clear the terminal window
         this->ansi->acSetCursor ( aesCUR_HOME ) ; // set cursor to origin
         this->ansi->ttyWrite ( gsOut ) ;          // display app title

         //* If a stat request on startup, then prompt *
         //* user for a keypress before continuing.    *
         if ( clArgs.staFlag || clArgs.dimFlag || clArgs.cfgFlag || clArgs.pakFlag )
         { this->pause () ; }

         this->captureCsvRecords ( this->orgPath, clArgs.capA, clArgs.capB ) ;
      }

      else
      {
         if ( (clArgs.userOption == clTermSize) ||
              (clArgs.userOption == clTermInfo) )
         { /* do nothing */ }
         else if ( (this->srcStats.fType == fmUNKNOWN_TYPE) ||
                   (this->srcStats.fType == fmTYPES) )
         {
            gsOut.compose( "\"%s\": File Not Found.\n\n", 
               (*this->orgPath == NULLCHAR ? "---" : this->orgPath) ) ;
         }
         else if ( ! this->srcStats.readAcc || !this->srcStats.writeAcc )
            gsOut = "Access Error! Check read/write permissions.\n\n" ;
         else if ( this->srcStats.fType != fmREG_TYPE )
            gsOut = "Specified File Is Not A \"Regular\" File.\n\n" ;
         else
         {
            gsOut = this->orgPath ;
            if ( (gsOut.find( CSV_EXT, (gsOut.gschars() - 5), false )) < ZERO )
               gsOut = "Filename Extension Must Be: \".csv\".\n\n" ;
            else
               gsOut = "Unknown Error.\n\n" ;
         }
         this->ansi->ttyWrite ( gsOut ) ;
      }
   }

}  //* End CsvView() *

//*************************
//*      DisplayData      *
//*************************
//********************************************************************************
//* Format the data for display and write it to the terminal window.             *
//*                                                                              *
//* The output format is specified by the 'dispfmt' member.                      *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Programmer's Note:                                                           *
//* a) The formatting algorithm for dfHoriz, and dfVert is very slow.            *
//*    Although speed is not critical for this application, we may need to tweak *
//*    it a bit for efficiency.                                                  *
//* b) There is some possibility that a record will be truncated due to the      *
//*    capacity of the gString object. So far, the records of our test files     *
//*    all fit comfortably, but keep an eye on this.                             *
//* c) If records contain pairs of double-quotation marks, it is possible, though*
//*    not certain that the designer intended them as delimiters.                *
//*    -- Example: ,"Allison Zhang, TZ Student",                                 *
//*    -- Commas OUTSIDE of double quotes are assumed to be field delimiters.    *
//*    -- Commas INSIDE double quote sequences are assumed to be ordinary text   *
//*       and are handled when parsing the field data for the first time.        *
//*       See SrcScan() method for more information.                             *
//*    -- Literal NEWLINE characters (or CR/LF pairs) may also be present        *
//*       within double quote sequences. For convenience of display-output       *
//*       formatting, during the initial scan of the data, these newlines WITHIN *
//*       delimeters are identified and converted to a printing character.       *
//*       (see convertDoubleQuotes() method)                                     *
//*       Thus, literal newlines within a field will not be seen here.           *
//* d) Note on invoking the 'less' utility via the popen() function:             *
//*    -- The '-c' '--clear-screen' option clears terminal window before display.*
//*    -- The '-S' '--chop-long-lines' option prevents automatic word wrap.      *
//*       The undisplayed data are revealed when the user presses the right      *
//*       arrow key which will shift the data leftward by half a screen.         *
//*    -- Limitations of using piped input to the 'less' utility:                *
//*       Buffer allocation for 'less' is not robust. When input to 'less' is    *
//*       from a pipe, the buffer space is ideally allocated as needed; however, *
//*       this is misleading. Actual buffer space is tied to the amount of data  *
//*       that has been displayed. Because buffering is unreliable, a large      *
//*       source file which cannot be displayed all on one page may cause a      *
//*       memory access violation when the utility exits.                        *
//*       Two parameters address buffering:                                      *
//*       1) --auto-buffers (B) option:                                          *
//*          By default, when data is read from a pipe, buffers are allocated    *
//*          automatically as needed.  If a large amount of data is read from    *
//*          the  pipe, this  can  cause a large amount of memory to be          *
//*          allocated.  The -B option disables this automatic allocation of     *
//*          buffers for pipes, so that only 64 KB (or the amount of space       *
//*          specified by the -b option) is used for the pipe.  Warning: use of  *
//*          -B can result in erroneous  display, since  only  the  most recently*
//*          viewed part of the piped data is kept in memory; any earlier data   *
//*          is lost.  Lost characters are displayed as question marks.          *
//*       2) --buffers=n (-bn) option:                                           *
//*          Specifies the amount of buffer space less will use for each file,   *
//*          in units of kilobytes (1024 bytes).  By default 64 KB of buffer     *
//*          space is used for each file (unless the file is a pipe; see the     *
//*          -B option). The -b option specifies instead that 'n' kilobytes of   *
//*          buffer space should be used for each file.  If n is -1, buffer space*
//*          is unlimited; that is, the entire file can be read into memory.     *
//*                                                                              *
//*       Because buffering in the 'less' utility is unreliable (as described    *
//*       above), it may cause a memory access violation when formatted data     *
//*       are piped. This does not affect the actual display of the data.        *
//*       The violation occurs when exiting 'less'. Reported error:              *
//*                    corrupted size vs. prev_size                              *
//*                    Aborted (core dumped)                                     *
//*       Therefore, if the source data are larger than the default 'less'       *
//*       buffer (64 Kibibytes), output is redirected to a temporary file rather *
//*       than being piped directly to 'less'. This is annoying, and if a more   *
//*       elegant fix can be found, it will be implemented.                      *
//*                                                                              *
//********************************************************************************

void CsvView::DisplayData ( void )
{
   #define DEBUG_VP (0)       // for debugging: test padding for substring output

   uint8_t *ibuff = new uint8_t[ibBYTES] ;// input buffer
   uint8_t *obuff = new uint8_t[ibBYTES] ;// output buffer
   uint8_t *phbuff = NULL ;               // storage for placeholder substrings
   uint8_t *phdata[this->fldCount] ;      // pointers to placeholder substrings
   gString gsOut, gs, gsHdr, gstmp ;      // text formatting
   short maxRows,                         // max output rows per screen
         rowsOut = 2,                     // number of rows written (title == 2)
//         obytes,                          // number of formatted data bytes
         olines ;                         // number of formatted data lines
   wchar_t usrChar ;                      // user response
   bool write2stdout = bool(this->dispfmt == dfQuick) ; // output target
   bool done = false ;                    // loop control

   //* Data specifically for vertical formatting *
   short hsi = ZERO,                      // index of header field data
         fsi = ZERO ;                     // index of record field data

   //* Get terminal dimensions *
   maxRows = this->termRows - 2 ;

   //* Construct output prefix *
   this->ComposeTitle ( gsOut, false ) ;
   gsOut.append( L'\n' ) ;
   if ( ! write2stdout && (*this->trgPath != NULLCHAR) )
   {
      gString gstp = this->trgPath ;
      this->ExtractFilename ( gstmp, gstp ) ;
      gs.compose( "On Exit, Copied To: %S\n", gstmp.gstr() ) ;
      gs.padCols( gs.gscols() * 2, L'-' ) ;
      gs.append( L'\n' ) ;
      rowsOut += 2 ;
   }

   //* Construct the placeholder substrings.                *
   //* Create a placholder substring for each field,        *
   //* and store them in 'phbuff' with pointers in phdata[].*
   phbuff = new uint8_t[this->maxRecWidth * 2] ;
   short b = ZERO ;
   for ( short f = ZERO ; f < this->fldCount ; ++f )
   {
      gstmp.clear() ;
      #if DEBUG_CSVV != 0 && DEBUG_VP != 0
      gstmp.padCols( this->maxFldWidth[f], L'-' ) ;
      #endif   // DEBUG_CSVV && DEBUG_VP
      gstmp.padCols( this->maxFldWidth[f] + 1 ) ; // create as field width (plus 1)
      gstmp.copy( &phbuff[b], gstmp.utfbytes() ) ;// save the substring data
      phdata[f] = (uint8_t*)(&phbuff[b]) ;        // record pointer to substring
      b += gstmp.utfbytes() ;
   }

   //* Open the file containing the (normalized) source. *
   ifstream ifs( this->srcPath, ifstream::in ) ;   // open the file
   if ( ! (ifs.is_open()) )   // Error opening file (unlikely)
   {
      this->ansi->ttyWrite ( L"\nError! Unable to open source file!\n" ) ;
      return ;       // note the early return
   }

   //* If not "Quick" format, pipe the output through 'less'. *
   if ( ! write2stdout )
   {
      //* If the source file is beyond the threshold size for  *
      //* a "safe" pipe to the 'less' utility, write the       *
      //* formatted data to a temporary file. This bypasses    *
      //* the piped output to 'less'. (see note above)         *
      char outTemp[MAX_PATH] ;
      bool srcOverflow = this->outputThresholdTest () ;

      //* If specified, open a temp file to save a copy of the displayed data. *
      ofstream cofs ;                        // formatted-data temp file
      if ( *this->trgPath )
      {
         gstmp.clear() ;
         this->tfm->createTempFile ( gstmp ) ;  // create a temporary file
         gstmp.copy( this->cpyPath, MAX_PATH ) ;// save the filespec
         cofs.open( this->cpyPath, ofstream::out | ofstream::trunc ) ; // open the file
      }

      FILE *output ;
      if ( srcOverflow )            // capture the output to a temp file
      {
         this->tfm->createTempFile ( gstmp ) ;
         gstmp.copy( outTemp, MAX_PATH ) ;
         output = fopen ( outTemp, "w" ) ;
      }

      //* Open the pipe to write the ouput directly *
      //* to the 'less' utility. (see notes above)  *
      else
         output = popen ( "less -c -S", "w" ) ;

      if ( output != NULL )
      {
         //* Write the app title and source path. *
         fprintf ( output, gsOut.ustr() ) ;
         if ( cofs.is_open() ) { cofs << gsOut.ustr() ; }
         if ( (gs.gschars()) > 1 )
            fprintf ( output, gs.ustr() ) ;

         //* If vertical output format, read the header (if any). *
         //* If no header, construct default header text.         *
         if ( this->dispfmt == dfVert )
         {
            if ( this->srcHeader )
            {
               ifs.getline( (char*)ibuff, ibBYTES, NEWLINE ) ;
               if ( (ifs.gcount()) > ZERO )
               {
                  ibuff[ifs.gcount() - 1] = NEWLINE ;
                  ibuff[ifs.gcount()] = NULLCHAR ;
                  gsHdr = (char*)ibuff ;
               }
               else
                  done = true ;     // this is unlikely
            }
            else
            {
               for ( short f = 1 ; f <= this->fldCount ; ++f )
                  gsHdr.append( "Field #% 2hd  ,", &f ) ;
               short indx = gsHdr.findlast( COMMA ) ;
               gsHdr.replace( COMMA, NEWLINE, indx ) ;
               this->maxHdrWidth = 10 ;   // override the calculated value
            }
         }

         //* Read the records of the source file. *
         while ( ! done )
         {
            ifs.getline( (char*)ibuff, ibBYTES, NEWLINE ) ;
            if ( ((ifs.gcount()) > ZERO) || (ifs.good()) )
            {
               //* Get the record and append a newline *
               gs.compose( "%s\n", ibuff ) ;

               //* Call the specified formatting method for each record. *
               if ( this->dispfmt == dfHoriz )
               {
                  this->ddFormatHorizontal ( gs, obuff, 
                                             (const uint8_t**)(phdata), olines ) ;
                  fprintf ( output, (char*)(obuff) ) ;
                  if ( cofs.is_open() ) { cofs << (char*)(obuff) ; }
               }
               //* Format each field, one field per row *
               else
               {
                  hsi = fsi = ZERO ;
                  for ( short f = ZERO ; f < this->fldCount ; ++f )
                  {
                     this->ddFormatVertical ( &gsHdr.gstr()[hsi], &gs.gstr()[fsi], gsOut ) ;
                     fprintf ( output, gsOut.ustr() ) ;
                     if ( cofs.is_open() ) { cofs << gsOut.ustr() ; }
                     hsi = gsHdr.after( COMMA, hsi ) ;
                     fsi = gs.after( COMMA, fsi ) ;
                  }
                  fprintf ( output, "\n" ) ; // space between records
                  if ( cofs.is_open() ) { cofs << "\n" ; }
               }
            }
            else
               done = true ;
         }

         //* Flush and close output stream *
         fflush ( output ) ;              // flush the output stream
         if ( srcOverflow )
         {
            fclose ( output ) ;           // close the temp file
            gstmp.compose( "less -c -S %s", outTemp ) ;
            system( gstmp.ustr() ) ;
         }
         else { pclose ( output ) ; }     // close the pipe

         write2stdout = false ;
      }
      else { write2stdout = true ; } // pipe unavailable, write to stdout (unlikely)

      //* If copy of output saved, close the file. *
      if ( cofs.is_open() ) { cofs.close() ; }
   }

   //* If "Quick" format (or if access to 'less' failed) *
   // Programmer's Note: As the name implies this is a "quick" output.
   // We do not compensate for output rows that auto-wrap. Instead, we 
   // simply truncate records that are wider than the window.
   if ( write2stdout )
   {
      //* Clear the terminal window *
      this->ansi->acEraseArea ( aesERASE_WIN ) ;// clear the terminal window
      this->ansi->acSetCursor ( aesCUR_HOME ) ; // set cursor to origin

      //* Display the output prefix *
      this->ansi->ttyWrite ( gsOut, false ) ;
      if ( (gs.gschars()) > 1 )
         this->ansi->ttyWrite ( gs ) ;

      while ( ! done )
      {
         ifs.getline( (char*)ibuff, ibBYTES, NEWLINE ) ;
         if ( ((ifs.gcount()) > ZERO) || (ifs.good()) )
         {
            gs.compose( "%s\n", ibuff ) ;
            this->ddFormatHorizontal ( gs, obuff, 
                                       (const uint8_t**)(phdata), olines, true ) ;

            //* Convert to wchar_t and write to terminal window.*
            gsOut = obuff ;
            this->ansi->ttyWrite ( gsOut ) ;

            //* If window is full, ask user for keypress to continue. *
            if ( (rowsOut += olines) >= maxRows )
            {
               this->ansi->acSetMod ( aesUNDERLINE ) ;
               this->ansi->ttyWrite ( 
                     L"Press a key for next page (or 'Q' to Quit)..." ) ;
               this->ansi->acSetMod ( aesUNDERLINE_OFF ) ;
               usrChar = this->ansi->acRead () ;          // wait for user input
               this->ansi->acEraseArea ( aesERASE_WIN ) ; // clear the terminal window
               this->ansi->acSetCursor ( aesCUR_HOME ) ;  // set cursor to origin
               rowsOut = ZERO ;                           // reset row counter
               if ( (usrChar == L'q') || (usrChar == L'Q') )
                  done = true ;
            }
         }
         else
            done = true ;
      }
   }

   //* Say 'Goodnight Gracie' *
   gsOut = this->orgPath ;
   this->ExtractFilename ( gs, gsOut ) ;
   gsOut.compose( "\n   \"%S\" - Output complete!   \n\n", gs.gstr() ) ;
   this->ansi->acSetMod ( aesUNDERLINE ) ;
   this->ansi->ttyWrite ( gsOut ) ;
   this->ansi->acSetMod ( aesUNDERLINE_OFF ) ;

   //* Close the source file *
   if ( (ifs.is_open()) )
      ifs.close() ;

   //* If specified, write the reformatted data to *
   //* target file in the current directory.       *
   if ( *this->trgPath != NULLCHAR )
      this->save2Target () ;

   //* Delete the temporary file *
   this->tfm->deleteTempFile ( this->srcPath ) ;

   if ( ibuff != NULL )  { delete [] ibuff ; } // release the dynamic allocations
   if ( obuff != NULL )  { delete [] obuff ; }
   if ( phbuff != NULL ) { delete [] phbuff ; }

}  //* End DisplayData() *

//*************************
//*  ddFormatHorizontal   *
//*************************
//********************************************************************************
//* Called by DispData() to format a record for display on one or more lines.    *
//*                                                                              *
//* If a field contains embedded newlines, then the output will be formatted for *
//* multiple lines, one line for each substring of the field with the greatest   *
//* number of substrings.                                                        *
//*                                                                              *
//* Input  : gssrc  : source data (trailing newline '\n' appended)               *
//*          obuff  : receives formatted output data                             *
//*                   (It is assumed that caller's buffer is AT LEAST ibBYTES)   *
//*                   (in length.                                            )   *
//*          phptr  : array of placeholder substrings                            *
//*          olines : (by reference) receives the number of output lines         *
//*          trunc  : (optional, 'false' by default)                             *
//*                   if 'true',  truncate output to fit width of terminal window*
//*                   if 'false', do not truncate output data                    *
//*                                                                              *
//* Returns: number of data bytes in 'obuff'                                     *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//* Records which contain embedded newline sequences must be handled as a        *
//* special case because they cannot be displayed by a single line of output.    *
//* Instead, they are displayed as a two-dimensional grid defined horizontally   *
//* by the number of fields and vertically by the maximum number of substrings   *
//* in any field of the record.                                                  *
//*                                                                              *
//* For a record of this form:      ABC,DEF\nGHI\nJKL,MNO\nPQR,STU               *
//*                                                                              *
//* The output will take this form: ABC GHI MNO STU                              *
//*                                 --- GHI PQR ---                              *
//*                                 --- JKL --- ---                              *
//*                                                                              *
//********************************************************************************

short CsvView::ddFormatHorizontal ( const gString& gssrc, uint8_t *obuff, 
                                    const uint8_t **phptr, short& olines, bool trunc )
{
   gString gstrg, gstmp ;                 // text formatting
   const wchar_t* wptr = gssrc.gstr() ;   // pointer to (wide) source data
   short wchars = gssrc.gschars(),        // number of source characters
         si = ZERO,                       // source index
         obytes = ZERO,                   // return value, total bytes in buffer
         ssMax,                           // max depth of substring levels
         fld = ZERO ;                     // field index
   bool  embeddedNlines = false,          // set if embedded newlines identified
         done = false ;                   // loop control

   obuff[obytes] = NULLCHAR ;             // initialize caller's buffer
   olines = ZERO ;                        // initialize caller's counter

   //* If no embedded newlines, then the record is displayed on a single line. *
   //* Else record is displayed on a two-dimensional grid. (see note above)    *
   for ( short si = ZERO ; si < wchars ; ++si )
   {
      if ( wptr[si] == SPECIAL_BSTRK )
      {
         if ( wptr[si + 1] == SPECIAL_LFEED )
         {
            embeddedNlines = true ;       // set the 'complex field' flag
            //* Determine max number of substrings in any field. *
            ssMax = countSubstrings ( wptr, wchars ) ;
            break ;
         }
      }
   }     // for(;;)

   //* If embedded newlines within one or more fields *
   if ( embeddedNlines )
   {
      wchar_t fldBuff[gsALLOCMIN] ;       // buffer for isolating field data
      short   fbIndx,                     // index into fldBuff
              sublevel = ZERO,            // number of source scans (substrings found)
              rowBase = ZERO,             // index for start of output line (truncation test)
              fi = ZERO ;                 // intra-field index
      gString gsFld,                      // field data formatting
              gsSub ;                     // substring buffer

      while ( ! done )
      {
         si = ZERO ;                      // reference top of source data
         fld = ZERO ;                     // reset to first field of record
         while ( wptr[si] != NULLCHAR )
         {
            //* Extract text of a field *
            fbIndx = ZERO ;
            while ( (si < wchars) && (wptr[si] != COMMA) && (wptr[si] != NEWLINE) )
               fldBuff[fbIndx++] = wptr[si++] ;
            fldBuff[fbIndx] = NULLCHAR ;     // terminate the string
            gsFld = fldBuff ;                // load data

            //* Replace special comma sequences with comma characters. *
            gsFld.replace( SPEC_COMMA_SEQ, COMMA, ZERO, true, true ) ;
            //gsFld.replace( SPEC_LFEED_SEQ, L"/", ZERO, true, true ) ;

            //* If field contains newline sequence(s) *
            if ( (fi = gsFld.find( SPEC_LFEED_SEQ )) > ZERO )
            {
               for ( short s = sublevel + 1 ; s > ZERO ; --s )
               {
                  if ( (fi = gsFld.find( SPEC_LFEED_SEQ )) > ZERO )
                  {
                     gsSub.loadChars( gsFld.gstr(), fi ) ;  // extract the substring
                     gsFld.shiftChars( -(fi + 2) ) ;        // discard substring and seperator
                     #if DEBUG_CSVV != 0 && DEBUG_VP != 0
                     gsSub.padCols( this->maxFldWidth[fld], L'-' ) ;
                     #endif   // DEBUG_CSVV && DEBUG_VP
                     gsSub.padCols( this->maxFldWidth[fld] + 1 ) ; // pad to edge of field (plus 1)
                  }
                  else  // gsFld contains only the last substring
                  {
                     gsSub = gsFld ;
                     gsFld.clear() ;
                     #if DEBUG_CSVV != 0 && DEBUG_VP != 0
                     gsSub.padCols( this->maxFldWidth[fld], L'-' ) ;
                     #endif   // DEBUG_CSVV && DEBUG_VP
                     gsSub.padCols( this->maxFldWidth[fld] + 1 ) ; // pad to edge of field (plus 1)
                     //* If this field contains no data for the current  *
                     //* substring level, load the placeholder substring.*
                     if ( s > 1 )
                        gsSub = phptr[fld] ;
                     break ;
                  }
               }     // for(;;)
               //* Append to data in output buffer *
               if ( (obytes + gsSub.utfbytes()) < ibBYTES ) // prevent buffer overrun
               {
                  gsSub.copy( &obuff[obytes], gsSub.utfbytes() ) ;
                  obytes += gsSub.utfbytes() - 1 ; // update total output bytes (excl. nullchar)
               }
            }
            else        // no "special" characters in the field
            {
               if ( sublevel > ZERO )     // load the phaceholder string for the field
               { gsFld = phptr[fld] ; }

               //* Pad to full column width (plus one) *
               else
               {
                  #if DEBUG_CSVV != 0 && DEBUG_VP != 0
                  gsFld.padCols( this->maxFldWidth[fld], L'-' ) ;
                  #endif   // DEBUG_CSVV && DEBUG_VP
                  gsFld.padCols( this->maxFldWidth[fld] + 1 ) ;
               }

               //* Append to data in output buffer *
               if ( (obytes + gsFld.utfbytes()) < ibBYTES ) // prevent buffer overrun
               {
                  gsFld.copy( &obuff[obytes], gsFld.utfbytes() ) ;
                  obytes += gsFld.utfbytes() - 1 ; // update total output bytes (excl. nullchar)
               }
            }
   
            if ( wptr[si] == COMMA )         // step over the delimiter
            { ++si ; ++fld ; }               // and reference the next field
            else                             // end-of-record
            {
               obuff[obytes++] = NEWLINE ;   // append a newline character
               obuff[obytes]   = NULLCHAR ;  // null-terminate the data
               ++olines ;                    // increment the line counter
               //* If specified, truncate the data to *
               //* the width of the terminal window.  *
               if ( trunc )
               {
                  gsSub = &obuff[rowBase] ;
                  if ( gsSub.gscols() >= this->termCols )
                  {
                     gsSub.limitCols( this->termCols - 2 ) ; // limit to window width
                     gsSub.append( NEWLINE ) ;           // re-append the newline
                     obytes = rowBase ;                  // target index
                     //* Scan for first non-whitespace character.            *
                     //* If the row now contains only whitespace, discard it.*
                     if ( (gsSub.scan()) < (gsSub.gschars() - 1) )
                     {
                        gsSub.copy( &obuff[obytes], gsSub.utfbytes() ) ;
                        obytes += gsSub.utfbytes() - 1 ;
                     }
                     else
                     {
                        obuff[obytes] = NULLCHAR ;       // terminate output data
                        --olines ;                       // decrement line count
                     }
                  }
               }
               rowBase = obytes ;
               break ;
            }
            //* Parsing found more records than the maximum for this file. *
            //* This means that count of non-embedded commas it too large. *
            //* Replace the records with a warning message.                *
            if ( fld >= this->fldCount )
            {
               //* Retain the first 15 bytes of source data. This should   *
               //* enough to identify the offending record.                *
               obytes = 16 ;
               gsSub = "**MALFORMED RECORD**\n" ;
               gsSub.copy( &obuff[obytes], gsSub.utfbytes() ) ;
               obytes += gsSub.utfbytes() - 1 ;
               olines = 1 ;      // one output line
               done = true ;     // end of outer loop
               break ;           // terminate inner loop
            }
         }     // while()

         //* If all substring levels of output stream *
         //* have been initialized, we're done.       *
         if ( ++sublevel >= ssMax ) { done = true ; }
      }  // while()
   }     // (embeddedNlines)

   //* Else, output as a single line. (This is considerably faster.) *
   else
   {
      //* Vertically align the field columns for display *
      this->alignFieldColumns ( gssrc, gstrg ) ;

      //* If specified, truncate the data to the width of the terminal window. *
      if ( trunc && ((gstrg.gscols()) >= this->termCols) )
      {
         gstrg.limitCols( this->termCols ) ;
         gstrg.append( NEWLINE ) ;
      }
      gstrg.copy( obuff, gsALLOCMIN ) ;
      olines = 1 ;
      obytes = gstrg.utfbytes() ;
   }

   return obytes ;

   #undef DEBUG_VP               // (defined in DisplayData())
}  //* End ddFormatHorizontal() *

//*************************
//*   alignFieldColumns   *
//*************************
//********************************************************************************
//* Called by ddFormatHorizontal() to pad the width of each field so that the    *
//* columns of displayed field data are vertically aligned.                      *
//*                                                                              *
//* a) Each field is padded to one more column than the calculated maximum, so   *
//*    columns will be visually seperated.                                       *
//* b) Embedded comma sequences are converted to commas.                         *
//* c) Embedded newlines should not be seen here, but if they are, they will be  *
//*    converted to a placeholder character, and the field width will be limited *
//*    as necessary.                                                             *
//*                                                                              *
//* Input  : gssrc : unformatted record data                                     *
//*          gstrg : received formatted text for display                         *
//*                  (includes terminating newline)                              *
//*                                                                              *
//* Returns: 'true;  if data successfully formatted                              *
//*          'false' if data truncated to fit 'gstrg'                            *
//********************************************************************************

bool CsvView::alignFieldColumns ( const gString& gssrc, gString& gstrg )
{
   const wchar_t* REPLACEMENT_LFEED = L"|" ;

   const wchar_t *wptr = gssrc.gstr() ;   // pointer to source data
   gString gsFmt ;                        // text formatting
   short wchars = gssrc.gschars(),        // source characters (for safety)
         si = ZERO,                       // source index
         fld = ZERO ;                     // field index

   gstrg.clear() ;                        // clear the output buffer

   while ( wptr[si] != NULLCHAR )
   {
      gsFmt.clear() ;                     // clear the formatting buffer

      //* Extract text of a field *
      while ( (si < wchars) && (wptr[si] != COMMA) && (wptr[si] != NEWLINE) )
         gsFmt.append( "%C", &wptr[si++] ) ;

      //* Replace special comma sequences with comma characters. *
      gsFmt.replace( SPEC_COMMA_SEQ, COMMA, ZERO, true, true ) ;
      //* Replace special newline sequences with placeholder characters.*
      gsFmt.replace( SPEC_LFEED_SEQ, REPLACEMENT_LFEED, ZERO, true, true ) ;

      //* If a field is wider than the calculated maximum, it means *
      //* that unprocessed embedded newlines were encountered.      *
      //* This is unlikely, but field is width limited just in case.*
      gsFmt.limitCols( this->maxFldWidth[fld] ) ;
      gsFmt.padCols( this->maxFldWidth[fld] + 1 ) ; // pad to full column width + 1
      gstrg.append( gsFmt.gstr() ) ;            // append to output

      if ( wptr[si] == COMMA )            // step over the delimiter
      { ++si ; ++fld ; }                  // and reference the next field
      else                                // si references the trailing newline
      { gstrg.append( L'\n' ) ; break ; } // terminate with a newline, and we're done
   }

   return ( (gstrg.gschars() < gsALLOCMIN) ? true : false ) ;

}  //* End alignFieldColumns() *

//*************************
//*    countSubstrings    *
//*************************
//********************************************************************************
//* Non-member method:                                                           *
//* ------------------                                                           *
//* Count the substrings in each field to determine maximum depth.               *
//*                                                                              *
//* Input  : wptr  : pointer to source character array (wide data)               *
//*                  (with embedded newline characters)                          *
//*          wchars: number of characters in source buffer                       *
//*                                                                              *
//* Returns: maximum number of substring in an field of the record               *
//********************************************************************************

static short countSubstrings ( const wchar_t *wptr, short wchars )
{
   gString gsFld ;                        // field buffer
   wchar_t fldBuff[gsALLOCMIN] ;          // intermediate buffer
   short si = ZERO,                       // source index
         fi,                              // field buffer index
         sscnt,                           // substring counter
         ssmax = ZERO ;                   // return value

   while ( wptr[si] != NULLCHAR )
   {
      //* Extract text of a field *
      fi = ZERO ;
      while ( (si < wchars) && (wptr[si] != COMMA) && (wptr[si] != NEWLINE) )
         fldBuff[fi++] = wptr[si++] ;
      ++si ;                           // step over the delimeter
      fldBuff[fi] = NULLCHAR ;         // terminate the string
      gsFld = fldBuff ;                // load data

      sscnt = 1 ;                      // assume at least one substring
      fi = ZERO ;
      while ( (fi = gsFld.after( SPEC_LFEED_SEQ, fi )) > ZERO )
         ++sscnt ;
      if ( sscnt > ssmax ) { ssmax = sscnt ; }
   }
   return ssmax ;

}  //* End countSubstrings() *

//*************************
//*   ddFormatVertical    *
//*************************
//********************************************************************************
//* Called by DispData() to format a single field from a record.                 *
//* The first (leftmost) field of 'fld' is extracted and concatenated with       *
//* the field header.                                                            *
//*                                                                              *
//* Encoded "special" characters within a field (newlines and commas) are        *
//* decoded for display.                                                         *
//*                                                                              *
//* Input  : hdr    : pointer to target field in header record                   *
//*          fld    : pointer to target field in field record                    *
//*          gsOut  : receives formatted output data (terminated by newline '\n')*
//*          trunc  : (optional, 'false' by default)                             *
//*                   if 'true',  truncate output to fit width of terminal window*
//*                   if 'false', do not truncate output data                    *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void CsvView::ddFormatVertical ( const wchar_t *hdr, const wchar_t *fld,  
                                 gString& gsOut, bool trunc )
{
   gString gspad ;                        // padding for intra-field line breaks
   short hsi = ZERO,                      // header source index
         fsi = ZERO ;                     // field source index

   gsOut.clear() ;                        // initialize target buffer

   //* Extract field header and pad it for uniform width. *
   while ( (hdr[hsi] != COMMA) && (hdr[hsi] != NEWLINE) )
      gsOut.append( "%C", &hdr[hsi++] ) ;
   gsOut.padCols( this->maxHdrWidth ) ;
   gsOut.append( L": " ) ;
   gspad.padCols( gsOut.gscols() ) ;      // offset to second column

   //* Extract text for the current field *
   while ( fld[fsi] != NEWLINE )
   {
      //* Potential escape indicator for special characters *
      if ( fld[fsi] == SPECIAL_BS )
      {
         if ( fld[fsi + 1] == SPECIAL_COMMA )      // embedded comma
         {
            gsOut.append( COMMA ) ;
            fsi += 2 ;
         }
         else if ( fld[fsi + 1] == SPECIAL_LFEED ) // embedded newline
         {
            gsOut.append( "\n%S", gspad.gstr() ) ;
            fsi += 2 ;
         }
         else     // backstroke is an ordinary character
            gsOut.append( "%C", &fld[fsi++] ) ;
      }

      //* Field delimeter or end-of-record *
      else if ( (fld[fsi] == COMMA) || (fld[fsi] == NEWLINE) )
         break ;

      else     // ordinary character
         gsOut.append( "%C", &fld[fsi++] ) ;
   }

   //* If specified, limit output width to width of terminal window. *
   if ( trunc && ((gsOut.gschars()) >= this->termCols) )
      gsOut.limitCols( this->termCols ) ;
   gsOut.append( NEWLINE ) ;


}  //* End ddFormatVertical() *

//*************************
//*        SrcScan        *
//*************************
//********************************************************************************
//* Perform preliminary scan on source file specified in the 'orgPath' member.   *
//* Gather basic file statistics.                                                *
//*                                                                              *
//* Re-code the data and store it in a temporary file. The re-coded data will    *
//* be used as the source for all subsequent operations.                         *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: 'true' if a valid CSV formatted file, else 'false'                  *
//*          The following data members will be initialized:                     *
//*             'srcScanned'                                                     *
//*             'srcCsvFmt',                                                     *
//*             'srcEncode'                                                      *
//*             'srcCrlf'                                                        *
//*             'recCount'                                                       *
//*             'fldCount'                                                       *
//*             'srcHeader' (if !hdrLock)                                        *
//*'******************************************************************************
//* Notes:                                                                       *
//* 1) CSV files are not written to any formal standard: beware!                 *
//* 2) A CSV file may or may not contain a header record, and if it does, there  *
//*    is no reliable way to determine whether it is indeed a header record.     *
//*    Theoretically, the MIME type could include an indicator for the presence  *
//*    of a header, but the MIME type is optional and/or unreliable.             *
//*    We take a practical approach to test for the presence of a header record: *
//*    Please see the notes in the parseHeader() method for more information.    *
//*    -- For a sample UTF-8 CSV file generated by GMail (Contacts), the header  *
//*       record is 926 bytes incl. newlines. For a similar GMail CSV file with  *
//*       UTF-16 encoding, the header record is 1731 byte including CR/LF pairs. *
//*    -- The sample UTF-8 CSV file from Microsloth includes the EF BB BF        *
//*       byte-order-mark even though Unicode neither specifies nor recommends   *
//*       this.                                                                  *
//* 3) Apparently, the MIME-type string is never embedded within the file itself,*
//*    as it would be in a metadata record of an HTML or CSS file, but is only   *
//*    found in the MIME-type database, which may, or may not be accurate.       *
//* 4) If the source file is UTF-16 encoded, it cannot be analysed directly.     *
//*    The UTF-16 data must first be converted to UTF-8 encoding. (see below)    *
//* 5) The first line of the file is critical in determining the encoding:       *
//*    presence or absence of a byte-order mark, how the line is terminated,     *
//*    and whether it contains a header record or an ordinary record.            *
//*    It may contain some combination of:                                       *
//*      a) "FF FE" or "FE FF" byte-order mark for UTF-16 encoding.              *
//*      b) a UTF-8 byte-order mark: "\xEF, \xBB, \xBF".                         *
//*      c) a header record.                                                     *
//*      d) a non-header record                                                  *
//*      e) the line should be terminated by a newline, either a simple          *
//*         ASCII '\n, or a UTF-16 pair: "\0\n" or "\n\0".                       *
//*    Note that if the first read returns an error, then the line was not       *
//*    terminated by a newline This means one of two things:                     *
//*      1) the file is not a valid CSV file, or                                 *
//*      2) the file contains only one line.                                     *
//*    Either way, it's not worth our time to decode it.                         *
//* 6) Our implementation converts all source files, both UTF-16 and UTF-8 to a  *
//*    well-ordered UTF-8 encoding, replacing CR/LF pairs with LF only, and      *
//*    replacing any newlines and commas WITHIN a field with ordinary ASCII      *
//*    characters which greatly simplifies the output formatting routines.       *
//*    -- Literal commas within field text are a special case. The field should  *
//*       have been enclosed within double quotation marks to ensure that the    *
//*       embedded commas are not interpreted as field delimeters. The quotation *
//*       marks should be inside and adjacent to the field's comma delimiters.   *
//*          Example: ,"Allison Zhang, TZ Student",                              *
//*       Double quotation mark characters which are not within and adjacent to  *
//*       comma delimiters, will be processed as ordinary text.                  *
//*    -- Some CSV files have newlines embedded WITHIN a record. They must be    *
//*       enclosed within a pair of double-quote secondary delimiters; however,  *
//*       this makes a straightforward getline() input loop impractical.         *
//*       To overcome this foolishness, we need to scan the captured line of data*
//*       for a comma/double-quote (,") sequence, or the UTF-16 varients of same.*
//*       See the readRecord() method for details.                               *
//*    -- If CR/LF pairs ("\r\n") are used to represent line breaks, they are    *
//*       replaced by simple linefeed characters. CarriageReturn/Linefeed        *
//*       sequences are an artifact of the days when teletype machines were used *
//*       for computer I/O. To our knowledge, only Microsloth applications are   *
//*       still using this relic of the 1960s for file formatting.               *
//*                                                                              *
//*                                                                              *
//********************************************************************************

bool CsvView::SrcScan ( void )
{
   #define DEBUG_PF (0)                   // for debugging parseFields()

//   const short ibBYTES = gsALLOCMIN * 3 ; // size of input buffer
//                                         // (note: larger than defined buffer)

   uint8_t *ibuff = new uint8_t[ibBYTES] ;// input buffer
   gString gs ;                           // text analysis
   ofstream ofs ;                         // re-coded source-data file
   short iBytes ;                         // number of bytes read for current line
   bool  done = false ;                   // loop control

   ifstream ifs( this->orgPath, ifstream::in ) ;   // open the file
   if ( (ifs.is_open()) )
   {
      //* Read the first line of the file and analyze whether it  *
      //* is actually in CSV format (see note above). On return,  *
      //* the 'srcCsvFmt' member flag will have been initialized. *
      bool goodread = this->readRecord ( ifs, ibuff, ibBYTES, iBytes, true ) ;
      if ( this->srcCsvFmt && goodread && iBytes > ZERO )
      {
         //* Scan the raw source for a carriage-return character, *
         //* ('\r' 0Dh), i.e. CR/LF pairs used for encoded data;  *
         //* and if found, set the 'srcCrlf' flag.                *
         for ( short i = ZERO ; i < iBytes ; ++i )
         { if ( ibuff[i] == CR ) { this->srcCrlf = true ; break ; } }

         //* Test for UTF-16 encoding and convert to UTF-8 encoding. *
         //* The byte-order-mark, if present, will be removed.       *
         //* The terminating newline (if present) will be deleted.   *
         if ( ! (this->isUTF16 ( ibuff, iBytes )) )
         {  //* UTF-8 is assumed; however, there may be a            *
            //* byte-order-mark, and if so, it will be removed.      *
            //* The terminating newline (if present) will be deleted.*
            this->isUTF8bom ( ibuff, iBytes ) ;
         }

         //* Create a temp file to contain the re-coded data. *
         this->tfm->createTempFile ( gs ) ;
         gs.copy( this->srcPath, MAX_PATH ) ;

         //* Copy the re-coded first record to the temp file. *
         ofs.open( this->srcPath, ofstream::out | ofstream::trunc ) ;
         if ( ofs.is_open() )
         { ofs << ibuff << endl ; }

         this->srcHeader = this->parseHeader ( ibuff, iBytes, this->fldCount ) ;
         ++this->recCount ;

         //* Now we have the field count, so allocate an array *
         //* for greatest-field-width, and initialize it with  *
         //* the field widths of the current record.           *
         this->maxFldWidth = new short[this->fldCount] ;
         for ( short f = ZERO ; f < this->fldCount ; ++f )
            this->maxFldWidth[f] = ZERO ;
         this->parseFields ( ibuff, iBytes ) ;

         //* For verticalized output, remember the target *
         //* width of the leftmost (header) column.       *
         for ( short f = ZERO ; f < this->fldCount ; ++f )
         {
            if ( this->maxFldWidth[f] > this->maxHdrWidth )
               this->maxHdrWidth = this->maxFldWidth[f] ;
         }
      }
      else  { done = true ; }                      // end-of-file

      //* Read the remaining lines of the file. *
      //* There SHOULD BE one record per line.  *
      while ( ! done )
      {
         goodread = this->readRecord ( ifs, ibuff, ibBYTES, iBytes ) ;
         if ( goodread && iBytes > ZERO )
         {
            ++this->recCount ;
            //* Convert UTF-16 data to UTF-8 encoding *
            if ( (this->srcEncode == etUTF16le) || (this->srcEncode == etUTF16be) )
               this->convert2UTF8 ( ibuff, iBytes, false ) ;
            //* Else convert CR/LF pairs to LF characters *
            else
               iBytes = this->deflateNewlines ( ibuff, iBytes ) ;

            //* Convert any field data within double-quotes *
            //* used as field delimiters.                   *
            this->convertDoubleQuotes ( ibuff, iBytes ) ;

            //* Write the formatted data to the temp file. *
            if ( ofs.is_open() )
            { ofs << ibuff ; ofs.flush() ; }

            //* Gather statistics of the individual fields. *
            this->parseFields ( ibuff, iBytes ) ;
         }
         else  { done = true ; }          // end-of-file
      }

      ifs.close() ;                       // close the source file

      if ( ofs.is_open() )                // if temp file is open, close it
         ofs.close() ;

      #if DEBUG_CSVV != 0 && DEBUG_PF != 0
      this->pause () ;
      #endif   // DEBUG_CSVV && DEBUG_PF
   }
   this->srcScanned = true ;              // initial scan complete

   if ( ibuff != NULL ) { delete [] ibuff ; } // release the dynamic allocation

   return this->srcCsvFmt ;

}  //* End SrcScan() *

//*************************
//*      readRecord       *
//*************************
//********************************************************************************
//* Read a single line from the source file.                                     *
//*                                                                              *
//* This operation is more complex than it should be due to potential UTF-16     *
//* encoding. The read should be terminated when the newline is encountered.     *
//* Unfortunately, the newline itself is not returned as part of the stream.     *
//* In addition, the ASCII newline character '\n' (0x0A) may not actually be     *
//* the last character on the line because UTF-16 encoding ends the line with    *
//* a 16-bit value of either 0x000A (big-endian) or 0x0A00 (little-endian).      *
//* This makes UTF-16LE encoding a special case because we need to read the      *
//* trailing 0x00 to complete the line.                                          *
//* See also the notes below about 'embedded literal newlines'.                  *
//* -- If the encoding flags have already been initialized, we can refer to them *
//*    for testing; otherwise, we can examine the byte-order-mark (if any) to    *
//*    determine whether the data are encoded as UTF-16LE.                       *
//* -- The captured byte stream is null terminated for safety, but the appended  *
//*    null terminator is not counted in the byte total.                         *
//*                                                                              *
//*                                                                              *
//* Input  : ifs    : (by reference) access to an open byte stream.              *
//*          ibuff  : pointer to buffer to receive input data                    *
//*          iblen  : size of 'ibuff' in bytes                                   *
//*          cnt    : (by reference) receives the number of bytes read from      *
//*                   the input stream                                           *
//*          testBom; (optional, 'false' by default)                             *
//*                   if 'false', byte order has already been established, so    *
//*                               member flags may be referenced                 *
//*                   if 'true', this is the first read from the file, so        *
//*                              byte order must be determined directly          *
//*                                                                              *
//* Returns: state of the ifstream::good() flag                                  *
//*          Important Note: If caller has set the 'testBom' flag, we not only   *
//*          test the BOM, we also do what we can to ensure that the record      *
//*          (and therefore the file as a whole) is actually a CSV record/file.  *
//*          and initialize the 'srcCsvFmt' member accordingly.                  *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//* 1) The data may be encoded as UTF-8, UTF-8 with BOM, UTF-16LE or UTF-16BE.   *
//* 2) Each record (except the last) must be terminated by a newline '\n' or     *
//*    UTF-16 equivalent.                                                        *
//* 3) Embedded commas are possible within a field, but if so, they must have    *
//*    been enclosed within a pair of double-quote secondary delimiters.         *
//*    Example: ,"Hello, World! ",                                               *
//*    If not properly delimited, the embedded comma will cause the record to    *
//*    be parsed incorrectly. Subsequent records may also be affected.           *
//* 4) Embedded newlines or CR/LF sequences are possible within a field, but     *
//*    if so, they must have been enclosed within a pair of double-quote         *
//*    secondary delimiters. Example:  ,"Hello\nWorld!",                         *
//*    This means that using ifstream getline() does not necessarily read an     *
//*    entire record. This can be handled in multiple ways.                      *
//*    Our approach is to read a "line" from the source, then scan it for        *
//*    double-quote delimiter sequences.                                         *
//*       Open:   (a double-quote character PRECEEDED by a comma)                *
//*       Close:  (a double-quote character FOLLOWED by a comma)                 *
//*       (First and last records will be without the leading or trailing        *
//*        comma, respectively.)                                                 *
//*    - Most records will have no secondary delimeters, so the initial read     *
//*      will efficiently capture the full record.                               *
//*    - If matching pair(s) of secondary delimeters are found, it indicates     *
//*      that there were no literal newline characters embedded between them     *
//*      which could have caused a truncated read of the record.                 *
//*    - If an unmatched opening delimiter is found, it indicates that the record*
//*      was truncated (or that the person who generated the file was a moron).  *
//*      In this case, we continue to read from the input stream until a newline *
//*      character is found which is OUTSIDE any delimiter pair, indicating that *
//*      the complete record has been read.                                      *
//*                                                                              *
//*                                                                              *
//********************************************************************************

bool CsvView::readRecord ( std::ifstream& ifs, uint8_t *ibuff, int iblen, 
                           short& cnt, bool testBom )
{
   #define DEBUG_RR (0)                   // for debugging only
   short dqcnt = ZERO ;                   // number of double-quote delimiters found
   bool readbyte = false,                 // set if read was incomplete
        status ;                          // return value

   ifs.getline ( (char*)ibuff, iblen, NEWLINE ) ;
   status = ifs.good() ;                  // set if newline found
   cnt = ifs.gcount() ;                   // number of raw bytes read (incl. newline)

   #if DEBUG_CSVV != 0 && DEBUG_RR != 0
   short   hlCnt = 4 ;
   uint8_t hlChr[5] = { "\x00\x0D\x00\x0A" } ;
   this->dumpHexRecord ( ibuff, cnt, "rdR:", false, hlCnt, hlChr ) ;
   gString gsdbg( " cnt:%hd\n", &cnt ) ;
   this->ansi->ttyWrite ( gsdbg ) ;
   #endif   // DEBUG_CSVV && DEBUG_RR

   //* If newline was read and included in the count, but was discarded,*
   //* append it to the buffer.                                         *
   //* Note: Except for the last record of the file, if newline was not *
   //* found, then it is PROBABLY not a CSV file.                       *
   if ( status )
      ibuff[cnt - 1] = NEWLINE ;       // append the newline
   else if ( cnt > ZERO )              // newline not found (last record in file)
      ibuff[cnt++] = NEWLINE ;         // append the newline

   //* If data captured, process it *
   if ( cnt > ZERO )
   {
      //* If encoding has not yet been established,   * 
      //* perform a manual test for UTF-16LE encoding.*
      if ( testBom )
      {
         //* If data are little-endian, set the flag to read    *
         //* the trailing MSB of the last character on the line.*
         if ( (ibuff[0] == UTF_LSB) && (ibuff[1] == UTF_MSB) )
            readbyte = true ;

         //* Scan for presence of at least one comma in the record.   *
         //* If BOTH newline terminator AND at least one comma found, *
         //* declare it a valid CSV format.                           *
         if ( status )
         {
            for ( short i = ZERO ; i < cnt ; ++i )
            { if ( ibuff[i] == COMMA ) { this->srcCsvFmt = true ; break ; } }
         }
      }

      //* If UTF-16LE, then the trailing nullchar for the line *
      //* was not read. Read it and append it to the buffer.   *
      else if ( this->srcEncode == etUTF16le )
         readbyte = true ;

      if ( readbyte )      // append the trailing nullchar
      {
         ifs.get( (char&)(ibuff[cnt]) ) ;
         if ( ifs.gcount() == 1 )
            ++cnt ;
      }

      ibuff[cnt] = NULLCHAR ;          // ensure that the string is terminated

      #if DEBUG_CSVV != 0 && DEBUG_RR != 0
      this->dumpHexRecord ( ibuff, (cnt + 1), "---:", false, hlCnt, hlChr ) ;
      gsdbg.compose( " cnt:%hd\n", &cnt ) ;
      this->ansi->ttyWrite ( gsdbg ) ;
      #endif   // DEBUG_CSVV && DEBUG_RR

      //* Scan for secondary delimiters. (see notes above) *
      dqcnt = this->dqDelimeterCount ( ibuff, cnt ) ;

      //* If un-matched delimiter, continue to read until an even *
      //* number of delimiters AND closing newline captured.      *
      //* Note that at the top of the loop, 'cnt' references the  *
      //* null terminator which was appended to the initial read. *
      short chcnt ;                    // input char count
      while ( (dqcnt % 2) != ZERO )
      {
         ifs.getline ( (char*)(&ibuff[cnt]), (iblen - cnt), NEWLINE ) ;
         cnt += chcnt = ifs.gcount() ; // number of raw bytes read (incl. newline)
         if ( (status = (ifs.good())) )// set if newline found
         {
            ibuff[cnt - 1] = NEWLINE ; // add the discarded newline

            //* For little-endian encoding, read the trailing MSB of the character.*
            if ( this->srcEncode == etUTF16le )
            {
               ifs.get( (char&)(ibuff[cnt]) ) ;
               if ( ifs.gcount() == 1 ) { ++cnt ; ++chcnt ; }
            }
         }
         else if ( chcnt > ZERO )      // newline not found (last record in file)
         {
            ibuff[cnt++] = NEWLINE ;   // append the missing newline

            //* For little-endian encoding, read the trailing MSB of the character.*
            if ( this->srcEncode == etUTF16le )
            {
               ifs.get( (char&)(ibuff[cnt]) ) ;
               if ( ifs.gcount() == 1 ) { ++cnt ; ++chcnt ; }
            }
         }
         ibuff[cnt] = NULLCHAR ;       // terminate to be safe
         dqcnt = this->dqDelimeterCount ( ibuff, cnt ) ;// count secondary delimeters

         #if DEBUG_CSVV != 0 && DEBUG_RR != 0
         gsdbg.compose( "dq%hd:", &dqcnt ) ;
         this->dumpHexRecord ( ibuff, (cnt + 1), gsdbg.ustr(), false, hlCnt, hlChr ) ;
         //this->dumpHexRecord ( ibuff, cnt, gsdbg.ustr(), false ) ;
         gsdbg.compose( " cnt:%hd\n", &cnt ) ;
         this->ansi->ttyWrite ( gsdbg ) ;
         #endif   // DEBUG_CSVV && DEBUG_RR

         //* If a newline was found OUTSIDE a delimeter pair, *
         //* the record is complete. (see notes above)        *
         if ( status && ((dqcnt % 2) == ZERO) )
            break ;

         if ( ! status )               // end-of-file reached w/o newline
            break ;
      }  // (delimeter scan)
   }

   #if DEBUG_CSVV != 0 && DEBUG_RR != 0
   if ( dqcnt > ZERO )
   {
      this->ansi->ttyWrite ( L"     Press A Key..." ) ;
      this->ansi->acRead () ;
      this->ansi->ttyWrite ( L'\n' ) ;
   }
   #endif   // DEBUG_CSVV && DEBUG_RR

   return status ;

   #undef DEBUG_RR
}  //* End readRecord() *

//*************************
//*   dqDelimeterCount    *
//*************************
//********************************************************************************
//* Count the number of double-quote delimeters in source data.                  *
//* The source data may be encoded as UTF-8, UTF-16LE or UTF-16BE.               *
//* Newlines may be represented by a single '\n' or by a "\r\n" pair.            *
//* UTF-8:                                                                       *
//*    >"xyz","xyz","xyz"\n<                                                     *
//*    >"xyz","xyz","xyz"\r\n<                                                   *
//* UTF-16LE:                                                                    *
//*    >"0x0y0z0"0,0"0x0y0z0"0,0"0x0y0z0"0\n0<                                   *
//*    >"0x0y0z0"0,0"0x0y0z0"0,0"0x0y0z0"0\r0\n0<                                *
//* UTF-16BE:                                                                    *
//*    >0"0x0y0z0"0,0"0x0y0z0"0,0"0x0y0z0"0\n<                                   *
//*    >0"0x0y0z0"0,0"0x0y0z0"0,0"0x0y0z0"0\r0\n<                                *
//*                                                                              *
//*                                                                              *
//* Input  : buff  : pointer to buffer containing the input-stream data          *
//*          cnt   : number of source data bytes                                 *
//*                                                                              *
//* Returns: number of double-quote sequences identified                         *
//********************************************************************************

short CsvView::dqDelimeterCount ( const uint8_t *buff, short cnt )
{
   const uint8_t CR = '\r' ;              // ASCII carriage return
   short sx = (cnt - 1),                  // index of last source byte
         dqCnt = ZERO ;                   // return value

   for ( short si = ZERO ; si < cnt ; ++si )
   {
      if ( buff[si] == DQ )
      {
         if ( (dqCnt % 2) == ZERO )
         {
            //* Test for opening sequence.                            *
            //* DQ is first character || character before DQ == COMMA.*
            switch ( this->srcEncode )
            {
               case etUTF16le:
                  if ( (si == ZERO) || 
                       ((si >= 2) && buff[si - 2] == COMMA) )
                  { ++dqCnt ; }
                  break ;
               case etUTF16be:
                  if ( (si == 1) || 
                       ((si >= 3) && buff[si - 2] == COMMA) )
                  { ++dqCnt ; }
                  break ;
               case etUTF8bom:
               case etUTF8:
               default:
                  if ( (si == ZERO) || (buff[si - 1] == COMMA) )
                  { ++dqCnt ; }
                  break ;
            }
         }
         else  // ((dqCnt % 2) == 1)
         {
            //* Test for closing sequence.                              *
            //* DQ is last character || next char == COMMA (or NEWLINE).*
            switch ( this->srcEncode )
            {
               case etUTF16le:
                  if ( (si == (sx - 1)) || 
                       ((si <= (sx - 2)) && 
                        ((buff[si + 2] == COMMA) || (buff[si + 2] == NEWLINE) ||
                         (buff[si + 2] == CR))) )
                  { ++dqCnt ; }
                  break ;
               case etUTF16be:
                  if ( (si == sx) || 
                       ((si <= (sx - 2)) && 
                        ((buff[si + 2] == COMMA) || (buff[si + 2] == NEWLINE) ||
                         (buff[si + 2] == CR))) )
                  { ++dqCnt ; }
                  break ;
               case etUTF8bom:
               case etUTF8:
               default:
                  if ( (si == sx) ||
                       ((si < sx) && 
                        ((buff[si + 1] == COMMA) || (buff[si + 1] == NEWLINE) ||
                         (buff[si + 1] == CR))) )
                  { ++dqCnt ; }
                  break ;
            }
         }
      }
   }
   return dqCnt ;

}  //* End dqDelimeterCount() *

//*************************
//*      parseHeader      *
//*************************
//********************************************************************************
//* Parse the source file header (if present).                                   *
//* a) Count the number of fields in the record.                                 *
//* b) Determine whether the record is actually a header record. (see note)      *
//*                                                                              *
//* Determining whether the record (probably) is a header record.                *
//*  -- Because the MIME type is optional, AND because the header specification  *
//*     is optional within the MIME type, it is a useless reference.             *
//*     We take a practical approach to test for the presence of a header record:*
//*  -- The record must contain at least one comma i.e. at least two fields.     *
//*     Otherwise we have to assume that the file is not CSV format.             *
//*  -- If the record DOES NOT contain adjacent commas, i.e. all fields contain  *
//*     data; then it is assumed to be a header record.                          *
//*  -- This is done on the assumption that for most implementations, there      *
//*     will be more fields available than are actually initialized.             *
//*  -- See the "--header" command-line option which user can use to specify     *
//*     if a header record is present, overriding this test.                     *
//*                                                                              *
//*                                                                              *
//* Input  : buff   : raw byte-stream data for the record (UTF-8 format)         *
//*          cnt    : number of data bytes in 'buff'                             *
//*          fldCnt : (by reference) receives the number of fields in the record *
//*                                                                              *
//* Returns: 'true' if header record "identified," else 'false'                  *
//********************************************************************************

bool CsvView::parseHeader ( const uint8_t *buff, short cnt, short& fldCnt )
{
   //* Return value: 
   //* If user specified the presence or absence of a header record *
   //* as a command-line option, then user's setting is returned.   *
   //* Else, the result of our test is returned.                    *
   bool status = bool(this->hdrLock ? this->srcHeader : true) ;

   //* Initialize caller's accumulator. At least one field is assumed,  *
   //* and a comma indicates an ADDITIONAL field because it is specified*
   //* that the last field in the record IS NOT followed by a comma.    *
   fldCnt = 1 ;

   //* Count the number of fields in the record, and *
   //* test for a header record (see notes above)    *
   for ( short i = ZERO ; i < cnt ; ++i )
   {
      if ( buff[i] == COMMA )
      {
         ++fldCnt ;
         if ( ! this->hdrLock && (buff[i + 1] == COMMA) )
            status = false ;
      }
   }
   // Programmer's Note: The above loop theoretically overruns the data 
   // by one; however, the last character of a record is never a comma, 
   // so the overrun will never happen.

   return status ;

}//* End parseHeader() *

//*************************
//*      parseFields      *
//*************************
//********************************************************************************
//* Parse the record and gather statistics.                                      *
//*  a) The width, in columns, of each field.                                    *
//*     If the width of the field is greater than the same field in previous     *
//*     records, save the width in 'maxFldWidth[]' array.                        *
//*     NOTE: value DOES NOT include the width of comma delimiter (if any)       *
//*     NOTE: For fields that contain embedded newlines, the width of each       *
//*           substring within the field is calculated, and the widest substring *
//*           in the field is used for the comparison.                           *
//*  b) if sum of all field widths is greater than 'maxRecWidth', update         *
//*     'maxRecWidth' with the new value.                                        *
//*     NOTE: 'maxRecWidth' DOES include the width of comma delimiters           *
//*                                                                              *
//* Programmer's Note: We may gather more data in future releases.               *
//*                    If so, it will be consolidated into a stats class object. *
//*                                                                              *
//* Input  : buff : UTF-8 encoded record (not including newline)                 *
//*          cnt  : number of data bytes in 'buff'                               *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void CsvView::parseFields ( const uint8_t *buff, short cnt )
{
   uint8_t fldbuff[gsALLOCMIN] ;          // temp buffer for field data
   gString gs ;                           // text analysis
   short recCols = ZERO,                  // column-count accumulator for record
         fldCols = ZERO,                  // column-count for field being scanned
         sindx   = ZERO,                  // field source data index
         tindx   = ZERO,                  // target data index
         findx   = ZERO ;                 // field index

   while ( sindx <= cnt )
   {
      if ( buff[sindx] == SPECIAL_BSTRK )
      {
         //* Replace special comma sequence with ordinary comma. *
         if ( buff[sindx + 1] == SPECIAL_COMMA )
         {
            fldbuff[tindx++] = COMMA ;          // insert a comma
            sindx += 2 ;                        // step over the special char sequence
         }
         //* Isolate the substring. *
         else if ( buff[sindx + 1] == SPECIAL_LFEED )
         {
            fldbuff[tindx] = NULLCHAR ;         // terminate the substring
            tindx = ZERO ;                      // reset target index
            gs = fldbuff ;                      // count substring columns
            recCols += fldCols = gs.gscols() ;  // add it to overall record length

            #if DEBUG_CSVV != 0 && DEBUG_PF != 0
            gString gsdbg( "           substr:%03hd\n", &fldCols ) ;
            this->ansi->ttyWrite ( gsdbg ) ;
            #endif   // DEBUG_CSVV && DEBUG_PF

            if ( fldCols > this->maxFldWidth[findx] ) // if wider than previous value
               this->maxFldWidth[findx] = fldCols ;   // update field max width
            sindx += 2 ;                        // step over the special char sequence
         }
      }
      else if ( (buff[sindx] != COMMA) && (buff[sindx] != NULLCHAR) )
         fldbuff[tindx++] = buff[sindx++] ;
      else
      {
         fldbuff[tindx] = NULLCHAR ;
         tindx = ZERO ;
         gs = fldbuff ;
         recCols += fldCols = gs.gscols() ;
         if ( buff[sindx] == COMMA )
            ++recCols ;
         if ( fldCols > this->maxFldWidth[findx] )
            this->maxFldWidth[findx] = fldCols ;
         ++sindx ;
         ++findx ;
      }
   }
   if ( recCols > this->maxRecWidth )
      this->maxRecWidth = recCols ;

   #if DEBUG_CSVV != 0 && DEBUG_PF != 0    // Note: This is defined in SrcScan()
   gString gsdbg( "mRW:%03hd mFW: ", &this->maxRecWidth ) ;
   for ( short i = ZERO ; i < this->fldCount ; ++i )
      gsdbg.append( "%02hd ", &this->maxFldWidth[i] ) ;
   gsdbg.append( L'\n' ) ;
   this->ansi->ttyWrite ( gsdbg ) ;
   #endif   // DEBUG_CSVV && DEBUG_PF

   #undef DEBUG_PF
}  //* End parseFields() *

//*************************
//*        isUTF16        *
//*************************
//********************************************************************************
//* Test whether file contains UTF-16 encoded data (with or without BOM).        *
//* Called with the data of the first record in the source file.                 *
//* A) If data are UTF-16,                                                       *
//*    a) If the record contains a byte-order-mark (BOM), it will be removed.    *
//*    b) Convert the UTF-16 data to UTF-8 encoding.                             *
//* B) For the UTF-8 data (native encoding or converted encoding):               *
//*    a) Convert each CR/LF pair to a single LF character.                      *
//*    b) If the data contain double-quote secondary delimeters, convert         *
//*       embedded commas and newlines (',' and '\n'), then remove the           *
//*       double-quote characters.                                               *
//*    c) The terminal newline of the record (if present) will be deleted.       *
//*                                                                              *
//* Ideally, a file that is encoded as UTF-16 _should_ begin with a BOM          *
//* (byte-order-mark), either:                                                   *
//*   "FF FE" UTF-16BE indicating a little-endian byte order                     *
//*   "FE FF" UTF-16LE indicating a big-endian byte order                        *
//* Of course, being a legacy Windoze fossil, it is possible that the BOM        *
//* will be missing entirely, in which case the presence of multiple null        *
//* characters (0x00) will give us a clue. Technically, a missing BOM in UTF-16  *
//* data _should_ indicate big-ending encoding, but again, Windoze...            *
//*                                                                              *
//*                                                                              *
//* Input  : buff : buffer containing the source data to be analyzed             *
//*          cnt  : (by reference) number of data bytes in buff                  *
//*                                                                              *
//* Returns: 'true' if UTF-16 encoding                                           *
//*             the source data will be reformatted to UTF-8 encoding            *
//*             on return 'cnt' reflects the number of bytes of converted data   *
//*             the 'srcEncode' member initialized to etUTF16LE or etUTF16BE.    *
//*          'false' if not UTF-16 encoding                                      *
//*             the 'srcEncode' member will not be modified                      *
//*                                                                              *
//********************************************************************************
//* Programmer's Note:                                                           *
//* UTF-32 also exists in the world, but is not supported by CSV formatting,     *
//* and is therefore not supported by this application.                          *
//*   "00 00 FE FF" UTF-32BE                                                     *
//*   "FF FE 00 00" UTF-32LE                                                     *
//*                                                                              *
//********************************************************************************

bool CsvView::isUTF16 ( uint8_t *buff, short& cnt )
{
   #define DEBUG_isUTF16 (0)              // for debugging only

   bool shift = false ;                   // set if BOM is to be shifted out

   this->srcEncode = etUTF8 ;             // assume UTF-8 encoding

   #if DEBUG_CSVV != 0 && DEBUG_isUTF16 != 0
   this->dumpHexRecord ( buff, cnt, "RAW:" ) ;
   #endif   // DEBUG_CSVV && DEBUG_isUTF16

   //* Determine whether source data are encoded as UTF-16. *
   if ( (buff[0] == UTF_MSB) && (buff[1] == UTF_LSB) )
   { this->srcEncode = etUTF16be ; shift = true ; }
   else if ( (buff[0] == UTF_LSB) && (buff[1] == UTF_MSB) )
   { this->srcEncode = etUTF16le ; shift = true ; }
   else
   {
      //* Scan the data (not including the null terminator).   *
      //* If null character found, then UTF-16 data identified.*
      //* If index is even, UTF-16BE.                          *
      //* If index is odd,  UTF-16LE.                          *
      for ( short i = ZERO ; i < (cnt - 1) ; ++i )
      {
         if ( buff[i] == 0x00 )
         { this->srcEncode = (i % 2) ? etUTF16le : etUTF16be ; break ; }
      }
   }

   //* If a byte-order-mark is present, discard it.*
   if ( shift )
   {
      for ( short si = 2, ti = ZERO ; si < cnt ; ++si, ++ti )
         buff[ti] = buff[si] ;
      cnt -= 2 ;           // adjust byte count
   }

   #if DEBUG_CSVV != 0 && DEBUG_isUTF16 != 0
   this->dumpHexRecord ( buff, cnt, "SHF:" ) ;
   #endif   // DEBUG_CSVV && DEBUG_isUTF16

   //* Convert UTF-16 to UTF-8 *
   if ( (this->srcEncode == etUTF16le) || (this->srcEncode == etUTF16be) )
   {
      this->convert2UTF8 ( buff, cnt ) ;  // (terminal newline will be deleted)

      #if DEBUG_CSVV != 0 && DEBUG_isUTF16 != 0
      this->dumpHexRecord ( buff, cnt, "CNV:" ) ;
      #endif   // DEBUG_CSVV && DEBUG_isUTF16
   }

   //* For source data that is already UTF-8 encoded, *
   //* convert CR/LF pairs to single LF characters.   *
   else
      cnt = this->deflateNewlines ( buff, cnt ) ;

   //* Convert data within double-quotes used as field delimiters. *
   this->convertDoubleQuotes ( buff, cnt ) ;

   #if DEBUG_CSVV != 0 && DEBUG_isUTF16 != 0
   this->dumpHexRecord ( buff, cnt, "FIN:" ) ;
   #endif   // DEBUG_CSVV && DEBUG_isUTF16

   return ( (this->srcEncode == etUTF16le || this->srcEncode == etUTF16be) ? true : false ) ;

   #undef DEBUG_isUTF16
}  //* End isUTF16() *

//*************************
//*     convert2UTF8      *
//*************************
//********************************************************************************
//* Convert a Unicode-16 string (big-endian or little-endian) to UTF-8 format.   *
//* a) If a CR/LF pair is found anywhere in the data, the CR/LF sequence         *
//*    will be replaced by a single LF character.                                *
//*    Example: .. .. .. .. 0D 00 0A 00 .. .. .. 00  becomes:                    *
//*             .. .. .. .. 0A .. .. .. .. .. .. 00                              *
//*             .. .. .. .. 00 0D 00 0A .. .. .. 00  becomes:                    *
//*             .. .. .. .. 0A .. .. .. .. .. .. 00                              *
//* b) If the data are terminated by a CR/LF/00 or LF/00 sequence, then the      *
//*    terminating CR/LF or LF will be deleted.  (null terminator retained)      *
//*    Example: .. .. .. .. 0D 00 0A 00 00  becomes:                             *
//*             .. .. .. .. 00                                                   *
//*             .. .. .. .. 00 0D 00 0A 00  becomes:                             *
//*             .. .. .. .. 00                                                   *
//*             .. .. .. .. 0A 00           becomes:                             *
//*             .. .. .. .. 00                                                   *
//*             .. .. .. .. 00 0A           becomes:                             *
//*             .. .. .. .. 00                                                   *
//*    (but see the 'xnl' argument which can be used to retain trailing newline) *
//* c) The converted data will be null-terminated on return, but the nullchar    *
//*    is not included in the byte count.                                        *
//*                                                                              *
//* -- Note that because the wchar_t type is a signed integer, the compiler      *
//*    wants to sign-extend. Don't allow this.                                   *
//*                                                                              *
//* Input  : buff : buffer containing the raw UTF-16 data to be converted        *
//*                 NOTE: Buffer must be at least gsMAXBYTES in length, and      *
//*                       preferably gsMAXBYTES * 2 or more.                     *
//*          cnt  : (by reference) on entry, number of data bytes in 'buff'      *
//*                 on return, contains number of bytes of converted data.       *
//*          xnl  : (optional, 'true' by default) no-new-line                    *
//*                 if 'true',  remove the trailing newline, if found            *
//*                 if 'false', retain the trailing newline                      *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void CsvView::convert2UTF8 ( uint8_t *buff, short& cnt, bool xnl )
{
   const uint32_t usxMASK10 = 0x00003FF ; // 10-bit mask
   const uint32_t usxMSU16  = 0x000D800 ; // MS unit mask value
   const uint32_t usxMSU16e = 0x000DBFF ; // end of MS unit range
   const uint32_t usxLSU16  = 0x000DC00 ; // LS unit mask value
   const uint32_t usxLSU16e = 0x000DFFF ; // end of LS unit range
   const uint32_t usxHIGH16 = 0x000E000 ; // beginning of high-range 16-bit codepoints
   const uint32_t usxMAX16  = 0x000FFFD ; // maximum value for 16-bit codepoint
   const uint32_t usxMIN20  = 0x0010000 ; // minimum value requiring 20-bit conversion
   const wchar_t *CRLF = L"\r\n" ;        // CR/LF pair (UTF-8)
   const wchar_t LF    = L'\n' ;          // LF character (UTF-8)

   gString gsTrg, gsTmp ;                 // temp storage for converted data
   uint32_t msUnit, lsUnit,               // for converting unit pairs
            cx ;                          // undecoded 16-bit character
   short si = ZERO,                       // source index
         rawBytes = cnt,                  // number of bytes remaining to be processed
         indx ;                           // general index

   while ( rawBytes > ZERO )
   {
      if ( this->srcEncode == etUTF16be )
      {
         cx = ((uint32_t(buff[si++]) << 8) & 0x0000FF00) ;
         cx |= (uint32_t(buff[si++]) & 0x000000FF) ;
      }
      else  // (etUTF16le)
      {
         cx = (uint32_t(buff[si++]) & 0x000000FF) ;
         cx |= ((uint32_t(buff[si++]) << 8) & 0x0000FF00) ;
      }
      rawBytes -= 2 ;

      //* If the character is fully represented by 16 bits *
      //* (most characters are)                            *
      if ( ((cx >= ZERO) && (cx < usxMSU16)) || 
           ((cx >= usxHIGH16) && (cx <= usxMAX16)) )
      {
         gsTrg.append( cx ) ;
      }

      //* Character is represented by 32 bits    *
      //* (20 bits actually used), 16 MSBs first.*
      else
      {
         msUnit = cx ;
         lsUnit = ZERO ;
         if ( this->srcEncode == etUTF16be )
         {
            lsUnit = ((uint32_t(buff[si++]) << 8) & 0x0000FF00) ;
            lsUnit |= (uint32_t(buff[si++]) & 0x000000FF) ;
         }
         else     // (little-endian)
         {
            lsUnit = (uint32_t(buff[si++]) & 0x000000FF) ;
            lsUnit |= ((uint32_t(buff[si++]) << 8) & 0x0000FF00) ;
         }
         rawBytes -= 2 ;

         //* Validate the range *
         if ( ((msUnit >= usxMSU16) && (msUnit <= usxMSU16e))
              &&
              ((lsUnit >= usxLSU16) && (lsUnit <= usxLSU16e)) )
         {
            cx = usxMIN20 + ((msUnit & usxMASK10) << 10) ;
            cx |= (lsUnit & usxMASK10) ;
         }
         else                 // invalid UTF-16 codepoint
            cx = L'?' ;
         gsTrg.append( cx ) ; // add the chararacter to output buffer
      }

      //* If interim buffer is almost full, save it to a secondary buffer.*
      if ( gsTrg.gschars() >= (gsALLOCMIN - 4) )
      {
         gsTmp = gsTrg ;            // save processed data
         gsTrg.clear() ;            // reinitialize the interim buffer
      }
   }  // while()

   //* Copy the converted data back to caller's buffer.*
   if ( (gsTmp.gschars()) > 1 )
   {
      //* Replace all CR/LF pairs with newline only.*
      gsTmp.replace( CRLF, LF, ZERO, true, true ) ;

      short trgchars = gsTrg.gschars() ;  // character count (incl. \0)
      indx = (gsTmp.gschars()) - 2 ;      // index last character (before \0)
      if ( xnl && (trgchars > 1) && (gsTmp.gstr()[indx] == LF) )
         gsTmp.limitChars( indx ) ;
      gsTmp.copy( (char*)buff, gsALLOCMIN ) ;
      cnt = gsTmp.utfbytes() - 1 ;     // bytes of processed data from secondary buffer
      if ( trgchars > 1 )
      {
         //* Replace all CR/LF pairs with newline only.*
         gsTrg.replace( CRLF, LF, ZERO, true, true ) ;

         if ( xnl && (gsTrg.gstr()[indx = gsTrg.gschars() - 2] == LF) )
            gsTrg.limitChars( indx ) ;
         indx = gsTmp.gschars() - 1 ;  // nullchar not included
         gsTrg.copy( (char*)(&buff[indx]), gsALLOCMIN ) ;
         cnt += gsTrg.utfbytes() - 1 ; // bytes of processed data returned
      }
   }
   else
   {
      //* Replace all CR/LF pairs with newline only.*
      gsTrg.replace( CRLF, LF, ZERO, true, true ) ;

      if ( xnl && (gsTrg.gstr()[indx = gsTrg.gschars() - 2] == LF) )
         gsTrg.limitChars( indx ) ;
      gsTrg.copy( (char*)buff, gsALLOCMIN ) ;
      cnt = gsTrg.utfbytes() - 1 ;     // bytes of processed data returned
   }

}  //* End convert2UTF8() *

//*************************
//*     convert2UTF16     *
//*************************
//********************************************************************************
//* Convert a UTF-8 string to UTF-16 (big-endian or little-endian).              *
//*                                                                              *
//*                                                                              *
//* Input  : buff : buffer containing the source data to be converted            *
//*                  Source data must be null terminated.                        *
//*                 on return, contains the converted data                       *
//*          cnt  : (by reference) number of source data bytes in buff           *
//*          etype: member of EncType specifies etUTF16le or etUTF16be           *
   //*          crlf : (optional, 'false' by default)                               *
   //*                 if 'true',  convert UTF-8 '\n' to a CR/LF pair               *
   //*                 if 'false,  convert UTF-8 '\n' to LF character only          *
//*                                                                              *
//* Returns: number of bytes of converted data returned                          *
//********************************************************************************
//* Notes:                                                                       *
//* stdlib defines the mbrtoc16() function  #include <cuchar>   (C++11)          *
//* std::size_t mbrtoc16 ( char16_t* pc16,        // receives converted UTF-16   *
//*                        const char* s,         // source multi-byte string    *
//*                        std::size_t n,         // max bytes to scan           *
//*                        std:mbstate_t* ps ) ;  // pointer to state object     *
//*                                                                              *
//* Return values:                                                               *
//*  0 == converted character was the null character                             *
//*  1 ... n == number of source bytes converted                                 *
//* -3 == the second element of a "surrogate pair" has been written              *
//*       (no input bytes processed)                                             *
//* -2 == next byte(s) may be incomplete character (nothing to pc16)             *
//* -1 == encoding error (nothing to pc16) (reference 'errno')                   *
//*                                                                              *
//* After conversion, the data must be written back to the buffer as a byte      *
//* stream using the specified endian-ness.                                      *
//*                                                                              *
//* The matching function is c16rtomb() to convert UTF-16 to a byte stream.      *
//*                                                                              *
//* mbr2c16() is the UTF-16 equivalent of the mbsrtowcs() function used by the   *
//* gString class to convert byte streams to wide (wchar_t) characters.          *
//*                                                                              *
//********************************************************************************

short CsvView::convert2UTF16 ( uint8_t *buff, short cnt, EncType etype )
{
   #define DEBUG_B2W (0)                  // for debugging only
   #if DEBUG_CSVV != 0 && DEBUG_B2W != 0
   gString gsdbg ;
   #endif   // DEBUG_CSVV && DEBUG_B2W

   short status,                          // status returned from library call
         si = ZERO,                       // source index
         owords = ZERO,                   // number of 16-bit units converted
         obytes = ZERO ;                  // number of bytes in UTF-16 byte stream
   if ( cnt < 1 ) { cnt = 1 ; }           // prevent stupidity
   short buf16size = cnt * 2 ;            // size of temporary buffer
   char16_t *buf16 = new char16_t[buf16size] ; // allocate the temp buffer

   //* Convert the UTF-8 data to UTF-16 *
   do
   {
      // Programmer's Note: All UTF-8 characters may be expressed using
      // four(4) or fewer bytes.
      status = mbrtoc16 ( &buf16[owords], (const char*)(&buff[si]), 4, NULL ) ;

      //* If bytes successfully converted to character *
      if ( status >= 1 )
      {
         si += status ;                   // step over converted bytes
         ++owords ;                       // advance output index
      }

      //* If second element of a surrogate pair converted *
      //* (No additional bytes were read from input.)     *
      else if ( status == -3 )
      {
         ++owords ;                       // advance output index
      }

      //* If "incomplete character" identified (unlikely), or  *
      //* encoding error (also unlikely)                       *
      else if ( (status == -2) || (status == -1) )
         break ;     // Error! abort the scan
   }
   while ( status != ZERO ) ;             // nullchar converted

   //* Convert the UTF-16 words to a byte stream. *
   //*   (either little-endian or big-endian)     *
   for ( si = ZERO ; si < owords ; ++si )
   {
      if ( etype == etUTF16be )
      {
         buff[obytes++] = uint8_t((buf16[si] & 0xFF00) >> 8) ;
         buff[obytes++] = uint8_t(buf16[si] & 0x00FF) ;
      }
      else if ( etype == etUTF16le )
      {
         buff[obytes++] = uint8_t(buf16[si] & 0x00FF) ;
         buff[obytes++] = uint8_t((buf16[si] & 0xFF00) >> 8) ;
      }

      #if DEBUG_CSVV != 0 && DEBUG_B2W != 0
      gsdbg.compose( "%02hhX %02hhX - ", &buff[obytes - 2], &buff[obytes - 1] ) ;
      this->ansi->ttyWrite ( gsdbg ) ;
      #endif   // DEBUG_CSVV && DEBUG_B2W
   }
   #if DEBUG_CSVV != 0 && DEBUG_B2W != 0
   gsdbg.compose( "\nconvert2UTF16( owords:%hd obytes:%hd )\n", 
                  &owords, &obytes ) ;
   this->ansi->ttyWrite ( gsdbg ) ;
   #endif   // DEBUG_CSVV && DEBUG_B2W

   if ( buf16 != NULL ) { delete [] buf16 ; }   // release the dynamic allocation
   return obytes ;

   #undef DEBUG_B2W
}  //* End convert2UTF16() *

//*************************
//*    convertNewlines    *
//*************************
//********************************************************************************
//* Convert two-byte "\n" sequences to linefeed characters (or CR/LF pairs).     *
//* This method is used during creation of CSV-formatted files to insert literal *
//* newlines into the text of a field.                                           *
//* Embedded literal newlines are not recommended, but the application needs to  *
//* support them (within double-quote delimeter pairs) so this method is used    *
//* to create test data.                                                         *
//*                                                                              *
//* Note: If the 'crlf' flag is set, then the ASCII newline character which ends *
//*       the source data is also converted to a CR/LF character pair.           *
//*                                                                              *
//* Input  : buff : buffer containing UTF-8 source data to be converted          *
//*                  Source data must be null terminated.                        *
//*                 on return, contains the converted data                       *
//*          cnt  : number of source data bytes in buff                          *
//*          crlf : (optional, 'false' by default)                               *
//*                 if 'true',  convert "\n" to a CR/LF pair                     *
//*                             If 'crlf' flag is set, also convert trailing     *
//*                             trailing newline character to CR/LF pair.        *
//*                 if 'false,  convert "\n" to LF character only                *
//*                             Trailing newline character is not modified.      *
//*                                                                              *
//* Returns: number of bytes of converted data returned                          *
//********************************************************************************

short CsvView::convertNewlines ( uint8_t *buff, short cnt, bool crlf )
{
   #define DEBUG_CNL (0)                  // for debugging only
   const uint8_t CR = '\r' ;              // ASCII carriage return
   const uint8_t LF = '\n' ;              // ASCII linefeed
   short si = ZERO,                       // source index
         ti,                              // target index
         ai ;                             // anchor index

   #if DEBUG_CSVV != 0 && DEBUG_CNL != 0
   bool report = false ;
   gString gsdbg( "cNL:" ) ;
   for ( short i = ZERO ; i <= cnt ; ++i )
   {
      gsdbg.append( " %02hhX", &buff[i] ) ;
      if ( buff[i] == SPECIAL_BS ) { report = true ; }
   }
   gsdbg.append( NEWLINE ) ;
   if ( report )
      this->ansi->ttyWrite ( gsdbg ) ;
   #endif   // DEBUG_CSVV && DEBUG_CNL

   while ( si < cnt )
   {
      if ( (buff[si] == SPECIAL_BS) && (buff[si + 1] == SPECIAL_LFEED) )
      {
         ti = si ;                     // target for data shift (overwrite backstroke)
         si += 2 ;                     // source for data shift (beyond "\\n")

         if ( crlf )
            buff[ti++] = CR ;
         buff[ti++] = LF ;
         ai = ti ;
         if ( ti < si )                // if trailing data to be shifted
         {
            while ( si < cnt )         // shift the data
               buff[ti++] = buff[si++] ;
            buff[ti] = NULLCHAR ;
         }
         si = ai ;                     // restore source index
         if ( ! crlf )                 // if converted to LF only, decrement byte count
            --cnt ;
      }
      else if ( crlf && (buff[si] == LF) )
      {
         ti = si ;
         buff[ti++] = CR ;
         buff[ti++] = LF ;
         buff[ti++] = NULLCHAR ;
         ++cnt ;
         break ;                       // terminate the loop
      }
      else
         ++si ;
   }  // while()

   #if DEBUG_CSVV != 0 && DEBUG_CNL != 0
   if ( report )
   {
      gsdbg.compose( "cNL:" ) ;
      for ( short i = ZERO ; i <= cnt ; ++i )
         gsdbg.append( " %02hhX", &buff[i] ) ;
      gsdbg.append( NEWLINE ) ;
      this->ansi->ttyWrite ( gsdbg ) ;
   }
   #endif   // DEBUG_CSVV && DEBUG_CNL

   return cnt ;

   #undef DEBUG_CNL
}  //* End convertNewlines() *

//*************************
//*    deflateNewlines    *
//*************************
//********************************************************************************
//* If the UTF-8 source data contain CR/LF pairs, convert each pair to a single  *
//* newline character:  "\r\n" -> "\n"                                           *
//*                                                                              *
//* Input  : buff : buffer containing UTF-8 source data to be converted          *
//*                  Source data must be null terminated.                        *
//*                 on return, contains the converted data                       *
//*          cnt  : number of source data bytes in buff                          *
//*                                                                              *
//* Returns: number of bytes of converted data returned                          *
//********************************************************************************

short CsvView::deflateNewlines ( uint8_t *buff, short cnt )
{
   #define DEBUG_DNL (0)                  // for debugging only
   const uint8_t CR = '\r' ;              // ASCII carriage return
   const uint8_t LF = '\n' ;              // ASCII linefeed
   short si = ZERO,                       // source index
         ti,                              // target index
         ai ;                             // anchor index

   #if DEBUG_CSVV != 0 && DEBUG_DNL != 0
   bool report = false ;
   gString gsdbg( "dNL:" ) ;
   for ( short i = ZERO ; i <= cnt ; ++i )
   {
      gsdbg.append( " %02hhX", &buff[i] ) ;
      if ( buff[i] == CR ) { report = true ; }
   }
   gsdbg.append( NEWLINE ) ;
   if ( report )
      this->ansi->ttyWrite ( gsdbg ) ;
   #endif   // DEBUG_CSVV && DEBUG_DNL

   while ( si < cnt )
   {
      if ( (buff[si] == CR) && (buff[si + 1] == LF) )
      {
         ti = si++ ;          // target is the CR character and source is LF character
         ai = si ;            // source index for next iteration of the loop
         while ( si <= cnt )  // shift the data (incl. null terminator)
            buff[ti++] = buff[si++] ;
         si = ai ;            // restore source index
         --cnt ;              // decrement the byte count
      }
      else
      { ++si ; ++ti ; }
   }

   #if DEBUG_CSVV != 0 && DEBUG_DNL != 0
   if ( report )
   {
      gsdbg.compose( "    " ) ;
      for ( short i = ZERO ; i <= cnt ; ++i )
         gsdbg.append( " %02hhX", &buff[i] ) ;
      gsdbg.append( NEWLINE ) ;
      this->ansi->ttyWrite ( gsdbg ) ;
   }
   #endif   // DEBUG_CSVV && DEBUG_DNL

   return cnt ;

   #undef DEBUG_DNL
}  //* End deflateNewlines() *

//*************************
//*       isUTF8bom       *
//*************************
//********************************************************************************
//* Test whether file contains a UTF-8 BOM, and if so, remove it.                *
//* This will reduce 'cnt' by three(3) bytes.                                    *
//*                                                                              *
//* The terminating newline '\n' or "\r\n" pair (if present) will also be        *
//* removed and  'cnt' will be reduced accordingly.                              *
//*                                                                              *
//* The returned data are null terminated.                                       *
//*                                                                              *
//* Input  : buff : buffer containing the source data to be analyzed             *
//*          cnt  : (by reference) number of data bytes in buff                  *
//*                 On return, this will be the number of bytes of adjusted data *
//*                                                                              *
//* Returns: 'true' if UTF-8 BOM removed 'cnt' will be reduced by three(3) bytes *
//*          'false' if UTF-8 BOM not found                                      *
//********************************************************************************

bool CsvView::isUTF8bom ( uint8_t *buff, short& cnt )
{
   #define DEBUG_isUTF8b (0)              // for debugging only

   short ti = ZERO ;                      // target index
   bool xbom = false ;                    // return value

   #if DEBUG_CSVV != 0 && DEBUG_isUTF8b != 0
   gString gsdbg( "BOM: " ) ;
   for ( short i = ZERO ; i < cnt ; ++i )
   {
      gsdbg.append( "%02hhX ", &buff[i] ) ;
      if ( buff[i] == '\0' )
         break ;
   }
   gsdbg.append( "\n" ) ;
   this->ansi->ttyWrite ( gsdbg ) ;
   #endif   // DEBUG_CSVV && DEBUG_isUTF8b

   if ( (buff[0] == UTF_BOM[0]) && (buff[1] == UTF_BOM[1]) && (buff[2] == UTF_BOM[2]) )
   {
      for ( short si = 3 ; si < cnt ; ++si, ++ti )
      { buff[ti] = buff[si] ; }
      this->srcEncode = etUTF8bom ;
      xbom = true ;
      cnt -= 3 ;
   }
   else
      ti = cnt ;

   if ( buff[ti - 1] == NEWLINE )         // delete trailing newline character
      --cnt ;
   buff[cnt] = NULLCHAR ;

   //* Convert any double-quotes used as field delimiters. *
   this->convertDoubleQuotes ( buff, cnt ) ;

   #if DEBUG_CSVV != 0 && DEBUG_isUTF8b != 0
   gsdbg.compose( "CNV: " ) ;
   for ( short i = ZERO ; i < cnt ; ++i )
   {
      gsdbg.append( "%02hhX ", &buff[i] ) ;
      if ( buff[i] == '\0' )
         break ;
   }
   gsdbg.append( "\"%s\"\n", buff ) ;
   this->ansi->ttyWrite ( gsdbg ) ;
   #endif   // DEBUG_CSVV && DEBUG_isUTF8b

   return xbom ;

   #undef DEBUG_isUTF8b
}  //* End isUTF8bom() *

//*************************
//*  convertDoubleQuotes  *
//*************************
//********************************************************************************
//* If double quotation marks used as field delimiters, remove them.             *
//* If there are commas and/or newline characters between these secondary        *
//* delimeters, replace them with temporary sequences indicating their positions.*
//* see insertSpecialChar() method for details.                                  *
//* These substitutions will prevent inaccurate parsing of fields.               *
//*                                                                              *
//* Be aware that the double-quote delimited field may be any field in the       *
//* record, so it is necessary to test for an opening double-quote at offset     *
//* zero and a closing double-quote followed by the null terminator.             *
//*                                                                              *
//* Input  : buff : buffer containing the UTF-8 source data to be analyzed       *
//*          cnt  : (by reference) number of data bytes in buff                  *
//*                                                                              *
//* Returns: 'true'  if double-quote delimiter pair(s) removed,                  *
//*                  'cnt' will be reduced by 2 bytes for each double-quote pair *
//*                        removed.                                              *
//*                  'cnt' will also be adjusted if special characters are       *
//*                        replaced by temporary sequences.                      *
//*          'false' if no modifications made                                    *
//********************************************************************************
//* Programmer's Note:                                                           *
//* -- If there are unmatched double-quote sequences, i.e. a malformed record,   *
//*    
//* -- Literal double-quote characters within fields, which ARE NOT enclosed     *
//*    within secondary delimeters SHOULD NOT cause a problem, but keep an eye   *
//*    on this.                                                                  *
//*    -- Experimental code which is currently disabled will replace embedded    *
//*       _literal_ double-quotation characters with single-quotation characters.*
//********************************************************************************

bool CsvView::convertDoubleQuotes ( uint8_t *buff, short& cnt )
{
   #define DEBUG_CDQ (0)                  // for debugging only

   short qi1 = -1, qi2 = -1,              // byte indices
         literalComma    = ZERO,
         literalLfeed = ZERO ;
   bool status = false ;                  // return value

   #if DEBUG_CSVV != 0 && DEBUG_CDQ != 0
   const uint8_t hl[2] = { SPECIAL_BS, '\0' } ;
   gString gsdbg ;                        // text formatting
   bool report = false ;
   #endif   // DEBUG_CSVV && DEBUG_CDQ

   for ( short si = ZERO; si < cnt ; ++si ) // scan for double-quote characters
   {
      //* If (potentially) within a set of double-quote delimiters,  *
      //* and a comma or linefeed is encountered, remember its index.*
      if ( qi1 >= ZERO )
      {
         if ( buff[si] == COMMA )
            literalComma = si ;
         else if ( buff[si] == NEWLINE )
            literalLfeed = si ;
      }

      if ( buff[si] == DQ )
      {
         if ( qi1 < ZERO )                // if initial DQ
         {
            if ( (si == ZERO) || (buff[si - 1] == COMMA) )
               qi1 = si ;                 // index the opening DQ
            //else { buff[si] = '\'' ; }

            #if DEBUG_CSVV != 0 && DEBUG_CDQ != 0
            report = true ;
            this->dumpHexRecord ( buff, (cnt + 1), "qi1:", true, 1, hl ) ;
            #endif   // DEBUG_CSVV && DEBUG_CDQ
         }
         else if ( (buff[si + 1] == COMMA) ||      // end-of-field 
                   (buff[si + 1] == NEWLINE) ||    // end-of-record
                   (buff[si + 1] == NULLCHAR) )    // end-of-record
         {
            qi2 = si ;                    // index the closing DQ

            //* If double-quote pair found AND if literal comma,   *
            //* or literal linefeed, then adjust contents of field.*
            //* Note that the field may contain multiple literal   *
            //* commas or linefeeds, so we scan the field again.   *
            if ( (literalComma > ZERO) || (literalLfeed > ZERO) )
            {
               //* Insert the special character *
               cnt = insertSpecialChar ( buff, cnt, 
                          (literalComma > ZERO ? literalComma : literalLfeed) ) ;
               ++qi2 ;                    // closing delimeter has moved forward

               #if DEBUG_CSVV != 0 && DEBUG_CDQ != 0
               if ( report )
               { this->dumpHexRecord ( buff, (cnt + 1), "    ", true, 1, hl ) ; }
               #endif   // DEBUG_CSVV && DEBUG_CDQ
               for ( short i = qi1 + 1 ; i < qi2 ; ++i )
               {
                  literalComma = literalLfeed = ZERO ; // reset the target indices
                  if ( buff[i] == COMMA )
                     literalComma = i ;
                  else if ( buff[i] == NEWLINE )
                     literalLfeed = i ;
                  if ( literalComma > ZERO || literalLfeed > ZERO )
                  {
                     cnt = insertSpecialChar ( buff, cnt, 
                           (literalComma > ZERO ? literalComma : literalLfeed) ) ;
                     ++qi2 ;              // closing delimeter has moved forward
                  }
               }
            }
            //* Delete the double-quote pair *
            for ( short ti = qi1, sx = qi1 + 1 ; sx < cnt ; )
            {
               if ( sx == qi2 )
                  ++sx ;
               buff[ti++] = buff[sx++] ;
            }
            cnt -= 2 ;                    // reduce total byte count
            buff[cnt] = NULLCHAR ;        // null terminate the string
            status = true ;               // source data modified

            qi1 = qi2 = -1 ;              // sequence complete
            si -= 2 ;                     // adjust source index
            literalComma = literalLfeed = ZERO ; // reset the target indices

            #if DEBUG_CSVV != 0 && DEBUG_CDQ != 0
            if ( report )
            {
               gsdbg = L"qi2:" ;
               //gsdbg.padCols( gsdbg.gscols() + dqsub ) ;
               this->dumpHexRecord ( buff, (cnt + 1), gsdbg.ustr(), true, 1, hl ) ;
            }
            #endif   // DEBUG_CSVV && DEBUG_CDQ
         }
         //else { buff[si] = '\'' ; }
      }
   }

   #if DEBUG_CSVV != 0 && DEBUG_CDQ != 0
   if ( report )
   {
      this->ansi->ttyWrite ( L"Press A Key..." ) ;
      this->ansi->acRead () ;
      this->ansi->ttyWrite ( L'\n' ) ;
   }
   #endif   // DEBUG_CSVV && DEBUG_CDQ

   return status ;

   #undef DEBUG_CDQ
}  //* End convertDoubleQuotes() *

//*************************
//*   insertSpecialChar   *
//*************************
//********************************************************************************
//* Non-member method:                                                           *
//* Called by convertDoubleQuotes() to replace one of the "special" characters   *
//* with a special-character sequence.                                           *
//*                                                                              *
//* Special Characters:                                                          *
//*  Newline (linefeed '\n'):                                                    *
//*  A newline embedded within secondary delimeters is replaced by the           *
//*  two-character sequence: "\n" ('\' + 'n')                                    *
//*                                                                              *
//*  Comma (','):                                                                *
//*  A comma embedded within secondary delimeters is replaced by the             *
//*  two-character sequence: "\c" ('\' + 'c')                                    *
//*                                                                              *
//* Input  : buff : buffer containing the UTF-8 source data to be analyzed       *
//*                 data are terminated by the characters: \n \0                 *
//*          cnt  : (by reference) number of data bytes in buff                  *
//*                                                                              *
//* Returns: number of bytes in the adjusted byte stream                         *
//********************************************************************************

static short insertSpecialChar ( uint8_t *buff, short cnt, short ip )
{
   short si, ti ;                         // source and target indices
   uint8_t specChar = buff[ip] ;          // character to be replaced

   buff[ip++] = SPECIAL_BS ;              // insert the designator character

   //* Shift the trailing data *
   si = cnt + 1 ;                         // reference the null terminator
   ti = si + 1 ;                          // refrence the newline
   while ( ti > ip )
      buff[ti--] = buff[si--] ;
   buff[ip] = (specChar == COMMA) ? SPECIAL_COMMA : SPECIAL_LFEED ;

   return ( ++cnt ) ;

}  //* End insertSpecialChar *

//*************************
//*  outputThresholdTest  *
//*************************
//********************************************************************************
//* Test size of source data for potential 'less' buffer overflow.               *
//*                                                                              *
//* If the source file is beyond the threshold size for a "safe" pipe to the     *
//* 'less' utility, write the formatted data to a temporary file. This bypasses  *
//* the piped output to 'less'. (see note in DisplayData())                      *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: 'true'  if file size >= threshold, else 'false'                     *
//********************************************************************************

bool CsvView::outputThresholdTest ( void )
{
   const int less_dflt_alloc = 1024 * 64 ;   // 64KibiBytes
   tnFName tnf ;                             // receives file stats
   gString gs( this->srcPath ) ;             // text formatting
   bool status = false ;                     // return value

   this->GetFileStats ( tnf, gs ) ;
   if ( tnf.fBytes >= less_dflt_alloc )
      status = true ;
   return status ;

}  //* End outputThresholdTest() *

//*************************
//*      save2Target      *
//*************************
//********************************************************************************
//* Save the data formatted for display to a file in the CWD.                    *
//* -- If the target exists, ask user for permission to overwrite.               *
//* -- Copy the temporary file to the current-working-directory (PWD), renaming  *
//*    the file.                                                                 *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void CsvView::save2Target ( void )
{
   gString gsOut ;               // text formatting
   wchar_t wch ;                 // user input
   short status = -1 ;           // status of copy operation
   bool  save = true ;           // permission to write

   //* If target exists, warn of overwrite. *
   if ( this->trgExists )
   {
      this->ansi->ttyWrite ( L"Target Exists! OK to overwrite? (y/n): " ) ;
      wch = this->ansi->acRead () ;
      if ( (wch != L'y') && (wch != L'Y') )
         save = false ;
      this->ansi->acSetCursor ( aesCUR_ABSCOL, 1 ) ;
      this->ansi->acEraseArea ( aesERASE_LINE ) ;
   }

   if ( save )
      status = this->CopyFile ( this->cpyPath, this->trgPath ) ;
   if ( status == ZERO )
   {
      gString gs( this->trgPath ) ;
      short fi = gs.findlast( L'/' )  + 1 ;
      gsOut.compose( "Output saved to '%S' in current directory.\n\n",
                     &gs.gstr()[fi] ) ;
   }
   else
      gsOut = "Error saving data to target!\n\n" ;
   this->ansi->acSetCursor ( aesCUR_PREVROW ) ;
   this->ansi->ttyWrite ( gsOut ) ;

}  //* End save2Target() *

//*************************
//*     ComposeTitle      *
//*************************
//********************************************************************************
//* Create a text string including the application title, application version    *
//* number and copywrite years.                                                  *
//*                                                                              *
//* Input  : gs     : (by reference) receives the formatting data                *
//*          withNL : (optional, 'false' by default)                             *
//*                   if 'true',  bracket the string with newline characters     *
//*                   if 'false', create the string using only the template      *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void CsvView::ComposeTitle ( gString& gs, bool withNL )
{
   static const wchar_t* const Copyright[] = 
   {
      { L"Copyright" },        // English
      { L"版权" },               // Chinese
   } ;

   gs.compose( titleTemplate, appName1, AppVersion, Copyright[this->lang], crYears ) ;
   if ( withNL )
   { gs.insert( NEWLINE ) ; gs.append( NEWLINE ) ;}

}  //* End ComposeTitle() *

//*************************
//*  GetCommandLineArgs   *
//*************************
//********************************************************************************
//* Capture user's command-line arguments.                                       *
//* Valid Arguments: see DisplayHelp() method.                                   *
//*                                                                              *
//* Input  : commArgs class object (by reference)                                *
//*                                                                              *
//* Returns: 'false' if normal startup                                           *
//*          'true'  if request for help or invalid command-line argument        *
//********************************************************************************
//* Programmer's Note: Even though it is not documented, we accept both          *
//* lowercase and uppercase option and sub-option characters where possible.     *
//* This allows us to add new, case-sensitive options at a later date.           *
//********************************************************************************

void CsvView::GetCommandLineArgs ( commArgs& ca )
{
   #define DEBUG_GCLA (0)        // for debugging only

   //* If user provided command-line arguments, parse them. *
   //* Note that first argument is application name.        *
   if ( ca.argCount > 1 )
   {
      gString gs ;               // text formatting
      short j = ZERO ;           // for multiple arguments in same token
      char  argLetter ;          // short-form arguments
      bool  multiarg = false,    // 'true' if concatenated arguments
            syntax = false ;     // 'true' if syntax error(s) on command line

      //* Extract the current-working-directory (PWD) from the environment.*
      for ( short i = ZERO ; ca.envList[i] != NULL ; ++i )
      {
         if ( *ca.envList[i] == 'P' )
         {
            gs = ca.envList[i] ;
            if ( (gs.find( L"PWD" )) == ZERO )
            {
               gs.shiftChars( -4 ) ;
               gs.copy( this->pwdPath, MAX_PATH ) ;
               break ;
            }
         }
      }

      #if DEBUG_CSVV != 0 && DEBUG_GCLA != 0
      gString gsdbg ;
      system ( "clear" ) ;
      if ( *this->tmpPath != NULLCHAR )
      {
         gsdbg.compose( "tmpPath :%s", this->tmpPath ) ;
         wcout << gsdbg.gstr() << endl ;
      }
      if ( *this->pwdPath != NULLCHAR )
      {
         gsdbg.compose( "pwdPath:%s", this->pwdPath ) ;
         wcout << gsdbg.gstr() << endl ;
      }
      #endif   // DEBUG_CSVV && DEBUG_GCLA

      for ( short i = 1 ; (i < ca.argCount) || (multiarg != false) ; i++ )
      {
         #if DEBUG_CSVV != 0 && DEBUG_GCLA != 0
         gsdbg.compose( "%hd) '%s'", &i, ca.argList[i] ) ;
         wcout << gsdbg.gstr() << endl ;
         #endif   // DEBUG_CSVV && DEBUG_GCLA

         if ( multiarg != false )   // if additional switches in same argument
            --i ;
         else
            j = ZERO ;

         //* If a command-line switch OR continuing previous switch *
         if ( ca.argList[i][j] == DASH || multiarg != false )
         {
            multiarg = false ;
            ++j ;

            if ( ca.argList[i][j] == DASH ) // (double dash)
            {  //* Long-form command switches *
               gs = ca.argList[i] ;

               //* Display application version number and copyright  *
               //* information, then exit.                           *
               //* Note: Full option: "--version", but only six(6)   *
               //*       characters are required.                    *
               if ( (gs.compare( L"--vers", true, 6 )) == ZERO )
               {  // Programmer's Note: Request for version number overrides
                  // all other options and error conditions. Among other things,
                  // this is because the documentation recommends that this 
                  // command be used to test the application build.
                  ca.userOption = clVersion ;
                  break ;     // ignore any remaining arguments
               }

               //* Invoke command-line help *
               else if ( (gs.compare( L"--help", true, 6 )) == ZERO )
               {  // Programmer's Note: Request for help overrides all options 
                  // except '--version'. Note however that parsing of remaining 
                  // arguments continues after receipt of a help request.
                  if ( (gs.find( L"less" )) > ZERO )
                     ca.userOption = clHelpless ;
                  else
                     ca.userOption = clHelp ;
                  continue ;   // finished with this option
               }

               //* If a request for source file stats *
               else if ( (gs.compare( L"--stat", true, 6 )) == ZERO )
               {
                  ca.userOption = clSrcStats ;
                  ca.staFlag = true ;
                  continue ;   // finished with this option
               }

               //* If a request for terminal dimensions *
               else if ( (gs.compare( L"--dime", true, 6 )) == ZERO )
               {
                  ca.userOption = clTermSize ;
                  ca.dimFlag = true ;
                  continue ;   // finished with this option
               }

               //* If a request for terminal configuration *
               else if ( (gs.compare( L"--term", true, 6 )) == ZERO )
               {
                  ca.userOption = clTermInfo ;
                  ca.cfgFlag = true ;
                  continue ;   // finished with this option
               }

               //* If a request for a pause before primary output *
               else if ( (gs.compare( L"--paus", true, 6 )) == ZERO )
               {
                  ca.pakFlag = true ;
                  continue ;   // finished with this option
               }

               //**********************************
               //* Option that requires arguments *
               //**********************************
               else
               {
                  //* Capture the option switch and all of its arguments.      *
                  //* If option and arguments are not separated by a '=',      *
                  //* append the next token which SHOULD contain all arguments *
                  //* because user SHOULD HAVE enclosed all elements of the    *
                  //* argument within single or double quotes.                 *
                  //* Note, however, that users are simple creatures, so if the*
                  //*       next token is not a valid argument for the argument*
                  //*       parsing of the sequence will fail.                 *
                  if ( ((gs.find( L'=' )) < ZERO) && ((i + 1) < ca.argCount) )
                        gs.append( "=%s", ca.argList[++i] ) ;
                  // Programmer's Note: If user includes an option that requires 
                  // an argument, but does not include an argument, the default 
                  // will not be used, and a syntax error will be reported.
                  short eq ;
                  if ( (eq = gs.after( L'=' )) > ZERO )
                  {
                     //* Specify the file to be displayed.*
                     if ( (gs.compare( L"--file", true, 6 )) == ZERO )
                     {
                        gs.shiftChars( -eq ) ;     // discard the option name
                        ca.userOption = this->gclaFilespec ( gs ) ;
                     }

                     //* Specify the display format for output.*
                     else if ( (gs.compare( L"--disp", true, 6 )) == ZERO )
                     {
                        gs.shiftChars( -eq ) ;     // discard the option name
                        argLetter = tolower( *gs.gstr() ) ;
                        switch ( argLetter )
                        {
                           //* Vertical Format *
                           case 'v':   this->dispfmt = dfVert ; break ;
                           //* Quick output to 'stdout' (horizontal (truncated) format)
                           case 'q':   this->dispfmt = dfQuick ; break ;
                           //* Horizontal Format (default) * 
                           case 'h':
                           default:
                              this->dispfmt = dfHoriz ;
                              break ;
                        }
                     }

                     //* Specify name of target file for re-encoded data. *
                     else if ( (gs.compare( L"--save", true, 6 )) == ZERO )
                     {
                        const char* DfltSaveasFilename = "csvrecords.txt" ;
                        char fname[MAX_FNAME] ;
                        tnFName tnf ;
                        short indx ;
                        gs.shiftChars( -eq ) ;     // discard the option name
                        //* Target must be in PWD, so isolate filename. *
                        if ( (indx = gs.findlast( L'/' )) >= ZERO )
                           gs.shiftChars ( -(indx + 1) ) ;
                        //* If no filename was provided, use the default filename.*
                        if ( gs.gschars() == 1 )
                           gs = DfltSaveasFilename ;

                        //* Create the full filespec *
                        gs.copy( fname, MAX_FNAME ) ;
                        gs.compose( "%s/%s",  this->pwdPath, fname ) ;

                        //* Determine whether target already exists. *
                        //* If it does, save the full filespec and   *
                        //* set the flag to warn of overwrite.       *
                        this->GetFileStats ( tnf, gs ) ;
                        if ( *tnf.fSpec != NULLCHAR && tnf.fType != fmUNKNOWN_TYPE )
                        {
                           gs = tnf.fSpec ;        // get full filespec
                           this->trgExists = true ;
                        }
                        //* Save the constructed/normalized filespec *
                        gs.copy( this->trgPath, MAX_PATH ) ;
                     }
                     
                     //* Specify the user-interface language. *
                     else if ( (gs.compare( L"--lang", true, 6 )) == ZERO )
                     {
                        // Note: This is a case insensitive test.
                        gs.shiftChars( -eq ) ;     // discard the option name
                        if ( (gs.find( L"zh" )) == ZERO )
                           this->lang = uilZH ;    // Zhongwen
                        else if ( (gs.find( L"en" )) == ZERO )
                           this->lang = uilEN ;    // English
                        else
                           this->lang = uilENV ;   // from environment (locale)
                     }

                     //* Specify whether the source file contains a header record.*
                     else if ( (gs.compare( L"--head", true, 6 )) == ZERO )
                     {
                        gs.shiftChars( -eq ) ;     // discard the option name
                        argLetter = tolower( *gs.gstr() ) ;
                        this->srcHeader = argLetter == 'y' ? true : false ;
                        this->hdrLock = true ;
                     }

                     //* Specify foreground (text) color attribute, OR *
                     //* specify background color attribute .          *
                     else if ( ((gs.compare( L"--fgnd", true, 6 )) == ZERO) ||
                               ((gs.compare( L"--bgnd", true, 6 )) == ZERO) )
                     {
                        bool fg   = gs.gstr()[2] == L'f' ? true : false,
                             bold = ((gs.find( L'i' )) > ZERO) ? true : false ;
                        aeSeq *trg = fg ? &ca.tConfig.foreground : &ca.tConfig.background ;
                        gs.shiftChars( -eq ) ;     // discard the option name
                        argLetter = tolower( *gs.gstr() ) ;
                        switch ( argLetter )
                        {
                           case L'b':
                              argLetter = tolower( gs.gstr()[2] ) ;
                              switch ( argLetter )
                              {
                                 case L'a':  // "black"
                                    if ( bold )
                                       *trg = fg ? aesFGb_BLACK : aesBGb_BLACK ;
                                    else
                                       *trg = fg ? aesFG_BLACK : aesBG_BLACK ;
                                    break ;
                                 case L'o':  // "brown"
                                    if ( bold )
                                       *trg = fg ? aesFGb_BROWN : aesBGb_BROWN ;
                                    else
                                       *trg = fg ? aesFG_BROWN : aesBG_BROWN ;
                                    break ;
                                 case L'u':  // "blue"
                                    if ( bold )
                                       *trg = fg ? aesFGb_BLUE : aesBGb_BLUE ;
                                    else
                                       *trg = fg ? aesFG_BLUE : aesBG_BLUE ;
                                    break ;
                              } ;
                              break ;
                           case L'r':  // "red"
                              if ( bold )
                                 *trg = fg ? aesFGb_RED : aesBGb_RED ;
                              else
                                 *trg = fg ? aesFG_RED : aesBG_RED ;
                              break ;
                           // "green" or "grey"
                           case L'g':
                              // Programmer's Note: The backward, barely literate Americans
                              // spell "grey" as "gray"; therefore, to avoid ambiguity, we 
                              // use the fourth character for our secondary comparison.
                              argLetter = tolower( gs.gstr()[3] ) ;
                              switch ( argLetter )
                              {
                                 case L'e':  // "green"
                                    if ( bold )
                                       *trg = fg ? aesFGb_GREEN : aesBGb_GREEN ;
                                    else
                                       *trg = fg ? aesFG_GREEN : aesBG_GREEN ;
                                    break ;
                                    // "grey"
                                 case L'y':
                                    if ( bold )
                                       *trg = fg ? aesFGb_GREY : aesBGb_GREY ;
                                    else
                                       *trg = fg ? aesFG_GREY : aesBG_GREY ;
                                    break ;
                              }
                              break ;
                           case L'm':  // "magenta"
                              if ( bold )
                                 *trg = fg ? aesFGb_MAGENTA : aesBGb_MAGENTA ;
                              else
                                 *trg = fg ? aesFG_MAGENTA : aesBG_MAGENTA ;
                              break ;
                           case L'c':  // "cyan"
                              if ( bold )
                                  *trg = fg ? aesFGb_CYAN : aesBGb_CYAN ;
                              else
                                  *trg = fg ? aesFG_CYAN : aesBG_CYAN ;
                              break ;
                           // terminal default
                           case L'd':
                              *trg = fg ? aesFG_DFLT : aesBG_DFLT ;
                              break ;
                           default:       // application default attribute will be used
                              break ;
                        }
                     }     // color attributes

                     //* For development: Convert a plain-text file  *
                     //* to a CSV file using the specified encoding. *
                     else if ( (gs.compare( L"--crea", true, 6 )) == ZERO )
                     {
                        gs.shiftChars( -eq ) ;     // discard the option name
                        gString gsname = gs ;
                        short off ;
                        if ( (off = gsname.after( COMMA )) > ZERO )
                        {
                           gsname.limitChars( off - 1 ) ;
                           gsname.strip() ;        // delete leading and trailing whitespace
                           gs.shiftChars ( -off ) ;// discard the filename
                           gs.strip() ;            // delete leading and trailing whitespace
                        }
                        ca.userOption = clCreate ;    // hope for the best

                        //* Validate the encoding type *
                        if ( (gs.find( L"utf16le" )) == ZERO )
                           ca.trgEncode = etUTF16le ;
                        else if ( (gs.find( L"utf16be" )) == ZERO )
                           ca.trgEncode = etUTF16be ;
                        else if ( (gs.find( L"utf8bom" )) == ZERO )
                           ca.trgEncode = etUTF8bom ;
                        else if ( (gs.find( L"utf8" )) == ZERO )
                           ca.trgEncode = etUTF8;
                        else
                        { syntax = true ; ca.userOption = clError ; }

                        //* Validate the source filespec *
                        if ( ca.userOption == clCreate )
                        {
                           if ( (off = gs.after( COMMA )) > ZERO )
                           {
                              //* If newlines are to be encoded *
                              //* as CR/LF pairs.               *
                              gs.shiftChars ( -off ) ;// discard the encoding type
                              gs.strip() ;            // delete leading and trailing whitespace
                              if ( (gs.gstr()[0] == L'c') || (gs.gstr()[0] == L'C') )
                              { ca.trgCrlf = true ; }
                           }

                           // Programmer's Note: As a convenience, we use (mis-use) 
                           // the gclaFilespec() method here. Because this option 
                           // overrides a request to display an existing file, 
                           // it _shouldn't_ cause a problem.

                           //* If the file exists and is a "regular" file  *
                           //* with read access, then we'll take the user's*
                           //* word that it is a plain text file.          *
                           //* 'orgPath' will contain the full filespec    *
                           //* if file exists, else the raw filespec.      *
                           this->gclaFilespec ( gsname ) ;
                           if ( ! this->srcStats.readAcc ||
                                (this->srcStats.fType != fmREG_TYPE) )
                           { ca.userOption = clError ; }
                        }
                        break ;     // ignore remaining arguments
                     }

                     //* For Development: capture a sequence of source records. *
                     else if ( (gs.compare( L"--capt", true, 6 )) == ZERO )
                     {
                        gs.shiftChars( -eq ) ;     // discard the option name
                        gString gsname = gs ;
                        short off, a, b ;
                        if ( (off = gsname.after( COMMA )) > ZERO )
                        {
                           gsname.limitChars( off - 1 ) ;
                           gsname.strip() ;        // delete leading and trailing whitespace
                           gs.shiftChars ( -off ) ;// discard the filename
                           gs.strip() ;            // delete leading and trailing whitespace

                           //* If the file exists and is a "regular" file  *
                           //* with read access, then we'll take the user's*
                           //* word that it is a plain text file.          *
                           //* 'orgPath' will contain the full filespec    *
                           //* if file exists, else the raw filespec.      *
                           this->gclaFilespec ( gsname ) ;
                           if ( this->srcStats.readAcc &&
                                (this->srcStats.fType == fmREG_TYPE) &&
                                ((gs.gscanf( "%hd , %hd", &a, &b )) == 2) &&
                                (a >= ZERO) && (b > a) )
                           {
                              ca.capA = a ;
                              ca.capB = b ;
                              ca.userOption = clCapture ;
                           }
                           else
                           { syntax = true ; ca.userOption = clError ; }
                        }
                        else
                        { syntax = true ; ca.userOption = clError ; }
                        break ;     // ignore remaining arguments
                     }

                  }  // (valid '=' delimiter)
                  else
                  {
                     syntax = true ;         // syntax error located
                     break ;                 // ignore any remaining arguments
                  }
               }     // (options with arguments
            }        // (long-form options)

            //************************
            //** Short-form Options **
            //************************
            else
            {
               argLetter = ca.argList[i][j] ;

               //** Short-form Help **
               if ( (argLetter == 'h') || (argLetter == 'H') )
               {
                  if ( ca.argList[i][j+1] == 'l' )
                     ca.userOption = clHelpless ;
                  else
                     ca.userOption = clHelp ;
               }

               //* Invalid short-form option.     *
               //* Ignore all remaining arguments.*
               else
               {
                  ca.userOption = clQuickHelp ;
                  syntax = true ;
               }
            }
         }     // (leading dash)

         //* Token does not begin with a DASH, so assume a filespec *
         else
         {
            gs = ca.argList[i] ;                   // copy the argument
            ca.userOption = this->gclaFilespec ( gs ) ; // get full path and specs
         }
      }  // for(;;)

      //* If a filespec was specified, it overrides stat requests.*
      if ( (*this->orgPath != NULLCHAR) && 
           ((ca.userOption == clSrcStats) || (ca.userOption == clTermSize) ||
            (ca.userOption == clTermInfo)) )
         ca.userOption = clOpen ;

      //* If syntax error(s) and no request for Help or Version *
      //* return signal for Quick Help.                         *
      if ( syntax && ((ca.userOption != clHelpless) && (ca.userOption != clHelp) &&
                      (ca.userOption != clVersion)) )
         ca.userOption = clQuickHelp ;

      //* Reconcile language option with locale. *
      this->gclaLanguage ( ca ) ;

      #if DEBUG_CSVV != 0 && DEBUG_GCLA != 0
      wcout << L"Press Enter..." ;
      wcin.get() ;
      wcout.flush() ;
      #endif   // DEBUG_CSVV && DEBUG_GCLA
   }

}  //* End GetCommandLineArgs() *

//*************************
//*     gclaFilespec      *
//*************************
//********************************************************************************
//* Validate source filespec specified as a command-line argument.               *
//* Filespec could be:                                                           *
//* 1) a filename (assumes current working directory)                            *
//* 2) a relative path to source file                                            *
//* 3) an absolute path to source file                                           *
//* Filespec may, or may not include "special" characters                        *
//* such as '~' ".." '.' '/' etc.                                                *
//*                                                                              *
//* Input  : gsSrc : raw filespec string                                         *
//*                                                                              *
//* Returns: member of enum clArgs: clOpen or clError                            *
//********************************************************************************

clArgs CsvView::gclaFilespec ( const gString& gsSrc )
{
   clArgs userOption = clError ;

   gsSrc.copy( this->orgPath, MAX_PATH ) ;   // save user's raw filespec

   //* If the file exists AND the file extension is ".csv", *
   //* then caller will determine whether it meets the      *
   //* necessary criteria for a CSV file.                   *
   //* Else, the type of error will be reported to user.    *
   if ( (this->GetFileStats ( this->srcStats, gsSrc )) == ZERO )
   {
      gString gs( this->srcStats.fSpec ) ;
      gs.copy( this->orgPath, MAX_PATH ) ; // overwrite raw spec with normalized path

      //* Verify that the file extension is ".csv" (case insensitive). *
      //* We don't like to enforce this, but there is almost no way to *
      //* verify that a file is actually in CSV format, so we need     *
      //* this extra verification.                                     *
      if ( (gs.find( CSV_EXT, (gs.gschars() - 5), false )) > ZERO )
         userOption = clOpen ;
   }

   return userOption ;

}  //* End gclaFilespec() *

//*************************
//*     gclaLanguage      *
//*************************
//********************************************************************************
//* Reconcile locale with user-interface language.                               *
//*  1) Get current locale from environment.                                     *
//*  2) Compare environment locale with requested language.                      *
//*     a) If a match, set 'lang' member to requested language.                  *
//*     b) If a mismatch, set ca.tConfig.localeName and 'lang' member to agree.  *
//*        Caller will set the specified locale.                                 *
//* If a specific user interface language is specified, it overrides automatic   *
//* language selection.                                                          *
//*                                                                              *
//* Input  : commArgs class object (by reference)                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Get locale from environment and compare with specified language:             *
//* If ENV specified:                                                            *
//*    a) env == EN, && lang == EN || ENV, then lang = uilEN                     *
//*    b) env == ZH, && lang == ZH || ENV, then lang =uilZH                      *
//*    c) if (env != EN && env != ZH) || (env not available), then               *
//*          set both locale and 'lang' to English                               *
//* If EN or ZH specified, set the locale to correspond to the selected language.*
//*                                                                              *
//********************************************************************************

void CsvView::gclaLanguage ( commArgs& ca )
{
   const char* dfltEnglish  = "en_US.utf8" ; // locale for English
   const char* dfltZhongwen = "zh_CN.utf8" ; // locale for Chinese (simplified)
   gString gsLocale ;                        // locale filename

   #if DEBUG_CSVV != 0 && DEBUG_GCLA != 0
   UILang origLang = this->lang ;
   const char* langName[uilENV + 1] =
   { { "uilEN" }, { "uilZH" }, { "uilENV" } } ;
   #endif   // DEBUG_CSVV && DEBUG_GCLA

   //* Scan the environment for the locale name. *
   for ( short i = ZERO ; ca.envList[i] != NULL ; ++i )
   {
      gsLocale = ca.envList[i] ;
      if ( (gsLocale.after( L"LANG=", ZERO, true )) == 5 )
      { gsLocale.shiftChars( -5 ) ; break ; }
   }
   //* If the locale was not found from the environment, *
   //* we have to make an assumption i.e. use default.   *
   if ( *ca.tConfig.localeName == NULLCHAR )
      gsLocale = dfltEnglish ;

   //* Compare specified language with system locale and reconcile *
   //* the two to support the specified user interface language.   *
   if ( (gsLocale.find( L"zh" )) == ZERO )
   {
      if ( (this->lang == uilENV) || (this->lang == uilZH) )
      { this->lang = uilZH ; }
      else  // ( this->lang == uilEN )
      {
         gsLocale = dfltEnglish ;
         gsLocale.copy( ca.tConfig.localeName, LOCALE_LEN ) ;
      }
   }
   else if ( (gsLocale.find( L"en" )) == ZERO )
   {
      if ( (this->lang == uilENV) || (this->lang == uilEN) )
      { this->lang = uilEN ; }
      else  // ( this->lang == uilZH )
      {
         gsLocale = dfltZhongwen ;
         gsLocale.copy( ca.tConfig.localeName, LOCALE_LEN ) ;
      }
   }
   else     // unsupported locale - set default language
   {        // Note: There may be a mismatch between the locale specified 
            //       in the environment and the user interface language.
      if ( this->lang == uilENV ) { this->lang = uilEN ; }
   }

   #if DEBUG_CSVV != 0 && DEBUG_GCLA != 0
   gsLocale.compose( "gclaLanguage( lang:%s -> %s localeName:%s )",
                     langName[origLang], langName[this->lang],
                     ((*ca.tConfig.localeName != NULLCHAR) ? 
                       ca.tConfig.localeName : "unknown") ) ;
   wcout << gsLocale << endl ;
   #endif   // DEBUG_CSVV && DEBUG_GCLA

   #undef DEBUG_GCLA
}  //* End gclaLanguage() *

//*************************
//*         pause         *
//*************************
//********************************************************************************
//* Ask user to press a key to continue output.                                  *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
void CsvView::pause ( void )
{
   const char* const msg1[] =
   { { "\nPress Any Key To Continue..." }, { "\n按任意键继续..." } } ;
   gString gsOut( msg1[this->lang] ) ;
   this->ansi->ttyWrite ( gsOut ) ;
   this->ansi->acPause () ;
   this->ansi->acSetCursor ( aesCUR_ABSCOL, ZERO ) ;  // set cursor to column zero
   this->ansi->acEraseArea ( aesERASE_LINE ) ;        // erase the prompt
}  //* End pause() *

//*************************
//*    DisplayVersion     *
//*************************
//********************************************************************************
//* Display the application version number and copyright information.            *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void CsvView::DisplayVersion ( void )
{
   static const char* const freeSoftware[] = 
   {
      {
      "License GPLv3+: GNU GPL version 3 <http://gnu.org/licenses/gpl.html>\n"
      "This is free software: you are free to modify and/or redistribute it\n"
      "under the terms set out in the license.\n"
      "There is NO WARRANTY, to the extent permitted by law.\n\n"
      },
      {
      "软件许可 GPLv3+: GNU GPL 版本 3 <http://gnu.org/licenses/gpl.html>\n"
      "这是免费软件：您可以根据许可证中规定的条款自由修改和/或重新分发它。\n"
      "在法律允许的范围内，不提供任何保证。\n\n"
      },
   } ;
   static const char* const linkLib[] = 
   { { "Link Library" }, { "链接库" } } ;

   gString gsOut ;
   this->ComposeTitle ( gsOut, true ) ;
   // Programmer's Note: This is fancy formatting :-)
   gString gsverb( "AnsiCmd %s v:%s", 
                   linkLib[this->lang], this->ansi->acVersion () ) ;
   gsverb.append( "  (gString v:%s)", gsverb.Get_gString_Version() ) ;
   gsverb.shiftCols( gsOut.scan() - 1 ) ;
   gsverb.padCols( ((gsOut.gscols()) / 2) ) ;
   gsverb.append( L'\n' ) ;
   gsverb.padCols( (gsOut.gscols()), L'-' ) ;
   gsOut.append( "%S\n", gsverb.gstr() ) ;

   this->ansi->acSetFg ( this->hdrAttr ) ;
   this->ansi->ttyWrite ( gsOut, false ) ;
   gsOut = freeSoftware[this->lang] ;
   this->ansi->ttyWrite ( gsOut ) ;

   #if DEBUG_ANSICMD != 0 || INCLUDE_API != 0
   const char* msg1 =
      "-----------------------------------------------------------------------\n" ;
   const char* msg2[] =
   {
      {
      "Alert :: The AnsiCmd library was built with the DEBUG_ANSICMD directive\n"
      "and/or the INCLUDE_API directive enabled. While this does not affect\n"
      "application functionality, disabling both of these directives will\n"
      "reduce the size of the binary by approximately 800 Kbytes.\n\n"
      },
      {
      "警报：AnsiCmd 库是在启用 DEBUG_ANSICMD 指令和/或 INCLUDE_API 指令的情况\n"
      "下构建的。虽然这不会影响应用程序功能，但禁用这两个指令将使二进制文件的\n"
      "大小减少大约 800 KB。\n"
      }
   } ;
   this->ansi->acSetCursor ( aesCUR_PREVROW ) ;
   this->ansi->ttyWrite ( msg1 ) ;
   gsOut = msg2[this->lang] ;
   this->ansi->ttyWrite ( gsOut ) ;
   #endif   // DEBUG_ANSICMD || INCLUDE_API

}  //* End DisplayVersion() *

//*************************
//*      DisplayHelp      *
//*************************
//********************************************************************************
//* Display command-line help message.                                           *
//*                                                                              *
//* Input  : userOption:                                                         *
//*               clHelp      - display message to stdout                        *
//*               clHelpless  - redirect stdout to the 'less' utility            *
//*               clQuickHelp - remind user that a source filename is required   *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//*                                                                              *
//*                                                                              *
//********************************************************************************

void CsvView::DisplayHelp ( clArgs userOption )
{
    //123456789-123456789-123456789-123456789-123456789-123456789-123456789-1
   const char* Help1 =
   "\nScan and format for display a Comma-Seperated-Values (CSV) file."
   "\nOptionally, save the formatted output. Source file is not modified."
   "\nSupports both standard UTF-8 and brain-dead (Windoze) UTF-16 encoding."
   "\nUsage: csvv [OPTIONS] filespec"
   "\nArguments may be specified in any order. For long-form options, four(4)"
   "\ncharacters are sufficient to uniquely identify the option."
   "\n  \"csvv --vers\" is equivalent to \"csvv --version\""
   "\n  For each option that requires one or more arguments, the option name"
   "\n  and its argument(s) may be separated by either '=' or ' ' (space)."
   "\n  Note that if the argument contains whitespace, then the entire "
   "\n  argument must be enclosed within single-quotes or double-quotes."
   "\n    Examples: --option=xyz"
   "\n              --option 'x y z'"
   "\n              --option=\"xyz\""
//   "\n  Short-form options (without arguments) may be concatenated."
//   "\n    Examples:"
   "\n-----------------------------------------------------------------------"
   "\n --filespec=FILE: Specify the CSV file to be formatted and displayed."
   "\n             Filespec may be relative or absolute path. Filename"
   "\n             extension must be \".csv\". Shell special characters"
   "\n             are recognized. If whitespace, enclose in quotes."
   "\n             Examples: csvv --filespec=email_list.csv"
   "\n                       csvv email_list.csv   (filespec command assumed)"
   "\n                       csvv --filespec='Work Contacts.csv'"
   "\n                       csvv --file=\"Cindy's Friends.csv\""
   "\n --display=[ h | v | q ] Specify the output format."
   "\n                Output is piped to the 'less' utility for display."
   "\n           h == Horizontal: One record per output line. (default)"
   "\n           v == Vertical: One field per output line with field header"
   "\n                          header on the left."
   "\n           q == Quick: Same format as \"Horizontal\" except that data"
   "\n                   are written directly to stdout."
   "\n --fgndattr=[black|red|green|brown|blue|magenta|cyan|grey|dflt]"
   "\n            Specify forground (text) color attribute for display."
   "\n            For intense (bold) attributes, append an 'i' to argument."
   "\n            Examples: --fgndattr 'red'     (default: blue)"
   "\n                      --fgndattr='redi'"
   "\n --bgndattr=[black|red|green|brown|blue|magenta|cyan|grey|dflt]"
   "\n            Specify backround color attribute for display."
   "\n            Ex: --bgndattr='cyan'     (default: dflt, terminal default)"
   "\n                --bgndattr 'blacki'"
   "\n --language=[en | zh | te] Language for user interface:"
   "\n            en == English"
   "\n            zh == 中文 (Zhōngwén - Chinese, simplified)"
   "\n            te == terminal environment (default)"
   "\n            By default, the language is determined by the \"locale\""
   "\n            specified in the terminal environment: \"zh_*\" for Chinese"
   "\n            else English."
   "\n --header=[y | n]  Specify whether the source file contains a header"
   "\n          record. Specify if programatic scan for header is incorrect."
   "\n          'y' == header record is present, 'n' == no header record."
// "-------------------------------------------------------------------------"
   "\n --saveas=TARGET : If specified, then on exit, a copy of the displayed"
   "\n          data will be saved to a file in the current directory."
   "\n          (For either Horizontal or Vertical display formats.)"
   "\n --stats: Report stats for specified source file."
   "\n --dimensions: Report terminal window dimensions (rows and columns)."
   "\n --help     (-h)  Display application help, then exit."
   "\n --helpless (-hl) Display application help using the 'less' utility."
   "\n --version  Display application version and copyright information."
   "\n-----------------------------------------------------------------------"
   "\n Options for development and debugging:"
   "\n---------------------------------------"
   "\n --create=SOURCE_FILENAME,ENCODING[,crlf]"
   "\n          For development purposes: Create a custom CSV file from the"
   "\n          specified plain text file using the specified encoding."
   "\n          Encoding options are: utf8 | utf8bom | utf16le | utf16be"
   "\n          By default, each record is terminated by a newline character"
   "\n          (\\n 0x0A); however if desired, newlines may be encoded using"
   "\n          the carriage-return/linefeed pair: (\\r\\n 0x0D 0x0A) by"
   "\n          including the optional \"crlf\" argument."
   "\n          Target file will be created in the same directory as the"
   "\n          source file, with the \".csv\" filename extension."
   "\n          Each record of the source file must be on a seperate line,"
   "\n          and fields must be seperated by commas (duh!)."
   "\n          Ex: csvv --create='MyRecords.txt,utf86bom'"
   "\n              csvv --create='Thunderbird Contacts.txt,utf16le,crlf'"
   "\n          See documentation for an example source file."
   "\n --capture=SOURCE_FILENAME,BEGIN,END  : Capture a range of records for"
   "\n          analysis. 'begin' and 'end' represent the indices of first"
   "\n          and last records to capture."
   "\n          (Header record, index 0, is always captured.)"
   "\n          Ex: csvv --capture='MyContacts.csv,4,6'"
   "\n --terminfo: Report terminal configuration. (primarily for debugging)"
   "\n --pause: For debugging, after diagnostic output, wait for keypress"
   "\n          before display of CSV record data."
   "\n " ;  // End Help1

   if ( userOption == clQuickHelp )
   {
      this->ansi->ttyWrite ( "CsvView: Please specify the name of a source file \n"
                             "in CSV format, or type: csvv --help               \n\n" ) ;
   }
   else
   {
      gString gs ;
      this->ComposeTitle ( gs, false ) ;
      bool write2stdout = true ;

      //* If specified, pipe the Help text to the 'less' utility.*
      if ( userOption == clHelpless )
      {
         FILE *output = popen ( "less -c", "w" ) ;
         if ( output != NULL )
         {
            fprintf ( output, gs.ustr() ) ;
            fprintf ( output, Help1 ) ;
            pclose ( output ) ;
            write2stdout = false ;
         }
         //* If popen() fails, write to stdout instead.*
      }

      //* Write Help text to stdout *
      if ( write2stdout != false )
      {
         this->ansi->acSetFg ( aesFG_DFLT ) ;      // default foreground attribute
         this->ansi->ttyWrite ( gs, false ) ;
         this->ansi->ttyWrite ( Help1, false ) ;
//         this->win->ttyWrite ( HelpTerm, false ) ;
         this->ansi->ttyWrite ( '\n' ) ;
      }
   }

}  //* End DisplayHelp() *

//*************************
//*                       *
//*************************
//********************************************************************************
//*                                                                              *
//*                                                                              *
//*                                                                              *
//* Input  :                                                                     *
//*                                                                              *
//* Returns:                                                                     *
//********************************************************************************

