/*
 * Copyright (C) 2019, Collabora Ltd.
 *   Author: George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation
 * version 2.1 of the License.
 *
 * 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
 *
 */

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

#include "gstomxallocator.h"
#include <gst/allocators/gstdmabuf.h>

GST_DEBUG_CATEGORY_STATIC (gst_omx_allocator_debug_category);
#define GST_CAT_DEFAULT gst_omx_allocator_debug_category

#define DEBUG_INIT \
  GST_DEBUG_CATEGORY_INIT (gst_omx_allocator_debug_category, "omxallocator", 0, \
      "debug category for gst-omx allocator class");

G_DEFINE_TYPE_WITH_CODE (GstOMXAllocator, gst_omx_allocator, GST_TYPE_ALLOCATOR,
    DEBUG_INIT);

enum
{
  SIG_OMXBUF_RELEASED,
  SIG_FOREIGN_MEM_RELEASED,
  LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

/* Custom allocator for memory associated with OpenMAX buffers
 *
 * The main purpose of this allocator is to track memory that is associated
 * with OpenMAX buffers, so that we know when the buffers can be released
 * back to OpenMAX.
 *
 * This allocator looks and behaves more like a buffer pool. It allocates
 * the memory objects before starting and sets a miniobject dispose function
 * on them, which allows them to return when their last ref count is dropped.
 *
 * The type of memory that this allocator manages is GstOMXMemory. However, it
 * is possible to manage a different type of memory, in which case the
 * GstOMXMemory object is used only internally. There are two supported cases:
 * - Allocate memory from the dmabuf allocator
 * - Take memory that was allocated externally and manage it here
 *
 * In both cases, this allocator will replace the miniobject dispose function
 * of these memory objects, so if they were acquired from here, they will also
 * return here on their last unref.
 *
 * The caller initially needs to configure how many memory objects will be
 * managed here by calling configure(). After that it needs to call
 * set_active(TRUE) and finally allocate() for each memory. Allocation is done
 * like this to facilitate calling allocate() from the alloc() function of
 * the buffer pool for each OMX buffer on the port.
 *
 * After the allocator has been activated and all buffers have been allocated,
 * the acquire() method can be called to retrieve a memory object. acquire() can
 * be given an OMX buffer index or pointer to locate and return the memory
 * object that corresponds to this OMX buffer. If the buffer is already
 * acquired, this will result in a GST_FLOW_ERROR.
 *
 * When the last reference count is dropped on a memory that was acquired from
 * here, its dispose function will ref it again and allow it to be acquired
 * again. In addition, the omxbuf-released signal is fired to let the caller
 * know that it can return this OMX buffer to the port, as it is no longer
 * used outside this allocator.
 */

/******************/
/** GstOMXMemory **/
/******************/

#define GST_OMX_MEMORY_TYPE "openmax"

GQuark
gst_omx_memory_quark (void)
{
  static GQuark quark = 0;

  if (quark == 0)
    quark = g_quark_from_static_string ("GstOMXMemory");

  return quark;
}

static GstOMXMemory *
gst_omx_memory_new (GstOMXAllocator * allocator, GstOMXBuffer * omx_buf,
    GstMemoryFlags flags, GstMemory * parent, gssize offset, gssize size)
{
  GstOMXMemory *mem;
  gint align;
  gsize maxsize;

  /* GStreamer uses a bitmask for the alignment while
   * OMX uses the alignment itself. So we have to convert
   * here */
  align = allocator->port->port_def.nBufferAlignment;
  if (align > 0)
    align -= 1;
  if (((align + 1) & align) != 0) {
    GST_WARNING ("Invalid alignment that is not a power of two: %u",
        (guint) allocator->port->port_def.nBufferAlignment);
    align = 0;
  }

  maxsize = omx_buf->omx_buf->nAllocLen;

  if (size == -1) {
    size = maxsize - offset;
  }

  mem = g_slice_new0 (GstOMXMemory);
  gst_memory_init (GST_MEMORY_CAST (mem), flags, (GstAllocator *) allocator,
      parent, maxsize, align, offset, size);

  mem->buf = omx_buf;

  return mem;
}

static gpointer
gst_omx_memory_map (GstMemory * mem, gsize maxsize, GstMapFlags flags)
{
  GstOMXMemory *omem = (GstOMXMemory *) mem;

  /* if we are using foreign_mem, the GstOMXMemory should never appear
   * anywhere outside this allocator, therefore it should never be mapped */
  g_return_val_if_fail (!omem->foreign_mem, NULL);

  return omem->buf->omx_buf->pBuffer;
}

static void
gst_omx_memory_unmap (GstMemory * mem)
{
}

static GstMemory *
gst_omx_memory_share (GstMemory * mem, gssize offset, gssize size)
{
  GstOMXMemory *omem = (GstOMXMemory *) mem;
  GstOMXMemory *sub;
  GstMemory *parent;

  /* find the real parent */
  if ((parent = mem->parent) == NULL)
    parent = mem;

  if (size == -1)
    size = mem->size - offset;

  /* the shared memory is always readonly */
  sub = gst_omx_memory_new ((GstOMXAllocator *) mem->allocator, omem->buf,
      GST_MINI_OBJECT_FLAGS (parent) | GST_MINI_OBJECT_FLAG_LOCK_READONLY,
      parent, offset, size);

  return (GstMemory *) sub;
}

GstOMXBuffer *
gst_omx_memory_get_omx_buf (GstMemory * mem)
{
  GstOMXMemory *omx_mem;

  if (GST_IS_OMX_ALLOCATOR (mem->allocator))
    omx_mem = (GstOMXMemory *) mem;
  else
    omx_mem = gst_mini_object_get_qdata (GST_MINI_OBJECT (mem),
        GST_OMX_MEMORY_QUARK);

  if (!omx_mem)
    return NULL;

  return omx_mem->buf;
}

/*********************/
/** GstOMXAllocator **/
/*********************/

static void
gst_omx_allocator_init (GstOMXAllocator * allocator)
{
  GstAllocator *alloc = GST_ALLOCATOR_CAST (allocator);

  alloc->mem_type = GST_OMX_MEMORY_TYPE;

  alloc->mem_map = gst_omx_memory_map;
  alloc->mem_unmap = gst_omx_memory_unmap;
  alloc->mem_share = gst_omx_memory_share;
  /* default copy & is_span */

  GST_OBJECT_FLAG_SET (allocator, GST_ALLOCATOR_FLAG_CUSTOM_ALLOC);

  g_mutex_init (&allocator->lock);
  g_cond_init (&allocator->cond);
}

GstOMXAllocator *
gst_omx_allocator_new (GstOMXComponent * component, GstOMXPort * port)
{
  GstOMXAllocator *allocator;

  allocator = g_object_new (gst_omx_allocator_get_type (), NULL);
  allocator->component = gst_omx_component_ref (component);
  allocator->port = port;

  return allocator;
}

static void
gst_omx_allocator_finalize (GObject * object)
{
  GstOMXAllocator *allocator = GST_OMX_ALLOCATOR (object);

  gst_omx_component_unref (allocator->component);
  g_mutex_clear (&allocator->lock);
  g_cond_clear (&allocator->cond);

  G_OBJECT_CLASS (gst_omx_allocator_parent_class)->finalize (object);
}

gboolean
gst_omx_allocator_configure (GstOMXAllocator * allocator, guint count,
    GstOMXAllocatorForeignMemMode mode)
{
  /* check if already configured */
  if (allocator->n_memories > 0)
    return FALSE;

  allocator->n_memories = count;
  allocator->foreign_mode = mode;
  if (mode == GST_OMX_ALLOCATOR_FOREIGN_MEM_DMABUF)
    allocator->foreign_allocator = gst_dmabuf_allocator_new ();

  return TRUE;
}

/* must be protected with allocator->lock */
static void
gst_omx_allocator_dealloc (GstOMXAllocator * allocator)
{
  /* might be called more than once */
  if (!allocator->memories)
    return;

  /* return foreign memory back to whoever lended it to us.
   * the signal handler is expected to increase the ref count of foreign_mem */
  if (allocator->foreign_mode == GST_OMX_ALLOCATOR_FOREIGN_MEM_OTHER_POOL) {
    gint i;
    GstOMXMemory *m;

    for (i = 0; i < allocator->memories->len; i++) {
      m = g_ptr_array_index (allocator->memories, i);

      /* this should not happen, but let's not crash for this */
      if (!m->foreign_mem) {
        GST_WARNING_OBJECT (allocator, "no foreign_mem to release");
        continue;
      }

      /* restore the original dispose function */
      GST_MINI_OBJECT_CAST (m->foreign_mem)->dispose =
          (GstMiniObjectDisposeFunction) m->foreign_dispose;

      g_signal_emit (allocator, signals[SIG_FOREIGN_MEM_RELEASED], 0, i,
          m->foreign_mem);
    }
  }

  g_ptr_array_foreach (allocator->memories, (GFunc) gst_memory_unref, NULL);
  g_ptr_array_free (allocator->memories, TRUE);
  allocator->memories = NULL;
  allocator->n_memories = 0;
  allocator->foreign_mode = GST_OMX_ALLOCATOR_FOREIGN_MEM_NONE;
  if (allocator->foreign_allocator) {
    g_object_unref (allocator->foreign_allocator);
    allocator->foreign_allocator = NULL;
  }

  g_cond_broadcast (&allocator->cond);
}

gboolean
gst_omx_allocator_set_active (GstOMXAllocator * allocator, gboolean active)
{
  gboolean changed = FALSE;

  /* on activation, _configure() must be called first */
  g_return_val_if_fail (!active || allocator->n_memories > 0, FALSE);

  g_mutex_lock (&allocator->lock);

  if (allocator->active != active)
    changed = TRUE;

  if (changed) {
    if (active) {
      allocator->memories = g_ptr_array_sized_new (allocator->n_memories);
      g_ptr_array_set_size (allocator->memories, allocator->n_memories);
    } else {
      if (g_atomic_int_get (&allocator->n_outstanding) == 0)
        gst_omx_allocator_dealloc (allocator);
    }
  }

  allocator->active = active;
  g_mutex_unlock (&allocator->lock);

  return changed;
}

void
gst_omx_allocator_wait_inactive (GstOMXAllocator * allocator)
{
  g_mutex_lock (&allocator->lock);
  while (allocator->memories)
    g_cond_wait (&allocator->cond, &allocator->lock);
  g_mutex_unlock (&allocator->lock);
}

static inline void
dec_outstanding (GstOMXAllocator * allocator)
{
  if (g_atomic_int_dec_and_test (&allocator->n_outstanding)) {
    /* keep a ref to the allocator because _dealloc() will free
     * all the memories and the memories might be the only thing holding
     * a reference to the allocator; we need to keep it alive until the
     * end of this function call */
    g_object_ref (allocator);

    /* take the lock so that _set_active() is not run concurrently */
    g_mutex_lock (&allocator->lock);

    /* now that we have the lock, check if we have been de-activated with
     * outstanding buffers */
    if (!allocator->active)
      gst_omx_allocator_dealloc (allocator);

    g_mutex_unlock (&allocator->lock);
    g_object_unref (allocator);
  }
}

GstFlowReturn
gst_omx_allocator_acquire (GstOMXAllocator * allocator, GstMemory ** memory,
    gint index, GstOMXBuffer * omx_buf)
{
  GstFlowReturn ret = GST_FLOW_OK;
  GstOMXMemory *omx_mem = NULL;

  /* ensure memories are not going to disappear concurrently */
  g_atomic_int_inc (&allocator->n_outstanding);

  if (!allocator->active) {
    ret = GST_FLOW_FLUSHING;
    goto beach;
  }

  if (index >= 0 && index < allocator->n_memories)
    omx_mem = g_ptr_array_index (allocator->memories, index);
  else if (omx_buf) {
    for (index = 0; index < allocator->n_memories; index++) {
      omx_mem = g_ptr_array_index (allocator->memories, index);
      if (omx_mem->buf == omx_buf)
        break;
    }
  }

  if (G_UNLIKELY (!omx_mem || index >= allocator->n_memories)) {
    GST_ERROR_OBJECT (allocator, "Failed to find OMX memory");
    ret = GST_FLOW_ERROR;
    goto beach;
  }

  if (G_UNLIKELY (omx_mem->buf->used)) {
    GST_ERROR_OBJECT (allocator,
        "Trying to acquire a buffer that is being used by the OMX port");
    ret = GST_FLOW_ERROR;
    goto beach;
  }

  omx_mem->acquired = TRUE;

  if (omx_mem->foreign_mem)
    *memory = omx_mem->foreign_mem;
  else
    *memory = GST_MEMORY_CAST (omx_mem);

beach:
  if (ret != GST_FLOW_OK)
    dec_outstanding (allocator);
  return ret;
}

/* installed as the GstMiniObject::dispose function of the acquired GstMemory */
static gboolean
gst_omx_allocator_memory_dispose (GstMemory * mem)
{
  GstOMXMemory *omx_mem;
  GstOMXAllocator *allocator;

  /* memory may be from our allocator, but
   * may as well be from the dmabuf allocator */
  if (GST_IS_OMX_ALLOCATOR (mem->allocator))
    omx_mem = (GstOMXMemory *) mem;
  else
    omx_mem = gst_mini_object_get_qdata (GST_MINI_OBJECT (mem),
        GST_OMX_MEMORY_QUARK);

  if (omx_mem->acquired) {
    /* keep the memory alive */
    gst_memory_ref (mem);

    omx_mem->acquired = FALSE;

    allocator = GST_OMX_ALLOCATOR (GST_MEMORY_CAST (omx_mem)->allocator);

    /* inform the upper layer that we are no longer using this GstOMXBuffer */
    g_signal_emit (allocator, signals[SIG_OMXBUF_RELEASED], 0, omx_mem->buf);

    dec_outstanding (allocator);

    /* be careful here, both the memory and the allocator
     * may have been free'd as part of the call to dec_outstanding() */

    return FALSE;
  }

  /* if the foreign memory had a dispose function, let that one decide
   * the fate of this memory. We are no longer going to be using it here */
  if (omx_mem->foreign_dispose)
    return omx_mem->foreign_dispose (GST_MINI_OBJECT_CAST (mem));

  return TRUE;
}

static inline void
install_mem_dispose (GstOMXMemory * mem)
{
  GstMemory *managed_mem = (GstMemory *) mem;

  if (mem->foreign_mem) {
    managed_mem = mem->foreign_mem;
    mem->foreign_dispose = GST_MINI_OBJECT_CAST (managed_mem)->dispose;
  }

  GST_MINI_OBJECT_CAST (managed_mem)->dispose =
      (GstMiniObjectDisposeFunction) gst_omx_allocator_memory_dispose;
}

/* the returned memory is transfer:none, ref still belongs to the allocator */
GstMemory *
gst_omx_allocator_allocate (GstOMXAllocator * allocator, gint index,
    GstMemory * foreign_mem)
{
  GstOMXMemory *mem;
  GstOMXBuffer *omx_buf;

  g_return_val_if_fail (allocator->port->buffers, NULL);
  g_return_val_if_fail (allocator->memories, NULL);
  g_return_val_if_fail (index >= 0 && index < allocator->n_memories, NULL);
  g_return_val_if_fail ((foreign_mem == NULL &&
          allocator->foreign_mode != GST_OMX_ALLOCATOR_FOREIGN_MEM_OTHER_POOL)
      || (foreign_mem != NULL
          && allocator->foreign_mode ==
          GST_OMX_ALLOCATOR_FOREIGN_MEM_OTHER_POOL), NULL);

  omx_buf = g_ptr_array_index (allocator->port->buffers, index);
  g_return_val_if_fail (omx_buf != NULL, NULL);

  mem = gst_omx_memory_new (allocator, omx_buf, 0, NULL, 0, -1);

  switch (allocator->foreign_mode) {
    case GST_OMX_ALLOCATOR_FOREIGN_MEM_NONE:
      install_mem_dispose (mem);
      break;
    case GST_OMX_ALLOCATOR_FOREIGN_MEM_DMABUF:
    {
      gint fd = GPOINTER_TO_INT (omx_buf->omx_buf->pBuffer);
      mem->foreign_mem =
          gst_dmabuf_allocator_alloc_with_flags (allocator->foreign_allocator, fd,
              omx_buf->omx_buf->nAllocLen, GST_FD_MEMORY_FLAG_DONT_CLOSE);
      gst_mini_object_set_qdata (GST_MINI_OBJECT (mem->foreign_mem),
          GST_OMX_MEMORY_QUARK, mem, NULL);
      install_mem_dispose (mem);
      break;
    }
    case GST_OMX_ALLOCATOR_FOREIGN_MEM_OTHER_POOL:
      mem->foreign_mem = foreign_mem;
      gst_mini_object_set_qdata (GST_MINI_OBJECT (mem->foreign_mem),
          GST_OMX_MEMORY_QUARK, mem, NULL);
      install_mem_dispose (mem);
      break;
    default:
      g_assert_not_reached ();
      break;
  }

  g_ptr_array_index (allocator->memories, index) = mem;
  return mem->foreign_mem ? mem->foreign_mem : (GstMemory *) mem;
}

static void
gst_omx_allocator_free (GstAllocator * allocator, GstMemory * mem)
{
  GstOMXMemory *omem = (GstOMXMemory *) mem;

  g_warn_if_fail (!omem->acquired);

  if (omem->foreign_mem)
    gst_memory_unref (omem->foreign_mem);

  g_slice_free (GstOMXMemory, omem);
}

static void
gst_omx_allocator_class_init (GstOMXAllocatorClass * klass)
{
  GObjectClass *object_class;
  GstAllocatorClass *allocator_class;

  object_class = (GObjectClass *) klass;
  allocator_class = (GstAllocatorClass *) klass;

  object_class->finalize = gst_omx_allocator_finalize;
  allocator_class->alloc = NULL;
  allocator_class->free = gst_omx_allocator_free;

  signals[SIG_OMXBUF_RELEASED] = g_signal_new ("omxbuf-released",
      G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0,
      NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_POINTER);

  signals[SIG_FOREIGN_MEM_RELEASED] = g_signal_new ("foreign-mem-released",
      G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0,
      NULL, NULL, NULL, G_TYPE_NONE, 2, G_TYPE_INT, G_TYPE_POINTER);
}
