#pragma once

#include "AudioTools/AudioCodecs/AudioCodecsBase.h"
#include "AudioTools/AudioCodecs/CodecOpus.h"
#include "AudioTools/CoreAudio/Buffers.h"
#include "oggz.h"

#define OGG_READ_SIZE (1024)
#define OGG_DEFAULT_BUFFER_SIZE (OGG_READ_SIZE)
// #define OGG_DEFAULT_BUFFER_SIZE (246)
// #define OGG_READ_SIZE (512)

namespace audio_tools {

/**
 * @brief Decoder for Ogg Container. Decodes a packet from an Ogg
 * container. The Ogg begin segment contains the AudioInfo structure. You can
 * subclass and overwrite the beginOfSegment() method to implement your own
 * headers
 * Dependency: https://github.com/pschatzmann/arduino-libopus
 * @ingroup codecs
 * @ingroup decoder
 * @author Phil Schatzmann
 * @copyright GPLv3
 */
class OggContainerDecoder : public ContainerDecoder {
 public:
  /**
   * @brief Construct a new OggContainerDecoder object
   */

  OggContainerDecoder() {
    p_codec = &dec_copy;
    out.setDecoder(p_codec);
  }

  OggContainerDecoder(AudioDecoder *decoder) { setDecoder(decoder); }

  OggContainerDecoder(AudioDecoder &decoder) { setDecoder(&decoder); }

  void setDecoder(AudioDecoder *decoder) {
    p_codec = decoder;
    out.setDecoder(p_codec);
  }

  /// Defines the output Stream
  void setOutput(Print &print) override { out.setOutput(&print); }

  void addNotifyAudioChange(AudioInfoSupport &bi) override {
    out.addNotifyAudioChange(bi);
    ContainerDecoder::addNotifyAudioChange(bi);
  }

  AudioInfo audioInfo() override { return out.audioInfo(); }

  bool begin(AudioInfo info) override {
    TRACED();
    this->info = info;
    return begin();
  }

  bool begin() override {
    TRACED();
    out.setAudioInfo(info);
    out.begin();
    if (p_oggz == nullptr) {
      p_oggz = oggz_new(OGGZ_READ | OGGZ_AUTO);  // OGGZ_NONSTRICT
      is_open = true;
      // Callback to Replace standard IO
      if (oggz_io_set_read(p_oggz, ogg_io_read, this) != 0) {
        LOGE("oggz_io_set_read");
        is_open = false;
      }
      // Callback
      if (oggz_set_read_callback(p_oggz, -1, read_packet, this) != 0) {
        LOGE("oggz_set_read_callback");
        is_open = false;
      }

      if (oggz_set_read_page(p_oggz, -1, read_page, this) != 0) {
        LOGE("oggz_set_read_page");
        is_open = false;
      }
    }
    return is_open;
  }

  void end() override {
    TRACED();
    flush();
    out.end();
    is_open = false;
    oggz_close(p_oggz);
    p_oggz = nullptr;
  }

  void flush() {
    LOGD("oggz_read...");
    while ((oggz_read(p_oggz, OGG_READ_SIZE)) > 0)
      ;
  }

  virtual size_t write(const uint8_t *data, size_t len) override {
    LOGD("write: %d", (int)len);

    // fill buffer
    size_t size_consumed = buffer.writeArray((uint8_t *)data, len);
    if (buffer.availableForWrite() == 0) {
      // Read all bytes into oggz, calling any read callbacks on the fly.
      flush();
    }
    // write remaining bytes
    if (size_consumed < len) {
      size_consumed += buffer.writeArray((uint8_t *)data + size_consumed,
                                         len - size_consumed);
      flush();
    }
    return size_consumed;
  }

  virtual operator bool() override { return is_open; }

 protected:
  EncodedAudioOutput out;
  CopyDecoder dec_copy;
  AudioDecoder *p_codec = nullptr;
  RingBuffer<uint8_t> buffer{OGG_DEFAULT_BUFFER_SIZE};
  OGGZ *p_oggz = nullptr;
  bool is_open = false;
  long pos = 0;

  // Final Stream Callback -> provide data to ogg
  static size_t ogg_io_read(void *user_handle, void *buf, size_t n) {
    LOGD("ogg_io_read: %d", (int)n);
    size_t result = 0;
    OggContainerDecoder *self = (OggContainerDecoder *)user_handle;
    if (self->buffer.available() >= n) {
      OggContainerDecoder *self = (OggContainerDecoder *)user_handle;
      result = self->buffer.readArray((uint8_t *)buf, n);
      self->pos += result;

    } else {
      result = 0;
    }
    return result;
  }

  // Process full packet
  static int read_packet(OGGZ *oggz, oggz_packet *zp, long serialno,
                         void *user_data) {
    LOGD("read_packet: %d", (int)zp->op.bytes);
    OggContainerDecoder *self = (OggContainerDecoder *)user_data;
    ogg_packet *op = &zp->op;
    int result = op->bytes;
    if (op->b_o_s) {
      self->beginOfSegment(op);
    } else if (op->e_o_s) {
      self->endOfSegment(op);
    } else {
      if (memcmp(op->packet, "OpusTags", 8) == 0) {
        self->beginOfSegment(op);
      } else {
        LOGD("process audio packet");
        int eff = self->out.write(op->packet, op->bytes);
        if (eff != result) {
          LOGE("Incomplere write");
        }
      }
    }
    // 0 = success
    return 0;
  }

  static int read_page(OGGZ *oggz, const ogg_page *og, long serialno,
                       void *user_data) {
    LOGD("read_page: %d", (int)og->body_len);
    // 0 = success
    return 0;
  }

  virtual void beginOfSegment(ogg_packet *op) {
    LOGD("bos");
    if (op->bytes == sizeof(AudioInfo)) {
      AudioInfo cfg(*(AudioInfo*)op->packet);
      cfg.logInfo();
      if (cfg.bits_per_sample == 16 || cfg.bits_per_sample == 24 ||
          cfg.bits_per_sample == 32) {
        setAudioInfo(cfg);
      } else {
        LOGE("Invalid AudioInfo")
      }
    } else {
      LOGE("Invalid Header")
    }
  }

  virtual void endOfSegment(ogg_packet *op) {
    // end segment not supported
    LOGW("e_o_s");
  }
};

/**
 * @brief Output class for the OggContainerEncoder. Each
 * write is ending up as container entry
 * @author Phil Schatzmann
 * @copyright GPLv3
 */
class OggContainerOutput : public AudioOutput {
 public:
  // Empty Constructor - the output stream must be provided with begin()
  OggContainerOutput() = default;

  /// Defines the output Stream
  void setOutput(Print &print) { p_out = &print; }

  /// starts the processing using the actual AudioInfo
  virtual bool begin() override {
    TRACED();
    assert(cfg.channels != 0);
    assert(cfg.sample_rate != 0);
    is_open = true;
    if (p_oggz == nullptr) {
      p_oggz = oggz_new(OGGZ_WRITE | OGGZ_NONSTRICT | OGGZ_AUTO);
      serialno = oggz_serialno_new(p_oggz);
      oggz_io_set_write(p_oggz, ogg_io_write, this);
      packetno = 0;
      granulepos = 0;

      if (!writeHeader()) {
        is_open = false;
        LOGE("writeHeader");
      }
    }
    return is_open;
  }

  /// stops the processing
  void end() override {
    TRACED();

    writeFooter();

    is_open = false;
    oggz_close(p_oggz);
    p_oggz = nullptr;
  }

  /// Writes raw data to be encoded and packaged
  virtual size_t write(const uint8_t *data, size_t len) override {
    if (data == nullptr) return 0;
    LOGD("OggContainerOutput::write: %d", (int)len);
    assert(cfg.channels != 0);

    // encode the data
    op.packet = (uint8_t *)data;
    op.bytes = len;
    if (op.bytes > 0) {
      int bytes_per_sample = cfg.bits_per_sample / 8;
      granulepos += op.bytes / bytes_per_sample;  // sample
      op.granulepos = granulepos;
      op.b_o_s = false;
      op.e_o_s = false;
      op.packetno = packetno++;
      is_audio = true;
      if (!writePacket(op, OGGZ_FLUSH_AFTER)) {
        return 0;
      }
    }
    // trigger pysical write
    while ((oggz_write(p_oggz, len)) > 0)
      ;

    return len;
  }
  bool isOpen() { return is_open; }

 protected:
  Print *p_out = nullptr;
  bool is_open = false;
  OGGZ *p_oggz = nullptr;
  ogg_packet op;
  ogg_packet oh;
  size_t granulepos = 0;
  size_t packetno = 0;
  long serialno = -1;
  bool is_audio = false;

  virtual bool writePacket(ogg_packet &op, int flag = 0) {
    LOGD("writePacket: %d", (int)op.bytes);
    long result = oggz_write_feed(p_oggz, &op, serialno, flag, NULL);
    if (result < 0 && result != OGGZ_ERR_OUT_OF_MEMORY) {
      LOGE("oggz_write_feed: %d", (int)result);
      return false;
    }
    return true;
  }

  virtual bool writeHeader() {
    TRACED();
    oh.packet = (uint8_t *)&cfg;
    oh.bytes = sizeof(AudioInfo);
    oh.granulepos = 0;
    oh.packetno = packetno++;
    oh.b_o_s = true;
    oh.e_o_s = false;
    is_audio = false;
    return writePacket(oh);
  }

  virtual bool writeFooter() {
    TRACED();
    op.packet = (uint8_t *)nullptr;
    op.bytes = 0;
    op.granulepos = granulepos;
    op.packetno = packetno++;
    op.b_o_s = false;
    op.e_o_s = true;
    is_audio = false;
    return writePacket(op, OGGZ_FLUSH_AFTER);
  }

  // Final Stream Callback
  static size_t ogg_io_write(void *user_handle, void *buf, size_t n) {
    LOGD("ogg_io_write: %d", (int)n);
    OggContainerOutput *self = (OggContainerOutput *)user_handle;
    if (self == nullptr) {
      LOGE("self is null");
      return 0;
    }
    // self->out.write((uint8_t *)buf, n);
    writeData<uint8_t>(self->p_out, (uint8_t *)buf, n);
    // 0 = continue
    return 0;
  }
};

/**
 * @brief Encoder for Ogg Container. Encodes a packet for an Ogg
 * container. The Ogg begin segment contains the AudioInfo structure. You can
 * subclass ond overwrite the writeHeader() method to implement your own header
 * logic. When an optional encoder is specified in the constructor we package
 * the encoded data.
 * Dependency: https://github.com/pschatzmann/arduino-libopus
 * @ingroup codecs
 * @ingroup encoder
 * @author Phil Schatzmann
 * @copyright GPLv3
 */
class OggContainerEncoder : public AudioEncoder {
 public:
  // Empty Constructor - the output stream must be provided with begin()
  OggContainerEncoder() = default;

  OggContainerEncoder(AudioEncoder *encoder) { setEncoder(encoder); }

  OggContainerEncoder(AudioEncoder &encoder) { setEncoder(&encoder); }

  /// Defines the output Stream
  void setOutput(Print &print) override { p_ogg->setOutput(print); }

  /// Provides "audio/pcm"
  const char *mime() override { return mime_pcm; }

  /// We actually do nothing with this
  virtual void setAudioInfo(AudioInfo info) override {
    AudioEncoder::setAudioInfo(info);
    p_ogg->setAudioInfo(info);
    if (p_codec != nullptr) p_codec->setAudioInfo(info);
  }

  virtual bool begin(AudioInfo from) override {
    setAudioInfo(from);
    return begin();
  }

  /// starts the processing using the actual AudioInfo
  virtual bool begin() override {
    TRACED();
    p_ogg->begin();
    if (p_codec==nullptr) return false;
    p_codec->setOutput(*p_ogg);
    return p_codec->begin(p_ogg->audioInfo());
  }

  /// stops the processing
  void end() override {
    TRACED();
    if (p_codec != nullptr) p_codec->end();
    p_ogg->end();
  }

  /// Writes raw data to be encoded and packaged
  virtual size_t write(const uint8_t *data, size_t len) override {
    if (!p_ogg->isOpen() || data == nullptr) return 0;
    LOGD("OggContainerEncoder::write: %d", (int)len);
    size_t result = 0;
    if (p_codec == nullptr) {
      result = p_ogg->write((const uint8_t *)data, len);
    } else {
      result = p_codec->write(data, len);
    }
    return result;
  }

  operator bool() override { return p_ogg->isOpen(); }

  bool isOpen() { return p_ogg->isOpen(); }

 protected:
  AudioEncoder *p_codec = nullptr;
  OggContainerOutput ogg;
  OggContainerOutput *p_ogg = &ogg;

  void setEncoder(AudioEncoder *enc) { p_codec = enc; }

  /// Replace the ogg output class
  void setOggOutput(OggContainerOutput *out) { p_ogg = out; }
};

}  // namespace audio_tools
