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

  Audacity: A Digital Audio Editor

  ImportRaw.cpp

  Dominic Mazzoni

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

\file ImportRaw.cpp
\brief Functions for guessing audio type and attempting to read from
unknown sample audio data.  Implements ImportRawDialog.

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

\class ImportRawDialog
\brief ImportRawDialog prompts you with options such as endianness
and sample size to help you importing data of an unknown format.

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



#include "ImportRaw.h"

#include "ImportPlugin.h"
#include "ImportUtils.h"

#include "AudioIOBase.h"
#include "FileFormats.h"
#include "Prefs.h"
#include "ProjectRate.h"
#include "SelectFile.h"
#include "ShuttleGui.h"
#include "UserException.h"
#include "WaveTrack.h"
#include "ProgressDialog.h"

#include <cmath>
#include <stdint.h>
#include <vector>

#include <wx/crt.h>
#include <wx/defs.h>
#include <wx/button.h>
#include <wx/choice.h>
#include <wx/combobox.h>
#include <wx/panel.h>
#include <wx/stattext.h>
#include <wx/textctrl.h>

// #include "RawAudioGuess.h"
#include "FormatClassifier.h"

#include "sndfile.h"

class ImportRawDialog final : public wxDialogWrapper {

  public:
   ImportRawDialog(wxWindow * parent, const wxString & fileName);
   ~ImportRawDialog();

   void OnOK(wxCommandEvent & event);
   void OnCancel(wxCommandEvent & event);
   void OnPlay(wxCommandEvent & event);
   void OnDetect(wxCommandEvent & event);
   void OnChoice(wxCommandEvent & event);

   // in and out
   // Make static to preserve value for next raw import
   static int mEncoding;
   static unsigned mChannels;
   static long long mOffset;
   static double mRate;
   static double mPercent;

 private:

   wxButton   *mOK;
   wxChoice   *mEncodingChoice;
   wxChoice   *mEndianChoice;
   wxChoice   *mChannelChoice;
   wxTextCtrl *mOffsetText;
   wxTextCtrl *mPercentText;
   wxComboBox *mRateText;

   std::vector<int> mEncodingSubtype;

   wxString mFileName;

   DECLARE_EVENT_TABLE()
};

// Initial value for Import Raw dialog
int ImportRawDialog::mEncoding = SF_FORMAT_RAW | SF_ENDIAN_CPU | SF_FORMAT_PCM_16;
unsigned ImportRawDialog::mChannels = 1;
long long ImportRawDialog::mOffset = 0;
double ImportRawDialog::mRate = 0;  // -> project rate
double ImportRawDialog::mPercent = 100.;


// This function leaves outTracks empty as an indication of error,
// but may also throw FileException to make use of the application's
// user visible error reporting.
void ImportRaw(const AudacityProject &project, wxWindow *parent, const wxString &fileName,
              WaveTrackFactory *trackFactory, TrackHolders &outTracks)
{
   outTracks.clear();

   TrackListHolder trackList;
   auto updateResult = ProgressResult::Success;

   {
      // On first run, set default sample rate from project rate
      if (ImportRawDialog::mRate < 100.)
         ImportRawDialog::mRate = ProjectRate::Get(project).GetRate();

      ImportRawDialog dlog(parent, fileName);
      dlog.ShowModal();
      if (!dlog.GetReturnCode())
         return;

      const int encoding = dlog.mEncoding;
      unsigned numChannels = dlog.mChannels;
      double rate = dlog.mRate;
      sf_count_t offset = (sf_count_t)dlog.mOffset;
      double percent = dlog.mPercent;

      SF_INFO sndInfo = { 0 };
      sndInfo.samplerate = (int)rate;
      sndInfo.channels = (int)numChannels;
      sndInfo.format = encoding | SF_FORMAT_RAW;

      wxFile f;   // will be closed when it goes out of scope
      SFFile sndFile;

      if (f.Open(fileName)) {
         // Even though there is an sf_open() that takes a filename, use the one that
         // takes a file descriptor since wxWidgets can open a file with a Unicode name and
         // libsndfile can't (under Windows).
         sndFile.reset(SFCall<SNDFILE*>(sf_open_fd, f.fd(), SFM_READ, &sndInfo, FALSE));
      }

      if (!sndFile){
         char str[1000];
         sf_error_str((SNDFILE *)NULL, str, 1000);
         wxPrintf("%s\n", str);

         throw FileException{ FileException::Cause::Open, fileName };
      }


      {
         int result = sf_command(sndFile.get(), SFC_SET_RAW_START_OFFSET, &offset, sizeof(offset));
         if (result != 0) {
            char str[1000];
            sf_error_str(sndFile.get(), str, 1000);
            wxPrintf("%s\n", str);

            throw FileException{ FileException::Cause::Read, fileName };
         }
      }
      SFCall<sf_count_t>(sf_seek, sndFile.get(), 0, SEEK_SET);

      auto totalFrames =
         // fraction of a sf_count_t value
         (sampleCount)(sndInfo.frames * percent / 100.0);

      //
      // Sample format:
      //
      // In general, go with the user's preferences.  However, if
      // the file is higher-quality, go with a format which preserves
      // the quality of the original file.
      //

      const auto format = ImportUtils::ChooseFormat(
         sf_subtype_to_effective_format(encoding));

      trackList = trackFactory->CreateMany(numChannels, format, rate);

      const auto maxBlockSize = (*trackList->Any<WaveTrack>().begin())->GetMaxBlockSize();

      SampleBuffer srcbuffer(maxBlockSize * numChannels, format);
      SampleBuffer buffer(maxBlockSize, format);

      decltype(totalFrames) framescompleted = 0;
      if (totalFrames < 0) {
         wxASSERT(false);
         totalFrames = 0;
      }

      auto msg = XO("Importing %s").Format( wxFileName::FileName(fileName).GetFullName() );

      /* i18n-hint: 'Raw' means 'unprocessed' here and should usually be translated.*/
      ProgressDialog progress(XO("Import Raw"), msg);

      size_t block;
      do {
         block =
            limitSampleBufferSize( maxBlockSize, totalFrames - framescompleted );

         sf_count_t sf_result;
         if (format == int16Sample)
            sf_result = SFCall<sf_count_t>(sf_readf_short, sndFile.get(), (short *)srcbuffer.ptr(), block);
         else
            sf_result = SFCall<sf_count_t>(sf_readf_float, sndFile.get(), (float *)srcbuffer.ptr(), block);

         if (sf_result >= 0) {
            block = sf_result;
         }
         else {
            // This is not supposed to happen, sndfile.h says result is always
            // a count, not an invalid value for error
            throw FileException{ FileException::Cause::Read, fileName };
         }

         if (block) {
            size_t c = 0;
            ImportUtils::ForEachChannel(*trackList, [&](auto& channel)
            {
               if (format == int16Sample) {
                  for (size_t j = 0; j < block; ++j)
                     ((short *)buffer.ptr())[j] =
                     ((short *)srcbuffer.ptr())[numChannels * j + c];
               }
               else {
                  for (size_t j = 0; j < block; ++j)
                     ((float *)buffer.ptr())[j] =
                     ((float *)srcbuffer.ptr())[numChannels * j + c];
               }

               channel.AppendBuffer(buffer.ptr(),
                  ((format == int16Sample) ? int16Sample : floatSample), block,
                  1, sf_subtype_to_effective_format(encoding));
               ++c;
            });
            framescompleted += block;
         }

         updateResult = progress.Update(
            framescompleted.as_long_long(),
            totalFrames.as_long_long()
         );
         if (updateResult != ProgressResult::Success)
            break;
         
      } while (block > 0 && framescompleted < totalFrames);
   }

   if (updateResult == ProgressResult::Failed || updateResult == ProgressResult::Cancelled)
      throw UserException{};

   ImportUtils::FinalizeImport(outTracks, move(*trackList));
}


// Get endian choice from SF_FORMAT
static int getEndianChoice(int sfFormat) {
   switch (sfFormat & SF_FORMAT_ENDMASK)
   {
      default:
      case SF_ENDIAN_FILE:
         return 0;
      case SF_ENDIAN_LITTLE:
         return 1;
      case SF_ENDIAN_BIG:
         return 2;
      case SF_ENDIAN_CPU:
         return 3;
   }
}


//
// ImportRawDialog
//

enum {
   ChoiceID = 9000,
   PlayID,
   DetectID,
};

BEGIN_EVENT_TABLE(ImportRawDialog, wxDialogWrapper)
   EVT_BUTTON(wxID_OK, ImportRawDialog::OnOK)
   EVT_BUTTON(wxID_CANCEL, ImportRawDialog::OnCancel)
   EVT_BUTTON(PlayID, ImportRawDialog::OnPlay)
   EVT_BUTTON(DetectID, ImportRawDialog::OnDetect)
   EVT_CHOICE(ChoiceID, ImportRawDialog::OnChoice)
END_EVENT_TABLE()

ImportRawDialog::ImportRawDialog(wxWindow * parent, const wxString & fileName)
:  wxDialogWrapper(parent, wxID_ANY, XO("Import Raw Data"),
            wxDefaultPosition, wxDefaultSize,
            wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER),
   mFileName(fileName)
{
   wxASSERT(0 < mChannels && mChannels <= 16);

   SetName();

   // Append filename at window title
   wxFileName wfn{ fileName };
   wxString windowTitle = XO("%s: %s").Format(GetTitle(), wfn.GetFullName()).Translation();
   wxDialog::SetTitle(windowTitle);

   ShuttleGui S(this, eIsCreating);
   TranslatableStrings encodings;

   int num = sf_num_encodings();

   int selection = 0;
   for (int i = 0; i < num; i++) {
      SF_INFO info = { 0 };

      int subtype = sf_encoding_index_to_subtype(i);
      info.format = SF_FORMAT_RAW + SF_ENDIAN_LITTLE + subtype;
      info.channels = 1;
      info.samplerate = 44100;

      if (sf_format_check(&info)) {
         mEncodingSubtype.push_back(subtype);
         encodings.push_back( Verbatim( sf_encoding_index_name(i) ) );

         if ((mEncoding & SF_FORMAT_SUBMASK) == subtype)
            selection = mEncodingSubtype.size() - 1;
      }
   }

   TranslatableStrings endians{
      /* i18n-hint: Refers to byte-order.  Don't translate "endianness" if you don't
          know the correct technical word. */
      XO("No endianness") ,
      /* i18n-hint: Refers to byte-order.  Don't translate this if you don't
       know the correct technical word. */
      XO("Little-endian") ,
      /* i18n-hint: Refers to byte-order.  Don't translate this if you don't
         know the correct technical word. */
      XO("Big-endian") ,
      /* i18n-hint: Refers to byte-order.  Don't translate "endianness" if you don't
         know the correct technical word. */
      XO("Default endianness") ,
   };

   int endian = getEndianChoice(mEncoding);

   TranslatableStrings chans{
      XO("1 Channel (Mono)") ,
      XO("2 Channels (Stereo)") ,
   };
   for (int i = 2; i < 16; i++) {
      chans.push_back( XO("%d Channels").Format( i + 1 ) );
   }

   S.StartVerticalLay(false);
   {
      S.SetBorder(5);
      S.StartTwoColumn();
      {
         mEncodingChoice = S.Id(ChoiceID).AddChoice(XXO("Encoding:"),
                                                    encodings,
                                                    selection);
         mEndianChoice = S.Id(ChoiceID).AddChoice(XXO("Byte order:"),
                                                  endians,
                                                  endian);
         mChannelChoice = S.Id(ChoiceID).AddChoice(XXO("Channels:"),
                                                   chans,
                                                   mChannels - 1);
      }
      S.EndTwoColumn();

      S.SetBorder(5);
      S.StartMultiColumn(3);
      {
         // Offset text
         /* i18n-hint: (noun)*/
         mOffsetText = S.AddTextBox(XXO("Start offset:"),
                                    wxString::Format(wxT("%d"), mOffset),
                                    12);
         S.AddUnits(XO("bytes"));

         // Percent text
         mPercentText = S.AddTextBox(XXO("Amount to import:"),
                                     wxT("100"),
                                     12);
         S.AddUnits(XO("%"));

         // Rate text
         wxArrayStringEx rates;
         for (int i = 0; i < AudioIOBase::NumStandardRates; i++) {
            rates.Add(
               wxString::Format(wxT("%d"), AudioIOBase::StandardRates[i]));
         }

         /* i18n-hint: (noun)*/
         mRateText = S.AddCombo(XXO("Sample rate:"),
                                wxString::Format(wxT("%d"), (int)mRate),
                                rates);
         /* i18n-hint: This is the abbreviation for "Hertz", or
            cycles per second. */
         S.AddUnits(XO("Hz"));
      }
      S.EndMultiColumn();

      S.SetBorder(5);
      S.StartTwoColumn();
      {
         /* i18n-hint: Guess format of raw file */
         S.Id(DetectID).AddButton(XXO("Detect"));

         //
         // Preview Pane goes here
         //

         S.AddStandardButtons();
      }
      S.EndTwoColumn();

      // Find the OK button, and change its text to 'Import'.
      // We MUST set mOK because it is used later.
      mOK = (wxButton *)wxWindow::FindWindowById(wxID_OK, this);
      mOK->SetLabel(_("&Import"));
   }
   S.EndVerticalLay();

   Fit();
   SetSizeHints(GetSize());

   Centre(wxBOTH);
}

ImportRawDialog::~ImportRawDialog()
{
}

void ImportRawDialog::OnOK(wxCommandEvent & WXUNUSED(event))
{
   long long l;

   mEncoding = mEncodingSubtype[mEncodingChoice->GetSelection()];
   mEncoding += (mEndianChoice->GetSelection() * 0x10000000);
   mChannels = mChannelChoice->GetSelection() + 1;
   mOffsetText->GetValue().ToLongLong(&l);
   mOffset = l;
   mPercentText->GetValue().ToDouble(&mPercent);
   mRateText->GetValue().ToDouble(&mRate);

   if (mChannels < 1 || mChannels > 16)
      mChannels = 1;
   if (mOffset < 0)
      mOffset = 0;
   if (mPercent < 0.0)
      mPercent = 0.0;
   if (mPercent > 100.0)
      mPercent = 100.0;
   if (mRate < 100.0)
      mRate = 100.0;
   // Highest preset sample rate supported in Audacity 2.3.0 is 384 kHz
   if (mRate > 384000.0)
      mRate = 384000.0;

   EndModal(true);
}

void ImportRawDialog::OnCancel(wxCommandEvent & WXUNUSED(event))
{
   EndModal(false);
}

void ImportRawDialog::OnPlay(wxCommandEvent & WXUNUSED(event))
{
}

void ImportRawDialog::OnDetect(wxCommandEvent & event)
{
   try {
      // Yes, FormatClassifier currently handles filenames in UTF8 format only, that's a TODO ...
      FormatClassifier theClassifier(mFileName.utf8_str());
      mEncoding = theClassifier.GetResultFormatLibSndfile();
      mChannels = theClassifier.GetResultChannels();
   } catch (...) {
      // Something went wrong in FormatClassifier, abort.
      return;
   }

   int selection = 0;
   auto iter = std::find(mEncodingSubtype.begin(), mEncodingSubtype.end(), mEncoding & SF_FORMAT_SUBMASK);
   if (iter != mEncodingSubtype.end())   // subtype found
       selection = std::distance(mEncodingSubtype.begin(), iter);

   int endian = getEndianChoice(mEncoding);

   mEncodingChoice->SetSelection(selection);
   mEndianChoice->SetSelection(endian);
   mChannelChoice->SetSelection(mChannels - 1);

   OnChoice(event);
}

void ImportRawDialog::OnChoice(wxCommandEvent & WXUNUSED(event))
{
   SF_INFO info;

   memset(&info, 0, sizeof(SF_INFO));

   mEncoding = mEncodingSubtype[mEncodingChoice->GetSelection()];
   mEncoding += (mEndianChoice->GetSelection() * 0x10000000);

   info.format = mEncoding | SF_FORMAT_RAW;
   info.channels = mChannelChoice->GetSelection() + 1;
   info.samplerate = 44100;

   //mOK = (wxButton *)wxWindow::FindWindowById(wxID_OK, this);
   if (sf_format_check(&info)) {
      mOK->Enable(true);
      return;
   }

   // Try it with 1-channel
   info.channels = 1;
   if (sf_format_check(&info)) {
      mChannelChoice->SetSelection(0);
      mOK->Enable(true);
      return;
   }

   // Otherwise, this is an unsupported format
   mOK->Enable(false);
}
