/****************************************************************************
 * apps/graphics/lvgl/port/lv_port_lcddev.c
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.  The
 * ASF licenses this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>
#include <nuttx/lcd/lcd_dev.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <semaphore.h>
#include "lv_port_lcddev.h"

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

/****************************************************************************
 * Private Type Declarations
 ****************************************************************************/

struct lcddev_obj_s
{
  int fd;
  lv_disp_draw_buf_t disp_draw_buf;
  lv_disp_drv_t disp_drv;
  struct lcddev_area_s area;

  pthread_t write_thread;
  sem_t flush_sem;
  sem_t wait_sem;
};

/****************************************************************************
 * Public Data
 ****************************************************************************/

/****************************************************************************
 * Private Functions
 ****************************************************************************/

/****************************************************************************
 * Name: lcddev_wait
 ****************************************************************************/

static void lcddev_wait(FAR lv_disp_drv_t *disp_drv)
{
  FAR struct lcddev_obj_s *lcddev_obj = disp_drv->user_data;

  sem_wait(&(lcddev_obj->wait_sem));

  /* Tell the flushing is ready */

  lv_disp_flush_ready(disp_drv);
}

/****************************************************************************
 * Name: lcddev_update_thread
 ****************************************************************************/

static FAR void *lcddev_update_thread(FAR void *arg)
{
  int ret = OK;
  int errcode = 0;
  FAR struct lcddev_obj_s *lcddev_obj = arg;

  while (ret == OK)
    {
      sem_wait(&lcddev_obj->flush_sem);

      ret = ioctl(lcddev_obj->fd, LCDDEVIO_PUTAREA,
                  (unsigned long)&(lcddev_obj->area));
      if (ret < 0)
        {
          errcode = errno;
        }

      sem_post(&lcddev_obj->wait_sem);
    }

  LV_LOG_ERROR("ioctl(LCDDEVIO_PUTAREA) failed: %d", errcode);
  close(lcddev_obj->fd);
  lcddev_obj->fd = -1;

  return NULL;
}

/****************************************************************************
 * Name: lcddev_flush
 ****************************************************************************/

static void lcddev_flush(FAR lv_disp_drv_t *disp_drv,
                         FAR const lv_area_t *area_p,
                         FAR lv_color_t *color_p)
{
  FAR struct lcddev_obj_s *lcddev_obj = disp_drv->user_data;

  lcddev_obj->area.row_start = area_p->y1;
  lcddev_obj->area.row_end = area_p->y2;
  lcddev_obj->area.col_start = area_p->x1;
  lcddev_obj->area.col_end = area_p->x2;
  lcddev_obj->area.data = (FAR uint8_t *)color_p;

  sem_post(&lcddev_obj->flush_sem);
}

/****************************************************************************
 * Name: lcddev_init
 ****************************************************************************/

static FAR lv_disp_t *lcddev_init(int fd,
                                  int hor_res,
                                  int ver_res,
                                  int line_buf)
{
  FAR lv_color_t *buf1 = NULL;
  FAR lv_color_t *buf2 = NULL;
  FAR struct lcddev_obj_s *lcddev_obj;
  const size_t buf_size = hor_res * line_buf * sizeof(lv_color_t);
  lcddev_obj = malloc(sizeof(struct lcddev_obj_s));

  if (lcddev_obj == NULL)
    {
      LV_LOG_ERROR("lcddev_obj_s malloc failed");
      return NULL;
    }

  buf1 = malloc(buf_size);
  if (buf1 == NULL)
    {
      LV_LOG_ERROR("display buf1 malloc failed");
      free(lcddev_obj);
      return NULL;
    }

#ifdef CONFIG_LV_PORT_LCDDEV_DOUBLE_BUFFER
  buf2 = malloc(buf_size);
  if (buf2 == NULL)
    {
      LV_LOG_ERROR("display buf2 malloc failed");
      free(lcddev_obj);
      free(buf1);
      return NULL;
    }
#endif

  LV_LOG_INFO("display buffer malloc success, buf size = %lu", buf_size);

  lcddev_obj->fd = fd;

  lv_disp_draw_buf_init(&(lcddev_obj->disp_draw_buf), buf1, buf2,
                        hor_res * line_buf);

  lv_disp_drv_init(&(lcddev_obj->disp_drv));
  lcddev_obj->disp_drv.flush_cb = lcddev_flush;
  lcddev_obj->disp_drv.draw_buf = &(lcddev_obj->disp_draw_buf);
  lcddev_obj->disp_drv.hor_res = hor_res;
  lcddev_obj->disp_drv.ver_res = ver_res;
  lcddev_obj->disp_drv.screen_transp = false;
#if ( LV_USE_USER_DATA != 0 )
  lcddev_obj->disp_drv.user_data = lcddev_obj;
#else
#error LV_USE_USER_DATA must be enabled
#endif
  lcddev_obj->disp_drv.wait_cb = lcddev_wait;

  /* Initialize the mutexes for buffer flushing synchronization */

  sem_init(&lcddev_obj->flush_sem, 0, 0);
  sem_init(&lcddev_obj->wait_sem, 0, 0);

  /* Initialize the buffer flushing thread */

  pthread_create(&(lcddev_obj->write_thread), NULL,
                 lcddev_update_thread, lcddev_obj);

  return lv_disp_drv_register(&(lcddev_obj->disp_drv));
}

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * Name: lv_port_lcddev_init
 *
 * Description:
 *   Lcd interface initialization.
 *
 * Input Parameters:
 *   dev_path - lcd device path, set to NULL to use the default path
 *   line_buf - Number of line buffers,
 *              set to 0 to use the default line buffer
 *
 * Returned Value:
 *   lv_disp object address on success; NULL on failure.
 *
 ****************************************************************************/

FAR lv_disp_t *lv_port_lcddev_init(FAR const char *dev_path, int line_buf)
{
  FAR const char *device_path = dev_path;
  int line_buffer = line_buf;
  int fd;
  int ret;
  FAR lv_disp_t *disp;

  struct fb_videoinfo_s vinfo;
  struct lcd_planeinfo_s pinfo;

  if (device_path == NULL)
    {
      device_path = CONFIG_LV_PORT_LCDDEV_DEFAULT_DEVICEPATH;
    }

  LV_LOG_INFO("lcddev %s opening", device_path);
  fd = open(device_path, 0);
  if (fd < 0)
    {
      LV_LOG_ERROR("lcddev open failed: %d", errno);
      return NULL;
    }

  LV_LOG_INFO("lcddev %s open success", device_path);

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

  LV_LOG_INFO("VideoInfo:");
  LV_LOG_INFO("      fmt: %u", vinfo.fmt);
  LV_LOG_INFO("     xres: %u", vinfo.xres);
  LV_LOG_INFO("     yres: %u", vinfo.yres);
  LV_LOG_INFO("  nplanes: %u", vinfo.nplanes);

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

  LV_LOG_INFO("PlaneInfo:");
  LV_LOG_INFO("      bpp: %u", pinfo.bpp);

  if (pinfo.bpp != LV_COLOR_DEPTH)
    {
      LV_LOG_ERROR("lcddev bpp = %d, LV_COLOR_DEPTH = %d,"
          " color depth does not match", pinfo.bpp, LV_COLOR_DEPTH);
      close(fd);
      return NULL;
    }

#ifdef CONFIG_LV_PORT_LCDDEV_FULL_SCREEN_BUFFER
  line_buffer = vinfo.yres;
#else
  if (line_buffer <= 0)
    {
      line_buffer = CONFIG_LV_PORT_LCDDEV_LINE_BUFFER_DEFAULT;
    }
#endif

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

  return disp;
}
