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

   SPDX-License-Identifier: GPL-2.0-or-later

   Audacity: A Digital Audio Editor

   ImportOpus.cpp

   Dmitry Vedenko

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


#include "Import.h"
#include "ImportPlugin.h"

#include <string_view>

#include<wx/string.h>
#include<wx/log.h>

#include<stdlib.h>

#include "Tags.h"
#include "WaveTrack.h"
#include "CodeConversions.h"
#include "ImportUtils.h"
#include "ImportProgressListener.h"
#include "CodeConversions.h"

#include <opus/opusfile.h>

#define DESC XO("Opus files")

static const auto exts = { L"opus", L"ogg" };

class OpusImportPlugin final : public ImportPlugin
{
public:
   OpusImportPlugin();
   ~OpusImportPlugin();

   wxString GetPluginStringID() override;
   TranslatableString GetPluginFormatDescription() override; 
   std::unique_ptr<ImportFileHandle> Open(
     const FilePath &Filename, AudacityProject*) override;
};

class OpusImportFileHandle final : public ImportFileHandleEx
{
public:
   explicit OpusImportFileHandle(const FilePath& filename);
   ~OpusImportFileHandle();

   bool IsOpen() const;

   TranslatableString GetFileDescription() override;
   ByteCount GetFileUncompressedBytes() override;
   void Import(ImportProgressListener &progressListener,
               WaveTrackFactory *trackFactory,
               TrackHolders &outTracks,
               Tags *tags) override;
   
   wxInt32 GetStreamCount() override;
   const TranslatableStrings &GetStreamInfo() override;
   void SetStreamUsage(wxInt32 StreamID, bool Use) override;

private:
   static int OpusReadCallback(void* stream, unsigned char* ptr, int nbytes);
   static int OpusSeekCallback(void* stream, opus_int64 offset, int whence);
   static opus_int64 OpusTellCallback(void* stream);
   static int OpusCloseCallback(void* stream);

   static TranslatableString GetOpusErrorString(int error);
   void LogOpusError(const char* method, int error);
   void NotifyImportFailed(ImportProgressListener& progressListener, int error);
   void NotifyImportFailed(ImportProgressListener& progressListener, const TranslatableString& error);

   wxFile mFile;

   OpusFileCallbacks mCallbacks;
   OggOpusFile* mOpusFile {};
   int mNumChannels {};
   int64_t mNumSamples {};

   // Opus internally uses 48kHz sample rate
   // The file header contains the sample rate of the original audio.
   // We ignore it and let Audacity resample the audio to the project sample rate.
   const double mSampleRate { 48000.0 };

   // Opus decodes to float samples internally, optionally converting them to int16.
   // We let Audacity to convert the stream to the project sample format.
   const sampleFormat mFormat { floatSample };
};

// ============================================================================
// OpusImportPlugin
// ============================================================================

OpusImportPlugin::OpusImportPlugin()
:  ImportPlugin(FileExtensions(exts.begin(), exts.end()))
{
}

OpusImportPlugin::~OpusImportPlugin()
{
}

wxString OpusImportPlugin::GetPluginStringID()
{
   return wxT("libopus");
}

TranslatableString OpusImportPlugin::GetPluginFormatDescription()
{
   return DESC;
}

std::unique_ptr<ImportFileHandle> OpusImportPlugin::Open(const FilePath &filename, AudacityProject*)
{   
   auto handle = std::make_unique<OpusImportFileHandle>(filename);

   if (!handle->IsOpen())
      return {};

   return std::move(handle);
}

static Importer::RegisteredImportPlugin registered{ "Opus",
   std::make_unique< OpusImportPlugin >()
};

// ============================================================================
// OpusImportFileHandle
// ============================================================================

OpusImportFileHandle::OpusImportFileHandle(const FilePath& filename)
    : ImportFileHandleEx { filename }
{
   // Try to open the file for reading
   if (!mFile.Open(filename, wxFile::read))
      return;

   OpusFileCallbacks callbacks = {
      OpusReadCallback,
      OpusSeekCallback,
      OpusTellCallback,
      OpusCloseCallback
   };

   int error = 0;
   mOpusFile = op_open_callbacks(this, &callbacks, nullptr, 0, &error);

   if (mOpusFile == nullptr)
   {
      LogOpusError("Error while opening Opus file", error);
      return;
   }

   mNumChannels = op_channel_count(mOpusFile, -1);
   mNumSamples = op_pcm_total(mOpusFile, -1);
}

TranslatableString OpusImportFileHandle::GetFileDescription()
{
   return DESC;
}

auto OpusImportFileHandle::GetFileUncompressedBytes() -> ByteCount
{
   return 0;
}

void OpusImportFileHandle::Import(ImportProgressListener &progressListener,
                                     WaveTrackFactory *trackFactory,
                                     TrackHolders &outTracks,
                                     Tags *tags)
{
   BeginImport();
   
   outTracks.clear();

   auto trackList = ImportUtils::NewWaveTrack(
      *trackFactory,
      mNumChannels,
      mFormat,
      mSampleRate);

   /* The number of samples to read in each loop */
   const size_t SAMPLES_TO_READ = (*trackList->Any<WaveTrack>().begin())->GetMaxBlockSize();
   uint64_t totalSamplesRead = 0;

   const auto bufferSize = mNumChannels * SAMPLES_TO_READ;

   ArrayOf<float> floatBuffer { bufferSize };

   uint64_t samplesRead = 0;

   do
   {
      int linkIndex { -1 };
      auto samplesPerChannelRead = op_read_float(mOpusFile, floatBuffer.get(), SAMPLES_TO_READ, &linkIndex);

      if (samplesPerChannelRead < 0 && samplesPerChannelRead != OP_HOLE)
      {
         NotifyImportFailed(progressListener, samplesPerChannelRead);
         return;
      }

      auto linkChannels = op_head(mOpusFile, linkIndex)->channel_count;

      if (linkChannels != mNumChannels)
      {
         NotifyImportFailed(progressListener, XO("file has changed the number of channels in the middle."));
         return;
      }

      unsigned chn = 0;
      ImportUtils::ForEachChannel(*trackList, [&](auto& channel)
      {
         channel.AppendBuffer(
            reinterpret_cast<constSamplePtr>(floatBuffer.get() +
            chn), mFormat, samplesRead, mNumChannels, mFormat
         );
         ++chn;
      });

      samplesRead = samplesPerChannelRead;
      totalSamplesRead += samplesRead;

      progressListener.OnImportProgress(double(totalSamplesRead) / mNumSamples);
   } while (!IsCancelled() && !IsStopped() && samplesRead != 0);

   if (IsCancelled())
   {
      progressListener.OnImportResult(
         ImportProgressListener::ImportResult::Cancelled);
      return;
   }

   if (totalSamplesRead < mNumSamples && !IsStopped())
   {
      progressListener.OnImportResult(ImportProgressListener::ImportResult::Error);
      return;
   }

   ImportUtils::FinalizeImport(outTracks, trackList);

   auto opusTags = op_tags(mOpusFile, -1);

   if (opusTags != nullptr)
   {
         for (int i = 0; i < opusTags->comments; ++i)
         {
            const auto comment = opusTags->user_comments[i];
            const auto commentLength = opusTags->comment_lengths[i];

            std::string_view tag { comment,
                                   std::string_view::size_type(commentLength) };

            const auto separator = tag.find('=');

            if (separator != std::string_view::npos)
            {
               auto name = audacity::ToWXString(tag.substr(0, separator));
               const auto value = audacity::ToWXString(tag.substr(separator + 1));

               // See: ImportOGG.cpp tags parsing
               if (name.Upper() == wxT("DATE") && !tags->HasTag(TAG_YEAR))
               {
                  long val;

                  if (value.length() == 4 && value.ToLong(&val))
                     name = TAG_YEAR;
               }

               tags->SetTag(name, value);
            }
         }
   }

   progressListener.OnImportResult(IsStopped()
                                   ? ImportProgressListener::ImportResult::Stopped
                                   : ImportProgressListener::ImportResult::Success);
}

wxInt32 OpusImportFileHandle::GetStreamCount()
{
   return 1;
}

const TranslatableStrings &OpusImportFileHandle::GetStreamInfo()
{
   static TranslatableStrings empty;
   return empty;
}

void OpusImportFileHandle::SetStreamUsage(wxInt32, bool)
{
}

int OpusImportFileHandle::OpusReadCallback(
   void* pstream, unsigned char* ptr, int nbytes)
{
   auto stream = static_cast<OpusImportFileHandle*>(pstream);

   if (!stream->mFile.IsOpened())
      return EOF;

   // OpusFile never reads more than 2^31 bytes at a time,
   // so we can safely cast ssize_t to int.
   return int(stream->mFile.Read(ptr, nbytes));
}

int OpusImportFileHandle::OpusSeekCallback(
   void* pstream, opus_int64 offset, int whence)
{
   auto stream = static_cast<OpusImportFileHandle*>(pstream);

   if (!stream->mFile.IsOpened())
      return -1;

   wxSeekMode wxWhence = whence == SEEK_SET ? wxFromStart :
                         whence == SEEK_CUR ? wxFromCurrent :
                         whence == SEEK_END ? wxFromEnd : wxFromStart;

   return stream->mFile.Seek(offset, wxWhence) != wxInvalidOffset ? 0 : -1;
}

opus_int64 OpusImportFileHandle::OpusTellCallback(void* pstream)
{
   auto stream = static_cast<OpusImportFileHandle*>(pstream);

   return static_cast<opus_int64>(stream->mFile.Tell());
}

int OpusImportFileHandle::OpusCloseCallback(void* pstream)
{
   auto stream = static_cast<OpusImportFileHandle*>(pstream);

   if (stream->mFile.IsOpened())
      return stream->mFile.Close() ? 0 : EOF;

   return 0;
}

TranslatableString OpusImportFileHandle::GetOpusErrorString (int error)
{
   switch (error)
   {
   case OP_EREAD:
      return XO("IO error reading from file");
   case OP_EFAULT:
      return XO("internal error");
   case OP_EIMPL:
      return XO("not implemented");
   case OP_EINVAL:
      return XO("invalid argument");
   case OP_ENOTFORMAT:
      return XO("not an Opus file");
   case OP_EBADHEADER:
      return XO("invalid header");
   case OP_EVERSION:
      return XO("unsupported version");
   case OP_EBADPACKET:
      return XO("invalid packet");
   case OP_EBADLINK:
      return XO("invalid stream structure");
   case OP_ENOSEEK:
      return XO("stream is not seekable");
   case OP_EBADTIMESTAMP:
      return XO("invalid timestamp");
   default:
      return {};
   }
}

void OpusImportFileHandle::LogOpusError(const char* method, int error)
{
   if (error == 0)
      return;

   if (error == OP_ENOTFORMAT)
      wxLogDebug("%s: Not Opus format", GetOpusErrorString(error).Translation());
   else
      wxLogError("%s: %s", method, GetOpusErrorString(error).Translation());
}

void OpusImportFileHandle::NotifyImportFailed(
   ImportProgressListener& progressListener, int error)
{
   NotifyImportFailed(progressListener, GetOpusErrorString(error));
}

void OpusImportFileHandle::NotifyImportFailed(
   ImportProgressListener& progressListener, const TranslatableString& error)
{
   ImportUtils::ShowMessageBox(
      XO("Failed to decode Opus file: %s").Format(error));

   if (IsCancelled())
      progressListener.OnImportResult(
         ImportProgressListener::ImportResult::Cancelled);
   else if (!IsStopped())
      progressListener.OnImportResult(
         ImportProgressListener::ImportResult::Error);
   else
      progressListener.OnImportResult(
         ImportProgressListener::ImportResult::Stopped);
}

bool OpusImportFileHandle::IsOpen() const
{
   return mOpusFile != nullptr;
}

OpusImportFileHandle::~OpusImportFileHandle()
{
   if (mOpusFile != nullptr)
      op_free(mOpusFile);
}
