/*
 * Copyright (C) 2020-2021 Aloysius Indrayanto
 *
 * This file is part of the eCxx library, see LICENSE file for the license details.
 */

/*
 * ----------------------------------------------------------------------------------
 * The code in this file is copied (with some modifications) from:
 *    core_esp8266_i2s.cpp -  I2S library for esp8266
 * ----------------------------------------------------------------------------------
 * Code taken and reworked from Espressif's I2S example
 *
 * Copyright (C) 2015 Hristo Gochkov. All rights reserved.
 *
 * This file is part of the esp8266 core for Arduino environment.
 *
 * This library is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any later
 * version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along
 * with this library; if not, write to the Free Software Foundation, Inc., 51
 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 * ----------------------------------------------------------------------------------
 */


#include <cmath>

#include <i2s_reg.h>
#include <ets_sys.h>

#include "CoreLib.h"
#include "CoreSys.h"
#include "I2SOutDriver.h"

#include <eCxx/Platform/NodeMCU/ESP8266Peripheral.h>
#include <eCxx/Platform/NodeMCU/PinName.h>


namespace eCxx {

namespace NodeMCU {


//
// Internal data
//
static constexpr uint16_t SlcBufLenMin =    4;         // Minimum length of each buffer (in 32-bit words)
static constexpr uint16_t SlcBufLenMax = 1024;         // Maximum length of each buffer (in 32-bit words)
static constexpr uint16_t SlcBufLenDef =   64;         // Default length of each buffer (in 32-bit words)

static constexpr uint8_t  SlcBufCnt    =    4;         // Number of buffers in the I2S circular buffer
static           uint16_t SlcBufLen    = SlcBufLenDef; // Current length of each buffer


// # A queue is used to keep track of the DMA buffers that are empty.
// # The ISR will push buffers to the back of the queue
// # The I2S transmitter will pull them from the front and fill them.
// # The queue contain pointers to the DMA buffers, not the data itself.
// # The queue depth is one smaller than the amount of buffers, because there is always
//   a buffer that is being used by the DMA subsystem right now and must not be written
//   simultaneously by user code.
struct SlcQueueItem {
             uint32_t      blockSize : 12;
             uint32_t      dataLen   : 12;
             uint32_t      unused    :  5;
             uint32_t      sub_sof   :  1;
             uint32_t      eof       :  1;
    volatile uint32_t      owner     :  1; // DMA can change this value
             uint32_t*     bufPtr;
             SlcQueueItem* nextItem;
};


struct I2SState {
             uint32_t*      slcQueue   [SlcBufCnt];
    volatile uint8_t        slcQueueLen;
             uint32_t*      slcBufPtr  [SlcBufCnt]; // Pointer to the I2S DMA buffer data
             SlcQueueItem   slcItems   [SlcBufCnt]; // I2S DMA buffer descriptors
             uint32_t*      curSlcBuf;              // Current buffer for writing
             uint32_t       curSlcBufPos;           // Position in the current buffer
             I2SCallbackISR callback;               // It must be a ICACHE_RAM_ATTR function that contains no long computational tasks
             void*          callbackArg;
             bool           resetFinishedItem;
             uint32_t       resetValue;
};


// This drive support I2S output (transmit) only
static I2SState* tx = nullptr;


// Last I2S sample rate requested
static uint32_t lastI2SSampleRate;


// Enabled pin flags
static bool enBCKPin = false;
static bool enWSPin  = false;


//
// Helper functions and ISR
//

// Pop the top off of the queue and return it
static uint32_t* ICACHE_RAM_ATTR i2sSlcQueueNextItem() __ECXX_ATTR_OPTIMIZE_2;
static uint32_t* ICACHE_RAM_ATTR i2sSlcQueueNextItem()
{
    uint32_t* item = tx->slcQueue[0];

    --tx->slcQueueLen;
    for(uint32_t i = 0; i < tx->slcQueueLen; ++i) tx->slcQueue[i] = tx->slcQueue[i + 1];

    return item;
}


// I2S ISR
static void ICACHE_RAM_ATTR i2sSlc_isr() __ECXX_ATTR_OPTIMIZE_2;
static void ICACHE_RAM_ATTR i2sSlc_isr()
{
    ETS_SLC_INTR_DISABLE();

    const uint32_t slcIntrStatus = SLCIS;

    SLCIC = 0xFFFFFFFF;

    if(slcIntrStatus & SLCIRXEOF) {
        // Get the finished item
        SlcQueueItem* finishedItem = reinterpret_cast<SlcQueueItem*>(SLCRXEDA);
        // When enabled, reset the finished item
        if(tx->resetFinishedItem) {
            for(uint32_t i = 0; i < SlcBufLen; ++i) {
                finishedItem->bufPtr[i] = tx->resetValue;
            }
        }
        // If all buffers are empty it means underflow
        if(tx->slcQueueLen >= SlcBufCnt - 1) {
            // Free space for finished_item
            i2sSlcQueueNextItem();
        }
        // Put the buffer pointer in the next queue
        tx->slcQueue[tx->slcQueueLen++] = finishedItem->bufPtr;
        // Call the user callback
        if(tx->callback) (*tx->callback)(tx->callbackArg);
    }

    ETS_SLC_INTR_ENABLE();
}


static bool i2sSlcBegin()
{
    // Allocate channel
    tx->slcQueueLen = 0;

    for(uint32_t i = 0; i < SlcBufCnt; ++i) {
        // Allocate memory
        tx->slcBufPtr[i] = reinterpret_cast<uint32_t*>( malloc( SlcBufLen * sizeof( tx->slcBufPtr[0][0] ) ) );
        if(!tx->slcBufPtr[i]) {
            // OOM, the upper layer will free up any partially allocated channels
            return false;
        }
        // Zeroes the memory
        memset( tx->slcBufPtr[i], 0, SlcBufLen * sizeof( tx->slcBufPtr[i][0] ) );
        // Set data
        tx->slcItems[i].unused    = 0;
        tx->slcItems[i].owner     = 1;
        tx->slcItems[i].eof       = 1;
        tx->slcItems[i].sub_sof   = 0;
        tx->slcItems[i].dataLen   = SlcBufLen * 4;
        tx->slcItems[i].blockSize = SlcBufLen * 4;
        tx->slcItems[i].bufPtr    = reinterpret_cast<uint32_t*>( &tx->slcBufPtr[i][0] );
        tx->slcItems[i].nextItem  = ( i < (SlcBufCnt - 1) )
                                  ? ( &tx->slcItems[i + 1] )
                                  : ( &tx->slcItems[0    ] );
    }

    ETS_SLC_INTR_DISABLE();

    SLCC0 |=  (SLCRXLR | SLCTXLR);
    SLCC0 &= ~(SLCRXLR | SLCTXLR);
    SLCIC  = 0xFFFFFFFF;

    // Configure DMA
    SLCC0   &= ~(SLCMM << SLCM);                  // Clear DMA MODE
    SLCC0   |= _BV(SLCM);                         // Set DMA MODE to 1
    SLCRXDC |= SLCBINR | SLCBTNR;                 // Enable INFOR_NO_REPLACE and TOKEN_NO_REPLACE
    SLCRXDC &= ~(SLCBRXFE | SLCBRXEM | SLCBRXFM); // Disable RX_FILL, RX_EOF_MODE, and RX_FILL_MODE

    // Feed DMA the 1st buffer description address
    // NOTE : # To send data to the I2S subsystem, counter-intuitively the RXLINK part is used (not the TXLINK part)!
    //        # The TXLINK part still needs a valid DMA descriptor, even if it is unused.
    SLCTXL &= ~(SLCTXLAM << SLCTXLA); // Clear TX descriptor address
    SLCRXL &= ~(SLCRXLAM << SLCRXLA); // Clear RX descriptor address

    SLCTXL |= reinterpret_cast<uint32_t>(&tx->slcItems[0]) << SLCTXLA; // Set dummy RX address
    SLCRXL |= reinterpret_cast<uint32_t>(&tx->slcItems[0]) << SLCRXLA; // Set real  TX address

    SLCIE = SLCIRXEOF; // Enable appropriate EOF IRQ

    ETS_SLC_INTR_ATTACH(i2sSlc_isr, nullptr);
    ETS_SLC_INTR_ENABLE();

    // Start transmission
    SLCTXL |= SLCTXLS;
    SLCRXL |= SLCRXLS;

    return true;
}


static void i2sSlcEnd()
{
    ETS_SLC_INTR_DISABLE();

    SLCIC   = 0xFFFFFFFF;
    SLCIE   = 0;
    SLCTXL &= ~(SLCTXLAM << SLCTXLA); // Clear TX descriptor address
    SLCRXL &= ~(SLCRXLAM << SLCRXLA); // Clear RX descriptor address

    for(uint32_t i = 0; i < SlcBufCnt; ++i) {
        free(tx->slcBufPtr[i]);
        tx->slcBufPtr[i] = nullptr;
    }
}


// Writes a buffer of frames into the DMA memory
// # Returns the amount of frames written
// # A frame is an int16_t for mono; two int16_t for stereo
static uint16_t i2sWriteBuffer(int16_t* frames, uint16_t frameCount, bool mono, bool nonBlocking)
{
    uint16_t framesWritten = 0;

    while(frameCount > 0) {
        // Make sure there is room in the current buffer
        if(!tx->curSlcBuf || tx->curSlcBufPos == SlcBufLen) {
            // Check if there are no buffers available
            if(tx->slcQueueLen == 0) {
                // Do not wait if nonblocking, simply return the number of frames written so far
                if(nonBlocking) break;
                // Wait
                for(;;) {
                    Application::yield();
                    if(tx->slcQueueLen > 0) break;
                }
            }
            // Get a new buffer
            ETS_SLC_INTR_DISABLE();
            tx->curSlcBuf = i2sSlcQueueNextItem();
            ETS_SLC_INTR_ENABLE();
            tx->curSlcBufPos = 0;
        }
        // Space available in the current buffer
        const uint16_t available = SlcBufLen - tx->curSlcBufPos;
              uint16_t fc        = (available < frameCount) ? available : frameCount;
        if(mono) {
            for(uint32_t i = 0; i < fc; ++i) {
                const uint16_t v = static_cast<uint16_t>(*frames++);
                tx->curSlcBuf[tx->curSlcBufPos++] = (v << 16) | v;
            }
        }
        else
        {
            for(uint32_t i = 0; i < fc; ++i) {
                const uint16_t v1 = static_cast<uint16_t>(*frames++);
                const uint16_t v2 = static_cast<uint16_t>(*frames++);
                tx->curSlcBuf[tx->curSlcBufPos++] = (v1 << 16) | v2;
            }
        }

        frameCount    -= fc;
        framesWritten += fc;
    }

    return framesWritten;
}


//
// I2S (output only) driver
//
extern bool i2sSetBufferLength(uint16_t bufferLength32)
{
    if(tx) return false;

         if(!bufferLength32               ) bufferLength32 = SlcBufLenDef;
    else if( bufferLength32 < SlcBufLenMin) bufferLength32 = SlcBufLenMin;
    else if( bufferLength32 > SlcBufLenMax) bufferLength32 = SlcBufLenMax;

    SlcBufLen = bufferLength32;

    return true;
}


extern bool i2sSetResetFinishedBuffer(bool enabled, uint32_t value)
{
    if(!tx) return false;

    tx->resetFinishedItem = enabled;
    tx->resetValue        = value;

    return true;
}


extern void i2sSetDividers(uint8_t div1, uint8_t div2)
{
    // Ensure dividers fit in bit fields
    div1 &= I2SBDM;
    div2 &= I2SCDM;

    // From:
    //     https://github.com/esp8266/Arduino/issues/2590
    // the transmitter must be reset while changing the configuration bits to avoid random distortion

    uint32_t i2scTemp = I2SC;

    // Hold transmitter in reset
    i2scTemp |= (I2STXR);
    I2SC      = i2scTemp;

    // Transmit master (active low), receive master (active_low), !bits_mod(==16 bits/channel), clear clock dividers
    i2scTemp &= ~( I2STSM | I2SRSM | (I2SBMM << I2SBM) | (I2SBDM << I2SBD) | (I2SCDM << I2SCD) );

    // I2SRF          = Right channel first (may be swapped form I2S spec of (WS==0) => left ?)
    // I2SMR          = MSB first
    // I2SRMS, I2STMS = 1-bit delay from WS to MSB (I2S format)
    // div1, div2 = Set I2S WS clock frequency.  BCLK seems to be generated from 32x this
    i2scTemp |= ( I2SRF | I2SMR | I2SRMS | I2STMS | (div1 << I2SBD) | (div2 << I2SCD) );
    I2SC      = i2scTemp;

    // Release reset
    i2scTemp &= ~(I2STXR);
    I2SC      = i2scTemp;
}


extern void i2sSetRate(uint32_t rateHz)
{
    if(rateHz == lastI2SSampleRate) return;
    lastI2SSampleRate = rateHz;

    uint32_t scaledBaseFreq = I2SBASEFREQ / 32;
    float    deltaBest      = scaledBaseFreq;

    uint8_t sbdDivBest = 1;
    uint8_t scdDivBest = 1;

    for(uint32_t i = 1; i < 64; ++i) {
        for(uint32_t j = i; j < 64; ++j) {
            const float newDelta = fabs( ( static_cast<float>(scaledBaseFreq) / i / j ) - rateHz );
            if(newDelta < deltaBest){
                deltaBest  = newDelta;
                sbdDivBest = i;
                scdDivBest = j;
            }
        }
    }

    i2sSetDividers(sbdDivBest, scdDivBest);
}


extern float i2sGetActualRate()
{ return static_cast<float>(I2SBASEFREQ) / 32 / ( (I2SC >> I2SBD) & I2SBDM ) / ( (I2SC >> I2SCD) & I2SCDM ); }


extern bool i2sBegin(I2SCallbackISR callback, void* callbackArg, bool enableBitClock, bool enableWordSelect)
{
    // Check if the callback is not in IRAM
    if( !NodeMCU::addressInIRAM(callback) ) {
        ::printf_P( PGM_STR("I2S callback is not in IRAM!\r\n").p );
        return false;
    }

    // Stop and free any previous initialization
    i2sEnd();

    // Allocate memory
    tx = reinterpret_cast<I2SState*>( calloc( 1, sizeof(*tx) ) ); // calloc() sets the memory to zero
    if(!tx) return false; // OOM error!

    tx->resetFinishedItem = true; // By default, reset the finished buffer to zero
    tx->resetValue        = 0;

    // Save the callback function and argument
    tx->callback    = callback;
    tx->callbackArg = callbackArg;

    // Set pin mode(s)
    enBCKPin = enableBitClock;
    enWSPin  = enableWordSelect;

                 pinMode(NodeMCU::I2S_DATA, FUNCTION_1);
    if(enBCKPin) pinMode(NodeMCU::I2S_BCK,  FUNCTION_1);
    if(enWSPin ) pinMode(NodeMCU::I2S_WS,   FUNCTION_1);

    if( !i2sSlcBegin() ) {
        // OOM in SLC memory allocations, tear it all down and abort
        i2sEnd();
        return false;
    }

    I2S_CLK_ENABLE();
    I2SIC = 0x3F;
    I2SIE = 0;

    // Reset I2S
    I2SC &= ~(I2SRST);
    I2SC |=   I2SRST;
    I2SC &= ~(I2SRST);

    // I2STXFMM, (I2SRXFMM==0) => 16-bit, dual channel data shifted in/out
    I2SFC &= ~( I2SDE | (I2STXFMM << I2STXFM) | (I2SRXFMM << I2SRXFM) ); // Set RX/TX FIFO_MOD = 0 and disable DMA (FIFO only)
    I2SFC |= I2SDE; // Enable DMA

    // I2STXCMM, (I2SRXCMM==0) => Dual channel mode
    I2SCC &= ~( (I2STXCMM << I2STXCM) | (I2SRXCMM << I2SRXCM) ); // Set RX/TX CHAN_MOD = 0

    // Ensure a sane clock is set, but do not change any pre-existing ones
    //
    // To make sure the other bits were not reset by a previous reset, store the present one, clear the flag,
    // then set the same value (writing all the needed configuration bits in the process)
    const uint32_t saveRate = lastI2SSampleRate;
    lastI2SSampleRate  = 0;
    i2sSetRate(saveRate ? saveRate : 44100);

    // Start transmission
    I2SC |= I2STXS;

    return true;
}


extern void i2sEnd()
{
    // Do nothing if I2S is not enabled
    if(!tx) return;

    // Disable any I2S send or receive
    I2SC &= ~(I2STXS | I2SRXS);

    // Reset I2S
    I2SC &= ~(I2SRST);
    I2SC |=   I2SRST;
    I2SC &= ~(I2SRST);

    i2sSlcEnd();

    // Restore pin mode(s)
                 pinMode(NodeMCU::I2S_DATA, INPUT);
    if(enBCKPin) pinMode(NodeMCU::I2S_BCK,  INPUT);
    if(enWSPin ) pinMode(NodeMCU::I2S_WS,   INPUT);

    enBCKPin = false;
    enWSPin  = false;

    // Free memory
    free(tx);
    tx = nullptr;
}


extern void i2sPause()
{
    if(!tx) return;

    I2SC   &= ~I2STXS;
    SLCRXL &= ~SLCRXLS;
}


extern void i2sResume()
{
    if(!tx) return;

    SLCRXL |= SLCRXLS;
    I2SC   |= I2STXS;
}


extern bool i2sIsFull()
{
    if(!tx) return false;
    return (!tx->curSlcBuf || tx->curSlcBufPos == SlcBufLen) && !tx->slcQueueLen;
}


extern bool i2sIsEmpty()
{
    if(!tx) return false;
    return tx->slcQueueLen >= (SlcBufCnt - 1);
}


extern uint16_t i2sAvailable()
{
    if(!tx) return 0;
    return (SlcBufCnt - tx->slcQueueLen) * SlcBufLen;
}


// Push a single, 32-bit sample to the I2S buffers; call it (on average) at least the current sample rate
extern bool i2sWriteSample(uint32_t sample, bool nonBlocking)
{
    if(!tx) return false;

    if(!tx->curSlcBuf || tx->curSlcBufPos == SlcBufLen) {
        if(!tx->slcQueueLen) {
            // Do not wait if nonblocking, simply notify the upper levels
            if(nonBlocking) return false;
            // Wait
            for(;;) {
                Application::yield();
                if(tx->slcQueueLen > 0) break;
            }
        }
        ETS_SLC_INTR_DISABLE();
        tx->curSlcBuf = i2sSlcQueueNextItem();
        ETS_SLC_INTR_ENABLE();
        tx->curSlcBufPos=0;
    }

    tx->curSlcBuf[tx->curSlcBufPos++] = sample;

    return true;
}


extern bool i2sWriteSampleLR(int16_t left, int16_t right)
{
    int32_t sample  = right & 0xFFFF;
            sample  = sample << 16;
            sample |= left  & 0xFFFF;

    return i2sWriteSample(sample);
}


extern uint16_t i2sWriteBufferMono(int16_t* frames, uint16_t frameCount, bool nonBlocking)
{ return i2sWriteBuffer(frames, frameCount, true, nonBlocking); }


extern uint16_t i2sWriteBufferStereo(int16_t* frames, uint16_t frameCount, bool nonBlocking)
{ return i2sWriteBuffer(frames, frameCount, false, nonBlocking); }


} // namespace NodeMCU

} // namespace eCxx
