#ifndef miniaudio_libopus_c
#define miniaudio_libopus_c

#include <audioapi/libs/miniaudio/decoders/libopus/miniaudio_libopus.h>

#if !defined(MA_NO_LIBOPUS)
#include <audioapi/external/include/opusfile/opusfile.h>
#endif

#include <assert.h>
#include <string.h> /* For memset(). */

static ma_result ma_libopus_ds_read(
    ma_data_source *pDataSource,
    void *pFramesOut,
    ma_uint64 frameCount,
    ma_uint64 *pFramesRead) {
  return ma_libopus_read_pcm_frames(
      (ma_libopus *)pDataSource, pFramesOut, frameCount, pFramesRead);
}

static ma_result ma_libopus_ds_seek(
    ma_data_source *pDataSource,
    ma_uint64 frameIndex) {
  return ma_libopus_seek_to_pcm_frame((ma_libopus *)pDataSource, frameIndex);
}

static ma_result ma_libopus_ds_get_data_format(
    ma_data_source *pDataSource,
    ma_format *pFormat,
    ma_uint32 *pChannels,
    ma_uint32 *pSampleRate,
    ma_channel *pChannelMap,
    size_t channelMapCap) {
  return ma_libopus_get_data_format(
      (ma_libopus *)pDataSource,
      pFormat,
      pChannels,
      pSampleRate,
      pChannelMap,
      channelMapCap);
}

static ma_result ma_libopus_ds_get_cursor(
    ma_data_source *pDataSource,
    ma_uint64 *pCursor) {
  return ma_libopus_get_cursor_in_pcm_frames(
      (ma_libopus *)pDataSource, pCursor);
}

static ma_result ma_libopus_ds_get_length(
    ma_data_source *pDataSource,
    ma_uint64 *pLength) {
  return ma_libopus_get_length_in_pcm_frames(
      (ma_libopus *)pDataSource, pLength);
}

static ma_data_source_vtable g_ma_libopus_ds_vtable = {
    ma_libopus_ds_read,
    ma_libopus_ds_seek,
    ma_libopus_ds_get_data_format,
    ma_libopus_ds_get_cursor,
    ma_libopus_ds_get_length,
    NULL, /* onSetLooping */
    0 /* flags */
};

#if !defined(MA_NO_LIBOPUS)
static int ma_libopus_of_callback__read(
    void *pUserData,
    unsigned char *pBufferOut,
    int bytesToRead) {
  ma_libopus *pOpus = (ma_libopus *)pUserData;
  ma_result result;
  size_t bytesRead;

  result = pOpus->onRead(
      pOpus->pReadSeekTellUserData,
      (void *)pBufferOut,
      bytesToRead,
      &bytesRead);

  if (result != MA_SUCCESS) {
    return -1;
  }

  return (int)bytesRead;
}

static int
ma_libopus_of_callback__seek(void *pUserData, ogg_int64_t offset, int whence) {
  ma_libopus *pOpus = (ma_libopus *)pUserData;
  ma_result result;
  ma_seek_origin origin;

  if (whence == SEEK_SET) {
    origin = ma_seek_origin_start;
  } else if (whence == SEEK_END) {
    origin = ma_seek_origin_end;
  } else {
    origin = ma_seek_origin_current;
  }

  result = pOpus->onSeek(pOpus->pReadSeekTellUserData, offset, origin);
  if (result != MA_SUCCESS) {
    return -1;
  }

  return 0;
}

static opus_int64 ma_libopus_of_callback__tell(void *pUserData) {
  ma_libopus *pOpus = (ma_libopus *)pUserData;
  ma_result result;
  ma_int64 cursor;

  if (pOpus->onTell == NULL) {
    return -1;
  }

  result = pOpus->onTell(pOpus->pReadSeekTellUserData, &cursor);
  if (result != MA_SUCCESS) {
    return -1;
  }

  return cursor;
}
#endif

static ma_result ma_libopus_init_internal(
    const ma_decoding_backend_config *pConfig,
    ma_libopus *pOpus) {
  ma_result result;
  ma_data_source_config dataSourceConfig;

  if (pOpus == NULL) {
    return MA_INVALID_ARGS;
  }

  memset(pOpus, 0, sizeof(*pOpus));
  pOpus->format = ma_format_f32; /* f32 by default. */

  if (pConfig != NULL &&
      (pConfig->preferredFormat == ma_format_f32 ||
       pConfig->preferredFormat == ma_format_s16)) {
    pOpus->format = pConfig->preferredFormat;
  } else {
    /* Getting here means something other than f32 and s16 was specified. Just
     * leave this unset to use the default format. */
  }

  dataSourceConfig = ma_data_source_config_init();
  dataSourceConfig.vtable = &g_ma_libopus_ds_vtable;

  result = ma_data_source_init(&dataSourceConfig, &pOpus->ds);
  if (result != MA_SUCCESS) {
    return result; /* Failed to initialize the base data source. */
  }

  return MA_SUCCESS;
}

MA_API ma_result ma_libopus_init(
    ma_read_proc onRead,
    ma_seek_proc onSeek,
    ma_tell_proc onTell,
    void *pReadSeekTellUserData,
    const ma_decoding_backend_config *pConfig,
    const ma_allocation_callbacks *pAllocationCallbacks,
    ma_libopus *pOpus) {
  ma_result result;

  (void)pAllocationCallbacks; /* Can't seem to find a way to configure memory
                                 allocations in libopus. */

  result = ma_libopus_init_internal(pConfig, pOpus);
  if (result != MA_SUCCESS) {
    return result;
  }

  if (onRead == NULL || onSeek == NULL) {
    return MA_INVALID_ARGS; /* onRead and onSeek are mandatory. */
  }

  pOpus->onRead = onRead;
  pOpus->onSeek = onSeek;
  pOpus->onTell = onTell;
  pOpus->pReadSeekTellUserData = pReadSeekTellUserData;

#if !defined(MA_NO_LIBOPUS)
  {
    int libopusResult;
    OpusFileCallbacks libopusCallbacks;

    /* We can now initialize the Opus decoder. This must be done after we've set
     * up the callbacks. */
    libopusCallbacks.read = ma_libopus_of_callback__read;
    libopusCallbacks.seek = ma_libopus_of_callback__seek;
    libopusCallbacks.close = NULL;
    libopusCallbacks.tell = ma_libopus_of_callback__tell;

    pOpus->of =
        op_open_callbacks(pOpus, &libopusCallbacks, NULL, 0, &libopusResult);
    if (pOpus->of == NULL) {
      return MA_INVALID_FILE;
    }

    return MA_SUCCESS;
  }
#else
  {
    /* libopus is disabled. */
    return MA_NOT_IMPLEMENTED;
  }
#endif
}

MA_API ma_result ma_libopus_init_file(
    const char *pFilePath,
    const ma_decoding_backend_config *pConfig,
    const ma_allocation_callbacks *pAllocationCallbacks,
    ma_libopus *pOpus) {
  ma_result result;

  (void)pAllocationCallbacks; /* Can't seem to find a way to configure memory
                                 allocations in libopus. */

  result = ma_libopus_init_internal(pConfig, pOpus);
  if (result != MA_SUCCESS) {
    return result;
  }

#if !defined(MA_NO_LIBOPUS)
  {
    int libopusResult;

    pOpus->of = op_open_file(pFilePath, &libopusResult);
    if (pOpus->of == NULL) {
      return MA_INVALID_FILE;
    }

    return MA_SUCCESS;
  }
#else
  {
    /* libopus is disabled. */
    (void)pFilePath;
    return MA_NOT_IMPLEMENTED;
  }
#endif
}

MA_API void ma_libopus_uninit(
    ma_libopus *pOpus,
    const ma_allocation_callbacks *pAllocationCallbacks) {
  if (pOpus == NULL) {
    return;
  }

  (void)pAllocationCallbacks;

#if !defined(MA_NO_LIBOPUS)
  {
    op_free((OggOpusFile *)pOpus->of);
  }
#else
  {
    /* libopus is disabled. Should never hit this since initialization would
     * have failed. */
    assert(MA_FALSE);
  }
#endif

  ma_data_source_uninit(&pOpus->ds);
}

MA_API ma_result ma_libopus_read_pcm_frames(
    ma_libopus *pOpus,
    void *pFramesOut,
    ma_uint64 frameCount,
    ma_uint64 *pFramesRead) {
  if (pFramesRead != NULL) {
    *pFramesRead = 0;
  }

  if (frameCount == 0) {
    return MA_INVALID_ARGS;
  }

  if (pOpus == NULL) {
    return MA_INVALID_ARGS;
  }

#if !defined(MA_NO_LIBOPUS)
  {
    /* We always use floating point format. */
    ma_result result = MA_SUCCESS; /* Must be initialized to MA_SUCCESS. */
    ma_uint64 totalFramesRead;
    ma_format format;
    ma_uint32 channels;

    ma_libopus_get_data_format(pOpus, &format, &channels, NULL, NULL, 0);

    totalFramesRead = 0;
    while (totalFramesRead < frameCount) {
      long libopusResult;
      ma_uint64 framesToRead;
      ma_uint64 framesRemaining;

      framesRemaining = (frameCount - totalFramesRead);
      framesToRead = 1024;
      if (framesToRead > framesRemaining) {
        framesToRead = framesRemaining;
      }

      if (format == ma_format_f32) {
        libopusResult = op_read_float(
            (OggOpusFile *)pOpus->of,
            (float *)ma_offset_pcm_frames_ptr(
                pFramesOut, totalFramesRead, format, channels),
            (int)(framesToRead * channels),
            NULL);
      } else {
        libopusResult = op_read(
            (OggOpusFile *)pOpus->of,
            (opus_int16 *)ma_offset_pcm_frames_ptr(
                pFramesOut, totalFramesRead, format, channels),
            (int)(framesToRead * channels),
            NULL);
      }

      if (libopusResult < 0) {
        result = MA_ERROR; /* Error while decoding. */
        break;
      } else {
        totalFramesRead += libopusResult;

        if (libopusResult == 0) {
          result = MA_AT_END;
          break;
        }
      }
    }

    if (pFramesRead != NULL) {
      *pFramesRead = totalFramesRead;
    }

    if (result == MA_SUCCESS && totalFramesRead == 0) {
      result = MA_AT_END;
    }

    return result;
  }
#else
  {
    /* libopus is disabled. Should never hit this since initialization would
     * have failed. */
    assert(MA_FALSE);

    (void)pFramesOut;
    (void)frameCount;
    (void)pFramesRead;

    return MA_NOT_IMPLEMENTED;
  }
#endif
}

MA_API ma_result
ma_libopus_seek_to_pcm_frame(ma_libopus *pOpus, ma_uint64 frameIndex) {
  if (pOpus == NULL) {
    return MA_INVALID_ARGS;
  }

#if !defined(MA_NO_LIBOPUS)
  {
    int libopusResult =
        op_pcm_seek((OggOpusFile *)pOpus->of, (ogg_int64_t)frameIndex);
    if (libopusResult != 0) {
      if (libopusResult == OP_ENOSEEK) {
        return MA_INVALID_OPERATION; /* Not seekable. */
      } else if (libopusResult == OP_EINVAL) {
        return MA_INVALID_ARGS;
      } else {
        return MA_ERROR;
      }
    }

    return MA_SUCCESS;
  }
#else
  {
    /* libopus is disabled. Should never hit this since initialization would
     * have failed. */
    assert(MA_FALSE);

    (void)frameIndex;

    return MA_NOT_IMPLEMENTED;
  }
#endif
}

MA_API ma_result ma_libopus_get_data_format(
    ma_libopus *pOpus,
    ma_format *pFormat,
    ma_uint32 *pChannels,
    ma_uint32 *pSampleRate,
    ma_channel *pChannelMap,
    size_t channelMapCap) {
  /* Defaults for safety. */
  if (pFormat != NULL) {
    *pFormat = ma_format_unknown;
  }
  if (pChannels != NULL) {
    *pChannels = 0;
  }
  if (pSampleRate != NULL) {
    *pSampleRate = 0;
  }
  if (pChannelMap != NULL) {
    memset(pChannelMap, 0, sizeof(*pChannelMap) * channelMapCap);
  }

  if (pOpus == NULL) {
    return MA_INVALID_OPERATION;
  }

  if (pFormat != NULL) {
    *pFormat = pOpus->format;
  }

#if !defined(MA_NO_LIBOPUS)
  {
    ma_uint32 channels = op_channel_count((OggOpusFile *)pOpus->of, -1);

    if (pChannels != NULL) {
      *pChannels = channels;
    }

    if (pSampleRate != NULL) {
      *pSampleRate = 48000;
    }

    if (pChannelMap != NULL) {
      ma_channel_map_init_standard(
          ma_standard_channel_map_vorbis, pChannelMap, channelMapCap, channels);
    }

    return MA_SUCCESS;
  }
#else
  {
    /* libopus is disabled. Should never hit this since initialization would
     * have failed. */
    assert(MA_FALSE);
    return MA_NOT_IMPLEMENTED;
  }
#endif
}

MA_API ma_result
ma_libopus_get_cursor_in_pcm_frames(ma_libopus *pOpus, ma_uint64 *pCursor) {
  if (pCursor == NULL) {
    return MA_INVALID_ARGS;
  }

  *pCursor = 0; /* Safety. */

  if (pOpus == NULL) {
    return MA_INVALID_ARGS;
  }

#if !defined(MA_NO_LIBOPUS)
  {
    ogg_int64_t offset = op_pcm_tell((OggOpusFile *)pOpus->of);
    if (offset < 0) {
      return MA_INVALID_FILE;
    }

    *pCursor = (ma_uint64)offset;

    return MA_SUCCESS;
  }
#else
  {
    /* libopus is disabled. Should never hit this since initialization would
     * have failed. */
    assert(MA_FALSE);
    return MA_NOT_IMPLEMENTED;
  }
#endif
}

MA_API ma_result
ma_libopus_get_length_in_pcm_frames(ma_libopus *pOpus, ma_uint64 *pLength) {
  if (pLength == NULL) {
    return MA_INVALID_ARGS;
  }

  *pLength = 0; /* Safety. */

  if (pOpus == NULL) {
    return MA_INVALID_ARGS;
  }

#if !defined(MA_NO_LIBOPUS)
  {
    ogg_int64_t length = op_pcm_total((OggOpusFile *)pOpus->of, -1);
    if (length < 0) {
      return MA_ERROR;
    }

    *pLength = (ma_uint64)length;

    return MA_SUCCESS;
  }
#else
  {
    /* libopus is disabled. Should never hit this since initialization would
     * have failed. */
    assert(MA_FALSE);
    return MA_NOT_IMPLEMENTED;
  }
#endif
}

/*
The code below defines the vtable that you'll plug into your `ma_decoder_config`
object.
*/
#if !defined(MA_NO_LIBOPUS)
static ma_result ma_decoding_backend_init__libopus(
    void *pUserData,
    ma_read_proc onRead,
    ma_seek_proc onSeek,
    ma_tell_proc onTell,
    void *pReadSeekTellUserData,
    const ma_decoding_backend_config *pConfig,
    const ma_allocation_callbacks *pAllocationCallbacks,
    ma_data_source **ppBackend) {
  ma_result result;
  ma_libopus *pOpus;

  (void)pUserData;

  pOpus = (ma_libopus *)ma_malloc(sizeof(*pOpus), pAllocationCallbacks);
  if (pOpus == NULL) {
    return MA_OUT_OF_MEMORY;
  }

  result = ma_libopus_init(
      onRead,
      onSeek,
      onTell,
      pReadSeekTellUserData,
      pConfig,
      pAllocationCallbacks,
      pOpus);
  if (result != MA_SUCCESS) {
    ma_free(pOpus, pAllocationCallbacks);
    return result;
  }

  *ppBackend = pOpus;

  return MA_SUCCESS;
}

static ma_result ma_decoding_backend_init_file__libopus(
    void *pUserData,
    const char *pFilePath,
    const ma_decoding_backend_config *pConfig,
    const ma_allocation_callbacks *pAllocationCallbacks,
    ma_data_source **ppBackend) {
  ma_result result;
  ma_libopus *pOpus;

  (void)pUserData;

  pOpus = (ma_libopus *)ma_malloc(sizeof(*pOpus), pAllocationCallbacks);
  if (pOpus == NULL) {
    return MA_OUT_OF_MEMORY;
  }

  result =
      ma_libopus_init_file(pFilePath, pConfig, pAllocationCallbacks, pOpus);
  if (result != MA_SUCCESS) {
    ma_free(pOpus, pAllocationCallbacks);
    return result;
  }

  *ppBackend = pOpus;

  return MA_SUCCESS;
}

static void ma_decoding_backend_uninit__libopus(
    void *pUserData,
    ma_data_source *pBackend,
    const ma_allocation_callbacks *pAllocationCallbacks) {
  ma_libopus *pOpus = (ma_libopus *)pBackend;

  (void)pUserData;

  ma_libopus_uninit(pOpus, pAllocationCallbacks);
  ma_free(pOpus, pAllocationCallbacks);
}

static ma_decoding_backend_vtable ma_gDecodingBackendVTable_libopus = {
    ma_decoding_backend_init__libopus,
    ma_decoding_backend_init_file__libopus,
    NULL, /* onInitFileW() */
    NULL, /* onInitMemory() */
    ma_decoding_backend_uninit__libopus};
ma_decoding_backend_vtable *ma_decoding_backend_libopus =
    &ma_gDecodingBackendVTable_libopus;
#else
ma_decoding_backend_vtable *ma_decoding_backend_libopus = NULL;
#endif

#endif /* miniaudio_libopus_c */
