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

  Audacity: A Digital Audio Editor

  ChangeTempo.cpp

  Vaughan Johnson, 
  Dominic Mazzoni

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

\class EffectChangeTempo
\brief An EffectSoundTouch provides speeding up or 
  slowing down tempo without changing pitch.

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

\class ChangeTempoDialog
\brief Dialog used with EffectChangeTempo

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

#include "../Audacity.h" // for USE_SOUNDTOUCH

#if USE_SOUNDTOUCH

#include "ChangeTempo.h"

#include "../ShuttleGui.h"
#include "TimeWarper.h"

#include <math.h>

#include <wx/button.h>
#include <wx/sizer.h>
#include <wx/stattext.h>
#include <wx/textctrl.h>
#include <wx/valtext.h>


//
// EffectChangeTempo
//

EffectChangeTempo::EffectChangeTempo()
{
   m_PercentChange = 0.0;
   m_FromBPM = 0.0; // indicates not yet set
   m_ToBPM = 0.0; // indicates not yet set
   m_FromLength = 0.0;
   m_ToLength = 0.0;
}

double EffectChangeTempo::CalcPreviewInputLength(double previewLength)
{
   return previewLength * (100.0 + m_PercentChange) / 100.0;
}

wxString EffectChangeTempo::GetEffectDescription() { 
   // Note: This is useful only after change amount has been set.
   return wxString::Format(_("Applied effect: %s %.1f%%"),
                           this->GetEffectName().c_str(),
                           m_PercentChange);
} 

bool EffectChangeTempo::Init()
{
   // The selection might have changed since the last time EffectChangeTempo
   // was invoked, so recalculate the Length parameters.
   m_FromLength = mT1 - mT0;
   m_ToLength = (m_FromLength * 100.0) / (100.0 + m_PercentChange);

   mSoundTouch = NULL;

   return true;
}

bool EffectChangeTempo::PromptUser()
{
   ChangeTempoDialog dlog(this, mParent);
   dlog.m_PercentChange = m_PercentChange;
   dlog.m_FromBPM = m_FromBPM;
   dlog.m_ToBPM = m_ToBPM;
   dlog.m_FromLength = m_FromLength;
   dlog.m_ToLength = m_ToLength;
   // Don't need to call TransferDataToWindow, although other
   //	Audacity dialogs (from which I derived this one) do it, because
   //	ShowModal calls stuff that eventually calls wxWindowBase::OnInitDialog,
   //	which calls dlog.TransferDataToWindow();
   dlog.CentreOnParent();
   dlog.ShowModal();

   if (dlog.GetReturnCode() == wxID_CANCEL)
      return false;

   m_PercentChange = dlog.m_PercentChange;
   m_FromBPM = dlog.m_FromBPM;
   m_ToBPM = dlog.m_ToBPM;
   m_ToLength = dlog.m_ToLength;
   return true;
}

bool EffectChangeTempo::TransferParameters( Shuttle & shuttle )
{  
   shuttle.TransferDouble(wxT("Percentage"),m_PercentChange,0.0);
   return true;
}

bool EffectChangeTempo::Process()
{
   mSoundTouch = new SoundTouch();
   mSoundTouch->setTempoChange(m_PercentChange);
   double mT1Dashed = mT0 + (mT1 - mT0)/(m_PercentChange/100.0 + 1.0);
   SetTimeWarper(new RegionTimeWarper(mT0, mT1,
            new LinearTimeWarper(mT0, mT0, mT1, mT1Dashed )));
   bool success = this->EffectSoundTouch::Process();
   if( success )
      mT1 = mT0 + (mT1 - mT0)/(m_PercentChange/100 + 1.);
   return success;
}

//----------------------------------------------------------------------------
// ChangeTempoDialog
//----------------------------------------------------------------------------

#define PERCENTCHANGE_MIN -99
#define PERCENTCHANGE_MAX 100 // warped above zero to actually go up to 400%
#define PERCENTCHANGE_SLIDER_WARP 1.30105 // warp power takes max from 100 to 400.

#define ID_TEXT_PERCENTCHANGE 10001
#define ID_SLIDER_PERCENTCHANGE 10002
#define ID_TEXT_FROMBPM 10003
#define ID_TEXT_TOBPM 10004
#define ID_TEXT_FROMLENGTH 10005
#define ID_TEXT_TOLENGTH 10006

// event table for ChangeTempoDialog

BEGIN_EVENT_TABLE(ChangeTempoDialog, EffectDialog)
    EVT_TEXT(ID_TEXT_PERCENTCHANGE, ChangeTempoDialog::OnText_PercentChange)
    EVT_SLIDER(ID_SLIDER_PERCENTCHANGE, ChangeTempoDialog::OnSlider_PercentChange)
    EVT_TEXT(ID_TEXT_FROMBPM, ChangeTempoDialog::OnText_FromBPM)
    EVT_TEXT(ID_TEXT_TOBPM, ChangeTempoDialog::OnText_ToBPM)
    EVT_TEXT(ID_TEXT_TOLENGTH, ChangeTempoDialog::OnText_ToLength)

    EVT_BUTTON(ID_EFFECT_PREVIEW, ChangeTempoDialog::OnPreview)
END_EVENT_TABLE()

ChangeTempoDialog::ChangeTempoDialog(EffectChangeTempo *effect, wxWindow *parent)
:  EffectDialog(parent, _("Change Tempo"), PROCESS_EFFECT),
   mEffect(effect)
{
   m_bLoopDetect = false;

   // NULL out these control members because there are some cases where the
   // event table handlers get called during this method, and those handlers that
   // can cause trouble check for NULL.
   m_pTextCtrl_PercentChange = NULL;
   m_pSlider_PercentChange = NULL;
   m_pTextCtrl_FromBPM = NULL;
   m_pTextCtrl_ToBPM = NULL;
   m_pTextCtrl_FromLength = NULL;
   m_pTextCtrl_ToLength = NULL;

   // effect parameters
   m_PercentChange = 0.0;
   m_FromBPM = 0.0; // indicates not yet set
   m_ToBPM = 0.0; // indicates not yet set
   m_FromLength = 0.0;
   m_ToLength = 0.0;

   Init();
}

void ChangeTempoDialog::PopulateOrExchange(ShuttleGui & S)
{
   wxTextValidator nullvld(wxFILTER_INCLUDE_CHAR_LIST);
   wxTextValidator numvld(wxFILTER_NUMERIC);

   S.AddSpace(0, 5);
   S.AddTitle(_("Change Tempo without Changing Pitch"));
   S.SetBorder(5);

   //
   S.StartMultiColumn(2, wxCENTER);
   {
      m_pTextCtrl_PercentChange = S.Id(ID_TEXT_PERCENTCHANGE)
         .AddTextBox(_("Percent Change:"), wxT(""), 12);
      m_pTextCtrl_PercentChange->SetValidator(numvld);
   }
   S.EndMultiColumn();

   //
   S.StartHorizontalLay(wxEXPAND);
   {
      S.SetStyle(wxSL_HORIZONTAL);
      m_pSlider_PercentChange = S.Id(ID_SLIDER_PERCENTCHANGE)
         .AddSlider(wxT(""), 0, (int)PERCENTCHANGE_MAX, (int)PERCENTCHANGE_MIN);
      m_pSlider_PercentChange->SetName(_("Percent Change"));
   }
   S.EndHorizontalLay();

   // 
   S.StartMultiColumn(5, wxCENTER);
   {
      //
      S.AddUnits(_("Beats per minute:"));

      m_pTextCtrl_FromBPM = S.Id(ID_TEXT_FROMBPM)
         .AddTextBox(_("from"), wxT(""), 12);
      m_pTextCtrl_FromBPM->SetName(_("From beats per minute"));
      m_pTextCtrl_FromBPM->SetValidator(numvld);

      m_pTextCtrl_ToBPM = S.Id(ID_TEXT_TOBPM)
         .AddTextBox(_("to"), wxT(""), 12);
      m_pTextCtrl_ToBPM->SetName(_("To beats per minute"));
      m_pTextCtrl_ToBPM->SetValidator(numvld);

      //
      S.AddUnits(_("Length (seconds):"));

      m_pTextCtrl_FromLength = S.Id(ID_TEXT_FROMLENGTH)
         .AddTextBox(_("from"), wxT(""), 12);
      m_pTextCtrl_FromLength->SetName(_("From length in seconds"));
      m_pTextCtrl_FromLength->SetValidator(nullvld);

      m_pTextCtrl_ToLength = S.Id(ID_TEXT_TOLENGTH)
         .AddTextBox(_("to"), wxT(""), 12);
      m_pTextCtrl_ToLength->SetName(_("To length in seconds"));
      m_pTextCtrl_ToLength->SetValidator(numvld);
   }
   S.EndMultiColumn();

   return;
}

bool ChangeTempoDialog::TransferDataToWindow()
{
   m_bLoopDetect = true;

   // percent change controls
   this->Update_Text_PercentChange();
   this->Update_Slider_PercentChange();

   // from/to BPM controls
   wxString str;
   if (m_pTextCtrl_FromBPM) {
      if (m_FromBPM != 0.0)
         str.Printf(wxT("%.3f"), m_FromBPM);
      else
         str = wxT("");
      m_pTextCtrl_FromBPM->SetValue(str);
   }
   if (m_pTextCtrl_ToBPM) {
      if (m_ToBPM != 0.0)
         str.Printf(wxT("%.3f"), m_ToBPM);
      else
         str = wxT("");
      m_pTextCtrl_ToBPM->SetValue(str);
   }

   // from/to Length controls
   if (m_pTextCtrl_FromLength) {
      str.Printf(wxT("%.2f"), m_FromLength);
      m_pTextCtrl_FromLength->SetValue(str);
      m_pTextCtrl_FromLength->Enable(false); // Disable because the value comes from the user selection.
   }
   if (m_pTextCtrl_ToLength) {
      str.Printf(wxT("%.2f"), m_ToLength);
      m_pTextCtrl_ToLength->SetValue(str);
   }

   m_bLoopDetect = false;

   return true;
}

bool ChangeTempoDialog::TransferDataFromWindow()
{
   wxString str;

   // percent change controls
   if (m_pTextCtrl_PercentChange) {
      str = m_pTextCtrl_PercentChange->GetValue();
      double newValue = 0;
      str.ToDouble(&newValue);
      m_PercentChange = newValue;
   }

   // Ignore Slider_PercentChange because TextCtrl_PercentChange
   // always tracks it & is more precise (decimal points).

   // from/to BPM controls
   double newValue;
   if (m_pTextCtrl_FromBPM) {
      str = m_pTextCtrl_FromBPM->GetValue();
      str.ToDouble(&newValue);
      m_FromBPM = newValue;
   }
   if (m_pTextCtrl_ToBPM) {
      str = m_pTextCtrl_ToBPM->GetValue();
      str.ToDouble(&newValue);
      m_ToBPM = newValue;
   }

   // from/to Length controls
   // Don't do m_pTextCtrl_ToLength. It's disabled.
   if (m_pTextCtrl_ToLength) {
      str = m_pTextCtrl_ToLength->GetValue();
      str.ToDouble(&newValue);
      m_ToLength = newValue;
   }

   return true;
}

// handler implementations for ChangeTempoDialog

void ChangeTempoDialog::OnText_PercentChange(wxCommandEvent & WXUNUSED(event))
{
   if (m_bLoopDetect)
      return;

   if (m_pTextCtrl_PercentChange) {
      wxString str = m_pTextCtrl_PercentChange->GetValue();
      double newValue = 0;
      str.ToDouble(&newValue);
      m_PercentChange = newValue;

      m_bLoopDetect = true;
      this->Update_Slider_PercentChange();
      this->Update_Text_ToBPM();
      this->Update_Text_ToLength();
      m_bLoopDetect = false;

      FindWindow(wxID_OK)->Enable(m_PercentChange > -100.0);
   }
}

void ChangeTempoDialog::OnSlider_PercentChange(wxCommandEvent & WXUNUSED(event))
{
   if (m_bLoopDetect)
      return;

   if (m_pSlider_PercentChange) {
      m_PercentChange = (double)(m_pSlider_PercentChange->GetValue());
      // Warp positive values to actually go up faster & further than negatives.
      if (m_PercentChange > 0.0)
         m_PercentChange = pow(m_PercentChange, PERCENTCHANGE_SLIDER_WARP);

      m_bLoopDetect = true;
      this->Update_Text_PercentChange();
      this->Update_Text_ToBPM();
      this->Update_Text_ToLength();
      m_bLoopDetect = false;
   }
}

void ChangeTempoDialog::OnText_FromBPM(wxCommandEvent & WXUNUSED(event))
{
   if (m_bLoopDetect)
      return;

   if (m_pTextCtrl_FromBPM) {
      wxString str = m_pTextCtrl_FromBPM->GetValue();
      double newValue;
      str.ToDouble(&newValue);
      m_FromBPM = newValue;

      m_bLoopDetect = true;

      this->Update_Text_ToBPM();

      m_bLoopDetect = false;
   }
}

void ChangeTempoDialog::OnText_ToBPM(wxCommandEvent & WXUNUSED(event))
{
   if (m_bLoopDetect)
      return;

   if (m_pTextCtrl_ToBPM) {
      wxString str = m_pTextCtrl_ToBPM->GetValue();
      double newValue;
      str.ToDouble(&newValue);
      m_ToBPM = newValue;

      m_bLoopDetect = true;

      // If FromBPM has already been set, then there's a new percent change.
      if (m_FromBPM != 0.0) {
         m_PercentChange = ((m_ToBPM * 100.0) / m_FromBPM) - 100.0;

         this->Update_Text_PercentChange();
         this->Update_Slider_PercentChange();

         this->Update_Text_ToLength();
      }
      
      m_bLoopDetect = false;
   }
}

void ChangeTempoDialog::OnText_ToLength(wxCommandEvent & WXUNUSED(event))
{
   if (m_bLoopDetect)
      return;

   if (m_pTextCtrl_ToLength) {
      wxString str = m_pTextCtrl_ToLength->GetValue();
      double newValue = 0;
      str.ToDouble(&newValue);
      m_ToLength = newValue;

      m_PercentChange = ((m_FromLength * 100.0) / m_ToLength) - 100.0;

      m_bLoopDetect = true;

      this->Update_Text_PercentChange();
      this->Update_Slider_PercentChange();

      this->Update_Text_ToBPM();
      
      m_bLoopDetect = false;
   }
}

void ChangeTempoDialog::OnPreview(wxCommandEvent & WXUNUSED(event))
{
   TransferDataFromWindow();

   // Save & restore parameters around Preview, because we didn't do OK.
   double oldPercentChange = mEffect->m_PercentChange;
   if( m_PercentChange < -99.0)
   {
      m_PercentChange = -99.0;
      this->Update_Text_PercentChange();
   }
   mEffect->m_PercentChange = m_PercentChange;
   mEffect->Preview();
   mEffect->m_PercentChange = oldPercentChange;
}

// helper fns

void ChangeTempoDialog::Update_Text_PercentChange()
{
   if (m_pTextCtrl_PercentChange) {
      wxString str;
      str.Printf(wxT("%.3f"), m_PercentChange);
      m_pTextCtrl_PercentChange->SetValue(str);
      FindWindow(wxID_OK)->Enable(m_PercentChange > -100.0);
   }
}

void ChangeTempoDialog::Update_Slider_PercentChange()
{
   if (m_pSlider_PercentChange) {
      double unwarped = m_PercentChange;
      if (unwarped > 0.0)
         // Un-warp values above zero to actually go up to PERCENTCHANGE_MAX.
         unwarped = pow(m_PercentChange, (1.0 / PERCENTCHANGE_SLIDER_WARP));

      // Add 0.5 to unwarped so trunc -> round.
      m_pSlider_PercentChange->SetValue((int)(unwarped + 0.5));
   }
}

void ChangeTempoDialog::Update_Text_ToBPM() 
// Use m_FromBPM & m_PercentChange to set new m_ToBPM & control.
{
   // Update ToBPM iff FromBPM has been set.
   if (m_FromBPM == 0.0)
      return;

   m_ToBPM = (((m_FromBPM * (100.0 + m_PercentChange)) / 100.0));
   if (m_pTextCtrl_ToBPM) {
      wxString str;
      str.Printf(wxT("%.3f"), m_ToBPM);
      m_pTextCtrl_ToBPM->SetValue(str);
   }
}

void ChangeTempoDialog::Update_Text_ToLength() 
// Use m_FromLength & m_PercentChange to set new m_ToLength & control.
{
   m_ToLength = (m_FromLength * 100.0) / (100.0 + m_PercentChange);
   if (m_pTextCtrl_ToLength) {
      wxString str;
      str.Printf(wxT("%.2f"), m_ToLength);
      m_pTextCtrl_ToLength->SetValue(str);
   }
}

#endif // USE_SOUNDTOUCH
