/*
 * tngp/theora.c
 *
 * Copyright (C) 2021 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 Vorbis / theora decoder, originally from libtheora/examples/player_example.c
 *
 */

#include "SDL.h"
#include "SDL_thread.h"
#include "theora/theoradec.h"
#include "vorbis/codec.h"
#include "tng.h"
#include "world.h"
#include "theora.h"

extern int main_musvol, verbose, cutscn_inloop;
extern char *main_me;
theora_t theora_ctx = { 0 };

static ogg_packet       op;
static ogg_sync_state   oy;
static ogg_page         og;
static ogg_stream_state vo;
static ogg_stream_state to;
static ogg_stream_state test;
static th_info          ti;
static th_comment       tc;
static th_dec_ctx       *td;
static th_setup_info    *ts;
static vorbis_info      vi;
static vorbis_dsp_state vd;
static vorbis_block     vb;
static vorbis_comment   vc;
static uint32_t         theora_base, theora_curr;

/**
 * get some data to parse
 */
static int theora_getdata(theora_t *ctx, ogg_sync_state *oy)
{
    char *buffer;
    int bytes = 4096;

    if(!ctx->f) return 0;
    buffer = ogg_sync_buffer(oy, 4096);
    if(world.tng[ctx->f->tng].offs[1] + 4096 > ctx->f->offs + ctx->f->size)
        bytes = ctx->f->offs + ctx->f->size - world.tng[ctx->f->tng].offs[1];
    bytes = world_readtng(ctx->f->tng, 1, buffer, bytes);
    if(bytes <= 0) return 0;
    return !ogg_sync_wrote(oy, bytes);
}

/**
 * Theora playback SDL_Mixer callback
 */
void theora_callback(int channel)
{
    Mix_Chunk *audio;
    if(channel) return;
    audio = theora_audio(&theora_ctx);
    if(audio) {
        Mix_VolumeChunk(audio, main_musvol);
        Mix_PlayChannel(channel, audio, 0);
    }
}

/**
 * Cleanup decoder
 */
static void theora_cleanup(theora_t *ctx)
{
    if(ts) { th_setup_free(ts); ts = NULL; }

    if(ctx->hasAudio) {
        ogg_stream_clear(&vo);
        vorbis_block_clear(&vb);
        vorbis_dsp_clear(&vd);
    }
    vorbis_comment_clear(&vc);
    vorbis_info_clear(&vi);

    if(ctx->hasVideo) ogg_stream_clear(&to);
    if(td) { th_decode_free(td); td = NULL; }
    th_comment_clear(&tc);
    th_info_clear(&ti);
    ogg_sync_clear(&oy);
}

/**
 * Detect streams
 */
static int theora_detect(theora_t *ctx)
{
    int ret;

    ogg_sync_init(&oy);
    vorbis_info_init(&vi);
    vorbis_comment_init(&vc);
    th_comment_init(&tc);
    th_info_init(&ti);
    td = NULL; ts = NULL;

    /* Ogg file open; parse the headers */
    /* Only interested in Vorbis/Theora streams */
    ret = ctx->hasVideo = ctx->hasAudio = ctx->w = ctx->h = 0;
    while (!ctx->stop && !ret) {
        if(!theora_getdata(ctx, &oy)) break;
        while(ogg_sync_pageout(&oy, &og) > 0) {
            if(!ogg_page_bos(&og)) {
                if(ctx->hasVideo) ogg_stream_pagein(&to, &og);
                if(ctx->hasAudio) ogg_stream_pagein(&vo, &og);
                ret = 1;
                break;
            }
            ogg_stream_init(&test, ogg_page_serialno(&og));
            ogg_stream_pagein(&test, &og);
            ogg_stream_packetout(&test, &op);
            if(!ctx->hasVideo && th_decode_headerin(&ti, &tc, &ts, &op) >= 0) {
                memcpy(&to, &test, sizeof(test));
                ctx->hasVideo = 1;
            } else
            if(!ctx->hasAudio && vorbis_synthesis_headerin(&vi, &vc, &op) >= 0) {
                memcpy(&vo, &test, sizeof(test));
                ctx->hasAudio = 1;
            } else
                ogg_stream_clear(&test);
        }
    }
    /* neither audio nor video streams found */
    if(!ctx->hasVideo && !ctx->hasAudio) {
        theora_cleanup(ctx);
        return 0;
    }

    /* we're expecting more header packets. */
    while(!ctx->stop && !world_eofasset(1, ctx->f) &&
      ((ctx->hasVideo && ctx->hasVideo < 3) || (ctx->hasAudio && ctx->hasAudio < 3))) {
        while(ctx->hasVideo && (ctx->hasVideo < 3)) {
            if(ogg_stream_packetout(&to, &op) != 1 || ctx->stop) break;
            if(!th_decode_headerin(&ti, &tc, &ts, &op)) { ctx->hasVideo = 0; break; }
            ctx->hasVideo++;
        }
        while(ctx->hasAudio && (ctx->hasAudio < 3)) {
            if(ogg_stream_packetout(&vo, &op) != 1 || ctx->stop) break;
            if(vorbis_synthesis_headerin(&vi, &vc, &op)) { ctx->hasAudio = 0; break; }
            ctx->hasAudio++;
        }
        if(ogg_sync_pageout(&oy, &og) > 0) {
            if(ctx->hasVideo) ogg_stream_pagein(&to, &og);
            if(ctx->hasAudio) ogg_stream_pagein(&vo, &og);
        } else
            theora_getdata(ctx, &oy);
    }

    /* and now we have it all.  initialize decoders */
    if(ctx->hasVideo && ti.pixel_fmt == TH_PF_420 && ti.pic_width > 0 && ti.pic_height > 0 &&
      ti.pic_width < 16384 && ti.pic_height < 16384 && (td = th_decode_alloc(&ti, ts))) {
        ret = 0; th_decode_ctl(td, TH_DECCTL_SET_PPLEVEL, &ret, sizeof(ret));   /* turn off post processing */
        ctx->w = ti.pic_width;
        ctx->h = ti.pic_height;
    } else {
        /* tear down the partial theora setup */
        th_info_clear(&ti);
        th_comment_clear(&tc);
        ctx->hasVideo = ctx->w = ctx->h = 0;
    }
    if(ts) { th_setup_free(ts); ts = NULL; }

    if(ctx->hasAudio){
        vorbis_synthesis_init(&vd, &vi);
        if(vi.channels > 2) {
            vorbis_info_clear(&vi);
            ctx->hasAudio = 0;
        } else
            vorbis_block_init(&vd, &vb);
    } else {
        vorbis_info_clear(&vi);
        vorbis_comment_clear(&vc);
    }
    /********************** that was only the setup so far... now do the real thing **********************/
    ctx->started = 1;
    return 1;
}

/**
 * Decode stream
 */
static int theora_decode(theora_t *ctx)
{
    ogg_int64_t granulepos;
    th_ycbcr_buffer ycbcr;
    uint8_t *dst, *p;
    int16_t *pcm, ival;
    uint32_t j, w, h;
    float **raw, val;
    int ret, i, uvoff, doread = 0;

    do {
        /* read in data */
        if(doread) {
            doread = 0;
            if(theora_getdata(ctx, &oy) > 0)
                while(!ctx->stop && ogg_sync_pageout(&oy, &og) > 0) {
                    if(ctx->hasVideo) ogg_stream_pagein(&to, &og);
                    if(ctx->hasAudio) ogg_stream_pagein(&vo, &og);
                }
            else
            if(ctx->atail == ctx->ahead && ctx->vtail == ctx->vhead) return 0;
            else return 1;
        }

        /*** parse audio ***/
        while(!ctx->stop && ctx->hasAudio && (ctx->ahead + 1) % THEORA_QUEUE_SIZE != ctx->atail) {
            /* if there's pending, decoded audio, grab it */
            if((ret = vorbis_synthesis_pcmout(&vd, &raw)) > 0) {
                vorbis_synthesis_read(&vd, ret);
                ctx->chunk[ctx->ahead].volume = MIX_MAX_VOLUME;
                ctx->chunk[ctx->ahead].allocated = 0;
                ctx->chunk[ctx->ahead].alen = i = sizeof(int16_t) * (vi.channels < 2 ? 2 : vi.channels) * ret;
                ctx->chunk[ctx->ahead].abuf = (Uint8*)realloc(ctx->chunk[ctx->ahead].abuf, i);
                /* deinterlaced float -> interlaced short int pcm */
                pcm =  (int16_t*)ctx->chunk[ctx->ahead].abuf;
                if(pcm) {
                    for(i = 0; i < ret; i++)
                        /* do mono -> stereo the simple and fast way */
                        if(vi.channels == 1) {
                            val = raw[0][i]; ival = (int16_t)(val < -1.0f ? -32768 : (val > 1.0f ? 32767 : val * 32767.0f));
                            *(pcm++) = ival;
                            *(pcm++) = ival;
                        } else
                            for(j = 0; j < (uint32_t)vi.channels; j++) {
                                val = raw[j][i];
                                *(pcm++) = (int16_t)(val < -1.0f ? -32768 : (val > 1.0f ? 32767 : val * 32767.0f));
                            }
                    /* add to queue */
                    ctx->ahead = (ctx->ahead + 1) % THEORA_QUEUE_SIZE;
                    ctx->started |= 2;
                }
            } else {
                if(ogg_stream_packetout(&vo, &op) > 0) {
                    if(vorbis_synthesis(&vb, &op) == 0) vorbis_synthesis_blockin(&vd, &vb);
                } else {
                    doread = 1;
                    break;
                }
            }
        }

        /*** parse video ***/
        while(!ctx->stop && ctx->hasVideo && (ctx->vhead + 1) % THEORA_QUEUE_SIZE != ctx->vtail) {
            /* theora is one in, one out... */
            if(ogg_stream_packetout(&to, &op) > 0) {
                if(op.granulepos >= 0)
                    th_decode_ctl(td, TH_DECCTL_SET_GRANPOS, &op.granulepos, sizeof(op.granulepos));
                granulepos = 0;
                if(th_decode_packetin(td, &op, &granulepos) == 0) {
                    if(th_decode_ycbcr_out(td, ycbcr) == 0) {
                        theora_curr = (uint32_t)(th_granule_time(td, granulepos) * 1000.0);
                        ctx->frame[ctx->vhead].playms = theora_curr + theora_base;
                        if(!ctx->frame[ctx->vhead].vbuf)
                            ctx->frame[ctx->vhead].vbuf = (uint8_t*)malloc(ti.pic_width * ti.pic_height * 2);
                        dst = ctx->frame[ctx->vhead].vbuf;
                        if(dst) {
                            /* frame420 to YUVPlanar */
                            p = ycbcr[0].data + (ti.pic_x & ~1) + ycbcr[0].stride * (ti.pic_y & ~1);
                            uvoff = (ti.pic_x / 2) + (ycbcr[1].stride) * (ti.pic_y / 2);
                            w = ti.pic_width / 2; h = ti.pic_height / 2;
                            for(j = 0; j < ti.pic_height; j++, dst += ti.pic_width, p += ycbcr[0].stride)
                                memcpy(dst, p, ti.pic_width);
                            for(j = 0, p = ycbcr[1].data + uvoff; j < h; j++, dst += w, p += ycbcr[1].stride) memcpy(dst, p, w);
                            for(j = 0, p = ycbcr[2].data + uvoff; j < h; j++, dst += w, p += ycbcr[2].stride) memcpy(dst, p, w);
                            /* add to queue */
                            ctx->vhead = (ctx->vhead + 1) % THEORA_QUEUE_SIZE;
                            ctx->started |= 4;
                        }
                    }
                }
            } else {
                doread = 1;
                break;
            }
        }
    } while(!ctx->stop && doread);
    return 1;
}

/**
 * Decoder and circular buffer producer
 */
static int theora_producer(void *data)
{
    theora_t *ctx = (theora_t *)data;

    do {
        if(!theora_detect(ctx)) goto cleanup;

        while(!ctx->stop && (ctx->hasVideo || ctx->hasAudio) && theora_decode(ctx));

        /**************************************** clean up *****************************************/
cleanup:
        theora_cleanup(ctx);
        if(world_eofasset(1, ctx->f)) {
            if(ctx->f == ctx->once) ctx->f = ctx->loop;
            if(ctx->loop) { world_seektng(ctx->loop->tng, 1, ctx->loop->offs); theora_base += theora_curr; cutscn_inloop = 1; }
        }
    } while(!ctx->stop && ctx->f && !world_eofasset(1, ctx->f));
    ctx->started = ctx->done = 1;
    return 0;
}

/**
 * Producer for single thread
 */
void theora_produce(theora_t *ctx)
{
    if(!ctx->th) {
        if(!ctx->stop && (ctx->hasVideo || ctx->hasAudio) && theora_decode(ctx)) return;
        theora_cleanup(ctx);
        if(world_eofasset(1, ctx->f)) {
            if(ctx->f == ctx->once) ctx->f = ctx->loop;
            if(ctx->loop) {
                world_seektng(ctx->loop->tng, 1, ctx->loop->offs); theora_base += theora_curr;
                theora_detect(ctx);
                if(ctx->hasAudio) {
                    while(ctx->ahead == ctx->atail) theora_produce(ctx);
                    theora_callback(0);
                }
            }
        }
    }
}

/**
 * Start decoder
 */
void theora_start(theora_t *ctx, world_asset_t *once, world_asset_t *loop)
{
    if(!ctx || (!once && !loop)) return;
    memset(ctx, 0, sizeof(theora_t));
    ctx->once = once; ctx->loop = loop; ctx->f = once ? once : loop;
    world_seektng(ctx->f->tng, 1, ctx->f->offs);
    if(!(ctx->th = SDL_CreateThread(theora_producer, "producer", ctx))) {
        if(verbose > 1) printf("%s: decoding video in main thread, expect lags and glitches\r\n", main_me);
        theora_detect(ctx);
        if(ctx->hasAudio) while(ctx->ahead == ctx->atail) theora_produce(ctx);
        ctx->baseticks = SDL_GetTicks();
        return;
    } else
        if(verbose > 1) printf("%s: decoding video in separate thread\r\n", main_me);
    /* wait until the producer thread detects the streams and sets hasAudio and hasVideo flags */
    while(!ctx->started) SDL_Delay(10);
    /* wait until there's some data in the circular buffers to consume */
    while(!ctx->done && ctx->started != (1 | (ctx->hasAudio ? 2 : 0) | (ctx->hasVideo ? 4 : 0))) SDL_Delay(10);
    ctx->baseticks = SDL_GetTicks();
}

/**
 * Stop decoder
 */
void theora_stop(theora_t *ctx)
{
    int i;

    if(!ctx) return;
    ctx->stop = 1;
    if(ctx->th) {
        SDL_WaitThread(ctx->th, &i); ctx->th = NULL;
        /* failsafe */
        while(ctx->started && !ctx->done) SDL_Delay(10);
    }
    theora_cleanup(ctx);
    /* free internal buffers */
    for(i = 0; i < THEORA_QUEUE_SIZE; i++) {
        if(ctx->chunk[i].abuf) { free(ctx->chunk[i].abuf); ctx->chunk[i].abuf = NULL; }
        if(ctx->frame[i].vbuf) { free(ctx->frame[i].vbuf); ctx->frame[i].vbuf = NULL; }
    }
    ctx->stop = ctx->done = ctx->started = ctx->ahead = ctx->atail = ctx->vhead = ctx->vtail = ctx->hasAudio = ctx->hasVideo = 0;
}

/**
 * Returns false if playing is finished (that is, decoding is done and everything is consumed from the buffers)
 */
int theora_playing(theora_t *ctx)
{
    return ctx && !ctx->stop && (!ctx->done || ctx->ahead != ctx->atail || ctx->vhead != ctx->vtail);
}

/**
 * Consume data from audio buffer
 */
Mix_Chunk *theora_audio(theora_t *ctx)
{
    volatile Mix_Chunk *ret;

    if(!ctx || !ctx->hasAudio || ctx->stop) return NULL;
    /* this is bad. we may have consumed the audio faster than the producer could produce. Wait for a bit. */
    if(ctx->th) while(!ctx->done && ctx->atail == ctx->ahead);
    /* if the queue is still empty, that means end of audio */
    if(ctx->atail == ctx->ahead) return NULL;
    ret = &ctx->chunk[ctx->atail];
    ctx->atail = (ctx->atail + 1) % THEORA_QUEUE_SIZE;
    return (Mix_Chunk*)ret;
}

/**
 * Consume data from video buffer and update a texture with it
 */
extern volatile uint32_t main_tick;
void theora_video(theora_t *ctx, SDL_Texture *texture)
{
    int i, pitch;
    Uint8 *y, *u, *v, *dst;
    Uint32 now;

    /* here it is not a problem if the queue is temporarily empty, since we're not running inside SDL mixer callbacks */
    if(!ctx || !texture || !ctx->hasVideo || ctx->stop || ctx->vtail == ctx->vhead) return;
    now = main_tick - ctx->baseticks;
    if(ctx->frame[ctx->vtail].playms > now) return;
    /* handle frame drop, when the next frame is in the past too */
    while((ctx->vtail + 1) % THEORA_QUEUE_SIZE != ctx->vhead && ctx->frame[(ctx->vtail + 1) % THEORA_QUEUE_SIZE].vbuf &&
      ctx->frame[(ctx->vtail + 1) % THEORA_QUEUE_SIZE].playms < now) ctx->vtail = (ctx->vtail + 1) % THEORA_QUEUE_SIZE;
    if(ctx->vtail == ctx->vhead || !ctx->frame[ctx->vtail].vbuf) return;
    /* update the texture with the current frame */
    i = ctx->vtail; ctx->vtail = (ctx->vtail + 1) % THEORA_QUEUE_SIZE;
    SDL_LockTexture(texture, NULL, (void**)&dst, &pitch);
    y = ctx->frame[i].vbuf;
    u = y + (ctx->w * ctx->h);
    v = u + ((ctx->w / 2) * (ctx->h / 2));
    for (i = 0; i < ctx->h; i++, y += ctx->w, dst += pitch) memcpy(dst, y, ctx->w);
    for (i = 0; i < ctx->h / 2; i++, u += ctx->w / 2, dst += pitch / 2) memcpy(dst, u, ctx->w / 2);
    for (i = 0; i < ctx->h / 2; i++, v += ctx->w / 2, dst += pitch / 2) memcpy(dst, v, ctx->w / 2);
    SDL_UnlockTexture(texture);
}
