/*
 * ____ DAPHNE COPYRIGHT NOTICE ____
 *
 * Copyright (C) 2001 Matt Ownby
 *
 * This file is part of DAPHNE, a laserdisc arcade game emulator
 *
 * DAPHNE 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.
 *
 * DAPHNE 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

// LDP-VLDP-AUDIO.CPP
// by Matt Ownby

// handles the audio portion of VLDP (using Ogg Vorbis)

#ifdef UNIX
//#define TRY_MMAP 1	// NOTE : this seems to fail on read-only filesystems
//(such as NTFS mounted from linux)
#endif

#include "ldp-vldp.h"
#include "../io/conout.h"
#include "../io/mpo_fileio.h"
#include "../sound/sound.h"
#include "../timer/timer.h"
#include <plog/Log.h>

#ifdef DEBUG
#include <assert.h> // this may include an extra .DLL in windows that I don't want to rely on
#endif

#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>

#include <vorbis/codec.h> // OGG VORBIS specific headers
#include <vorbis/vorbisfile.h>

#ifdef TRY_MMAP
#include <sys/mman.h>
#endif

// how much uncompressed audio we deal with at a time
#define AUDIO_BUF_CHUNK 4096

// Macros to lock and unlock the mutex for the audio to make sure we aren't
// playing audio while
// we are loading or seeking
#define OGG_LOCK SDL_mutexP(g_ogg_mutex)
#define OGG_UNLOCK SDL_mutexV(g_ogg_mutex)

/////////////////////////////////////////

typedef void *(*audiocopyproc)(void *dest, const void *src, size_t bytes_to_copy);
audiocopyproc paudiocopy = memcpy; // pointer to the audio copy procedure
                                   // (defaults to memcpy)

SDL_mutex *g_ogg_mutex   = NULL;
mpo_io *g_pIOAudioHandle = NULL;
OggVorbis_File s_ogg;

Uint32 g_audio_filesize = 0;     // total size of the audio stream
Uint32 g_audio_filepos  = 0;     // the position in the file of our audio stream
Uint8 *g_big_buf        = NULL;  // holds entire Ogg stream in RAM :)
bool g_audio_ready      = false; // whether audio is ready to be parsed
bool g_audio_playing    = false; // whether the audio is to be playing or not
Uint32 g_playing_timer  = 0;     // the time at which we began playing audio
Uint32 g_samples_played = 0; // how many samples have played since we've been
                             // timing
bool g_audio_left_muted  = false; // left audio channel enabled
bool g_audio_right_muted = false; // right audio channel enabled
string m_oggpath         = "";

#ifdef AUDIO_DEBUG
Uint64 g_u64CallbackByteCount      = 0;
unsigned int g_uCallbackFloodTimer = 0;
unsigned int g_uCallbackDbgTimer   = 0;
#endif

///////////////////////////////////////////////////////////////////////////////////

// resets mm states
void mmreset()
{
    g_audio_filepos = 0; // rewind back to position #0
}

// replaces fread
size_t mmread(void *ptr, size_t size, size_t nmemb, void *datasource)
{
    size_t bytes_to_read = size * nmemb; // how many bytes to be read
    // where to get the data from
    Uint8 *src = ((Uint8 *)datasource) + g_audio_filepos;

    //	printf("mmread being called.. size is %d, nmemb is %d, bytes_to_read is
    //%d\n", size, nmemb, bytes_to_read);

    if (g_audio_filepos + bytes_to_read > g_audio_filesize) {
        bytes_to_read = 0;
        if (g_audio_filepos < g_audio_filesize) {
            bytes_to_read = g_audio_filesize - g_audio_filepos;
        }
    }

    if (bytes_to_read != 0) {
        memcpy(ptr, src, bytes_to_read); // copy the memory
        g_audio_filepos += bytes_to_read;
    }

    return (bytes_to_read);
}

#ifdef WIN32
#define int64_t __int64
#endif

int mmseek(void *datasource, int64_t offset, int whence)
{
    int result = -1;

    //	printf("mmseek being called, whence is %d\n", whence);

    // just to get rid of warnings
    if (datasource) {
    }

    switch (whence) {
    case SEEK_SET:
        // bug fix by Arnaud Gibert
        if (offset <= g_audio_filesize) {
            // make sure offset is positive so we don't get into trouble
            if (offset >= 0) {
                g_audio_filepos = (Uint32)offset;
            } else {
                LOGW << "SEEK_SET used with a negative offset!";
            }
            result = 0;
        }
        break;
    case SEEK_CUR:
        if (offset + g_audio_filepos <= g_audio_filesize) {
            g_audio_filepos = (unsigned int)(g_audio_filepos + offset);
            result          = 0;
        }
        break;
    case SEEK_END:
        //		printf("SEEK_END being called, offset is %x, whence is %d!\n",
        //(Uint32) offset, whence);
        if (g_audio_filesize + offset <= g_audio_filesize) {
            g_audio_filepos = (unsigned int)(g_audio_filesize + offset);
            result          = 0;
        }
        break;
    }

    return result;
}

int mmclose(void *datasource)
{
    // safety check
    if (datasource != g_big_buf) {
        LOGE << "datasource != g_bigbuf, this should never happen!";
    }

#ifdef TRY_MMAP
    LOGD << "Unmapping audio stream from memory ...";
    munmap(g_big_buf, g_audio_filesize);
    datasource = NULL;
#else
    LOGD << "Freeing memory used to store audio stream...";
    // free(datasource);
    delete[] g_big_buf;
    g_big_buf = NULL;
#endif

    mpo_close(g_pIOAudioHandle);
    g_pIOAudioHandle = NULL;

    return 0;
}

long mmtell(void *datasource)
{
    //	printf("mmtell being called, filepos is %x\n", (Uint32)
    // g_audio_filepos);

    if (datasource) {
    }

    return g_audio_filepos;
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// public audio stuff

void ldp_vldp::enable_audio1()
{
    g_audio_left_muted = false;
    set_audiocopy_callback();
}

void ldp_vldp::enable_audio2()
{
    g_audio_right_muted = false;
    set_audiocopy_callback();
}

void ldp_vldp::disable_audio1()
{
    g_audio_left_muted = true;
    set_audiocopy_callback();
}

void ldp_vldp::disable_audio2()
{
    g_audio_right_muted = true;
    set_audiocopy_callback();
}

///////////////////////////////////////////////////////////////////////////////////////////////

// mute audio data
void *audiocopy_mute(void *dest, const void *src, size_t bytes_to_copy)
{
#ifdef WIN32
    ZeroMemory(dest, bytes_to_copy);
#else
    bzero(dest, bytes_to_copy);
#endif
    return NULL;
}

// copies left-channel audio data to right-channel
void *audiocopy_left_only(void *dest, const void *src, size_t bytes_to_copy)
{
#ifdef DEBUG
    assert(bytes_to_copy % 4 == 0); // stereo 16-bit audio should always be
                                    // divisible by 4
#endif
    Uint32 *dst32   = (Uint32 *)dest;
    Uint16 *src16_L = (Uint16 *)src; // point to first bit of left-channel data

    bytes_to_copy >>= 2; // divide by 4 because we will be advancing 4 bytes per
                         // iteration
    for (unsigned int index = 0; index < bytes_to_copy; index++) {
        *dst32 = (*src16_L << 16) | *src16_L; // we only care about left-channel
        dst32++;
        src16_L += 2; // skip over right channel data
    }
    return NULL; // for compatiblity reasons, we don't use this value
}

// copies right-channel audio data to left-channel
void *audiocopy_right_only(void *dest, const void *src, size_t bytes_to_copy)
{
#ifdef DEBUG
    assert(bytes_to_copy % 4 == 0); // stereo 16-bit audio should always be
                                    // divisible by 4
#endif

    Uint32 *dst32   = (Uint32 *)dest;
    Uint16 *src16_R = (Uint16 *)src + 1; // point to the first bit of data that
                                         // occurs on the right channel

    bytes_to_copy >>= 2; // divide by 4 because we will be advancing 4 bytes per
                         // iteration
    for (unsigned int index = 0; index < bytes_to_copy; index++) {
        // fastest method (third attempt)
        *dst32 = (*src16_R << 16) | *src16_R; // we only care about the
                                              // right-channel
        dst32++;
        src16_R += 2; // skip over left channel data
    }
    return NULL;
}

// sets the audio copy callbackto be used, based on which audio channels are
// muted
// this should only be called when the mute status of the channels is changed
void ldp_vldp::set_audiocopy_callback()
{
    if (g_audio_left_muted) {
        if (g_audio_right_muted)
            paudiocopy = audiocopy_mute; // both channels are muted
        else
            paudiocopy = audiocopy_right_only; // only right channel is on
    } else {
        if (g_audio_right_muted)
            paudiocopy = audiocopy_left_only; // only left channel is on
        else
            paudiocopy = (audiocopyproc)memcpy; // default, both channels on
    }
}

// Allow ogg file switching via LUA API based on suffix (ala -altaudio)
bool ldp_vldp::switch_altaudio(const char* suffix)
{
    string oggfile = m_oggpath;
    oggfile.replace(oggfile.length() - 4, 4, suffix);
    oggfile += ".ogg";

    m_altaudio_suffix = suffix;
    audio_pause();

    if (open_audio_stream(oggfile.c_str())) {
        if (get_status() == LDP_PLAYING) {
            audio_play(0);
            pre_change_speed(1, 1);
        }
        return true;
    }

    return false;
}

// changes file extension of m2vpath so it ends in .ogg (and add suffix for alt
// soundtrack if needed)
void ldp_vldp::oggize_path(string &oggpath, string m2vpath)
{
    oggpath = m_oggpath = m2vpath;
    oggpath.replace(oggpath.length() - 4, 4,
                    m_altaudio_suffix); // append optional alternate audio
                                        // suffix (if this string is blank it
                                        // should be ok)
    oggpath += ".ogg";
}

// initializes VLDP audio, returns 1 on success or 0 on failure
bool ldp_vldp::audio_init()
{
    bool result = false;

#ifdef AUDIO_DEBUG
    g_u64CallbackByteCount = 0;
    g_uCallbackFloodTimer  = 0;
    g_uCallbackDbgTimer    = GET_TICKS();
#endif

    // create a mutex to prevent threads from interfering
    g_ogg_mutex = SDL_CreateMutex();
    if (g_ogg_mutex) {
        result = true;
    }

    return result;
}

// shuts down VLDP audio
void ldp_vldp::audio_shutdown()
{
    // if we have an audio file still open, close it
    if (g_pIOAudioHandle != 0) {
        close_audio_stream();
    }

    // if we successfully created a mutex previously, then destroy it now
    if (g_ogg_mutex) {
        SDL_DestroyMutex(g_ogg_mutex);
        g_ogg_mutex = NULL;
    }
}

void ldp_vldp::close_audio_stream()
{
    OGG_LOCK;

    g_audio_ready   = false;
    g_audio_playing = false;
    ov_clear(&s_ogg);

    OGG_UNLOCK;
}

bool ldp_vldp::open_audio_stream(const string &strFilename)
{
    bool result              = false;
    ov_callbacks mycallbacks = {mmread, mmseek, mmclose, mmtell};

    OGG_LOCK; // can't have audio callback running during this

    // if an audio stream is already open, close it first
    if (g_pIOAudioHandle != 0) {
        close_audio_stream();
    }

    mmreset(); // reset the mm wrappers for new use

    g_pIOAudioHandle = mpo_open((m_mpeg_path + strFilename).c_str(), MPO_OPEN_READONLY);
    // if audio file was opened successfully
    if (g_pIOAudioHandle) {
        g_audio_filesize = static_cast<unsigned int>(g_pIOAudioHandle->size & 0xFFFFFFFF);
#ifdef TRY_MMAP
        g_big_buf = (Uint8 *)mmap(NULL, g_audio_filesize, PROT_READ, MAP_PRIVATE,
                                  fileno(g_pIOAudioHandle->handle), 0);
        if (!g_big_buf) {
            LOGE << "mmap failed";
        }
#else
        g_big_buf = new unsigned char[g_audio_filesize];
        if (g_big_buf) {
            mpo_read(g_big_buf, g_audio_filesize, NULL,
                     g_pIOAudioHandle); // read entire stream into RAM
        } else {
            LOGE << "out of memory";
        }
#endif
        if (g_big_buf) {
            int open_result = ov_open_callbacks(g_big_buf, &s_ogg, NULL, 0, mycallbacks);

            // if we opening the .OGG succeeded
            if (open_result == 0) {
                // now check to make sure it's stereo and the proper sample rate
                vorbis_info *info = ov_info(&s_ogg, -1);

                // if they meet the proper specification, let them proceed
                if ((info->channels == 2) && (info->rate == 44100)) {
                    g_audio_ready = true;
                    result        = true;
                } else {
                    LOGE << ".ogg file must have 2 channels and 44100 Hz";
                    LOGE << fmt(".ogg file has %u channel(s) and is %ld Hz",
                                info->channels, info->rate);
                    LOGE << ".ogg file ignored (you won't hear any audio)";
                }
            } else {
                LOGE << fmt("ov_open_callbacks failed! Error code is %d",
                            open_result);
                LOGE << fmt("OV_EREAD=%d OV_ENOTVORBIS=%d OV_EVERSION=%d "
                            "OV_EBADHEADER=%d OV_EFAULT=%d\n",
                            OV_EREAD,
                            OV_ENOTVORBIS,
                            OV_EVERSION,
                            OV_EBADHEADER,
                            OV_EFAULT);
            }
        }
        // else we've already printed error messages, so we don't need to do
        // anything else

        // close file if we got an earlier error
        if (!result) {
            mpo_close(g_pIOAudioHandle);
            g_pIOAudioHandle = NULL;

            // if we have memory allocated, de-allocate it
            if (g_big_buf) {
#ifdef TRY_MMAP
                munmap(g_big_buf, g_audio_filesize);
#else
                delete[] g_big_buf;
#endif
                g_big_buf = NULL;
            }
        }

    } // end if we could open file
    else {
// don't show this message to end-users, a surprising number of them report this
// as a bug and it's really getting annoying :)
        LOGD << "No audio file (" << strFilename <<
            ") was found to go with the opened video file";
    }

    OGG_UNLOCK;

    return result;
}

// seeks to a sample position in the audio stream
// returns true if successful or false if failed
bool ldp_vldp::seek_audio(Uint64 u64Samples)
{
    bool result = false;

    OGG_LOCK; // can't have audio callback running during this

    if (ov_seekable(&s_ogg)) {
        ov_pcm_seek(&s_ogg, u64Samples);
        g_audio_playing = false; // audio should not be playing immediately
                                 // after a seek
        result = true;
    } else {
        LOGE << "DOH! OGG stream is not seekable!";
    }

    OGG_UNLOCK;

    return result;
}

// starts playing the audio
void ldp_vldp::audio_play(Uint32 timer)
{
    OGG_LOCK;
    g_playing_timer  = timer;
    g_samples_played = 0;
    g_audio_playing  = true;
    OGG_UNLOCK;
}

// pauses the audio at the current position
void ldp_vldp::audio_pause()
{
    OGG_LOCK;
    g_audio_playing = false;
    OGG_UNLOCK;
}

////////////////////////////////////////////////////////////////////////////////////////

char g_small_buf[AUDIO_BUF_CHUNK]     = {0};
Uint8 g_leftover_buf[AUDIO_BUF_CHUNK] = {0};
int g_leftover_samples                = 0;

// our audio callback
void ldp_vldp_audio_callback(Uint8 *stream, int len, int unused)
{
#ifdef AUDIO_DEBUG
    g_u64CallbackByteCount += len;
    unsigned int uFloodTimer = (GET_TICKS() - g_uCallbackDbgTimer) / 1000;
    if (uFloodTimer != g_uCallbackFloodTimer) {
        g_uCallbackFloodTimer = uFloodTimer;
        LOGD << fmt("audio callback frequency is: %d", (g_u64CallbackByteCount / uFloodTimer) >> 2);
    }
#endif

    OGG_LOCK; // make sure nothing changes with any ogg stuff while we decode

    // if audio is ready to be read and if it is playing
    if (g_audio_ready && g_audio_playing) {
        bool audio_caught_up = false;
        int loop_count       = 0;

        // normally we only want to go through this loop once
        // The exception is if we are behind, in which case we want to process
        // audio until we're caught up again
        // we don't want to loop endlessly in here if there is a bug, which is
        // why we have a loop count
        while ((!audio_caught_up) && (loop_count++ < 10)) {
            long samples_read      = 0;
            int samples_copied     = 0;
            Uint32 bytes_to_read   = 0;
            Uint32 correct_samples = 0; // how many samples we should have
                                        // played up to this point
            int nop;

            // if we have some samples from last time for the audio stream
            if (g_leftover_samples) {
                if (g_leftover_samples <= len) {
                    paudiocopy(stream, g_leftover_buf, g_leftover_samples);
                    samples_copied += g_leftover_samples;
                    g_leftover_samples = 0;
                } else {
                    paudiocopy(stream, g_leftover_buf, len);
                    memmove(g_leftover_buf, g_leftover_buf + len,
                            g_leftover_samples - len); // shift remaining buf to
                                                       // front
                    // memmove is used because the memory area overlaps
                    samples_copied = len;
                    g_leftover_samples -= len;
                }
            }

            while (samples_copied < len) {
                samples_read =
                    ov_read(&s_ogg, &g_small_buf[0], AUDIO_BUF_CHUNK, 0, 2, 1, &nop);

                if (samples_read > 0) {
                    bytes_to_read = len - samples_copied; // how much space we
                                                          // have left to fill
                    // (samples_copied can and often is 0)

                    // if we have more space to fill than samples available,
                    // then we only want to read
                    // as many samples as we have available
                    if (bytes_to_read > (Uint32)samples_read) {
                        bytes_to_read = samples_read;
                    }
                    // else we have to split the buffer
                    else {
                        g_leftover_samples = samples_read - bytes_to_read;
                        memcpy(g_leftover_buf, g_small_buf + bytes_to_read, g_leftover_samples);
                    }

                    paudiocopy(stream + samples_copied, g_small_buf, bytes_to_read);
                    samples_copied += bytes_to_read;
                } // end if samples were read

                // if we got an error
                else if (samples_read < 0) {
                    LOGE << "Problem reading samples!";
                    g_audio_playing = false;
                    break;
                }

                // else, samples_read == 0 in which case we've come to the end
                // of the stream
                else {
                    LOGW << "End of audio stream detected!";
                    g_audio_playing = false;
                    break;
                }

            } // end while we have not filled the buffer

            // NOW WE CHECK TO SEE IF THE AUDIO IS LAGGING TOO FAR BEHIND
            // IF IT IS, WE NEED TO SKIP FORWARD

            g_samples_played += len; // update stats on how many samples have
                                     // played so we can make sure audio is in
                                     // sync

            // unsigned int cur_time = refresh_ms_time();
            unsigned int cur_time = g_ldp->get_elapsed_ms_since_play();
            // if our timer is set to the current time or some previous time
            if (g_playing_timer < cur_time) {
                // needs to be uint64 to prevent overflow from subsequent math
                static const Uint64 uBYTES_PER_S = sound::FREQ * sound::BYTES_PER_SAMPLE;
                // how many samples should have played 176.4 = 44.1 samples per
                // millisecond * 2 for stereo * 2 for 16-bit
                correct_samples =
                    (unsigned int)((uBYTES_PER_S * (cur_time - g_playing_timer)) / 1000);
            }
            // our timer is set to some time in the future (used with skipping)
            // so we actually should not have played any samples at this point
            else {
                // fprintf(stderr, "LDP-VLDP-AUDIO : Timer is in the future\n");
                correct_samples = 0;
            }

            // if we're ahead instead of behind, don't loop
            if (correct_samples <= g_samples_played) {
                audio_caught_up = true;
            }

            // if we're not too far behind, don't loop
            else if ((Sint32)(correct_samples - g_samples_played) < len) {
                audio_caught_up = true;
            }

            // if we're too far behind, notify the user for testing purposes
            else {
                LOGD << fmt("played %u, expected %u, timer=%u, curtime=%u",
                            g_samples_played,
                            correct_samples,
                            g_playing_timer,
                            g_ldp->get_elapsed_ms_since_play());
                audio_caught_up = false;
                SDL_Delay(0); // don't starve other processes while trying to
                              // catch up
            }
        } // end while we're not caught up

    } // end if audio is playing

    // Either we have no audio file opened OR
    // disc is not playing, pause audio and make sure we don't have any
    // extraneous audio lingering around when
    // we start playing again
    else {
// fill audio stream with silence since it will be expecting to get something
// back from us
#ifdef WIN32
        ZeroMemory(stream, len);
#else
        bzero(stream, len);
#endif

        g_leftover_samples = 0;
    }

    OGG_UNLOCK;
}
