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

  Audacity: A Digital Audio Editor

  ExportCL.cpp

  Joshua Haberman

  This code allows Audacity to export data by piping it to an external
  program.

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

#include "../Audacity.h"

#include "../ProjectSettings.h"

#include <wx/app.h>
#include <wx/button.h>
#include <wx/combobox.h>
#include <wx/filedlg.h>
#include <wx/log.h>
#include <wx/process.h>
#include <wx/sizer.h>
#include <wx/textctrl.h>
#if defined(__WXMSW__)
#include <wx/msw/registry.h> // for wxRegKey
#endif

#include "../FileNames.h"
#include "Export.h"

#include "../Mix.h"
#include "../Prefs.h"
#include "../ShuttleGui.h"
#include "../Track.h"
#include "../float_cast.h"
#include "../widgets/FileHistory.h"
#include "../widgets/AudacityMessageBox.h"
#include "../widgets/ProgressDialog.h"
#include "../wxFileNameWrapper.h"


//----------------------------------------------------------------------------
// ExportCLOptions
//----------------------------------------------------------------------------

class ExportCLOptions final : public wxPanelWrapper
{
public:
   ExportCLOptions(wxWindow *parent, int format);
   virtual ~ExportCLOptions();

   void PopulateOrExchange(ShuttleGui & S);
   bool TransferDataToWindow() override;
   bool TransferDataFromWindow() override;

   void OnBrowse(wxCommandEvent & event);

private:
   wxComboBox *mCmd;
   FileHistory mHistory;

   DECLARE_EVENT_TABLE()
};

#define ID_BROWSE 5000

BEGIN_EVENT_TABLE(ExportCLOptions, wxPanelWrapper)
   EVT_BUTTON(ID_BROWSE, ExportCLOptions::OnBrowse)
END_EVENT_TABLE()

///
///
ExportCLOptions::ExportCLOptions(wxWindow *parent, int WXUNUSED(format))
:  wxPanelWrapper(parent, wxID_ANY)
{
   mHistory.Load(*gPrefs, wxT("/FileFormats/ExternalProgramHistory"));

   if (mHistory.GetCount() == 0) {
      mHistory.AddFileToHistory(wxT("ffmpeg -i - \"%f\""), false);
      mHistory.AddFileToHistory(wxT("lame - \"%f\""), false);
   }

   mHistory.AddFileToHistory(gPrefs->Read(wxT("/FileFormats/ExternalProgramExportCommand"),
                                          mHistory.GetHistoryFile(0)),
                             false);

   ShuttleGui S(this, eIsCreatingFromPrefs);
   PopulateOrExchange(S);

   TransferDataToWindow();

   parent->Layout();
}

ExportCLOptions::~ExportCLOptions()
{
   TransferDataFromWindow();
}

///
///
void ExportCLOptions::PopulateOrExchange(ShuttleGui & S)
{
   wxArrayStringEx cmds;
   wxString cmd;

   for (size_t i = 0; i < mHistory.GetCount(); i++) {
      cmd = mHistory.GetHistoryFile(i);
      cmds.push_back(mHistory.GetHistoryFile(i));
   }
   cmd = cmds[0];

   S.StartVerticalLay();
   {
      S.StartHorizontalLay(wxEXPAND);
      {
         S.SetSizerProportion(1);
         S.StartMultiColumn(3, wxEXPAND);
         {
            S.SetStretchyCol(1);
            mCmd = S.AddCombo(XO("Command:"),
                              cmd,
                              cmds);
            S.Id(ID_BROWSE).AddButton(XO("Browse..."),
                                      wxALIGN_CENTER_VERTICAL);
            S.AddFixedText( {} );
            S.TieCheckBox(XO("Show output"),
                          {wxT("/FileFormats/ExternalProgramShowOutput"),
                           false});
         }
         S.EndMultiColumn();
      }
      S.EndHorizontalLay();

      S.AddTitle(XO(
/* i18n-hint: Some programmer-oriented terminology here:
   "Data" refers to the sound to be exported, "piped" means sent,
   and "standard in" means the default input stream that the external program,
   named by %f, will read.  And yes, it's %f, not %s -- this isn't actually used
   in the program as a format string.  Keep %f unchanged. */
"Data will be piped to standard in. \"%f\" uses the file name in the export window."));
   }
   S.EndVerticalLay();
}

///
///
bool ExportCLOptions::TransferDataToWindow()
{
   return true;
}

///
///
bool ExportCLOptions::TransferDataFromWindow()
{
   ShuttleGui S(this, eIsSavingToPrefs);
   PopulateOrExchange(S);

   wxString cmd = mCmd->GetValue();

   mHistory.AddFileToHistory(cmd, false);
   mHistory.Save(*gPrefs, wxT("/FileFormats/ExternalProgramHistory"));

   gPrefs->Write(wxT("/FileFormats/ExternalProgramExportCommand"), cmd);
   gPrefs->Flush();

   return true;
}

///
///
void ExportCLOptions::OnBrowse(wxCommandEvent& WXUNUSED(event))
{
   wxString path;
   FileExtension ext;
   FileNames::FileType type = FileNames::AllFiles;

#if defined(__WXMSW__)
   ext = wxT("exe");
   /* i18n-hint files that can be run as programs */
   type = { XO("Executables"), { ext } };
#endif

   path = FileNames::SelectFile(FileNames::Operation::Open,
      XO("Find path to command"),
      wxEmptyString,
      wxEmptyString,
      ext,
      { type },
      wxFD_OPEN | wxRESIZE_BORDER,
      this);
   if (path.empty()) {
      return;
   }

   if (path.Find(wxT(' ')) == wxNOT_FOUND) {
      mCmd->SetValue(path);
   }
   else {
      mCmd->SetValue(wxT('"') + path + wxT('"'));
   }

   mCmd->SetInsertionPointEnd();

   return;
}

//----------------------------------------------------------------------------
// ExportCLProcess
//----------------------------------------------------------------------------

static void Drain(wxInputStream *s, wxString *o)
{
   while (s->CanRead()) {
      char buffer[4096];

      s->Read(buffer, WXSIZEOF(buffer) - 1);
      buffer[s->LastRead()] = wxT('\0');
      *o += LAT1CTOWX(buffer);
   }
}

class ExportCLProcess final : public wxProcess
{
public:
   ExportCLProcess(wxString *output)
   {
#if defined(__WXMAC__)
      // Don't want to crash on broken pipe
      signal(SIGPIPE, SIG_IGN);
#endif

      mOutput = output;
      mActive = true;
      mStatus = -555;
      Redirect();
   }

   bool IsActive()
   {
      return mActive;
   }

   void OnTerminate(int WXUNUSED( pid ), int status)
   {
      Drain(GetInputStream(), mOutput);
      Drain(GetErrorStream(), mOutput);

      mStatus = status;
      mActive = false;
   }

   int GetStatus()
   {
      return mStatus;
   }

private:
   wxString *mOutput;
   bool mActive;
   int mStatus;
};

//----------------------------------------------------------------------------
// ExportCL
//----------------------------------------------------------------------------

/* this structure combines the RIFF header, the format chunk, and the data
 * chunk header */
struct wav_header {
   /* RIFF header */
   char riffID[4];            /* "RIFF" */
   wxUint32 lenAfterRiff;     /* basically the file len - 8, or samples len + 36 */
   char riffType[4];          /* "WAVE" */

   /* format chunk */
   char fmtID[4];             /* "fmt " */
   wxUint32 formatChunkLen;   /* (format chunk len - first two fields) 16 in our case */
   wxUint16 formatTag;        /* 1 for PCM */
   wxUint16 channels;
   wxUint32 sampleRate;
   wxUint32 avgBytesPerSec;   /* sampleRate * blockAlign */
   wxUint16 blockAlign;       /* bitsPerSample * channels (assume bps % 8 = 0) */
   wxUint16 bitsPerSample;

   /* data chunk header */
   char dataID[4];            /* "data" */
   wxUint32 dataLen;          /* length of all samples in bytes */
};

class ExportCL final : public ExportPlugin
{
public:

   ExportCL();

   // Required
   void OptionsCreate(ShuttleGui &S, int format) override;

   ProgressResult Export(AudacityProject *project,
               std::unique_ptr<ProgressDialog> &pDialog,
               unsigned channels,
               const wxFileNameWrapper &fName,
               bool selectedOnly,
               double t0,
               double t1,
               MixerSpec *mixerSpec = NULL,
               const Tags *metadata = NULL,
               int subformat = 0) override;
};

ExportCL::ExportCL()
:  ExportPlugin()
{
   AddFormat();
   SetFormat(wxT("CL"),0);
   AddExtension(wxT(""),0);
   SetMaxChannels(255,0);
   SetCanMetaData(false,0);
   SetDescription(XO("(external program)"),0);
}

ProgressResult ExportCL::Export(AudacityProject *project,
                      std::unique_ptr<ProgressDialog> &pDialog,
                      unsigned channels,
                      const wxFileNameWrapper &fName,
                      bool selectionOnly,
                      double t0,
                      double t1,
                      MixerSpec *mixerSpec,
                      const Tags *WXUNUSED(metadata),
                      int WXUNUSED(subformat))
{
   wxString output;
   wxString cmd;
   bool show;
   long rc;

   const auto path = fName.GetFullPath();

   // Retrieve settings
   gPrefs->Read(wxT("/FileFormats/ExternalProgramShowOutput"), &show, false);
   cmd = gPrefs->Read(wxT("/FileFormats/ExternalProgramExportCommand"), wxT("lame - \"%f.mp3\""));
   // Bug 2178 - users who don't know what they are doing will 
   // now get a file extension of .wav appended to their ffmpeg filename
   // and therefore ffmpeg will be able to choose a file type.
   if( cmd == wxT("ffmpeg -i - \"%f\"") && !fName.HasExt())
      cmd.Replace( "%f", "%f.wav" );
   cmd.Replace(wxT("%f"), path);

#if defined(__WXMSW__)
   // Give Windows a chance at finding lame command in the default location.
   wxString paths[] = {wxT("HKEY_LOCAL_MACHINE\\Software\\Lame for Audacity"),
                       wxT("HKEY_LOCAL_MACHINE\\Software\\FFmpeg for Audacity")};
   wxString opath;
   wxString npath;
   wxRegKey reg;

   wxGetEnv(wxT("PATH"), &opath);
   npath = opath;

   for (int i = 0; i < WXSIZEOF(paths); i++) {
      reg.SetName(paths[i]);

      if (reg.Exists()) {
         wxString ipath;
         reg.QueryValue(wxT("InstallPath"), ipath);
         if (!ipath.empty()) {
            npath += wxPATH_SEP + ipath;
         }
      }
   }

   wxSetEnv(wxT("PATH"),npath);
#endif

   // Kick off the command
   ExportCLProcess process(&output);

   {
#if defined(__WXMSW__)
      auto cleanup = finally( [&] {
         if (!opath.empty()) {
            wxSetEnv(wxT("PATH"),opath);
         }
      } );
#endif

      rc = wxExecute(cmd, wxEXEC_ASYNC, &process);
   }

   if (!rc) {
      AudacityMessageBox( XO("Cannot export audio to %s").Format( path ) );
      process.Detach();
      process.CloseOutput();

      return ProgressResult::Cancelled;
   }

   // Turn off logging to prevent broken pipe messages
   wxLogNull nolog;

   // establish parameters
   int rate = lrint( ProjectSettings::Get( *project ).GetRate());
   const size_t maxBlockLen = 44100 * 5;
   unsigned long totalSamples = lrint((t1 - t0) * rate);
   unsigned long sampleBytes = totalSamples * channels * SAMPLE_SIZE(int16Sample);

   // fill up the wav header
   wav_header header;
   header.riffID[0]        = 'R';
   header.riffID[1]        = 'I';
   header.riffID[2]        = 'F';
   header.riffID[3]        = 'F';
   header.lenAfterRiff     = wxUINT32_SWAP_ON_BE(sampleBytes + 36);
   header.riffType[0]      = 'W';
   header.riffType[1]      = 'A';
   header.riffType[2]      = 'V';
   header.riffType[3]      = 'E';

   header.fmtID[0]         = 'f';
   header.fmtID[1]         = 'm';
   header.fmtID[2]         = 't';
   header.fmtID[3]         = ' ';
   header.formatChunkLen   = wxUINT32_SWAP_ON_BE(16);
   header.formatTag        = wxUINT16_SWAP_ON_BE(1);
   header.channels         = wxUINT16_SWAP_ON_BE(channels);
   header.sampleRate       = wxUINT32_SWAP_ON_BE(rate);
   header.bitsPerSample    = wxUINT16_SWAP_ON_BE(SAMPLE_SIZE(int16Sample) * 8);
   header.blockAlign       = wxUINT16_SWAP_ON_BE(header.bitsPerSample * header.channels / 8);
   header.avgBytesPerSec   = wxUINT32_SWAP_ON_BE(header.sampleRate * header.blockAlign);
   header.dataID[0]        = 'd';
   header.dataID[1]        = 'a';
   header.dataID[2]        = 't';
   header.dataID[3]        = 'a';
   header.dataLen          = wxUINT32_SWAP_ON_BE(sampleBytes);

   // write the header
   wxOutputStream *os = process.GetOutputStream();
   os->Write(&header, sizeof(wav_header));

   // Mix 'em up
   const auto &tracks = TrackList::Get( *project );
   auto mixer = CreateMixer(
                            tracks,
                            selectionOnly,
                            t0,
                            t1,
                            channels,
                            maxBlockLen,
                            true,
                            rate,
                            int16Sample,
                            true,
                            mixerSpec);

   size_t numBytes = 0;
   samplePtr mixed = NULL;
   auto updateResult = ProgressResult::Success;

   {
      auto closeIt = finally ( [&] {
         // Should make the process die, before propagating any exception
         process.CloseOutput();
      } );

      // Prepare the progress display
      InitProgress( pDialog, XO("Export"),
         selectionOnly
            ? XO("Exporting the selected audio using command-line encoder")
            : XO("Exporting the audio using command-line encoder") );
      auto &progress = *pDialog;

      // Start piping the mixed data to the command
      while (updateResult == ProgressResult::Success && process.IsActive() && os->IsOk()) {
         // Capture any stdout and stderr from the command
         Drain(process.GetInputStream(), &output);
         Drain(process.GetErrorStream(), &output);

         // Need to mix another block
         if (numBytes == 0) {
            auto numSamples = mixer->Process(maxBlockLen);
            if (numSamples == 0) {
               break;
            }

            mixed = mixer->GetBuffer();
            numBytes = numSamples * channels;

            // Byte-swapping is neccesary on big-endian machines, since
            // WAV files are little-endian
#if wxBYTE_ORDER == wxBIG_ENDIAN
            wxUint16 *buffer = (wxUint16 *) mixed;
            for (int i = 0; i < numBytes; i++) {
               buffer[i] = wxUINT16_SWAP_ON_BE(buffer[i]);
            }
#endif
            numBytes *= SAMPLE_SIZE(int16Sample);
         }

         // Don't write too much at once...pipes may not be able to handle it
         size_t bytes = wxMin(numBytes, 4096);
         numBytes -= bytes;

         while (bytes > 0) {
            os->Write(mixed, bytes);
            if (!os->IsOk()) {
               updateResult = ProgressResult::Cancelled;
               break;
            }
            bytes -= os->LastWrite();
            mixed += os->LastWrite();
         }

         // Update the progress display
         updateResult = progress.Update(mixer->MixGetCurrentTime() - t0, t1 - t0);
      }
      // Done with the progress display
   }

   // Wait for process to terminate
   while (process.IsActive()) {
      wxMilliSleep(10);
      wxTheApp->Yield();
   }

   // Display output on error or if the user wants to see it
   if (process.GetStatus() != 0 || show) {
      // TODO use ShowInfoDialog() instead.
      wxDialogWrapper dlg(nullptr,
                   wxID_ANY,
                   XO("Command Output"),
                   wxDefaultPosition,
                   wxSize(600, 400),
                   wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER);
      dlg.SetName();

      ShuttleGui S(&dlg, eIsCreating);
      S
         .Style( wxTE_MULTILINE | wxTE_READONLY )
         .AddTextWindow(cmd + wxT("\n\n") + output);
      S.StartHorizontalLay(wxALIGN_CENTER, false);
      {
         S.Id(wxID_OK).AddButton(XO("&OK"), wxALIGN_CENTER, true);
      }
      dlg.GetSizer()->AddSpacer(5);
      dlg.Layout();
      dlg.SetMinSize(dlg.GetSize());
      dlg.Center();

      dlg.ShowModal();

      if (process.GetStatus() != 0)
         updateResult = ProgressResult::Failed;
   }

   return updateResult;
}

void ExportCL::OptionsCreate(ShuttleGui &S, int format)
{
   S.AddWindow( safenew ExportCLOptions{ S.GetParent(), format } );
}

static Exporter::RegisteredExportPlugin sRegisteredPlugin{ "CommandLine",
   []{ return std::make_unique< ExportCL >(); }
};
