//********************************************************************************
//* File       : AcEditDlg.cpp                                                   *
//* Author     : Mahlon R. Smith                                                 *
//*              Copyright (c) 2023-2026 Mahlon R. Smith, The Software Samurai   *
//*                 GNU GPL copyright notice in AcEdit.cpp.                      *
//* Date       : 02-Jan-2026                                                     *
//* Version    : (see AppVersion string)                                         *
//*                                                                              *
//* Description: Support file for AcEdit class. Dialog windows and user          *
//*              interface support.                                              *
//*                                                                              *
//********************************************************************************

//*****************
//* Include Files *
//*****************
#include "AcEdit.hpp"         //* Application-class definition

//***************
//* Definitions *
//***************
#if DEBUG_ANSICMD != 0 && DEBUG_LOG != 0
extern ofstream ofsdbg ;
extern gString  gsdbg ;
#endif   // DEBUG_ANSICMD && DEBUG_LOG

//**************
//* Local data *
//**************
//* Pointer to AcEdit object i.e. 'this->'    *
//* to handle callbacks from the ACDlg class. *
extern AcEdit *aceditPtr ;
//* Template used to communicate information  *
//* between orplanDialog() and the callback.  *
static const char* odSpecTemplate = "srcSpecs:%p ; bakSpecs:%p" ;

//********************
//* Local prototypes *
//********************
//* Non-member callback function which indirectly *
//* calls a member method to handle callbacks.    *
extern wchar_t nonmember_dlgCallback ( short ctrlIndex, wchar_t wkey, bool firstTime ) ;

//*************************
//*     recoverOrphan     *
//*************************
//********************************************************************************
//* Caller has found an orphaned backup file from a previous session.            *
//* Open a dialog and ask user how to handle the orphaned file.                  *
//*                                                                              *
//* Display:                                                                     *
//*  A) file path                                                                *
//*  B) Source: a) filename   b) mod date   c) file size                         *
//*  C) Orphan: a) filename   b) mod date   c) file size                         *
//* Options:                                                                     *
//*  1) Invoke 'diff' to compare files.                                          *
//*  2) Rename the backup file as an ordinary non-hidden file.                   *
//*     .#source_file.txt#  --> Recovered_01.txt                                 *
//*     This will allow user to open the file for edit.                          *
//*  2) Replace the source with the backup file.                                 *
//*     Original file is renamed: source_file.txt  --> source_file.txt~          *
//*     Then:                     .#source_file.#  --> source_file.txt           *
//*  4) Delete the orphaned file.                                                *
//*                                                                              *
//*                                                                              *
//* Input  : tnfSrc : (by reference) stats for source file                       *
//*          tnfBak : (by reference) stats of orphaned backup file               *
//*                                                                              *
//* Returns: currently always returns 'true'                                     *
//********************************************************************************

bool AcEdit::recoverOrphan ( const tnFName& tnfSrc, const tnFName& tnfBak )
{
   enum fldName
   {
      fldCONT = ZERO,            // continue: execute the selected operation
      fldEXIT,                   // exit the dialog
      fldCOMP,                   // compare source vs orphaned file
      fldSAVE,                   // save orphaned file with rename
      fldREPL,                   // replace source file with orphaned file
      fldTOSS,                   // discard the orphaned file
      fldSPEC,                   // storage for filespecs
      fldCOUNT
   } ;
   static const char* staticText[uilENV] = 
   {  // English
     //123456789 123456789 123456789 123456789 123456789 123456789 
      " \n"
      "Source File    : %s\n"
      "  Last Modified: %S  Size: %S bytes\n"
      "Recovered File : %s\n"
      "  Last Modified: %S  Size: %S bytes\n"
      "─────────── ─────────── ─────────── ─────────── ───────────\n"
      "\n\n\n\n\n"
      "                ==CONTINUE==   ====EXIT==== \n",

      // Zhongwen
      " \n"
      "源文件         : %s\n"
      "  最后修改时间 : %S  尺寸: %S 字节\n"
      "恢复的文件     : %s\n"
      "  最后修改时间 : %S  尺寸: %S 字节\n"
      "─────────── ─────────── ─────────── ─────────── ───────────\n"
      "\n\n\n\n\n"
      "                ==CONTINUE==   ====EXIT==== \n",
   } ;
   static const char* titleText[uilENV] =
   {
      " Recover Data From Abnormal Shutdown ",  // English
      "  从异常关机中恢复数据  ",                         // Zhongwen
   } ;
   static const char* Labels[uilENV][fldCOUNT] =
   {
      {  // English
         " =C^ONTINUE= ",
         " ===E^XIT=== ",
         "[ ] ^Compare the two files.",
         "[ ] ^Save the recovered file as \"Recovered_nn.txt\".",
         "[ ] ^Replace the source file with the recovered file.",
         "[ ] ^Discard the recovered temporary file.",
         "-----"                          // dummy record
      },
      {  // Zhongwen
         "   继续   ^O ",
         "    出口  ^X ",
         "[ ] 比较这两个文件。 ^C",
         "[ ] 将恢复的文件另存为 \"Recovered_nn.txt\"。 ^S",
         "[ ] 用恢复的文件替换源文件。 ^R",
         "[ ] 删除恢复的临时文件。  ^D",
      },
   } ;
   const acAttr pbAttr = acAttr(acaFGb_RED | acaBGb_GREEN) ;      // Pushbutton attribute
   const acAttr focusAttr = acAttr(acaFGb_GREEN | acaBGb_RED) ;   // Focus attribute
   const wchar_t grpChar = L'a' ;         // Radiobutton group identifier
   const short dlgHgt = 15 ;  // height for dialog window (based on static text height)
   const short dlgWid = 64 ;  // width for dialog window (based on static text width)
   const short maxNameWid = dlgWid - 21 ; // space available for filenames.

   gString gsStatic ;                     // static text for dialog interior
   gString srcMod, tmpMod ;               // formatted timestamps
   gString srcBytes, tmpBytes ;           // formatted file size
   gString gs ;
   WinPos wpCtrl( 6, (-(dlgWid / 2) + 3) ) ; // control position
   char statText[64] ;                    // content of textbox (addresses of stat objects)
   char srcFName[maxNameWid + 2] ;        // formatted filenames
   char bakFName[maxNameWid + 2] ;
   bool status = true ;                   // return value
   this->formatTimestamp ( tnfSrc.modTime, srcMod ) ;
   this->formatTimestamp ( tnfBak.modTime, tmpMod ) ;
   srcBytes.formatInt( tnfSrc.fBytes, 6 ) ;
   tmpBytes.formatInt( tnfBak.fBytes, 6 ) ;

   //* It is unlikely, but _possible_ that the filenames *
   //* will be too wide for the space available.         *
   //* If so, names are truncated to fit the space.      *
   gs = tnfSrc.fName ;
   gs.limitCols( maxNameWid ) ;
   gs.copy( srcFName, maxNameWid ) ;
   gs = tnfBak.fName ;
   gs.limitCols( maxNameWid ) ;
   gs.copy( bakFName, maxNameWid ) ;

   //* Format the static text displayed in dialog interior.*
   gsStatic.compose( staticText[this->lang], 
                     srcFName, srcMod.gstr(), srcBytes.gstr(),
                     bakFName, tmpMod.gstr(), tmpBytes.gstr() ) ;

   //* Format the text for storage in the Textbox fldSPEC. (see note below) *
   srcMod.compose( odSpecTemplate, &tnfSrc, &tnfBak ) ;
   srcMod.copy( statText, 64 ) ;

   skForm* skfp = new skForm
   (
      fldCOUNT,
      new skField[fldCOUNT]
      {
         //* Field index fldCONT: ftPB (Pushbutton) *
         {
            ftPB,                         // field type
            1,                            // field height (in rows)
            12,                           // field width (in columns)
            short(dlgHgt - 4), -14,       // Y/X offset in window
            Labels[this->lang][fldCONT],  // field text
            pbAttr,                       // color attributes
            //floChar,                    // text formatting option
            //ZERO,                       // insertion point (cursor offset)
            //false,                      // user accessible (not read-only)
            //false,                      // field outline disabled
            //true,                       // field is visible (not hidden)
            //true,                       // access lock enabled
            //false,                      // binary state 
            //NULLCHAR,                   // no hotkey specified (embedded in text)
            //noGROUP                     // not a group member
         },

         //* Field index fldEXIT: ftPB (Pushbutton) *
         {
            ftPB,                         // field type
            1,                            // field height (in rows)
            12,                           // field width (in columns)
            short(dlgHgt - 4), 1,         // Y/X offset in window
            Labels[this->lang][fldEXIT],  // field text
            pbAttr,                       // color attributes
            //floChar,                    // text formatting option
            //ZERO,                       // insertion point (cursor offset)
            //false,                      // user accessible (not read-only)
            //false,                      // field outline disabled
            //true,                       // field is visible (not hidden)
            //true,                       // access lock enabled
            //false,                      // binary state 
            //NULLCHAR,                   // no hotkey specified (embedded in text)
            //noGROUP                     // not a group member
         },

         //* Field index fldCOMP: ftRB (Radiobutton) *
         {
            ftRB,                         // field type
            1,                            // field height (in rows)
            3,                            // field width (in columns)
            wpCtrl.row, wpCtrl.col,       // Y/X offset in window
            Labels[this->lang][fldCOMP],  // field text
            acaUSEDFLT,                   // color attributes
            floChar,                      // text formatting option
            ZERO,                         // insertion point (n/a)
            false,                        // user accessible (not read-only)
            false,                        // field outline disabled
            false,                        // field is visible (not hidden)
            true,                         // access lock enabled
            true,                         // binary state 
            NULLCHAR,                     // no hotkey specified (embedded in text)
            grpChar                       // not a group member
         },

         //* Field index fldSAVE: ftRB (Radiobutton) *
         {
            ftRB,                         // field type
            1,                            // field height (in rows)
            3,                            // field width (in columns)
            short(wpCtrl.row += 1),       // Y/X offset in window
            wpCtrl.col,
            Labels[this->lang][fldSAVE],  // field text
            acaUSEDFLT,                   // color attributes
            floChar,                      // text formatting option
            ZERO,                         // insertion point (n/a)
            false,                        // user accessible (not read-only)
            false,                        // field outline disabled
            false,                        // field is visible (not hidden)
            true,                         // access lock enabled
            false,                        // binary state 
            NULLCHAR,                     // no hotkey specified (embedded in text)
            grpChar                       // not a group member
         },

         //* Field index fldREPL: ftRB (Radiobutton) *
         {
            ftRB,                         // field type
            1,                            // field height (in rows)
            3,                            // field width (in columns)
            short(wpCtrl.row += 1),       // Y/X offset in window
            wpCtrl.col,
            Labels[this->lang][fldREPL],  // field text
            acaUSEDFLT,                   // color attributes
            floChar,                      // text formatting option
            ZERO,                         // insertion point (n/a)
            false,                        // user accessible (not read-only)
            false,                        // field outline disabled
            false,                        // field is visible (not hidden)
            true,                         // access lock enabled
            false,                        // binary state 
            NULLCHAR,                     // no hotkey specified (embedded in text)
            grpChar                       // not a group member
         },

         //* Field index fldTOSS: ftRB (Radiobutton) *
         {
            ftRB,                         // field type
            1,                            // field height (in rows)
            3,                            // field width (in columns)
            short(wpCtrl.row += 1),       // Y/X offset in window
            wpCtrl.col,
            Labels[this->lang][fldTOSS],  // field text
            acaUSEDFLT,                   // color attributes
            floChar,                      // text formatting option
            ZERO,                         // insertion point (n/a)
            false,                        // user accessible (not read-only)
            false,                        // field outline disabled
            false,                        // field is visible (not hidden)
            true,                         // access lock enabled
            false,                        // binary state 
            NULLCHAR,                     // no hotkey specified (embedded in text)
            grpChar                       // not a group member
         },

         //* Field index fldSPEC: ftTB (Textbox) *
         // Programmer's Note: This control is set as both Hidden and Read-only.
         // It is used only to provide the callback function with pointers to 
         // file stat information for the two files being analyzed.
         // Yes, it is too cute by half, but it is an interesting experiment.
         {
            ftTB,                         // field type
            1,                            // field height (in rows)
            dlgWid - 2,                   // field width (in columns)
            ZERO, ZERO,                   // Y/X offset in window
            statText,                     // field text
            acaUSEDFLT,                   // color attributes
            floShift,                     // text formatting option
            ZERO,                         // insertion point (n/a)
            true,                         // set as read-only
            false,                        // field outline disabled
            true,                         // field is set invisible
            false,                        // access visibility lock disabled
//          false,                        // binary state 
//          NULLCHAR,                     // no hotkey specified (embedded in text)
//          noGROUP                       // not a group member
         },
      },
      //* Initialize remaining skForm parameters to default values.*
      ZERO,             // index of field with initial focus
      focusAttr         // default color attributes for control with focus
      //true,           // beep on unhandled keycode received
      //true,           // initial state of Insert/Overstrike flag
      //true,           // user access to Insert/Overstrike mode
      //false,          // enable cursor shape change for Insert/Overstrike
      //csBblock,       // cursor shape when Insert mode
      //csBuline        // cursor shape when Overstrike mode
   ) ;

   aceditPtr = this ;
   this->adlg = adOrphan ;    // indicate the active dialog (used by callback)

   //*******************
   //* Open the dialog *
   //*******************
   //* Launch the dialog and interact with user. *
   this->win->Dialog ( dlgHgt, dlgWid, skfp, 
                       gsStatic.ustr(), titleText[this->lang],
                       2, -1, ltSINGLE, acaUSEDFLT, this->dlgAttr, 
                       0, 1, nonmember_dlgCallback ) ;

   this->adlg = adNONE ;      // dialog inactive
   aceditPtr = NULL ;

   return status ;

}  //* End recoverOrphan() *

//*************************
//*      rorCallback      *
//*************************
//********************************************************************************
//* Callback method used by the "recoverOrphan" dialog.                          *
//*                                                                              *
//* Input  : ctrlIndex: index of field/control which currently has input focus   *
//*          wkey     : user's key input                                         *
//*          firstTime: The Dialog() method calls this method once with          *
//*                     firstTime==true, to perform any required initialization. *
//*                     All subsequent calls are made with firstTime==false.     *
//*                                                                              *
//* Returns: if keycode handled, returns the null character: L'\0'               *
//*          if kecode is not handled by this method: 'wkey' is returned.        *
//*          Special Case: returns wcsCTL_4 to signal that EditDialog() should   *
//*                        terminate the loop and return to caller.              *
//*                        Note that wcsCTL_4 is the ASCII code for the SIGQUIT  *
//*                        signal, and will never be sent as the 'wkey' value.   *
//********************************************************************************
//* Notes:                                                                       *
//* a) The static variables for message position and attribute should ideally    *
//*    be set on the first call; however, the dialog is not yet open at that     *
//*    point, so it must wait for the second call.                               *
//* b) The stat structures for the source and recovered backup files are         *
//*    referenced by pointers embedded within the dialog's Textbox (fldSPEC).    *
//*    Note that this control is both read-only and hidden. Its only use is      *
//*    to provide pointers to the relevant tnFName objects which describe        *
//*    the files being analyzed.                                                 *
//* c) The signal to close the dialog is generated either by user pressing the   *
//*    'Exit' pushbutton, OR when the selected operation has been completed.     *
//*                                                                              *
//*                                                                              *
//********************************************************************************

wchar_t AcEdit::rorCallback ( short ctrlIndex, wchar_t wkey, bool firstTime )
{
   enum fldName   // Note: This mirrors the enum in recoverOrphan(). Keep in synch.
   {
      fldCONT = ZERO,            // continue: execute the selected operation
      fldEXIT,                   // exit the dialog
      fldCOMP,                   // compare source vs orphaned file
      fldSAVE,                   // save orphaned file with rename
      fldREPL,                   // replace source file with orphaned file
      fldTOSS,                   // discard the orphaned file
      fldSPEC,                   // storage for filespecs
      fldCOUNT
   } ;
   static const char* msgSuccess[uilENV][fldCOUNT] =
   {
      {  // English
         "-------",              // dummy record
         "-------",              // dummy record
         "File comparison complete.",
         "Recovered file renamed as '%s'",
         "Source file replaced by recovered file.",
         "Recovered temporary file has been deleted."
      },
      {  // Zhongwen
         "-------",              // dummy record
         "-------",              // dummy record
         "文件比较完成。",
         "恢复的文件重命名为 '%s'",
         "源文件被恢复的文件替换。",
         "恢复的临时文件已被删除。"
      },
   } ;
   static const char* msgErrors[uilENV][fldCOUNT] =
   {
      {  // English
         "-------",              // dummy record
         "-------",              // dummy record
         "Error! - File comparison failed.",
         "Error! - Unable to rename recovered file.",
         "Error! - Unable to update source file.",
         "Error! - Unable to delete recovered temporary file."
      },
      {  // Zhongwen
         "-------",              // dummy record
         "-------",              // dummy record
         "错误！ - 文件比较失败。",
         "错误！ - 无法重命名恢复的文件。",
         "错误！ - 无法更新源文件。",
         "错误！ - 无法删除恢复的临时文件。"
      },
   } ;
   static const char* msgClose[uilENV] =
   {
      " PRESS ANY KEY TO EXIT ", // English
      " 按任意键退出 ",                // Zhongwen
   } ;
   const char* nameTemplate = "%s/Recovered_%02hd.txt" ;


   static WinPos msgPos ;                 // message position
   static acAttr msgAttr ;                // message attributes
   static tnFName *srcStats, *bakStats ;  // pointers to source and backup file stats
   const int exitWid = 50 ;               // width of exit message (centered)
   gString gs, 
           trgSpec ;
   tnFName trgStats ;
   char    trgPath[PATH_MAX] ;
   short   indx ;
   bool    done = false ;

   //*------------*
   //* First call *
   //*------------*
   if ( firstTime )
   { /* nothing to do at this time */ }
   else if ( msgPos.row == ZERO )         // if not yet initialized (see note above)
   {
      const skField *fptr ;
      bool gf = false ;
      if ( (gf = aceditPtr->win->Dialog_GetField ( fldTOSS, fptr )) )
      {
         msgPos = { short(fptr->orig.row + 1), short(fptr->orig.col + 3) } ;
         msgAttr = acAttr(acaFG_BLUE | acaBOLD | fptr->aca.acaBgnd) ;
      }
      //* Retrieve and store pointers to stat objects *
      aceditPtr->win->Dialog_GetText ( fldSPEC, gs ) ;
      gs.gscanf( odSpecTemplate, &srcStats, &bakStats ) ;
   }


   //* Note that the hotkey for a field has been automagically  *
   //* converted to wcsENTER.                                   *
   if ( (wkey == wcsENTER) || (wkey == wcsSPACE) )
   {
      switch ( ctrlIndex )
      {
         case fldCONT:                    // Continue Pushbutton
            //* Get active XOR Group member *
            aceditPtr->win->Dialog_GetStateXOR ( fldCOMP, indx ) ;
            switch ( indx )
            {
               case fldCOMP:              // compare the files
                  {
                  gString gsLog ;
                  if ( (this->win->acCreateTempfile ( gsLog )) )
                  {
                     this->compareFiles ( *srcStats, *bakStats, gsLog, L'L' ) ;
                     gs.compose( "less -c '%S'", gsLog.gstr() ) ;
                     this->win->ShellOut ( gs ) ;
                     this->win->Dialog_Refresh ( true ) ;
                     
                     aceditPtr->win->Dialog_Write ( msgSuccess[this->lang][fldCOMP], 
                                       msgPos.row, msgPos.col, true, msgAttr ) ;
                     this->win->acDeleteTempfile ( gsLog ) ;
                  }
                  else
                     aceditPtr->win->Dialog_Write ( msgErrors[this->lang][fldCOMP], 
                                       msgPos.row, msgPos.col, true, msgAttr ) ;
                  }
                  break ;

               case fldSAVE:              // save with rename
                  gs = bakStats->fSpec ;  // extract target path
                  this->ExtractPathname ( trgPath, gs ) ;

                  //* Calculate a serialized name for the file *
                  //* using the template to avoid duplication. *
                  indx = 1 ;
                  do
                  {
                     trgSpec.compose( nameTemplate, trgPath, &indx ) ;
                     ++indx ;             // advance the serial number
                  }
                  while ( (this->GetFileStats ( trgStats, trgSpec )) == ZERO ) ;

                  //* Rename the recovered backup file *
                  if ( (this->RenameFile ( bakStats->fSpec, trgSpec.ustr() )) == ZERO )
                  {
                     this->ExtractFilename ( trgStats.fName, trgSpec ) ;
                     gs.compose( msgSuccess[this->lang][fldSAVE], trgStats.fName ) ;
                     aceditPtr->win->Dialog_Write ( gs.gstr(),
                                       msgPos.row, msgPos.col, true, msgAttr ) ;
                     done = true ;        // signal that dialog is to be closed
                  }
                  else
                     gs = msgErrors[this->lang][fldSAVE] ;
                  aceditPtr->win->Dialog_Write ( gs.gstr(),
                                    msgPos.row, msgPos.col, true, msgAttr ) ;
                  break ;

               case fldREPL:              // replace source file with recovered file
                  //* Rename the source file as a "backup" file *
                  gs.compose( "%s~", srcStats->fSpec ) ;
                  if ( (this->RenameFile ( srcStats->fSpec, gs.ustr() )) == ZERO )
                  {
                     //* If rename of source is successful, rename *
                     //* the recovered backup file as source file. *
                     if ( (this->RenameFile ( bakStats->fSpec, srcStats->fSpec )) == ZERO )
                     {
                        gs = msgSuccess[this->lang][fldREPL] ;
                        done = true ;     // signal that dialog is to be closed
                     }
                     //* If the second rename fails, then there will be *
                     //* no file with the source-file name; so          *
                     //* restore the original name of the source file.  *
                     else
                     {
                        this->RenameFile ( gs.ustr(), srcStats->fSpec ) ;
                        gs = msgErrors[this->lang][fldREPL] ;
                     }
                     aceditPtr->win->Dialog_Write ( gs.gstr(),
                                       msgPos.row, msgPos.col, true, msgAttr ) ;
                  }
                  else
                     aceditPtr->win->Dialog_Write ( msgErrors[this->lang][fldREPL],
                                       msgPos.row, msgPos.col, true, msgAttr ) ;
                  break ;

               case fldTOSS:              // discard the orphaned file
                  if ( (this->DeleteFile ( bakStats->fSpec )) == ZERO )
                  {
                     gs = msgSuccess[this->lang][fldTOSS] ;
                     done = true ;        // signal that dialog is to be closed
                  }
                  else
                     gs = msgErrors[this->lang][fldTOSS] ;
                  aceditPtr->win->Dialog_Write ( gs.gstr(),
                                    msgPos.row, msgPos.col, true, msgAttr ) ;
                  break ;

               default: break ;           // should never happen
            }

            //* If operation complete, display the Exit message *
            //* and wait for user response.                     *
            if ( done )
            {
               aceditPtr->win->Dialog_SetFocus ( fldEXIT ) ;
               msgAttr = acAttr(msgAttr | acaREVERSE) ;
               gs = msgClose[this->lang] ;
               gs.padCols( exitWid, L' ', true ) ;
               aceditPtr->win->Dialog_Write ( gs.gstr(),
                                 msgPos.row + 2, msgPos.col, true, msgAttr ) ;
               aceditPtr->win->acPause () ;
               wkey = wcsCTL_4 ;
            }
            break ;

         case fldEXIT:                    // close the dialog
            //* If user was unable to make a decision, then *
            //* silently delete the orphaned file. Even if  *
            //* it were not deleted here, it would be       *
            //* overwritten later in the app initialization.*
/* TEMP */ ofsdbg << "fldEXIT - silently delete orphaned file." << endl ;
            this->DeleteFile ( bakStats->fSpec ) ;
            wkey = wcsCTL_4 ;
            break ;
         default:                         // (should never happen)
            break ;
      } ;
   }

   return wkey ;

}  //* End rorCallback() *

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

