#include "../Audacity.h" // for USE_* macros
#include "../AdornedRulerPanel.h"
#include "../Clipboard.h"
#include "../CommonCommandFlags.h"
#include "../LabelTrack.h"
#include "../Menus.h"
#include "../NoteTrack.h"
#include "../Prefs.h"
#include "../Project.h"
#include "../ProjectHistory.h"
#include "../ProjectSettings.h"
#include "../ProjectWindow.h"
#include "../SelectUtilities.h"
#include "../TimeTrack.h"
#include "../TrackPanel.h"
#include "../UndoManager.h"
#include "../ViewInfo.h"
#include "../WaveTrack.h"
#include "../commands/CommandContext.h"
#include "../commands/CommandManager.h"
#include "../commands/ScreenshotCommand.h"
#include "../export/Export.h"
#include "../prefs/PrefsDialog.h"
#include "../prefs/SpectrogramSettings.h"
#include "../prefs/WaveformSettings.h"
#include "../tracks/labeltrack/ui/LabelTrackView.h"
#include "../widgets/AudacityMessageBox.h"

// private helper classes and functions
namespace {
void FinishCopy
   (const Track *n, const Track::Holder &dest, TrackList &list)
{
   Track::FinishCopy( n, dest.get() );
   if (dest)
      list.Add( dest );
}

// Handle text paste (into active label), if any. Return true if did paste.
// (This was formerly the first part of overly-long OnPaste.)
bool DoPasteText(AudacityProject &project)
{
   auto &tracks = TrackList::Get( project );
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
   auto &window = ProjectWindow::Get( project );

   for (auto pLabelTrack : tracks.Any<LabelTrack>())
   {
      // Does this track have an active label?
      if (LabelTrackView::Get( *pLabelTrack ).HasSelection( project )) {

         // Yes, so try pasting into it
         auto &view = LabelTrackView::Get( *pLabelTrack );
         if (view.PasteSelectedText( project, selectedRegion.t0(),
                                            selectedRegion.t1() ))
         {
            ProjectHistory::Get( project )
               .PushState(_("Pasted text from the clipboard"), _("Paste"));

            // Make sure caret is in view
            int x;
            if (view.CalcCursorX( project, &x )) {
               window.ScrollIntoView(x);
            }

            return true;
         }
      }
   }
   return false;
}

// Return true if nothing selected, regardless of paste result.
// If nothing was selected, create and paste into NEW tracks.
// (This was formerly the second part of overly-long OnPaste.)
bool DoPasteNothingSelected(AudacityProject &project)
{
   auto &tracks = TrackList::Get( project );
   auto &trackFactory = TrackFactory::Get( project );
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
   auto &window = ProjectWindow::Get( project );

   // First check whether anything's selected.
   if (tracks.Selected())
      return false;
   else
   {
      const auto &clipboard = Clipboard::Get();
      auto clipTrackRange = clipboard.GetTracks().Any< const Track >();
      if (clipTrackRange.empty())
         return true; // nothing to paste

      Track* pFirstNewTrack = NULL;
      for (auto pClip : clipTrackRange) {
         Maybe<WaveTrack::Locker> locker;

         Track::Holder uNewTrack;
         Track *pNewTrack;
         pClip->TypeSwitch(
            [&](const WaveTrack *wc) {
               if ((clipboard.Project() != &project))
                  // Cause duplication of block files on disk, when copy is
                  // between projects
                  locker.create(wc);
               uNewTrack = trackFactory.NewWaveTrack(
                  wc->GetSampleFormat(), wc->GetRate()),
               pNewTrack = uNewTrack.get();
            },
#ifdef USE_MIDI
            [&](const NoteTrack *) {
               uNewTrack = trackFactory.NewNoteTrack(),
               pNewTrack = uNewTrack.get();
            },
#endif
            [&](const LabelTrack *) {
               uNewTrack = trackFactory.NewLabelTrack(),
               pNewTrack = uNewTrack.get();
            },
            [&](const TimeTrack *) {
               // Maintain uniqueness of the time track!
               pNewTrack = *tracks.Any<TimeTrack>().begin();
               if (!pNewTrack)
                  uNewTrack = trackFactory.NewTimeTrack(),
                  pNewTrack = uNewTrack.get();
            }
         );

         wxASSERT(pClip);

         pNewTrack->Paste(0.0, pClip);

         if (!pFirstNewTrack)
            pFirstNewTrack = pNewTrack;

         pNewTrack->SetSelected(true);
         if (uNewTrack)
            FinishCopy(pClip, uNewTrack, tracks);
         else
            Track::FinishCopy(pClip, pNewTrack);
      }

      // Select some pasted samples, which is probably impossible to get right
      // with various project and track sample rates.
      // So do it at the sample rate of the project
      AudacityProject *p = GetActiveProject();
      double projRate = ProjectSettings::Get( *p ).GetRate();
      double quantT0 = QUANTIZED_TIME(clipboard.T0(), projRate);
      double quantT1 = QUANTIZED_TIME(clipboard.T1(), projRate);
      selectedRegion.setTimes(
         0.0,   // anywhere else and this should be
                // half a sample earlier
         quantT1 - quantT0);

      ProjectHistory::Get( project )
         .PushState(_("Pasted from the clipboard"), _("Paste"));

      if (pFirstNewTrack)
         pFirstNewTrack->EnsureVisible();

      return true;
   }
}

}

namespace EditActions {

// Menu handler functions

struct Handler : CommandHandlerObject {

void OnUndo(const CommandContext &context)
{
   auto &project = context.project;
   auto &tracks = TrackList::Get( project );
   auto &trackPanel = TrackPanel::Get( project );
   auto &undoManager = UndoManager::Get( project );
   auto &window = ProjectWindow::Get( project );

   if (!ProjectHistory::Get( project ).UndoAvailable()) {
      AudacityMessageBox(_("Nothing to undo"));
      return;
   }

   // can't undo while dragging
   if (trackPanel.IsMouseCaptured()) {
      return;
   }

   undoManager.Undo(
      [&]( const UndoState &state ){
         ProjectHistory::Get( project ).PopState( state ); } );

   auto t = *tracks.Selected().begin();
   if (!t)
      t = *tracks.Any().begin();
   if (t)
      t->EnsureVisible();
}

void OnRedo(const CommandContext &context)
{
   auto &project = context.project;
   auto &tracks = TrackList::Get( project );
   auto &trackPanel = TrackPanel::Get( project );
   auto &undoManager = UndoManager::Get( project );
   auto &window = ProjectWindow::Get( project );

   if (!ProjectHistory::Get( project ).RedoAvailable()) {
      AudacityMessageBox(_("Nothing to redo"));
      return;
   }
   // Can't redo whilst dragging
   if (trackPanel.IsMouseCaptured()) {
      return;
   }

   undoManager.Redo(
      [&]( const UndoState &state ){
         ProjectHistory::Get( project ).PopState( state ); } );

   auto t = *tracks.Selected().begin();
   if (!t)
      t = *tracks.Any().begin();
   if (t)
      t->EnsureVisible();
}

void OnCut(const CommandContext &context)
{
   auto &project = context.project;
   auto &tracks = TrackList::Get( project );
   auto &trackPanel = TrackPanel::Get( project );
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
   auto &ruler = AdornedRulerPanel::Get( project );
   auto &window = ProjectWindow::Get( project );

   // This doesn't handle cutting labels, it handles
   // cutting the _text_ inside of labels, i.e. if you're
   // in the middle of editing the label text and select "Cut".

   for (auto lt : tracks.Selected< LabelTrack >()) {
      auto &view = LabelTrackView::Get( *lt );
      if (view.CutSelectedText( context.project )) {
         trackPanel.Refresh(false);
         return;
      }
   }

   auto &clipboard = Clipboard::Get();
   clipboard.Clear();

   auto pNewClipboard = TrackList::Create();
   auto &newClipboard = *pNewClipboard;

   tracks.Selected().Visit(
#if defined(USE_MIDI)
      [&](NoteTrack *n) {
         // Since portsmf has a built-in cut operator, we use that instead
         auto dest = n->Cut(selectedRegion.t0(),
                selectedRegion.t1());
         FinishCopy(n, dest, newClipboard);
      },
#endif
      [&](Track *n) {
         auto dest = n->Copy(selectedRegion.t0(),
                 selectedRegion.t1());
         FinishCopy(n, dest, newClipboard);
      }
   );

   // Survived possibility of exceptions.  Commit changes to the clipboard now.
   clipboard.Assign(
       std::move( newClipboard ),
       selectedRegion.t0(),
       selectedRegion.t1(),
       &project
   );

   // Proceed to change the project.  If this throws, the project will be
   // rolled back by the top level handler.

   (tracks.Any() + &Track::IsSelectedOrSyncLockSelected).Visit(
#if defined(USE_MIDI)
      [](NoteTrack*) {
         //if NoteTrack, it was cut, so do not clear anything

         // PRL:  But what if it was sync lock selected only, not selected?
      },
#endif
      [&](WaveTrack *wt, const Track::Fallthrough &fallthrough) {
         if (gPrefs->Read(wxT("/GUI/EnableCutLines"), (long)0)) {
            wt->ClearAndAddCutLine(
               selectedRegion.t0(),
               selectedRegion.t1());
         }
         else
            fallthrough();
      },
      [&](Track *n) {
         n->Clear(selectedRegion.t0(),
                  selectedRegion.t1());
      }
   );

   selectedRegion.collapseToT0();

   ProjectHistory::Get( project ).PushState(_("Cut to the clipboard"), _("Cut"));

   // Bug 1663
   //mRuler->ClearPlayRegion();
   ruler.DrawOverlays( true );
}

void OnDelete(const CommandContext &context)
{
   auto &project = context.project;
   auto &tracks = TrackList::Get( project );
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
   auto &window = ProjectWindow::Get( project );

   for (auto n : tracks.Any()) {
      if (n->GetSelected() || n->IsSyncLockSelected()) {
         n->Clear(selectedRegion.t0(), selectedRegion.t1());
      }
   }

   double seconds = selectedRegion.duration();

   selectedRegion.collapseToT0();

   ProjectHistory::Get( project ).PushState(wxString::Format(_("Deleted %.2f seconds at t=%.2f"),
                              seconds,
                              selectedRegion.t0()),
             _("Delete"));
}


void OnCopy(const CommandContext &context)
{
   auto &project = context.project;
   auto &tracks = TrackList::Get( project );
   auto &trackPanel = TrackPanel::Get( project );
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;

   for (auto lt : tracks.Selected< LabelTrack >()) {
      auto &view = LabelTrackView::Get( *lt );
      if (view.CopySelectedText( context.project )) {
         //trackPanel.Refresh(false);
         return;
      }
   }

   auto &clipboard = Clipboard::Get();
   clipboard.Clear();

   auto pNewClipboard = TrackList::Create();
   auto &newClipboard = *pNewClipboard;

   for (auto n : tracks.Selected()) {
      auto dest = n->Copy(selectedRegion.t0(),
              selectedRegion.t1());
      FinishCopy(n, dest, newClipboard);
   }

   // Survived possibility of exceptions.  Commit changes to the clipboard now.
   clipboard.Assign( std::move( newClipboard ),
      selectedRegion.t0(), selectedRegion.t1(), &project );

   //Make sure the menus/toolbar states get updated
   trackPanel.Refresh(false);
}

void OnPaste(const CommandContext &context)
{
   auto &project = context.project;
   auto &tracks = TrackList::Get( project );
   auto &trackFactory = TrackFactory::Get( project );
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
   const auto &settings = ProjectSettings::Get( project );
   auto &window = ProjectWindow::Get( project );

   auto isSyncLocked = settings.IsSyncLocked();

   // Handle text paste (into active label) first.
   if (DoPasteText(project))
      return;

   // If nothing's selected, we just insert NEW tracks.
   if (DoPasteNothingSelected(project))
      return;

   const auto &clipboard = Clipboard::Get();
   auto clipTrackRange = clipboard.GetTracks().Any< const Track >();
   if (clipTrackRange.empty())
      return;

   // Otherwise, paste into the selected tracks.
   double t0 = selectedRegion.t0();
   double t1 = selectedRegion.t1();

   auto pN = tracks.Any().begin();

   Track *ff = NULL;
   const Track *lastClipBeforeMismatch = NULL;
   const Track *mismatchedClip = NULL;
   const Track *prevClip = NULL;

   bool bAdvanceClipboard = true;
   bool bPastedSomething = false;

   auto pC = clipTrackRange.begin();
   size_t nnChannels=0, ncChannels=0;
   while (*pN && *pC) {
      auto n = *pN;
      auto c = *pC;
      if (n->GetSelected()) {
         bAdvanceClipboard = true;
         if (mismatchedClip)
            c = mismatchedClip;
         if (!c->SameKindAs(*n)) {
            if (!mismatchedClip) {
               lastClipBeforeMismatch = prevClip;
               mismatchedClip = c;
            }
            bAdvanceClipboard = false;
            c = lastClipBeforeMismatch;


            // If the types still don't match...
            while (c && !c->SameKindAs(*n)) {
               prevClip = c;
               c = * ++ pC;
            }
         }

         // Handle case where the first track in clipboard
         // is of different type than the first selected track
         if (!c) {
            c = mismatchedClip;
            while (n && (!c->SameKindAs(*n) || !n->GetSelected()))
            {
               // Must perform sync-lock adjustment before incrementing n
               if (n->IsSyncLockSelected()) {
                  auto newT1 = t0 + clipboard.Duration();
                  if (t1 != newT1 && t1 <= n->GetEndTime()) {
                     n->SyncLockAdjust(t1, newT1);
                     bPastedSomething = true;
                  }
               }
               n = * ++ pN;
            }
            if (!n)
               c = NULL;
         }

         // The last possible case for cross-type pastes: triggered when we try
         // to paste 1+ tracks from one type into 1+ tracks of another type. If
         // there's a mix of types, this shouldn't run.
         if (!c)
            // Throw, so that any previous changes to the project in this loop
            // are discarded.
            throw SimpleMessageBoxException{
               _("Pasting one type of track into another is not allowed.")
            };

         // We should need this check only each time we visit the leading
         // channel
         if ( n->IsLeader() ) {
            wxASSERT( c->IsLeader() ); // the iteration logic should ensure this

            auto cChannels = TrackList::Channels(c);
            ncChannels = cChannels.size();
            auto nChannels = TrackList::Channels(n);
            nnChannels = nChannels.size();

            // When trying to copy from stereo to mono track, show error and
            // exit
            // TODO: Automatically offer user to mix down to mono (unfortunately
            //       this is not easy to implement
            if (ncChannels > nnChannels)
            {
               if (ncChannels > 2) {
                  // TODO: more-than-two-channels-message
                  // Re-word the error message
               }
               // else

               // Throw, so that any previous changes to the project in this
               // loop are discarded.
               throw SimpleMessageBoxException{
                  _("Copying stereo audio into a mono track is not allowed.")
               };
            }
         }

         if (!ff)
            ff = n;
         
         wxASSERT( n && c && n->SameKindAs(*c) );
         Maybe<WaveTrack::Locker> locker;

         n->TypeSwitch(
            [&](WaveTrack *wn){
               const auto wc = static_cast<const WaveTrack *>(c);
               if (clipboard.Project() != &project)
                  // Cause duplication of block files on disk, when copy is
                  // between projects
                  locker.create(wc);
               bPastedSomething = true;
               wn->ClearAndPaste(t0, t1, wc, true, true);
            },
            [&](LabelTrack *ln){
               // Per Bug 293, users expect labels to move on a paste into
               // a label track.
               ln->Clear(t0, t1);

               ln->ShiftLabelsOnInsert( clipboard.Duration(), t0 );

               bPastedSomething |= ln->PasteOver(t0, c);
            },
            [&](Track *){
               bPastedSomething = true;
               n->Clear(t0, t1);
               n->Paste(t0, c);
            }
         );

         --nnChannels;
         --ncChannels;

         // When copying from mono to stereo track, paste the wave form
         // to both channels
         // TODO: more-than-two-channels
         // This will replicate the last pasted channel as many times as needed
         while (nnChannels > 0 && ncChannels == 0)
         {
            n = * ++ pN;
            --nnChannels;

            n->TypeSwitch(
               [&](WaveTrack *wn){
                  bPastedSomething = true;
                  // Note:  rely on locker being still be in scope!
                  wn->ClearAndPaste(t0, t1, c, true, true);
               },
               [&](Track *){
                  n->Clear(t0, t1);
                  bPastedSomething = true;
                  n->Paste(t0, c);
               }
            );
         }

         if (bAdvanceClipboard) {
            prevClip = c;
            c = * ++ pC;
         }
      } // if (n->GetSelected())
      else if (n->IsSyncLockSelected())
      {
         auto newT1 = t0 + clipboard.Duration();
         if (t1 != newT1 && t1 <= n->GetEndTime()) {
            n->SyncLockAdjust(t1, newT1);
            bPastedSomething = true;
         }
      }
      ++pN;
   }

   // This block handles the cases where our clipboard is smaller
   // than the amount of selected destination tracks. We take the
   // last wave track, and paste that one into the remaining
   // selected tracks.
   if ( *pN && ! *pC )
   {
      const auto wc =
         *clipboard.GetTracks().Any< const WaveTrack >().rbegin();
      Maybe<WaveTrack::Locker> locker;
      if (clipboard.Project() != &project && wc)
         // Cause duplication of block files on disk, when copy is
         // between projects
         locker.create(static_cast<const WaveTrack*>(wc));

      tracks.Any().StartingWith(*pN).Visit(
         [&](WaveTrack *wt, const Track::Fallthrough &fallthrough) {
            if (!wt->GetSelected())
               return fallthrough();

            if (wc) {
               bPastedSomething = true;
               wt->ClearAndPaste(t0, t1, wc, true, true);
            }
            else {
               auto tmp = trackFactory.NewWaveTrack(
                  wt->GetSampleFormat(), wt->GetRate());
               tmp->InsertSilence( 0.0,
                  // MJS: Is this correct?
                  clipboard.Duration() );
               tmp->Flush();

               bPastedSomething = true;
               wt->ClearAndPaste(t0, t1, tmp.get(), true, true);
            }
         },
         [&](LabelTrack *lt, const Track::Fallthrough &fallthrough) {
            if (!lt->GetSelected())
               return fallthrough();

            lt->Clear(t0, t1);

            // As above, only shift labels if sync-lock is on.
            if (isSyncLocked)
               lt->ShiftLabelsOnInsert(
                  clipboard.Duration(), t0);
         },
         [&](Track *n) {
            if (n->IsSyncLockSelected())
               n->SyncLockAdjust(t1, t0 + clipboard.Duration() );
         }
      );
   }

   // TODO: What if we clicked past the end of the track?

   if (bPastedSomething)
   {
      selectedRegion.setT1( t0 + clipboard.Duration() );

      ProjectHistory::Get( project )
         .PushState(_("Pasted from the clipboard"), _("Paste"));

      if (ff)
         ff->EnsureVisible();
   }
}

void OnDuplicate(const CommandContext &context)
{
   auto &project = context.project;
   auto &tracks = TrackList::Get( project );
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
   auto &window = ProjectWindow::Get( project );

   // This iteration is unusual because we add to the list inside the loop
   auto range = tracks.Selected();
   auto last = *range.rbegin();
   for (auto n : range) {
      // Make copies not for clipboard but for direct addition to the project
      auto dest = n->Copy(selectedRegion.t0(),
              selectedRegion.t1(), false);
      dest->Init(*n);
      dest->SetOffset(wxMax(selectedRegion.t0(), n->GetOffset()));
      tracks.Add( dest );

      // This break is really needed, else we loop infinitely
      if (n == last)
         break;
   }

   ProjectHistory::Get( project ).PushState(_("Duplicated"), _("Duplicate"));
}

void OnSplitCut(const CommandContext &context)
{
   auto &project = context.project;
   auto &tracks = TrackList::Get( project );
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
   auto &window = ProjectWindow::Get( project );

   auto &clipboard = Clipboard::Get();
   clipboard.Clear();

   auto pNewClipboard = TrackList::Create();
   auto &newClipboard = *pNewClipboard;

   Track::Holder dest;

   tracks.Selected().Visit(
      [&](WaveTrack *n) {
         dest = n->SplitCut(
            selectedRegion.t0(),
            selectedRegion.t1());
         if (dest)
            FinishCopy(n, dest, newClipboard);
      },
      [&](Track *n) {
         dest = n->Copy(selectedRegion.t0(),
                 selectedRegion.t1());
         n->Silence(selectedRegion.t0(),
                    selectedRegion.t1());
         if (dest)
            FinishCopy(n, dest, newClipboard);
      }
   );

   // Survived possibility of exceptions.  Commit changes to the clipboard now.
   clipboard.Assign( std::move( newClipboard ),
      selectedRegion.t0(), selectedRegion.t1(), &project );

   ProjectHistory::Get( project ).PushState(_("Split-cut to the clipboard"), _("Split Cut"));
}

void OnSplitDelete(const CommandContext &context)
{
   auto &project = context.project;
   auto &tracks = TrackList::Get( project );
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
   auto &window = ProjectWindow::Get( project );

   tracks.Selected().Visit(
      [&](WaveTrack *wt) {
         wt->SplitDelete(selectedRegion.t0(),
                         selectedRegion.t1());
      },
      [&](Track *n) {
         n->Silence(selectedRegion.t0(),
                    selectedRegion.t1());
      }
   );

   ProjectHistory::Get( project ).PushState(
      wxString::Format(_("Split-deleted %.2f seconds at t=%.2f"),
         selectedRegion.duration(),
         selectedRegion.t0()),
      _("Split Delete"));
}

void OnSilence(const CommandContext &context)
{
   auto &project = context.project;
   auto &tracks = TrackList::Get( project );
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;

   for ( auto n : tracks.Selected< WaveTrack >() )
      n->Silence(selectedRegion.t0(), selectedRegion.t1());

   ProjectHistory::Get( project ).PushState(
      wxString::Format(_("Silenced selected tracks for %.2f seconds at %.2f"),
         selectedRegion.duration(),
         selectedRegion.t0()),
      _("Silence"));
}

void OnTrim(const CommandContext &context)
{
   auto &project = context.project;
   auto &tracks = TrackList::Get( project );
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
   auto &window = ProjectWindow::Get( project );

   if (selectedRegion.isPoint())
      return;

   tracks.Selected().Visit(
      [&](WaveTrack *wt) {
         //Delete the section before the left selector
         wt->Trim(selectedRegion.t0(),
            selectedRegion.t1());
      }
   );

   ProjectHistory::Get( project ).PushState(
      wxString::Format(
         _("Trim selected audio tracks from %.2f seconds to %.2f seconds"),
         selectedRegion.t0(), selectedRegion.t1()),
         _("Trim Audio"));
}

void OnSplit(const CommandContext &context)
{
   auto &project = context.project;
   auto &tracks = TrackList::Get( project );
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;

   double sel0 = selectedRegion.t0();
   double sel1 = selectedRegion.t1();

   for (auto wt : tracks.Selected< WaveTrack >())
      wt->Split( sel0, sel1 );

   ProjectHistory::Get( project ).PushState(_("Split"), _("Split"));
#if 0
//ANSWER-ME: Do we need to keep this commented out OnSplit() code?
// This whole section no longer used...
   /*
    * Previous (pre-multiclip) implementation of "Split" command
    * This does work only when a range is selected!
    *
   TrackListIterator iter(tracks);

   Track *n = iter.First();
   Track *dest;

   TrackList newTracks;

   while (n) {
      if (n->GetSelected()) {
         double sel0 = selectedRegion.t0();
         double sel1 = selectedRegion.t1();

         dest = n->Copy(sel0, sel1);
         dest->Init(*n);
         dest->SetOffset(wxMax(sel0, n->GetOffset()));

         if (sel1 >= n->GetEndTime())
            n->Clear(sel0, sel1);
         else if (sel0 <= n->GetOffset()) {
            n->Clear(sel0, sel1);
            n->SetOffset(sel1);
         } else
            n->Silence(sel0, sel1);

         newTracks.Add(dest);
      }
      n = iter.Next();
   }

   TrackListIterator nIter(&newTracks);
   n = nIter.First();
   while (n) {
      tracks->Add(n);
      n = nIter.Next();
   }

   PushState(_("Split"), _("Split"));
   */
#endif
}

void OnSplitNew(const CommandContext &context)
{
   auto &project = context.project;
   auto &tracks = TrackList::Get( project );
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
   auto &window = ProjectWindow::Get( project );

   Track::Holder dest;

   // This iteration is unusual because we add to the list inside the loop
   auto range = tracks.Selected();
   auto last = *range.rbegin();
   for (auto track : range) {
      track->TypeSwitch(
         [&](WaveTrack *wt) {
            // Clips must be aligned to sample positions or the NEW clip will
            // not fit in the gap where it came from
            double offset = wt->GetOffset();
            offset = wt->LongSamplesToTime(wt->TimeToLongSamples(offset));
            double newt0 = wt->LongSamplesToTime(wt->TimeToLongSamples(
               selectedRegion.t0()));
            double newt1 = wt->LongSamplesToTime(wt->TimeToLongSamples(
               selectedRegion.t1()));
            dest = wt->SplitCut(newt0, newt1);
            if (dest) {
               dest->SetOffset(wxMax(newt0, offset));
               FinishCopy(wt, dest, tracks);
            }
         }
#if 0
         ,
         // LL:  For now, just skip all non-wave tracks since the other do not
         //      yet support proper splitting.
         [&](Track *n) {
            dest = n->Cut(viewInfo.selectedRegion.t0(),
                   viewInfo.selectedRegion.t1());
            if (dest) {
               dest->SetOffset(wxMax(0, n->GetOffset()));
               FinishCopy(n, dest, *tracks);
            }
         }
#endif
      );
      if (track == last)
         break;
   }

   ProjectHistory::Get( project )
      .PushState(_("Split to new track"), _("Split New"));
}

void OnJoin(const CommandContext &context)
{
   auto &project = context.project;
   auto &tracks = TrackList::Get( project );
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
   auto &window = ProjectWindow::Get( project );

   for (auto wt : tracks.Selected< WaveTrack >())
      wt->Join(selectedRegion.t0(),
               selectedRegion.t1());

   ProjectHistory::Get( project ).PushState(
      wxString::Format(_("Joined %.2f seconds at t=%.2f"),
         selectedRegion.duration(),
         selectedRegion.t0()),
      _("Join"));
}

void OnDisjoin(const CommandContext &context)
{
   auto &project = context.project;
   auto &tracks = TrackList::Get( project );
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
   auto &window = ProjectWindow::Get( project );

   for (auto wt : tracks.Selected< WaveTrack >())
      wt->Disjoin(selectedRegion.t0(),
                  selectedRegion.t1());

   ProjectHistory::Get( project ).PushState(
      wxString::Format(_("Detached %.2f seconds at t=%.2f"),
         selectedRegion.duration(),
         selectedRegion.t0()),
      _("Detach"));
}

void OnEditMetadata(const CommandContext &context)
{
   auto &project = context.project;
   (void)Exporter::DoEditMetadata( project,
      _("Edit Metadata Tags"), _("Metadata Tags"), true);
}

void OnPreferences(const CommandContext &context)
{
   auto &project = context.project;

   GlobalPrefsDialog dialog(&GetProjectFrame( project ) /* parent */ );

   if( ScreenshotCommand::MayCapture( &dialog ) )
      return;

   if (!dialog.ShowModal()) {
      // Canceled
      return;
   }

   // LL:  Moved from PrefsDialog since wxWidgets on OSX can't deal with
   //      rebuilding the menus while the PrefsDialog is still in the modal
   //      state.
   for (auto p : AllProjects{}) {
      MenuManager::Get(*p).RebuildMenuBar(*p);
// TODO: The comment below suggests this workaround is obsolete.
#if defined(__WXGTK__)
      // Workaround for:
      //
      //   http://bugzilla.audacityteam.org/show_bug.cgi?id=458
      //
      // This workaround should be removed when Audacity updates to wxWidgets
      // 3.x which has a fix.
      auto &window = GetProjectFrame( *p );
      wxRect r = window.GetRect();
      window.SetSize(wxSize(1,1));
      window.SetSize(r.GetSize());
#endif
   }
}

// Legacy functions, not used as of version 2.3.0

#if 0
void OnPasteOver(const CommandContext &context)
{
   auto &project = context.project;
   auto &selectedRegion = project.GetViewInfo().selectedRegion;

   if((AudacityProject::msClipT1 - AudacityProject::msClipT0) > 0.0)
   {
      selectedRegion.setT1(
         selectedRegion.t0() +
         (AudacityProject::msClipT1 - AudacityProject::msClipT0));
         // MJS: pointless, given what we do in OnPaste?
   }
   OnPaste(context);

   return;
}
#endif

}; // struct Handler

} // namespace

static CommandHandlerObject &findCommandHandler(AudacityProject &) {
   // Handler is not stateful.  Doesn't need a factory registered with
   // AudacityProject.
   static EditActions::Handler instance;
   return instance;
};

// Menu definitions

#define FN(X) findCommandHandler, \
   static_cast<CommandFunctorPointer>(& EditActions::Handler :: X)
#define XXO(X) _(X), wxString{X}.Contains("...")

MenuTable::BaseItemPtr LabelEditMenus( AudacityProject &project );

const ReservedCommandFlag
   CutCopyAvailableFlag{
      [](const AudacityProject &project){
         auto range = TrackList::Get( project ).Any<const LabelTrack>()
            + [&](const LabelTrack *pTrack){
               return LabelTrackView::Get( *pTrack ).IsTextSelected(
                  // unhappy const_cast because track focus might be set
                  const_cast<AudacityProject&>(project)
               );
            };
         if ( !range.empty() )
            return true;

         if (
            TimeSelectedPred( project )
         &&
            TracksSelectedPred( project )
         )
            return true;

         return false;
      },
      cutCopyOptions
   };

MenuTable::BaseItemPtr EditMenu( AudacityProject & )
{
   using namespace MenuTable;
   using Options = CommandManager::Options;

   static const auto NotBusyTimeAndTracksFlags =
      AudioIONotBusyFlag | TimeSelectedFlag | TracksSelectedFlag;

   // The default shortcut key for Redo is different on different platforms.
   static constexpr auto redoKey =
#ifdef __WXMSW__
      wxT("Ctrl+Y")
#else
      wxT("Ctrl+Shift+Z")
#endif
   ;

      // The default shortcut key for Preferences is different on different
      // platforms.
   static constexpr auto prefKey =
#ifdef __WXMAC__
      wxT("Ctrl+,")
#else
      wxT("Ctrl+P")
#endif
   ;

   return Menu( _("&Edit"),
      Command( wxT("Undo"), XXO("&Undo"), FN(OnUndo),
         AudioIONotBusyFlag | UndoAvailableFlag, wxT("Ctrl+Z") ),

      Command( wxT("Redo"), XXO("&Redo"), FN(OnRedo),
         AudioIONotBusyFlag | RedoAvailableFlag, redoKey ),
         
      Special( [](AudacityProject &project, wxMenu&) {
         // Change names in the CommandManager as a side-effect
         MenuManager::ModifyUndoMenuItems(project);
      }),

      Separator(),

      // Basic Edit commands
      /* i18n-hint: (verb)*/
      Command( wxT("Cut"), XXO("Cu&t"), FN(OnCut),
         AudioIONotBusyFlag | CutCopyAvailableFlag | NoAutoSelect,
         wxT("Ctrl+X") ),
      Command( wxT("Delete"), XXO("&Delete"), FN(OnDelete),
         AudioIONotBusyFlag | TracksSelectedFlag | TimeSelectedFlag | NoAutoSelect,
         wxT("Ctrl+K") ),
      /* i18n-hint: (verb)*/
      Command( wxT("Copy"), XXO("&Copy"), FN(OnCopy),
         AudioIONotBusyFlag | CutCopyAvailableFlag, wxT("Ctrl+C") ),
      /* i18n-hint: (verb)*/
      Command( wxT("Paste"), XXO("&Paste"), FN(OnPaste),
         AudioIONotBusyFlag, wxT("Ctrl+V") ),
      /* i18n-hint: (verb)*/
      Command( wxT("Duplicate"), XXO("Duplic&ate"), FN(OnDuplicate),
         NotBusyTimeAndTracksFlags, wxT("Ctrl+D") ),

      Separator(),

      Menu( _("R&emove Special"),
         /* i18n-hint: (verb) Do a special kind of cut*/
         Command( wxT("SplitCut"), XXO("Spl&it Cut"), FN(OnSplitCut),
            NotBusyTimeAndTracksFlags,
            Options{ wxT("Ctrl+Alt+X") }.UseStrictFlags() ),
         /* i18n-hint: (verb) Do a special kind of DELETE*/
         Command( wxT("SplitDelete"), XXO("Split D&elete"), FN(OnSplitDelete),
            NotBusyTimeAndTracksFlags,
            Options{ wxT("Ctrl+Alt+K") }.UseStrictFlags() ),

         Separator(),

         /* i18n-hint: (verb)*/
         Command( wxT("Silence"), XXO("Silence Audi&o"), FN(OnSilence),
            AudioIONotBusyFlag | TimeSelectedFlag | WaveTracksSelectedFlag,
            wxT("Ctrl+L") ),
         /* i18n-hint: (verb)*/
         Command( wxT("Trim"), XXO("Tri&m Audio"), FN(OnTrim),
            AudioIONotBusyFlag | TimeSelectedFlag | WaveTracksSelectedFlag,
            Options{ wxT("Ctrl+T") }.UseStrictFlags() )
      ),

      Separator(),

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

      Menu( _("Clip B&oundaries"),
         /* i18n-hint: (verb) It's an item on a menu. */
         Command( wxT("Split"), XXO("Sp&lit"), FN(OnSplit),
            AudioIONotBusyFlag | WaveTracksSelectedFlag,
            Options{ wxT("Ctrl+I") }.UseStrictFlags() ),
         Command( wxT("SplitNew"), XXO("Split Ne&w"), FN(OnSplitNew),
            AudioIONotBusyFlag | TimeSelectedFlag | WaveTracksSelectedFlag,
            Options{ wxT("Ctrl+Alt+I") }.UseStrictFlags() ),

         Separator(),

         /* i18n-hint: (verb)*/
         Command( wxT("Join"), XXO("&Join"), FN(OnJoin),
            NotBusyTimeAndTracksFlags, wxT("Ctrl+J") ),
         Command( wxT("Disjoin"), XXO("Detac&h at Silences"), FN(OnDisjoin),
            NotBusyTimeAndTracksFlags, wxT("Ctrl+Alt+J") )
      ),

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

      LabelEditMenus,

      Command( wxT("EditMetaData"), XXO("&Metadata..."), FN(OnEditMetadata),
         AudioIONotBusyFlag ),

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

#ifndef __WXMAC__
      Separator(),
#endif

      Command( wxT("Preferences"), XXO("Pre&ferences..."), FN(OnPreferences),
         AudioIONotBusyFlag, prefKey )
   );
}

MenuTable::BaseItemPtr ExtraEditMenu( AudacityProject & )
{
   using namespace MenuTable;
   using Options = CommandManager::Options;
   static const auto flags =
      AudioIONotBusyFlag | TracksSelectedFlag | TimeSelectedFlag;
   return Menu( _("&Edit"),
      Command( wxT("DeleteKey"), XXO("&Delete Key"), FN(OnDelete),
         (flags | NoAutoSelect),
         wxT("Backspace") ),
      Command( wxT("DeleteKey2"), XXO("Delete Key&2"), FN(OnDelete),
         (flags | NoAutoSelect),
         wxT("Delete") )
   );
}

auto canSelectAll = [](const AudacityProject &project){
   return MenuManager::Get( project ).mWhatIfNoSelection != 0; };
auto selectAll = []( AudacityProject &project, CommandFlag flagsRqd ){
   if ( MenuManager::Get( project ).mWhatIfNoSelection == 1 &&
      (flagsRqd & NoAutoSelect).none() )
      SelectUtilities::DoSelectAllAudio(project);
};

RegisteredMenuItemEnabler selectTracks{{
   []{ return TracksExistFlag; },
   []{ return TracksSelectedFlag; },
   canSelectAll,
   selectAll
}};

// Including time tracks.
RegisteredMenuItemEnabler selectAnyTracks{{
   []{ return TracksExistFlag; },
   []{ return AnyTracksSelectedFlag; },
   canSelectAll,
   selectAll
}};

RegisteredMenuItemEnabler selectWaveTracks{{
   []{ return WaveTracksExistFlag; },
   []{ return TimeSelectedFlag | WaveTracksSelectedFlag | CutCopyAvailableFlag; },
   canSelectAll,
   selectAll
}};

// Also enable select for the noise reduction case.
RegisteredMenuItemEnabler selectWaveTracks2{{
   []{ return WaveTracksExistFlag; },
   []{ return NoiseReductionTimeSelectedFlag | WaveTracksSelectedFlag | CutCopyAvailableFlag; },
   canSelectAll,
   selectAll
}};

#undef XXO
#undef FN
