//-------------------------------------------------------------------------
/*
Copyright (C) 2010 EDuke32 developers and contributors

This file is part of EDuke32.

EDuke32 is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 2
as published by the Free Software Foundation.

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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*/
//-------------------------------------------------------------------------

/*
 * A reimplementation of Jim Dose's FX_MAN routines, using  SDL_mixer 1.2.
 *   Whee. FX_MAN is also known as the "Apogee Sound System", or "ASS" for
 *   short. How strangely appropriate that seems.
 */

#define _NEED_SDLMIXER	1

#include <stdio.h>
#include <errno.h>

#if defined __APPLE__ && defined __BIG_ENDIAN__
// is* hacks for ppc...
# include "compat.h"
#endif

#include "duke3d.h"
#include "cache1d.h"

#include "sdl_inc.h"
#include "music.h"

#if !defined _WIN32 && !defined(GEKKO)
//# define FORK_EXEC_MIDI 1
#endif

#if defined FORK_EXEC_MIDI  // fork/exec based external midi player
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>
static char **external_midi_argv;
static pid_t external_midi_pid=-1;
static int8_t external_midi_restart=0;
#endif
static char *external_midi_tempfn = "/tmp/eduke32-music.mid";
static int32_t external_midi = 0;

int32_t MUSIC_ErrorCode = MUSIC_Ok;

static char warningMessage[80];
static char errorMessage[80];

static int32_t music_initialized = 0;
static int32_t music_context = 0;
static int32_t music_loopflag = MUSIC_PlayOnce;
static Mix_Music *music_musicchunk = NULL;

static void setErrorMessage(const char *msg)
{
    Bstrncpyz(errorMessage, msg, sizeof(errorMessage));
}

// The music functions...

const char *MUSIC_ErrorString(int32_t ErrorNumber)
{
    switch (ErrorNumber)
    {
    case MUSIC_Warning:
        return(warningMessage);

    case MUSIC_Error:
        return(errorMessage);

    case MUSIC_Ok:
        return("OK; no error.");

    case MUSIC_ASSVersion:
        return("Incorrect sound library version.");

    case MUSIC_SoundCardError:
        return("General sound card error.");

    case MUSIC_InvalidCard:
        return("Invalid sound card.");

    case MUSIC_MidiError:
        return("MIDI error.");

    case MUSIC_MPU401Error:
        return("MPU401 error.");

    case MUSIC_TaskManError:
        return("Task Manager error.");

        //case MUSIC_FMNotDetected:
        //    return("FM not detected error.");

    case MUSIC_DPMI_Error:
        return("DPMI error.");

    default:
        return("Unknown error.");
    } // switch

    return(NULL);
} // MUSIC_ErrorString

int32_t MUSIC_Init(int32_t SoundCard, int32_t Address)
{
    // Use an external MIDI player if the user has specified to do so
    char *command = getenv("EDUKE32_MUSIC_CMD");
    const SDL_version *linked = Mix_Linked_Version();

    UNREFERENCED_PARAMETER(SoundCard);
    UNREFERENCED_PARAMETER(Address);

    if (music_initialized)
    {
        setErrorMessage("Music system is already initialized.");
        return(MUSIC_Error);
    } // if

    if (SDL_VERSIONNUM(linked->major,linked->minor,linked->patch) < MIX_REQUIREDVERSION)
    {
        // reject running with SDL_Mixer versions older than what is stated in sdl_inc.h
        initprintf("You need at least v%d.%d.%d of SDL_mixer for music\n",SDL_MIXER_MIN_X,SDL_MIXER_MIN_Y,SDL_MIXER_MIN_Z);
        return(MUSIC_Error);
    }

    external_midi = (command != NULL && command[0] != 0);

    if (external_midi)
    {
#if defined FORK_EXEC_MIDI
        int32_t ws=1, numargs=0, pagesize=sysconf(_SC_PAGE_SIZE);
        char *c, *cmd;
        size_t sz;
#endif

        initprintf("Setting music command to \"%s\".\n", command);

#if !defined FORK_EXEC_MIDI
        if (Mix_SetMusicCMD(command)==-1)
        {
            perror("Mix_SetMusicCMD");
            goto fallback;
        }
#else

        if (pagesize==-1)
            goto fallback;

        for (c=command; *c; c++)
        {
            if (isspace(*c))
                ws = 1;
            else if (ws)
            {
                ws = 0;
                numargs++;
            }
        }

        if (numargs==0)
            goto fallback;

        sz = (numargs+2)*sizeof(char *) + (c-command+1);
        sz = ((sz+pagesize-1)/pagesize)*pagesize;
#if defined(__APPLE__) || defined(__ANDROID__)
        external_midi_argv = Bcalloc(1,sz+pagesize);
        if (!external_midi_argv)
            goto fallback;
        external_midi_argv = (char **)((intptr_t)external_midi_argv + (pagesize-(((intptr_t)external_midi_argv)&(pagesize-1))));
#else
        if (posix_memalign((void **)&external_midi_argv, pagesize, sz))
            goto fallback;
#endif
        cmd = (char *)external_midi_argv + (numargs+2)*sizeof(char *);
        Bmemcpy(cmd, command, c-command+1);

        ws = 1;
        numargs = 0;
        for (c=cmd; *c; c++)
        {
            if (isspace(*c))
            {
                ws = 1;
                *c = 0;
            }
            else if (ws)
            {
                ws = 0;
                external_midi_argv[numargs++] = c;
            }
        }
        external_midi_argv[numargs] = external_midi_tempfn;
        external_midi_argv[numargs+1] = NULL;

        if (mprotect(external_midi_argv, sz, PROT_READ)==-1)  // make argv and command string read-only
        {
            perror("MUSIC_Init: mprotect");
            goto fallback;
        }
# if 0
        {
            int i;
            initprintf("----Music argv:\n");
            for (i=0; i<numargs+1; i++)
                initprintf("  %s\n", external_midi_argv[i]);
            initprintf("----\n");
        }
# endif
#endif
        music_initialized = 1;
        return(MUSIC_Ok);

fallback:
        initprintf("Error setting music command, falling back to timidity.\n");
    }

    {
        static const char *s[] = { "/etc/timidity.cfg", "/etc/timidity/timidity.cfg", "/etc/timidity/freepats.cfg" };
        FILE *fp;
        int32_t i;

        for (i = (sizeof(s)/sizeof(s[0]))-1; i>=0; i--)
        {
            fp = Bfopen(s[i], "r");
            if (fp == NULL)
            {
                if (i == 0)
                {
                    initprintf("Error: couldn't open any of the following files:\n");
                    for (i = (sizeof(s)/sizeof(s[0]))-1; i>=0; i--)
                        initprintf("%s\n",s[i]);
                    return(MUSIC_Error);
                }
                continue;
            }
            else break;
        }
        Bfclose(fp);
    }

    music_initialized = 1;
    return(MUSIC_Ok);
} // MUSIC_Init


int32_t MUSIC_Shutdown(void)
{
    // TODO - make sure this is being called from the menu -- SA
#if !defined FORK_EXEC_MIDI
    if (external_midi)
        Mix_SetMusicCMD(NULL);
#endif

    MUSIC_StopSong();
    music_context = 0;
    music_initialized = 0;
    music_loopflag = MUSIC_PlayOnce;

    return(MUSIC_Ok);
} // MUSIC_Shutdown


void MUSIC_SetMaxFMMidiChannel(int32_t channel)
{
    UNREFERENCED_PARAMETER(channel);
} // MUSIC_SetMaxFMMidiChannel


void MUSIC_SetVolume(int32_t volume)
{
    volume = max(0, volume);
    volume = min(volume, 255);

    Mix_VolumeMusic(volume >> 1);  // convert 0-255 to 0-128.
} // MUSIC_SetVolume


void MUSIC_SetMidiChannelVolume(int32_t channel, int32_t volume)
{
    UNREFERENCED_PARAMETER(channel);
    UNREFERENCED_PARAMETER(volume);
} // MUSIC_SetMidiChannelVolume


void MUSIC_ResetMidiChannelVolumes(void)
{
} // MUSIC_ResetMidiChannelVolumes


int32_t MUSIC_GetVolume(void)
{
    return(Mix_VolumeMusic(-1) << 1);  // convert 0-128 to 0-255.
} // MUSIC_GetVolume


void MUSIC_SetLoopFlag(int32_t loopflag)
{
    music_loopflag = loopflag;
} // MUSIC_SetLoopFlag


int32_t MUSIC_SongPlaying(void)
{
    return((Mix_PlayingMusic()) ? TRUE : FALSE);
} // MUSIC_SongPlaying


void MUSIC_Continue(void)
{
    if (Mix_PausedMusic())
        Mix_ResumeMusic();
} // MUSIC_Continue


void MUSIC_Pause(void)
{
    Mix_PauseMusic();
} // MUSIC_Pause

int32_t MUSIC_StopSong(void)
{
#if defined FORK_EXEC_MIDI
    if (external_midi)
    {
        if (external_midi_pid > 0)
        {
            int32_t ret;
            struct timespec ts;

            external_midi_restart = 0;  // make SIGCHLD handler a no-op

            ts.tv_sec = 0;
            ts.tv_nsec = 5000000;  // sleep 5ms at most

            kill(external_midi_pid, SIGTERM);
            nanosleep(&ts, NULL);
            ret = waitpid(external_midi_pid, NULL, WNOHANG|WUNTRACED);
//            printf("(%d)", ret);

            if (ret != external_midi_pid)
            {
                if (ret==-1)
                    perror("waitpid");
                else
                {
                    // we tried to be nice, but no...
                    kill(external_midi_pid, SIGKILL);
                    initprintf("%s: wait for SIGTERM timed out.\n", __func__);
                    if (waitpid(external_midi_pid, NULL, WUNTRACED)==-1)
                        perror("waitpid (2)");
                }
            }

            external_midi_pid = -1;
        }

        return(MUSIC_Ok);
    }
#endif

    //if (!fx_initialized)
    if (!Mix_QuerySpec(NULL, NULL, NULL))
    {
        setErrorMessage("Need FX system initialized, too. Sorry.");
        return(MUSIC_Error);
    } // if

    if ((Mix_PlayingMusic()) || (Mix_PausedMusic()))
        Mix_HaltMusic();

    if (music_musicchunk)
        Mix_FreeMusic(music_musicchunk);

    music_musicchunk = NULL;

    return(MUSIC_Ok);
} // MUSIC_StopSong

#if defined FORK_EXEC_MIDI
static void playmusic()
{
    pid_t pid = vfork();

    if (pid==-1)  // error
    {
        initprintf("%s: vfork: %s\n", __func__, strerror(errno));
    }
    else if (pid==0)  // child
    {
        // exec without PATH lookup
        if (execv(external_midi_argv[0], external_midi_argv) < 0)
        {
            perror("execv");
            _exit(1);
        }
    }
    else  // parent
    {
        external_midi_pid = pid;
    }
}

static void sigchld_handler(int signo)
{
    if (signo==SIGCHLD && external_midi_restart)
    {
        int status;

        if (external_midi_pid > 0)
        {
            if (waitpid(external_midi_pid, &status, WUNTRACED)==-1)
                perror("waitpid (3)");

            if (WIFEXITED(status) && WEXITSTATUS(status)==0)
            {
                // loop ...
                playmusic();
            }
        }
    }
}
#endif

// Duke3D-specific.  --ryan.
// void MUSIC_PlayMusic(char *_filename)
int32_t MUSIC_PlaySong(char *song, int32_t loopflag)
{
    MUSIC_StopSong();

    if (external_midi)
    {
        FILE *fp;

#if defined FORK_EXEC_MIDI
        static int32_t sigchld_handler_set = 0;

        if (!sigchld_handler_set)
        {
            struct sigaction sa;
            sa.sa_handler=sigchld_handler;
            sa.sa_flags=0;
            sigemptyset(&sa.sa_mask);

            if (sigaction(SIGCHLD, &sa, NULL)==-1)
                initprintf("%s: sigaction: %s\n", __func__, strerror(errno));

            sigchld_handler_set = 1;
        }
#endif

        fp = Bfopen(external_midi_tempfn, "wb");
        if (fp)
        {
            fwrite(song, 1, g_musicSize, fp);
            Bfclose(fp);

#if defined FORK_EXEC_MIDI
            external_midi_restart = loopflag;
            playmusic();
#else
            music_musicchunk = Mix_LoadMUS(external_midi_tempfn);
            if (!music_musicchunk)
                initprintf("Mix_LoadMUS: %s\n", Mix_GetError());
#endif
        }
        else initprintf("%s: fopen: %s\n", __func__, strerror(errno));
    }
    else
        music_musicchunk = Mix_LoadMUS_RW(SDL_RWFromMem((char *) song, g_musicSize)
#if (SDL_MAJOR_VERSION > 1)
            , SDL_FALSE
#endif
            );

    if (music_musicchunk != NULL)
        if (Mix_PlayMusic(music_musicchunk, (loopflag == MUSIC_LoopSong)?-1:0) == -1)
            initprintf("Mix_PlayMusic: %s\n", Mix_GetError());

    return MUSIC_Ok;
}


void MUSIC_SetContext(int32_t context)
{
    music_context = context;
} // MUSIC_SetContext


int32_t MUSIC_GetContext(void)
{
    return(music_context);
} // MUSIC_GetContext


void MUSIC_SetSongTick(uint32_t PositionInTicks)
{
    UNREFERENCED_PARAMETER(PositionInTicks);
} // MUSIC_SetSongTick


void MUSIC_SetSongTime(uint32_t milliseconds)
{
    UNREFERENCED_PARAMETER(milliseconds);
}// MUSIC_SetSongTime


void MUSIC_SetSongPosition(int32_t measure, int32_t beat, int32_t tick)
{
    UNREFERENCED_PARAMETER(measure);
    UNREFERENCED_PARAMETER(beat);
    UNREFERENCED_PARAMETER(tick);
} // MUSIC_SetSongPosition


void MUSIC_GetSongPosition(songposition *pos)
{
    UNREFERENCED_PARAMETER(pos);
} // MUSIC_GetSongPosition


void MUSIC_GetSongLength(songposition *pos)
{
    UNREFERENCED_PARAMETER(pos);
} // MUSIC_GetSongLength


int32_t MUSIC_FadeVolume(int32_t tovolume, int32_t milliseconds)
{
    UNREFERENCED_PARAMETER(tovolume);
    Mix_FadeOutMusic(milliseconds);
    return(MUSIC_Ok);
} // MUSIC_FadeVolume


int32_t MUSIC_FadeActive(void)
{
    return((Mix_FadingMusic() == MIX_FADING_OUT) ? TRUE : FALSE);
} // MUSIC_FadeActive


void MUSIC_StopFade(void)
{
} // MUSIC_StopFade


void MUSIC_RerouteMidiChannel(int32_t channel, int32_t (*function)(int32_t, int32_t, int32_t))
{
    UNREFERENCED_PARAMETER(channel);
    UNREFERENCED_PARAMETER(function);
} // MUSIC_RerouteMidiChannel


void MUSIC_RegisterTimbreBank(char *timbres)
{
    UNREFERENCED_PARAMETER(timbres);
} // MUSIC_RegisterTimbreBank


void MUSIC_Update(void)
{}
