#include "../CommonCommandFlags.h"
#include "LabelTrack.h"
#include "MixAndRender.h"

#include "Prefs.h"
#include "Project.h"
#include "ProjectAudioIO.h"
#include "ProjectHistory.h"
#include "ProjectRate.h"
#include "../ProjectSettings.h"
#include "PluginManager.h"
#include "ProjectStatus.h"
#include "../ProjectWindows.h"
#include "../SelectUtilities.h"
#include "ShuttleGui.h"
#include "SyncLock.h"
#include "TrackFocus.h"
#include "../TrackPanel.h"
#include "../TrackUtilities.h"
#include "UndoManager.h"
#include "WaveClip.h"
#include "ViewInfo.h"
#include "Viewport.h"
#include "WaveTrack.h"
#include "CommandContext.h"
#include "EffectManager.h"
#include "../effects/EffectUI.h"
#include "QualitySettings.h"
#include "../tracks/playabletrack/wavetrack/ui/WaveTrackControls.h"
#include "../toolbars/ToolManager.h"
#include "../widgets/ASlider.h"
#include "AudacityMessageBox.h"
#include "ProgressDialog.h"
#include "DoEffect.h"

#include <wx/combobox.h>

#ifdef EXPERIMENTAL_SCOREALIGN
#include "../effects/ScoreAlignDialog.h"
#include "audioreader.h"
#include "scorealign.h"
#include "scorealign-glue.h"
#endif /* EXPERIMENTAL_SCOREALIGN */

// private helper classes and functions
namespace {

void DoMixAndRender(AudacityProject &project, bool toNewTrack)
{
   auto &tracks = TrackList::Get(project);
   auto &trackFactory = WaveTrackFactory::Get(project);
   auto rate = ProjectRate::Get(project).GetRate();
   auto defaultFormat = QualitySettings::SampleFormatChoice();
   auto &trackPanel = TrackPanel::Get(project);

   auto trackRange = tracks.Selected<WaveTrack>();
   auto newTrack = ::MixAndRender(trackRange.Filter<const WaveTrack>(),
      Mixer::WarpOptions{ tracks.GetOwner() },
      tracks.MakeUniqueTrackName(_("Mix")),
      &trackFactory, rate, defaultFormat, 0.0, 0.0);

   if (newTrack) {
      // Remove originals, get stats on what tracks were mixed

      // But before removing, determine the first track after the removal
      auto last = *trackRange.rbegin();
      auto insertionPoint = * ++ tracks.Find(last);

      auto selectedCount = trackRange.size();
      if (!toNewTrack)  {
         // Beware iterator invalidation!
         while (!trackRange.empty())
            tracks.Remove(**trackRange.first++);
      }

      // Add new tracks
      const bool stereo = newTrack->NChannels() > 1;
      const auto firstName = newTrack->GetName();
      tracks.Add(newTrack);
      const auto pNewTrack = *tracks.Any<WaveTrack>().rbegin();

      // Bug 2218, remember more things...
      if (selectedCount >= 1)
         pNewTrack->SetSelected(!toNewTrack);

      // Permute the tracks as needed
      // The new track appears after the old tracks (or where the old tracks
      // had been) so that they are in the same sync-lock group
      if (insertionPoint) {
         std::vector<Track *> arr;
         arr.reserve(tracks.Size());
         size_t iBegin = 0, ii = 0;
         for (const auto pTrack : tracks) {
            arr.push_back(pTrack);
            if (pTrack == insertionPoint)
               iBegin = ii;
            ++ii;
         }
         const auto end = arr.end(),
            mid = end - 1;
         std::rotate(arr.begin() + iBegin, mid, end);
         tracks.Permute(arr);
      }

      // Smart history/undo message
      if (selectedCount == 1) {
         auto msg = XO("Rendered all audio in track '%s'").Format(firstName);
         /* i18n-hint: Convert the audio into a more usable form, so apply
          * panning and amplification and write to some external file.*/
         ProjectHistory::Get( project ).PushState(msg, XO("Render"));
      }
      else {
         auto msg = (stereo
            ? XO("Mixed and rendered %d tracks into one new stereo track")
            : XO("Mixed and rendered %d tracks into one new mono track")
         )
            .Format((int)selectedCount);
         ProjectHistory::Get(project).PushState(msg, XO("Mix and Render"));
      }

      trackPanel.SetFocus();
      TrackFocus::Get(project).Set(pNewTrack);
      Viewport::Get(project).ShowTrack(*pNewTrack);
   }
}

void DoPanTracks(AudacityProject &project, float PanValue)
{
   auto &tracks = TrackList::Get( project );

   // count selected wave tracks
   const auto range = tracks.Any< WaveTrack >();
   const auto selectedRange = range + &Track::IsSelected;
   auto count = selectedRange.size();

   // iter through them, all if none selected.
   for (auto left : count == 0 ? range : selectedRange )
      left->SetPan( PanValue );

   auto flags = UndoPush::NONE;
   ProjectHistory::Get( project )
      /*i18n-hint: One or more audio tracks have been panned*/
      .PushState(XO("Panned audio track(s)"), XO("Pan Track"), flags);
         flags = flags | UndoPush::CONSOLIDATE;
}

enum {
   kAlignStartZero = 0,
   kAlignStartSelStart,
   kAlignStartSelEnd,
   kAlignEndSelStart,
   kAlignEndSelEnd,
   // The next two are only in one subMenu, so more easily handled at the end.
   kAlignEndToEnd,
   kAlignTogether
};

static const std::vector< ComponentInterfaceSymbol >
&alignLabels() { static std::vector< ComponentInterfaceSymbol > symbols{
   { wxT("StartToZero"),     XXO("Start to &Zero") },
   { wxT("StartToSelStart"), XXO("Start to &Cursor/Selection Start") },
   { wxT("StartToSelEnd"),   XXO("Start to Selection &End") },
   { wxT("EndToSelStart"),   XXO("End to Cu&rsor/Selection Start") },
   { wxT("EndToSelEnd"),     XXO("End to Selection En&d") },
}; return symbols; }

const size_t kAlignLabelsCount(){ return alignLabels().size(); }

void DoAlign(AudacityProject &project, int index, bool moveSel)
{
   auto &tracks = TrackList::Get( project );
   auto &selectedRegion = ViewInfo::Get( project ).selectedRegion;
   auto &viewport = Viewport::Get(project);

   TranslatableString action, shortAction;
   double delta = 0.0;
   double newPos = -1.0;

   auto trackRange = tracks.Selected<AudioTrack>();

   auto FindOffset =
      [](const Track *pTrack) { return pTrack->GetStartTime(); };

   auto firstTrackOffset = [&]{ return FindOffset( *trackRange.begin() ); };
   auto minOffset = [&]{ return trackRange.min( FindOffset ); };
   auto avgOffset = [&]{
      return trackRange.sum( FindOffset ) /
                             std::max( size_t(1), trackRange.size() ); };

   auto maxEndOffset = [&]{
      return std::max(0.0, trackRange.max(&Track::GetEndTime)); };

   switch(index) {
   case kAlignStartZero:
      delta = -minOffset();
      action = moveSel
         /* i18n-hint: In this and similar messages describing editing actions,
            the starting or ending points of tracks are re-"aligned" to other
            times, and the time selection may be "moved" too.  The first
            noun -- "start" in this example -- is the object of a verb (not of
            an implied preposition "from"). */
         ? XO("Aligned/Moved start to zero")
         : XO("Aligned start to zero");
      shortAction = moveSel
         /* i18n-hint: This and similar messages give shorter descriptions of
            the aligning and moving editing actions */
         ? XO("Align/Move Start")
         : XO("Align Start");
      break;
   case kAlignStartSelStart:
      delta = selectedRegion.t0() - minOffset();
      action = moveSel
         ? XO("Aligned/Moved start to cursor/selection start")
         : XO("Aligned start to cursor/selection start");
      shortAction = moveSel
         ? XO("Align/Move Start")
         : XO("Align Start");
      break;
   case kAlignStartSelEnd:
      delta = selectedRegion.t1() - minOffset();
      action = moveSel
         ? XO("Aligned/Moved start to selection end")
         : XO("Aligned start to selection end");
      shortAction = moveSel
         ? XO("Align/Move Start")
         : XO("Align Start");
      break;
   case kAlignEndSelStart:
      delta = selectedRegion.t0() - maxEndOffset();
      action = moveSel
         ? XO("Aligned/Moved end to cursor/selection start")
         : XO("Aligned end to cursor/selection start");
      shortAction =
         moveSel
         ? XO("Align/Move End")
         : XO("Align End");
      break;
   case kAlignEndSelEnd:
      delta = selectedRegion.t1() - maxEndOffset();
      action = moveSel
         ? XO("Aligned/Moved end to selection end")
         : XO("Aligned end to selection end");
      shortAction =
         moveSel
         ? XO("Align/Move End")
         : XO("Align End");
      break;
   // index set in alignLabelsNoSync
   case kAlignEndToEnd:
      newPos = firstTrackOffset();
      action = moveSel
         ? XO("Aligned/Moved end to end")
         : XO("Aligned end to end");
      shortAction =
         moveSel
         ? XO("Align/Move End to End")
         : XO("Align End to End");
      break;
   case kAlignTogether:
      newPos = avgOffset();
      action = moveSel
         ? XO("Aligned/Moved together")
         : XO("Aligned together");
      shortAction =
         moveSel
         ? XO("Align/Move Together")
         : XO("Align Together");
   }

   if ((unsigned)index >= kAlignLabelsCount()) {
      // This is an alignLabelsNoSync command.
      for (auto t : tracks.Selected<AudioTrack>()) {
         // This shifts different tracks in different ways, so no sync-lock
         // move.
         // Only align Wave and Note tracks end to end.
         t->MoveTo(newPos);
         if (index == kAlignEndToEnd)
            newPos += (t->GetEndTime() - t->GetStartTime());
      }
      if (index == kAlignEndToEnd)
         viewport.ZoomFitHorizontally();
   }

   if (delta != 0.0) {
      // For a fixed-distance shift move sync-lock selected tracks also.
      for (auto t : tracks.Any()
           + &SyncLock::IsSelectedOrSyncLockSelectedP)
         t->MoveTo(t->GetStartTime() + delta);
   }

   if (moveSel)
      selectedRegion.move(delta);

   ProjectHistory::Get( project ).PushState(action, shortAction);
}

#ifdef EXPERIMENTAL_SCOREALIGN

#ifdef USE_MIDI
static const ReservedCommandFlag&
   NoteTracksSelectedFlag() { static ReservedCommandFlag flag{
      [](const AudacityProject &project){
         return !TrackList::Get( project ).Selected<const NoteTrack>().empty();
      }
   }; return flag; }  //gsw
#endif

// rough relative amount of time to compute one
//    frame of audio or midi, or one cell of matrix, or one iteration
//    of smoothing, measured on a 1.9GHz Core 2 Duo in 32-bit mode
//    (see COLLECT_TIMING_DATA below)
#define AUDIO_WORK_UNIT 0.004F
#define MIDI_WORK_UNIT 0.0001F
#define MATRIX_WORK_UNIT 0.000002F
#define SMOOTHING_WORK_UNIT 0.000001F

// Write timing data to a file; useful for calibrating AUDIO_WORK_UNIT,
// MIDI_WORK_UNIT, MATRIX_WORK_UNIT, and SMOOTHING_WORK_UNIT coefficients
// Data is written to timing-data.txt; look in
//     audacity-src/win/Release/modules/
#define COLLECT_TIMING_DATA

// Audacity Score Align Progress class -- progress reports come here
class ASAProgress final : public SAProgress {
 private:
   float mTotalWork;
   float mFrames[2];
   long mTotalCells; // how many matrix cells?
   long mCellCount; // how many cells so far?
   long mPrevCellCount; // cell_count last reported with Update()
   std::optional<ProgressDialog> mProgress;
   #ifdef COLLECT_TIMING_DATA
      FILE *mTimeFile;
      wxDateTime mStartTime;
      long iterations;
   #endif

 public:
   ASAProgress() {
      smoothing = false;
      #ifdef COLLECT_TIMING_DATA
         mTimeFile = fopen("timing-data.txt", "w");
      #endif
   }
   ~ASAProgress() {
      #ifdef COLLECT_TIMING_DATA
         fclose(mTimeFile);
      #endif
   }
   void set_phase(int i) override {
      float work[2]; // chromagram computation work estimates
      float work2, work3 = 0; // matrix and smoothing work estimates
      SAProgress::set_phase(i);
      #ifdef COLLECT_TIMING_DATA
         long ms = 0;
         wxDateTime now = wxDateTime::UNow();
         wxFprintf(mTimeFile, "Phase %d begins at %s\n",
                 i, now.FormatTime());
         if (i != 0)
            ms = now.Subtract(mStartTime).GetMilliseconds().ToLong();
         mStartTime = now;
      #endif
      if (i == 0) {
         mCellCount = 0;
         for (int j = 0; j < 2; j++) {
            mFrames[j] = durations[j] / frame_period;
         }
         mTotalWork = 0;
         for (int j = 0; j < 2; j++) {
             work[j] =
                (is_audio[j] ? AUDIO_WORK_UNIT : MIDI_WORK_UNIT) * mFrames[j];
             mTotalWork += work[j];
         }
         mTotalCells = mFrames[0] * mFrames[1];
         work2 = mTotalCells * MATRIX_WORK_UNIT;
         mTotalWork += work2;
         // arbitrarily assume 60 iterations to fit smooth segments and
         // per frame per iteration is SMOOTHING_WORK_UNIT
         if (smoothing) {
            work3 =
               wxMax(mFrames[0], mFrames[1]) * SMOOTHING_WORK_UNIT * 40;
            mTotalWork += work3;
         }
         #ifdef COLLECT_TIMING_DATA
            wxFprintf(mTimeFile,
               " mTotalWork (an estimate) = %g\n", mTotalWork);
            wxFprintf(mTimeFile, " work0 = %g, frames %g, is_audio %d\n",
                    work[0], mFrames[0], is_audio[0]);
            wxFprintf(mTimeFile, " work1 = %g, frames %g, is_audio %d\n",
                    work[1], mFrames[1], is_audio[1]);
            wxFprintf(mTimeFile, "work2 = %g, work3 = %g\n", work2, work3);
         #endif
         mProgress.emplace(XO("Synchronize MIDI with Audio"),
                               XO("Synchronizing MIDI and Audio Tracks"));
      } else if (i < 3) {
         wxFprintf(mTimeFile,
                "Phase %d took %d ms for %g frames, coefficient = %g s/frame\n",
               i - 1, ms, mFrames[i - 1], (ms * 0.001) / mFrames[i - 1]);
      } else if (i == 3) {
        wxFprintf(mTimeFile,
                "Phase 2 took %d ms for %d cells, coefficient = %g s/cell\n",
                ms, mCellCount, (ms * 0.001) / mCellCount);
      } else if (i == 4) {
        wxFprintf(mTimeFile,
                "Phase 3 took %d ms for %d iterations on %g frames, "
                "coefficient = %g s per frame per iteration\n",
                ms, iterations, wxMax(mFrames[0], mFrames[1]),
                (ms * 0.001) / (wxMax(mFrames[0], mFrames[1]) * iterations));
      }
   }
   bool set_feature_progress(float s) override {
      float work;
      if (phase == 0) {
         float f = s / frame_period;
         work = (is_audio[0] ? AUDIO_WORK_UNIT : MIDI_WORK_UNIT) * f;
      } else if (phase == 1) {
         float f = s / frame_period;
         work = (is_audio[0] ? AUDIO_WORK_UNIT : MIDI_WORK_UNIT) * mFrames[0] +
                (is_audio[1] ? AUDIO_WORK_UNIT : MIDI_WORK_UNIT) * f;
      }
      auto updateResult = mProgress->Update((int)(work), (int)(mTotalWork));
      return (updateResult == ProgressResult::Success);
   }
   bool set_matrix_progress(int cells) override {
      mCellCount += cells;
      float work =
             (is_audio[0] ? AUDIO_WORK_UNIT : MIDI_WORK_UNIT) * mFrames[0] +
             (is_audio[1] ? AUDIO_WORK_UNIT : MIDI_WORK_UNIT) * mFrames[1];
      work += mCellCount * MATRIX_WORK_UNIT;
      auto updateResult = mProgress->Update((int)(work), (int)(mTotalWork));
      return (updateResult == ProgressResult::Success);
   }
   bool set_smoothing_progress(int i) override {
      iterations = i;
      float work =
             (is_audio[0] ? AUDIO_WORK_UNIT : MIDI_WORK_UNIT) * mFrames[0] +
             (is_audio[1] ? AUDIO_WORK_UNIT : MIDI_WORK_UNIT) * mFrames[1] +
             MATRIX_WORK_UNIT * mFrames[0] * mFrames[1];
      work += i * wxMax(mFrames[0], mFrames[1]) * SMOOTHING_WORK_UNIT;
      auto updateResult = mProgress->Update((int)(work), (int)(mTotalWork));
      return (updateResult == ProgressResult::Success);
   }
};


long mixer_process(void *mixer, float **buffer, long n)
{
   Mixer *mix = (Mixer *) mixer;
   long frame_count = mix->Process(std::max(0L, n));
   *buffer = (float *) mix->GetBuffer();
   return frame_count;
}

#endif // EXPERIMENTAL_SCOREALIGN

enum{
   kAudacitySortByTime = (1 << 1),
   kAudacitySortByName = (1 << 2),
};

void DoSortTracks( AudacityProject &project, int flags )
{
   auto GetTime = [](const Track &t) {
      return t.TypeSwitch<double>(
         [&](const WaveTrack &w) {
            auto stime = w.GetEndTime();

            int ndx;
            for (ndx = 0; ndx < w.GetNumClips(); ndx++) {
               const auto c = w.GetClip(ndx);
               if (c->GetVisibleSampleCount() == 0)
                  continue;
               stime = std::min(stime, c->GetPlayStartTime());
            }
            return stime;
         },
         [&](const LabelTrack& l) {
            return l.GetStartTime();
         }
      );
   };

   std::vector<Track *> arr;
   auto &tracks = TrackList::Get(project);
   arr.reserve(tracks.Size());

   // First find the permutation.
   for (const auto pTrack : tracks) {
      auto &track = *pTrack;
      const auto size = arr.size();
      size_t ndx = 0;
      for (; ndx < size; ++ndx) {
         Track &arrTrack = *arr[ndx];
         if (flags & kAudacitySortByName) {
            //do case insensitive sort - cmpNoCase returns less than zero if
            // the string is 'less than' its argument
            //also if we have case insensitive equality, then we need to sort
            // by case as well
            //We sort 'b' before 'B' accordingly.  We uncharacteristically
            // use greater than for the case sensitive
            //compare because 'b' is greater than 'B' in ascii.
            auto cmpValue = track.GetName().CmpNoCase(arrTrack.GetName());
            if (cmpValue < 0 ||
                  (0 == cmpValue &&
                     track.GetName().CompareTo(arrTrack.GetName()) > 0))
               break;
         }
         //sort by time otherwise
         else if (flags & kAudacitySortByTime)
            if (GetTime(track) < GetTime(arrTrack))
               break;
      }
      arr.insert(arr.begin() + ndx, &track);
   }

   // Now apply the permutation
   tracks.Permute(arr);
}

void SetTrackGain(AudacityProject &project, WaveTrack * wt, LWSlider * slider)
{
   wxASSERT(wt);
   float newValue = slider->Get();

   wt->SetVolume(newValue);

   ProjectHistory::Get( project )
      .PushState(XO("Adjusted gain"), XO("Gain"), UndoPush::CONSOLIDATE);

   TrackPanel::Get( project ).RefreshTrack(wt);
}

void SetTrackPan(AudacityProject &project, WaveTrack * wt, LWSlider * slider)
{
   wxASSERT(wt);
   float newValue = slider->Get();

   wt->SetPan(newValue);

   ProjectHistory::Get( project )
      .PushState(XO("Adjusted Pan"), XO("Pan"), UndoPush::CONSOLIDATE);

   TrackPanel::Get( project ).RefreshTrack(wt);
}

}

namespace {

// Menu handler functions

void OnStereoToMono(const CommandContext &context)
{
   EffectUI::DoEffect(
      PluginManager::Get().GetByCommandIdentifier(wxT("StereoToMono")),
      context.project, EffectManager::kConfigured);
}

void OnMixAndRender(const CommandContext &context)
{
   auto &project = context.project;
   DoMixAndRender(project, false);
}

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

void OnResample(const CommandContext &context)
{
   auto &project = context.project;
   auto projectRate = ProjectRate::Get(project).GetRate();
   auto &tracks = TrackList::Get(project);
   auto &undoManager = UndoManager::Get(project);
   auto &viewport = Viewport::Get(project);
   auto &window = GetProjectFrame(project);

   int newRate;

   while (true) {
      wxDialogWrapper dlg(&window, wxID_ANY, XO("Resample"));
      ShuttleGui S(&dlg, eIsCreating);
      wxString rate;
      wxComboBox *cb;

      rate.Printf(wxT("%ld"), lrint(projectRate));

      wxArrayStringEx rates{
         wxT("8000") ,
         wxT("11025") ,
         wxT("16000") ,
         wxT("22050") ,
         wxT("32000") ,
         wxT("44100") ,
         wxT("48000") ,
         wxT("88200") ,
         wxT("96000") ,
         wxT("176400") ,
         wxT("192000") ,
         wxT("352800") ,
         wxT("384000") ,
      };

      S.StartVerticalLay(true);
      {
         S.AddSpace(-1, 15);

         S.StartHorizontalLay(wxCENTER, false);
         {
            cb = S.AddCombo(XXO("New sample rate (Hz):"),
                            rate,
                            rates);
         }
         S.EndHorizontalLay();

         S.AddSpace(-1, 15);

         S.AddStandardButtons();
      }
      S.EndVerticalLay();

      dlg.Layout();
      dlg.Fit();
      dlg.Center();

      if (dlg.ShowModal() != wxID_OK)
         return;  // user cancelled dialog

      long lrate;
      if (cb->GetValue().ToLong(&lrate) && lrate >= 1 && lrate <= 1000000) {
         newRate = (int)lrate;
         break;
      }

      AudacityMessageBox(
         XO("The entered value is invalid"),
         XO("Error"),
         wxICON_ERROR,
         &window);
   }

   int ndx = 0;
   auto flags = UndoPush::NONE;
   for (auto wt : tracks.Selected<WaveTrack>()) {
      auto msg = XO("Resampling track %d").Format(++ndx);

      using namespace BasicUI;
      auto progress = MakeProgress(XO("Resample"), msg);

      // The resampling of a track may be stopped by the user.  This might
      // leave a track with multiple clips in a partially resampled state.
      // But the thrown exception will cause rollback in the application
      // level handler.

       wt->Resample(newRate, progress.get());

      // Each time a track is successfully, completely resampled,
      // commit that to the undo stack.  The second and later times,
      // consolidate.

      ProjectHistory::Get(project).PushState(
         XO("Resampled audio track(s)"), XO("Resample Track"), flags);
      flags = flags | UndoPush::CONSOLIDATE;
   }

   undoManager.StopConsolidating();

   // Need to reset
   viewport.DoScroll();
}

void OnRemoveTracks(const CommandContext &context)
{
   TrackUtilities::DoRemoveTracks( context.project );
}

static void MuteTracks(const CommandContext &context, bool mute, bool selected)
{
   auto &project = context.project;
   const auto &settings = ProjectSettings::Get( project );
   auto &tracks = TrackList::Get( project );

   const auto solo = TracksBehaviorsSolo.ReadEnum();
   const auto soloSimple = (solo == SoloBehaviorSimple);

   auto iter = selected ? tracks.Selected<PlayableTrack>() : tracks.Any<PlayableTrack>();
   for (auto pt : iter) {
      pt->SetMute(mute);
      if (soloSimple)
         pt->SetSolo(false);
   }

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

void OnMuteAllTracks(const CommandContext &context)
{
   MuteTracks(context, true, false);
}

void OnUnmuteAllTracks(const CommandContext &context)
{
   MuteTracks(context, false, false);
}

void OnMuteSelectedTracks(const CommandContext &context)
{
   MuteTracks(context, true, true);
}

void OnUnmuteSelectedTracks(const CommandContext &context)
{
   MuteTracks(context, false, true);
}

void OnPanLeft(const CommandContext &context)
{
   auto &project = context.project;
   DoPanTracks( project, -1.0);
}

void OnPanRight(const CommandContext &context)
{
   auto &project = context.project;
   DoPanTracks( project, 1.0);
}

void OnPanCenter(const CommandContext &context)
{
   auto &project = context.project;
   DoPanTracks( project, 0.0);
}

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

   DoAlign(project,
      context.index + kAlignLabelsCount(), false);
}

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

   bool bMoveWith;
   gPrefs->Read(wxT("/GUI/MoveSelectionWithTracks"), &bMoveWith, false);
   DoAlign(project, context.index, bMoveWith);
}

/*
// Now handled in OnAlign.
void OnAlignMoveSel(int index)
{
   DoAlign(index, true);
}
*/

void OnMoveSelectionWithTracks(const CommandContext &WXUNUSED(context) )
{
   bool bMoveWith;
   gPrefs->Read(wxT("/GUI/MoveSelectionWithTracks"), &bMoveWith, false);
   gPrefs->Write(wxT("/GUI/MoveSelectionWithTracks"), !bMoveWith);
   gPrefs->Flush();

}

#ifdef EXPERIMENTAL_SCOREALIGN
void OnScoreAlign(const CommandContext &context)
{
   auto &project = context.project;
   auto &tracks = TrackList::Get( project );
   const auto rate = ProjectSettings::Get( project ).GetRate();

   int numWaveTracksSelected = 0;
   int numNoteTracksSelected = 0;
   int numOtherTracksSelected = 0;
   double endTime = 0.0;

   // Iterate through once to make sure that there is exactly
   // one WaveTrack and one NoteTrack selected.
   tracks.Selected().Visit(
      [&](WaveTrack *wt) {
         numWaveTracksSelected++;
         endTime = endTime > wt->GetEndTime() ? endTime : wt->GetEndTime();
      },
      [&](NoteTrack *) {
         numNoteTracksSelected++;
      },
      [&](Track*) {
         numOtherTracksSelected++;
      }
   );

   if(numWaveTracksSelected == 0 ||
      numNoteTracksSelected != 1 ||
      numOtherTracksSelected != 0){
      AudacityMessageBox(
         XO("Please select at least one audio track and one MIDI track.") );
      return;
   }

   // Creating the dialog also stores dialog into gScoreAlignDialog so
   // that it can be deleted by CloseScoreAlignDialog() either here or
   // if the program is quit by the user while the dialog is up.
   ScoreAlignParams params;

   // safe because the class maintains a global resource pointer
   safenew ScoreAlignDialog(params);

   CloseScoreAlignDialog();

   if (params.mStatus != wxID_OK) return;

   // We're going to do it.
   //pushing the state before the change is wrong (I think)
   //PushState(XO("Sync MIDI with Audio"), XO("Sync MIDI with Audio"));
   // Make a copy of the note track in case alignment is canceled or fails
   auto holder = nt->Duplicate();
   auto alignedNoteTrack = static_cast<NoteTrack*>(holder.get());
   // Remove offset from NoteTrack because audio is
   // mixed starting at zero and incorporating clip offsets.
   if (alignedNoteTrack->GetOffset() < 0) {
      // remove the negative offset data before alignment
      nt->Clear(alignedNoteTrack->GetOffset(), 0);
   } else if (alignedNoteTrack->GetOffset() > 0) {
      alignedNoteTrack->Shift(alignedNoteTrack->GetOffset());
   }
   alignedNoteTrack->MoveTo(0);

   WaveTrackConstArray waveTracks =
      tracks->GetWaveTrackConstArray(true /* selectionOnly */);

   int result;
   {
      Mixer mix(
         waveTracks,              // const WaveTrackConstArray &inputTracks
         false, // mayThrow -- is this right?
         Mixer::WarpOptions{ *tracks },
         0.0,                     // double startTime
         endTime,                 // double stopTime
         2,                       // int numOutChannels
         44100u,                   // size_t outBufferSize
         true,                    // bool outInterleaved
         rate,                   // double outRate
         floatSample,             // sampleFormat outFormat
         true,                    // bool highQuality = true
         NULL);                   // MixerSpec *mixerSpec = NULL

      ASAProgress progress;

      // There's a lot of adjusting made to incorporate the note track offset into
      // the note track while preserving the position of notes within beats and
      // measures. For debugging, you can see just the pre-scorealign note track
      // manipulation by setting SKIP_ACTUAL_SCORE_ALIGNMENT. You could then, for
      // example, save the modified note track in ".gro" form to read the details.
      //#define SKIP_ACTUAL_SCORE_ALIGNMENT 1
#ifndef SKIP_ACTUAL_SCORE_ALIGNMENT
      result = scorealign((void *) &mix, &mixer_process,
         2 /* channels */, 44100.0 /* srate */, endTime,
         &alignedNoteTrack->GetSeq(), &progress, params);
#else
      result = SA_SUCCESS;
#endif
   }

   if (result == SA_SUCCESS) {
      tracks->Replace(nt, holder);
      AudacityMessageBox(
         XO("Alignment completed: MIDI from %.2f to %.2f secs, Audio from %.2f to %.2f secs.")
            .Format(
               params.mMidiStart, params.mMidiEnd,
               params.mAudioStart, params.mAudioEnd) );
      ProjectHistory::Get( project )
         .PushState(XO("Sync MIDI with Audio"), XO("Sync MIDI with Audio"));
   } else if (result == SA_TOOSHORT) {
      AudacityMessageBox(
         XO(
"Alignment error: input too short: MIDI from %.2f to %.2f secs, Audio from %.2f to %.2f secs.")
            .Format(
               params.mMidiStart, params.mMidiEnd,
               params.mAudioStart, params.mAudioEnd) );
   } else if (result == SA_CANCEL) {
      // wrong way to recover...
      //project.OnUndo(); // recover any changes to note track
      return; // no message when user cancels alignment
   } else {
      //project.OnUndo(); // recover any changes to note track
      AudacityMessageBox( XO("Internal error reported by alignment process.") );
   }
}
#endif /* EXPERIMENTAL_SCOREALIGN */

void OnSortTime(const CommandContext &context)
{
   auto &project = context.project;
   DoSortTracks(project, kAudacitySortByTime);

   ProjectHistory::Get( project )
      .PushState(XO("Tracks sorted by time"), XO("Sort by Time"));
}

void OnSortName(const CommandContext &context)
{
   auto &project = context.project;
   DoSortTracks(project, kAudacitySortByName);

   ProjectHistory::Get( project )
      .PushState(XO("Tracks sorted by name"), XO("Sort by Name"));
}

void OnSyncLock(const CommandContext &context)
{
   auto &project = context.project;
   auto &trackPanel = TrackPanel::Get( project );

   SyncLockTracks.Toggle();
   gPrefs->Flush();

   // Toolbar, project sync-lock handled within
   ToolManager::ModifyAllProjectToolbarMenus();

   trackPanel.Refresh(false);
}

///The following methods operate controls on specified tracks,
///This will pop up the track panning dialog for specified track
void OnTrackPan(const CommandContext &context)
{
   auto &project = context.project;
   auto &trackPanel = TrackPanel::Get( project );

   const auto track = TrackFocus::Get( project ).Get();
   if (track) track->TypeSwitch( [&](WaveTrack &wt) {
      LWSlider *slider = WaveTrackControls::PanSlider( trackPanel, wt );
      if (slider->ShowDialog())
         SetTrackPan(project, &wt, slider);
   });
}

void OnTrackPanLeft(const CommandContext &context)
{
   auto &project = context.project;
   auto &trackPanel = TrackPanel::Get( project );

   const auto track = TrackFocus::Get( project ).Get();
   if (track) track->TypeSwitch( [&](WaveTrack &wt) {
      LWSlider *slider = WaveTrackControls::PanSlider( trackPanel, wt );
      slider->Decrease(1);
      SetTrackPan(project, &wt, slider);
   });
}

void OnTrackPanRight(const CommandContext &context)
{
   auto &project = context.project;
   auto &trackPanel = TrackPanel::Get( project );

   const auto track = TrackFocus::Get( project ).Get();
   if (track) track->TypeSwitch( [&](WaveTrack &wt) {
      LWSlider *slider = WaveTrackControls::PanSlider( trackPanel, wt );
      slider->Increase(1);
      SetTrackPan(project, &wt, slider);
   });
}

void OnTrackGain(const CommandContext &context)
{
   auto &project = context.project;
   auto &trackPanel = TrackPanel::Get( project );

   /// This will pop up the track gain dialog for specified track
   const auto track = TrackFocus::Get( project ).Get();
   if (track) track->TypeSwitch( [&](WaveTrack &wt) {
      LWSlider *slider = WaveTrackControls::VolumeSlider( trackPanel, wt );
      if (slider->ShowDialog())
         SetTrackGain(project, &wt, slider);
   });
}

void OnTrackGainInc(const CommandContext &context)
{
   auto &project = context.project;
   auto &trackPanel = TrackPanel::Get( project );

   const auto track = TrackFocus::Get( project ).Get();
   if (track) track->TypeSwitch( [&](WaveTrack &wt) {
      LWSlider *slider = WaveTrackControls::VolumeSlider( trackPanel, wt );
      slider->Increase(1);
      SetTrackGain(project, &wt, slider);
   });
}

void OnTrackGainDec(const CommandContext &context)
{
   auto &project = context.project;
   auto &trackPanel = TrackPanel::Get( project );

   const auto track = TrackFocus::Get( project ).Get();
   if (track) track->TypeSwitch( [&](WaveTrack &wt) {
      LWSlider *slider = WaveTrackControls::VolumeSlider( trackPanel, wt );
      slider->Decrease(1);
      SetTrackGain(project, &wt, slider);
   });
}

void OnTrackMenu(const CommandContext &context)
{
   auto &project = context.project;
   auto &trackPanel = TrackPanel::Get( project );

   trackPanel.OnTrackMenu();
}

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

   // Use the temporary selection if it is specified, else the track focus
   auto track = context.temporarySelection.pTrack;
   if (!track)
      track = TrackFocus::Get( project ).Get();

   if (track) track->TypeSwitch( [&](PlayableTrack &t) {
      TrackUtilities::DoTrackMute(project, t, false);
   });
}

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

   const auto track = TrackFocus::Get( project ).Get();
   if (track) track->TypeSwitch( [&](PlayableTrack &t) {
      TrackUtilities::DoTrackSolo(project, t, false);
   });
}

void OnTrackClose(const CommandContext &context)
{
   auto &project = context.project;
   auto &trackPanel = TrackPanel::Get( project );

   const auto t = TrackFocus::Get( project ).Get();
   if (!t)
      return;

   auto isAudioActive = ProjectAudioIO::Get( project ).IsAudioActive();

   if (isAudioActive)
   {
      ProjectStatus::Get( project ).Set(
         XO("Can't delete track with active audio"));
      wxBell();
      return;
   }

   TrackUtilities::DoRemoveTrack(project, *t);

   trackPanel.UpdateViewIfNoTracks();
   trackPanel.Refresh(false);
}

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

   const auto focusedTrack = TrackFocus::Get( project ).Get();
   if (focusedTrack && tracks.CanMoveUp(*focusedTrack)) {
      DoMoveTrack(project, *focusedTrack, TrackUtilities::OnMoveUpID);
      trackPanel.Refresh(false);
   }
}

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

   const auto focusedTrack = TrackFocus::Get( project ).Get();
   if (focusedTrack && tracks.CanMoveDown(*focusedTrack)) {
      DoMoveTrack(project, *focusedTrack, TrackUtilities::OnMoveDownID);
      trackPanel.Refresh(false);
   }
}

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

   const auto focusedTrack = TrackFocus::Get( project ).Get();
   if (focusedTrack && tracks.CanMoveUp(*focusedTrack)) {
      DoMoveTrack(project, *focusedTrack, TrackUtilities::OnMoveTopID);
      trackPanel.Refresh(false);
   }
}

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

   const auto focusedTrack = TrackFocus::Get( project ).Get();
   if (focusedTrack && tracks.CanMoveDown(*focusedTrack)) {
      DoMoveTrack(project, *focusedTrack, TrackUtilities::OnMoveBottomID);
      trackPanel.Refresh(false);
   }
}

// Menu definitions

// Under /MenuBar
using namespace MenuRegistry;
auto TracksMenu()
{
   // Tracks Menu (formerly Project Menu)
   static auto menu = std::shared_ptr{
   Menu( wxT("Tracks"), XXO("&Tracks"),
      Section( "Add",
         Menu( wxT("Add"), XXO("Add &New") )
      ),

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

      Section( "",
         Menu( wxT("Mix"), XXO("Mi&x"),
            // Delayed evaluation
            // Stereo to Mono is an oddball command that is also subject to control
            // by the plug-in manager, as if an effect.  Decide whether to show or
            // hide it.
            [](AudacityProject&) -> std::unique_ptr<CommandItem> {
               const PluginID ID =
                  PluginManager::Get().GetByCommandIdentifier(wxT("StereoToMono"));
               const PluginDescriptor *plug = PluginManager::Get().GetPlugin(ID);
               if (plug && plug->IsEnabled())
                  return Command( wxT("Stereo to Mono"),
                     XXO("Mix Stereo Down to &Mono"), OnStereoToMono,
                     AudioIONotBusyFlag() | StereoRequiredFlag() |
                        WaveTracksSelectedFlag(), Options{} );
               else
                  return {};
            },
            Command( wxT("MixAndRender"), XXO("Mi&x and Render"),
               OnMixAndRender,
               AudioIONotBusyFlag() | WaveTracksSelectedFlag() ),
            Command( wxT("MixAndRenderToNewTrack"),
               XXO("Mix and Render to Ne&w Track"),
               OnMixAndRenderToNewTrack,
               AudioIONotBusyFlag() | WaveTracksSelectedFlag(), wxT("Ctrl+Shift+M") )
         ),

         Command( wxT("Resample"), XXO("&Resample..."), OnResample,
            AudioIONotBusyFlag() | WaveTracksSelectedFlag() )
      ),

      Section( "",
         Command( wxT("RemoveTracks"), XXO("Remo&ve Tracks"), OnRemoveTracks,
            AudioIONotBusyFlag() | AnyTracksSelectedFlag() )
      ),

      Section( "",
         Menu( wxT("Mute"), XXO("M&ute/Unmute"),
            Command( wxT("MuteAllTracks"), XXO("&Mute All Tracks"),
               OnMuteAllTracks, TracksExistFlag(), wxT("Ctrl+U") ),
            Command( wxT("UnmuteAllTracks"), XXO("&Unmute All Tracks"),
               OnUnmuteAllTracks, TracksExistFlag(), wxT("Ctrl+Shift+U") ),
            Command( wxT("MuteTracks"), XXO("Mut&e Tracks"),
               OnMuteSelectedTracks, EditableTracksSelectedFlag(), wxT("Ctrl+Alt+U") ),
            Command( wxT("UnmuteTracks"), XXO("U&nmute Tracks"),
               OnUnmuteSelectedTracks, EditableTracksSelectedFlag(), wxT("Ctrl+Alt+Shift+U") )
         ),

         Menu( wxT("Pan"), XXO("&Pan"),
            // As Pan changes are not saved on Undo stack,
            // pan settings for all tracks
            // in the project could very easily be lost unless we
            // require the tracks to be selected.
            Command( wxT("PanLeft"), XXO("&Left"), OnPanLeft,
               EditableTracksSelectedFlag(),
               Options{}.LongName( XO("Pan Left") ) ),
            Command( wxT("PanRight"), XXO("&Right"), OnPanRight,
               EditableTracksSelectedFlag(),
               Options{}.LongName( XO("Pan Right") ) ),
            Command( wxT("PanCenter"), XXO("&Center"), OnPanCenter,
               EditableTracksSelectedFlag(),
               Options{}.LongName( XO("Pan Center") ) )
         )
      ),

      Section( "",
         Menu( wxT("Align"), XXO("&Align Tracks"), // XO("Just Move Tracks"),
            Section( "",
               // Mutual alignment of tracks independent of selection or zero
               CommandGroup(wxT("Align"),
                  {
                     { wxT("EndToEnd"),     XXO("&Align End to End") },
                     { wxT("Together"),     XXO("Align &Together") },
                  },
                  OnAlignNoSync, AudioIONotBusyFlag() | EditableTracksSelectedFlag())
            ),

            Section( "",
               // Alignment commands using selection or zero
               CommandGroup(wxT("Align"),
                  alignLabels(),
                  OnAlign, AudioIONotBusyFlag() | EditableTracksSelectedFlag())
            ),

            Section( "",
               Command( wxT("MoveSelectionWithTracks"),
                  XXO("&Move Selection with Tracks"),
                  OnMoveSelectionWithTracks,
                  AlwaysEnabledFlag,
                  Options{}.CheckTest( wxT("/GUI/MoveSelectionWithTracks"), false ) )
            )
         ),

   #if 0
         // TODO: Can these labels be made clearer?
         // Do we need this sub-menu at all?
         Menu( wxT("MoveSelectionAndTracks"), XO("Move Sele&ction and Tracks"), {
            CommandGroup(wxT("AlignMove"), alignLabels(),
               OnAlignMoveSel, AudioIONotBusyFlag() | EditableTracksSelectedFlag()),
         } ),
   #endif

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

   #ifdef EXPERIMENTAL_SCOREALIGN
         Command( wxT("ScoreAlign"), XXO("Synchronize MIDI with Audio"),
            OnScoreAlign,
            AudioIONotBusyFlag() | NoteTracksSelectedFlag() | WaveTracksSelectedFlag() ),
   #endif // EXPERIMENTAL_SCOREALIGN

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

         Menu( wxT("Sort"), XXO("S&ort Tracks"),
            Command( wxT("SortByTime"), XXO("By &Start Time"), OnSortTime,
               TracksExistFlag(),
               Options{}.LongName( XO("Sort by Time") ) ),
            Command( wxT("SortByName"), XXO("By &Name"), OnSortName,
               TracksExistFlag(),
               Options{}.LongName( XO("Sort by Name") ) )
         )

         //////////////////////////////////////////////////////////////////////////
      ),

      Section( "",
         Command( wxT("SyncLock"), XXO("Keep tracks synchronized (Sync-&Lock)"),
            OnSyncLock, AlwaysEnabledFlag,
            Options{}.CheckTest(SyncLockTracks) )
      )

   ) };
   return menu;
}

AttachedItem sAttachment1{ Indirect(TracksMenu()) };

auto ExtraTrackMenu()
{
   static auto menu = std::shared_ptr{
   Menu( wxT("Track"), XXO("&Track"),
      Command( wxT("TrackPan"), XXO("Change P&an on Focused Track..."),
         OnTrackPan,
         TrackPanelHasFocus() | TracksExistFlag(), wxT("Shift+P") ),
      Command( wxT("TrackPanLeft"), XXO("Pan &Left on Focused Track"),
         OnTrackPanLeft,
         TrackPanelHasFocus() | TracksExistFlag(), wxT("Alt+Shift+Left") ),
      Command( wxT("TrackPanRight"), XXO("Pan &Right on Focused Track"),
         OnTrackPanRight,
         TrackPanelHasFocus() | TracksExistFlag(), wxT("Alt+Shift+Right") ),
      Command( wxT("TrackGain"), XXO("Change Gai&n on Focused Track..."),
         OnTrackGain,
         TrackPanelHasFocus() | TracksExistFlag(), wxT("Shift+G") ),
      Command( wxT("TrackGainInc"), XXO("&Increase Gain on Focused Track"),
         OnTrackGainInc,
         TrackPanelHasFocus() | TracksExistFlag(), wxT("Alt+Shift+Up") ),
      Command( wxT("TrackGainDec"), XXO("&Decrease Gain on Focused Track"),
         OnTrackGainDec,
         TrackPanelHasFocus() | TracksExistFlag(), wxT("Alt+Shift+Down") ),
      Command( wxT("TrackMenu"), XXO("Op&en Menu on Focused Track..."),
         OnTrackMenu,
         TracksExistFlag() | TrackPanelHasFocus(),
         Options{ wxT("Shift+M") }.SkipKeyDown() ),
      Command( wxT("TrackMute"), XXO("M&ute/Unmute Focused Track"),
         OnTrackMute,
         TracksExistFlag() | TrackPanelHasFocus(), wxT("Shift+U") ),
      Command( wxT("TrackSolo"), XXO("&Solo/Unsolo Focused Track"),
         OnTrackSolo,
         TracksExistFlag() | TrackPanelHasFocus(), wxT("Shift+S") ),
      Command( wxT("TrackClose"), XXO("Delete Fo&cused Track"),
         OnTrackClose,
         AudioIONotBusyFlag() | TrackPanelHasFocus() | TracksExistFlag(),
         wxT("Shift+C") ),
      Command( wxT("TrackMoveUp"), XXO("Move Focused Track U&p"),
         OnTrackMoveUp,
         AudioIONotBusyFlag() | TrackPanelHasFocus() | TracksExistFlag() ),
      Command( wxT("TrackMoveDown"), XXO("Move Focused Track Do&wn"),
         OnTrackMoveDown,
         AudioIONotBusyFlag() | TrackPanelHasFocus() | TracksExistFlag() ),
      Command( wxT("TrackMoveTop"), XXO("Move Focused Track to T&op"),
         OnTrackMoveTop,
         AudioIONotBusyFlag() | TrackPanelHasFocus() | TracksExistFlag() ),
      Command( wxT("TrackMoveBottom"), XXO("Move Focused Track to &Bottom"),
         OnTrackMoveBottom,
         AudioIONotBusyFlag() | TrackPanelHasFocus() | TracksExistFlag() )
   ) };
   return menu;
}

AttachedItem sAttachment2{ Indirect(ExtraTrackMenu()),
   wxT("Optional/Extra/Part2")
};

}
