#include "ogg.hpp"

#include "../debug/Logger.hpp"
#include "../audio/audio.hpp"
#include "../typedefs.hpp"

#include <string>
#include <vorbis/codec.h>
#include <vorbis/vorbisfile.h>

static debug::Logger logger("ogg");

namespace fs = std::filesystem;
using namespace audio;

static inline std::string vorbis_error_message(int code) {
    switch (code) {
        case 0: return "no error";
        case OV_EREAD: return "a read from media returned an error";
        case OV_ENOTVORBIS: return "the given file/data was not recognized as Ogg Vorbis data";
        case OV_EVERSION: return "vorbis version mismatch";
        case OV_EBADHEADER: return "invalid Vorbis bitstream header";
        case OV_EFAULT: return "internal logic fault";
        case OV_EINVAL: return "invalid read operation";
        case OV_EBADLINK: 
            return "the given link exists in the Vorbis data stream,"
                   " but is not decipherable due to garbacge or corruption";
        case OV_ENOSEEK:
            return "the given stream is not seekable";
        case OV_EIMPL:
            return "feature not implemented";
        default:
            return "unknown error ["+std::to_string(code)+"]";
    }
}

std::unique_ptr<audio::PCM> ogg::load_pcm(const fs::path& file, bool headerOnly) {
    OggVorbis_File vf;
    int code;
    if ((code = ov_fopen(file.u8string().c_str(), &vf))) {
        throw std::runtime_error("vorbis: "+vorbis_error_message(code));
    }
    std::vector<char> data;

    vorbis_info* info = ov_info(&vf, -1);
    uint channels = info->channels;
    uint sampleRate = info->rate;
    bool seekable = ov_seekable(&vf);
    size_t totalSamples = seekable ? ov_pcm_total(&vf, -1) : 0;

    if (!headerOnly) {
        const int bufferSize = 4096;
        int section = 0;
        char buffer[bufferSize];

        bool eof = false;
        while (!eof) {
            long ret = ov_read(&vf, buffer, bufferSize, 0, 2, true, &section);
            if (ret == 0) {
                eof = true;
            } else if (ret < 0) {
                logger.error() << "ogg::load_pcm: " << vorbis_error_message(ret);
            } else {
                data.insert(data.end(), std::begin(buffer), std::begin(buffer)+ret);
            }
        }
        totalSamples = data.size() / channels / 2;
    }
    ov_clear(&vf);
    return std::make_unique<PCM>(
        std::move(data), totalSamples, channels, 16, sampleRate, seekable
    );
}

class OggStream : public PCMStream {
    OggVorbis_File vf;
    bool closed = false;
    uint channels;
    uint sampleRate;
    size_t totalSamples = 0;
    bool seekable;
public:
    OggStream(OggVorbis_File vf) : vf(vf) {
        vorbis_info* info = ov_info(&vf, -1);
        channels = info->channels;
        sampleRate = info->rate;
        seekable = ov_seekable(&vf);
        if (seekable) {
            totalSamples = ov_pcm_total(&vf, -1);
        }
    }

    ~OggStream() {
        if (!closed) {
            close();
        }
    }

    size_t read(char* buffer, size_t bufferSize) override {
        if (closed) {
            return 0;
        }
        int bitstream = 0;
        long bytes = ov_read(&vf, buffer, bufferSize, 0, 2, true, &bitstream);
        if (bytes < 0) {
            logger.error() << "ogg::load_pcm: " << vorbis_error_message(bytes) << " " << bytes;
            return PCMStream::ERROR;
        }
        return bytes;
    }

    void close() override {
        if (!closed) {
            ov_clear(&vf);
            closed = true;
        }
    }

    bool isOpen() const override {
        return !closed;
    }

    size_t getTotalSamples() const override {
        return totalSamples;
    }

    duration_t getTotalDuration() const override {
        return static_cast<duration_t>(totalSamples) /
               static_cast<duration_t>(sampleRate);
    }

    uint getChannels() const override {
        return channels;
    }

    uint getSampleRate() const override {
        return sampleRate;
    }

    uint getBitsPerSample() const override {
        return 16;
    }

    bool isSeekable() const override {
        return seekable;
    }

    void seek(size_t position) override {
        if (!closed && seekable) {
            ov_pcm_seek(&vf, position);
        }
    }
};

std::unique_ptr<PCMStream> ogg::create_stream(const fs::path& file) {
    OggVorbis_File vf;
    int code;
    if ((code = ov_fopen(file.u8string().c_str(), &vf))) {
        throw std::runtime_error("vorbis: "+vorbis_error_message(code));
    }
    return std::make_unique<OggStream>(vf);
}
