//********************************************************************************
//* File       : TFMan.hpp                                                       *
//* Author     : Mahlon R. Smith                                                 *
//*              Copyright (c) 2023-2026 Mahlon R. Smith, The Software Samurai   *
//*                 GNU GPL copyright notice below                               *
//* Date       : 27-Dec-2025                                                     *
//* Version    : (see tfm_Version string below)                                  *
//*                                                                              *
//* Description: TFMan class definition.                                         *
//*              Temporary-File Manager creates manages and deletes temporary    *
//*              files on demand. The files are created in the system temp-file  *
//*              directory and are grouped within a temporary subdirectory       *
//*              within that directory. Deleting the TFMan object removes any    *
//*              temp files that the application may have forgotten to delete,   *
//*              so the application will always exit cleanly.                    *
//*                                                                              *
//*                                                                              *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//* Query the system for the temporary-directory path:                           *
//* --------------------------------------------------                           *
//* The temp_directory_path() functions requires C++17 support.                  *
//*                                                                              *
//* From <http://en.cppreference.com>                                            *
//* .................................                                            *
//* These functions are available in C++17 (released Jan. 2018):                 *
//* const char* fs::temp_directory_path ( void ) ;                               *
//* const char* fs::temp_directory_path ( std::error_code& ec ) ;                *
//*                                                                              *
//* These functions return the filespec of a directory suitable for temporary    *
//* files. The path is guaranteed to exist and to be a directory. The overload   *
//* that takes error_code& argument returns an empty path on error.              *
//*                                                                              *
//* On POSIX systems, the path may be specified by any of several system-specific*
//* environment variables, and if none of them are specified, the path "/tmp"    *
//* is returned by the system call.                                              *
//* .................................                                            *
//* In the author's FileMangler application, which was developed well before     *
//* C++17 was released, we implemented a _literal_ interpretaton of the above    *
//* description. Now that the development community has had time to upgrade      *
//* their compilers, (gcc v:8 and above) we are able to use the library function *
//* to obtain the temp-directory path.                                           *
//*                                                                              *
//* Note that the TFMan class does not take the system's assurance that the      *
//* target exists and is a directory. This information is independently          *
//* verified including user's read/write access to the directory.                *
//*                       - - - - - - - - - - - - -                              *
//*                                                                              *
//* Requesting a unique, hack-resistant filename for a temporary file.           *
//* ------------------------------------------------------------------           *
//* To create a subdirectory within the system temp-file directory:              *
//*   char * mkdtemp (char *Template);                                           *
//* This function returns the uniquely-named directory in which all other        *
//* application temporary files will be located.                                 *
//* This makes cleanup of temporary file during application exit as simple as    *
//* calling the TFMan destructor.                                                *
//* a) The 'mkdtemp' function returns a random sequence of characters for the    *
//*    filename: Example: "sakJhr"                                               *
//* b) A constant prefix is attached to the provided character sequence to       *
//*    enable easy identification of the subdirectory during application         *
//*    development. Example: "ANSICMD_sakJhr".                                   *
//* c) An alternate prefix string may be specified using the 'prefix' parameter. *
//*    Example prefix: "SAM" would yield the subdirectory name: "SAMsakJhr".     *
//*                                                                              *
//* It is assumed that only non-special filename characters will be used on the  *
//* temp-file-path. "Special characters" are those which may be interpreted      *
//* by the shell or other system functionality as requiring special handling.    *
//* Such special character should not be seen in this class. The algorithm for   *
//* escaping these special characters is slow and probably not worth the         *
//* complexity in this context.                                                  *
//* Please see notes in the author's FileMangler application. FMgr-class on      *
//* using 'rmdir' and 'unlink' on filenames which contain "special" characters.  *
//*                                                                              *
//* The library function used to create the individual temp files is:            *
//*   int mkstemp (char *Template);                                              *
//*                                                                              *
//* Note that both mkdtemp() and mkstemp() are "atomic", that is, file           *
//* creation by one process may not be interrupted by another process.           *
//*                       - - - - - - - - - - - - -                              *
//*                                                                              *
//* Within the application's temp-file subdirectory, temporary files take the    *
//* form: "TF_xxxxxx" where "TF_" is the specified prefix and the "xxxxxx"       *
//* sequence is replaced by the unique system-provided sequence.                 *
//*                                                                              *
//*                       - - - - - - - - - - - - -                              *
//* Note that the TFMan class uses the "statx" function to gather stats on       *
//* target files. This replaces the older "stat64" function.                     *
//* "statx" provides more information and more options flexibility, although     *
//* neither the extra information nor the extra options are used here.           *
//* This modification is done to remain in synch with the author's other projects*
//* which have transitioned to the advanced capability of the statx structure.   *
//********************************************************************************
//* Version History:                                                             *
//* ================                                                             *
//* v: 0.00.02 27-Dec-2025                                                       *
//*    -- Improve efficiency and add version report. This was done during        *
//*       development of the TFMTest demo app which exercises all functionality  *
//*       of this class.                                                         *
//*    -- Clean up the comments and remove the debugging code.                   *
//*    -- Move class documentation to it own chapter of the AnsiCmd docs.        *
//*                                                                              *
//* v: 0.00.01 24-Mar-2025                                                       *
//*    -- First effort. Based on the temp-file code of the author's FileMangler  *
//*       application, and refined during development of the AcEdit test app     *
//*       in the AnsiCmd package.                                                *
//*                                                                              *
//********************************************************************************

//****************
//* Header Files *
//****************

using namespace std ;         //* C++ Scope quailfier
#include <filesystem>         //* filesystem (C++17)
#include <fstream>            //* File I/O streams
#include <fcntl.h>            //* statx bitfield definitions
#include <sys/stat.h>         //* file stats (statx function, etc.)
#include <dirent.h>           //* manage directory entries
#include "gString.hpp"        //* Text analysis and formatting

//*******************
//* Constant Values *
//*******************

const char* const tfm_Version = "0.0.02" ;


typedef struct statx FileStats ;    //* C++ syntax for statx
typedef struct dirent64 deStats ;   //* C++ syntax for dirent64

//* Define the bitfields for statx() call:                  *
//* a) sx_dirfd : if absolute path, follow it, if relative  *
//*    path it is relative to CWD.                          *
//* b) sx_flags : target must have been previously mounted, *
//*    do not follow symlinks, accept the local system's    *
//*    stat info even if a remote system's stats may exist. *
//* c) sx_lflags: target must have been previously mounted, *
//*    follow symlink(s) to target, accept the local        *
//*    system's stat info (even if newer remote system's    *
//*    stats may exist).                                    *
//* d) sx_mask  : Report all available data _including_     *
//*    creation timestamp (if available).                   *
//* Note: The AT_STATX_SYNC_AS_STAT and AT_STATX_DONT_SYNC  *
//* bits may be in conflict; however, the documentation     *
//* is far from clear on this.                              *
const int sx_dirfd  = AT_FDCWD ;
const int sx_flags  = (AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW | AT_STATX_SYNC_AS_STAT) ;
const int sx_lflags = (AT_NO_AUTOMOUNT | AT_STATX_SYNC_AS_STAT) ;
const unsigned int sx_mask = (STATX_BASIC_STATS | STATX_BTIME) ;

class TFMan
{
   public:
   ~TFMan ( void )
   {
      //* First scan the target directory for its contents *
      //* and delete each remaining temp file (if any).    *
      //* Then delete the directory itself.                *
      if ( (this->clearTempFiles ()) )
         rmdir ( this->tfPath ) ;
   }  // destructor

   //* Default constructor.                                                *
   //* (Prefix for temp-file subdirectory name is set to default value.)   *
   //* Input  : none                                                       *
   //* Returns: implicitly returns pointer to object                       *
   TFMan ( void )
   {
      //* Default sub-directory name prefix *
      const char* const dfltPrefix = "TFMAN_" ;
      this->reset ( dfltPrefix ) ;
      this->createTempPath () ;
   }  // default constructor

   //* Initialization constructor                                          *
   //* Input  : prefix : pointer to a prefix for temp-file subdirectory    *
   //*                   name.                                             *
   //*                   -- Valid filename characters only                 *
   //*                   -- Maximum of 16 bytes including null terminator  *
   //* Returns: implicitly returns pointer to object                       *
   TFMan ( const char* prefix )
   {
      this->reset ( prefix ) ;
      this->createTempPath () ;
   }  // initialization constructor

   //* Create a temporary file.                                            *
   //* Input  : tmpPath : (by reference) gString object to receive filespec*
   //* Returns: 'true'  if file successfully created                       *
   //*                  file exists, and contains no data                  *
   //*          'false' if library call failed                             *
   bool createTempFile ( gString& tmpPath )
   {
      const char* mksTemplate = "XXXXXX" ;   // 'mkstemp' requires a 6-char placeholder
      char tn[PATH_MAX] ;                    // buffer to receive created temp filename
      gString gstmp( "%s/TF_%s", this->tfPath, mksTemplate ) ;
      gstmp.copy( tn, PATH_MAX ) ;
      int  descriptor ;                      // file descriptor for opened temp file
      bool status = false ;                  // return value

      tmpPath.clear() ;                      // initialize caller's buffer
      if ( (descriptor = mkstemp ( tn )) != (-1) ) // create the temp file
      {
         close ( descriptor ) ;              // close the file
         tmpPath = tn ;                      // copy target path to caller's buffer
         status = true ;                     // declare success
      }
      return status ;
   }  //* End createTempFile() *

   //* Delete a temporary file.                                            *
   //* Input  : tmpPath : (by reference) filespec of file to be deleted    *
   //* Returns: 'true'  if file successfully deleted                       *
   //*          'false' if library call failed (invalid filespec)          *
   bool deleteTempFile ( const gString& tmpPath )
   {
      bool status = false ;
      if ( ((tmpPath.find( this->tfPath )) == 0) &&
           ((unlink ( tmpPath.ustr() )) == 0) )
         status = true ;
      return status ;
   }  //* End deleteTempFile() *

   //* Delete all files in the application's temp-file subdirectory.       *
   //* Only files created by the TFMan class are affected.                 *
   //* Input  : none                                                       *
   //* Returns: 'true'  if all target files successfully deleted           *
   //*          'false' if unable to delete one or more target files       *
   bool clearTempFiles ( void )
   {
      const char PERIOD  = '.' ;
      bool status = false ;
      DIR* dirPtr = NULL ;
      if ( (dirPtr = opendir ( this->tfPath )) != NULL )
      {
         gString   tspec ;             // target filespec
         deStats*  destat ;            // directory-entry record
         short     idCount = 0,        // number of temp files found
                   rmCount = 0 ;       // number of temp files deleted
         bool      trgFile ;           // 'true' if file is to be deleted

         //* Read each item in the directory *
         while ( (destat = readdir64 ( dirPtr )) != NULL )
         {
            //* Do not include 'current dir' and 'parent dir' names.              *
            trgFile = true ;
            if ( destat->d_name[0] == PERIOD )
            {
               if (   destat->d_name[1] == '\0' 
                   || (destat->d_name[1] == PERIOD && destat->d_name[2] == '\0') )
                  trgFile = false ;
            }

            if ( trgFile != false )
            {
               ++idCount ;                // count of files identified

               //* Build full path/filename for target, then delete the file.*
               // Programmer's Note: The filetype test uses the compiler's 
               // primitive: DT_DIR: (unsigned char)(4).
               tspec.compose( "%s/%s", this->tfPath, destat->d_name ) ;
               if ( destat->d_type == DT_DIR )
               {
                  // Programmer's Note: At this time the creation of 
                  // subdirectories within our tempfile directory is not 
                  // supported, so this is an unnecessary step.
                  if ( (rmdir ( tspec.ustr() )) == 0 ) // remove the directory
                     ++rmCount ;
               }
               else
               {
                  if ( (unlink ( tspec.ustr() )) == 0 )
                     ++rmCount ;
               }
            }
         }
         closedir ( dirPtr ) ;      // Close the directory

         if ( rmCount == idCount )
            status = true ;
      }
      return status ;
   }  //* End clearTempFiles() *

   //* Get a pointer to the filespec of temp-file subdirectory.            *
   //* Input  : none                                                       *
   //* Returns: pointer to const buffer containing filespec                *
   const char* getTempPath ( void )
   { return ( this->tfPath ) ; }

   //* Report the active/inactive status.                                  *
   //* Input  : none                                                       *
   //* Returns: 'true'  if temp-file manager initialized and functional    *
   //*          'false' if initialization failed                           *
   bool isActive ( void )
   { return this->active ; }

   //* Report the TFMan-class version number.                              *
   //* Input  : none                                                       *
   //* Returns: pointer to constant string of the form: "0.0.00"           *
   //*          indicating the major/minor/step versions                   *
   const char* tfmVersion ( void )
   { return tfm_Version ; }

   private:

   //* Initialize data members.                                            *
   //* Input  : none                                                       *
   //* Returns: nothing                                                    *
   void reset ( const char* prefix )      // initialize data members
   {
      gString gs( prefix ) ;
      gs.copy( this->tfPrefix, 16 ) ;
      *this->tfPath = '\0' ;              // buffer set to empty string
      this->active = false ;              // reset the flag
   }

   //* Create the subdirectory which will contain application's temp files *
   //*                                                                     *
   //* Input  : none                                                       *
   //* Returns: 'true'  if path/dirname created successfully               *
   //*          'false' if library call failed (unlikely)                  *
   bool createTempPath ( void )
   {
      FileStats rawStats ;             // target file stats

      //* Get the filespec of the system temporary-file directory *
      std::filesystem::path p = std::filesystem::temp_directory_path () ;
      gString tdPath( p.c_str() ) ;    // temp-directory filespec

      //* Verify that this is a directory, and that user has   *
      //* read/write access. If so, create a subdirectory for  *
      //* the exclusive used of this application.              *
      //* The bitfields for statx() call are defined above.    *
      if ( (statx ( 0, tdPath.ustr(), sx_lflags, sx_mask, &rawStats )) == 0 )
      {
         if ( ((S_ISDIR(rawStats.stx_mode)) != false) &&
              ((access ( tdPath.ustr(), R_OK )) == 0) && 
              ((access ( tdPath.ustr(), W_OK )) == 0) )
         {
            char tn[PATH_MAX] ;        // receives the particularized filespec
            gString fullPath( "%S/%sXXXXXX", tdPath.gstr(), this->tfPrefix ) ;
            fullPath.copy( tn, PATH_MAX ) ;

            if ( (mkdtemp ( tn )) != NULL )  // create the subdirectory
            {
               fullPath = tn ;
               fullPath.copy( this->tfPath, PATH_MAX ) ;
               this->active = true ;
            }
            else     // error creating subdirectory - used base temp directory
               tdPath.copy( this->tfPath, PATH_MAX ) ;
         }
      }
      return this->active ;
   }  //* End createTempPath() *

   //** Data Members **
   char tfPath[PATH_MAX] ;
   char tfPrefix[16] ;
   bool active ;

} ;
