/*
 * Background music handling for Quakespasm (adapted from uHexen2)
 * Handles streaming music as raw sound samples and runs the midi driver
 *
 * Copyright (C) 1999-2005 Id Software, Inc.
 * Copyright (C) 2010-2018 O.Sezer <sezero@users.sourceforge.net>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#include "quakedef.hpp"
#include "snd_codec.hpp"
#include "bgmusic.hpp"

#include <vector>

#define MUSIC_DIRNAME	"music"


#define CDRIP_TYPES	(CODECTYPE_VORBIS | CODECTYPE_MP3 | CODECTYPE_FLAC | CODECTYPE_WAV | CODECTYPE_OPUS)
#define CDRIPTYPE(x)	(((x) & CDRIP_TYPES) != 0)

namespace music {
    bool looping;
    convar extmusic{"bgm_extmusic", "1", {.archive = true}};

    namespace {
        snd_stream_t *stream = nullptr;
        bool no_extmusic = false;
        float old_volume = -1.0f;

        constexpr unsigned int ANY_CODECTYPE = 0xFFFFFFFF;


        enum class player_kind {
            none = -1,
            midi_drv = 1,
            streamer
        };

        struct handler {
            unsigned int type; /* 1U << n (see snd_codec.h)	*/
            player_kind player; /* Enumerated bgm player type	*/
            int is_available; /* -1 means not present		*/
            const char *ext; /* Expected file extension	*/
            const char *dir; /* Where to look for music file */
        };

        std::vector<handler> active_handlers{};

        std::array wanted_handlers =
        {
            handler{
                .type = CODECTYPE_VORBIS, .player = player_kind::streamer, .is_available = -1, .ext = "ogg",
                .dir = MUSIC_DIRNAME
            },
            handler{
                .type = CODECTYPE_OPUS, .player = player_kind::streamer, .is_available = -1, .ext = "opus",
                .dir = MUSIC_DIRNAME
            },
            handler{
                .type = CODECTYPE_MP3, .player = player_kind::streamer, .is_available = -1, .ext = "mp3",
                .dir = MUSIC_DIRNAME
            },
            handler{
                .type = CODECTYPE_FLAC, .player = player_kind::streamer, .is_available = -1, .ext = "flac",
                .dir = MUSIC_DIRNAME
            },
            handler{
                .type = CODECTYPE_WAV, .player = player_kind::streamer, .is_available = -1, .ext = "wav",
                .dir = MUSIC_DIRNAME
            },
            handler{
                .type = CODECTYPE_MOD, .player = player_kind::streamer, .is_available = -1, .ext = "it", .dir = MUSIC_DIRNAME
            },
            handler{
                .type = CODECTYPE_MOD, .player = player_kind::streamer, .is_available = -1, .ext = "s3m",
                .dir = MUSIC_DIRNAME
            },
            handler{
                .type = CODECTYPE_MOD, .player = player_kind::streamer, .is_available = -1, .ext = "xm", .dir = MUSIC_DIRNAME
            },
            handler{
                .type = CODECTYPE_MOD, .player = player_kind::streamer, .is_available = -1, .ext = "mod",
                .dir = MUSIC_DIRNAME
            },
            handler{
                .type = CODECTYPE_UMX, .player = player_kind::streamer, .is_available = -1, .ext = "umx",
                .dir = MUSIC_DIRNAME
            },
            handler{.type = CODECTYPE_NONE, .player = player_kind::none, .is_available = -1, .ext = nullptr, .dir = nullptr}
        };

        void _play() {
            if (command::argc() == 2) {
                play(*command::argv(1));
            } else {
                console::info("music <musicfile>\n");
            }
        }

        void _pause() {
            pause();
        }

        void _resume() {
            resume();
        }

        void _loop() {
            if (command::argc() == 2) {
                if (std::is_eq(common::strcasecmp(command::argv(1).value(), "0")) ||
                    std::is_eq(common::strcasecmp(command::argv(1).value(), "off")))
                    looping = false;
                else if (std::is_eq(common::strcasecmp(command::argv(1).value(), "1")) ||
                         std::is_eq(common::strcasecmp(command::argv(1).value(), "on")))
                    looping = true;
                else if (std::is_eq(common::strcasecmp(command::argv(1).value(), "toggle")))
                    looping = !looping;

                if (stream) stream->loop = looping;
            }

            if (looping)
                console::info("Music will be looped\n");
            else
                console::info("Music will not be looped\n");
        }

        void _stop() {
            stop();
        }

        void _jump() {
            if (command::argc() != 2) {
                console::info("music_jump <ordernum>\n");
            } else if (stream) {
                S_CodecJumpToOrder(stream, static_cast<int>(std::strtol(command::argv(1)->c_str(), nullptr, 10)));
            }
        }

        void play_noext(const std::string &filename, const unsigned int allowed_types) {
            char tmp[MAX_QPATH];

            for (auto &handler: active_handlers) {
                if (!(handler.type & allowed_types)) {
                    continue;
                }
                if (!handler.is_available) {
                    continue;
                }
                q_snprintf(tmp, sizeof(tmp), "%s/%s.%s",
                           handler.dir, filename.c_str(), handler.ext);
                switch (handler.player) {
                    case player_kind::midi_drv:
                        /* not supported in quake */
                        break;
                    case player_kind::streamer:
                        stream = S_CodecOpenStreamType(tmp, handler.type, looping);
                        if (stream)
                            return; /* success */
                        break;
                    case player_kind::none:
                    default:
                        break;
                }
            }

            console::info("Couldn't handle music file %s\n", filename.c_str());
        }

        void update_stream() {
            bool did_rewind = false;
            int res; /* Number of bytes read. */
            int bufferSamples;
            int fileSamples;
            int fileBytes;
            byte raw[16384];

            if (stream->status != STREAM_PLAY) {
                return;
            }

            /* don't bother playing anything if musicvolume is 0 */
            if (bgmvolume.value <= 0) {
                return;
            }

            /* see how many samples should be copied into the raw buffer */
            if (s_rawend < paintedtime) {
                s_rawend = paintedtime;
            }

            while (s_rawend < paintedtime + MAX_RAW_SAMPLES) {
                bufferSamples = MAX_RAW_SAMPLES - (s_rawend - paintedtime);

                /* decide how much data needs to be read from the file */
                fileSamples = bufferSamples * stream->info.rate / shm->speed;
                if (!fileSamples)
                    return;

                /* our max buffer size */
                fileBytes = fileSamples * (stream->info.width * stream->info.channels);
                if (fileBytes > (int) sizeof(raw)) {
                    fileBytes = (int) sizeof(raw);
                    fileSamples = fileBytes /
                                  (stream->info.width * stream->info.channels);
                }

                /* Read */
                res = S_CodecReadStream(stream, fileBytes, raw);
                if (res < fileBytes) {
                    fileBytes = res;
                    fileSamples = res / (stream->info.width * stream->info.channels);
                }

                if (res > 0) /* data: add to raw buffer */
                {
                    S_RawSamples(fileSamples, stream->info.rate,
                                 stream->info.width,
                                 stream->info.channels,
                                 raw, bgmvolume.value);
                    did_rewind = false;
                } else if (res == 0) /* EOF */
                {
                    if (looping) {
                        if (did_rewind) {
                            console::info("Stream keeps returning EOF.\n");
                            stop();
                            return;
                        }

                        res = S_CodecRewindStream(stream);
                        if (res != 0) {
                            console::info("Stream seek error (%i), stopping.\n", res);
                            stop();
                            return;
                        }
                        did_rewind = true;
                    } else {
                        stop();
                        return;
                    }
                } else /* res < 0: some read error */
                {
                    console::info("Stream read error (%i), stopping.\n", res);
                    stop();
                    return;
                }
            }
        }
    }

    void init() {
        extmusic.inscribe();
        command::add("music", _play);
        command::add("music_pause", _pause);
        command::add("music_resume", _resume);
        command::add("music_loop", _loop);
        command::add("music_stop", _stop);
        command::add("music_jump", _jump);

        if (common::check_param("-noextmusic").has_value())
            no_extmusic = true;

        looping = true;

        for (auto i = 0; wanted_handlers[i].type != CODECTYPE_NONE; i++) {
            switch (wanted_handlers[i].player) {
                case player_kind::midi_drv:
                    /* not supported in quake */
                    break;
                case player_kind::streamer:
                    wanted_handlers[i].is_available =
                            S_CodecIsAvailable(wanted_handlers[i].type);
                    break;
                case player_kind::none:
                default:
                    break;
            }
            if (wanted_handlers[i].is_available != -1) {
                active_handlers.push_back(wanted_handlers[i]);
            }
        }
    }

    void shutdown() {
        stop();
        active_handlers.clear();
    }

    void play(const std::string &filename) {
        char tmp[MAX_QPATH];

        stop();

        if (active_handlers.empty())
            return;

        if (filename.empty()) {
            console::debug("null music file name\n");
            return;
        }

        const char *ext = COM_FileGetExtension(filename.c_str());
        if (*ext == '\0') /* try all things */
        {
            play_noext(filename, ANY_CODECTYPE);
            return;
        }

        const handler *chosen = nullptr;
        for (const auto &handler: active_handlers) {
            if (handler.is_available &&
                std::is_eq(common::strcasecmp(ext, handler.ext))) {
                chosen = &handler;
                break;
            }
        }
        if (chosen == nullptr) {
            console::info("Unhandled extension for %s\n", filename.c_str());
            return;
        }
        q_snprintf(tmp, sizeof(tmp), "%s/%s", chosen->dir, filename.c_str());
        switch (chosen->player) {
            case player_kind::midi_drv:
                /* not supported in quake */
                break;
            case player_kind::streamer:
                stream = S_CodecOpenStreamType(tmp, chosen->type, looping);
                if (stream)
                    return; /* success */
                break;
            case player_kind::none:
            default:
                break;
        }

        console::info("Couldn't handle music file %s\n", filename.c_str());
    }

    void stop() {
        if (stream) {
            stream->status = STREAM_NONE;
            S_CodecCloseStream(stream);
            stream = nullptr;
            s_rawend = 0;
        }
    }

    void update() {
        if (old_volume != bgmvolume.value) {
            if (bgmvolume.value < 0)
                bgmvolume.set("0");
            else if (bgmvolume.value > 1)
                bgmvolume.set("1");
            old_volume = bgmvolume.value;
        }
        if (stream)
            update_stream();
    }

    void pause() {
        if (stream) {
            if (stream->status == STREAM_PLAY)
                stream->status = STREAM_PAUSE;
        }
    }

    void resume() {
        if (stream) {
            if (stream->status == STREAM_PAUSE)
                stream->status = STREAM_PLAY;
        }
    }

    void play_cd_track(const byte track, const bool should_loop) {
        /* instead of searching by the order of music_handlers, do so by
         * the order of searchpath priority: the file from the searchpath
         * with the highest path_id is most likely from our own gamedir
         * itself. This way, if a mod has track02 as a *.mp3 file, which
         * is below *.ogg in the music_handler order, the mp3 will still
         * have priority over track02.ogg from, say, id1.
         */
        char tmp[MAX_QPATH];
        unsigned int path_id;

        stop();
        if (CDAudio_Play(track, should_loop) == 0) {
            return; /* success */
        }

        if (active_handlers.empty())
            return; // We obviously can't play anything without handlers.

        if (no_extmusic || extmusic.value == 0.0)
            return;

        unsigned int prev_id = 0;
        unsigned int type = 0;
        const char *ext = nullptr;
        for (const auto handler: active_handlers) {
            if (!handler.is_available) {
                continue;
            }
            if (!CDRIPTYPE(handler.type)) {
                continue;
            }
            q_snprintf(tmp, sizeof(tmp), "%s/track%02d.%s",
                       MUSIC_DIRNAME, static_cast<int>(track), handler.ext);
            if (!COM_FileExists(tmp, &path_id)) {
                continue;
            }
            if (path_id > prev_id) {
                prev_id = path_id;
                type = handler.type;
                ext = handler.ext;
            }
        }

        if (ext == nullptr)
            console::info("Couldn't find a cdrip for track %d\n", static_cast<int>(track));
        else {
            q_snprintf(tmp, sizeof(tmp), "%s/track%02d.%s",
                       MUSIC_DIRNAME, static_cast<int>(track), ext);
            stream = S_CodecOpenStreamType(tmp, type, should_loop);
            if (!stream)
                console::info("Couldn't handle music file %s\n", tmp);
        }
    }
}
