/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id$
 *
 * Copyright (C) 2005-2007 Miika Pekkarinen
 * Copyright (C) 2007-2008 Nicolas Pennequin
 *
 * 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 "playback.h"
#include "codec_thread.h"
#include "system.h"
#include "kernel.h"
#include "codecs.h"
#include "buffering.h"
#include "pcmbuf.h"
#include "dsp.h"
#include "abrepeat.h"
#include "metadata.h"
#include "splash.h"

/* Define LOGF_ENABLE to enable logf output in this file */
/*#define LOGF_ENABLE*/
#include "logf.h"

/* macros to enable logf for queues
   logging on SYS_TIMEOUT can be disabled */
#ifdef SIMULATOR
/* Define this for logf output of all queuing except SYS_TIMEOUT */
#define PLAYBACK_LOGQUEUES
/* Define this to logf SYS_TIMEOUT messages */
/*#define PLAYBACK_LOGQUEUES_SYS_TIMEOUT*/
#endif

#ifdef PLAYBACK_LOGQUEUES
#define LOGFQUEUE logf
#else
#define LOGFQUEUE(...)
#endif

#ifdef PLAYBACK_LOGQUEUES_SYS_TIMEOUT
#define LOGFQUEUE_SYS_TIMEOUT logf
#else
#define LOGFQUEUE_SYS_TIMEOUT(...)
#endif


/* Variables are commented with the threads that use them:
 * A=audio, C=codec, V=voice. A suffix of - indicates that
 * the variable is read but not updated on that thread.

 * Unless otherwise noted, the extern variables are located
 * in playback.c.
 */

/* Main state control */
volatile bool audio_codec_loaded SHAREDBSS_ATTR = false; /* Codec loaded? (C/A-) */

extern struct mp3entry *thistrack_id3,  /* the currently playing track */
                       *othertrack_id3; /* prev track during track-change-transition, or end of playlist,
                                         * next track otherwise */

/* Track change controls */
extern bool automatic_skip; /* Who initiated in-progress skip? (C/A-) */

/* Set to true if the codec thread should send an audio stop request
 * (typically because the end of the playlist has been reached).
 */
static bool codec_requested_stop = false;

extern struct event_queue audio_queue;
extern struct event_queue codec_queue;

extern struct codec_api ci; /* from codecs.c */

/* Codec thread */
unsigned int codec_thread_id;   /* For modifying thread priority later.
                                   Used by playback.c and pcmbuf.c */
static struct queue_sender_list codec_queue_sender_list;
static long codec_stack[(DEFAULT_STACK_SIZE + 0x2000)/sizeof(long)]
IBSS_ATTR;
static const char codec_thread_name[] = "codec";

/* function prototypes */
static bool codec_load_next_track(void);


/**************************************/

/** misc external functions */

int get_codec_base_type(int type)
{
    switch (type) {
        case AFMT_MPA_L1:
        case AFMT_MPA_L2:
        case AFMT_MPA_L3:
            return AFMT_MPA_L3;
    }

    return type;
}

const char *get_codec_filename(int cod_spec)
{
    const char *fname;

#ifdef HAVE_RECORDING
    /* Can choose decoder or encoder if one available */
    int type = cod_spec & CODEC_TYPE_MASK;
    int afmt = cod_spec & CODEC_AFMT_MASK;

    if ((unsigned)afmt >= AFMT_NUM_CODECS)
        type = AFMT_UNKNOWN | (type & CODEC_TYPE_MASK);

    fname = (type == CODEC_TYPE_ENCODER) ?
                audio_formats[afmt].codec_enc_root_fn :
                audio_formats[afmt].codec_root_fn;

    logf("%s: %d - %s",
        (type == CODEC_TYPE_ENCODER) ? "Encoder" : "Decoder",
        afmt, fname ? fname : "<unknown>");
#else /* !HAVE_RECORDING */
    /* Always decoder */
    if ((unsigned)cod_spec >= AFMT_NUM_CODECS)
        cod_spec = AFMT_UNKNOWN;
    fname = audio_formats[cod_spec].codec_root_fn;
    logf("Codec: %d - %s",  cod_spec, fname ? fname : "<unknown>");
#endif /* HAVE_RECORDING */

    return fname;
} /* get_codec_filename */

/* Borrow the codec thread and return the ID */
void codec_thread_do_callback(void (*fn)(void), unsigned int *id)
{
    /* Set id before telling thread to call something; it may be
     * needed before this function returns. */
    if (id != NULL)
        *id = codec_thread_id;

    /* Codec thread will signal just before entering callback */
    LOGFQUEUE("codec >| Q_CODEC_DO_CALLBACK");
    queue_send(&codec_queue, Q_CODEC_DO_CALLBACK, (intptr_t)fn);
}


/** codec API callbacks */

static void* codec_get_buffer(size_t *size)
{
    if (codec_size >= CODEC_SIZE)
        return NULL;
    *size = CODEC_SIZE - codec_size;
    return &codecbuf[codec_size];
}

static void codec_pcmbuf_insert_callback(
        const void *ch1, const void *ch2, int count)
{
    const char *src[2] = { ch1, ch2 };

    while (count > 0)
    {
        int out_count = dsp_output_count(ci.dsp, count);
        int inp_count;
        char *dest;

        /* Prevent audio from a previous track from playing */
        if (ci.new_track || ci.stop_codec)
            return;

        while ((dest = pcmbuf_request_buffer(&out_count)) == NULL)
        {
            cancel_cpu_boost();
            sleep(1);
            if (ci.seek_time || ci.new_track || ci.stop_codec)
                return;
        }

        /* Get the real input_size for output_size bytes, guarding
         * against resampling buffer overflows. */
        inp_count = dsp_input_count(ci.dsp, out_count);

        if (inp_count <= 0)
            return;

        /* Input size has grown, no error, just don't write more than length */
        if (inp_count > count)
            inp_count = count;

        out_count = dsp_process(ci.dsp, dest, src, inp_count);

        if (out_count <= 0)
            return;

        pcmbuf_write_complete(out_count);

        count -= inp_count;
    }
} /* codec_pcmbuf_insert_callback */

static void codec_set_elapsed_callback(unsigned long value)
{
    if (ci.seek_time)
        return;

#ifdef AB_REPEAT_ENABLE
    ab_position_report(value);
#endif

    unsigned long latency = pcmbuf_get_latency();
    if (value < latency)
        thistrack_id3->elapsed = 0;
    else
    {
        unsigned long elapsed = value - latency;
        if (elapsed > thistrack_id3->elapsed ||
            elapsed < thistrack_id3->elapsed - 2)
            {
                thistrack_id3->elapsed = elapsed;
            }
    }
}

static void codec_set_offset_callback(size_t value)
{
    if (ci.seek_time)
        return;

    unsigned long latency = pcmbuf_get_latency() * thistrack_id3->bitrate / 8;
    if (value < latency)
        thistrack_id3->offset = 0;
    else
        thistrack_id3->offset = value - latency;
}

/* helper function, not a callback */
static void codec_advance_buffer_counters(size_t amount)
{
    bufadvance(get_audio_hid(), amount);
    ci.curpos += amount;
}

/* copy up-to size bytes into ptr and return the actual size copied */
static size_t codec_filebuf_callback(void *ptr, size_t size)
{
    ssize_t copy_n;

    if (ci.stop_codec || !(audio_status() & AUDIO_STATUS_PLAY))
        return 0;

    copy_n = bufread(get_audio_hid(), size, ptr);

    /* Nothing requested OR nothing left */
    if (copy_n == 0)
        return 0;

    /* Update read and other position pointers */
    codec_advance_buffer_counters(copy_n);

    /* Return the actual amount of data copied to the buffer */
    return copy_n;
} /* codec_filebuf_callback */

static void* codec_request_buffer_callback(size_t *realsize, size_t reqsize)
{
    size_t copy_n = reqsize;
    ssize_t ret;
    void *ptr;

    if (!(audio_status() & AUDIO_STATUS_PLAY))
    {
        *realsize = 0;
        return NULL;
    }

    ret = bufgetdata(get_audio_hid(), reqsize, &ptr);
    if (ret >= 0)
        copy_n = MIN((size_t)ret, reqsize);

    if (copy_n == 0)
    {
        *realsize = 0;
        return NULL;
    }

    *realsize = copy_n;

    return ptr;
} /* codec_request_buffer_callback */

static void codec_advance_buffer_callback(size_t amount)
{
    codec_advance_buffer_counters(amount);
    codec_set_offset_callback(ci.curpos);
}

static void codec_advance_buffer_loc_callback(void *ptr)
{
    size_t amount = buf_get_offset(get_audio_hid(), ptr);
    codec_advance_buffer_callback(amount);
}

static bool codec_seek_buffer_callback(size_t newpos)
{
    logf("codec_seek_buffer_callback");

    int ret = bufseek(get_audio_hid(), newpos);
    if (ret == 0) {
        ci.curpos = newpos;
        return true;
    }
    else {
        return false;
    }
}

static void codec_seek_complete_callback(void)
{
    logf("seek_complete");
    /* If seeking-while-playing, pcm_is_paused() is true.
     * If seeking-while-paused, audio_status PAUSE is true.
     * A seamless seek skips this section. */
    bool audio_paused = audio_status() & AUDIO_STATUS_PAUSE;
    if (pcm_is_paused() || audio_paused)
    {
        /* Clear the buffer */
        pcmbuf_play_stop();
        dsp_configure(ci.dsp, DSP_FLUSH, 0);

        /* If seeking-while-playing, resume pcm playback */
        if (!audio_paused)
            pcmbuf_pause(false);
    }
    ci.seek_time = 0;
}

static void codec_discard_codec_callback(void)
{
    int *codec_hid = get_codec_hid();
    if (*codec_hid >= 0)
    {
        bufclose(*codec_hid);
        *codec_hid = -1;
    }
}

static bool codec_request_next_track_callback(void)
{
    int prev_codectype;

    if (ci.stop_codec || !(audio_status() & AUDIO_STATUS_PLAY))
        return false;

    prev_codectype = get_codec_base_type(thistrack_id3->codectype);
    if (!codec_load_next_track())
        return false;

    /* Seek to the beginning of the new track because if the struct
       mp3entry was buffered, "elapsed" might not be zero (if the track has
       been played already but not unbuffered) */
    codec_seek_buffer_callback(thistrack_id3->first_frame_offset);
    /* Check if the next codec is the same file. */
    if (prev_codectype == get_codec_base_type(thistrack_id3->codectype))
    {
        logf("New track loaded");
        codec_discard_codec_callback();
        return true;
    }
    else
    {
        logf("New codec:%d/%d", thistrack_id3->codectype, prev_codectype);
        return false;
    }
}

static void codec_configure_callback(int setting, intptr_t value)
{
    if (!dsp_configure(ci.dsp, setting, value))
        { logf("Illegal key:%d", setting); }
}

/* Initialize codec API */
void codec_init_codec_api(void)
{
    ci.dsp                 = (struct dsp_config *)dsp_configure(NULL, DSP_MYDSP,
                                                                CODEC_IDX_AUDIO);
    ci.codec_get_buffer    = codec_get_buffer;
    ci.pcmbuf_insert       = codec_pcmbuf_insert_callback;
    ci.set_elapsed         = codec_set_elapsed_callback;
    ci.read_filebuf        = codec_filebuf_callback;
    ci.request_buffer      = codec_request_buffer_callback;
    ci.advance_buffer      = codec_advance_buffer_callback;
    ci.advance_buffer_loc  = codec_advance_buffer_loc_callback;
    ci.seek_buffer         = codec_seek_buffer_callback;
    ci.seek_complete       = codec_seek_complete_callback;
    ci.request_next_track  = codec_request_next_track_callback;
    ci.discard_codec       = codec_discard_codec_callback;
    ci.set_offset          = codec_set_offset_callback;
    ci.configure           = codec_configure_callback;
}


/* track change */

static bool codec_load_next_track(void)
{
    intptr_t result = Q_CODEC_REQUEST_FAILED;

    audio_set_prev_elapsed(thistrack_id3->elapsed);

#ifdef AB_REPEAT_ENABLE
    ab_end_of_track_report();
#endif

    logf("Request new track");

    if (ci.new_track == 0)
    {
        ci.new_track++;
        automatic_skip = true;
    }

    if (!ci.stop_codec)
    {
        trigger_cpu_boost();
        LOGFQUEUE("codec >| audio Q_AUDIO_CHECK_NEW_TRACK");
        result = queue_send(&audio_queue, Q_AUDIO_CHECK_NEW_TRACK, 0);
    }

    switch (result)
    {
        case Q_CODEC_REQUEST_COMPLETE:
            LOGFQUEUE("codec |< Q_CODEC_REQUEST_COMPLETE");
            pcmbuf_start_track_change(automatic_skip);
            return true;

        case Q_CODEC_REQUEST_FAILED:
            LOGFQUEUE("codec |< Q_CODEC_REQUEST_FAILED");
            ci.new_track = 0;
            ci.stop_codec = true;
            codec_requested_stop = true;
            return false;

        default:
            LOGFQUEUE("codec |< default");
            ci.stop_codec = true;
            codec_requested_stop = true;
            return false;
    }
}

/** CODEC THREAD */
static void codec_thread(void)
{
    struct queue_event ev;
    int status;

    while (1) {
        status = 0;
        
#ifdef HAVE_CROSSFADE
        if (!pcmbuf_is_crossfade_active())
#endif
        {
            cancel_cpu_boost();
        }
            
        queue_wait(&codec_queue, &ev);
        codec_requested_stop = false;

        switch (ev.id) {
            case Q_CODEC_LOAD_DISK:
                LOGFQUEUE("codec < Q_CODEC_LOAD_DISK");
                queue_reply(&codec_queue, 1);
                audio_codec_loaded = true;
                ci.stop_codec = false;
                status = codec_load_file((const char *)ev.data, &ci);
                LOGFQUEUE("codec_load_file %s %d\n", (const char *)ev.data, status);
                break;

            case Q_CODEC_LOAD:
                LOGFQUEUE("codec < Q_CODEC_LOAD");
                if (*get_codec_hid() < 0) {
                    logf("Codec slot is empty!");
                    /* Wait for the pcm buffer to go empty */
                    while (pcm_is_playing())
                        yield();
                    /* This must be set to prevent an infinite loop */
                    ci.stop_codec = true;
                    LOGFQUEUE("codec > codec Q_AUDIO_PLAY");
                    queue_post(&codec_queue, Q_AUDIO_PLAY, 0);
                    break;
                }

                audio_codec_loaded = true;
                ci.stop_codec = false;
                status = codec_load_buf(*get_codec_hid(), &ci);
                LOGFQUEUE("codec_load_buf %d\n", status);
                break;

            case Q_CODEC_DO_CALLBACK:
                LOGFQUEUE("codec < Q_CODEC_DO_CALLBACK");
                queue_reply(&codec_queue, 1);
                if ((void*)ev.data != NULL)
                {
                    cpucache_invalidate();
                    ((void (*)(void))ev.data)();
                    cpucache_flush();
                }
                break;

#ifdef AUDIO_HAVE_RECORDING
            case Q_ENCODER_LOAD_DISK:
                LOGFQUEUE("codec < Q_ENCODER_LOAD_DISK");
                audio_codec_loaded = false; /* Not audio codec! */
                logf("loading encoder");
                ci.stop_encoder = false;
                status = codec_load_file((const char *)ev.data, &ci);
                logf("encoder stopped");
                break;
#endif /* AUDIO_HAVE_RECORDING */

            default:
                LOGFQUEUE("codec < default");
        }

        if (audio_codec_loaded)
        {
            if (ci.stop_codec)
            {
                status = CODEC_OK;
                if (!(audio_status() & AUDIO_STATUS_PLAY))
                    pcmbuf_play_stop();

            }
            audio_codec_loaded = false;
        }

        switch (ev.id) {
            case Q_CODEC_LOAD_DISK:
            case Q_CODEC_LOAD:
                LOGFQUEUE("codec < Q_CODEC_LOAD");
                if (audio_status() & AUDIO_STATUS_PLAY)
                {
                    if (ci.new_track || status != CODEC_OK)
                    {
                        if (!ci.new_track)
                        {
                            logf("Codec failure, %d %d", ci.new_track, status);
                            splash(HZ*2, "Codec failure");
                        }

                        if (!codec_load_next_track())
                        {
                            LOGFQUEUE("codec > audio Q_AUDIO_STOP");
                            /* End of playlist */
                            queue_post(&audio_queue, Q_AUDIO_STOP, 0);
                            break;
                        }
                    }
                    else
                    {
                        logf("Codec finished");
                        if (ci.stop_codec)
                        {
                            /* Wait for the audio to stop playing before
                             * triggering the WPS exit */
                            while(pcm_is_playing())
                            {
                                /* There has been one too many struct pointer swaps by now
                                 * so even though it says othertrack_id3, its the correct one! */
                                othertrack_id3->elapsed =
                                    othertrack_id3->length - pcmbuf_get_latency();
                                sleep(1);
                            }

                            if (codec_requested_stop)
                            {
                                LOGFQUEUE("codec > audio Q_AUDIO_STOP");
                                queue_post(&audio_queue, Q_AUDIO_STOP, 0);
                            }
                            break;
                        }
                    }

                    if (*get_codec_hid() >= 0)
                    {
                        LOGFQUEUE("codec > codec Q_CODEC_LOAD");
                        queue_post(&codec_queue, Q_CODEC_LOAD, 0);
                    }
                    else
                    {
                        const char *codec_fn =
                            get_codec_filename(thistrack_id3->codectype);
                        if (codec_fn)
                        {
                            LOGFQUEUE("codec > codec Q_CODEC_LOAD_DISK");
                            queue_post(&codec_queue, Q_CODEC_LOAD_DISK,
                                       (intptr_t)codec_fn);
                        }
                    }
                }
                break;

#ifdef AUDIO_HAVE_RECORDING
            case Q_ENCODER_LOAD_DISK:
                LOGFQUEUE("codec < Q_ENCODER_LOAD_DISK");

                if (status == CODEC_OK)
                    break;

                logf("Encoder failure");
                splash(HZ*2, "Encoder failure");

                if (ci.enc_codec_loaded < 0)
                    break;

                logf("Encoder failed to load");
                ci.enc_codec_loaded = -1;
                break;
#endif /* AUDIO_HAVE_RECORDING */

            default:
                LOGFQUEUE("codec < default");

        } /* end switch */
    }
}

void make_codec_thread(void)
{
    codec_thread_id = create_thread(
            codec_thread, codec_stack, sizeof(codec_stack),
            CREATE_THREAD_FROZEN,
            codec_thread_name IF_PRIO(, PRIORITY_PLAYBACK)
            IF_COP(, CPU));
    queue_enable_queue_send(&codec_queue, &codec_queue_sender_list,
                            codec_thread_id);
}
