/* GStreamer
 * Copyright (C) 2021 Seungha Yang <seungha@centricular.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif


#include "gstasioringbuffer.h"
#include <string.h>
#include "gstasioutils.h"
#include "gstasioobject.h"

GST_DEBUG_CATEGORY_STATIC (gst_asio_ring_buffer_debug);
#define GST_CAT_DEFAULT gst_asio_ring_buffer_debug

struct _GstAsioRingBuffer
{
  GstAudioRingBuffer parent;

  GstAsioDeviceClassType type;

  GstAsioObject *asio_object;
  guint *channel_indices;
  guint num_channels;
  ASIOBufferInfo **infos;

  guint64 callback_id;
  gboolean callback_installed;

  gboolean running;
  guint buffer_size;

  /* Used to detect sample gap */
  gboolean is_first;
  guint64 expected_sample_position;
  gboolean trace_sample_position;
};

enum
{
  PROP_0,
  PROP_DEVICE_INFO,
};

static void gst_asio_ring_buffer_dispose (GObject * object);

static gboolean gst_asio_ring_buffer_open_device (GstAudioRingBuffer * buf);
static gboolean gst_asio_ring_buffer_close_device (GstAudioRingBuffer * buf);
static gboolean gst_asio_ring_buffer_acquire (GstAudioRingBuffer * buf,
    GstAudioRingBufferSpec * spec);
static gboolean gst_asio_ring_buffer_release (GstAudioRingBuffer * buf);
static gboolean gst_asio_ring_buffer_start (GstAudioRingBuffer * buf);
static gboolean gst_asio_ring_buffer_stop (GstAudioRingBuffer * buf);
static guint gst_asio_ring_buffer_delay (GstAudioRingBuffer * buf);

static gboolean gst_asio_buffer_switch_cb (GstAsioObject * obj,
    glong index, ASIOBufferInfo * infos, guint num_infos,
    ASIOChannelInfo * input_channel_infos,
    ASIOChannelInfo * output_channel_infos,
    ASIOSampleRate sample_rate, glong buffer_size, gpointer user_data);

#define gst_asio_ring_buffer_parent_class parent_class
G_DEFINE_TYPE (GstAsioRingBuffer, gst_asio_ring_buffer,
    GST_TYPE_AUDIO_RING_BUFFER);

static void
gst_asio_ring_buffer_class_init (GstAsioRingBufferClass * klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  GstAudioRingBufferClass *ring_buffer_class =
      GST_AUDIO_RING_BUFFER_CLASS (klass);

  gobject_class->dispose = gst_asio_ring_buffer_dispose;

  ring_buffer_class->open_device =
      GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_open_device);
  ring_buffer_class->close_device =
      GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_close_device);
  ring_buffer_class->acquire = GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_acquire);
  ring_buffer_class->release = GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_release);
  ring_buffer_class->start = GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_start);
  ring_buffer_class->resume = GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_start);
  ring_buffer_class->stop = GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_stop);
  ring_buffer_class->delay = GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_delay);

  GST_DEBUG_CATEGORY_INIT (gst_asio_ring_buffer_debug,
      "asioringbuffer", 0, "asioringbuffer");
}

static void
gst_asio_ring_buffer_init (GstAsioRingBuffer * self)
{
}

static void
gst_asio_ring_buffer_dispose (GObject * object)
{
  GstAsioRingBuffer *self = GST_ASIO_RING_BUFFER (object);

  gst_clear_object (&self->asio_object);
  g_clear_pointer (&self->channel_indices, g_free);

  G_OBJECT_CLASS (parent_class)->dispose (object);
}

static gboolean
gst_asio_ring_buffer_open_device (GstAudioRingBuffer * buf)
{
  GstAsioRingBuffer *self = GST_ASIO_RING_BUFFER (buf);

  GST_DEBUG_OBJECT (self, "Open");

  return TRUE;
}

static gboolean
gst_asio_ring_buffer_close_device (GstAudioRingBuffer * buf)
{
  GstAsioRingBuffer *self = GST_ASIO_RING_BUFFER (buf);

  GST_DEBUG_OBJECT (self, "Close");

  return TRUE;
}

#define PACK_ASIO_64(v) ((v).lo | ((guint64)((v).hi) << 32))

static gboolean
gst_asio_buffer_switch_cb (GstAsioObject * obj, glong index,
    ASIOBufferInfo * infos, guint num_infos,
    ASIOChannelInfo * input_channel_infos,
    ASIOChannelInfo * output_channel_infos,
    ASIOSampleRate sample_rate, glong buffer_size,
    ASIOTime * time_info, gpointer user_data)
{
  GstAsioRingBuffer *self = (GstAsioRingBuffer *) user_data;
  GstAudioRingBuffer *ringbuffer = GST_AUDIO_RING_BUFFER_CAST (self);
  gint segment;
  guint8 *readptr;
  gint len;
  guint i, j;
  guint num_channels = 0;
  guint bps = GST_AUDIO_INFO_WIDTH (&ringbuffer->spec.info) >> 3;

  g_assert (index == 0 || index == 1);
  g_assert (num_infos >= self->num_channels);

  GST_TRACE_OBJECT (self, "Buffer Switch callback, index %ld", index);

  if (!gst_audio_ring_buffer_prepare_read (ringbuffer,
          &segment, &readptr, &len)) {
    GST_WARNING_OBJECT (self, "No segment available");
    return TRUE;
  }

  GST_TRACE_OBJECT (self, "segment %d, length %d", segment, len);

  /* Check missing frames */
  if (self->type == GST_ASIO_DEVICE_CLASS_CAPTURE) {
    if (self->is_first) {
      if (time_info) {
        self->expected_sample_position =
            PACK_ASIO_64 (time_info->timeInfo.samplePosition) + buffer_size;
        self->trace_sample_position = TRUE;
      } else {
        GST_WARNING_OBJECT (self, "ASIOTime is not available");
        self->trace_sample_position = FALSE;
      }

      self->is_first = FALSE;
    } else if (self->trace_sample_position) {
      if (!time_info) {
        GST_WARNING_OBJECT (self, "ASIOTime is not available");
        self->trace_sample_position = FALSE;
      } else {
        guint64 sample_position =
            PACK_ASIO_64 (time_info->timeInfo.samplePosition);
        if (self->expected_sample_position < sample_position) {
          guint64 gap_frames = sample_position - self->expected_sample_position;
          gint gap_size = gap_frames * bps;

          GST_WARNING_OBJECT (self, "%" G_GUINT64_FORMAT " frames are missing");

          while (gap_size >= len) {
            gst_audio_format_info_fill_silence (ringbuffer->spec.info.finfo,
                readptr, len);
            gst_audio_ring_buffer_advance (ringbuffer, 1);

            gst_audio_ring_buffer_prepare_read (ringbuffer,
                &segment, &readptr, &len);

            gap_size -= len;
          }
        }

        self->expected_sample_position = sample_position + buffer_size;
        GST_TRACE_OBJECT (self, "Sample Position %" G_GUINT64_FORMAT
            ", next: %" G_GUINT64_FORMAT, sample_position,
            self->expected_sample_position);
      }
    }
  }

  /* Given @infos might contain more channel data, pick channels what we want to
   * read */
  for (i = 0; i < num_infos; i++) {
    ASIOBufferInfo *info = &infos[i];

    if (self->type == GST_ASIO_DEVICE_CLASS_CAPTURE) {
      if (!info->isInput)
        continue;
    } else {
      if (info->isInput)
        continue;
    }

    for (j = 0; j < self->num_channels; j++) {
      if (self->channel_indices[j] != info->channelNum)
        continue;

      g_assert (num_channels < self->num_channels);
      self->infos[num_channels++] = info;
      break;
    }
  }

  if (num_channels < self->num_channels) {
    GST_ERROR_OBJECT (self, "Too small number of channel %d (expected %d)",
        num_channels, self->num_channels);
  } else {
    if (self->type == GST_ASIO_DEVICE_CLASS_CAPTURE ||
        self->type == GST_ASIO_DEVICE_CLASS_LOOPBACK_CAPTURE) {
      if (num_channels == 1) {
        memcpy (readptr, self->infos[0]->buffers[index], len);
      } else {
        guint gst_offset = 0, asio_offset = 0;

        /* Interleaves audio */
        while (gst_offset < len) {
          for (i = 0; i < num_channels; i++) {
            ASIOBufferInfo *info = self->infos[i];

            memcpy (readptr + gst_offset,
                ((guint8 *) info->buffers[index]) + asio_offset, bps);

            gst_offset += bps;
          }
          asio_offset += bps;
        }
      }
    } else {
      if (num_channels == 1) {
        memcpy (self->infos[0]->buffers[index], readptr, len);
      } else {
        guint gst_offset = 0, asio_offset = 0;

        /* Interleaves audio */
        while (gst_offset < len) {
          for (i = 0; i < num_channels; i++) {
            ASIOBufferInfo *info = self->infos[i];

            memcpy (((guint8 *) info->buffers[index]) + asio_offset,
                readptr + gst_offset, bps);

            gst_offset += bps;
          }
          asio_offset += bps;
        }
      }
    }
  }

  if (self->type == GST_ASIO_DEVICE_CLASS_RENDER)
    gst_audio_ring_buffer_clear (ringbuffer, segment);
  gst_audio_ring_buffer_advance (ringbuffer, 1);

  return TRUE;
}

static gboolean
gst_asio_ring_buffer_acquire (GstAudioRingBuffer * buf,
    GstAudioRingBufferSpec * spec)
{
  GstAsioRingBuffer *self = GST_ASIO_RING_BUFFER (buf);

  if (!self->asio_object) {
    GST_ERROR_OBJECT (self, "No configured ASIO object");
    return FALSE;
  }

  if (!self->channel_indices || self->num_channels == 0) {
    GST_ERROR_OBJECT (self, "No configured channels");
    return FALSE;
  }

  if (!gst_asio_object_set_sample_rate (self->asio_object,
          GST_AUDIO_INFO_RATE (&spec->info))) {
    GST_ERROR_OBJECT (self, "Failed to set sample rate");
    return FALSE;
  }

  spec->segsize = self->buffer_size *
      (GST_AUDIO_INFO_WIDTH (&spec->info) >> 3) *
      GST_AUDIO_INFO_CHANNELS (&spec->info);
  spec->segtotal = 2;

  buf->size = spec->segtotal * spec->segsize;
  buf->memory = (guint8 *) g_malloc (buf->size);
  gst_audio_format_info_fill_silence (buf->spec.info.finfo,
      buf->memory, buf->size);

  return TRUE;
}

static gboolean
gst_asio_ring_buffer_release (GstAudioRingBuffer * buf)
{
  GST_DEBUG_OBJECT (buf, "Release");

  g_clear_pointer (&buf->memory, g_free);

  return TRUE;
}

static gboolean
gst_asio_ring_buffer_start (GstAudioRingBuffer * buf)
{
  GstAsioRingBuffer *self = GST_ASIO_RING_BUFFER (buf);
  GstAsioObjectCallbacks callbacks;

  GST_DEBUG_OBJECT (buf, "Start");

  callbacks.buffer_switch = gst_asio_buffer_switch_cb;
  callbacks.user_data = self;

  self->is_first = TRUE;
  self->expected_sample_position = 0;

  if (!gst_asio_object_install_callback (self->asio_object, self->type,
          &callbacks, &self->callback_id)) {
    GST_ERROR_OBJECT (self, "Failed to install callback");
    return FALSE;
  }

  self->callback_installed = TRUE;

  if (!gst_asio_object_start (self->asio_object)) {
    GST_ERROR_OBJECT (self, "Failed to start");

    gst_asio_ring_buffer_stop (buf);

    return FALSE;
  }

  self->running = TRUE;

  return TRUE;
}

static gboolean
gst_asio_ring_buffer_stop (GstAudioRingBuffer * buf)
{
  GstAsioRingBuffer *self = GST_ASIO_RING_BUFFER (buf);

  GST_DEBUG_OBJECT (buf, "Stop");

  self->running = FALSE;

  if (!self->asio_object)
    return TRUE;

  if (self->callback_installed)
    gst_asio_object_uninstall_callback (self->asio_object, self->callback_id);

  self->callback_installed = FALSE;
  self->callback_id = 0;
  self->is_first = TRUE;
  self->expected_sample_position = 0;

  return TRUE;
}

static guint
gst_asio_ring_buffer_delay (GstAudioRingBuffer * buf)
{
  /* FIXME: impl. */

  return 0;
}

GstAsioRingBuffer *
gst_asio_ring_buffer_new (GstAsioObject * object, GstAsioDeviceClassType type,
    const gchar * name)
{
  GstAsioRingBuffer *self;

  g_return_val_if_fail (GST_IS_ASIO_OBJECT (object), nullptr);

  self =
      (GstAsioRingBuffer *) g_object_new (GST_TYPE_ASIO_RING_BUFFER,
      "name", name, nullptr);
  g_assert (self);

  self->type = type;
  self->asio_object = (GstAsioObject *) gst_object_ref (object);

  return self;
}

gboolean
gst_asio_ring_buffer_configure (GstAsioRingBuffer * buf,
    guint * channel_indices, guint num_channles, guint preferred_buffer_size)
{
  g_return_val_if_fail (GST_IS_ASIO_RING_BUFFER (buf), FALSE);
  g_return_val_if_fail (buf->asio_object != nullptr, FALSE);
  g_return_val_if_fail (num_channles > 0, FALSE);

  GST_DEBUG_OBJECT (buf, "Configure");

  buf->buffer_size = preferred_buffer_size;

  if (!gst_asio_object_create_buffers (buf->asio_object, buf->type,
          channel_indices, num_channles, &buf->buffer_size)) {
    GST_ERROR_OBJECT (buf, "Failed to configure");

    g_clear_pointer (&buf->channel_indices, g_free);
    buf->num_channels = 0;

    return FALSE;
  }

  GST_DEBUG_OBJECT (buf, "configured buffer size: %d", buf->buffer_size);

  g_free (buf->channel_indices);
  buf->channel_indices = g_new0 (guint, num_channles);

  for (guint i = 0; i < num_channles; i++)
    buf->channel_indices[i] = channel_indices[i];

  buf->num_channels = num_channles;

  g_clear_pointer (&buf->infos, g_free);
  buf->infos = g_new0 (ASIOBufferInfo *, num_channles);

  return TRUE;
}

GstCaps *
gst_asio_ring_buffer_get_caps (GstAsioRingBuffer * buf)
{
  g_return_val_if_fail (GST_IS_ASIO_RING_BUFFER (buf), nullptr);
  g_assert (buf->asio_object != nullptr);

  return gst_asio_object_get_caps (buf->asio_object,
      buf->type, buf->num_channels, buf->num_channels);
}