/*
 * tngp/media.c
 *
 * Copyright (C) 2022 bzt
 *
 * 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 3 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * @brief Media manager
 *
 */

#include <math.h>
#include "tngp.h"
#include "theora.h"
#include "SDL_mixer.h"
#include "SDL_thread.h"

#define MEDIA_PCM_NOISE_LEVEL   7

/*
 * Mixer channels:
 * 0 - reserved for theora
 * 1 - speech
 * 2..7 - sound effects (assigned by SDL_mixer)
 */

SDL_AudioDeviceID media_recdev = 0;
SDL_AudioSpec media_rec;
Mix_Chunk *media_chimesnd = NULL, *media_selsnd = NULL, *media_clksnd = NULL, *media_errsnd = NULL;
Mix_Chunk **media_snd = NULL, **media_spc = NULL;
Mix_Music *media_music = NULL;
SDL_RWops media_ops = { 0 };
SDL_Thread *media_strmus = NULL;
world_asset_t *media_once = NULL, *media_loop = NULL;
int media_verbose = 0;

/**
 * Get asset size
 */
Sint64 media_rw_size(SDL_RWops *ctx)
{
    world_asset_t *asset = (world_asset_t*)(ctx->hidden.unknown.data1 ? ctx->hidden.unknown.data1 : ctx->hidden.unknown.data2);
    if(!asset) return 0;
    return (Sint64)asset->size;
}

/**
 * Seek in asset
 */
Sint64 media_rw_seek(SDL_RWops *ctx, Sint64 offs, int whence)
{
    world_asset_t *asset = (world_asset_t*)(ctx->hidden.unknown.data1 ? ctx->hidden.unknown.data1 : ctx->hidden.unknown.data2);
    Sint64 newpos;

    if(!asset) return 0;
    switch (whence) {
    case RW_SEEK_SET:
        newpos = asset->offs + offs;
        break;
    case RW_SEEK_CUR:
        newpos = world.tng[asset->tng].offs[2] + offs;
        break;
    case RW_SEEK_END:
        newpos = asset->offs + asset->size + offs;
        break;
    default:
        return SDL_SetError("Unknown value for 'whence'");
    }
    if((uint64_t)newpos < asset->offs) newpos = asset->offs;
    if((uint64_t)newpos > asset->offs + asset->size) newpos = asset->offs + asset->size;
    world_seektng(asset->tng, 2, newpos);
    return newpos;
}

/**
 * Read from asset
 */
size_t media_rw_read(SDL_RWops *ctx, void *ptr, size_t size, size_t cnt)
{
    world_asset_t *asset = (world_asset_t*)(ctx->hidden.unknown.data1 ? ctx->hidden.unknown.data1 : ctx->hidden.unknown.data2);
    size_t total = size * cnt;

    if (!asset || (cnt <= 0) || (size <= 0) || ((total / cnt) != size)) { return 0; }
    if(world.tng[asset->tng].offs[2] + total > asset->offs + asset->size)
        total = asset->offs + asset->size - world.tng[asset->tng].offs[2];
    return world_readtng(asset->tng, 2, ptr, total);
}

/**
 * Load an entire OGG at once from a game file
 */
Mix_Chunk *media_load(world_asset_t *asset)
{
    Mix_Chunk *ret = NULL;
    uint8_t *ptr;

    if(!asset) return NULL;
    ptr = (uint8_t*)main_alloc(asset->size);
    world_seektng(asset->tng, 0, asset->offs);
    if(!world_readtng(asset->tng, 0, ptr, asset->size)) {
        if(verbose > 1) printf("tngp: error reading media\r\n");
        free(ptr);
        return NULL;
    }
    ret = Mix_LoadWAV_RW(SDL_RWFromConstMem(ptr, asset->size), 1);
    free(ptr);
    return ret;
}

/**
 * Recording callback
 */
void media_recordcb(void *ctx, uint8_t *pcm, int len)
{
    uLongf s;
    uint8_t buf[1536];

    (void)ctx;
    (void)buf;
    if(!pcm || len < 1) return;
    for(s = 0; (int)s < len && (int8_t)pcm[s] < MEDIA_PCM_NOISE_LEVEL && (int8_t)pcm[s] > -MEDIA_PCM_NOISE_LEVEL; s++);
    if((int)s >= len) return;
    if(verbose > 2) printf("tngp: got mic data %u bytes\n", len);
    memset(buf, 0, sizeof(buf));
    buf[4] = media_rec.freq == 44100 ? 1 : 0;
    buf[5] = media_rec.freq == AUDIO_S8 ? 0 : (media_rec.format == AUDIO_S16LSB ? 1 : 2);
    /* if PCM data does not fit, compress it */
    if(len > 1024) {
        buf[5] |= 0x10;
        compress2(buf + 6, &s, pcm, len, 9);
        if(s > 1024) {
            /* failsafe, should never needed. If even compressed data doesn't fit, cut into two */
            compress2(buf + 6, &s, pcm, len / 2, 9);
            client_send(13, buf, s + 6);
            compress2(buf + 6, &s, pcm + len / 2, len - len / 2, 9);
        }
        len = s;
    } else
        memcpy(buf + 6, pcm, len);
    client_send(13, buf, len + 6);
}

/**
 * Initialize media manager
 */
void media_init()
{
    SDL_AudioSpec want;

    if(audio) {
        /* load UI sounds that we always need */
        media_chimesnd = Mix_LoadWAV_RW(SDL_RWFromConstMem(binary_chime_ogg, sizeof(binary_chime_ogg)), 1);
        if(world.onselsnd >= 0 && world.onselsnd < world.numsound)
            media_selsnd = media_load(&world.sound[world.onselsnd]);
        if(world.onclksnd >= 0 && world.onclksnd < world.numsound)
            media_clksnd = media_load(&world.sound[world.onclksnd]);
        if(world.onerrsnd >= 0 && world.onerrsnd < world.numsound)
            media_errsnd = media_load(&world.sound[world.onerrsnd]);
        else
            media_errsnd = Mix_LoadWAV_RW(SDL_RWFromConstMem(binary_err_ogg, sizeof(binary_err_ogg)), 1);
        /* load everything else on-demand */
        media_snd = (Mix_Chunk**)main_alloc(world.numsound * sizeof(Mix_Chunk*));
        media_spc = (Mix_Chunk**)main_alloc(world.numspc * sizeof(Mix_Chunk*));
        /* for loading music in the background by SDL_mixer */
        media_ops.size = media_rw_size;
        media_ops.seek = media_rw_seek;
        media_ops.read = media_rw_read;
        /* for voice chat, get a recording device */
        memset(&media_rec, 0, sizeof(media_rec));
        memset(&want, 0, sizeof(want));
        media_recdev = 0;
        if(world.hdr.magic[0] != '#') {
            want.freq = 44100;
            want.format = AUDIO_S8;
            want.channels = 1;
            want.samples = 1024;
            want.callback = media_recordcb;
            media_recdev = SDL_OpenAudioDevice(SDL_GetAudioDeviceName(0, 1), 1, &want, &media_rec, 0);
            if(media_recdev > 0) SDL_PauseAudioDevice(media_recdev, 1);
            if(verbose > 1) printf("tngp: opening microphone %uHz, %s bit, %u channels %s\r\n", media_rec.freq,
                media_rec.format == AUDIO_S8 ? "8" : (media_rec.format == AUDIO_S16LSB ? "16" :
                (media_rec.format == AUDIO_S16MSB ? "16BE" : "???")), media_rec.channels,
                media_recdev > 0 ? "ok" : "failed");

        }
    }
}

/**
 * Open audio for playback
 */
void media_open()
{
    if(audio) {
        Mix_ReserveChannels(2);
        Mix_ChannelFinished(theora_callback);
    }
}

/**
 * Close audio
 */
void media_close()
{
    if(audio) {
        theora_ctx.stop = 1;
        Mix_ChannelFinished(NULL);
        Mix_HaltChannel(0);
        Mix_HaltChannel(1);
        Mix_HaltChannel(-1);
        SDL_Delay(10);
        theora_stop(&theora_ctx);
    }
}

/**
 * Free music
 */
void media_freemus()
{
    Mix_HookMusicFinished(NULL);
    Mix_HaltMusic();
    if(media_music) { Mix_FreeMusic(media_music); media_music = NULL; }
}

/**
 * Free speech resources only (pun intended :-) )
 */
void media_freespeech()
{
    int i;

    if(media_spc)
        for(i = 0; i < world.numspc; i++)
            if(media_spc[i]) { Mix_FreeChunk(media_spc[i]); media_spc[i] = NULL; }
}

/**
 * Free resources
 */
void media_free()
{
    int i;

    theora_ctx.stop = 1;
    if(audio) {
        Mix_ChannelFinished(NULL);
        Mix_HaltChannel(0);
        Mix_HaltChannel(1);
        Mix_HaltChannel(-1);
        SDL_Delay(10);
    }
    theora_stop(&theora_ctx);
    if(media_recdev > 0) { SDL_CloseAudioDevice(media_recdev); media_recdev = 0; }
    if(media_chimesnd) { Mix_FreeChunk(media_chimesnd); media_chimesnd = NULL; }
    if(media_errsnd) { Mix_FreeChunk(media_errsnd); media_errsnd = NULL; }
    if(media_selsnd) { Mix_FreeChunk(media_selsnd); media_selsnd = NULL; }
    if(media_clksnd) { Mix_FreeChunk(media_clksnd); media_clksnd = NULL; }
    if(media_snd) {
        for(i = 0; i < world.numsound; i++)
            if(media_snd[i]) Mix_FreeChunk(media_snd[i]);
        free(media_snd); media_snd = NULL;
    }
    if(media_spc) {
        for(i = 0; i < world.numspc; i++)
            if(media_spc[i]) Mix_FreeChunk(media_spc[i]);
        free(media_spc); media_spc = NULL;
    }
    media_freemus();
}

/**
 * Start recording
 */
void media_mic_start()
{
    if(audio && media_recdev > 0 && tab == TAB_GAME) {
        SDL_PauseAudioDevice(media_recdev, 0);
        if(verbose > 1) printf("tngp: start recording microphone\r\n");
    }
}

/**
 * Stop recording
 */
void media_mic_stop()
{
    if(audio && media_recdev > 0 && SDL_GetAudioDeviceStatus(media_recdev) != SDL_AUDIO_PAUSED) {
        SDL_PauseAudioDevice(media_recdev, 1);
        if(verbose > 1) printf("tngp: stop recording microphone\r\n");
    }
}

/**
 * Play a chime for volume setting feedback
 */
void media_chime(int vol)
{
    if(audio) {
        Mix_VolumeMusic(main_musvol);
        Mix_VolumeChunk(media_chimesnd, vol);
        Mix_HaltChannel(2);
        Mix_PlayChannel(2, media_chimesnd, 0);
    }
}

/**
 * UI error
 */
void media_playerr()
{
    if(audio && media_errsnd) {
        if(media_verbose) printf("tngp: media_playerr\r\n");
        Mix_VolumeChunk(media_errsnd, main_sfxvol);
        Mix_HaltChannel(2);
        Mix_PlayChannel(2, media_errsnd, 0);
    }
}

/**
 * UI item selected
 */
void media_playsel()
{
    if(audio && media_selsnd) {
        if(media_verbose) printf("tngp: media_playsel\r\n");
        Mix_VolumeChunk(media_selsnd, main_sfxvol);
        Mix_HaltChannel(2);
        Mix_PlayChannel(2, media_selsnd, 0);
    }
}

/**
 * UI item clicked
 */
void media_playclk()
{
    if(audio && media_clksnd) {
        if(media_verbose) printf("tngp: media_playclk\r\n");
        Mix_VolumeChunk(media_clksnd, main_sfxvol);
        Mix_HaltChannel(2);
        Mix_PlayChannel(2, media_clksnd, 0);
    }
}

/**
 * Get next music
 */
void media_nextmus()
{
    if(media_ops.hidden.unknown.data1) {
        if(media_music) { Mix_FreeMusic(media_music); media_music = NULL; }
        media_ops.hidden.unknown.data1 = NULL;
        if(media_ops.hidden.unknown.data2) {
            media_rw_seek(&media_ops, 0, RW_SEEK_SET);
            media_music = Mix_LoadMUS_RW(&media_ops, 0);
            if(media_music) {
                cutscn_inloop = 1;
                Mix_PlayMusic(media_music, 1);
                Mix_HookMusicFinished(media_nextmus);
                Mix_VolumeMusic(main_musvol);
            }
        }
    } else
    if(media_ops.hidden.unknown.data2 && media_music) {
        cutscn_inloop = 1;
        Mix_RewindMusic();
        Mix_PlayMusic(media_music, 1);
        Mix_HookMusicFinished(media_nextmus);
    }
}

/**
 * Need a helper thread because we have to wait FadeOutMusic...
 */
static int media_startmus(void *data)
{
    (void)data;
    if(Mix_PlayingMusic()) {
        Mix_HookMusicFinished(NULL);
        Mix_FadeOutMusic(250);
        SDL_Delay(260);
    }
    if(media_music) { Mix_FreeMusic(media_music); media_music = NULL; }
    media_ops.hidden.unknown.data1 = media_once; media_once = NULL;
    media_ops.hidden.unknown.data2 = media_loop; media_loop = NULL;
    media_rw_seek(&media_ops, 0, RW_SEEK_SET);
    media_music = Mix_LoadMUS_RW(&media_ops, 0);
    if(media_music) {
        Mix_PlayMusic(media_music, 1);
        Mix_HookMusicFinished(media_nextmus);
        Mix_VolumeMusic(main_musvol);
    }
    media_strmus = NULL;
    return 0;
}

/**
 * Start music
 */
void media_playmus(world_asset_t *once, world_asset_t *loop)
{
    int i;

    if(audio) {
        if(media_strmus) SDL_WaitThread(media_strmus, &i);
        /* ehh, we can't pass two pointers to the thread */
        media_once = once; media_loop = loop;
        media_strmus = SDL_CreateThread(media_startmus, "startmus", NULL);
        if(!media_strmus) {
            if(Mix_PlayingMusic()) {
                Mix_HookMusicFinished(NULL);
                Mix_HaltMusic();
                SDL_Delay(10);
            }
            media_startmus(NULL);
        } else
            SDL_DetachThread(media_strmus);
    }
}

/**
 * Stop music
 */
void media_stopmus(int dur)
{
    if(audio && Mix_PlayingMusic()) {
        Mix_HookMusicFinished(media_freemus);
        Mix_FadeOutMusic(dur);
    }
}

/**
 * Fade out all media
 */
void media_fadeout(int dur)
{
    if(audio) {
        media_stopmus(dur);
        Mix_ChannelFinished(NULL);
        Mix_FadeOutChannel(0, dur);
        Mix_FadeOutChannel(1, dur);
        Mix_FadeOutChannel(-1, dur);
    }
}

/**
 * Play "place in inventory" sound effect
 */
void media_playinv(int idx)
{
    if(world.objects && idx >= 0 && idx < world.numspr[SPR_TILE] &&
      world.objects[idx]->invsnd >= 0 && world.objects[idx]->invsnd < world.numsound)
        media_playsnd(world.objects[idx]->invsnd, 0, 0);
    else
        media_playclk();
}

/**
 * Play speech
 * tx, ty - distance in tiles
 */
void media_playspc(int idx, int tx, int ty)
{
    Sint16 angle = 0;   /* 0 to 360 clockwise, 0 North */
    Uint8 distance = 0; /* 0 closest, 255 maximum distance away */
    int dist;

    if(audio && idx >= 0 && idx < world.numspc) {
        if(tx || ty) {
            dist = (int)sqrt(tx*tx + ty*ty); if(dist >= world.mapsize) return;
            distance = (uint8_t)((dist << 8) / world.mapsize);
            angle = (int)(atan2((double)tx, (double)ty) * 180.0 / 3.14159265358979323846);
            if(world.type == TNG_TYPE_ISO) angle -= 45;
            if(angle < 0) angle += 360;
        }
        if(!media_spc[idx]) media_spc[idx] = media_load(&world.speech[idx]);
        if(media_spc[idx]) {
            Mix_VolumeChunk(media_spc[idx], main_spcvol);
            Mix_HaltChannel(1);
            if(distance)
                Mix_SetPosition(Mix_PlayChannel(1, media_spc[idx], 0), angle, distance);
            else
                Mix_PlayChannel(1, media_spc[idx], 0);
        }
    }
}

/**
 * Play sound effect
 * tx, ty - distance in tiles
 */
void media_playsnd(int idx, int tx, int ty)
{
    Sint16 angle = 0;
    Uint8 distance = 0;
    int dist;

    if(audio && idx >= 0 && idx < world.numsound) {
        if(media_verbose) printf("tngp: media_playsnd(%u)\r\n", idx);
        if(tx || ty) {
            dist = (int)sqrt(tx*tx + ty*ty); if(dist >= world.mapsize) return;
            distance = (uint8_t)((dist << 8) / world.mapsize);
            angle = (int)(atan2((double)tx, (double)ty) * 180.0 / 3.14159265358979323846);
            if(world.type == TNG_TYPE_ISO) angle -= 45;
            if(angle < 0) angle += 360;
        }
        if(!media_snd[idx]) media_snd[idx] = media_load(&world.sound[idx]);
        if(media_snd[idx]) {
            Mix_VolumeChunk(media_snd[idx], main_sfxvol);
            if(distance)
                Mix_SetPosition(Mix_PlayChannel(-1, media_snd[idx], 0), angle, distance);
            else
                Mix_PlayChannel(-1, media_snd[idx], 0);
        }
    }
}

/**
 * Play audio (voice chat)
 * hz: 0 - 22050, 1 - 44100
 * fmt: 0 - 8 bit, 1 - 16 bit, 2 - 16 bit BE
 */
void media_playaud(int hz, int fmt, uint8_t *buf, int len)
{
    Mix_Chunk chunk = { 0 };
    Sint16 *pcm, data;
    int i;

    if(buf && len > 0) {
        chunk.volume = main_spcvol;
        chunk.allocated = 1;
        chunk.alen = sizeof(int16_t) * 2 * (hz ? len : len * 2);
        chunk.abuf = (uint8_t*)malloc(chunk.alen);
        if(chunk.abuf) {
            pcm = (Sint16*)chunk.abuf;
            for(i = 0; i < len; i++) {
                /* 8 bit -> 16 bit */
                switch(fmt) {
                    case 0: data = buf[i] << 8; break;
                    case 1: data = (buf[i + 1] << 8) | buf[i]; i++; break;
                    case 2: data = (buf[i] << 8) | buf[i + 1]; i++; break;
                    default: data = 0; break;
                }
                /* mono -> stereo */
                *pcm++ = data; *pcm++ = data;
                /* 22050 Hz -> 44100 Hz */
                if(!hz) { *pcm++ = data; *pcm++ = data; }
            }
            Mix_PlayChannel(-1, &chunk, 0);
        }
    }

}
