#pragma once

#include <fstream>
#include <string>
#include <stdexcept>
#include <ogg/ogg.h>
#include <mutex>
#include "Concepts/AudioWriterConcept.h"
#include <spdlog/spdlog.h>

namespace snoop {

class OggAudioWriter {
    std::ofstream m_outputFile;
    ogg_stream_state m_oggStream = {};
    bool m_isInWriting = false;
    long m_packetNumber = 0;
    int64_t m_granulePos = 0;
    int m_sampleRate;
    int m_channels;
    std::mutex m_mutex;

public:
    OggAudioWriter( int sampleRate, int channels )
        : m_sampleRate( sampleRate ), m_channels( channels ) {
    }

    ~OggAudioWriter() {
        this->StopWriting();
    }

    void StartWriting( const std::string& filePath ) {
        std::lock_guard l( this->m_mutex );
        StartWritingInternal( filePath );
    }

    void StopWriting() {
        std::lock_guard l( this->m_mutex );
        StopWritingInternal();
    }

    void Write( const char* data, size_t size, size_t frames ) {
        std::lock_guard l( this->m_mutex );
        WriteInternal( data, size, frames );
    }

private:
    void StartWritingInternal( std::string filePath ) {
        if( this->m_isInWriting ) {
            this->StopWritingInternal();
        }
        m_isInWriting = true;

        this->m_packetNumber = 0;
        this->m_granulePos = 0;
        this->m_oggStream = {};
        this->m_outputFile = {};

        ogg_stream_init( &this->m_oggStream, 1 );
        this->m_outputFile.open( filePath, std::ios::binary );
        if( !this->m_outputFile.is_open() ) {
            throw std::runtime_error( "Failed to open file for writing: " + filePath );
        }

        this->WriteOpusHeader();
        this->WriteOpusTags();

        spdlog::info( "Started writing Ogg audio to {}", filePath );
    }

    void StopWritingInternal() {
        if( !m_isInWriting ) {
            return;
        }
        if( this->m_outputFile.is_open() ) {
            ogg_packet packet = {};
            packet.e_o_s = 1;
            packet.granulepos = this->m_granulePos;
            packet.packetno = this->m_packetNumber++;
            ogg_stream_packetin( &m_oggStream, &packet );
            this->WriteOggPages( true );

            this->m_outputFile.close();
            spdlog::info( "Stopped writing Ogg audio" );
        }
        ogg_stream_clear( &this->m_oggStream );
        this->m_isInWriting = false;
    }

    void WriteInternal( const char* data, size_t size, size_t frames ) {
        if( !this->m_isInWriting ) {
            return;
        }

        ogg_packet packet;
        packet.packet = reinterpret_cast<unsigned char*>(const_cast<char*>(data));
        packet.bytes = static_cast<long>(size);
        packet.b_o_s = this->m_packetNumber == 0 ? 1 : 0;
        packet.e_o_s = 0;
        packet.granulepos = this->m_granulePos += static_cast<ssize_t>(frames) * m_channels;
        packet.packetno = this->m_packetNumber++;

        ogg_stream_packetin( &this->m_oggStream, &packet );
        this->WriteOggPages();
    }

    void WriteOpusHeader() {
        static constexpr char magic[ ] = "OpusHead";
        unsigned char header[ 19 ] = { 0 };
        memcpy( header, reinterpret_cast<const void*>(magic), 8 );
        header[ 8 ] = 1; // version
        header[ 9 ] = m_channels;
        *reinterpret_cast<int*>( &header[ 12 ] ) = m_sampleRate;

        this->WriteInternal( reinterpret_cast<const char*>(header), sizeof( header ), 0 );
    }

    void WriteOpusTags() {
        constexpr char tags[ ] = "OpusTags\0\0\0\0\0\0\0\0";
        this->WriteInternal( tags, sizeof( tags ) - 1, 0 );
    }

    void WriteOggPages( bool flush = false ) {
        ogg_page page;
        while( ogg_stream_pageout( &m_oggStream, &page ) ) {
            this->m_outputFile.write( reinterpret_cast<const char*>(page.header), page.header_len );
            this->m_outputFile.write( reinterpret_cast<const char*>(page.body), page.body_len );
        }
        if( flush ) {
            while( ogg_stream_flush( &m_oggStream, &page ) ) {
                this->m_outputFile.write( reinterpret_cast<const char*>(page.header), page.header_len );
                this->m_outputFile.write( reinterpret_cast<const char*>(page.body), page.body_len );
            }
        }
    }
};

static_assert( AudioWriterConcept<OggAudioWriter>, "OggAudioWriter does not satisfy AudioWriterConcept" );

}