/**************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2006 Frederik M.J. Vestre
 * Based on vorbis.c codec interface:
 * Copyright (C) 2002 Bjrn Stenberg
 *
 * 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 software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ***************************************************************************/

#include "libspeex/speex/ogg.h"
#include "libspeex/speex/speex.h"
#include "libspeex/speex/speex_callbacks.h"
#include "libspeex/speex/speex_header.h"
#include "libspeex/speex/speex_stereo.h"
#include "libspeex/speex/speex_config_types.h"
#include "codeclib.h"

/* Room for one stereo frame of max size, 2*640 */
#define MAX_FRAME_SIZE 1280
#define CHUNKSIZE 10000  /*2kb*/
#define SEEK_CHUNKSIZE 7*CHUNKSIZE

CODEC_HEADER

spx_int16_t output[MAX_FRAME_SIZE] IBSS_ATTR;

static int get_more_data(spx_ogg_sync_state *oy)
{
    int bytes;
    char *buffer;

    buffer = (char *)spx_ogg_sync_buffer(oy,CHUNKSIZE);

    bytes = ci->read_filebuf(buffer, sizeof(char)*CHUNKSIZE);

    spx_ogg_sync_wrote(oy,bytes);

    return bytes;
}

/* The read/seek functions track absolute position within the stream */

static spx_int64_t get_next_page(spx_ogg_sync_state *oy,spx_ogg_page *og,
                                 spx_int64_t boundary)
{
    spx_int64_t localoffset = ci->curpos;
    long more;
    long ret;

    if (boundary > 0)
        boundary += ci->curpos;

    while (1) {
        more = spx_ogg_sync_pageseek(oy,og);

        if (more < 0) {
            /* skipped n bytes */
            localoffset-=more;
        } else {
            if (more == 0) {
                /* send more paramedics */
                if(!boundary)return(-1);
                {
                    ret = get_more_data(oy);
                    if (ret == 0)
                        return(-2);

                    if (ret < 0)
                        return(-3);
                }
            } else {
                /* got a page.  Return the offset at the page beginning,
                   advance the internal offset past the page end */

                spx_int64_t ret=localoffset;

                return(ret);
            }
        }
    }
}

static spx_int64_t seek_backwards(spx_ogg_sync_state *oy, spx_ogg_page *og,
                                  spx_int64_t wantedpos)
{
    spx_int64_t crofs;
    spx_int64_t *curoffset=&crofs;
    *curoffset=ci->curpos;
    spx_int64_t begin=*curoffset;
    spx_int64_t end=begin;
    spx_int64_t ret;
    spx_int64_t offset=-1;
    spx_int64_t avgpagelen=-1;
    spx_int64_t lastgranule=-1;

    short time = -1;

    while (offset == -1) {

        begin -= SEEK_CHUNKSIZE;

        if (begin < 0) {
            if (time < 0) {
                begin = 0;
                time++;
            } else {
                LOGF("Can't seek that early:%d\n",begin);
                return -3;  /* too early */
            }
        }

        *curoffset = begin;

        ci->seek_buffer(*curoffset);

        spx_ogg_sync_reset(oy);

        lastgranule = -1;

        while (*curoffset < end) {
            ret = get_next_page(oy,og,end-*curoffset);

            if (ret > 0) {
                if (lastgranule != -1) {
                    if (avgpagelen < 0)
                        avgpagelen = (spx_ogg_page_granulepos(og)-lastgranule);
                    else
                       avgpagelen=((spx_ogg_page_granulepos(og)-lastgranule)
                                   + avgpagelen) / 2;
                }

                lastgranule=spx_ogg_page_granulepos(og);

                if ((lastgranule - (avgpagelen/4)) < wantedpos &&
                    (lastgranule + avgpagelen + (avgpagelen/4)) > wantedpos) {

                    /*wanted offset found Yeay!*/

                    /*LOGF("GnPagefound:%d,%d,%d,%d\n",ret,
                           lastgranule,wantedpos,avgpagelen);*/

                    return ret;

                } else if (lastgranule > wantedpos) {  /*too late, seek more*/
                    if (offset != -1) {
                        LOGF("Toolate, returnanyway:%d,%d,%d,%d\n",
                             ret,lastgranule,wantedpos,avgpagelen);
                        return ret;
                    }
                    break;
                } else{ /*if (spx_ogg_page_granulepos(&og)<wantedpos)*/
                    /*too early*/
                    offset = ret;
                    continue;
                }
            } else if (ret == -3) 
                return(-3);
            else if (ret<=0)
                break;
            else if (*curoffset < end) {
                /*this should not be possible*/

                //LOGF("Seek:get_earlier_page:Offset:not_cached by granule:"\"%d,%d,%d,%d,%d\n",*curoffset,end,begin,wantedpos,curpos);

                offset=ret;
            }
        }
    }
    return -1;
}

static int speex_seek_page_granule(spx_int64_t pos, spx_int64_t curpos,
                                   spx_ogg_sync_state *oy,
                                   spx_int64_t headerssize)
{
    /* TODO: Someone may want to try to implement seek to packet, 
             instead of just to page (should be more accurate, not be any 
             faster) */

    spx_int64_t crofs;
    spx_int64_t *curbyteoffset = &crofs;
    *curbyteoffset = ci->curpos;
    spx_int64_t curoffset;
    curoffset = *curbyteoffset;
    spx_int64_t offset = 0;
    spx_ogg_page og = {0,0,0,0};
    spx_int64_t avgpagelen = -1;
    spx_int64_t lastgranule = -1;

    if(abs(pos-curpos)>10000 && headerssize>0 && curoffset-headerssize>10000) {
        /* if seeking for more that 10sec,
           headersize is known & more than 10kb is played,
           try to guess a place to seek from the number of
           bytes playe for this position, this works best when 
           the bitrate is relativly constant.
         */

        curoffset = (((*curbyteoffset-headerssize) * pos)/curpos)*98/100;
        if (curoffset < 0)
            curoffset=0;

        //spx_int64_t toffset=curoffset;

        ci->seek_buffer(curoffset);

        spx_ogg_sync_reset(oy);

        offset = get_next_page(oy,&og,-1);

        if (offset < 0) { /* could not find new page,use old offset */
            LOGF("Seek/guess/fault:%d->-<-%d,%d:%d,%d,%d\n",
                 curpos,0,pos,offset,0,
                 ci->curpos,/*stream_length*/0);

            curoffset = *curbyteoffset;

            ci->seek_buffer(curoffset);

            spx_ogg_sync_reset(oy);
        } else {
            if (spx_ogg_page_granulepos(&og) == 0 && pos > 5000) {
                LOGF("SEEK/guess/fault:%d->-<-%d,%d:%d,%d,%d\n",
                     curpos,spx_ogg_page_granulepos(&og),pos,
                     offset,0,ci->curpos,/*stream_length*/0);

                curoffset = *curbyteoffset;

                ci->seek_buffer(curoffset);

                spx_ogg_sync_reset(oy);
            } else {
                curoffset = offset;
                curpos = spx_ogg_page_granulepos(&og);
            }
        }
    }

    /* which way do we want to seek? */

    if (curpos > pos) {  /* backwards */
        offset = seek_backwards(oy,&og,pos);

        if (offset > 0) {
            *curbyteoffset = curoffset;
            return 1;
        }
    } else {  /* forwards */

        while ( (offset = get_next_page(oy,&og,-1)) > 0) {
            if (lastgranule != -1) {
               if (avgpagelen < 0)
                   avgpagelen = (spx_ogg_page_granulepos(&og) - lastgranule);
               else
                   avgpagelen = ((spx_ogg_page_granulepos(&og) - lastgranule)
                                 + avgpagelen) / 2;
            }

            lastgranule = spx_ogg_page_granulepos(&og);

            if ( ((lastgranule - (avgpagelen/4)) < pos && ( lastgranule + 
                  avgpagelen + (avgpagelen / 4)) > pos) ||
                 lastgranule > pos) {

                /*wanted offset found Yeay!*/

                *curbyteoffset = offset;

                return offset;
            }
        }
    }

    ci->seek_buffer(*curbyteoffset);

    spx_ogg_sync_reset(oy);

    LOGF("Seek failed:%d\n", offset);

    return -1;
}

static void *process_header(spx_ogg_packet *op,
                            int enh_enabled,
                            int *frame_size,
                            int *rate,
                            int *nframes,
                            int *channels,
                            SpeexStereoState *stereo,
                            int *extra_headers
                           )
{
    void *st;
    const SpeexMode *mode;
    SpeexHeader *header;
    int modeID;
    SpeexCallback callback;

    header = speex_packet_to_header((char*)op->packet, op->bytes);

    if (!header){
        DEBUGF ("Cannot read header\n");
        return NULL;
    }

    if (header->mode >= SPEEX_NB_MODES){
        DEBUGF ("Mode does not exist\n");
        return NULL;
    }

    modeID = header->mode;

    mode = speex_lib_get_mode(modeID);

    if (header->speex_version_id > 1) {
        DEBUGF("Undecodeable bitstream");
        return NULL;
    }

    if (mode->bitstream_version < header->mode_bitstream_version){
        DEBUGF("Undecodeable bitstream, newer bitstream");
        return NULL;
    }

    if (mode->bitstream_version > header->mode_bitstream_version){
        DEBUGF("Too old bitstream");
        return NULL;
    }
   
    st = speex_decoder_init(mode);
    if (!st){
        DEBUGF("Decoder init failed");
        return NULL;
    }
    speex_decoder_ctl(st, SPEEX_SET_ENH, &enh_enabled);
    speex_decoder_ctl(st, SPEEX_GET_FRAME_SIZE, frame_size);

    if (header->nb_channels!=1){
        callback.callback_id = SPEEX_INBAND_STEREO;
        callback.func = speex_std_stereo_request_handler;
        callback.data = stereo;
        speex_decoder_ctl(st, SPEEX_SET_HANDLER, &callback);
    }
    *channels = header->nb_channels;

    if (!*rate)
        *rate = header->rate;

    speex_decoder_ctl(st, SPEEX_SET_SAMPLING_RATE, rate);

    *nframes = header->frames_per_packet;

    *extra_headers = header->extra_headers;

    return st;
}

/* this is the codec entry point */
enum codec_status codec_main(void)
{
    SpeexBits bits;
    int error = 0;
    int eof = 0;
    spx_ogg_sync_state oy;
    spx_ogg_page og;
    spx_ogg_packet op;
    spx_ogg_stream_state os;
    spx_int64_t page_granule = 0, cur_granule = 0;
    int enh_enabled = 1;
    int nframes = 2;
    int eos = 0;
    SpeexStereoState *stereo;
    int channels = -1;
    int rate = 0, samplerate = 0;
    int extra_headers = 0;
    int stream_init = 0;
    int page_nb_packets, frame_size, packet_count = 0;
    int lookahead;
    int headerssize = -1;
    unsigned long strtoffset = 0;
    void *st = NULL;
    int j = 0;

    /* Ogg handling still uses mallocs, so reset the malloc buffer per track */
next_track:

    if (codec_init()) {
        error = CODEC_ERROR;
        goto exit;
    }
    stereo = speex_stereo_state_init();
    strtoffset = ci->id3->offset;

    while (!*ci->taginfo_ready && !ci->stop_codec)
       ci->sleep(1);

    spx_ogg_sync_init(&oy);
    spx_ogg_alloc_buffer(&oy,2*CHUNKSIZE);

    samplerate = ci->id3->frequency; 
    codec_set_replaygain(ci->id3);

    eof = 0;
    while (!eof) {
        ci->yield();
        if (ci->stop_codec || ci->new_track)
            break;

        /*seek (seeks to the page before the position) */
        if (ci->seek_time) {
            if(samplerate!=0&&packet_count>1){
                LOGF("Speex seek page:%d,%d,%d,%d\n",
                     ((spx_int64_t)ci->seek_time/1000) *
                     (spx_int64_t)samplerate,
                     page_granule, ci->seek_time,
                     (page_granule/samplerate)*1000, samplerate);

                speex_seek_page_granule(((spx_int64_t)ci->seek_time/1000) *
                                        (spx_int64_t)samplerate,
                                        page_granule, &oy, headerssize);
                ci->seek_complete();
            }
        }

next_page:
        /*Get the ogg buffer for writing*/
        if(get_more_data(&oy)<1){/*read error*/
            error=CODEC_ERROR;
            goto done;
        }

        /* Loop for all complete pages we got (most likely only one) */
        while (spx_ogg_sync_pageout(&oy, &og) == 1) {
            int packet_no;
            if (stream_init == 0) {
                spx_ogg_stream_init(&os, spx_ogg_page_serialno(&og));
                stream_init = 1;
            }

            /* Add page to the bitstream */
            spx_ogg_stream_pagein(&os, &og);

            page_granule = spx_ogg_page_granulepos(&og);
            page_nb_packets = spx_ogg_page_packets(&og);

            cur_granule = page_granule;

            /* Extract all available packets */
            packet_no=0;

            while (!eos && spx_ogg_stream_packetout(&os, &op)==1){
                /* If first packet, process as Speex header */
                if (packet_count==0){
                    st = process_header(&op, enh_enabled, &frame_size,
                                         &samplerate, &nframes, &channels,
                                         stereo, &extra_headers);

                    speex_decoder_ctl(st, SPEEX_GET_LOOKAHEAD, &lookahead);
                    if (!nframes)
                        nframes=1;

                    if (!st){
                        error=CODEC_ERROR;
                        goto exit;
                    }

                    ci->configure(DSP_SET_FREQUENCY, ci->id3->frequency);
                    ci->configure(DSP_SET_SAMPLE_DEPTH, 16);
                    if (channels == 2) {
                        ci->configure(DSP_SET_STEREO_MODE, STEREO_INTERLEAVED);
                    } else if (channels == 1) {
                        ci->configure(DSP_SET_STEREO_MODE, STEREO_MONO);
                    }

                    /* Speex header in its own page, add the whole page
                       headersize */
                    headerssize += og.header_len+og.body_len;

                } else if (packet_count<=1+extra_headers){
                    /* add packet to headersize */
                    headerssize += op.bytes;

                    /* Ignore extra headers */
                } else {
                    if (packet_count <= 2+extra_headers) {
                        if (strtoffset) {
                            ci->seek_buffer(strtoffset);
                            spx_ogg_sync_reset(&oy);
                            packet_count++;
                            goto next_page;
                        }
                    }
                    packet_no++;

                    if (op.e_o_s) /* End of stream condition */
                        eos=1;

                    /* Set Speex bitstream to point to Ogg packet */
                    speex_bits_set_bit_buffer(&bits, (char *)op.packet,
                                                     op.bytes);
                    for (j = 0; j != nframes; j++){
                        int ret;

                        /* Decode frame */
                        ret = speex_decode_int(st, &bits, output);

                        if (ret == -1)
                            break;

                        if (ret == -2)
                            break;

                        if (speex_bits_remaining(&bits) < 0)
                            break;

                        if (channels == 2)
                            speex_decode_stereo_int(output, frame_size, stereo);

                        if (frame_size > 0) {
                            spx_int16_t *frame_start = output + lookahead;

                            if (channels == 2)
                                frame_start += lookahead;
                            ci->pcmbuf_insert(frame_start, NULL,
                                              frame_size - lookahead);
                            lookahead = 0;
                            /* 2 bytes/sample */
                            cur_granule += frame_size / 2;

                            ci->set_offset((long) ci->curpos);

                            ci->set_elapsed((samplerate == 0) ? 0 :
                                             cur_granule * 1000 / samplerate);
                         }
                    }
                }
                packet_count++;
            }
        }
    }

done:
    if (ci->request_next_track()) {

        /* Clean things up for the next track */

        speex_decoder_destroy(st);

        if (stream_init == 1)
            spx_ogg_stream_reset(&os);

        spx_ogg_sync_reset(&oy);

        cur_granule = stream_init = rate = samplerate = headerssize 
            = packet_count = eos = 0;

        goto next_track;
    }

    error = CODEC_OK;

exit:
    speex_bits_destroy(&bits);

    if (stream_init)
       spx_ogg_stream_destroy(&os);

    spx_ogg_sync_destroy(&oy);

    return error;
}
