/****************************************************************************
 * drivers/audio/audio_i2s.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 <assert.h>
#include <debug.h>

#include <nuttx/audio/audio.h>
#include <nuttx/audio/i2s.h>
#include <nuttx/kmalloc.h>

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

struct audio_i2s_s
{
  struct audio_lowerhalf_s dev;
  FAR struct i2s_dev_s *i2s;
  bool playback;
};

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

static int audio_i2s_getcaps(FAR struct audio_lowerhalf_s *dev, int type,
                             FAR struct audio_caps_s *caps);
static int audio_i2s_shutdown(FAR struct audio_lowerhalf_s *dev);
#ifdef CONFIG_AUDIO_MULTI_SESSION
static int audio_i2s_configure(FAR struct audio_lowerhalf_s *dev,
                               FAR void *session,
                               FAR const struct audio_caps_s *caps);
static int audio_i2s_start(FAR struct audio_lowerhalf_s *dev,
                           FAR void *session);
#ifndef CONFIG_AUDIO_EXCLUDE_STOP
static int audio_i2s_stop(FAR struct audio_lowerhalf_s *dev,
                          FAR void *session);
#endif
#ifndef CONFIG_AUDIO_EXCLUDE_PAUSE_RESUME
static int audio_i2s_pause(FAR struct audio_lowerhalf_s *dev,
                           FAR void *session);
static int audio_i2s_resume(FAR struct audio_lowerhalf_s *dev,
                            FAR void *session);
#endif
static int audio_i2s_reserve(FAR struct audio_lowerhalf_s *dev,
                             FAR void **session);
static int audio_i2s_release(FAR struct audio_lowerhalf_s *dev,
                             FAR void *session);
#else
static int audio_i2s_configure(FAR struct audio_lowerhalf_s *dev,
                               FAR const struct audio_caps_s *caps);
static int audio_i2s_start(FAR struct audio_lowerhalf_s *dev);
#ifndef CONFIG_AUDIO_EXCLUDE_STOP
static int audio_i2s_stop(FAR struct audio_lowerhalf_s *dev);
#endif
#ifndef CONFIG_AUDIO_EXCLUDE_PAUSE_RESUME
static int audio_i2s_pause(FAR struct audio_lowerhalf_s *dev);
static int audio_i2s_resume(FAR struct audio_lowerhalf_s *dev);
#endif
static int audio_i2s_reserve(FAR struct audio_lowerhalf_s *dev);
static int audio_i2s_release(FAR struct audio_lowerhalf_s *dev);
#endif
static int audio_i2s_allocbuffer(FAR struct audio_lowerhalf_s *dev,
                                 FAR struct audio_buf_desc_s *bufdesc);
static int audio_i2s_freebuffer(FAR struct audio_lowerhalf_s *dev,
                                FAR struct audio_buf_desc_s *bufdesc);
static int audio_i2s_enqueuebuffer(FAR struct audio_lowerhalf_s *dev,
                                   FAR struct ap_buffer_s *apb);
static int audio_i2s_ioctl(FAR struct audio_lowerhalf_s *dev, int cmd,
                           unsigned long arg);
static void audio_i2s_callback(FAR struct i2s_dev_s *dev,
                               FAR struct ap_buffer_s *apb, FAR void *arg,
                               int result);

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

static const struct audio_ops_s g_audio_i2s_ops =
{
  audio_i2s_getcaps,       /* getcaps        */
  audio_i2s_configure,     /* configure      */
  audio_i2s_shutdown,      /* shutdown       */
  audio_i2s_start,         /* start          */
#ifndef CONFIG_AUDIO_EXCLUDE_STOP
  audio_i2s_stop,          /* stop           */
#endif
#ifndef CONFIG_AUDIO_EXCLUDE_PAUSE_RESUME
  audio_i2s_pause,         /* pause          */
  audio_i2s_resume,        /* resume         */
#endif
  audio_i2s_allocbuffer,   /* allocbuffer    */
  audio_i2s_freebuffer,    /* freebuffer     */
  audio_i2s_enqueuebuffer, /* enqueue_buffer */
  NULL,                    /* cancel_buffer  */
  audio_i2s_ioctl,         /* ioctl          */
  NULL,                    /* read           */
  NULL,                    /* write          */
  audio_i2s_reserve,       /* reserve        */
  audio_i2s_release        /* release        */
};

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

static int audio_i2s_getcaps(FAR struct audio_lowerhalf_s *dev, int type,
                             FAR struct audio_caps_s *caps)
{
  FAR struct audio_i2s_s *audio_i2s = (FAR struct audio_i2s_s *)dev;
  FAR struct i2s_dev_s *i2s = audio_i2s->i2s;

  /* Validate the structure */

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

  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.
         */

        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_i2s->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);
              break;
          }

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

        /* Provide capabilities of our OUTPUT unit */

      case AUDIO_TYPE_OUTPUT:
      case AUDIO_TYPE_INPUT:

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

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

              caps->ac_channels = 2;

              break;
          }

      default:
        I2S_IOCTL(i2s, AUDIOIOC_GETCAPS, (unsigned long)caps);
        break;
    }

  return caps->ac_len;
}

#ifdef CONFIG_AUDIO_MULTI_SESSION
static int audio_i2s_configure(FAR struct audio_lowerhalf_s *dev,
                               FAR void *session,
                               FAR const struct audio_caps_s *caps)
#else
static int audio_i2s_configure(FAR struct audio_lowerhalf_s *dev,
                               FAR const struct audio_caps_s *caps)
#endif
{
  FAR struct audio_i2s_s *audio_i2s = (FAR struct audio_i2s_s *)dev;
  FAR struct i2s_dev_s *i2s;
  int samprate;
  int nchannels;
  int bpsamp;
  int ret = OK;

  DEBUGASSERT(audio_i2s != NULL && caps != NULL);
  i2s = audio_i2s->i2s;
  audinfo("ac_type: %d\n", caps->ac_type);

  /* Process the configure operation */

  switch (caps->ac_type)
    {
      case AUDIO_TYPE_OUTPUT:
      case AUDIO_TYPE_INPUT:

        /* Save the current stream configuration */

        samprate  = caps->ac_controls.hw[0] |
                    (caps->ac_controls.b[3] << 16);
        nchannels = caps->ac_channels;
        bpsamp    = caps->ac_controls.b[2];

        if (audio_i2s->playback)
          {
            I2S_TXCHANNELS(i2s, nchannels);
            I2S_TXDATAWIDTH(i2s, bpsamp);
            I2S_TXSAMPLERATE(i2s, samprate);
          }
        else
          {
            I2S_RXCHANNELS(i2s, nchannels);
            I2S_RXDATAWIDTH(i2s, bpsamp);
            I2S_RXSAMPLERATE(i2s, samprate);
          }
        break;

      default:
        ret = I2S_IOCTL(i2s, AUDIOIOC_CONFIGURE, (unsigned long)caps);
        break;
    }

  return ret;
}

static int audio_i2s_shutdown(FAR struct audio_lowerhalf_s *dev)
{
  FAR struct audio_i2s_s *audio_i2s = (FAR struct audio_i2s_s *)dev;
  FAR struct i2s_dev_s *i2s = audio_i2s->i2s;

  return I2S_IOCTL(i2s, AUDIOIOC_SHUTDOWN, audio_i2s->playback);
}

#ifdef CONFIG_AUDIO_MULTI_SESSION
static int audio_i2s_start(FAR struct audio_lowerhalf_s *dev,
                           FAR void *session)
#else
static int audio_i2s_start(FAR struct audio_lowerhalf_s *dev)
#endif
{
  FAR struct audio_i2s_s *audio_i2s = (FAR struct audio_i2s_s *)dev;
  FAR struct i2s_dev_s *i2s = audio_i2s->i2s;

  return I2S_IOCTL(i2s, AUDIOIOC_START, audio_i2s->playback);
}

#ifndef CONFIG_AUDIO_EXCLUDE_STOP
#ifdef CONFIG_AUDIO_MULTI_SESSION
static int audio_i2s_stop(FAR struct audio_lowerhalf_s *dev,
                          FAR void *session)
#else
static int audio_i2s_stop(FAR struct audio_lowerhalf_s *dev)
#endif
{
  FAR struct audio_i2s_s *audio_i2s = (FAR struct audio_i2s_s *)dev;
  FAR struct i2s_dev_s *i2s = audio_i2s->i2s;

  return I2S_IOCTL(i2s, AUDIOIOC_STOP, audio_i2s->playback);
}
#endif

#ifndef CONFIG_AUDIO_EXCLUDE_PAUSE_RESUME
#ifdef CONFIG_AUDIO_MULTI_SESSION
static int audio_i2s_pause(FAR struct audio_lowerhalf_s *dev,
                           FAR void *session)
#else
static int audio_i2s_pause(FAR struct audio_lowerhalf_s *dev)
#endif
{
  FAR struct audio_i2s_s *audio_i2s = (FAR struct audio_i2s_s *)dev;
  FAR struct i2s_dev_s *i2s = audio_i2s->i2s;

  return I2S_IOCTL(i2s, AUDIOIOC_PAUSE, audio_i2s->playback);
}

#ifdef CONFIG_AUDIO_MULTI_SESSION
static int audio_i2s_resume(FAR struct audio_lowerhalf_s *dev,
                            FAR void *session)
#else
static int audio_i2s_resume(FAR struct audio_lowerhalf_s *dev)
#endif
{
  FAR struct audio_i2s_s *audio_i2s = (FAR struct audio_i2s_s *)dev;
  FAR struct i2s_dev_s *i2s = audio_i2s->i2s;

  return I2S_IOCTL(i2s, AUDIOIOC_RESUME, audio_i2s->playback);
}
#endif

static int audio_i2s_allocbuffer(FAR struct audio_lowerhalf_s *dev,
                                 FAR struct audio_buf_desc_s *bufdesc)
{
  FAR struct audio_i2s_s *audio_i2s = (FAR struct audio_i2s_s *)dev;
  FAR struct i2s_dev_s *i2s = audio_i2s->i2s;

  return I2S_IOCTL(i2s, AUDIOIOC_ALLOCBUFFER, (unsigned long)bufdesc);
}

static int audio_i2s_freebuffer(FAR struct audio_lowerhalf_s *dev,
                                FAR struct audio_buf_desc_s *bufdesc)
{
  FAR struct audio_i2s_s *audio_i2s = (FAR struct audio_i2s_s *)dev;
  FAR struct i2s_dev_s *i2s = audio_i2s->i2s;

  return I2S_IOCTL(i2s, AUDIOIOC_FREEBUFFER, (unsigned long)bufdesc);
}

static int audio_i2s_enqueuebuffer(FAR struct audio_lowerhalf_s *dev,
                                   FAR struct ap_buffer_s *apb)
{
  FAR struct audio_i2s_s *audio_i2s = (FAR struct audio_i2s_s *)dev;
  FAR struct i2s_dev_s *i2s = audio_i2s->i2s;

  if (audio_i2s->playback)
    {
      return I2S_SEND(i2s, apb, audio_i2s_callback, audio_i2s, 0);
    }
  else
    {
      return I2S_RECEIVE(i2s, apb, audio_i2s_callback, audio_i2s, 0);
    }
}

static int audio_i2s_ioctl(FAR struct audio_lowerhalf_s *dev, int cmd,
                           unsigned long arg)
{
  FAR struct audio_i2s_s *audio_i2s = (FAR struct audio_i2s_s *)dev;
  FAR struct i2s_dev_s *i2s = audio_i2s->i2s;

  return I2S_IOCTL(i2s, cmd, arg);
}

#ifdef CONFIG_AUDIO_MULTI_SESSION
static int audio_i2s_reserve(FAR struct audio_lowerhalf_s *dev,
                             FAR void **session)
#else
static int audio_i2s_reserve(FAR struct audio_lowerhalf_s *dev)
#endif
{
#ifdef CONFIG_AUDIO_MULTI_SESSION
  *session = (FAR void *)audio_i2s->playback;
#endif
  return OK;
}

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

static void audio_i2s_callback(FAR struct i2s_dev_s *dev,
                               FAR struct ap_buffer_s *apb,
                               FAR void *arg, int result)
{
  FAR struct audio_i2s_s *audio_i2s = arg;
  bool final = false;

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

#ifdef CONFIG_AUDIO_MULTI_SESSION
  audio_i2s->dev.upper(audio_i2s->dev.priv, AUDIO_CALLBACK_DEQUEUE, apb,
                       OK, NULL);
#else
  audio_i2s->dev.upper(audio_i2s->dev.priv, AUDIO_CALLBACK_DEQUEUE, apb,
                       OK);
#endif
  if (final)
    {
#ifdef CONFIG_AUDIO_MULTI_SESSION
      audio_i2s->dev.upper(audio_i2s->dev.priv, AUDIO_CALLBACK_COMPLETE,
                           NULL, OK, NULL);
#else
      audio_i2s->dev.upper(audio_i2s->dev.priv, AUDIO_CALLBACK_COMPLETE,
                           NULL, OK);
#endif
    }
}

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

FAR struct audio_lowerhalf_s *audio_i2s_initialize(FAR struct i2s_dev_s *i2s,
                                                   bool playback)
{
  FAR struct audio_i2s_s *audio_i2s;

  if (i2s == NULL)
    {
      return NULL;
    }

  audio_i2s = kmm_zalloc(sizeof(struct audio_i2s_s));
  if (audio_i2s == NULL)
    {
      return NULL;
    }

  audio_i2s->playback = playback;
  audio_i2s->i2s = i2s;
  audio_i2s->dev.ops = &g_audio_i2s_ops;

  return &audio_i2s->dev;
}