/**
 * @file lcd_api.c
 * @brief LCD API implementation
 * @author Tim S.
 * @author Lorenz C.
 */

#include "lcd_api.h"


static const char* TAG = "lcd_api";

static DMA2D_HandleTypeDef hDma2dHandler2;
static lcd_gif_t gifs[LCD_MAX_GIFS]; // Array of GIF slots

static lcd_gif_t* get_free_gif_slot(void);
static void gif_draw_cb(GIFDRAW* pDraw);
static inline void free_gif_slot(lcd_gif_t* gif);

void lcd_init(bool bl_on) {
    LOG_INFO(TAG, "Init LCD");

    BSP_LCD_Init();
    BSP_LCD_LayerDefaultInit(1, LCD_FB_START_ADDRESS);
    BSP_LCD_LayerDefaultInit(0, LCD_FB_START_ADDRESS + (BSP_LCD_GetXSize() * BSP_LCD_GetYSize()) * 4);
    BSP_LCD_SelectLayer(0);
    BSP_LCD_Clear(0);
    BSP_LCD_SelectLayer(1);
    BSP_LCD_Clear(0);
    BSP_LCD_SetLayerVisible(0, ENABLE);
    BSP_LCD_SetLayerVisible(1, ENABLE);
    if (bl_on) {
        HAL_GPIO_WritePin(GPIOK, GPIO_PIN_3, GPIO_PIN_SET);
        HAL_GPIO_WritePin(GPIOI, GPIO_PIN_12, GPIO_PIN_SET);
        LOG_INFO(TAG, "LCD initialise with backlight");
        return;
    }
    HAL_GPIO_WritePin(GPIOK, GPIO_PIN_3, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(GPIOI, GPIO_PIN_12, GPIO_PIN_RESET);
    LOG_INFO(TAG, "LCD initialise without backlight");
}

void lcd_task(void) {
    // Play the next frame of each GIF if it's time
    for (uint8_t i = 0; i < LCD_MAX_GIFS; i++) {
        int ret;

        // Check if the GIF is in use
        if (gifs[i].src == NULL) {
            continue;
        }

        // Check if it's time to play the next frame
        if (HAL_GetTick() - gifs[i].last_frame_time < gifs[i].frame_delay) {
            continue;
        }

        // Play the next frame
        ret = GIF_playFrame(&gifs[i].gif, &gifs[i].frame_delay, &gifs[i].gif);
        if (ret == 0) { // No more frames
            // Infinite loop
            if (gifs[i].loop_count <= 0) {
                GIF_reset(&gifs[i].gif);
                continue;
            }

            // Loop again
            if (gifs[i].cur_loop < gifs[i].loop_count) {
                GIF_reset(&gifs[i].gif);
                gifs[i].cur_loop++;
                continue;
            }

            // No more loops, free slot
            LOG_DEBUG(TAG, "GIF finished, freeing slot");
            free_gif_slot(&gifs[i]);
        } else if (ret == -1) { // Error
            LOG_WARN(TAG, "GIF_playFrame error, freeing slot");
            free_gif_slot(&gifs[i]);
        }

        // Update the last frame time
        gifs[i].last_frame_time = HAL_GetTick();
    }
}

void lcd_display_text(const char* text, uint16_t x_pos, uint16_t y_pos, uint32_t color, uint32_t bg_color, sFONT* font) {
    BSP_LCD_SelectLayer(1);
    LOG_INFO(TAG, "Display text: %s @x=%d,y=%d", text, x_pos, y_pos);

    uint16_t tot_length = x_pos + ((uint16_t)strlen(text) * font->Width);
    if ((x_pos % font->Width) != 0) {
        x_pos -= (x_pos % font->Width);
    }

    BSP_LCD_SetTextColor(color);
    BSP_LCD_SetBackColor(bg_color);
    BSP_LCD_SetFont(font);

    if (tot_length > BSP_LCD_GetXSize()) {
        for (unsigned int i = 0; i < (unsigned int)strlen(text); i++) {
            if ((x_pos) > BSP_LCD_GetXSize() - (font->Width) * 2) {
                if (isalpha(text[i - 1]) && isalpha(text[i])) {
                    BSP_LCD_DisplayChar(x_pos, y_pos, (uint8_t)'-');
                } else {
                    BSP_LCD_DisplayChar(x_pos, y_pos, (uint8_t)text[i]);
                }
                x_pos = 0;
                y_pos += font->Height;
                continue;
            }
            BSP_LCD_DisplayChar(x_pos, y_pos, (uint8_t)text[i]);
            x_pos += font->Width;
        }
        return;
    }
    BSP_LCD_DisplayStringAt(x_pos, y_pos, (uint8_t*)text, LEFT_MODE);
}

void lcd_draw_raw_img(const void* p_src, uint32_t x_pos, uint32_t y_pos, uint32_t x_size, uint32_t y_size, uint32_t color_mode) {
    LOG_INFO(TAG, "Displaying raw image: @x=%d, @y=%d, width=%d, height=%d", x_pos, y_pos, x_size, y_size);
    BSP_LCD_SelectLayer(0);
    uint32_t address = hLtdcHandler.LayerCfg[1].FBStartAdress + (((BSP_LCD_GetXSize() * y_pos) + x_pos) * (4));
    void* p_dst = (void*)address;

    hDma2dHandler2.Init.Mode = DMA2D_M2M_PFC;
    hDma2dHandler2.Init.ColorMode = DMA2D_ARGB8888;
    hDma2dHandler2.Init.OutputOffset = BSP_LCD_GetXSize() - x_size;

    hDma2dHandler2.LayerCfg[1].AlphaMode = DMA2D_NO_MODIF_ALPHA;
    hDma2dHandler2.LayerCfg[1].InputAlpha = 0xFF;
    hDma2dHandler2.LayerCfg[1].InputColorMode = color_mode;
    hDma2dHandler2.LayerCfg[1].InputOffset = 0;

    LOG_INFO(TAG, "DMA2D init");
    hDma2dHandler2.Instance = DMA2D;
    if (HAL_DMA2D_Init(&hDma2dHandler2) != HAL_OK) {
        LOG_CRIT(TAG, "HAL_DMA2D_Init error");
        return;
    }
    LOG_INFO(TAG, "DMA2D config layer");
    if (HAL_DMA2D_ConfigLayer(&hDma2dHandler2, 1) != HAL_OK) {
        LOG_CRIT(TAG, "HAL_DMA2D_ConfigLayer error");
        return;
    }
    LOG_INFO(TAG, "DMA2D start");
    if (HAL_DMA2D_Start(&hDma2dHandler2, (uint32_t)p_src, (uint32_t)p_dst, x_size, y_size) != HAL_OK) {
        LOG_CRIT(TAG, "HAL_DMA2D_Start error");
        return;
    }
    LOG_INFO(TAG, "DMA2D poll");
    HAL_DMA2D_PollForTransfer(&hDma2dHandler2, 10);
}

void lcd_draw_bmp_img(uint8_t* bmp_buff, uint32_t x_pos, uint32_t y_pos) {
    LOG_INFO(TAG, "Displaying BMP image: @x=%d, @y=%d", x_pos, y_pos);
    BSP_LCD_SelectLayer(0);
    BSP_LCD_DrawBitmap(x_pos, y_pos, bmp_buff);
}

void lcd_draw_img_from_fs(const char* name, uint32_t x_pos, uint32_t y_pos) {
    LOG_INFO(TAG, "Displaying BMP image %s: @x=%d, @y=%d", name, x_pos, y_pos);
    BSP_LCD_SelectLayer(0);
    llfs_file_t* file = llfs_file_open(name);
    if (file != NULL) {
        BSP_LCD_DrawBitmap(x_pos, y_pos, (uint8_t*)file->data);
        return;
    }
    LOG_WARN(TAG, "File \"%s\" not found", name);
}

void lcd_clear_text(void) {
    LOG_INFO(TAG, "Clear text");
    BSP_LCD_SelectLayer(1);
    BSP_LCD_Clear(0);
}

void lcd_clear_images(void) {
    LOG_INFO(TAG, "Clear images");
    BSP_LCD_SelectLayer(0);
    for (uint8_t i = 0; i < LCD_MAX_GIFS; i++) {
        if (gifs[i].src != NULL) {
            lcd_stop_gif(&gifs[i]);
        }
    }
    BSP_LCD_Clear(0);
}

void lcd_set_bg_color_layer0(uint32_t color){
    BSP_LCD_SelectLayer(0);
    BSP_LCD_Clear(color);
}

void lcd_stop_all_gifs(void) {
    for (uint8_t i = 0; i < LCD_MAX_GIFS; i++) {
        if (gifs[i].src != NULL) {
            lcd_stop_gif(&gifs[i]);
        }
    }
}

lcd_gif_t* lcd_draw_gif(uint8_t* src, size_t size, uint32_t x_pos, uint32_t y_pos) {
    BSP_LCD_SelectLayer(0);
    lcd_gif_t* gif;

    // Get a free GIF slot
    if ((gif = get_free_gif_slot()) == NULL) {
        LOG_WARN(TAG, "No free GIF slots");
        return NULL;
    }

    // Open the GIF and reset slot values
    GIF_begin(&(gif->gif), GIF_PALETTE_RGB888);
    if (GIF_openRAM(&(gif->gif), src, (int)size, gif_draw_cb)) {
        LOG_INFO(TAG, "Draw GIF: @x=%d, @y=%d with size: %d", x_pos, y_pos, size);
        gif->src = src;
        gif->x_pos = x_pos;
        gif->y_pos = y_pos;
        gif->last_frame_time = 0;
        gif->loop_count = GIF_getLoopCount(&(gif->gif));
        gif->cur_loop = 1;
        gif->frame_delay = 0;
        gif->gif.ucDrawType = GIF_DRAW_RAW;
        gif->gif.ucPaletteType = GIF_PALETTE_RGB888;
        return gif;
    }

    // Failed to open GIF
    LOG_WARN(TAG, "GIF_openRAM failed");
    free_gif_slot(gif);
    return NULL;
}

lcd_gif_t* lcd_draw_gif_from_fs(const char* name, uint32_t x_pos, uint32_t y_pos) {
    BSP_LCD_SelectLayer(0);
    lcd_gif_t* gif;
    llfs_file_t* file;

    // Get the file from llfs
    file = llfs_file_open(name);
    if (file == NULL) {
        LOG_WARN(TAG, "File \"%s\" not found", name);
        return NULL;
    }
    LOG_INFO(TAG, "Draw GIF %s", name);
    // Draw the GIF using the file data
    gif = lcd_draw_gif((uint8_t*)file->data, file->len, x_pos, y_pos);
    return gif;
}

void lcd_stop_gif(lcd_gif_t* gif) {
    free_gif_slot(gif);
}

bool lcd_gif_is_playing(lcd_gif_t* gif) {
    return gif->src != NULL;
}

/**
 * @brief Get a free GIF slot
 * Get the next GIF slot that is not in use
 *
 * @return Pointer to the GIF slot, NULL if none are available
 */
static lcd_gif_t* get_free_gif_slot(void) {
    for (int i = 0; i < 5; i++) {
        if (gifs[i].src == NULL) {
            return &gifs[i];
        }
    }
    return NULL;
}

/**
 * @brief Free a GIF slot
 * Release a GIF slot, and free the memory
 *
 * @param gif Pointer to the GIF slot to free
 */
static inline void free_gif_slot(lcd_gif_t* gif) {
    gif->src = NULL;
    GIF_close(&gif->gif);
}

/**
 * @brief Callback function used by the GIF decoder
 * This function is called by the GIF decoder to draw a new line of pixels
 * @todo See if it's possible to use DMA2D for this
 *
 * @param pDraw Pointer to the GIFDRAW struct
 */
static void gif_draw_cb(GIFDRAW* pDraw) {
    BSP_LCD_SelectLayer(0);
    lcd_gif_t* gif = (lcd_gif_t*)pDraw->pUser;
    uint8_t* palette = pDraw->pPalette24;   // The RGB888 color palette
    uint8_t* p_src = pDraw->pPixels;        // Source pixel pointer
    int y = pDraw->iY + pDraw->y;           // Current line being drawn

    // Calculate the destination address of the first pixel in the line
    uint32_t address = hLtdcHandler.LayerCfg[0].FBStartAdress + (((BSP_LCD_GetXSize() * gif->y_pos) + gif->x_pos) * 4)
                       + (y * (BSP_LCD_GetXSize() * 4));

    // Restore the background
    if (pDraw->ucDisposalMethod == 2) {
        for (int x = 0; x < pDraw->iWidth; x++) {
            if (p_src[x] == pDraw->ucTransparent) {
                p_src[x] = pDraw->ucBackground;
            }
        }
        pDraw->ucHasTransparency = 0;
    }

    // Draw each pixel in the line
    for (int x = 0; x < pDraw->iWidth; x++) {
        uint8_t pixel = *p_src++;

        // Skip transparent pixels
        if (pDraw->ucHasTransparency && pixel == pDraw->ucTransparent) {
            continue;
        }

        // Get the color from the palette and convert it to ARGB8888
        uint8_t *p = palette + (pixel * 3);
        uint32_t color = (0xFF << 24) | (p[0] << 16) | (p[1] << 8) | p[2];

        // Draw the pixel
        ((uint32_t *)address)[x] = color;
    }
}
