/**
 * @file ws2812_spi.c
 * @author your name (you@domain.com)
 * @brief 通过 SPI 硬件模拟 PWM 信号，驱动 WS2812 的程序
 * @version 0.1
 * @date 2023-10-05
 *
 * @copyright Copyright (c) 2023
 *
 */

#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <sys/cdefs.h>

#include "esp_log.h"
#include "esp_attr.h"
#include "esp_rom_gpio.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "soc/spi_periph.h"
#include "soc/gpio_periph.h"
#include "config/board_config.h"

#include "ws2812_spi.h"

#include "os/os.h"

#define CONFIG_SYS_LOG_LEVEL SYS_LOG_LEVEL_INF
#define SYS_LOG_DOMAIN "LED_STRIP_SPI"
#include "sys_log.h"

#define SPI_CLK_HZ 8000000
#define WS_HIGH 0x3
#define WS_LOW 0x3F
#define WS_RESET ~0
#define RESET_TIME_US 50
#define RESET_BIT_LENGTH (SPI_CLK_HZ * (RESET_TIME_US) / 1000000)
#define _POW_Y 1.3

typedef struct ws2812_spi_led_strip_s
{
    uint32_t buffer_size;
    uint16_t pow_tbl[0x100];
    uint32_t max_led_num;
    uint8_t dma_buffer[0];
} __ws2812_spi_led_buf_t;

static struct
{
    spi_host_device_t host_id;
    spi_device_handle_t spi_hdl;
    os_mutex_t mutex_hdl;
    uint8_t spi_pin;
    int max_transfer_sz;
} s_cm;

/**
 * @brief 初始化工作环境。这将初始化 SPI 驱动并申请显示缓存到对象中。可重初始化
 *
 * @param led_strip[out]    灯带驱动缓存
 * @param host_id           SPI peripheral that controls this bus @ref spi_host_device_t
 * @param max_led_num       最大可驱动灯条共多少个灯珠
 */
void ws2812_spi_led_strip_init(uint8_t host_id, uint16_t max_led_num)
{
    SYS_ASSERT(max_led_num > 0, "");

    int max_transfer_sz = max_led_num * 24 + RESET_BIT_LENGTH * 2;

    if (s_cm.max_transfer_sz > 0)
    {
        SYS_ASSERT(max_transfer_sz <= s_cm.max_transfer_sz, "");
    }

    if (s_cm.spi_hdl == NULL)
    {
        s_cm.max_transfer_sz = max_transfer_sz;
        esp_err_t ret;
        spi_bus_config_t buscfg = {
            .miso_io_num = -1,
            .mosi_io_num = -1,
            .sclk_io_num = -1,
            .quadwp_io_num = -1,
            .quadhd_io_num = -1,
            .max_transfer_sz = max_transfer_sz};

        // Initialize the SPI bus
        ret = spi_bus_initialize(host_id, &buscfg, SPI_DMA_CH_AUTO);
        ESP_ERROR_CHECK(ret);

        spi_device_interface_config_t devcfg = {
            .clock_speed_hz = SPI_CLK_HZ,
            .mode = 0,
            .spics_io_num = -1,
            .queue_size = 1,
            .pre_cb = NULL,
            .command_bits = 0,
            .address_bits = 0};
        // Attach the LCD to the SPI bus
        ret = spi_bus_add_device(host_id, &devcfg, &s_cm.spi_hdl);
        ESP_ERROR_CHECK(ret);

        s_cm.host_id = host_id;
        s_cm.spi_pin = ~0;
    }

    if (!os_mutex_is_valid(&s_cm.mutex_hdl))
    {
        os_mutex_create(&s_cm.mutex_hdl);
    }
}

/**
 * @brief 创建灯带驱动缓存
 *
 * @param led_strip[out]    灯带驱动缓存
 * @param leds              可驱动灯条共多少个灯珠
 */
ws2812_spi_led_buf_t *ws2812_spi_led_new_buf(uint16_t leds)
{
    __ws2812_spi_led_buf_t *led_strip;
    SYS_ASSERT(leds > 0, "");

    int max_transfer_sz = leds * 24 + RESET_BIT_LENGTH * 2;
    if (s_cm.max_transfer_sz > 0)
    {
        SYS_ASSERT(max_transfer_sz <= s_cm.max_transfer_sz, "");
    }

    led_strip = heap_caps_malloc(sizeof(__ws2812_spi_led_buf_t) + max_transfer_sz, MALLOC_CAP_DMA); // Critical to be DMA memory.
    memset(led_strip->dma_buffer, WS_RESET, max_transfer_sz);
    led_strip->buffer_size = max_transfer_sz;
    led_strip->max_led_num = leds;
    float max = pow(0x100, _POW_Y) / 0x100;
    for (int i = 0; i < 0x100; i++)
    {
        led_strip->pow_tbl[i] = (int)(pow(i, _POW_Y) / max) + 1;
        if (led_strip->pow_tbl[i] > 0xFF)
        {
            led_strip->pow_tbl[i] = 0xFF;
        }
    }
    led_strip->pow_tbl[0] = 0;

    /* 使所有数据合法化 */
    ws2812_spi_led_strip_clear((ws2812_spi_led_buf_t *)led_strip);

    return (ws2812_spi_led_buf_t *)led_strip;
}

/**
 * @brief 使所有显存清零。注意需要通过 ws2812_spi_led_strip_refresh() 方可把数据刷新到灯带中。
 *
 * @param buf 灯带驱动缓存
 */
void ws2812_spi_led_strip_clear(ws2812_spi_led_buf_t *buf)
{
    __ws2812_spi_led_buf_t *led_strip = (__ws2812_spi_led_buf_t *)buf;
    if (led_strip == NULL)
    {
        SYS_LOG_WRN("led strip buffer is null");
        return;
    }

    memset(led_strip->dma_buffer, WS_RESET, led_strip->buffer_size);
    for (int i = 0; i < led_strip->max_led_num; i++)
    {
        ws2812_spi_led_strip_set_pixel((ws2812_spi_led_buf_t *)led_strip, i, 0, 0, 0);
    }
}

/**
 * @brief 设一个灯珠为一个像素，设置单个像素的颜色。注意需要通过 ws2812_spi_led_strip_refresh() 方可把数据刷新到灯带中。
 *
 * @param buf   灯带驱动缓存
 * @param index 灯像素序号，0 .. led_strip->max_led_num - 1
 * @param red   红基色亮度，0 .. 255
 * @param green 绿基色亮度，0 .. 255
 * @param blue  蓝基色亮度，0 .. 255
 */
void ws2812_spi_led_strip_set_pixel(ws2812_spi_led_buf_t *buf, uint32_t index, uint32_t red, uint32_t green, uint32_t blue)
{
    __ws2812_spi_led_buf_t *led_strip = (__ws2812_spi_led_buf_t *)buf;
    if (led_strip == NULL)
    {
        SYS_LOG_WRN("led strip buffer is null");
        return;
    }

    if (index > led_strip->max_led_num)
    {
        SYS_LOG_WRN("index: %u > led_strip->max_led_num: %u", index, led_strip->max_led_num);
        return;
    }

    if (red > 255 || green > 255 || blue > 255)
    {
        SYS_LOG_WRN("param over range");
        return;
    }

    /* 套指数表 */
    red = led_strip->pow_tbl[red];
    green = led_strip->pow_tbl[green];
    blue = led_strip->pow_tbl[blue];
    uint32_t color = (green & 0xFF) << 16 | (red & 0xFF) << 8 | (blue & 0xFF);

    /* PWM 编码 */
    for (int i = 0; i < 24; i++)
    {
        int pos = RESET_BIT_LENGTH + index * 24 + i;
        led_strip->dma_buffer[pos] = ((color << i) & 0x800000) ? WS_HIGH : WS_LOW;
    }
}

/**
 * @brief 执行把显示缓存刷新到灯带
 *
 * @param buf   灯带驱动缓存
 * @param leds  实际刷新的灯珠数
 * @param pin   使用的引脚
 * @retval 0 操作成功
 * @retval != 0 操作失败
 */
int ws2812_spi_led_strip_refresh(ws2812_spi_led_buf_t *buf, uint32_t leds, uint8_t pin)
{
    __ws2812_spi_led_buf_t *led_strip = (__ws2812_spi_led_buf_t *)buf;
    if (led_strip == NULL || s_cm.spi_hdl == NULL)
    {
        SYS_LOG_ERR("led strip spi handler is null");
        return -1;
    }

    if (leds > led_strip->max_led_num)
    {
        SYS_LOG_WRN("leds(%d) > led_strip->max_led_num(%d)", leds, led_strip->max_led_num);
        leds = led_strip->max_led_num;
    }

    os_mutex_lock(&s_cm.mutex_hdl, OS_WAIT_FOREVER);

    uint32_t empty_bytes = (led_strip->max_led_num - leds) * 24;
    if (empty_bytes)
    {
        memset(&led_strip->dma_buffer[led_strip->buffer_size - empty_bytes], WS_RESET, empty_bytes);
    }

    spi_transaction_t t;
    memset(&t, 0, sizeof(t));
    t.length = led_strip->buffer_size * 8 - empty_bytes; // length is in bits
    t.tx_buffer = led_strip->dma_buffer;

    if (s_cm.spi_pin != pin)
    {
        if (GPIO_USED(s_cm.spi_pin))
        {
            drv_gpio_pin_configure(s_cm.spi_pin, _GPIO_DIR_OUT, _GPIO_PUD_PULL_UP);
        }

        if (GPIO_USED(pin))
        {
            drv_gpio_pin_configure(pin, _GPIO_DIR_OUT, _GPIO_PUD_PULL_UP);
            esp_rom_gpio_connect_out_signal(pin, spi_periph_signal[s_cm.host_id].spid_out, true, false);
        }
        s_cm.spi_pin = pin;
    }

    esp_err_t err = spi_device_transmit(s_cm.spi_hdl, &t);

    os_mutex_unlock(&s_cm.mutex_hdl);

    ESP_ERROR_CHECK(err);
    return err;
}
