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

   Audacity: A Digital Audio Editor
   Audacity(R) is copyright (c) 1999-2008 Audacity Team.
   License: GPL v2.  See License.txt.

   Dependencies.cpp

   Dominic Mazzoni
   Leland Lucius
   Markus Meyer
   LRN
   Michael Chinen
   Vaughan Johnson

   The primary function provided in this source file is
   ShowDependencyDialogIfNeeded.  It checks a project to see if
   any of its WaveTracks contain AliasBlockFiles; if so it
   presents a dialog to the user and lets them copy those block
   files into the project, making it self-contained.

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

\class AliasedFile
\brief An audio file that is referenced (pointed into) directly from
an Audacity .aup file rather thna Audacity having its own copies of the
data.

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

\class DependencyDialog
\brief DependencyDialog shows dependencies of an AudacityProject on 
AliasedFile s.

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

#include "Audacity.h"
#include "Dependencies.h"

#include <wx/button.h>
#include <wx/defs.h>
#include <wx/dialog.h>
#include <wx/filename.h>
#include <wx/listctrl.h>
#include <wx/menu.h>
#include <wx/progdlg.h>
#include <wx/choice.h>
#include <wx/clipbrd.h>
#include <wx/dataobj.h>
#include <wx/frame.h>
#include <wx/stattext.h>

#include "blockfile/SimpleBlockFile.h"
#include "DirManager.h"
#include "FileFormats.h"
#include "Prefs.h"
#include "Project.h"
#include "Sequence.h"
#include "ShuttleGui.h"
#include "WaveTrack.h"
#include "WaveClip.h"
#include "prefs/QualityPrefs.h"
#include "widgets/AudacityMessageBox.h"
#include "widgets/ProgressDialog.h"

#include <unordered_map>

using AliasedFileHash = std::unordered_map<wxString, AliasedFile*>;

// These two hash types are used only inside short scopes
// so it is safe to key them by plain pointers.
using ReplacedBlockFileHash = std::unordered_map<BlockFile *, BlockFilePtr>;
using BoolBlockFileHash = std::unordered_map<BlockFile *, bool>;

// Given a project, returns a single array of all SeqBlocks
// in the current set of tracks.  Enumerating that array allows
// you to process all block files in the current set.
static void GetAllSeqBlocks(AudacityProject *project,
                            BlockPtrArray *outBlocks)
{
   for (auto waveTrack : TrackList::Get( *project ).Any< WaveTrack >()) {
      for(const auto &clip : waveTrack->GetAllClips()) {
         Sequence *sequence = clip->GetSequence();
         BlockArray &blocks = sequence->GetBlockArray();
         for (size_t i = 0; i < blocks.size(); i++)
            outBlocks->push_back(&blocks[i]);
      }
   }
}

// Given an Audacity project and a hash mapping aliased block
// files to un-aliased block files, walk through all of the
// tracks and replace each aliased block file with its replacement.
// Note that this code respects reference-counting and thus the
// process of making a project self-contained is actually undoable.
static void ReplaceBlockFiles(BlockPtrArray &blocks,
                              ReplacedBlockFileHash &hash)
// NOFAIL-GUARANTEE
{
   for (const auto &pBlock : blocks) {
      auto &f = pBlock->f;
      const auto src = &*f;
      if (hash.count( src ) > 0) {
         const auto &dst = hash[src];
         f = dst;
      }
   }
}

void FindDependencies(AudacityProject *project,
                      AliasedFileArray &outAliasedFiles)
{
   sampleFormat format = QualityPrefs::SampleFormatChoice();

   BlockPtrArray blocks;
   GetAllSeqBlocks(project, &blocks);

   AliasedFileHash aliasedFileHash;
   BoolBlockFileHash blockFileHash;

   for (const auto &blockFile : blocks) {
      const auto &f = blockFile->f;
      if (f->IsAlias() && (blockFileHash.count( &*f ) == 0))
      {
         // f is an alias block we have not yet counted.
         blockFileHash[ &*f ] = true; // Don't count the same blockfile twice.
         auto aliasBlockFile = static_cast<AliasBlockFile*>( &*f );
         const wxFileName &fileName = aliasBlockFile->GetAliasedFileName();

         // In ProjectFSCK(), if the user has chosen to
         // "Replace missing audio with silence", the code there puts in an empty wxFileName.
         // Don't count those in dependencies.
         if (!fileName.IsOk())
            continue;

         const wxString &fileNameStr = fileName.GetFullPath();
         auto blockBytes = (SAMPLE_SIZE(format) *
                           aliasBlockFile->GetLength());
         if (aliasedFileHash.count(fileNameStr) > 0)
            // Already put this AliasBlockFile in aliasedFileHash.
            // Update block count.
            aliasedFileHash[fileNameStr]->mByteCount += blockBytes;
         else
         {
            // Haven't counted this AliasBlockFile yet.
            // Add to return array and internal hash.

            // PRL: do this in two steps so that we move instead of copying.
            // We don't have a moving push_back in all compilers.
            outAliasedFiles.push_back(AliasedFile{});
            outAliasedFiles.back() =
               AliasedFile {
                  wxFileNameWrapper { fileName },
                  wxLongLong(blockBytes), fileName.FileExists()
               };
            aliasedFileHash[fileNameStr] = &outAliasedFiles.back();
         }
      }
   }
}

// Given a project and a list of aliased files that should no
// longer be external dependencies (selected by the user), replace
// all of those alias block files with disk block files.
static void RemoveDependencies(AudacityProject *project,
                               AliasedFileArray &aliasedFiles)
// STRONG-GUARANTEE
{
   auto &dirManager = DirManager::Get( *project );

   ProgressDialog progress(
      XO("Removing Dependencies"),
      XO("Copying audio data into project..."));
   auto updateResult = ProgressResult::Success;

   // Hash aliasedFiles based on their full paths and
   // count total number of bytes to process.
   AliasedFileHash aliasedFileHash;
   wxLongLong totalBytesToProcess = 0;
   for (auto &aliasedFile : aliasedFiles) {
      totalBytesToProcess += aliasedFile.mByteCount;
      const wxString &fileNameStr = aliasedFile.mFileName.GetFullPath();
      aliasedFileHash[fileNameStr] = &aliasedFile;
   }

   BlockPtrArray blocks;
   GetAllSeqBlocks(project, &blocks);

   const sampleFormat format = QualityPrefs::SampleFormatChoice();
   ReplacedBlockFileHash blockFileHash;
   wxLongLong completedBytes = 0;
   for (const auto blockFile : blocks) {
      const auto &f = blockFile->f;
      if (f->IsAlias() && (blockFileHash.count( &*f ) == 0))
      {
         // f is an alias block we have not yet processed.
         auto aliasBlockFile = static_cast<AliasBlockFile*>( &*f );
         const wxFileName &fileName = aliasBlockFile->GetAliasedFileName();
         const wxString &fileNameStr = fileName.GetFullPath();

         if (aliasedFileHash.count(fileNameStr) == 0)
            // This aliased file was not selected to be replaced. Skip it.
            continue;

         // Convert it from an aliased file to an actual file in the project.
         auto len = aliasBlockFile->GetLength();
         BlockFilePtr newBlockFile;
         {
            SampleBuffer buffer(len, format);
            // We tolerate exceptions from NewBlockFile
            // and so we can allow exceptions from ReadData too
            f->ReadData(buffer.ptr(), format, 0, len);
            newBlockFile =
               dirManager.NewBlockFile( [&]( wxFileNameWrapper filePath ) {
                  return make_blockfile<SimpleBlockFile>(
                     std::move(filePath), buffer.ptr(), len, format);
               } );
         }

         // Update our hash so we know what block files we've done
         blockFileHash[ &*f ] = newBlockFile;

         // Update the progress bar
         completedBytes += SAMPLE_SIZE(format) * len;
         updateResult = progress.Update(completedBytes, totalBytesToProcess);
         if (updateResult != ProgressResult::Success)
            // leave the project unchanged
            return;
      }
   }

   // COMMIT OPERATIONS needing NOFAIL-GUARANTEE:

   // Above, we created a SimpleBlockFile contained in our project
   // to go with each AliasBlockFile that we wanted to migrate.
   // However, that didn't actually change any references to these
   // blockfiles in the Sequences, so we do that next...
   ReplaceBlockFiles(blocks, blockFileHash);
}

//
// DependencyDialog
//

class DependencyDialog final : public wxDialogWrapper
{
public:
   DependencyDialog(wxWindow *parent,
                    wxWindowID id,
                    AudacityProject *project,
                    AliasedFileArray &aliasedFiles,
                    bool isSaving);

private:
   void PopulateList();
   void PopulateOrExchange(ShuttleGui & S);

   // event handlers
   void OnCancel(wxCommandEvent& evt);
   void OnCopySelectedFiles(wxCommandEvent &evt);
   void OnList(wxListEvent &evt);
   void OnSize(wxSizeEvent &evt);
   void OnNo(wxCommandEvent &evt);
   void OnYes(wxCommandEvent &evt);
   void OnRightClick(wxListEvent& evt);
   void OnCopyToClipboard( wxCommandEvent& evt );


   void SaveFutureActionChoice();


   AudacityProject  *mProject;
   AliasedFileArray &mAliasedFiles;
   bool              mIsSaving;
   bool              mHasMissingFiles;
   bool              mHasNonMissingFiles;

   wxStaticText     *mMessageStaticText;
   wxListCtrl       *mFileListCtrl;
   wxButton         *mCopySelectedFilesButton;
   wxButton         *mCopyAllFilesButton;
   wxChoice         *mFutureActionChoice;

public:
   DECLARE_EVENT_TABLE()
};

enum {
   FileListID = 6000,
   CopySelectedFilesButtonID,
   CopyNamesToClipboardID,
   FutureActionChoiceID
};

BEGIN_EVENT_TABLE(DependencyDialog, wxDialogWrapper)
   EVT_LIST_ITEM_SELECTED(FileListID, DependencyDialog::OnList)
   EVT_LIST_ITEM_DESELECTED(FileListID, DependencyDialog::OnList)
   EVT_LIST_ITEM_RIGHT_CLICK(FileListID, DependencyDialog::OnRightClick )
   EVT_BUTTON(CopySelectedFilesButtonID, DependencyDialog::OnCopySelectedFiles)
   EVT_SIZE(DependencyDialog::OnSize)
   EVT_BUTTON(wxID_NO, DependencyDialog::OnNo) // mIsSaving ? "Cancel Save" : "Save Without Copying"
   EVT_BUTTON(wxID_YES, DependencyDialog::OnYes) // "Copy All Files (Safer)"
   EVT_BUTTON(wxID_CANCEL, DependencyDialog::OnCancel)  // "Cancel Save"
   EVT_MENU(CopyNamesToClipboardID,DependencyDialog::OnCopyToClipboard)
END_EVENT_TABLE()

DependencyDialog::DependencyDialog(wxWindow *parent,
                                   wxWindowID id,
                                   AudacityProject *project,
                                   AliasedFileArray &aliasedFiles,
                                   bool isSaving)
: wxDialogWrapper(parent, id, XO("Project Depends on Other Audio Files"),
            wxDefaultPosition, wxDefaultSize,
            (isSaving ?
                  (wxDEFAULT_DIALOG_STYLE & ~wxCLOSE_BOX) : // no close box when saving
                  wxDEFAULT_DIALOG_STYLE) |
               wxRESIZE_BORDER),
   mProject(project),
   mAliasedFiles(aliasedFiles),
   mIsSaving(isSaving),
   mHasMissingFiles(false),
   mHasNonMissingFiles(false),
   mMessageStaticText(NULL),
   mFileListCtrl(NULL),
   mCopySelectedFilesButton(NULL),
   mCopyAllFilesButton(NULL),
   mFutureActionChoice(NULL)
{
   SetName();
   ShuttleGui S(this, eIsCreating);
   PopulateOrExchange(S);
}

static const TranslatableString kStdMsg()
{
   return
XO("Copying these files into your project will remove this dependency.\
\nThis is safer, but needs more disk space.");
}

static const TranslatableString kExtraMsgForMissingFiles()
{
   return
XO("\n\nFiles shown as MISSING have been moved or deleted and cannot be copied.\
\nRestore them to their original location to be able to copy into project.");
}

void DependencyDialog::PopulateOrExchange(ShuttleGui& S)
{
   S.SetBorder(5);
   S.StartVerticalLay();
   {
      mMessageStaticText = S.AddVariableText(kStdMsg(), false);

      S.StartStatic(XO("Project Dependencies"),1);
      {
         mFileListCtrl = S.Id(FileListID).AddListControlReportMode({
            { XO("Audio File"), wxLIST_FORMAT_LEFT, 220 },
            { XO("Disk Space"), wxLIST_FORMAT_LEFT, 120 }
         });
         PopulateList();

         mCopySelectedFilesButton =
            S.Id(CopySelectedFilesButtonID)
               .Focus()
               .Disable(mFileListCtrl->GetSelectedItemCount() <= 0)
               .AddButton(
                  XO("Copy Selected Files"),
                  wxALIGN_LEFT, true);
      }
      S.EndStatic();

      S.StartHorizontalLay(wxALIGN_CENTRE,0);
      {
         if (mIsSaving) {
            S.Id(wxID_CANCEL).AddButton(XO("Cancel Save"));
            S.Id(wxID_NO).AddButton(XO("Save Without Copying"));
         }
         else
            S.Id(wxID_NO).AddButton(XO("Do Not Copy"));

         mCopyAllFilesButton =
            S.Id(wxID_YES)
               // Enabling mCopyAllFilesButton is also done in PopulateList,
               // but at its call above, mCopyAllFilesButton does not yet exist.
               .Disable(mHasMissingFiles)
               .AddButton(XO("Copy All Files (Safer)"));

      }
      S.EndHorizontalLay();

      if (mIsSaving)
      {
         S.StartHorizontalLay(wxALIGN_LEFT,0);
         {
            mFutureActionChoice =
               S.Id(FutureActionChoiceID).AddChoice(
                  XO("Whenever a project depends on other files:"),
                  {
                     /*i18n-hint: One of the choices of what you want Audacity to do when
                     * Audacity finds a project depends on another file.*/
                     XO("Ask me") ,
                     /*i18n-hint: One of the choices of what you want Audacity to do when
                     * Audacity finds a project depends on another file.*/
                     XO("Always copy all files (safest)") ,
                     /*i18n-hint: One of the choices of what you want Audacity to do when
                     * Audacity finds a project depends on another file.*/
                     XO("Never copy any files") ,
                  },
                  0 // "Ask me"
               );
         }
         S.EndHorizontalLay();
      }
      else
      {
         mFutureActionChoice = NULL;
      }
   }
   S.EndVerticalLay();
   Layout();
   Fit();
   SetMinSize(GetSize());
   Center();
}

void DependencyDialog::PopulateList()
{
   mFileListCtrl->DeleteAllItems();

   mHasMissingFiles = false;
   mHasNonMissingFiles = false;
   long i = 0;
   for (const auto &aliasedFile : mAliasedFiles) {
      const wxFileName &fileName = aliasedFile.mFileName;
      wxLongLong byteCount = (aliasedFile.mByteCount * 124) / 100;
      bool bOriginalExists = aliasedFile.mbOriginalExists;

      if (bOriginalExists)
      {
         mFileListCtrl->InsertItem(i, fileName.GetFullPath());
         mHasNonMissingFiles = true;
         mFileListCtrl->SetItemState(i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
      }
      else
      {
         mFileListCtrl->InsertItem(i,
            wxString::Format( _("MISSING %s"), fileName.GetFullPath() ) );
         mHasMissingFiles = true;
         mFileListCtrl->SetItemState(i, 0, wxLIST_STATE_SELECTED); // Deselect.
         mFileListCtrl->SetItemTextColour(i, *wxRED);
      }
      mFileListCtrl->SetItem(i, 1, Internat::FormatSize(byteCount));
      mFileListCtrl->SetItemData(i, long(bOriginalExists));

      ++i;
   }

   auto msg = kStdMsg();
   if (mHasMissingFiles)
      msg += kExtraMsgForMissingFiles();
   mMessageStaticText->SetLabel(msg.Translation());

   if (mCopyAllFilesButton)
      mCopyAllFilesButton->Enable(!mHasMissingFiles);
}

void DependencyDialog::OnList(wxListEvent &evt)
{
   if (!mCopySelectedFilesButton || !mFileListCtrl)
      return;

   wxString itemStr = evt.GetText();
   if (evt.GetData() == 0)
      // This list item is one of mAliasedFiles for which
      // the original is missing, i.e., moved or deleted.
      // wxListCtrl does not provide for items that are not
      // allowed to be selected, so always deselect these items.
      mFileListCtrl->SetItemState(evt.GetIndex(), 0, wxLIST_STATE_SELECTED); // Deselect.

   mCopySelectedFilesButton->Enable(
      mFileListCtrl->GetSelectedItemCount() > 0);
}

void DependencyDialog::OnSize(wxSizeEvent &evt)
{
   int fileListCtrlWidth, fileListCtrlHeight;
   mFileListCtrl->GetSize(&fileListCtrlWidth, &fileListCtrlHeight);

   // File path is column 0. File size is column 1.
   // File size column is always 120 px wide.
   // Also subtract 8 from file path column width for borders.
   mFileListCtrl->SetColumnWidth(0, fileListCtrlWidth - 120 - 8);
   mFileListCtrl->SetColumnWidth(1, 120);
   wxDialogWrapper::OnSize(evt);
}

void DependencyDialog::OnNo(wxCommandEvent & WXUNUSED(event))
{
   SaveFutureActionChoice();
   EndModal(wxID_NO);
}

void DependencyDialog::OnYes(wxCommandEvent & WXUNUSED(event))
{
   SaveFutureActionChoice();
   EndModal(wxID_YES);
}

void DependencyDialog::OnCopySelectedFiles(wxCommandEvent & WXUNUSED(event))
{
   AliasedFileArray aliasedFilesToDelete, remainingAliasedFiles;

   long i = 0;
   for( const auto &file : mAliasedFiles ) {
      if (mFileListCtrl->GetItemState(i, wxLIST_STATE_SELECTED))
         aliasedFilesToDelete.push_back( file );
      else
         remainingAliasedFiles.push_back( file );
      ++i;
   }

   // provides STRONG-GUARANTEE
   RemoveDependencies(mProject, aliasedFilesToDelete);

   // COMMIT OPERATIONS needing NOFAIL-GUARANTEE:
   mAliasedFiles.swap( remainingAliasedFiles );
   PopulateList();

   if (mAliasedFiles.empty() || !mHasNonMissingFiles)
   {
      SaveFutureActionChoice();
      EndModal(wxID_NO);  // Don't need to remove dependencies
   }
}

void DependencyDialog::OnRightClick( wxListEvent& event)
{
   static_cast<void>(event);
   wxMenu menu;
   menu.Append(CopyNamesToClipboardID, _("&Copy Names to Clipboard"));
   PopupMenu(&menu);
}

void DependencyDialog::OnCopyToClipboard( wxCommandEvent& evt )
{
   static_cast<void>(evt);
   wxString Files;
   for (const auto &aliasedFile : mAliasedFiles) {
      const wxFileName & fileName = aliasedFile.mFileName;
      wxLongLong byteCount = (aliasedFile.mByteCount * 124) / 100;
      bool bOriginalExists = aliasedFile.mbOriginalExists;
      // All fields quoted, as e.g. size may contain a comma in the number.
      Files += wxString::Format( "\"%s\", \"%s\", \"%s\"\n",
         fileName.GetFullPath(), 
         Internat::FormatSize( byteCount), 
         bOriginalExists ? "OK":"Missing" );
   }

   // copy data onto clipboard
   if (wxTheClipboard->Open()) {
      // Clipboard owns the data you give it
      wxTheClipboard->SetData(safenew wxTextDataObject(Files));
      wxTheClipboard->Close();
   }
}

void DependencyDialog::OnCancel(wxCommandEvent& WXUNUSED(event))
{
   if (mIsSaving)
   {
      int ret = AudacityMessageBox(
         XO(
"If you proceed, your project will not be saved to disk. Is this what you want?"),
         XO("Cancel Save"),
         wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT, this);
      if (ret != wxYES)
         return;
   }

   EndModal(wxID_CANCEL);
}

void DependencyDialog::SaveFutureActionChoice()
{
   if (mFutureActionChoice)
   {
      wxString savePref;
      int sel = mFutureActionChoice->GetSelection();
      switch (sel)
      {
      case 1: savePref = wxT("copy"); break;
      case 2: savePref = wxT("never"); break;
      default: savePref = wxT("ask");
      }
      FileFormatsSaveWithDependenciesSetting.Write( savePref );
      gPrefs->Flush();
   }
}

// Checks for alias block files, modifies the project if the
// user requests it, and returns true if the user continues.
// Returns false only if the user clicks Cancel.
bool ShowDependencyDialogIfNeeded(AudacityProject *project,
                                  bool isSaving)
{
   auto pWindow = FindProjectFrame( project );
   AliasedFileArray aliasedFiles;
   FindDependencies(project, aliasedFiles);

   if (aliasedFiles.empty()) {
      if (!isSaving)
      {
         auto msg =
#ifdef EXPERIMENTAL_OD_DATA
XO("Your project is currently self-contained; it does not depend on any external audio files. \
\n\nIf you change the project to a state that has external dependencies on imported \
files, it will no longer be self-contained. If you then Save without copying those files in, \
you may lose data.");
#else
XO("Your project is self-contained; it does not depend on any external audio files. \
\n\nSome older Audacity projects may not be self-contained, and care \n\
is needed to keep their external dependencies in the right place.\n\
New projects will be self-contained and are less risky.");
#endif
         AudacityMessageBox(
            msg,
            XO("Dependency Check"),
            wxOK | wxICON_INFORMATION,
            pWindow);
      }
      return true; // Nothing to do.
   }

   if (isSaving)
   {
#ifdef EXPERIMENTAL_OD_DATA
      wxString action =
         FileFormatsSaveWithDependenciesSetting.Read();
      if (action == wxT("copy"))
      {
         // User always wants to remove dependencies
         RemoveDependencies(project, aliasedFiles);
         return true;
      }
      if (action == wxT("never"))
         // User never wants to remove dependencies
         return true;
#else 
      RemoveDependencies(project, aliasedFiles);
      return true;
#endif
   }

   DependencyDialog dlog(pWindow, -1, project, aliasedFiles, isSaving);
   int returnCode = dlog.ShowModal();
   if (returnCode == wxID_CANCEL)
      return false;
   else if (returnCode == wxID_YES)
      RemoveDependencies(project, aliasedFiles);

   return true;
}

