/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2011 Marcin Bukat
 * Copyright (C) 2011 Andrew Ryabinin
 *
 * 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 "system.h"
#include "kernel.h"
#include "audio.h"
#include "string.h"
#include "panic.h"
#include "audiohw.h"
#include "sound.h"
#include "pcm-internal.h"
#include "pcm_sink.h"

static int locked = 0;

/* Mask the DMA interrupt */
static void sink_lock(void)
{
    if (++locked == 1)
    {
        int old = disable_irq_save();
        INTC_IMR &= ~IRQ_ARM_HDMA; /* mask HDMA interrupt */ 
        restore_irq(old);
    }
}

/* Unmask the DMA interrupt if enabled */
static void sink_unlock(void)
{
    if(--locked == 0)
    {
        int old = disable_irq_save();
        INTC_IMR |= IRQ_ARM_HDMA; /* unmask HDMA interrupt */
        restore_irq(old);
    }
}

static void sink_dma_stop(void)
{
    HDMA_CON0 = 0x00;
    HDMA_ISR = 0x00;

    locked = 1;
}

static void hdma_i2s_transfer(const void *addr, size_t size)
{
    SCU_CLKCFG &= ~CLKCFG_HDMA; /* enable HDMA clock */

    commit_discard_dcache_range(addr, size);

    HDMA_ISRC0 = (uint32_t)addr;               /* source address */
    HDMA_IDST0 = (uint32_t)&I2S_TXR;           /* i2s tx fifo */
    HDMA_ICNT0 = (uint16_t)((size>>2) - 1);    /* number of dma transactions
                                                * of transfer size bytes
                                                * (zero based)
                                                */

    HDMA_ISR = ((1<<13) |    /* mask ch1 accumulation overflow irq */
                (1<<12) |    /* mask ch0 accumulation overflow irq */
                (1<<11) |    /* mask ch1 page count down irq */
                (0<<10) |    /* UNMASK ch0 page count down irq */
                 (1<<9) |    /* mask ch0 transfer irq */
                 (1<<8) |    /* mask ch1 transfer irq */
                 (0<<5) |    /* clear ch1 accumulation overflow flag */
                 (0<<4) |    /* clear ch0 accumulation overflow flag */
                 (0<<3) |    /* clear ch1 count down to zero flag */
                 (0<<2) |    /* clear ch0 count down to zero flag */
                 (0<<1) |    /* clear ch1 active flag */
                 (0<<0));    /* clear ch0 active flag */

    HDMA_ISCNT0 = 0x07;      /* slice size in transfer size units (zero base) */

    HDMA_IPNCNTD0 = 0x01;    /* page count */

    HDMA_CON0 = ((0<<23) |   /* page mode */
                 (1<<22) |   /* slice mode */
                 (1<<21) |   /* DMA enable */
                 (1<<18) |   /* generate interrupt */
                 (0<<16) |   /* on-the-fly is not supported by rk27xx */
                 (5<<13) |   /* transfer mode inc8 */
                  (6<<9) |   /* external hdreq from i2s tx */
                  (0<<7) |   /* increment source address */
                  (1<<5) |   /* fixed destination address */
                  (2<<3) |   /* transfer size = 32bits word */
                  (0<<1) |   /* command of software DMA (not relevant) */
                  (1<<0));   /* hardware trigger DMA mode */
}

static void sink_dma_start(const void *addr, size_t size)
{
    /* Stop any DMA in progress */
    sink_dma_stop();

    /* kick in DMA transfer */
    hdma_i2s_transfer(addr, size);
}

static void i2s_init(void)
{
#if defined(HAVE_RK27XX_CODEC)
    /* iomux I2S internal */
    SCU_IOMUXA_CON &= ~(1<<19);  /* i2s external bit */
    SCU_IOMUXB_CON &= ~((1<<4) | /* i2s_mclk */
                        (1<<3) | /* i2s_sdo */
                        (1<<2) | /* i2s_sdi */
                        (1<<1) | /* i2s_lrck */
                        (1<<0)); /* i2s_bck */
#else
    /* iomux I2S external */
    SCU_IOMUXA_CON |= (1<<19);   /* i2s external bit */
    SCU_IOMUXB_CON |= ((1<<4) |  /* i2s_mclk */
                       (1<<3) |  /* i2s_sdo */
                       (1<<2) |  /* i2s_sdi */
                       (1<<1) |  /* i2s_lrck */
                       (1<<0));  /* i2s_bck */
#endif

    /* enable i2s clocks */
    SCU_CLKCFG &= ~(CLKCFG_PCLK_I2S | CLKCFG_I2S);
    
    /* configure I2S module */
    I2S_IER = 0; /* disable all i2s interrupts */

    I2S_TXCTL = (1<<16) |  /* LRCK/SCLK = 64 */
                (4<<8)  |  /* MCLK/SCLK = 4 */
                (1<<4)  |  /* 16bit samples */
                (0<<3)  |  /* stereo */
                (0<<1)  |  /* I2S IF */
#ifdef CODEC_SLAVE
                (1<<0);    /* master mode */
#else
                (0<<0);    /* slave mode */
#endif

    /* the fifo is 16x32bits according to my tests
     * while the docs state 32x32bits
     */
    I2S_FIFOSTS = (1<<18) | /* Tx trigger level half full */
                  (1<<16);  /* Rx trigger level half full */
                  
    I2S_OPR = (1<<17) |  /* reset Tx */
              (1<<16) |  /* reset Rx */
              (0<<6)  |  /* HDMA Req1 enable */
              (1<<5)  |  /* HDMA Req2 disable */
              (0<<4)  |  /* Req1 for Tx fifo */
              (1<<3)  |  /* Req2 for Rx fifo */
              (0<<2)  |  /* normal operation */
#ifdef CODEC_SLAVE
              (1<<1)  |  /* start Tx (master mode) */
              (0<<0);    /* do not start Rx (master mode) */
                         /* setting Rx bit to 1 result in choppy audio */
#else
              (0<<1)  |  /* not used in slave mode */
              (0<<0);    /* not used in slave mode */
#endif
}

#ifdef CODEC_SLAVE
/* When codec is slave we need to setup i2s MCLK clock using codec pll.
 * The MCLK frequency is 256*codec frequency as i2s setup is:
 * LRCK/SCLK = 64 and MCLK/SCLK = 4 (see i2s_init() for reference)
 *
 * PLL output frequency:
 * Fout = ((Fref / (CLKR+1)) * (CLKF+1)) / (CLKOD+1)
 * Fref = 24 MHz
 */
static void set_codec_freq(unsigned int freq)
{
    long timeout;

    /* {CLKR, CLKF, CLKOD, CODECPLL_DIV} */
    static const unsigned int pcm_freq_params[HW_NUM_FREQ][4] = 
    {
        [HW_FREQ_96] = {24, 255, 4, 1},
        [HW_FREQ_48] = {24, 127, 4, 1},
        [HW_FREQ_44] = {24, 293, 4, 4},
        [HW_FREQ_32] = {24, 127, 4, 2},
        [HW_FREQ_24] = {24, 127, 4, 3},
        [HW_FREQ_22] = {24, 146, 4, 4},
        [HW_FREQ_16] = {24, 127, 5, 4},
        [HW_FREQ_12] = {24, 127, 4, 7},
        [HW_FREQ_11] = {24, 146, 4, 9},
        [HW_FREQ_8]  = {24, 127, 5, 9},
    };
    /* select divider output from codec pll */
    SCU_DIVCON1 &= ~((1<<9) | (0xF<<5));
    SCU_DIVCON1 |= (pcm_freq_params[freq][3]<<5);

    /* Codec PLL power up */
    SCU_PLLCON3 &= ~(1<<22);

    SCU_PLLCON3 = (1<<24) |    /* Saturation behavior enable */
                  (1<<23) |    /* Enable fast locking circuit */
                  (pcm_freq_params[freq][0]<<16) | /* CLKR factor */
                  (pcm_freq_params[freq][1]<<4)  | /* CLKF factor */
                  (pcm_freq_params[freq][2]<<1) ; /* CLKOD factor */

/* wait for CODEC PLL lock with 10 ms timeout
 * datasheet states that pll lock should take approx. 0.3 ms
 */
    timeout = current_tick + (HZ/100);
    while (!(SCU_STATUS & (1<<2)))
        if (TIME_AFTER(current_tick, timeout))
            break;

}
#endif

static void sink_dma_init(void)
{
    /* unmask HDMA interrupt in INTC */
    INTC_IMR |= IRQ_ARM_HDMA;
    INTC_IECR |= IRQ_ARM_HDMA;

    audiohw_preinit();
    
    i2s_init();
}

static void sink_set_freq(uint16_t freq)
{
#ifdef CODEC_SLAVE
    set_codec_freq(freq);
#endif

    audiohw_set_frequency(freq);
}

/* audio DMA ISR called when chunk from callers buffer has been transfered */
void INT_HDMA(void)
{
    const void *start;
    size_t size;

    if (pcm_play_dma_complete_callback(PCM_DMAST_OK, &start, &size))
    {
        hdma_i2s_transfer(start, size);
        pcm_play_dma_status_callback(PCM_DMAST_STARTED);
    }
}

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 = audiohw_postinit,
        .set_freq = sink_set_freq,
        .lock     = sink_lock,
        .unlock   = sink_unlock,
        .play     = sink_dma_start,
        .stop     = sink_dma_stop,
    },
};

/****************************************************************************
 ** Recording DMA transfer
 **/
/* TODO */
