#include "../Audacity.h"
#include "../Experimental.h"

#include "../AdornedRulerPanel.h"
#include "../AudioIO.h"
#include "../CommonCommandFlags.h"
#include "../SpectrumAnalyst.h"
#include "../Prefs.h"
#include "../Project.h"
#include "../ProjectAudioIO.h"
#include "../ProjectAudioManager.h"
#include "../ProjectHistory.h"
#include "../ProjectSelectionManager.h"
#include "../ProjectSettings.h"
#include "../ProjectWindow.h"
#include "../SelectUtilities.h"
#include "../TimeDialog.h"
#include "../TrackPanel.h"
#include "../WaveTrack.h"
#include "../commands/CommandContext.h"
#include "../commands/CommandManager.h"
#include "../toolbars/ControlToolBar.h"
#include "../tracks/ui/SelectHandle.h"
#include "../tracks/playabletrack/wavetrack/ui/WaveTrackView.h"
#include "../tracks/playabletrack/wavetrack/ui/WaveTrackViewConstants.h"

// private helper classes and functions
namespace {

void DoNextPeakFrequency(AudacityProject &project, bool up)
{
   auto &tracks = TrackList::Get( project );
   auto &viewInfo = ViewInfo::Get( project );

   // Find the first selected wave track that is in a spectrogram view.
   const WaveTrack *pTrack {};
   for ( auto wt : tracks.Selected< const WaveTrack >() ) {
      const auto displays = WaveTrackView::Get( *wt ).GetDisplays();
      bool hasSpectrum = (displays.end() != std::find(
         displays.begin(), displays.end(),
         WaveTrackSubView::Type{ WaveTrackViewConstants::Spectrum, {} }
      ) );
      if ( hasSpectrum ) {
         pTrack = wt;
         break;
      }
   }

   if (pTrack) {
      SpectrumAnalyst analyst;
      SelectHandle::SnapCenterOnce(analyst, viewInfo, pTrack, up);
      ProjectHistory::Get( project ).ModifyState(false);
   }
}

double NearestZeroCrossing
(AudacityProject &project, double t0)
{
   const auto &settings = ProjectSettings::Get( project );
   auto rate = settings.GetRate();
   auto &tracks = TrackList::Get( project );

   // Window is 1/100th of a second.
   auto windowSize = size_t(std::max(1.0, rate / 100));
   Floats dist{ windowSize, true };

   int nTracks = 0;
   for (auto one : tracks.Selected< const WaveTrack >()) {
      auto oneWindowSize = size_t(std::max(1.0, one->GetRate() / 100));
      Floats oneDist{ oneWindowSize };
      auto s = one->TimeToLongSamples(t0);
      // fillTwo to ensure that missing values are treated as 2, and hence do
      // not get used as zero crossings.
      one->Get((samplePtr)oneDist.get(), floatSample,
               s - (int)oneWindowSize/2, oneWindowSize, fillTwo);


      // Looking for actual crossings.
      double prev = 2.0;
      for(size_t i=0; i<oneWindowSize; i++){
         float fDist = fabs( oneDist[i]); // score is absolute value
         if( prev * oneDist[i] > 0 ) // both same sign?  No good.
            fDist = fDist + 0.4; // No good if same sign.
         else if( prev > 0.0 )
            fDist = fDist + 0.1; // medium penalty for downward crossing.
         prev = oneDist[i];
         oneDist[i] = fDist;
      }

      // TODO: The mixed rate zero crossing code is broken,
      // if oneWindowSize > windowSize we'll miss out some
      // samples - so they will still be zero, so we'll use them.
      for(size_t i = 0; i < windowSize; i++) {
         size_t j;
         if (windowSize != oneWindowSize)
            j = i * (oneWindowSize-1) / (windowSize-1);
         else
            j = i;

         dist[i] += oneDist[j];
         // Apply a small penalty for distance from the original endpoint
         // We'll always prefer an upward  
         dist[i] +=
            0.1 * (abs(int(i) - int(windowSize/2))) / float(windowSize/2);
      }
      nTracks++;
   }

   // Find minimum
   int argmin = 0;
   float min = 3.0;
   for(size_t i=0; i<windowSize; i++) {
      if (dist[i] < min) {
         argmin = i;
         min = dist[i];
      }
   }

   // If we're worse than 0.2 on average, on one track, then no good.
   if(( nTracks == 1 ) && ( min > (0.2*nTracks) ))
      return t0;
   // If we're worse than 0.6 on average, on multi-track, then no good.
   if(( nTracks > 1 ) && ( min > (0.6*nTracks) ))
      return t0;

   return t0 + (argmin - (int)windowSize/2) / rate;
}

// If this returns true, then there was a key up, and nothing more to do,
// after this function has completed.
// (at most this function just does a ModifyState for the keyup)
bool OnlyHandleKeyUp( const CommandContext &context )
{
   auto &project = context.project;
   auto evt = context.pEvt;
   bool bKeyUp = (evt) && evt->GetEventType() == wxEVT_KEY_UP;

   if( ProjectAudioIO::Get( project ).IsAudioActive() )
      return bKeyUp;
   if( !bKeyUp )
      return false;

   ProjectHistory::Get( project ).ModifyState(false);
   return true;
}

enum CursorDirection {
   DIRECTION_LEFT = -1,
   DIRECTION_RIGHT = +1
};

enum SelectionOperation {
    SELECTION_EXTEND,
    SELECTION_CONTRACT,
    CURSOR_MOVE
};

enum TimeUnit {
    TIME_UNIT_SECONDS,
    TIME_UNIT_PIXELS
};

struct SeekInfo
{
   wxLongLong mLastSelectionAdjustment { ::wxGetUTCTimeMillis() };
   double mSeekShort{ 0.0 };
   double mSeekLong{ 0.0 };
};

void SeekWhenAudioActive(double seekStep, wxLongLong &lastSelectionAdjustment)
{
   auto gAudioIO = AudioIO::Get();
#ifdef EXPERIMENTAL_IMPROVED_SEEKING
   if (gAudioIO->GetLastPlaybackTime() < lastSelectionAdjustment) {
      // Allow time for the last seek to output a buffer before
      // discarding samples again
      // Do not advance mLastSelectionAdjustment
      return;
   }
#endif
   lastSelectionAdjustment = ::wxGetUTCTimeMillis();

   gAudioIO->SeekStream(seekStep);
}

// Handles moving a selection edge with the keyboard in snap-to-time mode;
// returns the moved value.
// Will move at least minPix pixels -- set minPix positive to move forward,
// negative to move backward.
// Helper for moving by keyboard with snap-to-grid enabled
double GridMove
(AudacityProject &project, double t, int minPix)
{
   const auto &settings = ProjectSettings::Get( project );
   auto rate = settings.GetRate();
   auto &viewInfo = ViewInfo::Get( project );
   auto format = settings.GetSelectionFormat();

   NumericConverter nc(NumericConverter::TIME, format, t, rate);

   // Try incrementing/decrementing the value; if we've moved far enough we're
   // done
   double result;
   minPix >= 0 ? nc.Increment() : nc.Decrement();
   result = nc.GetValue();
   if (std::abs(viewInfo.TimeToPosition(result) - viewInfo.TimeToPosition(t))
       >= abs(minPix))
       return result;

   // Otherwise, move minPix pixels, then snap to the time.
   result = viewInfo.OffsetTimeByPixels(t, minPix);
   nc.SetValue(result);
   result = nc.GetValue();
   return result;
}

double OffsetTime
(AudacityProject &project,
 double t, double offset, TimeUnit timeUnit, int snapToTime)
{
   auto &viewInfo = ViewInfo::Get( project );

    if (timeUnit == TIME_UNIT_SECONDS)
        return t + offset; // snapping is currently ignored for non-pixel moves

    if (snapToTime == SNAP_OFF)
        return viewInfo.OffsetTimeByPixels(t, (int)offset);

    return GridMove(project, t, (int)offset);
}

// Moving a cursor, and collapsed selection.
void MoveWhenAudioInactive
(AudacityProject &project, double seekStep, TimeUnit timeUnit)
{
   auto &viewInfo = ViewInfo::Get( project );
   auto &trackPanel = TrackPanel::Get( project );
   auto &tracks = TrackList::Get( project );
   auto &ruler = AdornedRulerPanel::Get( project );
   const auto &settings = ProjectSettings::Get( project );
   auto &window = ProjectWindow::Get( project );

   // If TIME_UNIT_SECONDS, snap-to will be off.
   int snapToTime = settings.GetSnapTo();
   const double t0 = viewInfo.selectedRegion.t0();
   const double end = std::max( 
      tracks.GetEndTime(),
      viewInfo.GetScreenEndTime());

   // Move the cursor
   // Already in cursor mode?
   if( viewInfo.selectedRegion.isPoint() )
   {
      double newT = OffsetTime(project,
         t0, seekStep, timeUnit, snapToTime);
      // constrain.
      newT = std::max(0.0, newT);
      newT = std::min(newT, end);
      // Move 
      viewInfo.selectedRegion.setT0(
         newT,
         false); // do not swap selection boundaries
      viewInfo.selectedRegion.collapseToT0();

      // Move the visual cursor, avoiding an unnecessary complete redraw
      trackPanel.DrawOverlays(false);
      ruler.DrawOverlays(false);
   }
   else
   {
      // Transition to cursor mode.
      if( seekStep < 0 )
         viewInfo.selectedRegion.collapseToT0();
      else
         viewInfo.selectedRegion.collapseToT1();
      trackPanel.Refresh(false);
   }

   // Make sure NEW position is in view
   window.ScrollIntoView(viewInfo.selectedRegion.t1());
   return;
}

void SeekWhenAudioInactive
(AudacityProject &project, double seekStep, TimeUnit timeUnit,
SelectionOperation operation)
{
   auto &viewInfo = ViewInfo::Get( project );
   auto &tracks = TrackList::Get( project );
   const auto &settings = ProjectSettings::Get( project );
   auto &window = ProjectWindow::Get( project );

   if( operation == CURSOR_MOVE )
   {
      MoveWhenAudioInactive( project, seekStep, timeUnit);
      return;
   }

   int snapToTime = settings.GetSnapTo();
   const double t0 = viewInfo.selectedRegion.t0();
   const double t1 = viewInfo.selectedRegion.t1();
   const double end = std::max( 
      tracks.GetEndTime(),
      viewInfo.GetScreenEndTime());

   // Is it t0 or t1 moving?
   bool bMoveT0 = (operation == SELECTION_CONTRACT && seekStep > 0) ||
	   (operation == SELECTION_EXTEND && seekStep < 0);
   // newT is where we want to move to
   double newT = OffsetTime( project,
      bMoveT0 ? t0 : t1, seekStep, timeUnit, snapToTime);
   // constrain to be in the track/screen limits.
   newT = std::max( 0.0, newT );
   newT = std::min( newT, end);
   // optionally constrain to be a contraction, i.e. so t0/t1 do not cross over
   if( operation == SELECTION_CONTRACT )
      newT = bMoveT0 ? std::min( t1, newT ) : std::max( t0, newT );

   // Actually move
   if( bMoveT0 )
      viewInfo.selectedRegion.setT0( newT );
   else 
      viewInfo.selectedRegion.setT1( newT );

   // Ensure it is visible
   window.ScrollIntoView(newT);
}

// Handle small cursor and play head movements
void SeekLeftOrRight
(AudacityProject &project, double direction, SelectionOperation operation,
 SeekInfo &info)
{
   // PRL:  What I found and preserved, strange though it be:
   // During playback:  jump depends on preferences and is independent of the
   // zoom and does not vary if the key is held
   // Else: jump depends on the zoom and gets bigger if the key is held

   if( ProjectAudioIO::Get( project ).IsAudioActive() )
   {
      if( operation == CURSOR_MOVE )
         SeekWhenAudioActive( info.mSeekShort * direction,
            info.mLastSelectionAdjustment );
      else if( operation == SELECTION_EXTEND )
         SeekWhenAudioActive( info.mSeekLong * direction,
            info.mLastSelectionAdjustment );
      // Note: no action for CURSOR_CONTRACT
      return;
   }

   // If the last adjustment was very recent, we are
   // holding the key down and should move faster.
   const wxLongLong curtime = ::wxGetUTCTimeMillis();
   enum { MIN_INTERVAL = 50 };
   const bool fast =
      (curtime - info.mLastSelectionAdjustment < MIN_INTERVAL);

   info.mLastSelectionAdjustment = curtime;

   // How much faster should the cursor move if shift is down?
   enum { LARGER_MULTIPLIER = 4 };
   const double seekStep = (fast ? LARGER_MULTIPLIER : 1.0) * direction;

   SeekWhenAudioInactive( project, seekStep, TIME_UNIT_PIXELS, operation);
}

// Move the cursor forward or backward, while paused or while playing.
void DoCursorMove(
   AudacityProject &project, double seekStep,
   wxLongLong &lastSelectionAdjustment)
{
   if (ProjectAudioIO::Get( project ).IsAudioActive()) {
      SeekWhenAudioActive(seekStep, lastSelectionAdjustment);
   }
   else
   {
      lastSelectionAdjustment = ::wxGetUTCTimeMillis();
      MoveWhenAudioInactive(project, seekStep, TIME_UNIT_SECONDS);
   }

   ProjectHistory::Get( project ).ModifyState(false);
}

void DoBoundaryMove(AudacityProject &project, int step, SeekInfo &info)
{
   auto &viewInfo = ViewInfo::Get( project );
   auto &tracks = TrackList::Get( project );
   auto &window = ProjectWindow::Get( project );

   // step is negative, then is moving left.  step positive, moving right.
   // Move the left/right selection boundary, to expand the selection

   // If the last adjustment was very recent, we are
   // holding the key down and should move faster.
   wxLongLong curtime = ::wxGetUTCTimeMillis();
   int pixels = step;
   if( curtime - info.mLastSelectionAdjustment < 50 )
   {
      pixels *= 4;
   }
   info.mLastSelectionAdjustment = curtime;

   // we used to have a parameter boundaryContract to say if expanding or
   // contracting.  it is no longer needed.
   bool bMoveT0 = (step < 0 );// ^ boundaryContract ;

   if( ProjectAudioIO::Get( project ).IsAudioActive() )
   {
      auto gAudioIO = AudioIO::Get();
      double indicator = gAudioIO->GetStreamTime();
      if( bMoveT0 )
         viewInfo.selectedRegion.setT0(indicator, false);
      else
         viewInfo.selectedRegion.setT1(indicator);

      ProjectHistory::Get( project ).ModifyState(false);
      return;
   }

   const double t0 = viewInfo.selectedRegion.t0();
   const double t1 = viewInfo.selectedRegion.t1();
   const double end = std::max( 
      tracks.GetEndTime(),
      viewInfo.GetScreenEndTime());

   double newT = viewInfo.OffsetTimeByPixels( bMoveT0 ? t0 : t1, pixels);
   // constrain to be in the track/screen limits.
   newT = std::max( 0.0, newT );
   newT = std::min( newT, end);
   // optionally constrain to be a contraction, i.e. so t0/t1 do not cross over
   //if( boundaryContract )
   //   newT = bMoveT0 ? std::min( t1, newT ) : std::max( t0, newT );

   // Actually move
   if( bMoveT0 )
      viewInfo.selectedRegion.setT0( newT );
   else 
      viewInfo.selectedRegion.setT1( newT );

   // Ensure it is visible
   window.ScrollIntoView(newT);

   ProjectHistory::Get( project ).ModifyState(false);
}

}

namespace SelectActions {


// Menu handler functions

struct Handler
   : CommandHandlerObject // MUST be the first base class!
   , ClientData::Base
   , PrefsListener
{

void OnSelectAll(const CommandContext &context)
{
   SelectUtilities::DoSelectAll( context.project );
}

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

   selectedRegion.collapseToT0();
   SelectUtilities::SelectNone( project );
   ProjectHistory::Get( project ).ModifyState(false);
}

void OnSelectAllTracks(const CommandContext &context)
{
   auto &project = context.project;
   SelectUtilities::DoSelectTimeAndTracks( project, false, true );
}

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

   bool selected = false;
   for (auto t : tracks.Any()
         + &Track::IsSyncLockSelected - &Track::IsSelected) {
      t->SetSelected(true);
      selected = true;
   }

   if (selected)
      ProjectHistory::Get( project ).ModifyState(false);
}

//this pops up a dialog which allows the left selection to be set.
//If playing/recording is happening, it sets the left selection at
//the current play position.
void OnSetLeftSelection(const CommandContext &context)
{
   auto &project = context.project;
   auto token = ProjectAudioIO::Get( project ).GetAudioIOToken();
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
   const auto &settings = ProjectSettings::Get( project );
   auto &window = GetProjectFrame( project );

   bool bSelChanged = false;
   auto gAudioIO = AudioIOBase::Get();
   if ((token > 0) && gAudioIO->IsStreamActive(token))
   {
      double indicator = gAudioIO->GetStreamTime();
      selectedRegion.setT0(indicator, false);
      bSelChanged = true;
   }
   else
   {
      auto fmt = settings.GetSelectionFormat();
      auto rate = settings.GetRate();

      TimeDialog dlg(&window, XO("Set Left Selection Boundary"),
         fmt, rate, selectedRegion.t0(), XO("Position"));

      if (wxID_OK == dlg.ShowModal())
      {
         //Get the value from the dialog
         selectedRegion.setT0(
            std::max(0.0, dlg.GetTimeValue()), false);
         bSelChanged = true;
      }
   }

   if (bSelChanged)
   {
      ProjectHistory::Get( project ).ModifyState(false);
   }
}

void OnSetRightSelection(const CommandContext &context)
{
   auto &project = context.project;
   auto token = ProjectAudioIO::Get( project ).GetAudioIOToken();
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
   const auto &settings = ProjectSettings::Get( project );
   auto &window = GetProjectFrame( project );

   bool bSelChanged = false;
   auto gAudioIO = AudioIOBase::Get();
   if ((token > 0) && gAudioIO->IsStreamActive(token))
   {
      double indicator = gAudioIO->GetStreamTime();
      selectedRegion.setT1(indicator, false);
      bSelChanged = true;
   }
   else
   {
      auto fmt = settings.GetSelectionFormat();
      auto rate = settings.GetRate();

      TimeDialog dlg(&window, XO("Set Right Selection Boundary"),
         fmt, rate, selectedRegion.t1(), XO("Position"));

      if (wxID_OK == dlg.ShowModal())
      {
         //Get the value from the dialog
         selectedRegion.setT1(
            std::max(0.0, dlg.GetTimeValue()), false);
         bSelChanged = true;
      }
   }

   if (bSelChanged)
   {
      ProjectHistory::Get( project ).ModifyState(false);
   }
}

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

   double kWayOverToRight = std::numeric_limits<double>::max();

   auto range = tracks.Selected();
   if ( ! range )
      return;

   double minOffset = range.min( &Track::GetStartTime );

   if( minOffset >=
       (kWayOverToRight * (1 - std::numeric_limits<double>::epsilon()) ))
      return;

   selectedRegion.setT0(minOffset);

   ProjectHistory::Get( project ).ModifyState(false);
}

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

   double kWayOverToLeft = std::numeric_limits<double>::lowest();

   auto range = tracks.Selected();
   if ( ! range )
      return;

   double maxEndOffset = range.max( &Track::GetEndTime );

   if( maxEndOffset <=
       (kWayOverToLeft * (1 - std::numeric_limits<double>::epsilon()) ))
      return;

   selectedRegion.setT1(maxEndOffset);

   ProjectHistory::Get( project ).ModifyState(false);
}

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

   auto range = tracks.Selected();
   double maxEndOffset = range.max( &Track::GetEndTime );
   double minOffset = range.min( &Track::GetStartTime );

   if( maxEndOffset < minOffset)
      return;

   viewInfo.selectedRegion.setTimes( minOffset, maxEndOffset );
   ProjectHistory::Get( project ).ModifyState(false);
}

// Handler state:
SelectedRegion mRegionSave{};

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

   mRegionSave = selectedRegion;
}

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

   if ((mRegionSave.t0() == 0.0) &&
       (mRegionSave.t1() == 0.0))
      return;

   selectedRegion = mRegionSave;

   ProjectHistory::Get( project ).ModifyState(false);
}

#ifdef EXPERIMENTAL_SPECTRAL_EDITING

// Handler state:
double mLastF0{ SelectedRegion::UndefinedFrequency };
double mLastF1{ SelectedRegion::UndefinedFrequency };

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

   const double f0 = selectedRegion.f0();
   const double f1 = selectedRegion.f1();
   const bool haveSpectralSelection =
   !(f0 == SelectedRegion::UndefinedFrequency &&
     f1 == SelectedRegion::UndefinedFrequency);
   if (haveSpectralSelection)
   {
      mLastF0 = f0;
      mLastF1 = f1;
      selectedRegion.setFrequencies
      (SelectedRegion::UndefinedFrequency, SelectedRegion::UndefinedFrequency);
   }
   else
      selectedRegion.setFrequencies(mLastF0, mLastF1);

   ProjectHistory::Get( project ).ModifyState(false);
}

void OnNextHigherPeakFrequency(const CommandContext &context)
{
   auto &project = context.project;
   DoNextPeakFrequency(project, true);
}

void OnNextLowerPeakFrequency(const CommandContext &context)
{
   auto &project = context.project;
   DoNextPeakFrequency(project, false);
}
#endif

// Handler state:
bool mCursorPositionHasBeenStored{ false };
double mCursorPositionStored{ 0.0 };

void OnSelectCursorStoredCursor(const CommandContext &context)
{
   auto &project = context.project;
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
   auto isAudioActive = ProjectAudioIO::Get( project ).IsAudioActive();

   if (mCursorPositionHasBeenStored) {
      auto gAudioIO = AudioIO::Get();
      double cursorPositionCurrent = isAudioActive
         ? gAudioIO->GetStreamTime()
         : selectedRegion.t0();
      selectedRegion.setTimes(
         std::min(cursorPositionCurrent, mCursorPositionStored),
         std::max(cursorPositionCurrent, mCursorPositionStored));

      ProjectHistory::Get( project ).ModifyState(false);
   }
}

void OnCursorPositionStore(const CommandContext &context)
{
   auto &project = context.project;
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
   auto isAudioActive = ProjectAudioIO::Get( project ).IsAudioActive();

   auto gAudioIO = AudioIO::Get();
   mCursorPositionStored =
      isAudioActive ? gAudioIO->GetStreamTime() : selectedRegion.t0();
   mCursorPositionHasBeenStored = true;
}

void OnZeroCrossing(const CommandContext &context)
{
   auto &project = context.project;
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
   const auto &settings = ProjectSettings::Get( project );

   const double t0 = NearestZeroCrossing(project, selectedRegion.t0());
   if (selectedRegion.isPoint())
      selectedRegion.setTimes(t0, t0);
   else {
      const double t1 = NearestZeroCrossing(project, selectedRegion.t1());
      // Empty selection is generally not much use, so do not make it if empty.
      if( fabs( t1 - t0 ) * settings.GetRate() > 1.5 )
         selectedRegion.setTimes(t0, t1);
   }

   ProjectHistory::Get( project ).ModifyState(false);
}

void OnSnapToOff(const CommandContext &context)
{
   auto &project = context.project;
   ProjectSelectionManager::Get( project ).AS_SetSnapTo(SNAP_OFF);
}

void OnSnapToNearest(const CommandContext &context)
{
   auto &project = context.project;
   ProjectSelectionManager::Get( project ).AS_SetSnapTo(SNAP_NEAREST);
}

void OnSnapToPrior(const CommandContext &context)
{
   auto &project = context.project;
   ProjectSelectionManager::Get( project ).AS_SetSnapTo(SNAP_PRIOR);
}

void OnSelToStart(const CommandContext &context)
{
   auto &project = context.project;
   auto &window = ProjectWindow::Get( project );
   window.Rewind(true);
   ProjectHistory::Get( project ).ModifyState(false);
}

void OnSelToEnd(const CommandContext &context)
{
   auto &project = context.project;
   auto &window = ProjectWindow::Get( project );
   window.SkipEnd(true);
   ProjectHistory::Get( project ).ModifyState(false);
}

// Handler state:
SeekInfo mSeekInfo;

void OnSelExtendLeft(const CommandContext &context)
{
   if( !OnlyHandleKeyUp( context ) )
      SeekLeftOrRight( context.project, DIRECTION_LEFT, SELECTION_EXTEND,
         mSeekInfo );
}

void OnSelExtendRight(const CommandContext &context)
{
   if( !OnlyHandleKeyUp( context ) )
      SeekLeftOrRight( context.project, DIRECTION_RIGHT, SELECTION_EXTEND,
         mSeekInfo );
}

void OnSelSetExtendLeft(const CommandContext &context)
{
   DoBoundaryMove( context.project, DIRECTION_LEFT, mSeekInfo);
}

void OnSelSetExtendRight(const CommandContext &context)
{
   DoBoundaryMove( context.project, DIRECTION_RIGHT, mSeekInfo);
}

void OnSelContractLeft(const CommandContext &context)
{
   if( !OnlyHandleKeyUp( context ) )
      SeekLeftOrRight( context.project, DIRECTION_RIGHT, SELECTION_CONTRACT,
         mSeekInfo );
}

void OnSelContractRight(const CommandContext &context)
{
   if( !OnlyHandleKeyUp( context ) )
      SeekLeftOrRight( context.project, DIRECTION_LEFT, SELECTION_CONTRACT,
         mSeekInfo );
}

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

   selectedRegion.collapseToT0();
   ProjectHistory::Get( project ).ModifyState(false);
   window.ScrollIntoView(selectedRegion.t0());
}

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

   selectedRegion.collapseToT1();
   ProjectHistory::Get( project ).ModifyState(false);
   window.ScrollIntoView(selectedRegion.t1());
}

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

   double kWayOverToRight = std::numeric_limits<double>::max();

   auto trackRange = tracks.Selected();
   if (trackRange.empty())
      // This should have been prevented by command manager
      return;

   // Range is surely nonempty now
   auto minOffset = std::max( 0.0, trackRange.min( &Track::GetOffset ) );

   if( minOffset >=
       (kWayOverToRight * (1 - std::numeric_limits<double>::epsilon()) ))
      return;

   selectedRegion.setTimes(minOffset, minOffset);
   ProjectHistory::Get( project ).ModifyState(false);
   window.ScrollIntoView(selectedRegion.t0());
}

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

   double kWayOverToLeft = std::numeric_limits<double>::lowest();

   auto trackRange = tracks.Selected();
   if (trackRange.empty())
      // This should have been prevented by command manager
      return;

   // Range is surely nonempty now
   auto maxEndOffset = trackRange.max( &Track::GetEndTime );

   if( maxEndOffset <
       (kWayOverToLeft * (1 - std::numeric_limits<double>::epsilon()) ))
      return;

   selectedRegion.setTimes(maxEndOffset, maxEndOffset);
   ProjectHistory::Get( project ).ModifyState(false);
   window.ScrollIntoView(selectedRegion.t1());
}

void OnSkipStart(const CommandContext &context)
{
   auto &project = context.project;
   wxCommandEvent evt;

   auto &controlToolBar = ControlToolBar::Get( project );
   controlToolBar.OnRewind(evt);
   ProjectHistory::Get( project ).ModifyState(false);
}

void OnSkipEnd(const CommandContext &context)
{
   auto &project = context.project;
   wxCommandEvent evt;

   auto &controlToolBar = ControlToolBar::Get( project );
   controlToolBar.OnFF(evt);
   ProjectHistory::Get( project ).ModifyState(false);
}

void OnCursorLeft(const CommandContext &context)
{
   if( !OnlyHandleKeyUp( context ) )
      SeekLeftOrRight( context.project, DIRECTION_LEFT, CURSOR_MOVE,
         mSeekInfo );
}

void OnCursorRight(const CommandContext &context)
{
   if( !OnlyHandleKeyUp( context ) )
      SeekLeftOrRight( context.project, DIRECTION_RIGHT, CURSOR_MOVE,
         mSeekInfo );
}

void OnCursorShortJumpLeft(const CommandContext &context)
{
   DoCursorMove( context.project,
      -mSeekInfo.mSeekShort, mSeekInfo.mLastSelectionAdjustment );
}

void OnCursorShortJumpRight(const CommandContext &context)
{
   DoCursorMove( context.project,
      mSeekInfo.mSeekShort, mSeekInfo.mLastSelectionAdjustment );
}

void OnCursorLongJumpLeft(const CommandContext &context)
{
   DoCursorMove( context.project,
      -mSeekInfo.mSeekLong, mSeekInfo.mLastSelectionAdjustment );
}

void OnCursorLongJumpRight(const CommandContext &context)
{
   DoCursorMove( context.project,
      mSeekInfo.mSeekLong, mSeekInfo.mLastSelectionAdjustment );
}

void OnSeekLeftShort(const CommandContext &context)
{
   auto &project = context.project;
   SeekLeftOrRight( project, DIRECTION_LEFT, CURSOR_MOVE, mSeekInfo );
}

void OnSeekRightShort(const CommandContext &context)
{
   auto &project = context.project;
   SeekLeftOrRight( project, DIRECTION_RIGHT, CURSOR_MOVE, mSeekInfo );
}

void OnSeekLeftLong(const CommandContext &context)
{
   auto &project = context.project;
   SeekLeftOrRight( project, DIRECTION_LEFT, SELECTION_EXTEND, mSeekInfo );
}

void OnSeekRightLong(const CommandContext &context)
{
   auto &project = context.project;
   SeekLeftOrRight( project, DIRECTION_RIGHT, SELECTION_EXTEND, mSeekInfo );
}

#if 1
// Legacy functions, not used as of version 2.3.0
void OnSelectAllTime(const CommandContext &context)
{
   auto &project = context.project;
   SelectUtilities::DoSelectTimeAndTracks( project, true, false );
}
#endif

void UpdatePrefs() override
{
   gPrefs->Read(wxT("/AudioIO/SeekShortPeriod"), &mSeekInfo.mSeekShort, 1.0);
   gPrefs->Read(wxT("/AudioIO/SeekLongPeriod"), &mSeekInfo.mSeekLong, 15.0);
}
Handler()
{
   UpdatePrefs();
}
Handler( const Handler & ) PROHIBITED;
Handler &operator=( const Handler & ) PROHIBITED;

}; // struct Handler

} // namespace

// Handler is stateful.  Needs a factory registered with
// AudacityProject.
static const AudacityProject::AttachedObjects::RegisteredFactory key{
   [](AudacityProject&) {
      return std::make_unique< SelectActions::Handler >(); } };

static CommandHandlerObject &findCommandHandler(AudacityProject &project) {
   return project.AttachedObjects::Get< SelectActions::Handler >( key );
};

// Menu definitions

#define FN(X) (& SelectActions::Handler :: X)

namespace {
using namespace MenuTable;
BaseItemSharedPtr SelectMenu()
{
   using Options = CommandManager::Options;
   static BaseItemSharedPtr menu{
   ( FinderScope{ findCommandHandler },
   /* i18n-hint: (verb) It's an item on a menu. */
   Menu( wxT("Select"), XO("&Select"),
      Section( "Basic",
         Command( wxT("SelectAll"), XXO("&All"), FN(OnSelectAll),
            TracksExistFlag(),
            Options{ wxT("Ctrl+A"), XO("Select All") } ),
         Command( wxT("SelectNone"), XXO("&None"), FN(OnSelectNone),
            TracksExistFlag(),
            Options{ wxT("Ctrl+Shift+A"), XO("Select None") } ),

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

         Menu( wxT("Tracks"), XO("&Tracks"),
            Command( wxT("SelAllTracks"), XXO("In All &Tracks"),
               FN(OnSelectAllTracks),
               TracksExistFlag(),
               wxT("Ctrl+Shift+K") )

   #ifdef EXPERIMENTAL_SYNC_LOCK
            ,
            Command( wxT("SelSyncLockTracks"), XXO("In All &Sync-Locked Tracks"),
               FN(OnSelectSyncLockSel),
               TracksSelectedFlag() | IsSyncLockedFlag(),
               Options{ wxT("Ctrl+Shift+Y"), XO("Select Sync-Locked") } )
   #endif
         ),

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

         Menu( wxT("Region"), XO("R&egion"),
            Section( "",
               Command( wxT("SetLeftSelection"), XXO("&Left at Playback Position"),
                  FN(OnSetLeftSelection), TracksExistFlag(),
                  Options{ wxT("["), XO("Set Selection Left at Play Position") } ),
               Command( wxT("SetRightSelection"), XXO("&Right at Playback Position"),
                  FN(OnSetRightSelection), TracksExistFlag(),
                  Options{ wxT("]"), XO("Set Selection Right at Play Position") } ),
               Command( wxT("SelTrackStartToCursor"), XXO("Track &Start to Cursor"),
                  FN(OnSelectStartCursor), AlwaysEnabledFlag,
                  Options{ wxT("Shift+J"), XO("Select Track Start to Cursor") } ),
               Command( wxT("SelCursorToTrackEnd"), XXO("Cursor to Track &End"),
                  FN(OnSelectCursorEnd), AlwaysEnabledFlag,
                  Options{ wxT("Shift+K"), XO("Select Cursor to Track End") } ),
               Command( wxT("SelTrackStartToEnd"), XXO("Track Start to En&d"),
                  FN(OnSelectTrackStartToEnd), AlwaysEnabledFlag,
                  Options{}.LongName( XO("Select Track Start to End") ) )
            ),

            Section( "",
               // GA: Audacity had 'Store Re&gion' here previously. There is no
               // one-step way to restore the 'Saved Cursor Position' in Select Menu,
               // so arguably using the word 'Selection' to do duty for both saving
               // the region or the cursor is better. But it does not belong in a
               // 'Region' submenu.
               Command( wxT("SelSave"), XXO("S&tore Selection"), FN(OnSelectionSave),
                  WaveTracksSelectedFlag() ),
               // Audacity had 'Retrieve Regio&n' here previously.
               Command( wxT("SelRestore"), XXO("Retrieve Selectio&n"),
                  FN(OnSelectionRestore), TracksExistFlag() )
            )
         ),

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

   #ifdef EXPERIMENTAL_SPECTRAL_EDITING
         Menu( wxT("Spectral"), XO("S&pectral"),
            Command( wxT("ToggleSpectralSelection"),
               XXO("To&ggle Spectral Selection"), FN(OnToggleSpectralSelection),
               TracksExistFlag(), wxT("Q") ),
            Command( wxT("NextHigherPeakFrequency"),
               XXO("Next &Higher Peak Frequency"), FN(OnNextHigherPeakFrequency),
               TracksExistFlag() ),
            Command( wxT("NextLowerPeakFrequency"),
               XXO("Next &Lower Peak Frequency"), FN(OnNextLowerPeakFrequency),
               TracksExistFlag() )
         )
   #endif
      ),

      Section( "",
         Command( wxT("SelCursorStoredCursor"),
            XXO("Cursor to Stored &Cursor Position"),
            FN(OnSelectCursorStoredCursor), TracksExistFlag(),
            Options{}.LongName( XO("Select Cursor to Stored") ) ),

         Command( wxT("StoreCursorPosition"), XXO("Store Cursor Pos&ition"),
            FN(OnCursorPositionStore),
            WaveTracksExistFlag() )
         // Save cursor position is used in some selections.
         // Maybe there should be a restore for it?
      ),

      Section( "",
         Command( wxT("ZeroCross"), XXO("At &Zero Crossings"),
            FN(OnZeroCrossing), TracksSelectedFlag(),
            Options{ wxT("Z"), XO("Select Zero Crossing") } )
      )
   ) ) };
   return menu;
}

AttachedItem sAttachment1{
   wxT(""),
   Shared( SelectMenu() )
};

BaseItemSharedPtr ExtraSelectionMenu()
{
   static BaseItemSharedPtr menu{
   ( FinderScope{ findCommandHandler },
   Menu( wxT("Select"), XO("&Selection"),
      Command( wxT("SnapToOff"), XXO("Snap-To &Off"), FN(OnSnapToOff),
         AlwaysEnabledFlag ),
      Command( wxT("SnapToNearest"), XXO("Snap-To &Nearest"),
         FN(OnSnapToNearest), AlwaysEnabledFlag ),
      Command( wxT("SnapToPrior"), XXO("Snap-To &Prior"), FN(OnSnapToPrior),
         AlwaysEnabledFlag ),
      Command( wxT("SelStart"), XXO("Selection to &Start"), FN(OnSelToStart),
         AlwaysEnabledFlag, wxT("Shift+Home") ),
      Command( wxT("SelEnd"), XXO("Selection to En&d"), FN(OnSelToEnd),
         AlwaysEnabledFlag, wxT("Shift+End") ),
      Command( wxT("SelExtLeft"), XXO("Selection Extend &Left"),
         FN(OnSelExtendLeft),
         TracksExistFlag() | TrackPanelHasFocus(),
         wxT("Shift+Left\twantKeyup\tallowDup") ),
      Command( wxT("SelExtRight"), XXO("Selection Extend &Right"),
         FN(OnSelExtendRight),
         TracksExistFlag() | TrackPanelHasFocus(),
         wxT("Shift+Right\twantKeyup\tallowDup") ),
      Command( wxT("SelSetExtLeft"), XXO("Set (or Extend) Le&ft Selection"),
         FN(OnSelSetExtendLeft),
         TracksExistFlag() | TrackPanelHasFocus() ),
      Command( wxT("SelSetExtRight"), XXO("Set (or Extend) Rig&ht Selection"),
         FN(OnSelSetExtendRight),
         TracksExistFlag() | TrackPanelHasFocus() ),
      Command( wxT("SelCntrLeft"), XXO("Selection Contract L&eft"),
         FN(OnSelContractLeft),
         TracksExistFlag() | TrackPanelHasFocus(),
         wxT("Ctrl+Shift+Right\twantKeyup") ),
      Command( wxT("SelCntrRight"), XXO("Selection Contract R&ight"),
         FN(OnSelContractRight),
         TracksExistFlag() | TrackPanelHasFocus(),
         wxT("Ctrl+Shift+Left\twantKeyup") )
   ) ) };
   return menu;
}

AttachedItem sAttachment2{
   wxT("Optional/Extra/Part1"),
   Shared( ExtraSelectionMenu() )
};
}

namespace {
BaseItemSharedPtr CursorMenu()
{
   using Options = CommandManager::Options;
   static const auto CanStopFlags = AudioIONotBusyFlag() | CanStopAudioStreamFlag();

   // JKC: ANSWER-ME: How is 'cursor to' different to 'Skip To' and how is it
   // useful?
   // GA: 'Skip to' moves the viewpoint to center of the track and preserves the
   // selection. 'Cursor to' does neither. 'Center at' might describe it better
   // than 'Skip'.
   static BaseItemSharedPtr menu{
   ( FinderScope{ findCommandHandler },
   Menu( wxT("Cursor"), XO("&Cursor to"),
      Command( wxT("CursSelStart"), XXO("Selection Star&t"),
         FN(OnCursorSelStart),
         TimeSelectedFlag(),
         Options{}.LongName( XO("Cursor to Selection Start") ) ),
      Command( wxT("CursSelEnd"), XXO("Selection En&d"),
         FN(OnCursorSelEnd),
         TimeSelectedFlag(),
         Options{}.LongName( XO("Cursor to Selection End") ) ),

      Command( wxT("CursTrackStart"), XXO("Track &Start"),
         FN(OnCursorTrackStart),
         TracksSelectedFlag(),
         Options{ wxT("J"), XO("Cursor to Track Start") } ),
      Command( wxT("CursTrackEnd"), XXO("Track &End"),
         FN(OnCursorTrackEnd),
         TracksSelectedFlag(),
         Options{ wxT("K"), XO("Cursor to Track End") } ),

      Command( wxT("CursProjectStart"), XXO("&Project Start"),
         FN(OnSkipStart),
         CanStopFlags,
         Options{ wxT("Home"), XO("Cursor to Project Start") } ),
      Command( wxT("CursProjectEnd"), XXO("Project E&nd"), FN(OnSkipEnd),
         CanStopFlags,
         Options{ wxT("End"), XO("Cursor to Project End") } )
   ) ) };
   return menu;
}

AttachedItem sAttachment0{
   wxT("Transport/Basic"),
   Shared( CursorMenu() )
};

BaseItemSharedPtr ExtraCursorMenu()
{
   static BaseItemSharedPtr menu{
   ( FinderScope{ findCommandHandler },
   Menu( wxT("Cursor"), XO("&Cursor"),
      Command( wxT("CursorLeft"), XXO("Cursor &Left"), FN(OnCursorLeft),
         TracksExistFlag() | TrackPanelHasFocus(),
         wxT("Left\twantKeyup\tallowDup") ),
      Command( wxT("CursorRight"), XXO("Cursor &Right"), FN(OnCursorRight),
         TracksExistFlag() | TrackPanelHasFocus(),
         wxT("Right\twantKeyup\tallowDup") ),
      Command( wxT("CursorShortJumpLeft"), XXO("Cursor Sh&ort Jump Left"),
         FN(OnCursorShortJumpLeft),
         TracksExistFlag() | TrackPanelHasFocus(), wxT(",") ),
      Command( wxT("CursorShortJumpRight"), XXO("Cursor Shor&t Jump Right"),
         FN(OnCursorShortJumpRight),
         TracksExistFlag() | TrackPanelHasFocus(), wxT(".") ),
      Command( wxT("CursorLongJumpLeft"), XXO("Cursor Long J&ump Left"),
         FN(OnCursorLongJumpLeft),
         TracksExistFlag() | TrackPanelHasFocus(), wxT("Shift+,") ),
      Command( wxT("CursorLongJumpRight"), XXO("Cursor Long Ju&mp Right"),
         FN(OnCursorLongJumpRight),
         TracksExistFlag() | TrackPanelHasFocus(), wxT("Shift+.") )
   ) ) };
   return menu;
}

AttachedItem sAttachment4{
   wxT("Optional/Extra/Part2"),
   Shared( ExtraCursorMenu() )
};

BaseItemSharedPtr ExtraSeekMenu()
{
   static BaseItemSharedPtr menu{
   ( FinderScope{ findCommandHandler },
   Menu( wxT("Seek"), XO("See&k"),
      Command( wxT("SeekLeftShort"), XXO("Short Seek &Left During Playback"),
         FN(OnSeekLeftShort), AudioIOBusyFlag(), wxT("Left\tallowDup") ),
      Command( wxT("SeekRightShort"),
         XXO("Short Seek &Right During Playback"), FN(OnSeekRightShort),
         AudioIOBusyFlag(), wxT("Right\tallowDup") ),
      Command( wxT("SeekLeftLong"), XXO("Long Seek Le&ft During Playback"),
         FN(OnSeekLeftLong), AudioIOBusyFlag(), wxT("Shift+Left\tallowDup") ),
      Command( wxT("SeekRightLong"), XXO("Long Seek Rig&ht During Playback"),
         FN(OnSeekRightLong), AudioIOBusyFlag(), wxT("Shift+Right\tallowDup") )
   ) ) };
   return menu;
}

AttachedItem sAttachment5{
   wxT("Optional/Extra/Part1"),
   Shared( ExtraSeekMenu() )
};

}

#undef FN
