/****************************************************************************
 * drivers/audio/audio_dma.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/audio/audio_dma.h>
#include <nuttx/kmalloc.h>
#include <nuttx/queue.h>

#include <debug.h>

/****************************************************************************
 * Private Types
 ****************************************************************************/

struct audio_dma_s
{
  struct audio_lowerhalf_s dev;
  struct dma_chan_s *chan;
  uintptr_t src_addr;
  uintptr_t dst_addr;
  uint8_t *alloc_addr;
  uint8_t alloc_index;
  uint8_t fifo_width;
  bool playback;
  bool xrun;
  struct dq_queue_s pendq;
  apb_samp_t buffer_size;
  apb_samp_t buffer_num;
};

/****************************************************************************
 * Private Function Prototypes
 ****************************************************************************/

static int audio_dma_getcaps(struct audio_lowerhalf_s *dev, int type,
                             struct audio_caps_s *caps);
static int audio_dma_shutdown(struct audio_lowerhalf_s *dev);
#ifdef CONFIG_AUDIO_MULTI_SESSION
static int audio_dma_configure(struct audio_lowerhalf_s *dev,
                               void *session,
                               const struct audio_caps_s *caps);
static int audio_dma_start(struct audio_lowerhalf_s *dev,
                           void *session);
#ifndef CONFIG_AUDIO_EXCLUDE_STOP
static int audio_dma_stop(struct audio_lowerhalf_s *dev, void *session);
#endif
#ifndef CONFIG_AUDIO_EXCLUDE_PAUSE_RESUME
static int audio_dma_pause(struct audio_lowerhalf_s *dev,
                           void *session);
static int audio_dma_resume(struct audio_lowerhalf_s *dev,
                            void *session);
#endif
static int audio_dma_reserve(struct audio_lowerhalf_s *dev,
                             void **session);
static int audio_dma_release(struct audio_lowerhalf_s *dev,
                             void *session);
#else
static int audio_dma_configure(struct audio_lowerhalf_s *dev,
                               const struct audio_caps_s *caps);
static int audio_dma_start(struct audio_lowerhalf_s *dev);
#ifndef CONFIG_AUDIO_EXCLUDE_STOP
static int audio_dma_stop(struct audio_lowerhalf_s *dev);
#endif
#ifndef CONFIG_AUDIO_EXCLUDE_PAUSE_RESUME
static int audio_dma_pause(struct audio_lowerhalf_s *dev);
static int audio_dma_resume(struct audio_lowerhalf_s *dev);
#endif
static int audio_dma_reserve(struct audio_lowerhalf_s *dev);
static int audio_dma_release(struct audio_lowerhalf_s *dev);
#endif
static int audio_dma_allocbuffer(struct audio_lowerhalf_s *dev,
                                 struct audio_buf_desc_s *bufdesc);
static int audio_dma_freebuffer(struct audio_lowerhalf_s *dev,
                                struct audio_buf_desc_s *bufdesc);
static int audio_dma_enqueuebuffer(struct audio_lowerhalf_s *dev,
                                   struct ap_buffer_s *apb);
static int audio_dma_ioctl(struct audio_lowerhalf_s *dev, int cmd,
                           unsigned long arg);
static void audio_dma_callback(struct dma_chan_s *chan, void *arg,
                               ssize_t len);

/****************************************************************************
 * Private Data
 ****************************************************************************/

static const struct audio_ops_s g_audio_dma_ops =
{
  .getcaps = audio_dma_getcaps,
  .configure = audio_dma_configure,
  .shutdown = audio_dma_shutdown,
  .start = audio_dma_start,
#ifndef CONFIG_AUDIO_EXCLUDE_STOP
  .stop = audio_dma_stop,
#endif
#ifndef CONFIG_AUDIO_EXCLUDE_PAUSE_RESUME
  .pause = audio_dma_pause,
  .resume = audio_dma_resume,
#endif
  .allocbuffer = audio_dma_allocbuffer,
  .freebuffer = audio_dma_freebuffer,
  .enqueuebuffer = audio_dma_enqueuebuffer,
  .ioctl = audio_dma_ioctl,
  .reserve = audio_dma_reserve,
  .release = audio_dma_release,
};

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

static int audio_dma_getcaps(struct audio_lowerhalf_s *dev, int type,
                             struct audio_caps_s *caps)
{
  struct audio_dma_s *audio_dma = (struct audio_dma_s *)dev;

  /* Validate the structure */

  DEBUGASSERT(caps && caps->ac_len >= sizeof(struct audio_caps_s));
  audinfo("type=%d ac_type=%d\n", type, caps->ac_type);

  /* Fill in the caller's structure based on requested info */

  caps->ac_format.hw  = 0;
  caps->ac_controls.w = 0;

  switch (caps->ac_type)
    {
      /* Caller is querying for the types of units we support */

      case AUDIO_TYPE_QUERY:

        /* Provide our overall capabilities.  The interfacing software
         * must then call us back for specific info for each capability.
         */

        caps->ac_channels = 2;       /* Stereo output */

        if (caps->ac_subtype == AUDIO_TYPE_QUERY)
          {
              /* We don't decode any formats!  Only something above us in
               * the audio stream can perform decoding on our behalf.
               */

              /* The types of audio units we implement */

              if (audio_dma->playback)
              {
                caps->ac_controls.b[0] = AUDIO_TYPE_OUTPUT;
              }
              else
              {
                caps->ac_controls.b[0] = AUDIO_TYPE_INPUT;
              }

              caps->ac_format.hw = 1 << (AUDIO_FMT_PCM - 1);
          }

        caps->ac_controls.b[0] = AUDIO_SUBFMT_END;
        break;

        /* Provide capabilities of our OUTPUT unit */

      case AUDIO_TYPE_OUTPUT:
      case AUDIO_TYPE_INPUT:

        caps->ac_channels = 2;

        if (caps->ac_subtype == AUDIO_TYPE_QUERY)
          {
            /* Report the Sample rates we support */

              caps->ac_controls.hw[0] = AUDIO_SAMP_RATE_DEF_ALL;
          }

        break;
   }

  /* Return the length of the audio_caps_s struct for validation of
   * proper Audio device type.
   */

  return caps->ac_len;
}

#ifdef CONFIG_AUDIO_MULTI_SESSION
static int audio_dma_configure(struct audio_lowerhalf_s *dev,
                               void *session,
                               const struct audio_caps_s *caps)
#else
static int audio_dma_configure(struct audio_lowerhalf_s *dev,
                               const struct audio_caps_s *caps)
#endif
{
  struct audio_dma_s *audio_dma = (struct audio_dma_s *)dev;
  struct dma_config_s cfg;
  int ret = -EINVAL;

  DEBUGASSERT(audio_dma && caps);
  audinfo("ac_type: %d\n", caps->ac_type);

  /* Process the configure operation */

  switch (caps->ac_type)
    {
      case AUDIO_TYPE_OUTPUT:
        if (audio_dma->playback)
          {
            memset(&cfg, 0, sizeof(struct dma_config_s));
            cfg.direction = DMA_MEM_TO_DEV;
            if (audio_dma->fifo_width)
              cfg.dst_width = audio_dma->fifo_width;
            else
              cfg.dst_width = caps->ac_controls.b[2] / 8;
            ret = DMA_CONFIG(audio_dma->chan, &cfg);
          }
        break;
      case AUDIO_TYPE_INPUT:
        if (!audio_dma->playback)
          {
            memset(&cfg, 0, sizeof(struct dma_config_s));
            cfg.direction = DMA_DEV_TO_MEM;
            if (audio_dma->fifo_width)
              cfg.src_width = audio_dma->fifo_width;
            else
              cfg.src_width = caps->ac_controls.b[2] / 8;
            ret = DMA_CONFIG(audio_dma->chan, &cfg);
          }
        break;
      default:
        ret = -ENOTTY;
        break;
    }

  return ret;
}

static int audio_dma_shutdown(struct audio_lowerhalf_s *dev)
{
  /* apps enqueued buffers, but doesn't start. stop here to
   * clear audio_dma->pendq.
   */

#ifndef CONFIG_AUDIO_EXCLUDE_STOP
#ifdef CONFIG_AUDIO_MULTI_SESSION
  audio_dma_stop(dev, NULL);
#else
  audio_dma_stop(dev);
#endif
#endif

  return OK;
}

#ifdef CONFIG_AUDIO_MULTI_SESSION
static int audio_dma_start(struct audio_lowerhalf_s *dev, void *session)
#else
static int audio_dma_start(struct audio_lowerhalf_s *dev)
#endif
{
  struct audio_dma_s *audio_dma = (struct audio_dma_s *)dev;

  return DMA_START_CYCLIC(audio_dma->chan, audio_dma_callback, audio_dma,
                          audio_dma->dst_addr, audio_dma->src_addr,
                          audio_dma->buffer_num * audio_dma->buffer_size,
                          audio_dma->buffer_size);
}

#ifndef CONFIG_AUDIO_EXCLUDE_STOP
#ifdef CONFIG_AUDIO_MULTI_SESSION
static int audio_dma_stop(struct audio_lowerhalf_s *dev, void *session)
#else
static int audio_dma_stop(struct audio_lowerhalf_s *dev)
#endif
{
  struct audio_dma_s *audio_dma = (struct audio_dma_s *)dev;
  struct ap_buffer_s *apb;

  DMA_STOP(audio_dma->chan);

  while (!dq_empty(&audio_dma->pendq))
    {
      apb = (struct ap_buffer_s *)dq_remfirst(&audio_dma->pendq);
#ifdef CONFIG_AUDIO_MULTI_SESSION
      audio_dma->dev.upper(audio_dma->dev.priv, AUDIO_CALLBACK_DEQUEUE,
                           apb, OK, NULL);
#else
      audio_dma->dev.upper(audio_dma->dev.priv, AUDIO_CALLBACK_DEQUEUE,
                           apb, OK);
#endif
    }

#ifdef CONFIG_AUDIO_MULTI_SESSION
  audio_dma->dev.upper(audio_dma->dev.priv, AUDIO_CALLBACK_COMPLETE,
                       NULL, OK, NULL);
#else
  audio_dma->dev.upper(audio_dma->dev.priv, AUDIO_CALLBACK_COMPLETE,
                       NULL, OK);
#endif
  audio_dma->xrun = false;
  return OK;
}
#endif

#ifndef CONFIG_AUDIO_EXCLUDE_PAUSE_RESUME
#ifdef CONFIG_AUDIO_MULTI_SESSION
static int audio_dma_pause(struct audio_lowerhalf_s *dev, void *session)
#else
static int audio_dma_pause(struct audio_lowerhalf_s *dev)
#endif
{
  struct audio_dma_s *audio_dma = (struct audio_dma_s *)dev;

  return DMA_PAUSE(audio_dma->chan);
}

#ifdef CONFIG_AUDIO_MULTI_SESSION
static int audio_dma_resume(struct audio_lowerhalf_s *dev, void *session)
#else
static int audio_dma_resume(struct audio_lowerhalf_s *dev)
#endif
{
  struct audio_dma_s *audio_dma = (struct audio_dma_s *)dev;

  if (dq_empty(&audio_dma->pendq))
    {
      return -EINVAL;
    }

  return DMA_RESUME(audio_dma->chan);
}
#endif

static int audio_dma_allocbuffer(struct audio_lowerhalf_s *dev,
                                 struct audio_buf_desc_s *bufdesc)
{
  struct audio_dma_s *audio_dma = (struct audio_dma_s *)dev;
  struct ap_buffer_s *apb;

  if (bufdesc->numbytes != audio_dma->buffer_size)
    {
      return -EINVAL;
    }

  if (audio_dma->alloc_index == audio_dma->buffer_num)
    {
      return -ENOMEM;
    }

  if (!audio_dma->alloc_addr)
    {
      audio_dma->alloc_addr = kumm_memalign(32,
                                            audio_dma->buffer_num *
                                            audio_dma->buffer_size);
      if (!audio_dma->alloc_addr)
        {
          return -ENOMEM;
        }

      if (audio_dma->playback)
        audio_dma->src_addr = up_addrenv_va_to_pa(audio_dma->alloc_addr);
      else
        audio_dma->dst_addr = up_addrenv_va_to_pa(audio_dma->alloc_addr);
    }

  apb = kumm_zalloc(sizeof(struct ap_buffer_s));
  *bufdesc->u.pbuffer = apb;

  /* Test if the allocation was successful or not */

  if (*bufdesc->u.pbuffer == NULL)
    {
      return -ENOMEM;
    }

  /* Populate the buffer contents */

  apb->i.channels = 2;
  apb->crefs      = 1;
  apb->nmaxbytes  = audio_dma->buffer_size;
  apb->samp = audio_dma->alloc_addr +
              audio_dma->alloc_index *
              audio_dma->buffer_size;
  audio_dma->alloc_index++;
  nxmutex_init(&apb->lock);

  return sizeof(struct audio_buf_desc_s);
}

static int audio_dma_freebuffer(struct audio_lowerhalf_s *dev,
                                struct audio_buf_desc_s *bufdesc)
{
  struct audio_dma_s *audio_dma = (struct audio_dma_s *)dev;
  struct ap_buffer_s *apb;

  apb = bufdesc->u.buffer;
  audio_dma->alloc_index--;
  nxmutex_destroy(&apb->lock);
  kumm_free(apb);

  if (audio_dma->alloc_index == 0)
    {
      kumm_free(audio_dma->alloc_addr);
      audio_dma->alloc_addr = NULL;
    }

  return sizeof(struct audio_buf_desc_s);
}

static int audio_dma_enqueuebuffer(struct audio_lowerhalf_s *dev,
                                   struct ap_buffer_s *apb)
{
  struct audio_dma_s *audio_dma = (struct audio_dma_s *)dev;
  irqstate_t flags;

  if (audio_dma->playback)
    up_clean_dcache((uintptr_t)apb->samp,
                    (uintptr_t)apb->samp + apb->nbytes);

  apb->flags |= AUDIO_APB_OUTPUT_ENQUEUED;

  flags = enter_critical_section();
  dq_addlast(&apb->dq_entry, &audio_dma->pendq);
  leave_critical_section(flags);

  if (audio_dma->xrun)
    {
      audio_dma->xrun = false;
      return audio_dma_resume(dev);
    }

  return OK;
}

static int audio_dma_ioctl(struct audio_lowerhalf_s *dev, int cmd,
                           unsigned long arg)
{
  struct audio_dma_s *audio_dma = (struct audio_dma_s *)dev;
  struct ap_buffer_info_s *bufinfo;

  switch (cmd)
    {
      /* Report our preferred buffer size and quantity */

      case AUDIOIOC_GETBUFFERINFO:
        audinfo("AUDIOIOC_GETBUFFERINFO:\n");
        bufinfo              = (struct ap_buffer_info_s *)arg;
        bufinfo->buffer_size = audio_dma->buffer_size;
        bufinfo->nbuffers    = audio_dma->buffer_num;

        return OK;

      case AUDIOIOC_SETBUFFERINFO:
        audinfo("AUDIOIOC_GETBUFFERINFO:\n");
        bufinfo                = (struct ap_buffer_info_s *)arg;
        audio_dma->buffer_size = bufinfo->buffer_size;
        audio_dma->buffer_num  = bufinfo->nbuffers;
        kumm_free(audio_dma->alloc_addr);
        audio_dma->alloc_addr = NULL;
        audio_dma->alloc_index = 0;

        return OK;
    }

  return -ENOTTY;
}

#ifdef CONFIG_AUDIO_MULTI_SESSION
static int audio_dma_reserve(struct audio_lowerhalf_s *dev, void **session)
#else
static int audio_dma_reserve(struct audio_lowerhalf_s *dev)
#endif
{
  return OK;
}

#ifdef CONFIG_AUDIO_MULTI_SESSION
static int audio_dma_release(struct audio_lowerhalf_s *dev, void *session)
#else
static int audio_dma_release(struct audio_lowerhalf_s *dev)
#endif
{
  return OK;
}

static void audio_dma_callback(struct dma_chan_s *chan,
                               void *arg, ssize_t len)
{
  struct audio_dma_s *audio_dma = (struct audio_dma_s *)arg;
  struct ap_buffer_s *apb;
  bool final = false;

  apb = (struct ap_buffer_s *)dq_remfirst(&audio_dma->pendq);
  if (!apb)
    {
      /* xrun */

      DMA_PAUSE(audio_dma->chan);
      audio_dma->xrun = true;
      return;
    }

  if (!audio_dma->playback)
    up_invalidate_dcache((uintptr_t)apb->samp,
                         (uintptr_t)apb->samp + apb->nbytes);

  if ((apb->flags & AUDIO_APB_FINAL) != 0)
    final = true;

#ifdef CONFIG_AUDIO_MULTI_SESSION
    audio_dma->dev.upper(audio_dma->dev.priv, AUDIO_CALLBACK_DEQUEUE,
                         apb, OK, NULL);
#else
    audio_dma->dev.upper(audio_dma->dev.priv, AUDIO_CALLBACK_DEQUEUE,
                         apb, OK);
#endif
  if (final)
    {
#ifdef CONFIG_AUDIO_MULTI_SESSION
      audio_dma_stop(&audio_dma->dev, NULL);
#else
      audio_dma_stop(&audio_dma->dev);
#endif
    }
  else if (dq_empty(&audio_dma->pendq))
    {
      /* xrun */

      DMA_PAUSE(audio_dma->chan);
      audio_dma->xrun = true;
    }
}

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

struct audio_lowerhalf_s *audio_dma_initialize(struct dma_dev_s *dma_dev,
                                               uint8_t chan_num,
                                               bool playback,
                                               uint8_t fifo_width,
                                               uintptr_t fifo_addr)
{
  struct audio_dma_s *audio_dma;

  if (!dma_dev)
    {
      return NULL;
    }

  audio_dma = kmm_zalloc(sizeof(struct audio_dma_s));
  if (!audio_dma)
    {
      return NULL;
    }

  audio_dma->chan = DMA_GET_CHAN(dma_dev, chan_num);
  if (!audio_dma->chan)
    {
      kmm_free(audio_dma);
      return NULL;
    }

  audio_dma->playback = playback;
  audio_dma->fifo_width = fifo_width;

  if (audio_dma->playback)
    audio_dma->dst_addr = up_addrenv_va_to_pa((void *)fifo_addr);
  else
    audio_dma->src_addr = up_addrenv_va_to_pa((void *)fifo_addr);

  audio_dma->buffer_size = CONFIG_AUDIO_BUFFER_NUMBYTES;
  audio_dma->buffer_num  = CONFIG_AUDIO_NUM_BUFFERS;
  dq_init(&audio_dma->pendq);

  audio_dma->dev.ops = &g_audio_dma_ops;
  return &audio_dma->dev;
}