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

  Audacity: A Digital Audio Editor

  Benchmark.cpp

  Dominic Mazzoni

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

\class BenchmarkDialog
\brief BenchmarkDialog is used for measuring performance and accuracy
of sample block storage.

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


#include "Benchmark.h"

#include <wx/app.h>
#include <wx/log.h>
#include <wx/textctrl.h>
#include <wx/button.h>
#include <wx/checkbox.h>
#include <wx/choice.h>
#include <wx/dialog.h>
#include <wx/sizer.h>
#include <wx/stattext.h>
#include <wx/timer.h>
#include <wx/utils.h>
#include <wx/valgen.h>
#include <wx/valtext.h>
#include <wx/intl.h>

// Tenacity libraries
#include <lib-files/FileNames.h>
#include <lib-preferences/Prefs.h>

#include "SampleBlock.h"
#include "shuttle/ShuttleGui.h"
#include "Project.h"
#include "WaveClip.h"
#include "WaveTrack.h"
#include "Sequence.h"
#include "ProjectRate.h"
#include "ViewInfo.h"

#include "SelectFile.h"
#include "widgets/AudacityMessageBox.h"
#include "widgets/wxPanelWrapper.h"

// Change these to the desired format...should probably make the
// choice available in the dialog
#define SampleType short
#define SampleFormat int16Sample

class BenchmarkDialog final : public wxDialogWrapper
{
public:
   // constructors and destructors
   BenchmarkDialog( wxWindow *parent, TenacityProject &project );

   void MakeBenchmarkDialog();

private:
   // WDR: handler declarations
   void OnRun( wxCommandEvent &event );
   void OnSave( wxCommandEvent &event );
   void OnClear( wxCommandEvent &event );
   void OnClose( wxCommandEvent &event );

   void Printf(const TranslatableString &str);
   void HoldPrint(bool hold);
   void FlushPrint();

   TenacityProject &mProject;
   const ProjectRate &mRate;

   bool      mHoldPrint;
   wxString  mToPrint;

   wxString  mBlockSizeStr;
   wxString  mDataSizeStr;
   wxString  mNumEditsStr;
   wxString  mRandSeedStr;

   bool      mBlockDetail;
   bool      mEditDetail;

   wxTextCtrl  *mText;

private:
   DECLARE_EVENT_TABLE()
};

void RunBenchmark( wxWindow *parent, TenacityProject &project )
{
   /*
   int action = AudacityMessageBox(
XO("This will close all project windows (without saving)\nand open the Audacity Benchmark dialog.\n\nAre you sure you want to do this?"),
      XO("Benchmark"),
      wxYES_NO | wxICON_EXCLAMATION,
      NULL);

   if (action != wxYES)
      return;

   for ( auto pProject : AllProjects{} )
      GetProjectFrame( *pProject ).Close();
   */

   BenchmarkDialog dlog{ parent, project };

   dlog.CentreOnParent();

   dlog.ShowModal();
}

//
// BenchmarkDialog
//

enum {
   RunID = 1000,
   BSaveID,
   ClearID,
   StaticTextID,
   BlockSizeID,
   DataSizeID,
   NumEditsID,
   RandSeedID
};

BEGIN_EVENT_TABLE(BenchmarkDialog, wxDialogWrapper)
   EVT_BUTTON( RunID,   BenchmarkDialog::OnRun )
   EVT_BUTTON( BSaveID,  BenchmarkDialog::OnSave )
   EVT_BUTTON( ClearID, BenchmarkDialog::OnClear )
   EVT_BUTTON( wxID_CANCEL, BenchmarkDialog::OnClose )
END_EVENT_TABLE()

BenchmarkDialog::BenchmarkDialog(
   wxWindow *parent, TenacityProject &project)
   :
      /* i18n-hint: Benchmark means a software speed test */
      wxDialogWrapper( parent, 0, XO("Benchmark"),
                wxDefaultPosition, wxDefaultSize,
                wxDEFAULT_DIALOG_STYLE |
                wxRESIZE_BORDER)
   , mProject(project)
   , mRate{ ProjectRate::Get(project) }
{
   SetName();

   mBlockSizeStr = wxT("64");
   mNumEditsStr = wxT("100");
   mDataSizeStr = wxT("32");
   mRandSeedStr = wxT("234657");

   mBlockDetail = false;
   mEditDetail = false;

   HoldPrint(false);

   MakeBenchmarkDialog();
}

// WDR: handler implementations for BenchmarkDialog

void BenchmarkDialog::OnClose(wxCommandEvent & WXUNUSED(event))
{
   EndModal(0);
}

void BenchmarkDialog::MakeBenchmarkDialog()
{
   ShuttleGui S(this, eIsCreating);

   // Strings don't need to be translated because this class doesn't
   // ever get used in a stable release.

   S.StartVerticalLay(true);
   {
      S.SetBorder(8);
      S.StartMultiColumn(4);
      {
         //
         S.Id(BlockSizeID)
            .Validator<wxTextValidator>(wxFILTER_NUMERIC, &mBlockSizeStr)
            .AddTextBox(XXO("Disk Block Size (KB):"),
                                             wxT(""),
                                             12);

         //
         S.Id(NumEditsID)
            .Validator<wxTextValidator>(wxFILTER_NUMERIC, &mNumEditsStr)
            .AddTextBox(XXO("Number of Edits:"),
                                            wxT(""),
                                            12);

         //
         S.Id(DataSizeID)
            .Validator<wxTextValidator>(wxFILTER_NUMERIC, &mDataSizeStr)
            .AddTextBox(XXO("Test Data Size (MB):"),
                                            wxT(""),
                                            12);

         ///
         S.Id(RandSeedID)
            .Validator<wxTextValidator>(wxFILTER_NUMERIC, &mRandSeedStr)
            /* i18n-hint: A "seed" is a number that initializes a
               pseudorandom number generating algorithm */
            .AddTextBox(XXO("Random Seed:"),
                                            wxT(""),
                                            12);

      }
      S.EndMultiColumn();

      //
      S.Validator<wxGenericValidator>(&mBlockDetail)
         .AddCheckBox(XXO("Show detailed info about each block file"),
                           false);

      //
      S.Validator<wxGenericValidator>(&mEditDetail)
         .AddCheckBox(XXO("Show detailed info about each editing operation"),
                           false);

      //
      mText = S.Id(StaticTextID)
         /* i18n-hint noun */
         .Name(XO("Output"))
         .Style( wxTE_MULTILINE | wxTE_READONLY | wxTE_RICH )
         .MinSize( { 500, 200 } )
         .AddTextWindow(wxT(""));

      //
      S.SetBorder(10);
      S.StartHorizontalLay(wxALIGN_LEFT | wxEXPAND, false);
      {
         S.StartHorizontalLay(wxALIGN_LEFT, false);
         {
            S.Id(RunID).AddButton(XXO("Run"), wxALIGN_CENTRE, true);
            S.Id(BSaveID).AddButton(XXO("Save"));
            /* i18n-hint verb; to empty or erase */
            S.Id(ClearID).AddButton(XXO("Clear"));
         }
         S.EndHorizontalLay();

         S.StartHorizontalLay(wxALIGN_CENTER, true);
         {
            // Spacer
         }
         S.EndHorizontalLay();

         S.StartHorizontalLay(wxALIGN_NOT | wxALIGN_LEFT, false);
         {
            /* i18n-hint verb */
            S.Id(wxID_CANCEL).AddButton(XXO("Close"));
         }
         S.EndHorizontalLay();
      }
      S.EndHorizontalLay();
   }
   S.EndVerticalLay();

   Fit();
   SetSizeHints(GetSize());
}

void BenchmarkDialog::OnSave( wxCommandEvent & WXUNUSED(event))
{
/* i18n-hint: Benchmark means a software speed test;
   leave untranslated file extension .txt */
   auto fName = XO("benchmark.txt").Translation();

   fName = SelectFile(FileNames::Operation::Export,
      XO("Export Benchmark Data as:"),
      wxEmptyString,
      fName,
      wxT("txt"),
      { FileNames::TextFiles },
      wxFD_SAVE | wxRESIZE_BORDER,
      this);

   if (fName.empty())
      return;

   mText->SaveFile(fName);
}

void BenchmarkDialog::OnClear(wxCommandEvent & WXUNUSED(event))
{
   mText->Clear();
}

void BenchmarkDialog::Printf(const TranslatableString &str)
{
   auto s = str.Translation();
   mToPrint += s;
   if (!mHoldPrint)
      FlushPrint();
}

void BenchmarkDialog::HoldPrint(bool hold)
{
   mHoldPrint = hold;

   if (!mHoldPrint)
      FlushPrint();
}

void BenchmarkDialog::FlushPrint()
{
   while(mToPrint.length() > 100) {
      mText->AppendText(mToPrint.Left(100));
      mToPrint = mToPrint.Right(mToPrint.length() - 100);
   }
   if (mToPrint.length() > 0)
      mText->AppendText(mToPrint);
   mToPrint = wxT("");
}

void BenchmarkDialog::OnRun( wxCommandEvent & WXUNUSED(event))
{
   TransferDataFromWindow();

   if (!Validate())
      return;

   // This code will become part of libaudacity,
   // and this class will be phased out.
   long blockSize, numEdits, dataSize, randSeed;

   mBlockSizeStr.ToLong(&blockSize);
   mNumEditsStr.ToLong(&numEdits);
   mDataSizeStr.ToLong(&dataSize);
   mRandSeedStr.ToLong(&randSeed);

   if (blockSize < 1 || blockSize > 1024) {
      AudacityMessageBox(
         XO("Block size should be in the range 1 - 1024 KB.") );
      return;
   }

   if (numEdits < 1 || numEdits > 10000) {
      AudacityMessageBox(
         XO("Number of edits should be in the range 1 - 10000.") );
      return;
   }

   if (dataSize < 1 || dataSize > 2000) {
      AudacityMessageBox(
         XO("Test data size should be in the range 1 - 2000 MB.") );
      return;
   }

   bool editClipCanMove = true;
   gPrefs->Read(wxT("/GUI/EditClipCanMove"), &editClipCanMove);
   gPrefs->Write(wxT("/GUI/EditClipCanMove"), false);
   gPrefs->Flush();

   // Remember the old blocksize, so that we can restore it later.
   auto oldBlockSize = Sequence::GetMaxDiskBlockSize();
   Sequence::SetMaxDiskBlockSize(blockSize * 1024);

   const auto cleanup = finally( [&] {
      Sequence::SetMaxDiskBlockSize(oldBlockSize);
      gPrefs->Write(wxT("/GUI/EditClipCanMove"), editClipCanMove);
      gPrefs->Flush();
   } );

   wxBusyCursor busy;

   HoldPrint(true);

   const auto t =
      WaveTrackFactory{ mRate,
                    SampleBlockFactory::New( mProject )  }
         .NewWaveTrack(SampleFormat);

   t->SetRate(1);

   srand(randSeed);

   uint64_t nChunks, chunkSize;
   //chunkSize = 7500ull + (rand() % 1000ull);
   chunkSize = 200ull + (rand() % 100ull);
   nChunks = (dataSize * 1048576ull) / (chunkSize*sizeof(SampleType));
   while (nChunks < 20 || chunkSize > (blockSize*1024)/4)
   {
      chunkSize = std::max( uint64_t(1), (chunkSize / 2) + (rand() % 100) );
      nChunks = (dataSize * 1048576ull) / (chunkSize*sizeof(SampleType));
   }

   // The chunks are the pieces we move around in the test.
   // They are (and are supposed to be) a different size to
   // the blocks that make the sample blocks.  That way we get to
   // do some testing of when edit chunks cross sample block boundaries.
   Printf( XO("Using %lld chunks of %lld samples each, for a total of %.1f MB.\n")
      .Format( nChunks, chunkSize, nChunks*chunkSize*sizeof(SampleType)/1048576.0 ) );

   int trials = numEdits;

   using Samples = ArrayOf<SampleType>;
   Samples small1{nChunks};
   Samples block{chunkSize};

   Printf( XO("Preparing...\n") );

   wxTheApp->Yield();
   FlushPrint();

   int v;
   int bad;
   int z;
   long elapsed;
   wxString tempStr;
   wxStopWatch timer;

   for (uint64_t i = 0; i < nChunks; i++) {
      v = SampleType(rand());
      small1[i] = v;
      for (uint64_t b = 0; b < chunkSize; b++)
         block[b] = v;

      t->Append((samplePtr)block.get(), SampleFormat, chunkSize);
   }
   t->Flush();

   // This forces the WaveTrack to flush all of the appends (which is
   // only necessary if you want to access the Sequence class directly,
   // as we're about to do).
   t->GetEndTime();

   if (t->GetClipByIndex(0)->GetPlaySamplesCount() != nChunks * chunkSize) {
      Printf( XO("Expected len %lld, track len %lld.\n")
         .Format(
            nChunks * chunkSize,
            t->GetClipByIndex(0)->GetPlaySamplesCount()
               .as_long_long() ) );
      goto fail;
   }

   Printf( XO("Performing %d edits...\n").Format( trials ) );
   wxTheApp->Yield();
   FlushPrint();

   timer.Start();
   for (z = 0; z < trials; z++) {
      // First chunk to cut
      // 0 <= x0 < nChunks
      const uint64_t x0 = rand() % nChunks;

      // Number of chunks to cut
      // 1 <= xlen <= nChunks - x0
      const uint64_t xlen = 1 + (rand() % (nChunks - x0));
      if (mEditDetail)
         Printf( XO("Cut: %lld - %lld \n")
            .Format( x0 * chunkSize, (x0 + xlen) * chunkSize) );

      Track::Holder tmp;
      try {
         tmp = t->Cut(double (x0 * chunkSize), double ((x0 + xlen) * chunkSize));
      }
      catch (const TenacityException&) {
         Printf( XO("Trial %d\n").Format( z ) );
         Printf( XO("Cut (%lld, %lld) failed.\n")
            .Format( (x0 * chunkSize), (x0 + xlen) * chunkSize) );
         Printf( XO("Expected len %lld, track len %lld.\n")
            .Format(
               nChunks * chunkSize,
               t->GetClipByIndex(0)->GetPlaySamplesCount()
                  .as_long_long() ) );
         goto fail;
      }

      // Position to paste
      // 0 <= y0 <= nChunks - xlen
      const uint64_t y0 = rand() % (nChunks - xlen + 1);

      if (mEditDetail)
         Printf( XO("Paste: %lld\n").Format( y0 * chunkSize ) );

      try {
         t->Paste((double)(y0 * chunkSize), tmp.get());
      }
      catch (const TenacityException&) {
         Printf( XO("Trial %d\nFailed on Paste.\n").Format( z ) );
         goto fail;
      }

      if (t->GetClipByIndex(0)->GetPlaySamplesCount() != nChunks * chunkSize) {
         Printf( XO("Trial %d\n").Format( z ) );
         Printf( XO("Expected len %lld, track len %lld.\n")
            .Format(
               nChunks * chunkSize,
               t->GetClipByIndex(0)->GetPlaySamplesCount()
                  .as_long_long() ) );
         goto fail;
      }

      // Permute small1 correspondingly to the cut and paste
      auto first = &small1[0];
      if (x0 + xlen < nChunks)
         std::rotate( first + x0, first + x0 + xlen, first + nChunks );
      std::rotate( first + y0, first + nChunks - xlen, first + nChunks );
   }

   elapsed = timer.Time();

   if (mBlockDetail) {
      auto seq = t->GetClipByIndex(0)->GetSequence();
      seq->DebugPrintf(seq->GetBlockArray(), seq->GetNumSamples(), &tempStr);
      mToPrint += tempStr;
   }
   Printf( XO("Time to perform %d edits: %ld ms\n").Format( trials, elapsed ) );
   FlushPrint();
   wxTheApp->Yield();


#if 0
   Printf( XO("Checking file pointer leaks:\n") );
   Printf( XO("Track # blocks: %ld\n").Format( t->GetBlockArray()->size() ) );
   Printf( XO("Disk # blocks: \n") );
   system("ls .audacity_temp/* | wc --lines");
#endif

   Printf( XO("Doing correctness check...\n") );
   FlushPrint();
   wxTheApp->Yield();

   bad = 0;
   timer.Start();
   for (uint64_t i = 0; i < nChunks; i++) {
      v = small1[i];
      t->Get((samplePtr)block.get(), SampleFormat, i * chunkSize, chunkSize);
      for (uint64_t b = 0; b < chunkSize; b++)
         if (block[b] != v) {
            bad++;
            if (bad < 10)
               Printf( XO("Bad: chunk %lld sample %lld\n").Format( i, b ) );
            b = chunkSize;
         }
   }
   if (bad == 0)
      Printf( XO("Passed correctness check!\n") );
   else
      Printf( XO("Errors in %d/%lld chunks\n").Format( bad, nChunks ) );

   elapsed = timer.Time();

   Printf( XO("Time to check all data: %ld ms\n").Format( elapsed ) );
   Printf( XO("Reading data again...\n") );

   wxTheApp->Yield();
   FlushPrint();

   timer.Start();

   for (uint64_t i = 0; i < nChunks; i++) {
      v = small1[i];
      t->Get((samplePtr)block.get(), SampleFormat, i * chunkSize, chunkSize);
      for (uint64_t b = 0; b < chunkSize; b++)
         if (block[b] != v)
            bad++;
   }

   elapsed = timer.Time();

   Printf( XO("Time to check all data (2): %ld ms\n").Format( elapsed ) );

   Printf( XO("At 44100 Hz, %d bytes per sample, the estimated number of\n simultaneous tracks that could be played at once: %.1f\n" )
      .Format( SAMPLE_SIZE(SampleFormat), (nChunks*chunkSize/44100.0)/(elapsed/1000.0) ) );

   goto success;

 fail:
   Printf( XO("TEST FAILED!!!\n") );

 success:

   Printf( XO("Benchmark completed successfully.\n") );
   HoldPrint(false);
}
