#pragma once

#include <Gosu/Audio.hpp>
#include <Gosu/Platform.hpp>
#include <Gosu/Utility.hpp>
#include <sndfile.h>

#ifdef GOSU_IS_WIN
#define NOMINMAX
#include <windows.h>
#endif

namespace Gosu
{
    class SndFile : public AudioFile
    {
        SNDFILE* file;
        SF_INFO info;
        Reader reader;
        Buffer buffer;
        
        // /DELAYLOAD doesn't work with libsndfile.dll (is this still true?); manually lazy-load it.
    #ifdef GOSU_IS_WIN
        static HMODULE dll()
        {
            static HMODULE dll = LoadLibrary(L"libsndfile.dll");
            if (!dll) throw std::runtime_error("Cannot find libsndfile.dll");
            return dll;
        }
        
    #define CREATE_STUB(NAME, RETURN, PARAMS, NAMES)                        \
        static RETURN NAME PARAMS                                           \
        {                                                                   \
            typedef RETURN (__cdecl *NAME##_ptr) PARAMS;                    \
            static NAME##_ptr f = (NAME##_ptr)GetProcAddress(dll(), #NAME); \
            if (!f) {                                                       \
                throw std::runtime_error("Cannot find " ## #NAME);          \
            }                                                               \
            return f NAMES;                                                 \
        }
        CREATE_STUB(sf_open_virtual, SNDFILE*,
            (SF_VIRTUAL_IO* sfvirtual, int mode, SF_INFO* sfinfo, void* user_data),
            (sfvirtual, mode, sfinfo, user_data))
        CREATE_STUB(sf_close, int,
            (SNDFILE* sndfile),
            (sndfile))
        CREATE_STUB(sf_readf_short, sf_count_t,
            (SNDFILE* sndfile, short* ptr, sf_count_t items),
            (sndfile, ptr, items))
        CREATE_STUB(sf_seek, sf_count_t,
            (SNDFILE* sndfile, sf_count_t frames, int whence),
            (sndfile, frames, whence))
        CREATE_STUB(sf_strerror, const char*,
            (SNDFILE* sndfile),
            (sndfile))
    #undef CREATE_STUB
    #endif
        
        static sf_count_t get_filelen(SndFile* self)
        {
            return self->buffer.size();
        }
        
        static sf_count_t seek(sf_count_t offset, int whence, SndFile* self)
        {
            switch (whence) {
            case SEEK_SET:
                self->reader.set_position(offset);
                break;
            case SEEK_CUR:
                self->reader.seek(offset);
                break;
            case SEEK_END:
                self->reader.set_position(self->buffer.size() - offset);
                break;
            }

            if (self->reader.position() > self->buffer.size()) {
                self->reader.set_position(self->buffer.size());
            }
            
            return 0;
        }
        
        static sf_count_t read(void* ptr, sf_count_t count, SndFile* self)
        {
            sf_count_t avail = self->buffer.size() - self->reader.position();
            count = std::min(avail, count);
            self->reader.read(ptr, count);
            return count;
        }
        
        static sf_count_t tell(SndFile* self)
        {
            return self->reader.position();
        }
        
        static SF_VIRTUAL_IO* io_interface()
        {
            static SF_VIRTUAL_IO io;
            io.get_filelen = (sf_vio_get_filelen)&get_filelen;
            io.seek        = (sf_vio_seek)&seek;
            io.read        = (sf_vio_read)&read;
            io.tell        = (sf_vio_tell)&tell;
            io.write       = nullptr;
            return &io;
        }
        
    public:
        SndFile(Reader reader)
        : file(nullptr), reader(buffer.front_reader())
        {
            info.format = 0;
            buffer.resize(reader.resource().size() - reader.position());
            reader.read(buffer.data(), buffer.size());
            file = sf_open_virtual(io_interface(), SFM_READ, &info, this);
            if (!file) {
                throw std::runtime_error(sf_strerror(nullptr));
            }
        }
        
        SndFile(const std::string& filename)
        : file(nullptr), reader(buffer.front_reader())
        {
            info.format = 0;
            // TODO: Not sure if this is still necessary.
            // Can libsndfile open UTF-8 filenames on Windows?
        #ifdef GOSU_IS_WIN
            load_file(buffer, filename);
            file = sf_open_virtual(io_interface(), SFM_READ, &info, this);
        #else
            file = sf_open(filename.c_str(), SFM_READ, &info);
        #endif
            if (!file) {
                throw std::runtime_error(sf_strerror(nullptr));
            }
        }
        
        ~SndFile() override
        {
            if (file) {
                sf_close(file);
            }
        }
        
        ALenum format() const override
        {
            switch (info.channels) {
            case 1:
                return AL_FORMAT_MONO16;
            case 2:
                return AL_FORMAT_STEREO16;
            default:
                throw std::runtime_error("Too many channels in audio file");
            };
        }
        
        ALuint sample_rate() const override
        {
            return info.samplerate;
        }
        
        std::size_t read_data(void* dest, std::size_t length) override
        {
            int frame_size = sizeof(short) * info.channels;
            return sf_readf_short(file, (short*)dest, length / frame_size) * frame_size;
        }
        
        void rewind() override
        {
            sf_seek(file, 0, SEEK_SET);
        }
    };
}