/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2005 by Nick Lanham
 * Copyright (C) 2010 by Thomas Martitz
 *
 * 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.
 *
 ****************************************************************************/

#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))

#include "autoconf.h"

#include <stdlib.h>
#include <stdbool.h>
#include <stdio.h>

#include <3ds/ndsp/ndsp.h>
#include <3ds/ndsp/channel.h>
#include <3ds/services/dsp.h>
#include <3ds/synchronization.h>
#include <3ds/allocator/linear.h>

#include "config.h"
#include "debug.h"
#include "sound.h"
#include "audiohw.h"
#include "system.h"
#include "panic.h"

#ifdef HAVE_RECORDING
#include "audiohw.h"
#ifdef HAVE_SPDIF_IN
#include "spdif.h"
#endif
#endif

#include "pcm.h"
#include "pcm-internal.h"
#include "pcm_sampr.h"
#include "pcm_mixer.h"
#include "pcm_sink.h"

/*#define LOGF_ENABLE*/
#include "logf.h"

#ifdef DEBUG
extern bool debug_audio;
#endif

extern const char      *audiodev;

/* Bytes left in the Rockbox PCM frame buffer. */
static size_t _pcm_buffer_size = 0;


/* Rockbox PCM frame buffer. */
static const void  *_pcm_buffer = NULL;

/*
    1: PCM thread suspended.
    0: PCM thread running.
    These are used by pcm_play_[lock|unlock] or pcm_play_dma_[start|stop|pause]. These need to be
    separated because of nested calls for suspending and stopping.
*/
static volatile int _dsp_enabled  = 1;
static volatile int _pcm_shutdown = 0;


/* Mutex for PCM thread suspend/unsuspend. */
static RecursiveLock _pcm_lock_mtx;         /* audio device mutex */
static LightEvent    _dsp_callback_event;   /* dsp callback synchronization flag */

static Thread _pcm_thread;
static int _pcm_thread_id = -1;

/* DSP wave buffers */
static ndspWaveBuf _dsp_wave_bufs[3];
static s16 *_dsp_audio_buffer = NULL;

static inline bool is_in_audio_thread(int audio_thread_id)
{
    /* The device thread locks the same mutex, but not through the public API.
       This check is in case the application, in the audio callback,
       tries to lock the thread that we've already locked from the
       device thread...just in case we only have non-recursive mutexes. */
    if ( (sys_thread_id() == audio_thread_id)) {
        return true;
    }

    return false;
}

static void sink_lock(void)
{
    if (!is_in_audio_thread(_pcm_thread_id)) {
        RecursiveLock_Lock(&_pcm_lock_mtx);
    }
}

static void sink_unlock(void)
{
     if (!is_in_audio_thread(_pcm_thread_id)) {
        RecursiveLock_Unlock(&_pcm_lock_mtx);
    }
}

static void pcm_write_to_soundcard(const void *pcm_buffer, size_t pcm_buffer_size, ndspWaveBuf *dsp_buffer)
{
    s16 *buffer = dsp_buffer->data_pcm16;
    memcpy(buffer, pcm_buffer, pcm_buffer_size);

    dsp_buffer->nsamples = pcm_buffer_size / 2 / sizeof(s16);
    ndspChnWaveBufAdd(0, dsp_buffer);
    DSP_FlushDataCache(buffer, pcm_buffer_size);
}

bool fill_buffer(ndspWaveBuf *dsp_buffer)
{
    if(_pcm_buffer_size == 0)
    {
        /* Retrive a new PCM buffer from Rockbox. */
        if(!pcm_play_dma_complete_callback(PCM_DMAST_OK, &_pcm_buffer, &_pcm_buffer_size))
        {
            /* DEBUGF("DEBUG %s: No new buffer.\n", __func__); */
            svcSleepThread(10000);
            return false;
        }
    }
    pcm_play_dma_status_callback(PCM_DMAST_STARTED);

    /* This relies on Rockbox PCM frame buffer size == ALSA PCM frame buffer size. */
    pcm_write_to_soundcard(_pcm_buffer, _pcm_buffer_size, dsp_buffer);
    _pcm_buffer_size = 0;

    return true;
}

void pcm_thread_run(void* nothing)
{
    (void) nothing;

    DEBUGF("DEBUG %s: Thread start.\n", __func__);
    
    _pcm_thread_id = sys_thread_id();

    while(!AtomicGet(&_pcm_shutdown))
    {
        RecursiveLock_Lock(&_pcm_lock_mtx);
        for(size_t i = 0; i < ARRAY_SIZE(_dsp_wave_bufs); ++i) {
            if(_dsp_wave_bufs[i].status != NDSP_WBUF_DONE) {
                continue;
            }
            
            if(!fill_buffer(&_dsp_wave_bufs[i])) {
                continue;
            }
        }
        RecursiveLock_Unlock(&_pcm_lock_mtx);

        // Wait for a signal that we're needed again before continuing,
        // so that we can yield to other things that want to run
        // (Note that the 3DS uses cooperative threading)
        LightEvent_Wait(&_dsp_callback_event);
    }

    DEBUGF("DEBUG %s: Thread end.\n", __func__);
}

void dsp_callback(void *const nul_) {
    (void)nul_;

    if(AtomicGet(&_pcm_shutdown)) {
        return;
    }
    
    LightEvent_Signal(&_dsp_callback_event);
}

static void sink_set_freq_nolock(uint16_t freq)
{
    ndspChnReset(0);
    
    ndspSetOutputMode(NDSP_OUTPUT_STEREO);

    ndspChnSetRate(0, hw_freq_sampr[freq]);
    ndspChnSetFormat(0, NDSP_FORMAT_STEREO_PCM16);
    /* ndspChnSetInterp(0, NDSP_INTERP_POLYPHASE); */
    /* ndspChnSetInterp(0, NDSP_INTERP_NONE); */
    ndspChnSetInterp(0, NDSP_INTERP_LINEAR);

    float mix[12];
    memset(mix, 0, sizeof(mix));
    mix[0] = 1.0;
    mix[1] = 1.0;
    ndspChnSetMix(0, mix);

    memset(_dsp_wave_bufs, 0, sizeof(_dsp_wave_bufs) * ARRAY_SIZE(_dsp_wave_bufs));
    
    const size_t wave_buffer_size = MIX_FRAME_SAMPLES * 2 * sizeof(s16);
    size_t buffer_size = wave_buffer_size * ARRAY_SIZE(_dsp_wave_bufs);

    _dsp_audio_buffer = (s16 *)linearAlloc(buffer_size);

    s16 *buffer = _dsp_audio_buffer;
    for (unsigned i = 0; i < ARRAY_SIZE(_dsp_wave_bufs); i++) {
        _dsp_wave_bufs[i].data_vaddr = buffer;
        _dsp_wave_bufs[i].status     = NDSP_WBUF_DONE;
        
        buffer += wave_buffer_size / sizeof(buffer[0]);
    }
    
    ndspChnSetPaused(0, true);
    ndspSetCallback(dsp_callback, NULL);

    AtomicSet(&_pcm_shutdown, 0);
    AtomicSet(&_dsp_enabled, 1);

    // Start the thread, passing our opusFile as an argument.
    _pcm_thread = threadCreate(pcm_thread_run,
                               NULL,
                               32 * 1024,       /* 32kB stack size */
                               0x18,            /* high priority */ 
                              -1,              /* run on any core */ false);
}

static void sink_dma_start(const void *addr, size_t size)
{
    _pcm_buffer = addr;
    _pcm_buffer_size = size;

    RecursiveLock_Lock(&_pcm_lock_mtx);
    ndspChnSetPaused(0, false);
    RecursiveLock_Unlock(&_pcm_lock_mtx);
}

static void sink_dma_stop(void)
{
    RecursiveLock_Lock(&_pcm_lock_mtx);
    ndspChnSetPaused(0, true);
    RecursiveLock_Unlock(&_pcm_lock_mtx);
}

/* TODO: implement recording */
#ifdef HAVE_RECORDING
void pcm_rec_lock(void)
{
}

void pcm_rec_unlock(void)
{
}

void pcm_rec_dma_init(void)
{
}

void pcm_rec_dma_close(void)
{
}

void pcm_rec_dma_start(void *start, size_t size)
{
    (void)start;
    (void)size;
    
    
}

void pcm_rec_dma_stop(void)
{
}

const void * pcm_rec_dma_get_peak_buffer(void)
{
    return NULL;
}

void audiohw_set_recvol(int left, int right, int type)
{
    (void)left;
    (void)right;
    (void)type;
}

#ifdef HAVE_SPDIF_IN
unsigned long spdif_measure_frequency(void)
{
    return 0;
}
#endif

#endif /* HAVE_RECORDING */

static void sink_dma_init(void)
{
    Result ndsp_init_res = ndspInit();
    if (R_FAILED(ndsp_init_res)) {
        if ((R_SUMMARY(ndsp_init_res) == RS_NOTFOUND) && (R_MODULE(ndsp_init_res) == RM_DSP)) {
            logf("DSP init failed: dspfirm.cdc missing!");
        } else {
            logf("DSP init failed. Error code: 0x%lX", ndsp_init_res);
        }
        return;
    }

    RecursiveLock_Init(&_pcm_lock_mtx);
    LightEvent_Init(&_dsp_callback_event, RESET_ONESHOT);
}

static void sink_dma_postinit(void)
{
}

static void sink_set_freq(uint16_t freq)
{
    sink_lock();
    sink_set_freq_nolock(freq);
    sink_unlock();
}

void pcm_close_device(void)
{
    RecursiveLock_Lock(&_pcm_lock_mtx);
    AtomicSet(&_pcm_shutdown, 1);
    LightEvent_Signal(&_dsp_callback_event);

    threadJoin(_pcm_thread, UINT64_MAX);
    threadFree(_pcm_thread);
    _pcm_thread_id = -1;
    RecursiveLock_Unlock(&_pcm_lock_mtx);
    
    ndspSetCallback(NULL, NULL);
    
    ndspChnReset(0);
    if (_dsp_audio_buffer != NULL) {
        linearFree(_dsp_audio_buffer);
        _dsp_audio_buffer = NULL;
    }

    ndspExit();
}

/* moved from drivers/audio/ctru.c */
void audiohw_close(void)
{
    pcm_close_device();
}

struct pcm_sink builtin_pcm_sink = {
    .caps = {
        .samprs       = hw_freq_sampr,
        .num_samprs   = HW_NUM_FREQ,
        .default_freq = HW_FREQ_DEFAULT,
    },
    .ops = {
        .init     = sink_dma_init,
        .postinit = sink_dma_postinit,
        .set_freq = sink_set_freq,
        .lock     = sink_lock,
        .unlock   = sink_unlock,
        .play     = sink_dma_start,
        .stop     = sink_dma_stop,
    },
};
