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

  Audacity: A Digital Audio Editor

  SpectralDataDialog.cpp

  Edward Hui

*******************************************************************//*!

\class SpectralDataDialog
\brief Provides UI for spectral editing and parameters adjustments

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

#ifdef EXPERIMENTAL_BRUSH_TOOL

#include <type_traits>

#include <wx/app.h>
#include <wx/defs.h>
#include <wx/button.h>
#include <wx/checkbox.h>
#include <wx/dialog.h>
#include <wx/event.h>
#include <wx/frame.h>
#include <wx/imaglist.h>
#include <wx/intl.h>
#include <wx/listctrl.h>
#include <wx/settings.h>
#include <wx/spinctrl.h>
#include <wx/statline.h>
#include <wx/stattext.h>
#include <wx/textctrl.h>
#include <wx/tglbtn.h>

#include "../images/Arrow.xpm"
#include "../images/Empty9x16.xpm"

#include "AllThemeResources.h"
#include "AudioIO.h"
#include "ClientData.h"
#include "Clipboard.h"
#include "CommandContext.h"
#include "CommandManager.h"
#include "UndoManager.h"
#include "Prefs.h"
#include "Project.h"
#include "ProjectHistory.h"
#include "ProjectSettings.h"
#include "ProjectWindows.h"
#include "ShuttleGui.h"
#include "SpectralDataManager.h"
#include "tracks/playabletrack/wavetrack/ui/SpectrumView.h"
#include "Theme.h"
#include "TrackPanel.h"
#include "WaveTrack.h"

#include "commands/CommandManagerWindowClasses.h"
#include "AudacityMessageBox.h"
#include "wxPanelWrapper.h" // to inherit

// If defined, make a checkbox for smart selection of the fundamental
// independently of overtones
#undef SMART_CHECKBOX

enum {
   ID_BRUSH_BUTTON = 10000,
#ifdef SMART_CHECKBOX
   ID_CHECKBOX_SMART,
#endif
   ID_CHECKBOX_OVERTONES,
   ID_SLIDER_BRUSH_SIZE
};

class wxButton;
class wxCheckBox;
class wxListCtrl;
class wxListEvent;
class wxSpinCtrl;
class wxTextCtrl;
class AudacityProject;
class ShuttleGui;
class UndoManager;

class SpectralDataDialog final : public wxDialogWrapper,
      public PrefsListener,
      public ClientData::Base,
      public TopLevelKeystrokeHandlingWindow
{

      public:
         static SpectralDataDialog &Get(AudacityProject &project);
         static SpectralDataDialog *Find(AudacityProject *pProject);

         explicit SpectralDataDialog(AudacityProject &parent);

         void UpdateDisplayForClipboard(ClipboardChangeMessage);
         void UpdateDisplay(UndoRedoMessage);
         void DoUpdateDisplay();
         void UpdateControls( bool active );

         bool Show( bool show = true ) override;

      private:
         void Populate(ShuttleGui & S);

         void OnAudioIO(AudioIOEvent ev);
         void DoUpdate();

         void OnCloseWindow(wxCloseEvent &event);
         void OnApply(wxCommandEvent &event);
         void OnBrushButton(wxCommandEvent &event);
         void OnBrushSizeSlider(wxCommandEvent &event);
         void OnCheckSmartSelection(wxCommandEvent &event);
         void OnCheckOvertones(wxCommandEvent &event);

         // PrefsListener implementation
         void UpdatePrefs() override;

         Observer::Subscription mAudioIOSubscription
            , mUndoSubscription
            , mClipboardSubscription
         ;
         AudacityProject   &mProject;
         wxToggleButton *mBrushButton = nullptr;
         bool              mAudioIOBusy { false };

      public:
         void DoToolChanged();

         DECLARE_EVENT_TABLE()
};

class AUDACITY_DLL_API SpectralDataDialogWorker final
      : public ClientData::Base{
public:
   explicit SpectralDataDialogWorker( AudacityProject &project );
   ~SpectralDataDialogWorker();

   void OnToolChanged(ProjectSettingsEvent);
   void OnIdle(wxIdleEvent &evt);
private:
   Observer::Subscription mSettingsSubscription;
   AudacityProject &mProject;
   unsigned mPrevNViews = 0;
};

BEGIN_EVENT_TABLE(SpectralDataDialog, wxDialogWrapper)
   EVT_TOGGLEBUTTON(ID_BRUSH_BUTTON, SpectralDataDialog::OnBrushButton)
 #ifdef SMART_CHECKBOX
   EVT_CHECKBOX(ID_CHECKBOX_SMART, SpectralDataDialog::OnCheckSmartSelection)
#endif
   EVT_CHECKBOX(ID_CHECKBOX_OVERTONES, SpectralDataDialog::OnCheckOvertones)
   EVT_CLOSE(SpectralDataDialog::OnCloseWindow)
   EVT_SLIDER(ID_SLIDER_BRUSH_SIZE, SpectralDataDialog::OnBrushSizeSlider)
END_EVENT_TABLE()

#define Title XO("Spectral Data Control Panel")

SpectralDataDialog::SpectralDataDialog(AudacityProject &parent)
   : mProject(parent)
   , wxDialogWrapper(FindProjectFrame( &parent ), wxID_ANY, Title,
                         wxDefaultPosition, wxDefaultSize,
                         wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER )

{
   SetName();
   //------------------------- Main section --------------------
   // Construct the GUI.
   ShuttleGui S(this, eIsCreating);
   Populate(S);
   CentreOnParent();

   mAudioIOSubscription = AudioIO::Get()
      ->Subscribe(*this, &SpectralDataDialog::OnAudioIO);

   mClipboardSubscription = Clipboard::Get()
      .Subscribe(*this, &::SpectralDataDialog::UpdateDisplayForClipboard);

   mUndoSubscription = UndoManager::Get(parent)
      .Subscribe(*this, &SpectralDataDialog::UpdateDisplay);

   DoToolChanged();
}

static const AttachedWindows::RegisteredFactory key{
      []( AudacityProject &project ){
         return safenew SpectralDataDialog( project );
      }
};

static wxToggleButton *MakeButton(wxWindow *pParent)
{
   auto button = safenew wxBitmapToggleButton{
      pParent, ID_BRUSH_BUTTON, theTheme.Bitmap(bmpSpectralBrush) };
   // Name isn't shown but may be pronounced by a screen reader
   button->SetName(XO("Brush Tool").Translation());
   return button;
}

void SpectralDataDialog::Populate(ShuttleGui & S)
{
   mBrushButton = MakeButton(this);

   S.StartVerticalLay(true);
   S.SetBorder(10);
   {
      S.AddVariableText(XO("Spectral Brush"));
      S.AddWindow(mBrushButton, wxALIGN_LEFT);

      S.AddVariableText(XO("Brush radius"));
      S.Id(ID_SLIDER_BRUSH_SIZE)
            .Style(wxSL_HORIZONTAL)
            .Name(XO("Custom brush size"))
            .AddSlider( {}, 5, 10, 1);

      S.AddWindow(safenew wxStaticLine{ S.GetParent() });

      S.Id(ID_CHECKBOX_OVERTONES)
         .AddCheckBox(
            XXO("Auto-select overtones (beta)"),
            false);

#ifdef SMART_CHECKBOX
      S.Id(ID_CHECKBOX_SMART)
            .AddCheckBox(XXO("Enable smart selection"), false);
#endif

      S.AddVariableText(
         XO("Select the fundamental frequency\n"
            "and release the mouse"));
   }
   S.EndVerticalLay();
   // ----------------------- End of main section --------------

   Layout();
   Fit();
   SetMinSize(GetSize());
}

void SpectralDataDialog::OnAudioIO(AudioIOEvent ev)
{
   if (ev.type != AudioIOEvent::MONITOR && ev.type != AudioIOEvent::PAUSE)
      mAudioIOBusy = ev.on;
}

void SpectralDataDialog::UpdateDisplayForClipboard(ClipboardChangeMessage)
{
   DoUpdateDisplay();
}

void SpectralDataDialog::UpdateDisplay(UndoRedoMessage message)
{
   switch (message.type) {
   case UndoRedoMessage::Pushed:
   case UndoRedoMessage::Modified:
   case UndoRedoMessage::UndoOrRedo:
   case UndoRedoMessage::Reset:
   case UndoRedoMessage::Purge:
      break;
   default:
      return;
   }
   DoUpdateDisplay();
}

void SpectralDataDialog::DoUpdateDisplay()
{
   if(IsShown())
      DoUpdate();
}

void SpectralDataDialog::UpdateControls( bool active )
{
   if (mBrushButton)
      mBrushButton->SetValue(active);
}

static bool IsBrushToolActive(AudacityProject &project)
{
   return ProjectSettings::Get(project).GetTool() == ToolCodes::brushTool;
}

bool SpectralDataDialog::Show( bool show )
{
   if ( show && !IsShown())
      DoUpdate();
   if ( IsShown() && !show && IsBrushToolActive(mProject) )
      ProjectSettings::Get(mProject).SetTool(ToolCodes::selectTool);
   auto result = wxDialogWrapper::Show( show );
   CommandManager::Get( mProject ).UpdateCheckmarks( mProject );
   return result;
}

void SpectralDataDialog::DoUpdate()
{
}

void SpectralDataDialog::OnCloseWindow(wxCloseEvent & WXUNUSED(event))
{
   this->Show(false);
}

// PrefsListener implementation
void SpectralDataDialog::UpdatePrefs()
{
   bool shown = IsShown();
   if (shown) {
      Show(false);
   }

   SetSizer(nullptr);
   DestroyChildren();

   SetTitle(Title);
   ShuttleGui S(this, eIsCreating);
   Populate(S);
   DoToolChanged();

   if (shown) {
      Show(true);
   }
}

void SpectralDataDialog::OnBrushButton(wxCommandEvent &event) {
   if (mBrushButton->GetValue())
      ProjectSettings::Get(mProject).SetTool(ToolCodes::brushTool);
   else
      // Don't stay up
      mBrushButton->SetValue(true);
}

static const AudacityProject::AttachedObjects::RegisteredFactory sSpectralWorkerKey{
   []( AudacityProject &project ){
      return std::make_shared< SpectralDataDialogWorker >( project );
   }
};

AttachedWindows::RegisteredFactory sSpectralDataDialogKey{
      []( AudacityProject &parent ) -> wxWeakRef< wxWindow > {
         return safenew SpectralDataDialog( parent );
      }
};

SpectralDataDialog &SpectralDataDialog::Get( AudacityProject &project )
{
   // Ensure existence of the dialog
   return static_cast<SpectralDataDialog&>(
      GetAttachedWindows(project).Get( sSpectralDataDialogKey ) );
}

SpectralDataDialog *SpectralDataDialog::Find( AudacityProject *pProject )
{
   // Return a pointer to the dialog only if it exists
   return pProject
      ? static_cast<SpectralDataDialog*>(
         GetAttachedWindows(*pProject).Find( sSpectralDataDialogKey ) )
      : nullptr;
}

// Current workflow for spectral editing dialog:
// 1. ProjectSettings change event listened by Worker::OnToolChanged()
// 2. In Worker::OnToolChanged(), get Dialog from AttachedWindows and invoke Show()
// 3. Dialog::OnApply() listens to the apply button in Dialog, which calculates and applies the effect
SpectralDataDialogWorker::SpectralDataDialogWorker(AudacityProject &project)
   : mProject{ project }
{
   mSettingsSubscription = ProjectSettings::Get(project)
      .Subscribe(*this, &SpectralDataDialogWorker::OnToolChanged);
   wxTheApp->Bind(wxEVT_IDLE, &SpectralDataDialogWorker::OnIdle, this);
}

SpectralDataDialogWorker::~SpectralDataDialogWorker()
{
   wxTheApp->Unbind(wxEVT_IDLE, &SpectralDataDialogWorker::OnIdle, this);
}

void SpectralDataDialogWorker::OnToolChanged(ProjectSettingsEvent evt)
{
   if (evt.type == ProjectSettingsEvent::ChangedTool) {
      // Find not Get to avoid creating the dialog if not yet done
      if (auto pDialog = SpectralDataDialog::Find(&mProject);
          pDialog && pDialog->IsShown())
         pDialog->DoToolChanged();
      else {
         // Dialog is hidden, or not yet constructed
         using Type = std::underlying_type_t<decltype(ToolCodes::brushTool)>;
         constexpr auto value = static_cast<Type>(ToolCodes::brushTool);

         auto &projectSettings = ProjectSettings::Get( mProject );
         if (projectSettings.GetTool() == value) {
            auto oldValue = evt.oldValue;
            if (oldValue + 1 == value)
               // continue tool rotation
               wxTheApp->CallAfter([&]{ projectSettings.SetTool(
                  (value + 1) % ToolCodes::numTools); });
            else if ((oldValue + ToolCodes::numTools - 1 ) % ToolCodes::numTools
               == value)
               // continue backwards tool rotation
               wxTheApp->CallAfter([&]{ projectSettings.SetTool(
                  (value + ToolCodes::numTools - 1 ) % ToolCodes::numTools); });
            else
               // restore old tool value
               wxTheApp->CallAfter([&]{ projectSettings.SetTool(oldValue); });
         }
      }
   }
}

static bool HasVisibleSpectralView(WaveTrack *wt)
{
   auto &view = ChannelView::Get(*wt->GetChannel(0));
   if (auto waveChannelViewPtr = dynamic_cast<WaveChannelView*>(&view)) {
      const auto range = waveChannelViewPtr->GetSubViews();
      return std::any_of( range.begin(), range.end(),
         [](const auto &pair){
            return dynamic_cast<SpectrumView*>(pair.second.get()); } );
   }
   return false;
}

static unsigned CountVisibleSpectralViews( AudacityProject &project )
{
   const auto range = TrackList::Get(project).Any< WaveTrack >();
   return std::count_if( range.begin(), range.end(), HasVisibleSpectralView );
}

void SpectralDataDialogWorker::OnIdle(wxIdleEvent &evt)
{
   evt.Skip();
   auto nViews = CountVisibleSpectralViews(mProject);
   if (nViews > mPrevNViews) {
      // Some track transitioned its view to spectral or multi.
      // Show the dialog.
      auto &dialog = SpectralDataDialog::Get(mProject);
      dialog.Show();
      dialog.Raise();
   }
   else if (nViews == 0 && mPrevNViews > 0) {
      // The last spectrum view was closed.
      // Hide the dialog.
      if (auto pDialog = SpectralDataDialog::Find(&mProject))
         pDialog->Hide();
   }
   mPrevNViews = nViews;
}

void SpectralDataDialog::DoToolChanged()
{
   UpdateControls( IsBrushToolActive(mProject) );
}

void SpectralDataDialog::OnBrushSizeSlider(wxCommandEvent &event) {
   auto &projectSettings = ProjectSettings::Get( mProject );
   projectSettings.SetBrushRadius(event.GetInt());
}

void SpectralDataDialog::OnCheckSmartSelection(wxCommandEvent &event){
   int isSelected = event.GetInt();
   wxASSERT(isSelected == 0 || isSelected == 1);
   auto &projectSettings = ProjectSettings::Get( mProject );
   projectSettings.SetSmartSelection(isSelected);
}

void SpectralDataDialog::OnCheckOvertones(wxCommandEvent &event){
   int isSelected = event.GetInt();
   wxASSERT(isSelected == 0 || isSelected == 1);
   auto &projectSettings = ProjectSettings::Get( mProject );
   projectSettings.SetOvertones(isSelected);
#ifndef SMART_CHECKBOX
   // One checkbox controls both things
   OnCheckSmartSelection(event);
#endif
}

namespace {
void OnSpectralEditingPanel(const CommandContext &context)
{
   auto &project = context.project;
   auto &dialog = SpectralDataDialog::Get(project);
   dialog.Show( !dialog.IsShown() );
}

using namespace MenuRegistry;
MenuRegistry::AttachedItem sAttachment{
   wxT("View/Other/Toolbars/Toolbars/Other"),
   Command( wxT("ShowSpectralSelectionPanel"),
      XXO("Spectra&l Selection Panel"),
      OnSpectralEditingPanel,
      AlwaysEnabledFlag,
      Options{}
         .CheckTest( [](AudacityProject &project) {
            // Find not Get to avoid creating the dialog if not yet done
            auto pDialog = SpectralDataDialog::Find(&project);
            return pDialog && pDialog->IsShown();
         } ) )
};

}

#endif
