/**
 * @file lv_nuttx_lcd.c
 *
 */

/*********************
 *      INCLUDES
 *********************/

#include "lv_nuttx_lcd.h"

#if LV_USE_NUTTX

#if LV_USE_NUTTX_LCD

#include <sys/ioctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <debug.h>
#include <errno.h>
#include <fcntl.h>
#include <nuttx/lcd/lcd_dev.h>

#include "../../../lvgl.h"
#include "../../lvgl_private.h"

/*********************
 *      DEFINES
 *********************/

/**********************
 *      TYPEDEFS
 **********************/

typedef struct {
    /* fd should be defined at the beginning */
    int fd;
    lv_display_t * disp;
    struct lcddev_area_s area;
    struct lcddev_area_align_s align_info;
} lv_nuttx_lcd_t;

/**********************
 *  STATIC PROTOTYPES
 **********************/

static int32_t align_round_up(int32_t v, uint16_t align);
static void rounder_cb(lv_event_t * e);
static void flush_cb(lv_display_t * disp, const lv_area_t * area_p,
                     uint8_t * color_p);
static lv_display_t * lcd_init(int fd, int hor_res, int ver_res);
static void display_release_cb(lv_event_t * e);

/**********************
 *  STATIC VARIABLES
 **********************/

/**********************
 *      MACROS
 **********************/

/**********************
 *   GLOBAL FUNCTIONS
 **********************/

lv_display_t * lv_nuttx_lcd_create(const char * dev_path)
{
    struct fb_videoinfo_s vinfo;
    struct lcd_planeinfo_s pinfo;
    lv_display_t * disp;
    int fd;
    int ret;

    LV_ASSERT_NULL(dev_path);

    LV_LOG_USER("lcd %s opening", dev_path);
    fd = open(dev_path, 0);
    if(fd < 0) {
        perror("Error: cannot open lcd device");
        return NULL;
    }

    LV_LOG_USER("lcd %s open success", dev_path);

    ret = ioctl(fd, LCDDEVIO_GETVIDEOINFO,
                (unsigned long)((uintptr_t)&vinfo));
    if(ret < 0) {
        perror("Error: ioctl(LCDDEVIO_GETVIDEOINFO) failed");
        close(fd);
        return NULL;
    }

    ret = ioctl(fd, LCDDEVIO_GETPLANEINFO,
                (unsigned long)((uintptr_t)&pinfo));
    if(ret < 0) {
        perror("ERROR: ioctl(LCDDEVIO_GETPLANEINFO) failed");
        close(fd);
        return NULL;
    }

    disp = lcd_init(fd, vinfo.xres, vinfo.yres);
    if(disp == NULL) {
        close(fd);
    }

    return disp;
}

/**********************
 *   STATIC FUNCTIONS
 **********************/

static int32_t align_round_up(int32_t v, uint16_t align)
{
    return (v + align - 1) & ~(align - 1);
}

static void rounder_cb(lv_event_t * e)
{
    lv_nuttx_lcd_t * lcd = lv_event_get_user_data(e);
    lv_area_t * area = lv_event_get_param(e);
    struct lcddev_area_align_s * align_info = &lcd->align_info;
    int32_t w;
    int32_t h;

    area->x1 &= ~(align_info->col_start_align - 1);
    area->y1 &= ~(align_info->row_start_align - 1);

    w = align_round_up(lv_area_get_width(area), align_info->width_align);
    h = align_round_up(lv_area_get_height(area), align_info->height_align);

    area->x2 = area->x1 + w - 1;
    area->y2 = area->y1 + h - 1;
}

static void flush_cb(lv_display_t * disp, const lv_area_t * area_p,
                     uint8_t * color_p)
{
    lv_nuttx_lcd_t * lcd = disp->driver_data;
    lv_color_format_t cf = lv_display_get_color_format(disp);

    lcd->area.row_start = area_p->y1;
    lcd->area.row_end = area_p->y2;
    lcd->area.col_start = area_p->x1;
    lcd->area.col_end = area_p->x2;
    lcd->area.stride = lv_draw_buf_width_to_stride(lcd->area.col_end - lcd->area.col_start + 1, cf);
    lcd->area.data = (uint8_t *)color_p + (LV_COLOR_FORMAT_IS_INDEXED(cf) ?
                                           LV_COLOR_INDEXED_PALETTE_SIZE(cf) * 4 : 0);
    ioctl(lcd->fd, LCDDEVIO_PUTAREA, (unsigned long) & (lcd->area));
    lv_display_flush_ready(disp);
}

static lv_display_t * lcd_init(int fd, int hor_res, int ver_res)
{
    lv_draw_buf_t * draw_buf = NULL;
    lv_draw_buf_t * draw_buf_2 = NULL;
    lv_nuttx_lcd_t * lcd = lv_malloc_zeroed(sizeof(lv_nuttx_lcd_t));
    LV_ASSERT_MALLOC(lcd);
    if(lcd == NULL) {
        LV_LOG_ERROR("lv_nuttx_lcd_t malloc failed");
        return NULL;
    }

    lv_display_t * disp = lv_display_create(hor_res, ver_res);
    if(disp == NULL) {
        lv_free(lcd);
        return NULL;
    }

    lv_color_format_t cf = lv_display_get_color_format(disp);   /* Use default cf */
#if LV_NUTTX_LCD_BUFFER_COUNT > 0
    lv_display_render_mode_t render_mode = LV_DISPLAY_RENDER_MODE_FULL;
#else
    lv_display_render_mode_t render_mode = LV_DISPLAY_RENDER_MODE_PARTIAL;
#endif

    draw_buf = lv_draw_buf_create(hor_res, ver_res, cf, LV_STRIDE_AUTO);
    if(draw_buf == NULL) {
        LV_LOG_ERROR("display draw_buf create failed");
        lv_free(lcd);
        return NULL;
    }

#if LV_NUTTX_LCD_BUFFER_COUNT == 2
    draw_buf_2 = lv_draw_buf_create(hor_res, ver_res, cf, LV_STRIDE_AUTO);
    if(draw_buf_2 == NULL) {
        LV_LOG_ERROR("display draw_buf_2 malloc failed");
        lv_free(lcd);
        lv_draw_buf_destroy(draw_buf);
        return NULL;
    }
#endif

    lcd->fd = fd;
    if(ioctl(fd, LCDDEVIO_GETAREAALIGN, &lcd->align_info) < 0) {
        perror("Error: ioctl(LCDDEVIO_GETAREAALIGN) failed");
    }

    lcd->disp = disp;
    lv_display_set_draw_buffers(lcd->disp, draw_buf, draw_buf_2);
    lv_display_set_render_mode(lcd->disp, render_mode);
    lv_display_set_flush_cb(lcd->disp, flush_cb);
    lv_display_add_event_cb(lcd->disp, rounder_cb, LV_EVENT_INVALIDATE_AREA, lcd);
    lv_display_add_event_cb(lcd->disp, display_release_cb, LV_EVENT_DELETE, lcd->disp);
    lv_display_set_driver_data(lcd->disp, lcd);

    return lcd->disp;
}

static void display_release_cb(lv_event_t * e)
{
    lv_display_t * disp = (lv_display_t *) lv_event_get_user_data(e);
    lv_nuttx_lcd_t * dsc = lv_display_get_driver_data(disp);
    if(dsc) {
        lv_display_set_driver_data(disp, NULL);
        lv_display_set_flush_cb(disp, NULL);

        /* clear display buffer */
        if(disp->buf_1) {
            lv_draw_buf_destroy(disp->buf_1);
            disp->buf_1 = NULL;
        }
        if(disp->buf_2) {
            lv_draw_buf_destroy(disp->buf_2);
            disp->buf_2 = NULL;
        }

        /* close device fb */
        if(dsc->fd >= 0) {
            close(dsc->fd);
            dsc->fd = -1;
        }
        lv_free(dsc);
        LV_LOG_USER("Done");
    }
}
#endif /*LV_USE_NUTTX_LCD*/

#endif /* LV_USE_NUTTX*/
