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

  Audacity: A Digital Audio Editor

  @file PluginStartupRegistration.cpp

  @author Vitaly Sverchinsky

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

#include "PluginStartupRegistration.h"

#include <thread>

#include <wx/log.h>
#include <wx/app.h>
#include <wx/stattext.h>
#include <wx/gauge.h>
#include <wx/button.h>
#include <wx/timer.h>
#include <wx/sizer.h>

#include "PluginManager.h"
#include "PluginDescriptor.h"
#include "wxPanelWrapper.h"

namespace
{
   enum {
      OnPluginScanTimeout = wxID_HIGHEST + 1,
   };

   class PluginScanDialog : public wxDialogWrapper
   {
      wxStaticText* mText{nullptr};
      wxStaticText* mElapsed{nullptr};
      wxGauge* mProgress{nullptr};
      std::chrono::system_clock::time_point mStartTime;
   public:
      
      PluginScanDialog(
         wxWindow* parent,
         wxWindowID winid,
         const TranslatableString& title)
         : wxDialogWrapper(parent, winid, title)
      {
         auto rootSizer = std::make_unique<wxBoxSizer>(wxVERTICAL);
         auto topSizer = std::make_unique<wxBoxSizer>(wxHORIZONTAL);
         topSizer->Add(mText =
            safenew wxStaticText(
               this,
               wxID_ANY,
               wxEmptyString,
               wxDefaultPosition,
               wxDefaultSize,
               wxST_ELLIPSIZE_START | wxST_NO_AUTORESIZE),
            1, wxEXPAND);
         topSizer->AddSpacer(5);
         topSizer->Add(safenew wxButton(this, wxID_IGNORE, _("&Skip")), 0);

         auto timerSizer = std::make_unique<wxBoxSizer>(wxHORIZONTAL);
         timerSizer->Add(
            safenew wxStaticText(
               this,
               wxID_ANY,
               _("Elapsed Time:"),
               wxDefaultPosition,
               wxDefaultSize,
               wxALIGN_RIGHT | wxALIGN_CENTRE_VERTICAL), 1, wxEXPAND);
         timerSizer->AddSpacer(5);
         timerSizer->Add(mElapsed =
            safenew wxStaticText(
               this,
               wxID_ANY,
               wxEmptyString,
               wxDefaultPosition,
               wxDefaultSize,
               wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL), 1, wxEXPAND);
         
         rootSizer->Add(topSizer.release(), 0, wxEXPAND | wxALL, 10);
         rootSizer->Add(mProgress =
            safenew wxGauge(this, wxID_ANY, 1000),
            0, wxEXPAND | wxLEFT | wxRIGHT, 10);
         rootSizer->AddSpacer(10);
         rootSizer->Add(timerSizer.release(), 0, wxEXPAND);
#ifdef __WXMAC__
         rootSizer->Add(CreateButtonSizer(wxCANCEL), 0, wxEXPAND);
#else
         rootSizer->Add(CreateButtonSizer(wxCANCEL), 0, wxEXPAND | wxALL, 10);
#endif    
         SetSizer(rootSizer.release());

         Bind(wxEVT_SHOW, &PluginScanDialog::OnShow, this);
         Bind(wxEVT_IDLE, &PluginScanDialog::OnIdle, this);

         SetInitialSize({500, -1});
      }

      void UpdateProgress(const wxString& text, float progress)
      {
         mText->SetLabel(text);
         mText->SetToolTip(text);
         mProgress->SetValue(mProgress->GetRange() * progress);
      }

      void OnShow(wxShowEvent& evt)
      {
         evt.Skip();
         if(evt.IsShown())
         {
            mStartTime = std::chrono::system_clock::now();
            UpdateElapsedTime();
         }
      }
      
      void OnIdle(wxIdleEvent& evt)
      {
         evt.Skip();
         UpdateElapsedTime();
      }

      void UpdateElapsedTime()
      {
         auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - mStartTime).count();
         mElapsed->SetLabel(wxTimeSpan(0, 0, 0, elapsed).Format("%H:%M:%S"));
      }
   };
}

PluginStartupRegistration::PluginStartupRegistration(const std::map<wxString, std::vector<wxString>>& pluginsToProcess)
{
   for(auto& p : pluginsToProcess)
      mPluginsToProcess.push_back(p);
}

void PluginStartupRegistration::OnInternalError(const wxString& error)
{
   StopWithError(error);
}

void PluginStartupRegistration::OnPluginFound(const PluginDescriptor& desc)
{
   if(!mValidProviderFound)
      mFailedPluginsCache.clear();

   mValidProviderFound = true;
   if(!desc.IsValid())
      mFailedPluginsCache.push_back(desc);
   PluginManager::Get().RegisterPlugin(PluginDescriptor { desc });
}

void PluginStartupRegistration::OnPluginValidationFailed(const wxString& providerId, const wxString& path)
{
   PluginID ID = providerId + wxT("_") + path;
   PluginDescriptor pluginDescriptor;
   pluginDescriptor.SetPluginType(PluginTypeStub);
   pluginDescriptor.SetID(ID);
   pluginDescriptor.SetProviderID(providerId);
   pluginDescriptor.SetPath(path);
   pluginDescriptor.SetEnabled(false);
   pluginDescriptor.SetValid(false);

   //Multiple providers can report same module paths
   //do not register until all associated providers have tried to load the module
   mFailedPluginsCache.push_back(std::move(pluginDescriptor));
}


void PluginStartupRegistration::OnValidationFinished()
{
   ++mCurrentPluginProviderIndex;
   if(mValidProviderFound ||
      mPluginsToProcess[mCurrentPluginIndex].second.size() == mCurrentPluginProviderIndex)
   {
      if(!mFailedPluginsCache.empty())
      {
         //we've tried all providers associated with same module path...
         if(!mValidProviderFound)
         {
            //...but none of them succeeded
            mFailedPluginsPaths.push_back(mFailedPluginsCache[0].GetPath());

            //Same plugin path, but different providers, we need to register all of them
            for(auto& desc : mFailedPluginsCache)
               PluginManager::Get().RegisterPlugin(std::move(desc));
         }
         //plugin type was detected, but plugin instance validation has failed
         else
         {
            for(auto& desc : mFailedPluginsCache)
            {
               if(desc.GetPluginType() != PluginTypeStub)
                  mFailedPluginsPaths.push_back(desc.GetPath());
            }
         }
      }
      ++mCurrentPluginIndex;
      mCurrentPluginProviderIndex = 0;
      mValidProviderFound = false;
      mFailedPluginsCache.clear();
   }
   ProcessNext();
}

const std::vector<wxString>& PluginStartupRegistration::GetFailedPluginsPaths() const noexcept
{
   return mFailedPluginsPaths;
}

void PluginStartupRegistration::Run(std::chrono::seconds timeout)
{
   PluginScanDialog dialog(nullptr, wxID_ANY, XO("Searching for plugins"));
   wxTimer timeoutTimer(&dialog, OnPluginScanTimeout);
   mScanDialog = &dialog;
   mTimeoutTimer = &timeoutTimer;
   mTimeout = timeout;

   dialog.Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) {
      evt.Skip();
      if(evt.GetId() == wxID_IGNORE)
         Skip();
   });
   dialog.Bind(wxEVT_TIMER, [this](wxTimerEvent& evt) {
      if(evt.GetId() == OnPluginScanTimeout)
      {
         if(mValidator && mValidator->InactiveSince() < mRequestStartTime)
            Skip();
         //else
         //   wxMessageBox("Please check for plugin popups!");
      }
      else
         evt.Skip();
   });
   dialog.Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& evt) {
      evt.Skip();
      mValidator.reset();
      PluginManager::Get().Save();
      PluginManager::Get().NotifyPluginsChanged();
   });

   dialog.CenterOnScreen();
   ProcessNext();
   dialog.ShowModal();
}

void PluginStartupRegistration::Stop()
{
   if(auto dialog = mScanDialog.get())
      dialog->Close();
}

void PluginStartupRegistration::Skip()
{
   //Drop current validator, no more callbacks will be received from now
   mValidator->SetDelegate(nullptr);
   //While on Linux and MacOS socket `shutdown()` wakes up `select()` almost
   //immediately, on Windows it sometimes get delayed on unspecified amount
   //of time. As we do not expect any data we can safely move remaining
   //operations to another thread.
   std::thread([validator = std::shared_ptr<AsyncPluginValidator>(std::move(mValidator))]{ }).detach();

   if(!mValidProviderFound)
   {
      // Validator didn't report anything yet or it tried
      // one or more providers that didn't recognize the plugin.
      // In that case we assume that none of the remaining providers
      // can recognize that plugin.
      // Note: create stub `PluginDescriptors` for each associated provider
      for(;mCurrentPluginProviderIndex < mPluginsToProcess[mCurrentPluginIndex].second.size(); ++mCurrentPluginProviderIndex)
         OnPluginValidationFailed(
            mPluginsToProcess[mCurrentPluginIndex].second[mCurrentPluginProviderIndex],
            mPluginsToProcess[mCurrentPluginIndex].first);
      mCurrentPluginProviderIndex = mPluginsToProcess[mCurrentPluginIndex].second.size() - 1;
   }
   //else
   //    Don't assume that `OnValidationFinished()` and `OnPluginFound()`
   //    aren't deferred within run loop

   OnValidationFinished();
}

void PluginStartupRegistration::StopWithError(const wxString& msg)
{
   //TODO: show error dialog?
   wxLogError("Plugin registration error: %s", msg);
   Stop();
}

void PluginStartupRegistration::ProcessNext()
{
   if(mCurrentPluginIndex == mPluginsToProcess.size())
   {
      Stop();
      return;
   }

   try
   {
      if(auto dialog = static_cast<PluginScanDialog*>(mScanDialog.get()))
      {
         const auto progress = static_cast<float>(mCurrentPluginIndex) / static_cast<float>(mPluginsToProcess.size());
         dialog->UpdateProgress(
            mPluginsToProcess[mCurrentPluginIndex].first,
            progress);
      }
      if(!mValidator)
         mValidator = std::make_unique<AsyncPluginValidator>(*this);

      mValidator->Validate(
         mPluginsToProcess[mCurrentPluginIndex].second[mCurrentPluginProviderIndex],
         mPluginsToProcess[mCurrentPluginIndex].first
      );
      mRequestStartTime = std::chrono::system_clock::now();
      if(auto timer = mTimeoutTimer.get())
         timer->StartOnce(std::chrono::duration_cast<std::chrono::milliseconds>(mTimeout).count());
   }
   catch(std::exception& e)
   {
      StopWithError(e.what());
   }
   catch(...)
   {
      StopWithError("unknown error");
   }
}

