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

  Audacity: A Digital Audio Editor

  UndoManager.cpp

  Dominic Mazzoni

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

\class UndoManager
\brief Works with HistoryDialog to provide the Undo functionality.

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

\class UndoStackElem
\brief Holds one item with description and time range for the
UndoManager

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


#include "Audacity.h"
#include "UndoManager.h"

#include <wx/hashset.h>

#include "BlockFile.h"
#include "Clipboard.h"
#include "Diags.h"
#include "Project.h"
#include "Sequence.h"
#include "WaveClip.h"
#include "WaveTrack.h"          // temp
#include "NoteTrack.h"  // for Sonify* function declarations
#include "Diags.h"
#include "Tags.h"


#include <unordered_set>

wxDEFINE_EVENT(EVT_UNDO_PUSHED, wxCommandEvent);
wxDEFINE_EVENT(EVT_UNDO_MODIFIED, wxCommandEvent);
wxDEFINE_EVENT(EVT_UNDO_OR_REDO, wxCommandEvent);
wxDEFINE_EVENT(EVT_UNDO_RESET, wxCommandEvent);

using ConstBlockFilePtr = const BlockFile*;
using Set = std::unordered_set<ConstBlockFilePtr>;

struct UndoStackElem {

   UndoStackElem(std::shared_ptr<TrackList> &&tracks_,
      const TranslatableString &description_,
      const TranslatableString &shortDescription_,
      const SelectedRegion &selectedRegion_,
      const std::shared_ptr<Tags> &tags_)
      : state(std::move(tracks_), tags_, selectedRegion_)
      , description(description_)
      , shortDescription(shortDescription_)
   {
   }

   UndoState state;
   TranslatableString description;
   TranslatableString shortDescription;
};

static const AudacityProject::AttachedObjects::RegisteredFactory key{
   [](AudacityProject &project)
      { return std::make_unique<UndoManager>( project ); }
};

UndoManager &UndoManager::Get( AudacityProject &project )
{
   return project.AttachedObjects::Get< UndoManager >( key );
}

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

UndoManager::UndoManager( AudacityProject &project )
   : mProject{ project }
{
   current = -1;
   saved = -1;
   ResetODChangesFlag();
}

UndoManager::~UndoManager()
{
   ClearStates();
}

namespace {
   SpaceArray::value_type
   CalculateUsage(const TrackList &tracks, Set *seen)
   {
      SpaceArray::value_type result = 0;

      //TIMER_START( "CalculateSpaceUsage", space_calc );
      for (auto wt : tracks.Any< const WaveTrack >())
      {
         // Scan all clips within current track
         for(const auto &clip : wt->GetAllClips())
         {
            // Scan all blockfiles within current clip
            auto blocks = clip->GetSequenceBlockArray();
            for (const auto &block : *blocks)
            {
               const auto &file = block.f;

               // Accumulate space used by the file if the file was not
               // yet seen
               if ( !seen || (seen->count( &*file ) == 0 ) )
               {
                  unsigned long long usage{ file->GetSpaceUsage() };
                  result += usage;
               }

               // Add file to current set
               if (seen)
                  seen->insert( &*file );
            }
         }
      }

      return result;
   }
}

void UndoManager::CalculateSpaceUsage()
{
   space.clear();
   space.resize(stack.size(), 0);

   Set seen;

   // After copies and pastes, a block file may be used in more than
   // one place in one undo history state, and it may be used in more than
   // one undo history state.  It might even be used in two states, but not
   // in another state that is between them -- as when you have state A,
   // then make a cut to get state B, but then paste it back into state C.

   // So be sure to count each block file once only, in the last undo item that
   // contains it.

   // Why the last and not the first? Because the user of the History dialog
   // may DELETE undo states, oldest first.  To reclaim disk space you must
   // DELETE all states containing the block file.  So the block file's
   // contribution to space usage should be counted only in that latest state.

   for (size_t nn = stack.size(); nn--;)
   {
      // Scan all tracks at current level
      auto &tracks = *stack[nn]->state.tracks;
      space[nn] = CalculateUsage(tracks, &seen);
   }

   mClipboardSpaceUsage = CalculateUsage(
      Clipboard::Get().GetTracks(), nullptr);

   //TIMER_STOP( space_calc );
}

wxLongLong_t UndoManager::GetLongDescription(
   unsigned int n, TranslatableString *desc, wxString *size)
{
   n -= 1; // 1 based to zero based

   wxASSERT(n < stack.size());
   wxASSERT(space.size() == stack.size());

   *desc = stack[n]->description;

   *size = Internat::FormatSize(space[n]);

   return space[n];
}

void UndoManager::GetShortDescription(unsigned int n, TranslatableString *desc)
{
   n -= 1; // 1 based to zero based

   wxASSERT(n < stack.size());

   *desc = stack[n]->shortDescription;
}

void UndoManager::SetLongDescription(
  unsigned int n, const TranslatableString &desc)
{
   n -= 1;

   wxASSERT(n < stack.size());

   stack[n]->description = desc;
}

void UndoManager::RemoveStateAt(int n)
{
   stack.erase(stack.begin() + n);
}


void UndoManager::RemoveStates(int num)
{
   for (int i = 0; i < num; i++) {
      RemoveStateAt(0);

      current -= 1;
      saved -= 1;
   }
}

void UndoManager::ClearStates()
{
   RemoveStates(stack.size());
   current = -1;
   saved = -1;
}

unsigned int UndoManager::GetNumStates()
{
   return stack.size();
}

unsigned int UndoManager::GetCurrentState()
{
   return current + 1;  // the array is 0 based, the abstraction is 1 based
}

bool UndoManager::UndoAvailable()
{
   return (current > 0);
}

bool UndoManager::RedoAvailable()
{
   return (current < (int)stack.size() - 1);
}

void UndoManager::ModifyState(const TrackList * l,
                              const SelectedRegion &selectedRegion,
                              const std::shared_ptr<Tags> &tags)
{
   if (current == wxNOT_FOUND) {
      return;
   }

   SonifyBeginModifyState();
   // Delete current -- not necessary, but let's reclaim space early
   stack[current]->state.tracks.reset();

   // Duplicate
   auto tracksCopy = TrackList::Create( nullptr );
   for (auto t : *l) {
      if ( t->GetId() == TrackId{} )
         // Don't copy a pending added track
         continue;
      tracksCopy->Add(t->Duplicate());
   }

   // Replace
   stack[current]->state.tracks = std::move(tracksCopy);
   stack[current]->state.tags = tags;

   stack[current]->state.selectedRegion = selectedRegion;
   SonifyEndModifyState();

   // wxWidgets will own the event object
   mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_MODIFIED } );
}

void UndoManager::PushState(const TrackList * l,
                            const SelectedRegion &selectedRegion,
                            const std::shared_ptr<Tags> &tags,
                            const TranslatableString &longDescription,
                            const TranslatableString &shortDescription,
                            UndoPush flags)
{
   unsigned int i;

   if ( ((flags & UndoPush::CONSOLIDATE) != UndoPush::MINIMAL) &&
       lastAction.Translation() == longDescription.Translation() &&
       mayConsolidate ) {
      ModifyState(l, selectedRegion, tags);
      // MB: If the "saved" state was modified by ModifyState, reset
      //  it so that UnsavedChanges returns true.
      if (current == saved) {
         saved = -1;
      }
      return;
   }

   auto tracksCopy = TrackList::Create( nullptr );
   for (auto t : *l) {
      if ( t->GetId() == TrackId{} )
         // Don't copy a pending added track
         continue;
      tracksCopy->Add(t->Duplicate());
   }

   mayConsolidate = true;

   i = current + 1;
   while (i < stack.size()) {
      RemoveStateAt(i);
   }

   // Assume tags was duplicated before any changes.
   // Just save a NEW shared_ptr to it.
   stack.push_back(
      std::make_unique<UndoStackElem>
         (std::move(tracksCopy),
            longDescription, shortDescription, selectedRegion, tags)
   );

   current++;

   if (saved >= current) {
      saved = -1;
   }

   lastAction = longDescription;

   // wxWidgets will own the event object
   mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_PUSHED } );
}

void UndoManager::SetStateTo(unsigned int n, const Consumer &consumer)
{
   n -= 1;

   wxASSERT(n < stack.size());

   current = n;

   lastAction = {};
   mayConsolidate = false;

   consumer( stack[current]->state );

   // wxWidgets will own the event object
   mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_RESET } );
}

void UndoManager::Undo(const Consumer &consumer)
{
   wxASSERT(UndoAvailable());

   current--;

   lastAction = {};
   mayConsolidate = false;

   consumer( stack[current]->state );

   // wxWidgets will own the event object
   mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_OR_REDO } );
}

void UndoManager::Redo(const Consumer &consumer)
{
   wxASSERT(RedoAvailable());

   current++;

   /*
   if (!RedoAvailable()) {
      *sel0 = stack[current]->sel0;
      *sel1 = stack[current]->sel1;
   }
   else {
      current++;
      *sel0 = stack[current]->sel0;
      *sel1 = stack[current]->sel1;
      current--;
   }
   */

   lastAction = {};
   mayConsolidate = false;

   consumer( stack[current]->state );

   // wxWidgets will own the event object
   mProject.QueueEvent( safenew wxCommandEvent{ EVT_UNDO_OR_REDO } );
}

bool UndoManager::UnsavedChanges() const
{
   return (saved != current) || HasODChangesFlag();
}

void UndoManager::StateSaved()
{
   saved = current;
   ResetODChangesFlag();
}

// currently unused
//void UndoManager::Debug()
//{
//   for (unsigned int i = 0; i < stack.size(); i++) {
//      for (auto t : stack[i]->tracks->Any())
//         wxPrintf(wxT("*%d* %s %f\n"),
//                  i, (i == (unsigned int)current) ? wxT("-->") : wxT("   "),
//                t ? t->GetEndTime()-t->GetStartTime() : 0);
//   }
//}

///to mark as unsaved changes without changing the state/tracks.
void UndoManager::SetODChangesFlag()
{
   mODChangesMutex.Lock();
   mODChanges=true;
   mODChangesMutex.Unlock();
}

bool UndoManager::HasODChangesFlag() const
{
   bool ret;
   mODChangesMutex.Lock();
   ret=mODChanges;
   mODChangesMutex.Unlock();
   return ret;
}

void UndoManager::ResetODChangesFlag()
{
   mODChangesMutex.Lock();
   mODChanges=false;
   mODChangesMutex.Unlock();
}
