/**********************************************************************

  Audacity: A Digital Audio Editor

  ProjectFSCK.cpp

  A function that performs consistency checks on the tree of block files

  Paul Licameli split this out of DirManager.cpp

**********************************************************************/

#include "ProjectFSCK.h"

#include <wx/log.h>
#include <wx/string.h>

#include "BlockFile.h"
#include "DirManager.h"
#include "widgets/AudacityMessageBox.h"
#include "Internat.h"
#include "MemoryX.h"
#include "MissingAliasFileDialog.h"
#include "ondemand/ODManager.h"
#include "widgets/MultiDialog.h"
#include "widgets/ProgressDialog.h"

// Check the BlockFiles against the disk state.
// Missing Blockfile data can be regenerated if possible or replaced with silence.
// Orphan blockfiles can be deleted.
// Note that even BlockFiles not referenced by the current savefile (but locked
// by history) will be reflected in the mBlockFileHash, and that's a
// good thing; this is one reason why we use the hash and not the most
// recent savefile.
int ProjectFSCK(
   DirManager &dm, const bool bForceError, const bool bAutoRecoverMode)
{
   // In earlier versions of this method, enumerations of errors were
   // all done in sequence, then the user was prompted for each type of error.
   // The enumerations are now interleaved with prompting, because, for example,
   // user choosing to replace missing aliased block files with silence
   // needs to put in SilentBlockFiles and DELETE the corresponding auf files,
   // so those would then not be cumulated in missingAUFHash.
   // We still do the FindX methods outside the conditionals,
   // so the log always shows all found errors.

   int action; // choice of action for each type of error
   int nResult = 0;

   if (bForceError && !bAutoRecoverMode)
   {
      // TODO: Replace with more user friendly error message?
      /* i18n-hint: The audacity project file is XML and has 'tags' in it,
         rather like html tags <something>some stuff</something>.
         This error message is about the tags that hold the sequence information.
         The error message is confusing to users in English, and could just say
         "Found problems with <sequence> when checking project file." */
      wxString msg = _("Project check read faulty Sequence tags.");
      const wxChar *buttons[] =
         {_("Close project immediately with no changes"),
            _("Continue with repairs noted in log, and check for more errors. This will save the project in its current state, unless you \"Close project immediately\" on further error alerts."),
            NULL};
      wxLog::FlushActive(); // MultiDialog has "Show Log..." button, so make sure log is current.
      action = ShowMultiDialog(msg, _("Warning - Problems Reading Sequence Tags"), buttons);
      if (action == 0)
         nResult = FSCKstatus_CLOSE_REQ;
      else
         nResult = FSCKstatus_CHANGED | FSCKstatus_SAVE_AUP;
   }

   FilePaths filePathArray; // *all* files in the project directory/subdirectories
   auto dirPath = ( dm.GetDataFilesDir() );
   DirManager::RecursivelyEnumerateWithProgress(
      dirPath,
      filePathArray,          // output: all files in project directory tree
      wxEmptyString,          // All dirs
      wxEmptyString,          // All files
      true, false,
      dm.NumBlockFiles(),  // rough guess of how many BlockFiles will be found/processed, for progress
      _("Inspecting project file data"));

   //
   // MISSING ALIASED AUDIO FILES
   //
   MissingAliasFilesDialog::SetShouldShow(false);
   BlockHash missingAliasFilesAUFHash;   // (.auf) AliasBlockFiles whose aliased files are missing
   BlockHash missingAliasFilesPathHash;  // full paths of missing aliased files
   dm.FindMissingAliasFiles(missingAliasFilesAUFHash, missingAliasFilesPathHash);

   if ((nResult != FSCKstatus_CLOSE_REQ) && !missingAliasFilesAUFHash.empty())
   {
      // In auto-recover mode, we always create silent blocks, and do not ask user.
      // This makes sure the project is complete next time we open it.
      if (bAutoRecoverMode)
         action = 2;
      else
      {
         wxString msgA =
_("Project check of \"%s\" folder \
\ndetected %lld missing external audio file(s) \
\n('aliased files'). There is no way for Audacity \
\nto recover these files automatically. \
\n\nIf you choose the first or second option below, \
\nyou can try to find and restore the missing files \
\nto their previous location. \
\n\nNote that for the second option, the waveform \
\nmay not show silence. \
\n\nIf you choose the third option, this will save the \
\nproject in its current state, unless you \"Close \
\nproject immediately\" on further error alerts.");
         wxString msg;
         msg.Printf(msgA, dm.GetProjectName(), (long long) missingAliasFilesPathHash.size());
         const wxChar *buttons[] =
            {_("Close project immediately with no changes"),
               _("Treat missing audio as silence (this session only)"),
               _("Replace missing audio with silence (permanent immediately)."),
               NULL};
         wxLog::FlushActive(); // MultiDialog has "Show Log..." button, so make sure log is current.
         action = ShowMultiDialog(msg, _("Warning - Missing Aliased File(s)"), buttons);
      }

      if (action == 0)
         nResult = FSCKstatus_CLOSE_REQ;
      else
      {
         // LL:  A progress dialog should probably be used here
         BlockHash::iterator iter = missingAliasFilesAUFHash.begin();
         while (iter != missingAliasFilesAUFHash.end())
         {
            // This type cast is safe. We checked that it's an alias block file earlier.
            BlockFilePtr b = iter->second.lock();
            wxASSERT(b);
            if (b) {
               auto ab = static_cast< AliasBlockFile * > ( &*b );

               if (action == 2)
               {
                  // silence the blockfiles by yanking the filename
                  // This is done, eventually, in PCMAliasBlockFile::ReadData()
                  // and ODPCMAliasBlockFile::ReadData, in the stack of b->Recover().
                  // There, if the mAliasedFileName is bad, it zeroes the data.
                  wxFileNameWrapper dummy;
                  dummy.Clear();
                  ab->ChangeAliasedFileName(std::move(dummy));

                  // If recovery fails for one file, silence it,
                  // and don't try to recover other files but
                  // silence them too.  GuardedCall will cause an appropriate
                  // error message for the user.
                  GuardedCall(
                     [&] { ab->Recover(); },
                     [&] (AudacityException*) { action = 1; }
                  );

                  nResult = FSCKstatus_CHANGED | FSCKstatus_SAVE_AUP;
               }

               if (action == 1)
                  // Silence error logging for this block in this session.
                  ab->SilenceAliasLog();
            }
            ++iter;
         }
         if ((action == 2) && bAutoRecoverMode)
            wxLogWarning(_("   Project check replaced missing aliased file(s) with silence."));
      }
   }

   //
   // MISSING ALIAS (.AUF) AliasBlockFiles
   //
   // Alias summary regeneration must happen after checking missing aliased files.
   //
   BlockHash missingAUFHash;              // missing (.auf) AliasBlockFiles
   dm.FindMissingAUFs(missingAUFHash);
   if ((nResult != FSCKstatus_CLOSE_REQ) && !missingAUFHash.empty())
   {
      // In auto-recover mode, we just recreate the alias files, and do not ask user.
      // This makes sure the project is complete next time we open it.
      if (bAutoRecoverMode)
         action = 0;
      else
      {
         wxString msgA =
_("Project check of \"%s\" folder \
\ndetected %lld missing alias (.auf) blockfile(s). \
\nAudacity can fully regenerate these files \
\nfrom the current audio in the project.");
         wxString msg;
         msg.Printf(msgA, dm.GetProjectName(), (long long) missingAUFHash.size());
         const wxChar *buttons[] = {_("Regenerate alias summary files (safe and recommended)"),
                                    _("Fill in silence for missing display data (this session only)"),
                                    _("Close project immediately with no further changes"),
                                    NULL};
         wxLog::FlushActive(); // MultiDialog has "Show Log..." button, so make sure log is current.
         action = ShowMultiDialog(msg, _("Warning - Missing Alias Summary File(s)"), buttons);
      }

      if (action == 2)
         nResult = FSCKstatus_CLOSE_REQ;
      else
      {
         // LL:  A progress dialog should probably be used here
         BlockHash::iterator iter = missingAUFHash.begin();
         while (iter != missingAUFHash.end())
         {
            BlockFilePtr b = iter->second.lock();
            wxASSERT(b);
            if (b) {
               if(action==0) {
                  //regenerate from data
                  // If recovery fails for one file, silence it,
                  // and don't try to recover other files but
                  // silence them too.  GuardedCall will cause an appropriate
                  // error message for the user.
                  GuardedCall(
                     [&] {
                        b->Recover();
                        nResult |= FSCKstatus_CHANGED;
                     },
                     [&] (AudacityException*) { action = 1; }
                  );
               }

               if (action==1){
                  // Silence error logging for this block in this session.
                  b->SilenceLog();
               }
            }
            ++iter;
         }
         if ((action == 0) && bAutoRecoverMode)
            wxLogWarning(_("   Project check regenerated missing alias summary file(s)."));
      }
   }

   //
   // MISSING (.AU) SimpleBlockFiles
   //
   BlockHash missingAUHash;               // missing data (.au) blockfiles
   dm.FindMissingAUs(missingAUHash);
   if ((nResult != FSCKstatus_CLOSE_REQ) && !missingAUHash.empty())
   {
      // In auto-recover mode, we just always create silent blocks.
      // This makes sure the project is complete next time we open it.
      if (bAutoRecoverMode)
         action = 2;
      else
      {
         wxString msgA =
_("Project check of \"%s\" folder \
\ndetected %lld missing audio data (.au) blockfile(s), \
\nprobably due to a bug, system crash, or accidental \
\ndeletion. There is no way for Audacity to recover \
\nthese missing files automatically. \
\n\nIf you choose the first or second option below, \
\nyou can try to find and restore the missing files \
\nto their previous location. \
\n\nNote that for the second option, the waveform \
\nmay not show silence.");
         wxString msg;
         msg.Printf(msgA, dm.GetProjectName(), (long long) missingAUHash.size());
         const wxChar *buttons[] =
            {_("Close project immediately with no further changes"),
               _("Treat missing audio as silence (this session only)"),
               _("Replace missing audio with silence (permanent immediately)"),
               NULL};
         wxLog::FlushActive(); // MultiDialog has "Show Log..." button, so make sure log is current.
         action = ShowMultiDialog(msg, _("Warning - Missing Audio Data Block File(s)"), buttons);
      }

      if (action == 0)
         nResult = FSCKstatus_CLOSE_REQ;
      else
      {
         // LL:  A progress dialog should probably be used here
         BlockHash::iterator iter = missingAUHash.begin();
         while (iter != missingAUHash.end())
         {
            BlockFilePtr b = iter->second.lock();
            wxASSERT(b);
            if (b) {
               if (action == 2)
               {
                  //regenerate from data
                  // If recovery fails for one file, silence it,
                  // and don't try to recover other files but
                  // silence them too.  GuardedCall will cause an appropriate
                  // error message for the user.
                  GuardedCall(
                     [&] {
                        //regenerate with zeroes
                        b->Recover();
                        nResult |= FSCKstatus_CHANGED;
                     },
                     [&] (AudacityException*) { action = 1; }
                  );
               }

               if (action == 1)
                  b->SilenceLog();
            }
            ++iter;
         }
         if ((action == 2) && bAutoRecoverMode)
            wxLogWarning(_("   Project check replaced missing audio data block file(s) with silence."));
      }
   }

   //
   // ORPHAN BLOCKFILES (.au and .auf files that are not in the project.)
   //
   FilePaths orphanFilePathArray;     // orphan .au and .auf files
   dm.FindOrphanBlockFiles(filePathArray, orphanFilePathArray);

   if ((nResult != FSCKstatus_CLOSE_REQ) && !orphanFilePathArray.empty())
   {
      // In auto-recover mode, leave orphan blockfiles alone.
      // They will be deleted when project is saved the first time.
      if (bAutoRecoverMode)
      {
         wxLogWarning(_("   Project check ignored orphan block file(s). They will be deleted when project is saved."));
         action = 1;
      }
      else
      {
         wxString msgA =
_("Project check of \"%s\" folder \
\nfound %d orphan block file(s). These files are \
\nunused by this project, but might belong to \
other projects. \
\nThey are doing no harm and are small.");
         wxString msg;
         msg.Printf(msgA, dm.GetProjectName(), (int)orphanFilePathArray.size());

         const wxChar *buttons[] =
            {_("Continue without deleting; ignore the extra files this session"),
            _("Close project immediately with no further changes"),
            _("Delete orphan files (permanent immediately)"),
            NULL};
         wxLog::FlushActive(); // MultiDialog has "Show Log..." button, so make sure log is current.
         action = ShowMultiDialog(msg, _("Warning - Orphan Block File(s)"), buttons);
      }

      if (action == 1)
         nResult = FSCKstatus_CLOSE_REQ;
      // Nothing is done if (action == 0).
      else if (action == 2)
      {
         // FSCKstatus_CHANGED was bogus here.
         // The files are deleted, so "Undo Project Repair" could not do anything.
         // Plus they affect none of the valid tracks, so incorrect to mark them changed,
         // and no need for refresh.
         //    nResult |= FSCKstatus_CHANGED;
         for ( const auto &orphan : orphanFilePathArray )
            wxRemoveFile(orphan);
      }
   }

   if ((nResult != FSCKstatus_CLOSE_REQ) && !ODManager::HasLoadedODFlag())
   {
      // Remove any empty directories.
      ProgressDialog pProgress
         (_("Progress"),
         _("Cleaning up unused directories in project data"));
      // nDirCount is for updating pProgress. +1 because we may DELETE dirPath.
      int nDirCount = DirManager::RecursivelyCountSubdirs(dirPath) + 1;
      DirManager::RecursivelyRemoveEmptyDirs(dirPath, nDirCount, &pProgress);
   }

   // Summarize and flush the log.
   if (bForceError ||
         !missingAliasFilesAUFHash.empty() ||
         !missingAUFHash.empty() ||
         !missingAUHash.empty() ||
         !orphanFilePathArray.empty())
   {
      wxLogWarning(_("Project check found file inconsistencies inspecting the loaded project data."));
      wxLog::FlushActive(); // Flush is modal and will clear the log (both desired).

      // In auto-recover mode, we didn't do any ShowMultiDialog calls above, so put up an alert.
      if (bAutoRecoverMode)
         ::AudacityMessageBox(
            _("Project check found file inconsistencies during automatic recovery.\n\nSelect 'Help > Diagnostics > Show Log...' to see details."),
            _("Warning: Problems in Automatic Recovery"),
            wxOK  | wxICON_EXCLAMATION);
   }

   MissingAliasFilesDialog::SetShouldShow(true);
   return nResult;
}
