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


#include <eCxx/Platform/WS2812BDriver.h>

#include "CoreSys.h"


namespace eCxx {


/*
 * https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them
 *
 * Datasheet timing constraints
 *     Symbol    Time H (nS)   Time L (nS)
 *     -------   -----------   -----------
 *     Logic 0   350 ± 150         800 ± 150
 *     Logic 1   700 ± 150         600 ± 150
 *     Reset                   ≥ 50000
 *
 * Simplified timing constraints
 *     Symbol    Time H (nS)   Time L (nS)
 *     -------   -----------   -----------
 *     Logic 0   200 -  500    450 ~ 5000
 *     Logic 1   550 - 5500    450 ~ 5000
 *     Reset                   ≥ 6000
 *
 * DMA bit pattern
 *     Symbol    Bit pattern
 *     -------   -----------
 *     Logic 0   1000
 *     Logic 1   1110
 *
 * One bit becomes four bits. Therefore, for 800 kBps and 24-bit RGB pixel:
 *     # The sample rate must be set to 800000 / 32 * 4 = 100000
 *     # Each RGB 888 data frame (24 bits) will become 24 * 4 / 32 = 3 bytes
 *     # Minimum DMA buffer size = (number_of_leds * 3) bytes
 */


//
// Convert one uint8_t to WS2812B bitstream
//
static inline eCxx::uint32_t cnvU8ToBs(eCxx::uint8_t v) __ECXX_ATTR_ALWAYS_INLINE __ECXX_ATTR_OPTIMIZE_2;
static inline eCxx::uint32_t cnvU8ToBs(eCxx::uint8_t v)
{
    static constexpr eCxx::uint16_t cnvNibbleLUT[16] = {
        0b1000100010001000, 0b1000100010001110, 0b1000100011101000, 0b1000100011101110,
        0b1000111010001000, 0b1000111010001110, 0b1000111011101000, 0b1000111011101110,
        0b1110100010001000, 0b1110100010001110, 0b1110100011101000, 0b1110100011101110,
        0b1110111010001000, 0b1110111010001110, 0b1110111011101000, 0b1110111011101110
    };

    return ( static_cast<eCxx::uint32_t>( cnvNibbleLUT[v & 0x0F] )       ) |
           ( static_cast<eCxx::uint32_t>( cnvNibbleLUT[v >>   4] ) << 16 );
}


//
// I2S callback function
//
static uint8_t* ws2812b_rawByteBuffer = nullptr;
static uint16_t ws2812b_rawByteSize   = 0;

static void ICACHE_RAM_ATTR ws2812b_i2sCallback()
{
    // WARNING : Do not disable interrupt here or the system will hung and restart!

    if(!ws2812b_rawByteBuffer) return;

    const uint8_t* i = ws2812b_rawByteBuffer;
    const uint8_t* e = ws2812b_rawByteBuffer + ws2812b_rawByteSize;

    while(i != e) {
        Application::i2sOut().writeSample( cnvU8ToBs(*i), false );
        ++i;
    }

    while( Application::i2sOut().writeSample(0, true) );
}


//
// A simple WS2812B-based LED strip/matrix driver
//
WS2812BDriver::WS2812BDriver()
: _pin(0), _rawByteBuffer(nullptr), _rawByteSize(0)
{}


WS2812BDriver::~WS2812BDriver()
{ if(_rawByteBuffer) end(); }


bool WS2812BDriver::begin(uint8_t pin, uint16_t numberOfLEDs, const uint8_t* rawByteBuffer)
{
    // Check if already initialized
    if(_rawByteBuffer) return false;

    // Only one WS2812B driver can be active in NodeMCU platform
    if(ws2812b_rawByteBuffer) return false;

    // Check the parameters
    if(pin != NodeMCU::I2S_DATA || !numberOfLEDs || numberOfLEDs > MaximumNumberOfLEDs || !rawByteBuffer) return false;

    // Save the settings
    _pin           = pin;
    _rawByteBuffer = rawByteBuffer;
    _rawByteSize   = numberOfLEDs * 3;

    // Allocate internal buffer
    ws2812b_rawByteBuffer = reinterpret_cast<uint8_t*>( malloc(_rawByteSize) );
    ws2812b_rawByteSize   = _rawByteSize;

    // Set pin mode to output and set it to low
    Application::setPinMode(_pin, Application::PinMode::Output);
    Application::digitalWrite(_pin, false);

    // Initialize I2SOut
    Application::i2sOut().begin(
        100000,              // Sample rate
        ws2812b_i2sCallback, // WS2812B callback function
        _rawByteSize + 32,   // Increase the DMA buffer size for the trailing zeroes (WS2812B reset code)
        false,               // Disable I2S_BCK pin
        false                // Disable I2S_WS  pin
    );

    // Done
    return true;
}


void WS2812BDriver::end()
{
    // Check if it is not yet initialized
    if(!_rawByteBuffer) return;

    // Uninitialize I2SOut
    Application::i2sOut().end();

    // Set pin mode to input
    Application::setPinMode(_pin, Application::PinMode::Input);

    // Deallocate internal buffer
    free(ws2812b_rawByteBuffer);

    ws2812b_rawByteBuffer = nullptr;
    ws2812b_rawByteSize   = 0;

    // Clear the settings
    _rawByteBuffer = nullptr;
    _rawByteSize   = 0;
}


void WS2812BDriver::update()
{
    // Check if it is not yet initialized
    if(!_rawByteBuffer) return;

    // Ensure atomic operation
    eCxx::AtomicGuard g;

    // Copy the data to the internal buffer for safety
    memcpy(ws2812b_rawByteBuffer, _rawByteBuffer, _rawByteSize);
}


} // namespace eCxx
