//********************************************************************************
//* File       : CsvFile.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: Generic support for file manipulation, including application    *
//* temporary files.                                                             *
//*                                                                              *
//*                                                                              *
//********************************************************************************
//* 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'.             *
//********************************************************************************

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

//***************
//* Definitions *
//***************

//***************
//* Prototypes  *
//***************
static void ccrFormat ( uint8_t *ibuff, short ibytes, ofstream& ofs ) ;

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


//*************************
//*     DeleteFile        *
//*************************
//********************************************************************************
//* Delete (unlink) the specified source file.                                   *
//*                                                                              *
//* Input  : fspec : full path/filename specification of source                  *
//*                                                                              *
//* Returns: OK if successful, else returns errno value                          *
//********************************************************************************
//* Programmer's Note: The C-language version of 'unlink' INSISTS on doing its   *
//* own escaping of special characters. If it receives escaped characters, it    *
//* escapes the '\' characters, negating the previous escape sequence.           *
//********************************************************************************

short CsvView::DeleteFile ( const char* fspec )
{
   short    status ;

   if ( (status = unlink ( fspec )) != ZERO )
      status = errno ;                    // return 'errno' value

   return status ;

}  //* End DeleteFile() *

//************************
//*       CopyFile       *
//************************
//********************************************************************************
//* Copy the source file to target.                                              *
//* It is assumed that user has write access to the target directory.            *
//* If user does not have write access, the operation will fail.                 *
//*                                                                              *
//* Input  : srcSpec : filespec of source file                                   *
//*          trgSpec : filespec of target file                                   *
//*                                                                              *
//* Returns: OK if successful, else returns errno value                          *
//********************************************************************************
//* Notes:                                                                       *
//* -- This method does not handle "special characters" in the filespec strings. *
//*    Instead we rely on the fact that caller has normalized the strings        *
//*    through the C library function, 'realpath'. (see GetFileStats())          *
//* -- For the same reason, this method does not handle symlinks within the      *
//*    filespecs because 'realpath' will have resolved the indirection.          *
//* -- The return value from the 'system' function is not reliable.              *
//*    A -1 is returned if the shell process cannot be executed.                 *
//*    Otherwise the status of the shell process is returned. This IS NOT        *
//*    necessarily the same as the exit value of the executed command.           *
//*    'errno' may or may not contain the actual status value.                   *
//*                                                                              *
//********************************************************************************

short CsvView::CopyFile ( const char* srcSpec, const char* trgSpec )
{
   const short bSIZE = MAX_PATH * 3 ;  // size of command buffer
   char     cmd[bSIZE] ;               // holds command string
   short    status ;                   // return value

   snprintf ( cmd, bSIZE, 
              "cp --preserve=all \"%s\" \"%s\" 1>/dev/null 2>/dev/null", 
              srcSpec, trgSpec ) ;

   if ( (status = system ( cmd )) != ZERO )
      status = errno ;         // save 'errno' value

   return status ;

}  //* End CopyFile() *

#if 0    // REFERENCE
//************************
//*       MoveFile       *
//************************
//********************************************************************************
//********************************************************************************

//*************************
//*      RenameFile       *
//*************************
//********************************************************************************
//* Rename the specified source file to specified destination file.              *
//*                                                                              *
//* For the rename() primitive: If 'srcspec' refers to a symbolic link the       *
//* link is renamed; if 'trgspec' refers to a symbolic link the link will be     *
//* overwritten.                                                                 *
//*                                                                              *
//* Input  : srcspec : full path/filename specification of source                *
//*          trgspec : full path/filename specification of target                *
//*                                                                              *
//* Returns: OK if successful, else returns errno value                          *
//********************************************************************************
//* Programmer's Note: The C-language version of 'rename' INSISTS on doing its   *
//* own escaping of special characters. If it receives escaped characters, it    *
//* escapes the '\' characters, negating the previous escape sequence.           *
//********************************************************************************

short RenameFile ( const char* srcspec, const char* trgspec ) ;
short CsvView::RenameFile ( const char* srcspec, const char* trgspec )
{
   short    status ;

   if ( (status = rename ( srcspec, trgspec )) != OK )
      status = errno ;                 // save 'errno' value
   return status ;

}  //* End RenameFile() *

//************************
//*    DeleteEmptyDir    *
//************************
//********************************************************************************
//* Delete the specified directory. (directory must be empty)                    *
//*                                                                              *
//*                                                                              *
//* Input  : dirPath : full path specification of source                         *
//*                                                                              *
//* Returns: OK if successful, else returns errno value                          *
//********************************************************************************
//* Programmer's Note: The C-language version of 'rmdir' INSISTS on doing its    *
//* own escaping of special characters. If it receives escaped characters, it    *
//* escapes the '\' characters, negating the previous escape sequence.           *
//********************************************************************************

short DeleteEmptyDir ( const char* dirPath ) ;
short CsvView::DeleteEmptyDir ( const char* dirPath )
{
   short    status ;

   if ( (status = rmdir ( dirPath )) != ZERO )
      status = errno ;                 // save 'errno' value
   return status ;

}  //* End DeleteEmptyDir() *
#endif   // REFERENCE

//*************************
//*     GetFileStats      *
//*************************
//********************************************************************************
//* Perform a 'stat' ('lstat') on the file specified by the full path string.    *
//* By default the stats for the specified file are returned, but if target is   *
//* a symbolic link, the symbolic link is followed i.e. stats for link target    *
//* are returned.                                                                *
//*                                                                              *
//*                                                                              *
//* Input  : tnFile : tnFName class object (by reference) to hold 'stat' data    *
//*          fPath  : (by reference ) target filespec                            *
//*                                                                              *
//* Returns: ZERO if successful, all data members of tnFile will be initialized  *
//*          Else returns system 'errno' value                                   *
//*            From 'realpath':                                                  *
//*            If errno == EACCES, at least one path component is not readable   *
//*            If errno == ENOENT, at least one path component does not exist    *
//*            From 'stat' i.e. 'stat64':                                        *
//*            If errno == ENOENT, file does not exist                           *
//*                                (or link target does not exist)               *
//*            If errno == EACCES, the process lacks permission for a container  *
//*                                directory                                     *
//*            If errno == ENOTDIR, an element of the path s/b a directory,      *
//*                                 but is not (unlikely)                        *
//*                                                                              *
//*           If error during system call, on return:                            *
//*            - tnFile.readAcc == false and all other members are undefined     *
//*             Exception:                                                       *
//*             If error is a broken symbolic link: [DISABLED]                   *
//*             - tnFile.fName == name of link target if known, else "unknown"   *
//*             - tnFile.fSpec == full filespec if known, else "unknown"         *
//*             - tnFile.readAcc == false                                        *
//*             - tnFile.fType == fmTYPES                                        *
//*             - All other members of 'tnFile' are undefined                    *
//********************************************************************************
//* -- Note that 'pathname' must be an absolute path. If it is, then 'dirfd'     *
//*    (file-descriptor) is ignored.                                             *
//* File descriptor options are not used here:                                   *
//* - If 'pathname' is not an absolute path, then setting 'dirfg' to the         *
//*   constant: AT_FDCWD (-100) indicates that 'pathname' contains a relative    *
//*   path.                                                                      *
//* - If dirfd==filedescriptor# of a directory, then 'pathname' is relative to   *
//*    that diretory, UNLESS:                                                    *
//*   - 'pathname' is an empty string, in which case 'dirfd' is a file           *
//*      descriptor for any type of file, not just a directory.                  *
//*                                                                              *
//* statx() Prototype:                                                           *
//*   include file: <sys/stat.h> (include <fcntl.h> for AT_* constants.)         *
//*   int statx(int dirfd, const char *restrict pathname, int flags,             *
//*             unsigned int mask, struct statx *restrict statxbuf);             *
//*   'flags'   :                                                                *
//*      AT_EMPTY_PATH (not used)                                                *
//*      AT_NO_AUTOMOUNT - set: do not auto-mount the target                     *
//*      AT_SYMLINK_NOFOLLOW - reset: follow the symline                         *
//*      AT_STATX_SYNC_AS_STAT - set: emulate the 'stat' function                *
//*      AT_STATX_FORCE_SYNC - (not used)                                        *
//*      AT_STATX_DONT_SYNC - set: returns cached data, if available             *
//*   'mask'    :                                                                *
//*      STATX_TYPE          mode (stx_mode & S_IFMT) with filetype bitmask      *
//*      STATX_MODE          mode (stx_mode & ~S_IFMT) without filetype bitmask  *
//*      STATX_NLINK         number of hard links (stx_nlink)                    *
//*      STATX_UID           user ID (stx_uid)                                   *
//*      STATX_GID           group ID (stx_gid)                                  *
//*      STATX_ATIME         timestamp - last access (stx_atime)                 *
//*      STATX_MTIME         timestamp - last modification (stx_mtime)           *
//*      STATX_CTIME         timestamp - last status update (stx_ctime)          *
//*      STATX_INO           inode (stx_ino)                                     *
//*      STATX_SIZE          file size in bytes (stx_size)                       *
//*      STATX_BLOCKS        allocated storage blocks (stx_blocks)               *
//*      STATX_BASIC_STATS   [All of the above]                                  *
//*      STATX_BTIME         timestamp - creation time (stx_btime)               *
//*      STATX_ALL           (deprecated, not used) STATX_BASIC_STATS|STATX_BTIME*
//*      STATX_MNT_ID        (not used) Want stx_mnt_id (since Linux 5.8)        *
//*      STATX_DIOALIGN      (not used)(stx_dio_mem_align&&stx_dio_offset_align) *
//*                                                                              *
//*   'statxbuf': pointer to 'statx' structure                                   *
//*                                                                              *
//*                                                                              *
//* This structure replaces the int64_t timestamp values in the stat64 structure.*
//* struct statx_timestamp                                                       *
//* {                                                                            *
//*    __s64 tv_sec;    /* Seconds since the Epoch (UNIX time) */                *
//     __u32 tv_nsec;   /* Nanoseconds since tv_sec */                           *
//  };                                                                           *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//********************************************************************************

short CsvView::GetFileStats ( tnFName& tnFile, const gString& fPath )
{
   //* Define the bitfields for statx() call *
   const int sx_flags = (AT_NO_AUTOMOUNT | AT_STATX_SYNC_AS_STAT | AT_STATX_DONT_SYNC) ;
   const unsigned int sx_mask = (STATX_BASIC_STATS | STATX_BTIME) ;

   char fullPath[MAX_PATH] ;        // full path/filename
   gString gsfp, gsfn ;             // text management
   short status = ZERO ;            // return value

   //* Provides an orderly crash in case of failure *
   tnFile.fBytes = ZERO ;
   tnFile.fType = fmUNKNOWN_TYPE ;
   tnFile.readAcc = tnFile.writeAcc = false ;

   //* Convert the provided filename string to a full filespec. *
   //* Symbolic links _are_followed.                            *
   if ( (realpath ( fPath.ustr(), fullPath )) != NULL )
   {
      gsfp = fullPath ;
      gsfp.copy( tnFile.fSpec, MAX_PATH ) ;     // save the full filespec
      this->ExtractFilename ( gsfn, gsfp ) ;
      gsfn.copy( tnFile.fName, MAX_FNAME ) ;    // isolate the target filename

      //* If successful 'stat' of the target, decode the raw stats. *
      if ( (statx ( 0, tnFile.fSpec, sx_flags, sx_mask, &tnFile.rawStats )) == 0 )
      {
         tnFile.fType = this->DecodeFileType ( tnFile.rawStats.stx_mode ) ; // file type
         tnFile.fBytes = tnFile.rawStats.stx_size ;    // number of bytes in file

         this->DecodeEpochTime ( tnFile.rawStats.stx_mtime.tv_sec, // mod time
                                 tnFile.modTime ) ;
         tnFile.readAcc = this->ReadPermission ( tnFile, fPath.ustr() ) ;
         tnFile.writeAcc = this->WritePermission ( tnFile, fPath.ustr() ) ;
      }
      else     // stat failed. return the 'errno' value (see note above)
         status = errno ;

   }     // realpath()

   //* 'realpath' failed. This is likely a broken symbolic link.*
   else
   {
      //* A broken link causes 'stat' to fail, but that is not actually  *
      //* an error. It is a a successful determination that the link     *
      //* target is missing or inaccessible.                             *
      status = errno ;
   }

   return status ;

}  //* End GetFileStats() *

//*************************
//*    ExtractFilename    *
//*************************
//********************************************************************************
//* Given a string that may contain one of the following:                        *
//*  - a filename                                                                *
//*  - a relative path/filename                                                  *
//*  - an absolute path/filename                                                 *
//* extract the filename string from the input string and return it to caller.   *
//*                                                                              *
//* Input  : nameBuff: buffer to hold filename                                   *
//*          pfSource: source filename or path/filename                          *
//*                                                                              *
//* Returns: 'true' if filename successfully isolated                            *
//*          'false' in special case of pfSource=="/" (at root directory)        *
//*                  OR if invalid input                                         *
//********************************************************************************
//* Programmer's Note: It is assumed that caller has sent us a valid filespec    *
//* in pfSource. If not, the source will be returned in the target.              *
//********************************************************************************

bool  ExtractFilename ( gString& nameBuff, const gString& pfSource ) ;
bool CsvView::ExtractFilename ( gString& nameBuff, const gString& pfSource )
{
   bool status = false ;      // return value

   short fIndx = (pfSource.findlast( fSLASH )) + 1 ;
   nameBuff = &pfSource.gstr()[fIndx] ;
   if ( ((nameBuff.gschars()) > 2) ||
        (((nameBuff.gschars()) == 2) && (*nameBuff.gstr() != fSLASH)) )
   {
      status = true ;
   }
   return status ;

}  //* End ExtractFilename() *
bool CsvView::ExtractFilename ( char nameBuff[MAX_FNAME], const gString& pfSource )
{
   bool status = false ;      // return value

   gString gs ;
   status = this->ExtractFilename ( gs, pfSource ) ;
   gs.copy( nameBuff, MAX_FNAME ) ;

   return status ;

}  //* End ExtractFilename() *

//*************************
//*   FormatDateString    *
//*************************
//********************************************************************************
//* Produce a formatted output string based on data in a localTime object.       *
//*   Format:  "dd Mmm yyyy hh:mm:ss"                                            *
//*                                                                              *
//* Input  : ft   : initialized localTime structure (by reference)               *
//*                Note: 'day', the day-of-the-week field is ignored.            *
//*          str  : buffer to receive formatted data                             *
//*          spec : (optional, 'false' by default)                               *
//*                 if 'false', format in non-technical format                   *
//*                 if 'true',  format as a canonical timestamp                  *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void CsvView::FormatDateString ( const localTime& ft, gString& str, bool spec )
{
   //* Month-name strings for file info formatting *
   const wchar_t wMonthString[13][4] =
   {
      { L"   " },
      { L"Jan" }, { L"Feb" }, { L"Mar" }, { L"Apr" }, { L"May" }, { L"Jun" },
      { L"Jul" }, { L"Aug" }, { L"Sep" }, { L"Oct" }, { L"Nov" }, { L"Dec" }
   } ;

   if ( spec )
   {
      str.compose( "%04hd-%02hd-%02hdT%02hd:%02hd:%02hd",
                   &ft.year,  &ft.month,   &ft.date, 
                   &ft.hours, &ft.minutes, &ft.seconds ) ; 
   }
   else
   {
      str.compose( L"%02hu %S %04hu %02hu:%02hu:%02hu",
                   &ft.date, wMonthString[ft.month], &ft.year, 
                   &ft.hours, &ft.minutes, &ft.seconds ) ;
   }

}  //* End FormatDateString() *

//*************************
//*    DecodeFileType     *
//*************************
//********************************************************************************
//* Extract the file type from the "st_mode" element of the stat{} structure     *
//* and return the equivalent member of enum fmFType.                            *
//*                                                                              *
//* PRIVATE METHOD.                                                              *
//*                                                                              *
//* The POSIX standard defines certain macros to access the file "st_mode".      *
//* There are several layers to the macros, but for Linux EXT2, EXT3 we          *
//* finally get down to the actual bits in /usr/include/bits/stat.h:             *
//* See also page 135 of "Linux Programming by Example."                         *
//* The actual macros that use these bitmasks are used in this routine.          *
//*                                                                              *
//* Encoding of the file mode.                                                   *
//* #define     __S_IFMT        0170000 /* These bits determine file type.       *
//* File types.                                                                  *
//* #define     __S_IFDIR       0040000  Directory.                              *
//* #define     __S_IFCHR       0020000  Character device.                       *
//* #define     __S_IFBLK       0060000  Block device.                           *
//* #define     __S_IFREG       0100000  Regular file.                           *
//* #define     __S_IFIFO       0010000  FIFO.                                   *
//* #define     __S_IFLNK       0120000  Symbolic link.                          *
//* #define     __S_IFSOCK      0140000  Socket.                                 *
//*                                                                              *
//* POSIX defines additional macros of the for S_ISxxx versions of these         *
//* macros, and they are used here.                                              *
//*                                                                              *
//* Input Values : mode: st_mode field of file's stat{} strucutre                *
//*                                                                              *
//* Return Value : member of enum fmFType                                        *
//********************************************************************************

fmFType CsvView::DecodeFileType ( uint32_t mode )
{
   fmFType     ftype = fmUNKNOWN_TYPE ;

   if ( (S_ISREG(mode)) != false )        ftype = fmREG_TYPE ;
   else if ( (S_ISDIR(mode)) != false )   ftype = fmDIR_TYPE ;
   else if ( (S_ISLNK(mode)) != false )   ftype = fmLINK_TYPE ;
   else if ( (S_ISCHR(mode)) != false )   ftype = fmCHDEV_TYPE ;
   else if ( (S_ISBLK(mode)) != false )   ftype = fmBKDEV_TYPE ;
   else if ( (S_ISFIFO(mode)) != false )  ftype = fmFIFO_TYPE ;
   else if ( (S_ISSOCK(mode)) != false )  ftype = fmSOCK_TYPE ;
   
   return ftype ;
   
}  //* End DecodeFileType() *

//*************************
//*    DecodeEpochTime    *
//*************************
//********************************************************************************
//* Convert GNU/UNIX epoch time code (seconds since the epoch: Jan. 01, 1970)    *
//* to local time in a structured format (class localTime).                      *
//*                                                                              *
//* See the info page for the 'stat' command for more information.               *
//* See also the notes in DirEntryDisplayFormat() above.                         *
//*                                                                              *
//* Input  : eTime  : epoch time: seconds since the epoch                        *
//*                   this is a signed, 64-bit value                             *
//*                   (Note that the nanoseconds value of the epoch time is )    *
//*                   (ridiculous for desktop computing and is ignored here.)    *
//*          ftTime : (by reference, initial values ignored)                     *
//*                                                                              *
//* Returns: nothing, but all members of ftTime will be initialized              *
//********************************************************************************
//* System conversion from time_t code to local time is returned in this         *
//* structure.                                                                   *
//*                                                                              *
//*  struct tm                                                                   *
//*  {                                                                           *
//*     int  tm_sec ;          // 0-59  (leap seconds to 61)                     *
//*     int  tm_min ;          // 0-59                                           *
//*     int  tm_hour ;         // 0-23                                           *
//*     int  tm_mday ;         // 1-31                                           *
//*     int  tm_mon ;          // 0-11                                           *
//*     int  tm_year ;         // since 1900                                     *
//*     int  tm_wday ;         // 0-6                                            *
//*     int  tm_yday ;         // 0-365                                          *
//*     int  tm_isdst ;        // >0 == is DST, 0 == not DST, <0 == unknown      *
//*  } ;                                                                         *
//*                                                                              *
//* -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - -  *
//* Note that we do a bit of defensive programming in anticipation of the        *
//* dreaded year 2038 overflow of the 32-bit time_t type.                        *
//* -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - -  *
//* Daylight Savings Time:                                                       *
//* ----------------------                                                       *
//* Daylight Savings Time adjustments are not handled uniformly across           *
//* all filesystems: beware.                                                     *
//********************************************************************************

void CsvView::DecodeEpochTime ( int64_t eTime, localTime& ftTime )
{
   ftTime.reset() ;           // initialize caller's data
   if ( eTime >= ZERO )       // defend against stupidity
   {  //* Width of time_t is system dependent *
      ftTime.epoch = eTime ;
      ftTime.sysepoch = (time_t)eTime ;   // (possible narrowing)

      //* Receives the broken-down time.              *
      Tm bdt ;       // receives broken-down time
      if ( (localtime_r ( &ftTime.sysepoch, &bdt )) != NULL )
      {
         //* Translate to localTime format *
         ftTime.date    = bdt.tm_mday ;         // today's date
         ftTime.month   = bdt.tm_mon + 1 ;      // month
         ftTime.year    = bdt.tm_year + 1900 ;  // year
         ftTime.hours   = bdt.tm_hour ;         // hour
         ftTime.minutes = bdt.tm_min ;          // minutes
         ftTime.seconds = bdt.tm_sec ;          // seconds
         ftTime.day     = bdt.tm_wday ;         // day-of-week (0 == Sunday)
         ftTime.julian  = bdt.tm_yday ;         // Julian date (0 == Jan.01)

         #if DECODE_TZ != 0   // time-zone data
         ftTime.gmtoffset = bdt.tm_gmtoff ;     // offset as number of seconds
         ftTime.dst = (bdt.tm_isdst > ZERO ) ? true : false ; // DST in effect

         #if 1    // additional time zone data (not very useful) *
         gString gs( bdt.tm_zone ) ;
         gs.copy( ftTime.timezone, ltFMT_LEN ) ;
         int absOffset = abs(ftTime.gmtoffset) ;
         int hOffset = absOffset / (60*60),     // convert offset seconds to 
             mOffset = absOffset % (60*60) ;    // hours and minutes
         if ( ftTime.gmtoffset < ZERO )
            hOffset = -(hOffset) ;
         gs.compose( L"UTC%+d:%02d", &hOffset, &mOffset ) ;
         gs.copy( ftTime.utc_zone, ltFMT_LEN ) ;
         #endif   // additional time zone data
         #endif   // DECODE_TZ
      }
   }

}  //* End DecodeEpochTime() *

//*************************
//*    ReadPermission     *
//*************************
//********************************************************************************
//* Determine whether user has 'read' permission for the specified file,         *
//*                                                                              *
//* IMPORTANT NOTE:                                                              *
//* True read permission on a directory name actually requires BOTH 'Read' and   *
//* 'Execute' permission.                                                        *
//* a) 'Execute' only, will allow us to set the directory to CWD, but            *
//*    will not allow us to read its contents.                                   *
//* b) 'Read' only, will allow us to read the NAMES of the files contained in    *
//*    the directory (but NOT the other stats), but will not allow us to set     *
//*    the directory as the CWD.                                                 *
//* Therefore, if caller wants a report of read access on a directory name,      *
//* we indicate read access ONLY if user has both 'Read' and 'Execute'           *
//* permission. If caller wants to enable read access on a directory name, we    *
//* enable BOTH 'Read' and 'Execute' permissions.                                *
//*                                                                              *
//* Input : sStats: (by reference) stats of file to be tested                    *
//*                 sStats.readAcc will be set/reset according to result         *
//*                 and if 'enable', then st_mode bits will be updated,          *
//*                 other members remain unchanged                               *
//*         sPath : path/filename of file to be tested                           *
//*                                                                              *
//* Returns: 'true' if user has read permission, else 'false'                    *
//********************************************************************************
//* Programmer's Note: For symbolic links, read access is actually more complex  *
//* than simply reading a bitfield. For this application, however, we are only   *
//* interested in the link target, not the link itself. The "access" function    *
//* always follows symbolic links, so we don't need to worry about permissions   *
//* for symbolic link files.                                                     *
//* See the FileMangler version of this method for more information              *
//********************************************************************************

bool CsvView::ReadPermission ( tnFName& sStats, const char* sPath )
{
   //* Determine user's read access *
   sStats.readAcc = bool((access ( sPath, R_OK )) == ZERO) ;
   if ( sStats.readAcc != false && sStats.fType == fmDIR_TYPE )
      sStats.readAcc = bool((access ( sPath, X_OK )) == ZERO) ;

   return sStats.readAcc ;

}  //* End ReadPermission() *

//*************************
//*    WritePermission    *
//*************************
//********************************************************************************
//* Determine whether user has 'write' permission for the specified file,        *
//* and optionally enable owner/group write permission.                          *
//*                                                                              *
//* Input : sStats: (by reference) stats of file to be tested                    *
//*                 sStats.writeAcc will be set/reset according to result        *
//*                 and if 'enable', then st_mode bits will be updated,          *
//*                 other members remain unchanged                               *
//*         sPath : path/filename of file to be tested                           *
//*                                                                              *
//* Returns: 'true' if user has write permission, else 'false'                   *
//********************************************************************************

bool CsvView::WritePermission ( tnFName& sStats, const char* sPath )
{
   //* Determine user's write access *
   sStats.writeAcc = bool((access ( sPath, W_OK )) == ZERO) ;

   return sStats.writeAcc ;

}  //* End WritePermission() *
 
//*************************
//*     createCsvFile     *
//*************************
//********************************************************************************
//* For development:                                                             *
//* ----------------                                                             *
//* Create a sample CSV-format file fromplain-text data.                         *
//* -- Implementation is simplistic; don't expect miracles.                      *
//* -- Source must be a standard (UTF-8) plain text file.                        *
//* -- Any reasonable number of records may be specified.                        *
//* -- Any reasonable number of fields within a record may be specified; however,*
//*    if debugging output is enabled, try to keep total characters per record   *
//*    low so debugging output will be easier to read and interpret.             *
//* -- Secondary delimeter pairs are supported. ( ,"xxxxxx", ) This means that   *
//*    literal commas and literal newlines may be embedded within a field.       *
//*    (See readRecord() for info on how source data within secondary delimeters *
//*     are handled.)                                                            *
//*    -- To specify a literal comma within a field:                             *
//*       a) Enclose the field within a secondary delimeter pair.                *
//*       b) Write the literal comma(s) at any position between the delimeters.  *
//*          Example: >field one,"field, two ,with,commas",field three<          *
//*    -- To specify a literal newline within a field:                           *
//*       a) Enclose the field within a secondary delimeter pair.                *
//*       b) Specify newline(s) using the two-character sequence '\' and 'n'     *
//*          within the text.                                                    *
//*          Example: >field one,"field\ntwo\n with newlines",field three<       *
//*          Note: The 'crlf' argument determines whether the literal newline    *
//*                will be encoded as a CR/LF pair or as an LF only.             *
//*       IMPORTANT NOTE: For the first record of the file (header record) only: *
//*          literal newlines ARE NOT PERMITTED within secondary delimeters.     *
//*          The reason for this is that when the application scans the first    *
//*          record of the file (see SrcScan()), the type of encoding has not    *
//*          yet been determined.                                                *
//*                                                                              *
//* Input  : srcSpec: filespec of source data (plain text)                       *
//*          etype  : member of EncType specifying the encoding for target file  *
//*          crlf   : (optional, 'false' by default)                             *
//*                   if 'true',  replace newline '\n' characters with a         *
//*                               CR/LF pair: "\r\n"                             *
//*                   if 'false', encode newline characters as written           *
//*                                                                              *
//* Returns: 'true' if target file created,                                      *
//*          'false' if invalid parameter(s) or file-creation if error           *
//********************************************************************************
//* Notes;                                                                       *
//* -- For UTF-16LE and UTF-16BE encoding, we use the mbr2c16() library function *
//*    to perform the conversions. See convert2UTF16() method for more info.     *
//********************************************************************************

bool CsvView::createCsvFile ( const char *srcSpec, EncType etype, bool crlf )
{
   const char *encName[] =                // display strings for encoding types
   { "UTF-8", "UTF-8bom", "UTF-16LE", "UTF-16BE", "UNKNOWN" } ;
   gString gsOut, gsTrg ;                 // text formatting
   tnFName tnf ;                          // target file stats
   short ibytes,                          // bytes read by input call
         indx ;                           // character index
   bool done = false,                     // loop control
        status = false ;                  // return value

   this->ComposeTitle ( gsOut, false ) ;
   this->ansi->acEraseArea ( aesERASE_WIN ) ;// clear the terminal window
   this->ansi->acSetCursor ( aesCUR_HOME ) ; // set cursor to origin
   this->ansi->ttyWrite ( gsOut ) ;
   this->ansi->acSetMod ( aesUNDERLINE ) ;
   this->ansi->ttyWrite ( L"\nCreate a Comma-Seperated-Values (CSV) file.\n" ) ;
   this->ansi->acSetMod ( aesUNDERLINE_OFF ) ;

   //* Construct the target filespec *
   gsTrg = srcSpec ;
   if ( (indx = gsTrg.findlast( L'.' )) > ZERO )
      gsTrg.limitChars( indx ) ;
   gsTrg.append( CSV_EXT ) ;

   gsOut.compose( "Source  : %s\n"
                  "Target  : %S\n"
                  "Encoding: %s (crlf:%hhd)\n", 
                  srcSpec, gsTrg.gstr(), encName[etype], &crlf ) ;
   this->ansi->ttyWrite ( gsOut ) ;

   //* Warn user if target file exists *
   if ( (this->GetFileStats ( tnf, gsTrg )) == ZERO )
   {
      this->ansi->acSetMod ( aesREVERSE ) ;
      this->ansi->ttyWrite ( L" Warning! " ) ;
      this->ansi->acSetMod ( aesREVERSE_OFF ) ;
      this->ansi->ttyWrite ( 
         L" Target file exists and will be overwritten. Continue? (y/n)" ) ;
      wchar_t ui = this->ansi->acRead () ;
      if ( (ui != L'y') && (ui != L'Y') )
      {
         this->ansi->ttyWrite ( L"\nOperation aborted.\n\n" ) ;
         return false ;             // note the early return
      }
      this->ansi->ttyWrite ( L'\n' ) ;
   }

   //* Open the source file *
   ifstream ifs( srcSpec, ifstream::in ) ;
   if ( ! (ifs.is_open()) )   // Error opening file (unlikely)
   {
      this->ansi->acSetMod ( aesREVERSE ) ;
      this->ansi->ttyWrite ( L" Error! " ) ;
      this->ansi->acSetMod ( aesREVERSE_OFF ) ;
      this->ansi->ttyWrite ( L" Unable to open the source file. Operation aborted.\n\n" ) ;
      return false ;                // note the early return
   }

   //* Open the target file *
   ofstream ofs( gsTrg.ustr(), ofstream::out | ofstream::trunc ) ;
   if ( (ofs.is_open()) && (ifs.is_open()) )
   {
      uint8_t *ibuff = new uint8_t[ibBYTES] ; // input buffer

      //* If encoding requires a byte-order-mark *
      if ( etype == etUTF16le )
         ofs << UTF_LSB << UTF_MSB ;
      else if ( etype == etUTF16be )
         ofs << UTF_MSB << UTF_LSB ;
      else if ( etype == etUTF8bom )
         ofs << UTF_BOM ;

      while ( ! done )
      {
         ifs.getline( (char*)ibuff, ibBYTES, NEWLINE ) ;
         if ( ((ibytes = ifs.gcount()) > ZERO) || (ifs.good()) )
         {
            if ( ibuff[0] == '#' )        // discard comment lines
               continue ;

            ibuff[ibytes - 1] = NEWLINE ; // append a newline
            ibuff[ibytes] = NULLCHAR ;    // null-terminate the string

            //* Convert embedded literal newlines.               *
            //* Also, if 'crlf' is set, convert terminal newline.*
            ibytes = this->convertNewlines ( ibuff, ibytes, crlf ) ;

            if ( etype == etUTF16le )        // encode for UTF-16LE
               ibytes = this->convert2UTF16 ( ibuff, ibytes, etUTF16le ) ;

            else  if ( etype == etUTF16be )  // encode for UTF-16BE
               ibytes = this->convert2UTF16 ( ibuff, ibytes, etUTF16be ) ;

            else  // ( (etype == etUTF8) || (etype == etUTF8bom) )
            { /* nothing to do */ }

            //* Write the encoded data to target *
            ofs.write ( (char*)ibuff, ibytes ) ;
         }
         else
            done = true ;                 // end-of-file
      }

      ofs.flush() ;                       // flush the output stream
      status = true ;                     // target file has been created
      if ( ibuff != NULL )                // release the dynamic allocation
      { delete [] ibuff ; }
   }
   
   if ( (ifs.is_open()) )                 // close the source file
      ifs.close() ;
   if ( (ofs.is_open()) )                 // close the output file
      ofs.close() ;

   //* Gather and display statistics on the newly-created file.*
   gsTrg.copy( this->orgPath, MAX_PATH ) ;         // copy filespec
   this->GetFileStats ( this->srcStats, gsTrg ) ;  // get stats
   this->SrcScan () ;                              // scan the file
   this->ansi->ttyWrite ( L'\n' ) ;                // breathing room
   this->dumpFileStats ( false ) ;                 // display the results
   this->tfm->deleteTempFile ( this->srcPath ) ;   // delete the temp file

   this->ansi->ttyWrite ( L"\nEncoding complete. Press any key to exit ..." ) ;
   this->ansi->acRead () ;
   this->ansi->ttyWrite ( L"\n\n" ) ;
   return status ;

}  //* End createCsvFile() *

//*************************
//*   captureCsvRecords   *
//*************************
//********************************************************************************
//* For development:                                                             *
//* ----------------                                                             *
//* Capture the header record and the records within the specified range.        *
//* For source records which are causing parsing errors OR are causing ugly      *
//* output, capture the offending records.                                       *
//* -- We duplicate the initial code of the SrcScan() method to capture the      *
//*    header record.                                                            *
//*                                                                              *
//* -- Format the records for analysis and save them to a file in the current    *
//*    directory. Target file is: captured_records                               *
//*                                                                              *
//*                                                                              *
//* Input  : srcSpec: filespec of source data (plain text)                       *
//*          indxA  : index of first record to be captured                       *
//*          indxB  : index of last record to be captured                        *
//*                                                                              *
//* Returns: 'true' if records captured                                          *
//*          'false' if invalid parameter(s) or file-creation if error           *
//********************************************************************************

bool CsvView::captureCsvRecords ( const char *srcSpec, short indxA, short indxB )
{
   const char *capTarget = "captured_records.txt" ;

//   const short ibBYTES = gsALLOCMED ;     // 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
         recIndex = ZERO ;                // index of current record
   bool  done = false,                    // loop control
         status = false ;                 // return value

   gs.compose( "%s/%s", this->pwdPath, capTarget ) ;
   gs.copy( this->trgPath, gsALLOCMIN ) ;
   gs.compose( "Source: '%s'\n"
               "Target: '%s'\n"
               "Capture Records: %hd through %hd\n",
                srcSpec, this->trgPath, &indxA, &indxB ) ;
   this->ansi->ttyWrite ( gs ) ;

   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 ) ;
         }

         this->srcHeader = this->parseHeader ( ibuff, iBytes, this->fldCount ) ;
         ++this->recCount ;
         ibuff[iBytes++] = NEWLINE ;   // restore header record's newline terminator
         ibuff[iBytes]   = NULLCHAR ;

         //* Open the target file. Write the stats and the header record. *
         ofs.open( this->trgPath, ofstream::out | ofstream::trunc ) ;
         if ( ofs.is_open() )
         {
            ofs << gs.ustr() ;
            gs.compose( "Header:%s (%hd bytes), fldCount:%hd\n",
                        (this->srcHeader ? "yes" : "no"), 
                        &iBytes, &this->fldCount ) ;
            this->ansi->ttyWrite ( gs ) ;       // copy to display
            gs.clear() ;
            gs.padCols( 80, L'=' ) ;
            gs.append( NEWLINE ) ;
            this->ansi->ttyWrite ( gs ) ;       // copy to display
            ofs << gs.ustr() << endl ;          // copy to target

            //* Report the contents of the header record *
            gs.compose( "HEADER RECORD - index:%02hd \n", &recIndex ) ;
            this->ansi->acSetMod ( aesUNDERLINE ) ;   // copy to display
            this->ansi->ttyWrite ( gs ) ;
            this->ansi->acSetMod ( aesUNDERLINE_OFF ) ;
            gs.padCols( (gs.gscols() * 2), L'-' ) ;   // copy to target
            gs.append( NEWLINE ) ;
            ofs << gs.ustr() ;
            ccrFormat ( ibuff, iBytes, ofs ) ;

            //* Read records until initial index reached. *
            while ( !done && recIndex < indxA )
            {
               goodread = this->readRecord ( ifs, ibuff, ibBYTES, iBytes ) ;
               if ( goodread && (iBytes > ZERO) )
                  ++recIndex ;
               else  // unexpected end-of-file
                  done = true ;
            }

            //* Report initial indexed record. *
            if ( ! done )
            {
               gs.compose( "RECORD INDEX :%02hd \n", &recIndex ) ;
               this->ansi->acSetMod ( aesUNDERLINE ) ;   // copy to display
               this->ansi->ttyWrite ( gs ) ;
               this->ansi->acSetMod ( aesUNDERLINE_OFF ) ;
               gs.padCols( (gs.gscols() * 2), L'-' ) ;   // copy to target
               gs.append( NEWLINE ) ;
               ofs << gs.ustr() ;
               if ( (this->srcEncode == etUTF16le) || (this->srcEncode == etUTF16be) )
                  this->convert2UTF8 ( ibuff, iBytes, false ) ;
               ccrFormat ( ibuff, iBytes, ofs ) ;
            }

            //* Report remainder of the record sequence. *
            while ( ! done && (recIndex < indxB) )
            {
               goodread = this->readRecord ( ifs, ibuff, ibBYTES, iBytes ) ;
               if ( goodread && (iBytes > ZERO) )
               {
                  ++recIndex ;

                  gs.compose( "RECORD INDEX :%02hd \n", &recIndex ) ;
                  this->ansi->acSetMod ( aesUNDERLINE ) ;   // copy to display
                  this->ansi->ttyWrite ( gs ) ;
                  this->ansi->acSetMod ( aesUNDERLINE_OFF ) ;
                  gs.padCols( (gs.gscols() * 2), L'-' ) ;   // copy to target
                  gs.append( NEWLINE ) ;
                  ofs << gs.ustr() ;
                  if ( (this->srcEncode == etUTF16le) || (this->srcEncode == etUTF16be) )
                     this->convert2UTF8 ( ibuff, iBytes, false ) ;
                  ccrFormat ( ibuff, iBytes, ofs ) ;
               }
               else  // unexpected end-of-file
               { done = true ; }
            }

            //* Report number of records captured and total records scanned. *
            gs.compose( "\nTotal Records Scanned:%hd -- Bye-Bye!\n\n", &recIndex ) ;
            this->ansi->ttyWrite ( gs ) ; // copy to display
         }
      }
      else     // invalid file format
      { this->ansi->ttyWrite ( L"\nError! Source file is not CSV format.\n\n" ) ; }

      ifs.close() ;                       // close the source file
      if ( ofs.is_open() )                // if target file is open, close it
         ofs.close() ;
   }

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

   return status ;

}  //* End captureCsvRecords() *

//*************************
//*       ccrFormat       *
//*************************
//********************************************************************************
//* Non-member method, called by captureCsvRecords().                            *
//*                                                                              *
//*                                                                              *
//* Input  : ibuff  : pointer to source buffer                                   *
//*                   on return, source is formatted to fit the window           *
//*          ibytes : number of source-data bytes                                *
//*          ofs    : (by reference) address of an open output stream            *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

static void ccrFormat ( uint8_t *ibuff, short ibytes, ofstream& ofs )
{
   const short bCHUNK = 40 ;              // bytes read per iteration of loop
   gString gsAsc, gsHex, gs ;             // text formatting
   const wchar_t *wptr ;                  // pointer to wide source data
   const short   *colptr ;                // pointer to column-count array
   int   wchars,                          // number of wide characters
         si = ZERO ;                      // source index

   while ( si < ibytes )
   {
      gs.loadChars( &ibuff[si], bCHUNK ) ;   // load (approx.) number of bytes (whole chars)
      si += gs.utfbytes() - 1 ;              // advance the source index
      wptr = gs.gstr() ;                     // point to the wide data
      colptr = gs.gscols( wchars ) ;         // character-width array
      --wchars ;                             // disregard the null terminator

      gsAsc = L">" ;
      gsHex.clear() ;
      for ( short wi = ZERO ; wi < wchars ; ++wi )
      {
         if ( wptr[wi] < 0x00FF )
         {
            if ( colptr[wi] == 1 )              // single-column character
               gsAsc.append( " %C ", &wptr[wi] ) ;
            else if ( colptr[wi] > 1 )          // _probably_ two-column character
               gsAsc.append( "%C ", &wptr[wi] ) ;
            else                                // zero-width character, probably newline
               gsAsc.append( L"\\n " ) ;
            gsHex.append( " %02X", &wptr[wi] ) ;
         }
         else
         {
            if ( colptr[wi] == 1 )              // single-column character
               gsAsc.append( "   %C ", &wptr[wi] ) ;
            else if ( colptr[wi] > 1 )          // _probably_ two-column character
               gsAsc.append( "  %C ", &wptr[wi] ) ;
            else                                // zero-width character, probably newline
               gsAsc.append( L"  \\n " ) ;
            gsHex.append( " %04X", &wptr[wi] ) ;
         }
      }
      gsAsc.append( NEWLINE ) ;              // terminate the output lines
      gsHex.append( NEWLINE ) ;
      ofs << gsAsc.ustr() << gsHex.ustr() ;
//short b = gsHex.utfbytes() ; gsAsc.compose( "outbytes:%hd\n", &b ) ; ofs << gsAsc.ustr() ;
   }
   ofs << endl ;                          // flush the output buffer

}  //* End ccrFormat() *

//*************************
//*     dumpTermSize      *
//*************************
//********************************************************************************
//* For Debugging Only: Report the dimensions of the terminal window.            *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void CsvView::dumpTermSize ( void )
{
   const wchar_t *boxtop = L"┌───────────────────────────────────────┐\n"
                            "│ " ;
   const wchar_t *boxbot = L"│\n"
                            "└───────────────────────────────────────┘\n" ;
   short termRows, termCols ;
   this->ansi->acGetTermDimensions ( termRows, termCols ) ;
   gString gsOut ;
   this->ComposeTitle ( gsOut, false ) ;
   this->ansi->acSetCursorStyle ( csHide ) ;    // make cursor invisible
   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
   gsOut.compose(                               // format dimensions for display
      "\n%STerminal Size: %2hd rows x %3hd columns  %S\n", 
      boxtop, &termRows, &termCols, boxbot ) ;
   this->ansi->acSetMod ( aesREVERSE ) ;        // reverse fgnd/bgnd
   this->ansi->ttyWrite ( gsOut ) ;             // display the data
   this->ansi->acSetMod ( aesREVERSE_OFF ) ;    // restore fgnd/bgnd
   this->ansi->acSetCursorStyle ( csShow ) ;    // make cursor visible

}  //* End dumpTermSize() *

//*************************
//*     dumpTermInfo      *
//*************************
//********************************************************************************
//* For Debugging Only: Display current terminal configuration.                  *
//*                                                                              *
//* Input  : clr  : if 'true',  clear window and display title                   *
//*                 if 'false', display data at current cursor position          *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void CsvView::dumpTermInfo ( bool clr )
{
   gString gsOut ;
   TermIos tios ;
   this->ansi->acGetTermInfo ( stdIn, tios ) ;

   const wchar_t * en = L"enabled " ;
   const wchar_t * di = L"disabled" ;
   short csize  = short(((tios.c_cflag & CSIZE) >> 4) + 5) ; // (see note above)
   bool istrip  = (tios.c_iflag & ISTRIP),
        opost   = (tios.c_oflag & OPOST),
        onlcr   = (tios.c_oflag & ONLCR),
        xtabs   = (tios.c_oflag & XTABS),
        cread   = (tios.c_cflag & CREAD),
        icanon  = (tios.c_lflag & ICANON),
        echo    = (tios.c_lflag & ECHO),
        echonl  = (tios.c_lflag & ECHONL),
        echoctl = (tios.c_lflag & ECHOCTL),
        iexten  = (tios.c_lflag & IEXTEN) ;

   this->ansi->acSetCursorStyle ( csHide ) ;    // make cursor invisible
   if ( clr )
   {
      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
   }

   gsOut.compose( "Terminal Configuration Flags\n"
                  "---------------------------------------------\n"
                  "istrip : 7-bit input                 %S\n"
                  "opost  : output flags                %S\n"
                  "onlcr  : convert newlines to cr/lf   %S\n"
                  "xtabs  : convert tabs to spaces      %S\n"
                  "cread  : read terminal input         %S\n"
                  "csize  : character width             %hd bits\n"
                  "icanon : enable canonical input      %S\n"
                  "echo   : echo character input        %S\n"
                  "echonl : echo newlines               %S\n"
                  "echoctl: echo control chars as (^x)  %S\n"
                  "iexten : posix extensions            %S\n\n",
                  (istrip ? en : di),
                  (opost ? en : di),
                  (onlcr ? en : di),
                  (xtabs ? en : di),
                  (cread ? en : di),
                  &csize,
                  (icanon ? en : di),
                  (echo ? en : di),
                  ((echonl && icanon) ? en : di),
                  (echoctl ? en : di),
                  (iexten ? en : di) ) ;
   this->ansi->ttyWrite ( gsOut ) ;             // display the data
   this->ansi->acSetCursorStyle ( csShow ) ;    // make cursor visible

}  //* Ed dumpTermInfo() *

//*************************
//*     dumpFileStats     *
//*************************
//********************************************************************************
//* Display the source file's stats and CSV configuration info.                  *
//*                                                                              *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void CsvView::dumpFileStats ( bool clr )
{
   gString gsOut ;

   if ( this->srcStats.fType != fmTYPES )          // if stats have been read
   {
      wchar_t ftype[FI_MAX_FIELDWIDTH + 1] ;       // temp storage file type
      wchar_t fsize[FI_MAX_FIELDWIDTH + 1] ;       // temp storage file size
      gString gsTstamp ;                           // formatted timestamp

      this->ansi->acSetCursorStyle ( csHide ) ;    // make cursor invisible
      if ( clr )
      {
         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
      }

      switch ( this->srcStats.fType )
      {
         case fmREG_TYPE:     gsOut = L"REG" ;     break ;
         case fmDIR_TYPE:     gsOut = L"DIR" ;     break ;
         case fmLINK_TYPE:    gsOut = L"LINK" ;    break ;
         case fmCHDEV_TYPE:   gsOut = L"CHDEV" ;   break ;
         case fmBKDEV_TYPE:   gsOut = L"BKDEV" ;   break ;
         case fmFIFO_TYPE:    gsOut = L"FIFO" ;    break ;
         case fmSOCK_TYPE:    gsOut = L"SOCK" ;    break ;
         default:             gsOut = L"UNKNOWN" ; break ;
      }
      gsOut.copy( ftype, FI_MAX_FIELDWIDTH ) ;
      gsOut.formatInt( this->srcStats.fBytes, FI_MAX_FIELDWIDTH, true ) ;
      gsOut.copy( fsize, FI_MAX_FIELDWIDTH ) ;
      this->FormatDateString ( this->srcStats.modTime, gsTstamp, true ) ;

      gsOut.compose( "Source File Stats\n"
                     "------------------------------------\n"
                     "Filename   : %s\n"
                     "File type  : %S\n"
                     "File bytes : %S\n"
                     "Mod time   : %S\n"
                     "Permissions: r:%hhd w:%hhd\n"
                     , this->srcStats.fName,
                     ftype,
                     fsize,
                     gsTstamp.gstr(),
                     &this->srcStats.readAcc, &this->srcStats.writeAcc
                   ) ;

      //* If analysis of file contents not yet performed, do it now.*
      if ( ! this->srcScanned )
         this->SrcScan () ;
      if ( this->srcCsvFmt )
      {
         gsOut.append( "Encoding   : %s Cr:%s Hdr:%s\n"
                       "Records    : %-4hd     Fields : %-3hd\n",
                       (this->srcEncode == etUTF8 ? "UTF-8   " :
                         this->srcEncode == etUTF8bom ? "UTF-8bom" :
                         this->srcEncode == etUTF16le ? "UTF-16LE" :
                         this->srcEncode == etUTF16be ? "UTF-16BE" : "UNKNOWN"),
                       (this->srcCrlf ? "yes" : "no "),
                       (this->srcHeader ? "yes" : "no "),
                       &this->recCount, &this->fldCount
                     ) ;
      }
      else
         gsOut.append( "Source file is not in CSV format.\n" ) ;

      this->ansi->ttyWrite ( gsOut ) ;             // display the data
      this->ansi->acSetCursorStyle ( csShow ) ;    // make cursor visible
   }

   //* Source file not specified, or file stats not available.*
   else
   {
      this->ansi->ttyWrite ( L"File Stats Not Available\n" ) ;
   }

}  //* Ennd dumpFileStats() *

//*************************
//*     dumpHexRecord     *
//*************************
//********************************************************************************
//* For Debugging Only: Display the specified record as hexidecimal bytes.       *
//*                                                                              *
//* Input  : buff  : pointer to byte data (data must be null terminated)         *
//*          cnt   : number of bytes in 'buff'                                   *
//*          head  : header for output     Example: "abc:"                       *
//*          nl    : (optional, 'true' by default)                               *
//*                  if 'true',  terminate the output with a newline             *
//*                  if 'false', terminate the output and end-of-data            *
//*          hlcnt : (optional, zero by default) number of bytes in hlight       *
//*                  range: 1-4 bytes                                            *
//*          hlight: (optional, null pointer by default)                         *
//*                  pointer to a byte sequence (ignored if hlcnt == zero)       *
//*                  If specified, source sequences which match 'hlight' will    *
//*                  be displayed in an alternate color.                         *
//*                    Examples: hlcnt==2:"\d\n" hlcnt==4:"\d\0\a\0"             *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void CsvView::dumpHexRecord ( const uint8_t *buff, short cnt, const char *head, 
                              bool nl, short hlcnt, const uint8_t *hlight )
{
   const wchar_t * aRev  = L"\x1B[7m" ;   // aesREVERSE escape sequence
   const wchar_t * aNorm = L"\x1B[27m" ;  // aesREVERSE_OFF escape sequence

   gString gsOut( head ) ;                // text formatting
   short headCols = gsOut.gscols(),       // width of header string
         brkCol = this->termCols - headCols - 1,  // line break column
         attrCnt,                         // byte counter for alt attribute
         outCols = ZERO ;                 // output column count
   bool  altAttr ;                        // set if alternate attribute

   //* Leave space for caller's message (4 extra bytes of output).*
   if ( ! nl ) { brkCol -= 12 ; }

   for ( short si = ZERO ; si < cnt ; ++si )
   {
      if ( ! altAttr && (hlcnt > ZERO) && (buff[si] == hlight[0]) )
      {
         altAttr = true ;                 // provisionally enable
         if ( (hlcnt > 1) && (buff[si + 1] != hlight[1]) )   { altAttr = false ; }
         if ( (hlcnt > 2) && (buff[si + 2] != hlight[2]) )   { altAttr = false ; }
         if ( (hlcnt > 3) && (buff[si + 3] != hlight[3]) )   { altAttr = false ; }

         if ( altAttr )
         {
            this->ansi->ttyWrite ( gsOut ) ;    // write the previously formatted data
            outCols = gsOut.gscols() ;          // columns of previous-formatted data
            this->ansi->ttyWrite ( aRev ) ;     // set the highlight attribute
            attrCnt = hlcnt ;                   // number of bytes to be highlighted
            gsOut.clear() ;                     // clear the buffer
            for ( ; attrCnt > ZERO ; ++si )     // format the highlighted data
            {
               gsOut.append( " %02hhX", &buff[si] ) ;
               --attrCnt ;
            }
            this->ansi->ttyWrite ( gsOut ) ;    // write the highlighted data
            outCols += gsOut.gscols() ;         // add columns for highlighted bytes
            gsOut.clear() ;                     // clear the buffer
            this->ansi->ttyWrite ( aNorm ) ;    // reset the highlight attribute
            altAttr = false ;
         }
      }
      gsOut.append( " %02hhX", &buff[si] ) ;
      if ( altAttr && --attrCnt == ZERO )
      {
         this->ansi->ttyWrite ( aNorm ) ;
         altAttr = false ;
      }
      if ( ((gsOut.gscols() + outCols) >= brkCol) || (si == (cnt - 1)) )
      {
         if ( (si != (cnt - 1)) || nl )
            gsOut.append( L'\n' ) ;
         this->ansi->ttyWrite ( gsOut ) ;
         outCols = gsOut.gscols() ;
         gsOut.clear() ;
         gsOut.padCols( headCols, L'-' ) ;
         outCols = ZERO ;
      }
   }

}  //* End dumpHexRecord() *

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

