/*****************************************************************************
 * Copyright (c) 2014-2020 OpenRCT2 developers
 *
 * For a complete list of all authors, please refer to contributors.md
 * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
 *
 * OpenRCT2 is licensed under the GNU General Public License version 3.
 *****************************************************************************/

#include "AudioContext.h"
#include "AudioFormat.h"

#include <SDL.h>
#include <algorithm>
#include <openrct2/audio/AudioSource.h>
#include <openrct2/common.h>

namespace OpenRCT2::Audio
{
    /**
     * An audio source where raw PCM data is streamed directly from
     * a file.
     */
    class FileAudioSource final : public ISDLAudioSource
    {
    private:
        AudioFormat _format = {};
        SDL_RWops* _rw = nullptr;
        uint64_t _dataBegin = 0;
        uint64_t _dataLength = 0;

    public:
        ~FileAudioSource() override
        {
            Unload();
        }

        [[nodiscard]] uint64_t GetLength() const override
        {
            return _dataLength;
        }

        [[nodiscard]] AudioFormat GetFormat() const override
        {
            return _format;
        }

        size_t Read(void* dst, uint64_t offset, size_t len) override
        {
            size_t bytesRead = 0;
            int64_t currentPosition = SDL_RWtell(_rw);
            if (currentPosition != -1)
            {
                size_t bytesToRead = static_cast<size_t>(std::min<uint64_t>(len, _dataLength - offset));
                int64_t dataOffset = _dataBegin + offset;
                if (currentPosition != dataOffset)
                {
                    int64_t newPosition = SDL_RWseek(_rw, dataOffset, SEEK_SET);
                    if (newPosition == -1)
                    {
                        return 0;
                    }
                }
                bytesRead = SDL_RWread(_rw, dst, 1, bytesToRead);
            }
            return bytesRead;
        }

        bool LoadWAV(SDL_RWops* rw)
        {
            constexpr uint32_t DATA = 0x61746164;
            constexpr uint32_t FMT = 0x20746D66;
            constexpr uint32_t RIFF = 0x46464952;
            constexpr uint32_t WAVE = 0x45564157;
            constexpr uint16_t pcmformat = 0x0001;

            Unload();

            if (rw == nullptr)
            {
                return false;
            }
            _rw = rw;

            uint32_t chunkId = SDL_ReadLE32(rw);
            if (chunkId != RIFF)
            {
                log_verbose("Not a WAV file");
                return false;
            }

            // Read and discard chunk size
            SDL_ReadLE32(rw);
            uint32_t chunkFormat = SDL_ReadLE32(rw);
            if (chunkFormat != WAVE)
            {
                log_verbose("Not in WAVE format");
                return false;
            }

            uint32_t fmtChunkSize = FindChunk(rw, FMT);
            if (!fmtChunkSize)
            {
                log_verbose("Could not find FMT chunk");
                return false;
            }

            uint64_t chunkStart = SDL_RWtell(rw);

            WaveFormat waveFormat{};
            SDL_RWread(rw, &waveFormat, sizeof(waveFormat), 1);
            SDL_RWseek(rw, chunkStart + fmtChunkSize, RW_SEEK_SET);
            if (waveFormat.encoding != pcmformat)
            {
                log_verbose("Not in proper format");
                return false;
            }

            _format.freq = waveFormat.frequency;
            switch (waveFormat.bitspersample)
            {
                case 8:
                    _format.format = AUDIO_U8;
                    break;
                case 16:
                    _format.format = AUDIO_S16LSB;
                    break;
                default:
                    log_verbose("Invalid bits per sample");
                    return false;
            }
            _format.channels = waveFormat.channels;

            uint32_t dataChunkSize = FindChunk(rw, DATA);
            if (dataChunkSize == 0)
            {
                log_verbose("Could not find DATA chunk");
                return false;
            }

            _dataLength = dataChunkSize;
            _dataBegin = SDL_RWtell(rw);
            return true;
        }

    private:
        static uint32_t FindChunk(SDL_RWops* rw, uint32_t wantedId)
        {
            uint32_t subchunkId = SDL_ReadLE32(rw);
            uint32_t subchunkSize = SDL_ReadLE32(rw);
            if (subchunkId == wantedId)
            {
                return subchunkSize;
            }
            constexpr uint32_t FACT = 0x74636166;
            constexpr uint32_t LIST = 0x5453494c;
            constexpr uint32_t BEXT = 0x74786562;
            constexpr uint32_t JUNK = 0x4B4E554A;
            while (subchunkId == FACT || subchunkId == LIST || subchunkId == BEXT || subchunkId == JUNK)
            {
                SDL_RWseek(rw, subchunkSize, RW_SEEK_CUR);
                subchunkId = SDL_ReadLE32(rw);
                subchunkSize = SDL_ReadLE32(rw);
                if (subchunkId == wantedId)
                {
                    return subchunkSize;
                }
            }
            return 0;
        }

        void Unload()
        {
            if (_rw != nullptr)
            {
                SDL_RWclose(_rw);
                _rw = nullptr;
            }
            _dataBegin = 0;
            _dataLength = 0;
        }
    };

    IAudioSource* AudioSource::CreateStreamFromWAV(const std::string& path)
    {
        IAudioSource* source = nullptr;
        SDL_RWops* rw = SDL_RWFromFile(path.c_str(), "rb");
        if (rw != nullptr)
        {
            return AudioSource::CreateStreamFromWAV(rw);
        }
        return source;
    }

    IAudioSource* AudioSource::CreateStreamFromWAV(SDL_RWops* rw)
    {
        auto source = new FileAudioSource();
        if (!source->LoadWAV(rw))
        {
            delete source;
            source = nullptr;
        }
        return source;
    }

    IAudioSource* AudioSource::CreateStreamFromWAV(std::unique_ptr<IStream> stream)
    {
        auto rw = new SDL_RWops();
        *rw = {};
        rw->type = SDL_RWOPS_UNKNOWN;
        rw->hidden.unknown.data1 = stream.release();
        rw->seek = [](SDL_RWops* ctx, Sint64 offset, int whence) {
            auto ptr = static_cast<IStream*>(ctx->hidden.unknown.data1);
            ptr->Seek(offset, whence);
            return static_cast<Sint64>(ptr->GetPosition());
        };
        rw->read = [](SDL_RWops* ctx, void* buf, size_t size, size_t maxnum) {
            auto ptr = static_cast<IStream*>(ctx->hidden.unknown.data1);
            return static_cast<size_t>(ptr->TryRead(buf, size * maxnum) / size);
        };
        rw->size = [](SDL_RWops* ctx) {
            auto ptr = static_cast<IStream*>(ctx->hidden.unknown.data1);
            return static_cast<Sint64>(ptr->GetLength());
        };
        rw->close = [](SDL_RWops* ctx) {
            auto ptr = static_cast<IStream*>(ctx->hidden.unknown.data1);
            delete ptr;
            ctx->hidden.unknown.data1 = nullptr;
            delete ctx;
            return 0;
        };
        return CreateStreamFromWAV(rw);
    }

} // namespace OpenRCT2::Audio
