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

Audacity: A Digital Audio Editor

AutoRecoveryDialog.cpp

Paul Licameli split from AutoRecovery.cpp

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

#include "AutoRecoveryDialog.h"

#include "ActiveProjects.h"
#include "ProjectManager.h"
#include "ProjectFileIO.h"
#include "ProjectFileManager.h"
#include "shuttle/ShuttleGui.h"
#include "TempDirectory.h"
#include "widgets/AudacityMessageBox.h"
#include "widgets/wxPanelWrapper.h"

#include <wx/dir.h>
#include <wx/evtloop.h>
#include <wx/filefn.h>
#include <wx/filename.h>
#include <wx/dataview.h>

enum {
   ID_QUIT_AUDACITY = 10000,
   ID_DISCARD_SELECTED,
   ID_RECOVER_SELECTED,
   ID_SKIP,
   ID_FILE_LIST
};

class AutoRecoveryDialog final : public wxDialogWrapper
{
public:
   explicit AutoRecoveryDialog(TenacityProject *proj);

   bool HasRecoverables() const;
   FilePaths GetRecoverables();

private:
   void Populate(ShuttleGui &S);
   void PopulateList();
   bool HaveChecked();

   void OnQuitAudacity(wxCommandEvent &evt);
   void OnDiscardSelected(wxCommandEvent &evt);
   void OnRecoverSelected(wxCommandEvent &evt);
   void OnSkip(wxCommandEvent &evt);

   FilePaths mFiles;
   wxDataViewListCtrl *mFileList;
   TenacityProject *mProject;

public:
   DECLARE_EVENT_TABLE()
};

BEGIN_EVENT_TABLE(AutoRecoveryDialog, wxDialogWrapper)
   EVT_BUTTON(ID_QUIT_AUDACITY, AutoRecoveryDialog::OnQuitAudacity)
   EVT_BUTTON(ID_DISCARD_SELECTED, AutoRecoveryDialog::OnDiscardSelected)
   EVT_BUTTON(ID_RECOVER_SELECTED, AutoRecoveryDialog::OnRecoverSelected)
   EVT_BUTTON(ID_SKIP, AutoRecoveryDialog::OnSkip)
END_EVENT_TABLE()

AutoRecoveryDialog::AutoRecoveryDialog(TenacityProject *project)
:  wxDialogWrapper(nullptr, wxID_ANY, XO("Automatic Crash Recovery"),
                   wxDefaultPosition, wxDefaultSize,
                   (wxDEFAULT_DIALOG_STYLE & (~wxCLOSE_BOX)) | wxRESIZE_BORDER), // no close box
   mProject(project)
{
   SetName();
   ShuttleGui S(this, eIsCreating);
   Populate(S);
}

bool AutoRecoveryDialog::HasRecoverables() const
{
   return mFiles.size() > 0;
}

FilePaths AutoRecoveryDialog::GetRecoverables()
{
   return mFiles;
}

void AutoRecoveryDialog::Populate(ShuttleGui &S)
{
   S.SetBorder(5);
   S.StartVerticalLay(wxEXPAND, 1);
   {
      S.AddFixedText(
         XO("The following projects were not saved properly the last time Tenacity was run and "
            "can be automatically recovered.\n\n"
            "After recovery, save the projects to ensure changes are written to disk."),
         false,
         500);

      S.StartStatic(XO("Recoverable &projects"), 1);
      {
         mFileList = safenew wxDataViewListCtrl(this, ID_FILE_LIST);
         mFileList->SetMinSize(wxSize(-1, 120));
         mFileList->AppendToggleColumn(
            /*i18n-hint: (verb).  It instruct the user to select items.*/
            _("Select")
         );
         mFileList->AppendTextColumn(
            /*i18n-hint: (noun).  It's the name of the project to recover.*/
            _("Name")
         );
         S.Id(ID_FILE_LIST)
            .Prop(1)
            .Position(wxEXPAND | wxALL)
            .AddWindow(mFileList);

         PopulateList();
      }
      S.EndStatic();

      S.StartHorizontalLay(wxALIGN_CENTRE, 0);
      {
         S.Id(ID_QUIT_AUDACITY).AddButton(XXO("&Quit Tenacity"));
         S.Id(ID_DISCARD_SELECTED).AddButton(XXO("&Discard Selected"));
         S.Id(ID_RECOVER_SELECTED).AddButton(XXO("&Recover Selected"), wxALIGN_CENTRE, true);
         S.Id(ID_SKIP).AddButton(XXO("&Skip"));

         SetAffirmativeId(ID_RECOVER_SELECTED);
         SetEscapeId(ID_SKIP);
      }
      S.EndHorizontalLay();
   }
   S.EndVerticalLay();

   Layout();
   Fit();
   SetMinSize(GetSize());

   // Sometimes it centers on wxGTK and sometimes it doesn't.
   // Yielding before centering seems to be a good workaround,
   // but will leave to implement on a rainy day.
   Center();
}

void AutoRecoveryDialog::PopulateList()
{
   wxString tempdir = TempDirectory::TempDir();
   wxString pattern = wxT("*.") + FileNames::UnsavedProjectExtension();
   FilePaths files;

   wxDir::GetAllFiles(tempdir, &files, pattern, wxDIR_FILES);

   FilePaths active = ActiveProjects::GetAll();

   for (auto file : active)
   {
      wxFileName fn = file;
      if (fn.FileExists())
      {
         FilePath fullPath = fn.GetFullPath();
         if (files.Index(fullPath) == wxNOT_FOUND)
         {
            files.push_back(fullPath);
         }
      }
      else
      {
         ActiveProjects::Remove(file);
      }
   }

   FilePath activeFile;
   if (mProject)
   {
      auto &projectFileIO = ProjectFileIO::Get(*mProject);
      activeFile = projectFileIO.GetFileName();
   }

   mFiles.clear();
   mFileList->DeleteAllItems();
   wxVector<wxVariant> data;

   for (auto file : files)
   {
      wxFileName fn = file;
      if (fn != activeFile)
      {
         // TODO: Consider storing the file paths as row data instead via
         // second param of AppendItem or SetItemData
         mFiles.push_back(fn.GetFullPath());
         data.clear();
         data.push_back(true); // Toggle enabled
         data.push_back(fn.GetName());
         mFileList->AppendItem(data);
      }
   }

   if (mFiles.GetCount() > 0)
   {
      mFileList->SelectRow(0);
      mFileList->SetFocus();
   }
}

bool AutoRecoveryDialog::HaveChecked()
{
   unsigned int cnt = mFileList->GetItemCount();
   for (unsigned int row = 0; row < cnt; ++row)
   {
      if (mFileList->GetToggleValue(row, 0))
      {
         return true;
      }
   }

   AudacityMessageBox(XO("No projects selected"), XO("Automatic Crash Recovery"));

   return false;
}

void AutoRecoveryDialog::OnQuitAudacity(wxCommandEvent &WXUNUSED(evt))
{
   EndModal(ID_QUIT_AUDACITY);
}

void AutoRecoveryDialog::OnDiscardSelected(wxCommandEvent &WXUNUSED(evt))
{
   if (!HaveChecked())
   {
      return;
   }

   unsigned int cnt = mFileList->GetItemCount();
   bool selectedTemporary = false;

   for (unsigned int row = 0; row < cnt; ++row)
   {
      if (!mFileList->GetToggleValue(row, 0))
         continue;

      FilePath fileName = mFiles[row];
      wxFileName file(fileName);
      if (file.GetExt().IsSameAs(FileNames::UnsavedProjectExtension()))
      {
         selectedTemporary = true;
         break;
      }
   }

   // Don't give this warning message if all of the checked items are
   // previously saved, non-temporary projects.
   if (selectedTemporary) {
      int ret = AudacityMessageBox(
         XO("Are you sure you want to discard the selected projects?\n\n"
            "Choosing \"Yes\" permanently deletes the selected projects immediately."),
         XO("Automatic Crash Recovery"),
         wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT, this);

      if (ret == wxNO)
         return;
   }

   FilePaths files;

   for (unsigned int row = 0; row < cnt; ++row)
   {
      if (!mFileList->GetToggleValue(row, 0))
      {
         // Keep in list
         files.push_back(mFiles[row]);
         continue;
      }

      FilePath fileName = mFiles[row];

      // Only remove it from disk if it appears to be a temporary file.
      wxFileName file(fileName);
      if (file.GetExt().IsSameAs(FileNames::UnsavedProjectExtension()))
      {
         file.SetFullName(wxT(""));

         wxFileName temp(TempDirectory::TempDir(), wxT(""));
         if (file == temp)
            ProjectFileIO::RemoveProject(fileName);
      }
      else
         // Don't remove from disk, but do (later) open the database
         // of this saved file, and discard edits
         files.push_back(fileName);

      // Forget all about it
      ActiveProjects::Remove(fileName);
   }

   PopulateList();

   mFiles = files;

   if (mFileList->GetItemCount() == 0)
   {
      EndModal(ID_DISCARD_SELECTED);
   }
}

void AutoRecoveryDialog::OnRecoverSelected(wxCommandEvent &WXUNUSED(evt))
{
   if (!HaveChecked())
   {
      return;
   }

   FilePaths files;

   unsigned int cnt = mFileList->GetItemCount();

   for (unsigned int row = 0; row < cnt; ++row)
   {
      if (mFileList->GetToggleValue(row, 0))
      {
         files.push_back(mFiles[row]);
      }
   }

   if (files.IsEmpty())
   {
      AudacityMessageBox(XO("No projects selected"), XO("Automatic Crash Recovery"));
   }

   mFiles = files;

   EndModal(ID_RECOVER_SELECTED);
}

void AutoRecoveryDialog::OnSkip(wxCommandEvent &WXUNUSED(evt))
{
   EndModal(ID_SKIP);
}

////////////////////////////////////////////////////////////////////////////

static bool RecoverAllProjects(const FilePaths &files,
                               TenacityProject *&pproj)
{
   // Open a project window for each auto save file
   wxString filename;
   bool result = true;

   for (auto &file: files)
   {
      TenacityProject *proj = nullptr;
      // Reuse any existing project window, which will be the empty project
      // created at application startup
      std::swap(proj, pproj);

      // Open project.
      if (ProjectManager::OpenProject(proj, file, false, true) == nullptr)
      {
         result = false;
      }
   }

   return result;
}

static void DiscardAllProjects(const FilePaths &files)
{
   // Open and close each file, invisibly, removing its Autosave blob
   for (auto &file: files)
      ProjectFileManager::DiscardAutosave(file);
}

bool ShowAutoRecoveryDialogIfNeeded(TenacityProject *&pproj, bool *didRecoverAnything)
{
   if (didRecoverAnything)
   {
      *didRecoverAnything = false;
   }

   bool success = true;

   // Under wxGTK3, the auto recovery dialog will not get
   // the focus since the project window hasn't been allowed
   // to completely initialize.
   //
   // Yielding seems to allow the initialization to complete.
   //
   // Additionally, it also corrects a sizing issue in the dialog
   // related to wxWidgets bug:
   //
   //    http://trac.wxwidgets.org/ticket/16440
   //
   // This must be done before "dlg" is declared.
   wxEventLoopBase::GetActive()->YieldFor(wxEVT_CATEGORY_UI);

   AutoRecoveryDialog dialog(pproj);

   if (dialog.HasRecoverables())
   {
      int ret = dialog.ShowModal();
      
      switch (ret)
      {
      case ID_SKIP:
         success = true;
         break;

      case ID_DISCARD_SELECTED:
         DiscardAllProjects(dialog.GetRecoverables());
         success = true;
         break;

      case ID_RECOVER_SELECTED:
         success = RecoverAllProjects(dialog.GetRecoverables(), pproj);
         if (success)
         {
            if (didRecoverAnything)
            {
               *didRecoverAnything = true;
            }
         }
         break;

      default:
         // This includes ID_QUIT_AUDACITY
         return false;
      }
   }
   
   return success;
}
