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

Audacity: A Digital Audio Editor

ProjectFileIO.cpp

Paul Licameli split from AudacityProject.cpp

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

#include "ProjectFileIO.h"

#include <wx/frame.h>

#include "AutoRecovery.h"
#include "DirManager.h"
#include "FileNames.h"
#include "Project.h"
#include "ProjectFileIORegistry.h"
#include "ProjectSettings.h"
#include "Tags.h"
#include "ViewInfo.h"
#include "WaveTrack.h"
#include "widgets/AudacityMessageBox.h"
#include "widgets/NumericTextCtrl.h"

static void RefreshAllTitles(bool bShowProjectNumbers )
{
   for ( auto pProject : AllProjects{} ) {
      if ( !GetProjectFrame( *pProject ).IsIconized() ) {
         ProjectFileIO::Get( *pProject ).SetProjectTitle(
            bShowProjectNumbers ? pProject->GetProjectNumber() : -1 );
      }
   }
}

TitleRestorer::TitleRestorer(
   wxTopLevelWindow &window, AudacityProject &project )
{
   if( window.IsIconized() )
      window.Restore();
   window.Raise(); // May help identifying the window on Mac

   // Construct this project's name and number.
   sProjName = project.GetProjectName();
   if ( sProjName.empty() ) {
      sProjName = _("<untitled>");
      UnnamedCount = std::count_if(
         AllProjects{}.begin(), AllProjects{}.end(),
         []( const AllProjects::value_type &ptr ){
            return ptr->GetProjectName().empty();
         }
      );
      if ( UnnamedCount > 1 ) {
         sProjNumber.Printf(
            "[Project %02i] ", project.GetProjectNumber() + 1 );
         RefreshAllTitles( true );
      } 
   }
   else
      UnnamedCount = 0;
}

TitleRestorer::~TitleRestorer() {
   if( UnnamedCount > 1 )
      RefreshAllTitles( false );
}

static const AudacityProject::AttachedObjects::RegisteredFactory sFileIOKey{
   []( AudacityProject &parent ){
      auto result = std::make_shared< ProjectFileIO >( parent );
      return result;
   }
};

ProjectFileIO &ProjectFileIO::Get( AudacityProject &project )
{
   return project.AttachedObjects::Get< ProjectFileIO >( sFileIOKey );
}

const ProjectFileIO &ProjectFileIO::Get( const AudacityProject &project )
{
   return Get( const_cast< AudacityProject & >( project ) );
}

// PRL: I preserve this handler function for an event that was never sent, but
// I don't know the intention.

ProjectFileIO::ProjectFileIO( AudacityProject &project )
   : mProject{ project }
{
   UpdatePrefs();
}

ProjectFileIO::~ProjectFileIO() = default;

void ProjectFileIO::UpdatePrefs()
{
   SetProjectTitle();
}

// Pass a number in to show project number, or -1 not to.
void ProjectFileIO::SetProjectTitle( int number)
{
   auto &project = mProject;
   auto pWindow = project.GetFrame();
   if ( !pWindow )
      return;
   auto &window = *pWindow;
   wxString name = project.GetProjectName();

   // If we are showing project numbers, then we also explicitly show "<untitled>" if there
   // is none.
   if( number >= 0 ){
      /* i18n-hint: The %02i is the project number, the %s is the project name.*/
      name = wxString::Format( _TS("[Project %02i] Audacity \"%s\""), number+1 ,
         name.empty() ? "<untitled>" : (const char *)name );
   }
   // If we are not showing numbers, then <untitled> shows as 'Audacity'.
   else if( name.empty() )
   {
      SetLoadedFromAup( false );
      name = _TS("Audacity");
   }

   if ( IsRecovered() )
   {
      name += wxT(" ");
      /* i18n-hint: E.g this is recovered audio that had been lost.*/
      name += _("(Recovered)");
   }

   window.SetTitle( name );
   window.SetName(name);       // to make the nvda screen reader read the correct title
}

// Most of this string was duplicated 3 places. Made the warning consistent in this global.
// The %s is to be filled with the version string.
// PRL:  Do not statically allocate a string in _() !
static TranslatableString gsLegacyFileWarning() { return
XO("This file was saved by Audacity version %s. The format has changed. \
\n\nAudacity can try to open and save this file, but saving it in this \
\nversion will then prevent any 1.2 or earlier version opening it. \
\n\nAudacity might corrupt the file in opening it, so you should \
back it up first. \
\n\nOpen this file now?");
}

bool ProjectFileIO::WarnOfLegacyFile( )
{
   auto &project = mProject;
   auto &window = GetProjectFrame( project );
   auto msg = gsLegacyFileWarning().Format( XO("1.0 or earlier") );

   // Stop icon, and choose 'NO' by default.
   int action =
      AudacityMessageBox(
         msg,
         XO("Warning - Opening Old Project File"),
         wxYES_NO | wxICON_STOP | wxNO_DEFAULT | wxCENTRE,
         &window);
   return (action != wxNO);
}

bool ProjectFileIO::HandleXMLTag(const wxChar *tag, const wxChar **attrs)
{
   auto &project = mProject;
   auto &window = GetProjectFrame( project );
   auto &viewInfo = ViewInfo::Get( project );
   auto &dirManager = DirManager::Get( project );
   auto &settings = ProjectSettings::Get( project );
   bool bFileVersionFound = false;
   wxString fileVersion = _("<unrecognized version -- possibly corrupt project file>");
   wxString audacityVersion = _("<unrecognized version -- possibly corrupt project file>");
   int requiredTags = 0;
   long longVpos = 0;

   // The auto-save data dir the project has been recovered from
   FilePath recoveryAutoSaveDataDir;

   // loop through attrs, which is a null-terminated list of
   // attribute-value pairs
   while(*attrs) {
      const wxChar *attr = *attrs++;
      const wxChar *value = *attrs++;

      if (!value || !XMLValueChecker::IsGoodString(value))
         break;

      if (viewInfo.ReadXMLAttribute(attr, value)) {
         // We need to save vpos now and restore it below
         longVpos = std::max(longVpos, long(viewInfo.vpos));
         continue;
      }

      if (!wxStrcmp(attr, wxT("datadir")))
      {
         //
         // This is an auto-saved version whose data is in another directory
         //
         // Note: This attribute must currently be written and parsed before
         //       any other attributes
         //
         if ((value[0] != 0) && XMLValueChecker::IsGoodPathString(value))
         {
            // Remember that this is a recovered project
            mIsRecovered = true;
            recoveryAutoSaveDataDir = value;
         }
      }

      else if (!wxStrcmp(attr, wxT("version")))
      {
         fileVersion = value;
         bFileVersionFound = true;
         requiredTags++;
      }

      else if (!wxStrcmp(attr, wxT("audacityversion"))) {
         audacityVersion = value;
         requiredTags++;
      }

      else if (!wxStrcmp(attr, wxT("projname"))) {
         FilePath projName;
         FilePath projPath;

         if (mIsRecovered) {
            // Fake the filename as if we had opened the original file
            // (which was lost by the crash) rather than the one in the
            // auto save folder
            wxFileName realFileDir;
            realFileDir.AssignDir(recoveryAutoSaveDataDir);
            realFileDir.RemoveLastDir();

            wxString realFileName = value;
            if (realFileName.length() >= 5 &&
                realFileName.Right(5) == wxT("_data"))
            {
               realFileName = realFileName.Left(realFileName.length() - 5);
            }

            if (realFileName.empty())
            {
               // A previously unsaved project has been recovered, so fake
               // an unsaved project. The data files just stay in the temp
               // directory
               dirManager.SetLocalTempDir(recoveryAutoSaveDataDir);
               project.SetFileName( wxT("") );
               projName = wxT("");
               projPath = wxT("");
            }
            else
            {
               realFileName += wxT(".aup");
               projPath = realFileDir.GetFullPath();
               project.SetFileName(
                  wxFileName{ projPath, realFileName }.GetFullPath() );
               mbLoadedFromAup = true;
               projName = value;
            }

            SetProjectTitle();
         }
         else {
            projName = value;
            projPath = wxPathOnly( project.GetFileName() );
         }

         if (!projName.empty())
         {
            // First try to load the data files based on the _data dir given in the .aup file
            // If this fails then try to use the filename of the .aup as the base directory
            // This is because unzipped projects e.g. those that get transfered between mac-pc
            // have encoding issues and end up expanding the wrong filenames for certain
            // international characters (such as capital 'A' with an umlaut.)
            if (!dirManager.SetProject(projPath, projName, false))
            {
               projName = project.GetProjectName() + wxT("_data");
               if (!dirManager.SetProject(projPath, projName, false)) {
                  AudacityMessageBox(
                     XO("Couldn't find the project data folder: \"%s\"")
                        .Format( projName ),
                     XO("Error Opening Project"),
                     wxOK | wxCENTRE,
                     &window);
                  return false;
               }
            }
         }

         requiredTags++;
      }

      else if (!wxStrcmp(attr, wxT("rate"))) {
         double rate;
         Internat::CompatibleToDouble(value, &rate);
         settings.SetRate( rate );
      }

      else if (!wxStrcmp(attr, wxT("snapto"))) {
         settings.SetSnapTo(wxString(value) == wxT("on") ? true : false);
      }

      else if (!wxStrcmp(attr, wxT("selectionformat")))
         settings.SetSelectionFormat(
            NumericConverter::LookupFormat( NumericConverter::TIME, value) );

      else if (!wxStrcmp(attr, wxT("audiotimeformat")))
         settings.SetAudioTimeFormat(
            NumericConverter::LookupFormat( NumericConverter::TIME, value) );

      else if (!wxStrcmp(attr, wxT("frequencyformat")))
         settings.SetFrequencySelectionFormatName(
            NumericConverter::LookupFormat( NumericConverter::FREQUENCY, value ) );

      else if (!wxStrcmp(attr, wxT("bandwidthformat")))
         settings.SetBandwidthSelectionFormatName(
            NumericConverter::LookupFormat( NumericConverter::BANDWIDTH, value ) );
   } // while

   if (longVpos != 0) {
      // PRL: It seems this must happen after SetSnapTo
       viewInfo.vpos = longVpos;
   }

   // Specifically detect newer versions of Audacity
   // WARNING: This will need review/revision if we ever have a version string
   // such as 1.5.10, i.e. with 2 digit numbers.
   // We're able to do a shortcut and use string comparison because we know
   // that does not happen.
   // TODO: Um.  We actually have released 0.98 and 1.3.14 so the comment
   // above is inaccurate.

   if (!bFileVersionFound ||
         (fileVersion.length() != 5) || // expecting '1.1.0', for example
         // JKC: I commentted out next line.  IsGoodInt is not for
         // checking dotted numbers.
         //!XMLValueChecker::IsGoodInt(fileVersion) ||
         (fileVersion > wxT(AUDACITY_FILE_FORMAT_VERSION)))
   {
      /* i18n-hint: %s will be replaced by the version number.*/
      auto msg = XO("This file was saved using Audacity %s.\nYou are using Audacity %s. You may need to upgrade to a newer version to open this file.")
         .Format(audacityVersion,
                 AUDACITY_VERSION_STRING);
      AudacityMessageBox(
         msg,
         XO("Can't open project file"),
         wxOK | wxICON_EXCLAMATION | wxCENTRE,
         &window);
      return false;
   }

   // NOTE: It looks as if there was some confusion about fileversion and audacityversion.
   // fileversion NOT being increased when file formats changed, so unfortunately we're
   // using audacityversion to rescue the situation.

   // KLUDGE: guess the true 'fileversion' by stripping away any '-beta-Rc' stuff on
   // audacityVersion.
   // It's fairly safe to do this as it has already been established that the
   // puported file version was five chars long.
   fileVersion = audacityVersion.Mid(0,5);

   bool bIsOld = fileVersion < wxT(AUDACITY_FILE_FORMAT_VERSION);
   bool bIsVeryOld = fileVersion < wxT("1.1.9" );
   // Very old file versions could even have the file version starting
   // with text: 'AudacityProject Version 0.95'
   // Atoi return zero in this case.
   bIsVeryOld |= wxAtoi( fileVersion )==0;
   // Specifically detect older versions of Audacity
   if ( bIsOld | bIsVeryOld ) {
      auto msg = gsLegacyFileWarning().Format( audacityVersion );

      int icon_choice = wxICON_EXCLAMATION;
      if( bIsVeryOld )
         // Stop icon, and choose 'NO' by default.
         icon_choice = wxICON_STOP | wxNO_DEFAULT;
      int action = AudacityMessageBox(
         msg,
         XO("Warning - Opening Old Project File"),
         wxYES_NO | icon_choice | wxCENTRE,
         &window);
      if (action == wxNO)
         return false;
   }

   if (wxStrcmp(tag, wxT("audacityproject")) &&
       wxStrcmp(tag, wxT("project"))) {
      // If the tag name is not one of these two (the NEW name is
      // "project" with an Audacity namespace, but we don't detect
      // the namespace yet), then we don't know what the error is
      return false;
   }

   if (requiredTags < 3)
      return false;

   // All other tests passed, so we succeed
   return true;
}

XMLTagHandler *ProjectFileIO::HandleXMLChild(const wxChar *tag)
{
   auto &project = mProject;
   auto fn = ProjectFileIORegistry::Lookup( tag );
   if (fn)
      return fn( project );

   return nullptr;
}

void ProjectFileIO::WriteXMLHeader(XMLWriter &xmlFile) const
{
   xmlFile.Write(wxT("<?xml "));
   xmlFile.Write(wxT("version=\"1.0\" "));
   xmlFile.Write(wxT("standalone=\"no\" "));
   xmlFile.Write(wxT("?>\n"));

   xmlFile.Write(wxT("<!DOCTYPE "));
   xmlFile.Write(wxT("project "));
   xmlFile.Write(wxT("PUBLIC "));
   xmlFile.Write(wxT("\"-//audacityproject-1.3.0//DTD//EN\" "));
   xmlFile.Write(wxT("\"http://audacity.sourceforge.net/xml/audacityproject-1.3.0.dtd\" "));
   xmlFile.Write(wxT(">\n"));
}

void ProjectFileIO::WriteXML(
   XMLWriter &xmlFile, FilePaths *strOtherNamesArray)
// may throw
{
   auto &proj = mProject;
   auto &tracks = TrackList::Get( proj );
   auto &viewInfo = ViewInfo::Get( proj );
   auto &dirManager = DirManager::Get( proj );
   auto &tags = Tags::Get( proj );
   const auto &settings = ProjectSettings::Get( proj );

   bool bWantSaveCopy = (strOtherNamesArray != nullptr);

   //TIMER_START( "AudacityProject::WriteXML", xml_writer_timer );
   // Warning: This block of code is duplicated in Save, for now...
   wxFileName project { proj.GetFileName() };
   if (project.GetExt() == wxT("aup"))
      project.SetExt( {} );
   auto projName = project.GetFullName() + wxT("_data");
   // End Warning -DMM

   xmlFile.StartTag(wxT("project"));
   xmlFile.WriteAttr(wxT("xmlns"), wxT("http://audacity.sourceforge.net/xml/"));

   if (mAutoSaving)
   {
      //
      // When auto-saving, remember full path to data files directory
      //
      // Note: This attribute must currently be written and parsed before
      //       all other attributes
      //
      xmlFile.WriteAttr(wxT("datadir"), dirManager.GetDataFilesDir());

      // Note that the code at the start assumes that if mFileName has a value
      // then the file has been saved.  This is not neccessarily true when
      // autosaving as it gets set by AddImportedTracks (presumably as a proposal).
      // I don't think that mDirManager.projName gets set without a save so check that.
      if( !IsProjectSaved() )
         projName = wxT("_data");
   }

   xmlFile.WriteAttr(wxT("projname"), projName);
   xmlFile.WriteAttr(wxT("version"), wxT(AUDACITY_FILE_FORMAT_VERSION));
   xmlFile.WriteAttr(wxT("audacityversion"), AUDACITY_VERSION_STRING);

   viewInfo.WriteXMLAttributes(xmlFile);
   xmlFile.WriteAttr(wxT("rate"), settings.GetRate());
   xmlFile.WriteAttr(wxT("snapto"), settings.GetSnapTo() ? wxT("on") : wxT("off"));
   xmlFile.WriteAttr(wxT("selectionformat"),
                     settings.GetSelectionFormat().Internal());
   xmlFile.WriteAttr(wxT("frequencyformat"),
                     settings.GetFrequencySelectionFormatName().Internal());
   xmlFile.WriteAttr(wxT("bandwidthformat"),
                     settings.GetBandwidthSelectionFormatName().Internal());

   tags.WriteXML(xmlFile);

   unsigned int ndx = 0;
   tracks.Any().Visit(
      [&](WaveTrack *pWaveTrack) {
         if (bWantSaveCopy) {
            if (!pWaveTrack->IsLeader())
               return;

            //vvv This should probably be a method, WaveTrack::WriteCompressedTrackXML().
            xmlFile.StartTag(wxT("import"));
            xmlFile.WriteAttr(wxT("filename"),
               (*strOtherNamesArray)[ndx]); // Assumes mTracks order hasn't changed!

            // Don't store "channel" and "linked" tags because the importer can figure that out,
            // e.g., from stereo Ogg files.
            //    xmlFile.WriteAttr(wxT("channel"), t->GetChannel());
            //    xmlFile.WriteAttr(wxT("linked"), t->GetLinked());

            const auto offset =
               TrackList::Channels( pWaveTrack ).min( &WaveTrack::GetOffset );
            xmlFile.WriteAttr(wxT("offset"), offset, 8);
            xmlFile.WriteAttr(wxT("mute"), pWaveTrack->GetMute());
            xmlFile.WriteAttr(wxT("solo"), pWaveTrack->GetSolo());
            pWaveTrack->Track::WriteCommonXMLAttributes( xmlFile, false );

            // Don't store "rate" tag because the importer can figure that out.
            //    xmlFile.WriteAttr(wxT("rate"), pWaveTrack->GetRate());
            xmlFile.WriteAttr(wxT("gain"), (double)pWaveTrack->GetGain());
            xmlFile.WriteAttr(wxT("pan"), (double)pWaveTrack->GetPan());
            xmlFile.EndTag(wxT("import"));

            ndx++;
         }
         else {
            pWaveTrack->SetAutoSaveIdent(mAutoSaving ? ++ndx : 0);
            pWaveTrack->WriteXML(xmlFile);
         }
      },
      [&](Track *t) {
         t->WriteXML(xmlFile);
      }
   );

   if (!mAutoSaving)
   {
      // Only write closing bracket when not auto-saving, since we may add
      // recording log data to the end of the file later
      xmlFile.EndTag(wxT("project"));
   }
   //TIMER_STOP( xml_writer_timer );

}

static wxString CreateUniqueName()
{
   static int count = 0;
   return wxDateTime::Now().Format(wxT("%Y-%m-%d %H-%M-%S")) +
          wxString::Format(wxT(" N-%i"), ++count);
}

//
// This small template class resembles a try-finally block
//
// It sets var to val_entry in the constructor and
// var to val_exit in the destructor.
//
template <typename T>
class VarSetter
{
public:
   VarSetter(T* var, T val_entry, T val_exit)
   {
      mVar = var;
      mValExit = val_exit;
      *var = val_entry;
   }

   ~VarSetter()
   {
      *mVar = mValExit;
   }
private:
   T* mVar;
   T mValExit;
};

void ProjectFileIO::AutoSave()
{
   auto &project = mProject;
   auto &window = GetProjectFrame( project );
   //    SonifyBeginAutoSave(); // part of RBD's r10680 stuff now backed out

   // To minimize the possibility of race conditions, we first write to a
   // file with the extension ".tmp", then rename the file to .autosave
   wxString projName;

   auto fileName = project.GetFileName();
   if (fileName.empty())
      projName = wxT("New Project");
   else
      projName = wxFileName{ fileName }.GetName();

   wxString fn = wxFileName(FileNames::AutoSaveDir(),
      projName + wxString(wxT(" - ")) + CreateUniqueName()).GetFullPath();

   // PRL:  I found a try-catch and rewrote it,
   // but this guard is unnecessary because AutoSaveFile does not throw
   bool success = GuardedCall< bool >( [&]
   {
      VarSetter<bool> setter(&mAutoSaving, true, false);

      AutoSaveFile buffer;
      WriteXMLHeader( buffer );
      WriteXML( buffer, nullptr );

      wxFFile saveFile;
      saveFile.Open(fn + wxT(".tmp"), wxT("wb"));
      return buffer.Write(saveFile);
   } );

   if (!success)
      return;

   // Now that we have a NEW auto-save file, DELETE the old one
   DeleteCurrentAutoSaveFile();

   if (!mAutoSaveFileName.empty())
      return; // could not remove auto-save file

   if (!wxRenameFile(fn + wxT(".tmp"), fn + wxT(".autosave")))
   {
      AudacityMessageBox(
         XO("Could not create autosave file: %s")
            .Format( fn + wxT(".autosave") ),
         XO("Error"),
         wxICON_STOP,
         &window);
      return;
   }

   mAutoSaveFileName += fn + wxT(".autosave");
   // no-op cruft that's not #ifdefed for NoteTrack
   // See above for further comments.
   //   SonifyEndAutoSave();
}

void ProjectFileIO::DeleteCurrentAutoSaveFile()
{
   auto &project = mProject;
   auto &window = GetProjectFrame( project );
   if (!mAutoSaveFileName.empty())
   {
      if (wxFileExists(mAutoSaveFileName))
      {
         if (!wxRemoveFile(mAutoSaveFileName))
         {
            AudacityMessageBox(
               XO("Could not remove old autosave file: %s")
                  .Format( mAutoSaveFileName ),
               XO("Error"),
               wxICON_STOP,
               &window);
            return;
         }
      }

      mAutoSaveFileName = wxT("");
   }
}

bool ProjectFileIO::IsProjectSaved() const {
   auto &project = mProject;
   auto &dirManager = DirManager::Get( project );
   // This is true if a project was opened from an .aup
   // Otherwise it becomes true only when a project is first saved successfully
   // in DirManager::SetProject
   return (!dirManager.GetProjectName().empty());
}

void ProjectFileIO::Reset()
{
   mProject.SetFileName( {} );
   mIsRecovered = false;
   mbLoadedFromAup = false;
   SetProjectTitle();
}
