/****************************************************************************
 * apps/graphics/ft80x/ft80x_audio.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 <sys/stat.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <assert.h>
#include <errno.h>

#include <nuttx/lcd/ft80x.h>

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

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

/* These configuration settings define the graphics memory region that we
 * will use for audio buffering.
 */

#define AUDIO_BUFOFFSET    CONFIG_GRAPHICS_FT80X_AUDIO_BUFOFFSET
#define AUDIO_BUFSIZE      CONFIG_GRAPHICS_FT80X_AUDIO_BUFSIZE
#define AUDIO_BUFEND       (AUDIO_BUFOFFSET + AUDIO_BUFSIZE)

#if AUDIO_BUFEND > FT80X_RAM_G_SIZE
#  error "Audio buffer extends beyond RAM G"
#endif

#define RAMG_STARTADDR     (FT80X_RAM_G + AUDIO_BUFOFFSET)
#define RAMG_ENDADDR       (FT80X_RAM_G + AUDIO_BUFEND)

/* The display list buffer will be re-purposed as an I/O buffer for the
 * transfer of audio data to RAM G.
 */

#if FT80X_DL_BUFSIZE > AUDIO_BUFSIZE
#  define MAX_DLBUFFER     AUDIO_BUFSIZE
#else
#  define MAX_DLBUFFER     FT80X_DL_BUFSIZE
#endif

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

/****************************************************************************
 * Name: ft80x_audio_enable
 *
 * Description:
 *   Play an short sound effect.  If there is a audio amplifier on board
 *   (such as TPA6205A or LM4864), then there may also be an active low
 *   audio shutdown output.  That output is controlled by this interface.
 *
 * Input Parameters:
 *   fd     - The file descriptor of the FT80x device.  Opened by the
 *            caller with write access.
 *   enable - True: Enabled the audio amplifier; false: disable
 *
 * Returned Value:
 *   Zero (OK) on success.  A negated errno value on failure.
 *
 ****************************************************************************/

int ft80x_audio_enable(int fd, bool enable)
{
#ifndef CONFIG_LCD_FT80X_AUDIO_NOSHUTDOWN
  int ret;

  ret = ioctl(fd, FT80X_IOC_AUDIO, (unsigned long)enable);
  if (ret < 0)
    {
      ret = -errno;
      ft80x_err("ERROR: ioctl(FT80X_IOC_AUDIO) failed: %d\n", ret);
    }

  return ret;

#else
  return OK;
#endif
}

/****************************************************************************
 * Name: ft80x_audio_playsound
 *
 * Description:
 *   Play an short sound effect.
 *
 *   NOTE:  It may be necessary to enable the audio amplifier with
 *   ft80x_audio_enable() prior to calling this function.
 *
 * Input Parameters:
 *   fd     - The file descriptor of the FT80x device.  Opened by the
 *            caller with write access.
 *   effect - The sound effect to use (see FT80X_EFFECT_* definitions).
 *   pitch  - Pitch associated with the sound effect (see FT80X_NOTE_*
 *            definitions).  May be zero if there is no pitch associated
 *            with the effect.
 *
 * Returned Value:
 *   Zero (OK) on success.  A negated errno value on failure.
 *
 ****************************************************************************/

int ft80x_audio_playsound(int fd, uint16_t effect, uint16_t pitch)
{
  uint32_t cmds[2];

  cmds[0] = effect | pitch;
  cmds[1] = 1;

  return ft80x_putregs(fd, FT80X_REG_SOUND, 2, cmds);
}

/****************************************************************************
 * Name: ft80x_audio_playfile
 *
 * Description:
 *   Play an audio file.  Audio files must consist of raw sample data.
 *
 *   NOTE:  It may be necessary to enable the audio amplifier with
 *   ft80x_audio_enable() prior to calling this function.
 *
 * 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.
 *   filepath  - Absolute path to the audio file
 *   format    - Audio format.  One of:
 *
 *               AUDIO_FORMAT_LINEAR  Linear Sample format
 *               AUDIO_FORMAT_ULAW    uLaw Sample format
 *               AUDIO_FORMAT_ADPCM   4-bit IMA ADPCM Sample format
 *
 *   frequency - Audio sample frequency (<65,536)
 *   volume    - Playback volume (0=mute; 255=max)
 *
 * Returned Value:
 *   Zero (OK) on success.  A negated errno value on failure.
 *
 ****************************************************************************/

int ft80x_audio_playfile(int fd, FAR struct ft80x_dlbuffer_s *buffer,
                         FAR const char *filepath, uint8_t format,
                         uint16_t frequency, uint8_t volume)
{
  struct ft80x_cmd_memset_s memset;
  struct stat buf;
  size_t freespace;
  size_t remaining;
  size_t readlen;
  ssize_t nread;
  uint32_t offset;
  uint32_t readptr;
  bool started;
  int audiofd;
  int ret;

  DEBUGASSERT(filepath != NULL);

  /* Open the audio file */

  audiofd = open(filepath, O_RDONLY);
  if (audiofd < 0)
    {
      int errcode = errno;
      ft80x_err("ERROR: open of %s failed: %d\n", filepath, errcode);
      return -errcode;
    }

  /* Get the size of the file */

  ret = fstat(audiofd, &buf);
  if (ret < 0)
    {
      ft80x_err("ERROR: fstat of %s failed: %d\n", filepath, ret);
      goto errout_with_fd;
    }

  ft80x_info("File: %s\n", filepath);
  ft80x_info("Size: %lu\n", (unsigned long)buf.st_size);

  /* Loop until the whole audio file has been sent */

  remaining = buf.st_size;
  offset    = 0;
  started   = false;

  while (remaining > 0)
    {
      /* How much can we read on this pass? */

      readlen = remaining;

      /* Clip to the amount that will fit into the display buffer (which we
       * are re-purposing for audio data buffer for now)
       */

      if (readlen > MAX_DLBUFFER)
        {
          readlen = MAX_DLBUFFER;
        }

      /* Check size of the buffer we can fit into RAM G */

      do
        {
          /* Get the current playback position */

          readptr = 0;
          ret = ft80x_getreg32(fd, FT80X_REG_PLAYBACK_READPTR, &readptr);
          if (ret < 0)
            {
              ft80x_err("ERROR: ft80x_getreg32 failed: %d\n", ret);
              goto errout_with_fd;
            }

          /* Check if the readptr is before the current offset.  In this
           * case, we can use all of the RAM G from the current offset to
           * the end of the buffer.
           *
           *   Case 1:  readptr <= offset
           *
           *     +--------+-------+-----+
           *     |        |       |xxxxx|
           *     +--------+-------+-----+
           *     |        |       |     ^ end buffer
           *     |        |       ^ offset
           *     |        ^ readptr
           *     ^ start buffer
           *
           * NOTE: There is a race condition here:  The readptr could
           * overtake the offset before we can perform the write.
           */

          if (readptr <= offset)
            {
              freespace = AUDIO_BUFSIZE - offset;
              break;
            }

          /* No, the readptr is after the offset.  In this case we can use
           * all of the RAM G from the current offset to the readptr.
           *
           *   Case 2:  readptr > offset
           *
           *     +--------+-------+-----+
           *     |        |xxxxxxx|     |
           *     +--------+-------+-----+
           *     |        |       |     ^ end buffer
           *     |        |       ^ readptr
           *     |        ^ offset
           *     ^ start buffer
           */

          else
            {
              freespace = readptr - offset;
            }

          /* Terminate the poll when all the memory to end of the RAM G
           * buffer is available (see the 'break' above), when an optimally
           * sized space is available (MAX_DLBUFFER), there is space to
           * write all of the 'remaining' file data, or all of the memory is
           * free up to the end of the RAM G buffer (actually already
           * handled by the above 'break')
           */
        }
      while (freespace < MAX_DLBUFFER &&
             freespace < remaining &&
             freespace < (AUDIO_BUFSIZE - offset));

      /* Clip to the amount that will fit at the tail of the RAM G buffer */

      if (readlen > freespace)
        {
          readlen = freespace;
        }

      /* Read the data into the available display list buffer */

      nread = read(audiofd, buffer->dlbuffer, readlen);
      if (nread < 0)
        {
          ret = -errno;
          ft80x_err("ERROR: read from %s failed: %d\n", filepath, ret);
          goto errout_with_fd;
        }

      /* Since we know the size of the file, we don't expect EOF */

      DEBUGASSERT(nread > 0);

      /* Copy the data read into the graphics memory */

      ret = ft80x_ramg_write(fd, offset, buffer->dlbuffer, nread);
      if (ret < 0)
        {
          ft80x_err("ERROR: ft80x_ramg_write failed: %d\n", ret);
          goto errout_with_fd;
        }

      /* Update pointers and counts */

      offset    += nread;
      remaining -= nread;

      /* Wrap the offset back to the beginning of the buffer if necessary */

      if (offset >= AUDIO_BUFSIZE)
        {
          offset = 0;
        }

      /* If we have started playing the audio file in RAGM_G.  NOTE that
       * there is a race condition here.  If the chunk size is small or if
       * the data transfer rate is low, then there may be data under-run
       * which will disrupt the continuity of the audio.
       */

      if (!started)
        {
          /* Start playing at the beginning of graphics memory
           * Set the audio playback start address
           */

          ret = ft80x_putreg32(fd, FT80X_REG_PLAYBACK_START,
                               RAMG_STARTADDR);
          if (ret < 0)
            {
              ft80x_err("ERROR: ft80x_putreg32 failed: %d\n", ret);
              goto errout_with_fd;
            }

          /* Set the length of the audio buffer */

          ret = ft80x_putreg32(fd, FT80X_REG_PLAYBACK_LENGTH,
                               AUDIO_BUFSIZE);
          if (ret < 0)
            {
              ft80x_err("ERROR: ft80x_putreg32 failed: %d\n", ret);
              goto errout_with_fd;
            }

          /* Set the sample frequency */

          ret = ft80x_putreg16(fd, FT80X_REG_PLAYBACK_FREQ, frequency);
          if (ret < 0)
            {
              ft80x_err("ERROR: ft80x_putreg16 failed: %d\n", ret);
              goto errout_with_fd;
            }

          /* Set the audio format */

          ret = ft80x_putreg8(fd, FT80X_REG_PLAYBACK_FORMAT, format);
          if (ret < 0)
            {
              ft80x_err("ERROR: ft80x_putreg8 failed: %d\n", ret);
              goto errout_with_fd;
            }

          /* Loop when we get to the end of the file */

          ret = ft80x_putreg8(fd, FT80X_REG_PLAYBACK_LOOP, 1);
          if (ret < 0)
            {
              ft80x_err("ERROR: ft80x_putreg8 failed: %d\n", ret);
              goto errout_with_fd;
            }

          /* Set the volume */

          ret = ft80x_putreg8(fd, FT80X_REG_VOL_PB, volume);
          if (ret < 0)
            {
              ft80x_err("ERROR: ft80x_putreg8 failed: %d\n", ret);
              goto errout_with_fd;
            }

          /* And start the playback */

          ret = ft80x_putreg8(fd, FT80X_REG_PLAYBACK_PLAY, 1);
          if (ret < 0)
            {
              ft80x_err("ERROR: ft80x_putreg8 failed: %d\n", ret);
              goto errout_with_fd;
            }

          started = true;
        }
    }

  /* Transfer is complete.  'offset' points to the end of the file in RAM G.
   * Clear all of the RAM G at the end of the file so that audio is muted
   * when the end of file is encountered.
   *
   *   Case 1:  readptr <= offset
   *
   *     +--------+-------+-----+
   *     |00000000|       |00000|
   *     +--------+-------+-----+
   *     |        |       |     ^ end buffer
   *     |        |       ^ offset
   *     |        ^ readptr
   *     ^ start buffer
   *
   *   Case 2:  readptr > offset
   *
   *     +--------+-------+-----+
   *     |        |0000000|     |
   *     +--------+-------+-----+
   *     |        |       |     ^ end buffer
   *     |        |       ^ readptr
   *     |        ^ offset
   *     ^ start buffer
   */

  memset.cmd       = FT80X_CMD_MEMSET;
  memset.value     = 0;

  readptr = 0;
  ret = ft80x_getreg32(fd, FT80X_REG_PLAYBACK_READPTR, &readptr);
  if (ret < 0)
    {
      ft80x_err("ERROR: ft80x_getreg32 failed: %d\n", ret);
      goto errout_with_fd;
    }

  if (readptr <= offset)
    {
      if (offset < AUDIO_BUFSIZE)
        {
          memset.ptr  = RAMG_STARTADDR + offset;
          memset.num  = AUDIO_BUFSIZE - offset;

          ret = ft80x_coproc_send(fd, (FAR const uint32_t *)&memset, 4);
          if (ret < 0)
            {
              ft80x_err("ERROR: ft80x_coproc_send failed: %d\n", ret);
              goto errout_with_fd;
            }
        }

      if (readptr > 0)
        {
          memset.ptr  = RAMG_STARTADDR;
          memset.num  = readptr;

          ret = ft80x_coproc_send(fd, (FAR const uint32_t *)&memset, 4);
          if (ret < 0)
            {
              ft80x_err("ERROR: ft80x_coproc_send failed: %d\n", ret);
              goto errout_with_fd;
            }
        }
    }
  else /* if (readptr > offset) */
    {
      memset.ptr  = RAMG_STARTADDR + offset;
      memset.num  = readptr - offset;

      ret = ft80x_coproc_send(fd, (FAR const uint32_t *)&memset, 4);
      if (ret < 0)
        {
          ft80x_err("ERROR: ft80x_coproc_send failed: %d\n", ret);
          goto errout_with_fd;
        }
    }

  /* Wait until the read pointer wraps back to the beginning of the buffer */

  do
    {
      readptr = 0;
      ret = ft80x_getreg32(fd, FT80X_REG_PLAYBACK_READPTR, &readptr);
      if (ret < 0)
        {
          ft80x_err("ERROR: ft80x_getreg32 failed: %d\n", ret);
          goto errout_with_fd;
        }
    }
  while (readptr > offset);

  /* Wait until the read pointer pass through write pointer into the zeroed
   * area.
   *
   * REVISIT:  What if the offset is at the very end of the buffer?
   */

  do
    {
      readptr = 0;
      ret = ft80x_getreg32(fd, FT80X_REG_PLAYBACK_READPTR, &readptr);
      if (ret < 0)
        {
          ft80x_err("ERROR: ft80x_getreg32 failed: %d\n", ret);
          goto errout_with_fd;
        }
    }
  while (readptr < offset);

  /* The file is done... Stop looping */

  ret = ft80x_putreg8(fd, FT80X_REG_PLAYBACK_LOOP, 1);
  if (ret < 0)
    {
      ft80x_err("ERROR: ft80x_putreg8 failed: %d\n", ret);
      goto errout_with_fd;
    }

  /* Set the playback length to zero */

  ret = ft80x_putreg32(fd, FT80X_REG_PLAYBACK_LENGTH, 0);
  if (ret < 0)
    {
      ft80x_err("ERROR: ft80x_putreg32 failed: %d\n", ret);
      goto errout_with_fd;
    }

  /* Set the volume to zero */

  ret = ft80x_putreg8(fd, FT80X_REG_VOL_PB, volume);
  if (ret < 0)
    {
      ft80x_err("ERROR: ft80x_putreg8 failed: %d\n", ret);
      goto errout_with_fd;
    }

  /* And stop the playback */

  ret = ft80x_putreg8(fd, FT80X_REG_PLAYBACK_PLAY, 1);
  if (ret < 0)
    {
      ft80x_err("ERROR: ft80x_putreg8 failed: %d\n", ret);
    }

errout_with_fd:
  close(audiofd);
  return ret;
}