/****************************************************************************
 * apps/graphics/ft80x/ft80x_dl.c
 *
 *   Copyright (C) 2018 Gregory Nutt. All rights reserved.
 *   Author: Gregory Nutt <gnutt@nuttx.org>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 * 3. Neither the name NuttX nor the names of its contributors may be
 *    used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 ****************************************************************************/

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

#include <nuttx/config.h>

#include <sys/types.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <errno.h>

#include <nuttx/lcd/ft80x.h>

#include "graphics/ft80x.h"
#include "ft80x.h"

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

/****************************************************************************
 * Name: ft80x_dl_dump
 *
 * Description:
 *   Dump the content of the display list to stdout as it is written from
 *   the local display buffer to hardware.
 *
 * Input Parameters:
 *   buffer - An instance of struct ft80x_dlbuffer_s allocated by the caller.
 *   data   - Data being written.
 *   len    - Number of bytes being written
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

#ifdef CONFIG_GRAPHICS_FT80X_DEBUG_INFO
static void ft80x_dl_dump(FAR struct ft80x_dlbuffer_s *buffer,
                          FAR const void *data, size_t len)
{
  size_t nwords;
  int max;
  int i;
  int j;

  printf("Writing display list:\n");
  printf("  buffer: dlsize=%lu dloffset=%lu coproc=%u\n",
         (unsigned long)buffer->dlsize, (unsigned long)buffer->dloffset,
         buffer->coproc);
  printf("  write:  data=%p length=%lu\n",
         data, (unsigned long)len);

  nwords = len >> 2;
  for (i = 0; i < nwords; i += 8)
    {
      printf("  %04x: ", i << 2);

      max = i + 4;
      if (max >= nwords)
        {
          max = nwords - i;
        }

      for (j = i; j < max; j++)
        {
          printf(" %08x", buffer->dlbuffer[j]);
        }

      putchar(' ');

      max = i + 8;
      if (i + max >= nwords)
        {
          max = nwords - i;
        }

      for (j = i + 4; j < max; j++)
        {
          printf(" %08x", buffer->dlbuffer[j]);
        }

      putchar('\n');
    }
}
#else
#  define ft80x_dl_dump(b,d,l)
#endif

/****************************************************************************
 * Name: ft80x_dl_append
 *
 * Description:
 *   Append the display list data to the appropriate memory region.
 *
 * Input Parameters:
 *   fd     - The file descriptor of the FT80x device.  Opened by the caller
 *            with write access.
 *   buffer - An instance of struct ft80x_dlbuffer_s allocated by the caller.
 *   data   - A pointer to the start of the data to be written.
 *   len    - The number of bytes to be written.
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

static int ft80x_dl_append(int fd, FAR struct ft80x_dlbuffer_s *buffer,
                           FAR const void *data, size_t len)
{
  int ret;

  ft80x_dl_dump(buffer, data, len);
  if (buffer->coproc)
    {
      /* Append data to RAM CMD */

      ret = ft80x_ramcmd_append(fd, data, len);
    }
  else
    {
      /* Append data to RAM DL */

      ret = ft80x_ramdl_append(fd, data, len);
    }

  return ret;
}

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

/****************************************************************************
 * Name: ft80x_dl_start
 *
 * Description:
 *   Start a new display list.  This function will:
 *
 *   1) Set the total display list size to zero
 *   2) Set the display list buffer offset to zero
 *   3) Reposition the VFS so that subsequent writes will be to the
 *      beginning of the hardware display list.
 *      (Only for DL memory commands)
 *   4) Write the CMD_DLSTART command into the local display list buffer
 *      (Only for co-processor commands)
 *
 * Input Parameters:
 *   fd     - The file descriptor of the FT80x device.  Opened by the caller
 *            with write access.
 *   buffer - An instance of struct ft80x_dlbuffer_s allocated by the caller.
 *   coproc - True: Use co-processor FIFO; false: Use DL memory.
 *
 * Returned Value:
 *   Zero (OK) on success.  A negated errno value on failure.
 *
 ****************************************************************************/

int ft80x_dl_start(int fd, FAR struct ft80x_dlbuffer_s *buffer, bool coproc)
{
  struct ft80x_cmd_dlstart_s dlstart;
  int ret;

  ft80x_info("fd=%d buffer=%p\n", fd, buffer);
  DEBUGASSERT(fd >= 0 && buffer != NULL);

  /* 1) Set the total display list size to zero
   * 2) Set the display list buffer offset to zero
   */

  buffer->coproc   = coproc;
  buffer->dlsize   = 0;
  buffer->dloffset = 0;

  if (!coproc)
    {
      /* 3) Reposition the VFS so that subsequent writes will be to the
       *    beginning of the hardware display list.
       */

      ret = ft80x_ramdl_rewind(fd);
      if (ret < 0)
        {
          ft80x_err("ERROR: ft80x_ramdl_rewind failed: %d\n", ret);
        }
    }
  else
    {
      /* 4) Write the CMD_DLSTART command into the local display list
       *    buffer. (Only for co-processor commands)
       */

      dlstart.cmd = FT80X_CMD_DLSTART;
      ret = ft80x_dl_data(fd, buffer, &dlstart,
                          sizeof(struct ft80x_cmd_dlstart_s));
      if (ret < 0)
        {
          ft80x_err("ERROR: ft80x_dl_data failed: %d\n", ret);
        }
    }

  return ret;
}

/****************************************************************************
 * Name: ft80x_dl_end
 *
 * Description:
 *   Terminate the display list.  This function will:
 *
 *   1) Add the DISPLAY command to the local display list buffer to finish
 *      the last display
 *   2) If using co-processor RAM CMD, add the CMD_SWAP to the DL command
 *      list
 *   3) Flush the local display buffer to hardware and set the display list
 *      buffer offset to zero.
 *   4) Swap to the newly created display list (DL memory case only).
 *   5) For the case of the co-processor RAM CMD, it will also wait for the
 *      FIFO to be emptied.
 *
 * Input Parameters:
 *   fd     - The file descriptor of the FT80x device.  Opened by the caller
 *            with write access.
 *   buffer - An instance of struct ft80x_dlbuffer_s allocated by the caller.
 *
 * Returned Value:
 *   Zero (OK) on success.  A negated errno value on failure.
 *
 ****************************************************************************/

int ft80x_dl_end(int fd, FAR struct ft80x_dlbuffer_s *buffer)
{
  struct
  {
    struct ft80x_dlcmd_s display;
    struct ft80x_cmd32_s swap;
  } s;

  size_t size;
  int ret;

  ft80x_info("fd=%d buffer=%p\n", fd, buffer);
  DEBUGASSERT(fd >= 0 && buffer != NULL);

  /* 1) Add the DISPLAY command to the local display list buffer to finish
   *    the last display
   */

  s.display.cmd = FT80X_DISPLAY();
  size          = sizeof(struct ft80x_dlcmd_s);

  /* 2) If using co-processor RAM CMD, add the CMD_SWAP to the DL command
   *    list
   */

  if (buffer->coproc)
    {
      s.swap.cmd = FT80X_CMD_SWAP;
      size       = sizeof(s);
    };

  ret = ft80x_dl_data(fd, buffer, &s, size);
  if (ret < 0)
    {
      ft80x_err("ERROR: ft80x_dl_data failed: %d\n", ret);
      return ret;
    }

  /* 3) Flush the local display buffer to hardware and set the display list
   *    buffer offset to zero.
   */

  ret = ft80x_dl_flush(fd, buffer, false);
  if (ret < 0)
    {
      ft80x_err("ERROR: ft80x_dl_flush failed: %d\n", ret);
    }

  /* 4) Swap to the newly created display list (DL memory case only). */

  if (!buffer->coproc)
    {
      ret = ft80x_dl_swap(fd);
      if (ret < 0)
        {
          ft80x_err("ERROR: ft80x_dl_swap failed: %d\n", ret);
          return ret;
        }
    }

  /* 5) For the case of the co-processor RAM CMD, it will also wait for the
   *    FIFO to be emptied.
   */

  if (buffer->coproc)
    {
      ret = ft80x_ramcmd_waitfifoempty(fd);
      if (ret < 0)
        {
          ft80x_err("ERROR: ft80x_ramcmd_waitfifoempty failed: %d\n", ret);
          return ret;
        }
    }

  return ret;
}

/****************************************************************************
 * Name: ft80x_dl_data
 *
 * Description:
 *   Add data to the display list and increment the display list buffer
 *   offset.  If the data will not fit into the local display buffer, then
 *   the local display buffer will first be flushed to hardware in order to
 *   free up space.
 *
 * Input Parameters:
 *   fd     - The file descriptor of the FT80x device.  Opened by the caller
 *            with write access.
 *   buffer - An instance of struct ft80x_dlbuffer_s allocated by the caller.
 *   data   - The data to be added to the display list
 *   datlen - The length of the data to be added to the display list.  If
 *            this is not an even multiple of 4 bytes, then the actual length
 *            will be padded with zero bytes to achieve alignment.
 *
 * Returned Value:
 *   Zero (OK) on success.  A negated errno value on failure.
 *
 ****************************************************************************/

int ft80x_dl_data(int fd, FAR struct ft80x_dlbuffer_s *buffer,
                  FAR const void *data, size_t datlen)
{
  FAR uint8_t *bufptr;
  size_t padlen;
  int ret;

  ft80x_info("fd=%d buffer=%p data=%p datlen=%u\n", fd, buffer, data, datlen);
  DEBUGASSERT(fd >= 0 && buffer != NULL && data != NULL && datlen > 0);

  if (datlen > 0)
    {
      /* This is the length of the data after alignment */

      padlen = (datlen + 3) & ~3;
      if (padlen != datlen)
        {
          ft80x_warn("WARNING: Length padded to %u->%u\n", datlen, padlen);
        }

      /* Is there enough space in the local display list buffer to hold the
       * new data?
       */

      if ((size_t)buffer->dloffset + padlen > FT80X_DL_BUFSIZE)
        {
          /* No... flush the local buffer */

          ret = ft80x_dl_flush(fd, buffer, false);
          if (ret < 0)
            {
              ft80x_err("ERROR: ft80x_dl_flush failed: %d\n", ret);
              return ret;
            }
        }

      /* Special case:  The new data won't fit into our local display list
       * buffer.  Here we can assume the flush above occurred.  We can then
       * work around by writing directly, unbuffered from the caller's
       * buffer.
       */

      if (padlen > FT80X_DL_BUFSIZE)
        {
          size_t writelen;

          /* Write the aligned portion of the data directly to the FT80x
           * hardware display list.
           */

          writelen = datlen & ~3;
          ret = ft80x_dl_append(fd, buffer, data, writelen);
          if (ret < 0)
            {
              ft80x_err("ERROR: ft80x_dl_append failed: %d\n", ret);
              return ret;
            }

          buffer->dlsize += writelen;

          /* Is there any unaligned remainder?  If the original data length
           * was aligned, then we should have writelen == datlen == padlen.
           * If it the length was unaligned, then we should have writelen <
           * datlen < padlen
           */

          if (writelen < datlen)
            {
              /* Yes... we will handle the unaligned bytes below. */

              data    = (FAR void *)((uintptr_t)data + writelen);
              datlen -= writelen;
              padlen -= writelen;
            }
          else
            {
              /* No.. then we are finished */

              return OK;
            }
        }

      /* Copy the data into the local display list buffer */

      bufptr            = (FAR uint8_t *)buffer->dlbuffer;
      bufptr           += buffer->dloffset;
      memcpy(bufptr, data, datlen);

      bufptr           += datlen;
      buffer->dloffset += datlen;

     /* Then append zero bytes as necessary to achieve alignment */

      while (datlen < padlen)
        {
          *bufptr++     = 0;
          buffer->dloffset++;
          datlen++;
        }

      buffer->dlsize   += padlen;
    }

  return OK;
}

/****************************************************************************
 * Name: ft80x_dl_string
 *
 * Description:
 *   Add the string along with its NUL terminator to the display list and
 *   increment the display list buffer offset.  If the length of the string
 *   with its NUL terminator is not an even multiple of 4 bytes, then the
 *   actual length will be padded with zero bytes to achieve alignment.
 *
 *   If the data will not fit into the local display buffer, then the local
 *   display buffer will first be flushed to hardware in order to free up
 *   space.
 *
 * Input Parameters:
 *   fd     - The file descriptor of the FT80x device.  Opened by the caller
 *            with write access.
 *   buffer - An instance of struct ft80x_dlbuffer_s allocated by the caller.
 *   str    - The string to be added to the display list.  If NUL, then a
 *            NUL string will be added to the display list.
 *
 * Returned Value:
 *   Zero (OK) on success.  A negated errno value on failure.
 *
 ****************************************************************************/

int ft80x_dl_string(int fd, FAR struct ft80x_dlbuffer_s *buffer,
                    FAR const char *str)
{
  FAR uint8_t *bufptr;
  int datlen;
  int padlen;
  int ret;

  ft80x_info("fd=%d buffer=%p str=%p\n", fd, buffer, str);
  DEBUGASSERT(fd >= 0 && buffer != NULL);

  /* Get the length the the string (excluding the NUL terminator) */

  if (str == NULL)
    {
      str    = "";
      datlen = 0;
    }
  else
    {
      datlen = strlen(str);
    }

  /* This is the length of the string after the NUL terminator is added and
   * the length is aligned to 32-bit boundary alignment.
   */

  padlen = (datlen + 4) & ~3;
  if (padlen != (datlen + 1))
    {
      ft80x_warn("WARNING: Length padded to %u->%u\n", datlen, padlen);
    }

  /* Is there enough space in the local display list buffer to hold the new
   * string?
   */

  if ((size_t)buffer->dloffset + padlen > FT80X_DL_BUFSIZE)
    {
      /* No... flush the local buffer */

      ret = ft80x_dl_flush(fd, buffer, false);
      if (ret < 0)
        {
          ft80x_err("ERROR: ft80x_dl_flush failed: %d\n", ret);
          return ret;
        }
    }

  /* Special case:  The new string won't fit into our local display list
   * buffer.  Here we can assume the flush above occurred.  We can then
   * work around by writing directly, unbuffered from the caller's
   * buffer.
   */

  if (padlen > FT80X_DL_BUFSIZE)
    {
      size_t writelen;

      /* Write the aligned portion of the string directly to the FT80x
       * hardware display list.
       */

      writelen = datlen & ~3;
      ret = ft80x_dl_append(fd, buffer, str, writelen);
      if (ret < 0)
        {
          ft80x_err("ERROR: ft80x_dl_append failed: %d\n", ret);
          return ret;
        }

      buffer->dlsize += writelen;

      /* There should always be an unaligned remainder  If the original
       * string length was aligned, then we should have writelen == datlen <
       * padlen. If it the length was unaligned, then we should have
       * writelen < datlen < padlen
       */

      DEBUGASSERT(writelen < padlen);

      /* We will handle the unaligned remainder below. */

      str     = &str[writelen];
      datlen -= writelen;
      padlen -= writelen;
    }

  /* Copy the data into the local display list buffer */

  bufptr            = (FAR uint8_t *)buffer->dlbuffer;
  bufptr           += buffer->dloffset;
  strcpy((FAR char *)bufptr, str);

  /* NOTE: that strcpy will copy the NUL terminator too */

  datlen++;

  /* Update pointers/offsets */

  bufptr           += datlen;
  buffer->dloffset += datlen;

  /* Then append zero bytes as necessary to achieve alignment */

  while (datlen < padlen)
    {
      *bufptr++     = 0;
      buffer->dloffset++;
      datlen++;
    }

  buffer->dlsize   += padlen;
  return OK;
}

/****************************************************************************
 * Name: ft80x_dl_flush
 *
 * Description:
 *   Flush the current contents of the local local display list buffer to
 *   hardware and reset the local display list buffer offset to zero.
 *
 * Input Parameters:
 *   fd     - The file descriptor of the FT80x device.  Opened by the caller with
 *            write access.
 *   buffer - An instance of struct ft80x_dlbuffer_s allocated by the caller.
 *   wait   - True: wait until data has been consumed by the co-processor
 *            (only for co-processor destination); false:  Send to hardware
 *            and return immediately.
 *
 * Returned Value:
 *   Zero (OK) on success.  A negated errno value on failure.
 *
 ****************************************************************************/

int ft80x_dl_flush(int fd, FAR struct ft80x_dlbuffer_s *buffer, bool wait)
{
  int ret;

  ft80x_info("fd=%d buffer=%p dloffset=%u\n", fd, buffer, buffer->dloffset);
  DEBUGASSERT(fd >= 0 && buffer != NULL);

  /* Write the content of the local display buffer to hardware. */

  ret = ft80x_dl_append(fd, buffer, buffer->dlbuffer, buffer->dloffset);
  if (ret < 0)
    {
      ft80x_err("ERROR: ft80x_dl_append failed: %d\n", ret);
      return ret;
    }

  buffer->dloffset = 0;

  /* For the case of the co-processor RAM CMD, it will also wait for the
   * FIFO to be emptied if wait == true.
   */

  if (wait && buffer->coproc)
    {
      ret = ft80x_ramcmd_waitfifoempty(fd);
      if (ret < 0)
        {
          ft80x_err("ERROR: ft80x_ramcmd_waitfifoempty failed: %d\n", ret);
          return ret;
        }
    }

  return OK;
}

/****************************************************************************
 * Name: ft80x_dl_create
 *
 * Description:
 *   For simple display lists, this function combines all functionality into
 *   a single combined.  This function does the following:
 *
 *   1) Calls ft80x_dl_dlstart() to initialize the display list.
 *   2) Calls ft80x_dl_data() to transfer the simple display list
 *   3) Calls ft80x_dl_end() to complete the display list
 *
 * Input Parameters:
 *   fd     - The file descriptor of the FT80x device.  Opened by the caller
 *            with write access.
 *   buffer - An instance of struct ft80x_dlbuffer_s allocated by the caller.
 *   data   - Pointer to a uint32_t array containing the simple display list
 *   nwords - The number of 32-bit words in the array.
 *   coproc - True: Use co-processor FIFO; false: Use DL memory.
 *
 * Returned Value:
 *   Zero (OK) on success.  A negated errno value on failure.
 *
 ****************************************************************************/

int ft80x_dl_create(int fd, FAR struct ft80x_dlbuffer_s *buffer,
                    FAR const uint32_t *cmds, unsigned int nwords,
                    bool coproc)
{
  int ret;

  ft80x_info("fd=%d buffer=%p cmds=%p nwords=%u coproc=%u\n",
             fd, buffer, cmds, nwords, coproc);
  DEBUGASSERT(fd >= 0 && buffer != NULL && cmds != NULL && nwords > 0);

  /* Create the hardware display list */

  ret = ft80x_dl_start(fd, buffer, coproc);
  if (ret < 0)
    {
      ft80x_err("ERROR: ft80x_dl_start failed: %d\n", ret);
      return ret;
    }

  /* Copy the rectangle data into the display list */

  ret = ft80x_dl_data(fd, buffer, cmds, nwords << 2);
  if (ret < 0)
    {
      ft80x_err("ERROR: ft80x_dl_data failed: %d\n", ret);
      return ret;
    }

  /* And terminate the display list */

  ret = ft80x_dl_end(fd, buffer);
  if (ret < 0)
    {
      ft80x_err("ERROR: ft80x_dl_end failed: %d\n", ret);
      return ret;
    }

  return OK;
}
