/*
 * Copyright (c) 2026 Martin Javier Klöckner <mklockner@fi.uba.ar>
 *
 * See file `LICENSE` for full details
 */

#include "main.h"
#include "display_i2c.h"
#include <stdbool.h>

// Demo includes
#include "logger.h"
#include "dwt.h"
#include "systick.h"

#define DISPLAY_I2C_ADDRESS         0x27
#define DISPLAY_ASYNC_QUEUE_LEN     256
#define DISPLAY_ASYNC_CLEAR_DEL_MS  2ul
#define DISPLAY_TICK_CNT_INI_MS     0ul
#define DISPLAY_TICK_DMA_TIMEOUT_MS 100ul

#define DISPLAY_INIT_SET_8BIT 0x03
#define DISPLAY_INIT_SET_4BIT   0x02

#define DISPLAY_CMD_CLEAR_DISPLAY   0x01
#define DISPLAY_CMD_SET_CURSOR_HOME 0x02
#define DISPLAY_CMD_ENTRY_MODE_SET  0x04
#define DISPLAY_CMD_DISPLAY_CONTROL 0x08
#define DISPLAY_CMD_SHIFT_CONTROL   0x10
#define DISPLAY_CMD_FUNCTION_SET    0x20
#define DISPLAY_CGRAM_ADDR          0x40
#define DISPLAY_DDRAM_ADDR          0x80

#define DISPLAY_CMD_FUNCTION_SET_8BITS    0x10
#define DISPLAY_CMD_FUNCTION_SET_4BITS    0x00
#define DISPLAY_CMD_FUNCTION_SET_5x8DOTS  0x00
#define DISPLAY_CMD_FUNCTION_SET_2LINES   0x08
#define DISPLAY_CMD_FUNCTION_SET_1LINE    0x00
#define DISPLAY_CMD_FUNCTION_SET_5x10DOTS 0x04

#define DISPLAY_CMD_ENTRY_MODE_SET_INCREMENT_CURSOR 0x02
#define DISPLAY_CMD_ENTRY_MODE_SET_DECREMENT_CURSOR 0x00
#define DISPLAY_CMD_ENTRY_MODE_SET_SHIFT            0x01
#define DISPLAY_CMD_ENTRY_MODE_SET_NO_SHIFT         0x00

#define DISPLAY_CMD_DISPLAY_CONTROL_DISPLAY_ON   0x04
#define DISPLAY_CMD_DISPLAY_CONTROL_DISPLAY_OFF  0x00
#define DISPLAY_CMD_DISPLAY_CONTROL_CURSOR_ON    0x02
#define DISPLAY_CMD_DISPLAY_CONTROL_CURSOR_OFF   0x00
#define DISPLAY_CMD_DISPLAY_CONTROL_BLINK_ON     0x01
#define DISPLAY_CMD_DISPLAY_CONTROL_BLINK_OFF    0x00

#define DISPLAY_LINE1_FIRST_CHAR_ADDR 0
#define DISPLAY_LINE2_FIRST_CHAR_ADDR 64
#define DISPLAY_LINE3_FIRST_CHAR_ADDR 20
#define DISPLAY_LINE4_FIRST_CHAR_ADDR 84

// Private variables
typedef enum {
    DISPLAY_CMD,
    DISPLAY_CHAR,
    DISPLAY_DELAY
} display_data_type_e;

typedef struct {
    display_data_type_e type;
    uint8_t data;
} display_data_t;

typedef struct {
    uint8_t head;
    uint8_t tail;
    uint8_t count;
    display_data_t queue[DISPLAY_ASYNC_QUEUE_LEN];
} display_async_queue_t;

static bool b_display_i2c_dma_done;
static uint32_t g_display_i2c_tick, g_display_i2c_dma_tick;
static uint8_t async_buffer[4];
static display_async_queue_t display_async_queue;
static display_data_t curr_tx_data;

// Private functions declaration
static void display_i2c_send_cmd(uint8_t cmd);
static void display_i2c_send_data(uint8_t data);
static void display_i2c_async_write(display_data_t data);
static void display_i2c_async_enqueue(display_data_t data);
static display_data_t display_i2c_async_dequeue(void);
void display_i2c_recover(void);
uint8_t display_i2c_is_stuck(void);

// Public functions definition
void display_i2c_init(void)
{
    HAL_Delay(50);

    display_i2c_send_cmd(DISPLAY_INIT_SET_8BIT);
    HAL_Delay(5);

    display_i2c_send_cmd(DISPLAY_INIT_SET_8BIT);
    HAL_Delay(1);

    display_i2c_send_cmd(DISPLAY_INIT_SET_8BIT);
    HAL_Delay(1);

    display_i2c_send_cmd(DISPLAY_INIT_SET_4BIT);
    HAL_Delay(1);

    display_i2c_send_cmd(DISPLAY_CMD_FUNCTION_SET |
                         DISPLAY_CMD_FUNCTION_SET_4BITS |
                         DISPLAY_CMD_FUNCTION_SET_2LINES |
                         DISPLAY_CMD_FUNCTION_SET_5x8DOTS);
    HAL_Delay(1);

    display_i2c_send_cmd(DISPLAY_CMD_DISPLAY_CONTROL |
                         DISPLAY_CMD_DISPLAY_CONTROL_DISPLAY_OFF);
    HAL_Delay(1);

    display_i2c_send_cmd(DISPLAY_CMD_CLEAR_DISPLAY);
    HAL_Delay(1);

    display_i2c_send_cmd(DISPLAY_CMD_ENTRY_MODE_SET |
                         DISPLAY_CMD_ENTRY_MODE_SET_INCREMENT_CURSOR |
                         DISPLAY_CMD_ENTRY_MODE_SET_NO_SHIFT);
    HAL_Delay(1);

    display_i2c_send_cmd(DISPLAY_CMD_DISPLAY_CONTROL |
                         DISPLAY_CMD_DISPLAY_CONTROL_DISPLAY_ON |
                         DISPLAY_CMD_DISPLAY_CONTROL_CURSOR_OFF |
                         DISPLAY_CMD_DISPLAY_CONTROL_BLINK_OFF);
    HAL_Delay(1);

    b_display_i2c_dma_done = true;
    g_display_i2c_tick = DISPLAY_TICK_CNT_INI_MS;
    g_display_i2c_dma_tick = DISPLAY_TICK_CNT_INI_MS;

    display_async_queue.head = 0;
    display_async_queue.tail = 0;
    display_async_queue.count = 0;
}

void display_i2c_set_cursor_pos(uint8_t x, uint8_t y)
{
    switch (y)
    {
        case 0:
            display_i2c_send_cmd(DISPLAY_DDRAM_ADDR |
                    (DISPLAY_LINE1_FIRST_CHAR_ADDR + x));
        break;
        case 1:
            display_i2c_send_cmd(DISPLAY_DDRAM_ADDR |
                    (DISPLAY_LINE2_FIRST_CHAR_ADDR + x));
        break;
    }
}

void display_i2c_clear(void)
{
    display_i2c_send_cmd(DISPLAY_CMD_CLEAR_DISPLAY);
    HAL_Delay(1);
}

void display_i2c_write_string(const char *str)
{
    while (*str)
    {
        display_i2c_send_data(*str++);
    }
}

void display_i2c_async_set_cursor_pos(uint8_t x, uint8_t y)
{
    uint8_t cmd;

    switch (y)
    {
        case 0:
            cmd = (DISPLAY_DDRAM_ADDR | (DISPLAY_LINE1_FIRST_CHAR_ADDR + x));
        break;
        case 1:
            cmd = (DISPLAY_DDRAM_ADDR | (DISPLAY_LINE2_FIRST_CHAR_ADDR + x));
        break;
        default: break;
    }

    display_i2c_async_enqueue((display_data_t){DISPLAY_CMD, cmd});
}

void display_i2c_async_clear(void)
{
    display_i2c_async_enqueue((display_data_t){DISPLAY_CMD, DISPLAY_CMD_CLEAR_DISPLAY});
    display_i2c_async_enqueue((display_data_t){DISPLAY_DELAY, 0});
}

void display_i2c_async_write_string(const char *str)
{
    while (*str)
    {
        display_i2c_async_enqueue((display_data_t){DISPLAY_CHAR, *str++});
    }
}

bool display_i2c_async_done(void)
{
    return b_display_i2c_dma_done;
}

void display_i2c_async_tick(void)
{
    if (0 < g_display_i2c_tick)
    {
        g_display_i2c_tick--;
    }
    else
    {
        if ((display_async_queue.count != 0) && b_display_i2c_dma_done)
        {
            curr_tx_data = display_i2c_async_dequeue();
            display_i2c_async_write(curr_tx_data);
        }
    }

    if (false == b_display_i2c_dma_done)
    {
        if (0 < g_display_i2c_dma_tick)
        {
            g_display_i2c_dma_tick--;
        }
        else
        {
            // DMA timeout. Assume error. Try to recover i2c peripheral and
            // send again last sent byte.
            //
            // TODO: report the error in some way if there are repeated losses.

            if (display_i2c_is_stuck())
            {
                display_i2c_recover();
            }

            display_i2c_async_write(curr_tx_data);
        }
    }
}

// Private functions definitions
static void display_i2c_send_cmd(uint8_t cmd)
{
    uint8_t buffer[4], data_u, data_l;

    data_u = (cmd & 0xF0);
    data_l = ((cmd << 4) & 0xF0);

    buffer[0] = data_u | 0x0C;
    buffer[1] = data_u | 0x08;
    buffer[2] = data_l | 0x0C;
    buffer[3] = data_l | 0x08;

    HAL_I2C_Master_Transmit(&hi2c1, DISPLAY_I2C_ADDRESS<<1, buffer, 4, 100);
}

static void display_i2c_send_data(uint8_t data)
{
    uint8_t data_u, data_l;
    uint8_t data_t[4];

    data_u = (data & 0xF0);
    data_l = ((data << 4) & 0XF0);

    data_t[0] = data_u | 0x0D;
    data_t[1] = data_u | 0x09;
    data_t[2] = data_l | 0x0D;
    data_t[3] = data_l | 0x09;

    HAL_I2C_Master_Transmit(&hi2c1, DISPLAY_I2C_ADDRESS<<1, (uint8_t *)data_t, 4, 100);
}

static void display_i2c_async_write(display_data_t data)
{
    uint8_t data_u, data_l;
    memset(async_buffer, 0, sizeof(async_buffer));

    data_u = (data.data & 0xF0);
    data_l = ((data.data << 4) & 0xF0);

    switch (data.type)
    {
        case DISPLAY_CMD:
            async_buffer[0] = data_u | 0x0C;
            async_buffer[1] = data_u | 0x08;
            async_buffer[2] = data_l | 0x0C;
            async_buffer[3] = data_l | 0x08;
        break;

        case DISPLAY_CHAR:
            async_buffer[0] = data_u | 0x0D;
            async_buffer[1] = data_u | 0x09;
            async_buffer[2] = data_l | 0x0D;
            async_buffer[3] = data_l | 0x09;
        break;

        case DISPLAY_DELAY:
            g_display_i2c_tick = DISPLAY_ASYNC_CLEAR_DEL_MS;
        break;

        default: break;
    }

    if (DISPLAY_DELAY != data.type)
    {
        HAL_I2C_Master_Transmit_DMA(&hi2c1, DISPLAY_I2C_ADDRESS<<1, async_buffer, 4);
        b_display_i2c_dma_done = false;
        g_display_i2c_dma_tick = DISPLAY_TICK_DMA_TIMEOUT_MS;
    }
}

void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
    b_display_i2c_dma_done = true;
}

static void display_i2c_async_enqueue(display_data_t data)
{
    display_async_queue.count++;
    display_async_queue.queue[display_async_queue.head++] = data;
}

static display_data_t display_i2c_async_dequeue(void)
{
    display_async_queue.count--;
    return display_async_queue.queue[display_async_queue.tail++];
}

uint8_t display_i2c_is_stuck(void)
{
    if ((hi2c1.Instance->SR1 & I2C_SR1_AF) ||
        (hi2c1.Instance->SR2 & I2C_SR2_BUSY) ||
        (DMA1_Channel6->CCR & DMA_CCR_EN) ||
        (hi2c1.State != HAL_I2C_STATE_READY))
    {
        return 1;
    }

    return 0;
}


void display_i2c_recover(void)
{
    HAL_DMA_Abort(hi2c1.hdmatx);

    HAL_I2C_DeInit(&hi2c1);

    __HAL_RCC_I2C1_FORCE_RESET();
    __HAL_RCC_I2C1_RELEASE_RESET();

    __HAL_UNLOCK(&hi2c1);
    hi2c1.State = HAL_I2C_STATE_READY;

    systick_delay_us(200);

    HAL_I2C_Init(&hi2c1);
}
