/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id$
 *
 * Copyright (C) 2007 by Michael Sevakis
 *
 * All files in this archive are subject to the GNU General Public License.
 * See the file COPYING in the source tree root for full license agreement.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/
#include <stdlib.h>
#include "system.h"
#include "kernel.h"
#include "logf.h"
#include "audio.h"
#include "sound.h"

/**
 * Aspects implemented in the target-specific portion:
 *
 * ==Playback==
 *   Public -
 *      pcm_postinit
 *      pcm_get_bytes_waiting
 *      pcm_play_lock
 *      pcm_play_unlock
 *   Semi-private -
 *      pcm_play_dma_init
 *      pcm_play_dma_init
 *      pcm_play_dma_start
 *      pcm_play_dma_stop
 *      pcm_play_dma_pause
 *      pcm_play_dma_get_peak_buffer
 *   Data Read/Written within TSP -
 *      pcm_curr_sampr (RW)
 *      pcm_callback_for_more (R)
 *      pcm_playing (R)
 *      pcm_paused (R)
 *
 * ==Recording==
 *   Public -
 *      pcm_rec_lock
 *      pcm_rec_unlock
 *   Semi-private -
 *      pcm_rec_dma_init
 *      pcm_rec_dma_close
 *      pcm_rec_dma_start
 *      pcm_rec_dma_stop
 *      pcm_rec_dma_get_peak_buffer
 *   Data Read/Written within TSP -
 *      pcm_rec_peak_addr (RW)
 *      pcm_callback_more_ready (R)
 *      pcm_recording (R)
 *
 * States are set _after_ the target's pcm driver is called so that it may
 * know from whence the state is changed. One exception is init.
 *
 */

/* the registered callback function to ask for more mp3 data */
volatile pcm_more_callback_type pcm_callback_for_more
    NOCACHEBSS_ATTR = NULL;
/* PCM playback state */
volatile bool pcm_playing NOCACHEBSS_ATTR = false;
/* PCM paused state. paused implies playing */
volatile bool pcm_paused NOCACHEBSS_ATTR = false;
/* samplerate of currently playing audio - undefined if stopped */
unsigned long pcm_curr_sampr NOCACHEBSS_ATTR = 0;

/**
 * Do peak calculation using distance squared from axis and save a lot
 * of jumps and negation. Don't bother with the calculations of left or
 * right only as it's never really used and won't save much time.
 *
 * Used for recording and playback.
 */
static void pcm_peak_peeker(const void *addr, int count, int peaks[2])
{
    int32_t peak_l = 0, peak_r = 0;
    int32_t peaksq_l = 0, peaksq_r = 0;

    do
    {
        int32_t value = *(int32_t *)addr;
        int32_t ch, chsq;
#ifdef ROCKBOX_BIG_ENDIAN
        ch = value >> 16;
#else
        ch = (int16_t)value;
#endif
        chsq = ch*ch;
        if (chsq > peaksq_l)
            peak_l = ch, peaksq_l = chsq;

#ifdef ROCKBOX_BIG_ENDIAN
        ch = (int16_t)value;
#else
        ch = value >> 16;
#endif
        chsq = ch*ch;
        if (chsq > peaksq_r)
            peak_r = ch, peaksq_r = chsq;

        addr += 16;
        count -= 4;
    }
    while (count > 0);

    peaks[0] = abs(peak_l);
    peaks[1] = abs(peak_r);
}

void pcm_calculate_peaks(int *left, int *right)
{
    static int peaks[2] = { 0, 0 };
    static unsigned long last_peak_tick = 0;
    static unsigned long frame_period   = 0;

    long tick = current_tick;

    /* Throttled peak ahead based on calling period */
    long period = tick - last_peak_tick;

    /* Keep reasonable limits on period */
    if (period < 1)
        period = 1;
    else if (period > HZ/5)
        period = HZ/5;

    frame_period = (3*frame_period + period) >> 2;

    last_peak_tick = tick;

    if (pcm_playing && !pcm_paused)
    {
        const void *addr;
        int count, framecount;

        addr = pcm_play_dma_get_peak_buffer(&count);

        framecount = frame_period*pcm_curr_sampr / HZ;
        count = MIN(framecount, count);

        if (count > 0)
            pcm_peak_peeker(addr, count, peaks);
    }
    else
    {
        peaks[0] = peaks[1] = 0;
    }

    if (left)
        *left = peaks[0];

    if (right)
        *right = peaks[1];
}

/****************************************************************************
 * Functions that do not require targeted implementation but only a targeted
 * interface
 */

/* This should only be called at startup before any audio playback or
   recording is attempted */
void pcm_init(void)
{
    logf("pcm_init");

    pcm_play_dma_stopped_callback();

    logf(" pcm_play_dma_init");
    pcm_play_dma_init();
}

/* Common code to pcm_play_data and pcm_play_pause */
static void pcm_play_data_start(unsigned char *start, size_t size)
{
    if (!(start && size))
    {
        pcm_more_callback_type get_more = pcm_callback_for_more;
        size = 0;
        if (get_more)
        {
            logf(" get_more");
            get_more(&start, &size);
        }
    }

    if (start && size)
    {
        logf(" pcm_play_dma_start");
        pcm_play_dma_start(start, size);
        pcm_playing = true;
        pcm_paused = false;
        return;
    }

    /* Force a stop */
    logf(" pcm_play_dma_stop");
    pcm_play_dma_stop();
    pcm_play_dma_stopped_callback();
}

void pcm_play_data(pcm_more_callback_type get_more,
                   unsigned char *start, size_t size)
{
    logf("pcm_play_data");

    pcm_play_lock();

    pcm_callback_for_more = get_more;

    logf(" pcm_play_dma_start");
    pcm_play_data_start(start, size);

    pcm_play_unlock();
}

void pcm_play_pause(bool play)
{
    logf("pcm_play_pause: %s", play ? "play" : "pause");

    pcm_play_lock();

    if (play == pcm_paused && pcm_playing)
    {
        if (!play)
        {
            logf(" pcm_play_dma_pause");
            pcm_play_dma_pause(true);
            pcm_paused = true;
        }
        else if (pcm_get_bytes_waiting() > 0)
        {
            logf(" pcm_play_dma_pause");
            pcm_play_dma_pause(false);
            pcm_paused = false;
        }
        else
        {
            logf(" pcm_play_dma_start: no data");
            pcm_play_data_start(NULL, 0);
        }
    }
    else
    {
        logf(" no change");
    }

    pcm_play_unlock();
}

void pcm_play_stop(void)
{
    logf("pcm_play_stop");

    pcm_play_lock();

    if (pcm_playing)
    {
        logf(" pcm_play_dma_stop");
        pcm_play_dma_stop();
        pcm_play_dma_stopped_callback();
    }
    else
    {
        logf(" not playing");
    }

    pcm_play_unlock();
}

void pcm_play_dma_stopped_callback(void)
{
    pcm_callback_for_more = NULL;
    pcm_paused = false;
    pcm_playing = false;
}

/**/

bool pcm_is_playing(void)
{
    return pcm_playing;
}

bool pcm_is_paused(void)
{
    return pcm_paused;
}

void pcm_mute(bool mute)
{
#ifndef SIMULATOR
    audiohw_mute(mute);
#endif

    if (mute)
        sleep(HZ/16);
}

#ifdef HAVE_RECORDING
/** Low level pcm recording apis **/

/* Next start for recording peaks */
const volatile void *pcm_rec_peak_addr NOCACHEBSS_ATTR = NULL;
/* the registered callback function for when more data is available */
volatile pcm_more_callback_type2
    pcm_callback_more_ready NOCACHEBSS_ATTR = NULL;
/* DMA transfer in is currently active */
volatile bool pcm_recording NOCACHEBSS_ATTR = false;

/**
 * Return recording peaks - From the end of the last peak up to
 *                          current write position.
 */
void pcm_calculate_rec_peaks(int *left, int *right)
{
    static int peaks[2];

    if (pcm_recording)
    {
        const void *addr;
        int count;

        addr = pcm_rec_dma_get_peak_buffer(&count);

        if (count > 0)
        {
            pcm_peak_peeker(addr, count, peaks);

            if (addr == pcm_rec_peak_addr)
                pcm_rec_peak_addr = (int32_t *)addr + count;
        }
    }
    else
    {
        peaks[0] = peaks[1] = 0;
    }

    if (left)
        *left = peaks[0];

    if (right)
        *right = peaks[1];
} /* pcm_calculate_rec_peaks */

/****************************************************************************
 * Functions that do not require targeted implementation but only a targeted
 * interface
 */
void pcm_init_recording(void)
{
    logf("pcm_init_recording");

    /* Recording init is locked unlike general pcm init since this is not
     * just a one-time event at startup and it should and must be safe by
     * now. */
    pcm_rec_lock();

    logf(" pcm_rec_dma_init");
    pcm_rec_dma_stopped_callback();
    pcm_rec_dma_init();

    pcm_rec_unlock();
}

void pcm_close_recording(void)
{
    logf("pcm_close_recording");

    pcm_rec_lock();

    if (pcm_recording)
    {
        logf(" pcm_rec_dma_stop");
        pcm_rec_dma_stop();
        pcm_rec_dma_stopped_callback();
    }

    logf(" pcm_rec_dma_close");
    pcm_rec_dma_close();

    pcm_rec_unlock();
}

void pcm_record_data(pcm_more_callback_type2 more_ready,
                     void *start, size_t size)
{
    logf("pcm_record_data");

    if (!(start && size))
    {
        logf(" no buffer");
        return;
    }

    pcm_rec_lock();

    pcm_callback_more_ready = more_ready;

    logf(" pcm_rec_dma_start");
    pcm_rec_dma_start(start, size);
    pcm_recording = true;

    pcm_rec_unlock();
} /* pcm_record_data */

void pcm_stop_recording(void)
{
    logf("pcm_stop_recording");

    pcm_rec_lock();

    if (pcm_recording)
    {
        logf(" pcm_rec_dma_stop");
        pcm_rec_dma_stop();
        pcm_rec_dma_stopped_callback();
    }

    pcm_rec_unlock();
} /* pcm_stop_recording */

void pcm_rec_dma_stopped_callback(void)
{
    pcm_recording = false;
    pcm_callback_more_ready = NULL;
}

#endif /* HAVE_RECORDING */
