/*
 * lcd_i2c.c
 *
 *  Created on: Oct 16, 2025
 *      Author: ew_05
 */


#include "lcd_i2c.h"
#include <string.h>
#include <stdio.h>

/* ----------------------- PCF8574 bit mapping helpers ----------------------- */
#define LCD_EN          0x04U
#define LCD_RW          0x02U
#define LCD_RS          0x01U
#define LCD_BACKLIGHT   0x08U
#define LCD_NOBACKLIGHT 0x00U

/* ------------------------------- Driver state ------------------------------ */
static I2C_HandleTypeDef *s_hi2c = NULL;
static volatile uint8_t   s_tx_done = 0;
static volatile lcd_state_t s_state = LCD_STATE_READY;
static volatile uint8_t   s_i2c_error_count = 0;
static uint8_t            s_backlight = LCD_BACKLIGHT;

/* --------------------------- Internal declarations ------------------------- */
static HAL_StatusTypeDef wait_device_ready(uint32_t timeout_ms);
static HAL_StatusTypeDef wait_peripheral_ready(uint32_t timeout_ms);
static HAL_StatusTypeDef send_nibble_seq(uint8_t data, uint8_t control, uint32_t xferOpt);
static HAL_StatusTypeDef send_with_retry(uint8_t data, uint8_t control, uint32_t xferOpt);
static HAL_StatusTypeDef send_cmd(uint8_t cmd, uint32_t xferOpt);
static HAL_StatusTypeDef send_data(uint8_t data, uint32_t xferOpt);

/* ------------------------------- Public API -------------------------------- */

void lcd_i2c_attach(I2C_HandleTypeDef *hi2c)
{
    s_hi2c = hi2c;
}

HAL_StatusTypeDef lcd_init(void)
{
    if (s_hi2c == NULL) return HAL_ERROR;

    // Allow LCD power to stabilize
    HAL_Delay(50);

    // Init sequence: 0x30 * 3 to ensure 8-bit mode, then 4-bit mode
    (void)send_cmd(0x30, I2C_FIRST_FRAME);
    HAL_Delay(5);
    (void)send_cmd(0x30, I2C_NEXT_FRAME);
    HAL_Delay(1);
    (void)send_cmd(0x30, I2C_NEXT_FRAME);
    HAL_Delay(1);

    // Switch to 4-bit mode
    (void)send_cmd(0x20, I2C_NEXT_FRAME);
    HAL_Delay(1);

    // Function set: 4-bit, 2 lines, 5x8 dots
    (void)send_cmd(0x28, I2C_NEXT_FRAME);
    HAL_Delay(1);

    // Display off
    (void)send_cmd(0x08, I2C_NEXT_FRAME);
    HAL_Delay(1);

    // Clear display
    (void)send_cmd(0x01, I2C_NEXT_FRAME);
    HAL_Delay(2);

    // Entry mode: increment, no shift
    (void)send_cmd(0x06, I2C_NEXT_FRAME);
    HAL_Delay(1);

    // Display on, cursor off, blink off
    HAL_StatusTypeDef st = send_cmd(0x0C, I2C_LAST_FRAME);
    HAL_Delay(1);

    // Ensure backlight is applied
    if (st == HAL_OK) {
        (void)lcd_backlight_on();
    }
    return st;
}

HAL_StatusTypeDef lcd_clear(void)
{
    HAL_StatusTypeDef st = send_cmd(0x01, I2C_FIRST_AND_LAST_FRAME);
    HAL_Delay(2);
    return st;
}

HAL_StatusTypeDef lcd_put_cur(uint8_t row, uint8_t col)
{
    uint8_t addr = 0x80; // DDRAM base
    switch (row) {
        case 0: addr = 0x80 + col; break;
        case 1: addr = 0xC0 + col; break;
        case 2: addr = 0x94 + col; break; // for 20x4
        case 3: addr = 0xD4 + col; break; // for 20x4
        default: addr = 0x80 + col; break;
    }
    return send_cmd(addr, I2C_FIRST_AND_LAST_FRAME);
}

HAL_StatusTypeDef lcd_write_char(char c)
{
    return send_data((uint8_t)c, I2C_FIRST_AND_LAST_FRAME);
}

HAL_StatusTypeDef lcd_write(uint8_t row, uint8_t col, const char *str)
{
    if (str == NULL) return HAL_ERROR;

    size_t n = strlen(str);
    if (n == 0) return HAL_OK;

    HAL_StatusTypeDef st = lcd_put_cur(row, col);
    if (st != HAL_OK) return st;

    for (size_t i = 0; i < n; ++i) {
        uint32_t opt = (i == n - 1) ? I2C_LAST_FRAME : I2C_NEXT_FRAME;
        st = send_data((uint8_t)str[i], opt);
        if (st != HAL_OK) return st;
    }
    HAL_Delay(1);
    return HAL_OK;
}

HAL_StatusTypeDef lcd_backlight_on(void)
{
    s_backlight = LCD_BACKLIGHT;
    // push a no-op to update expander outputs
    return send_cmd(0x00, I2C_FIRST_AND_LAST_FRAME);
}

HAL_StatusTypeDef lcd_backlight_off(void)
{
    s_backlight = LCD_NOBACKLIGHT;
    return send_cmd(0x00, I2C_FIRST_AND_LAST_FRAME);
}

void lcd_get_status(uint8_t *i2c_errors, lcd_state_t *state)
{
    if (i2c_errors) *i2c_errors = s_i2c_error_count;
    if (state)       *state = s_state;
}

void lcd_recover_bus(void)
{
    if (s_hi2c == NULL) return;

    // Disable I2C peripheral
    __HAL_I2C_DISABLE(s_hi2c);
    HAL_Delay(LCD_BUS_RECOVERY_DELAY_MS);

    // Re-init peripheral (HAL keeps init struct)
    (void)HAL_I2C_Init(s_hi2c);

    // Reset internal flags
    s_state   = LCD_STATE_READY;
    s_tx_done = 0;

    HAL_Delay(LCD_BUS_RECOVERY_DELAY_MS);
}

/* ----------------------------- HAL Callbacks ------------------------------- */
/* These override weak HAL callbacks at link time. Keep them in one module.   */

void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
    if (s_hi2c && hi2c == s_hi2c) {
        s_tx_done = 1;
        s_state   = LCD_STATE_READY;
        // Optional: blink a board LED elsewhere to avoid coupling here.
    }
}

void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c)
{
    if (s_hi2c && hi2c == s_hi2c) {
        s_state = LCD_STATE_ERROR;
        s_i2c_error_count++;
        // Avoid printf here to keep ISR short; log in application layer if needed.
    }
}

/* ---------------------------- Internal helpers ----------------------------- */

static HAL_StatusTypeDef wait_device_ready(uint32_t timeout_ms)
{
    if (s_hi2c == NULL) return HAL_ERROR;

    const uint32_t t0 = HAL_GetTick();
    const uint16_t addr8 = (uint16_t)(LCD_I2C_ADDRESS_7B << 1);

    while (HAL_I2C_IsDeviceReady(s_hi2c, addr8, 3, 5) != HAL_OK) {
        if ((HAL_GetTick() - t0) >= timeout_ms) {
            return HAL_TIMEOUT;
        }
        HAL_Delay(5);
    }
    return HAL_OK;
}

static HAL_StatusTypeDef wait_peripheral_ready(uint32_t timeout_ms)
{
    if (s_hi2c == NULL) return HAL_ERROR;

    const uint32_t t0 = HAL_GetTick();
    while (HAL_I2C_GetState(s_hi2c) != HAL_I2C_STATE_READY) {
        if ((HAL_GetTick() - t0) >= timeout_ms) {
            return HAL_TIMEOUT;
        }
    }
    return HAL_OK;
}

static HAL_StatusTypeDef send_nibble_seq(uint8_t data, uint8_t control, uint32_t xferOpt)
{
    if (s_hi2c == NULL) return HAL_ERROR;

    // Build the 4-step enable pulse sequence for two 4-bit transfers
    uint8_t tx[4];
    tx[0] = s_backlight | control | (data & 0xF0) | LCD_EN;
    tx[1] = s_backlight | control | (data & 0xF0);
    tx[2] = s_backlight | control | ((uint8_t)(data << 4) & 0xF0) | LCD_EN;
    tx[3] = s_backlight | control | ((uint8_t)(data << 4) & 0xF0);

    if (xferOpt == I2C_FIRST_FRAME) {
        HAL_StatusTypeDef st = wait_device_ready(LCD_I2C_TIMEOUT_MS);
        if (st != HAL_OK) return st;
    }

    HAL_StatusTypeDef st = wait_peripheral_ready(LCD_I2C_TIMEOUT_MS);
    if (st != HAL_OK) return st;

    s_tx_done = 0;
    s_state   = LCD_STATE_BUSY;

    const uint16_t addr8 = (uint16_t)(LCD_I2C_ADDRESS_7B << 1);

    st = HAL_I2C_Master_Seq_Transmit_IT(s_hi2c, addr8, tx, sizeof(tx), xferOpt);
    if (st != HAL_OK) {
        s_state = LCD_STATE_ERROR;
        return st;
    }

    const uint32_t t0 = HAL_GetTick();
    while (!s_tx_done) {
        if ((HAL_GetTick() - t0) >= LCD_OP_TIMEOUT_MS) {
            s_state = LCD_STATE_ERROR;
            return HAL_TIMEOUT;
        }
        // busy wait; this is short (<= LCD_OP_TIMEOUT_MS)
    }

    return HAL_OK;
}

static HAL_StatusTypeDef send_with_retry(uint8_t data, uint8_t control, uint32_t xferOpt)
{
    HAL_StatusTypeDef st = HAL_ERROR;
    uint8_t retries = 3;

    while (retries--) {
        st = send_nibble_seq(data, control, xferOpt);
        if (st == HAL_OK) {
            s_i2c_error_count = 0;
            return HAL_OK;
        }
        if (retries > 0) {
            lcd_recover_bus();
            HAL_Delay(LCD_BUS_RECOVERY_DELAY_MS * (3U - retries)); // backoff
        }
    }
    return st;
}

static HAL_StatusTypeDef send_cmd(uint8_t cmd, uint32_t xferOpt)
{
    return send_with_retry(cmd, 0x00U, xferOpt);
}

static HAL_StatusTypeDef send_data(uint8_t data, uint32_t xferOpt)
{
    return send_with_retry(data, LCD_RS, xferOpt);
}
