/* GStreamer
 * Copyright (C) 2020 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.
 */

#include "gstmfvideobuffer.h"
#include <string.h>

GST_DEBUG_CATEGORY_EXTERN (gst_mf_video_buffer_debug);
#define GST_CAT_DEFAULT gst_mf_video_buffer_debug

/* *INDENT-OFF* */
IGstMFVideoBuffer::IGstMFVideoBuffer ()
  : ref_count_ (1)
  , current_len_ (0)
  , contiguous_len_ (0)
  , data_ (nullptr)
  , contiguous_data_ (nullptr)
  , info_ (nullptr)
  , contiguous_info_ (nullptr)
  , locked_ (false)
  , wrapped_ (false)
  , user_data_ (nullptr)
  , notify_ (nullptr)
{
}

IGstMFVideoBuffer::~IGstMFVideoBuffer ()
{
  if (info_)
    gst_video_info_free (info_);
  if (contiguous_info_)
    gst_video_info_free (contiguous_info_);

  g_free (contiguous_data_);

  if (!wrapped_)
    g_free (data_);
}

HRESULT
IGstMFVideoBuffer::CreateInstance (GstVideoInfo * info,
    IMFMediaBuffer ** buffer)
{
  HRESULT hr = S_OK;
  IGstMFVideoBuffer * self;

  if (!info || !buffer)
    return E_INVALIDARG;

  self = new IGstMFVideoBuffer ();

  if (!self)
    return E_OUTOFMEMORY;

  hr = self->Initialize (info);

  if (SUCCEEDED (hr))
    hr = self->QueryInterface (IID_PPV_ARGS (buffer));

  self->Release ();

  return hr;
}

HRESULT
IGstMFVideoBuffer::CreateInstanceWrapped (GstVideoInfo * info,
    BYTE * data, DWORD length, IMFMediaBuffer ** buffer)
{
  HRESULT hr = S_OK;
  IGstMFVideoBuffer * self;

  if (!info || !data || length == 0 || !buffer)
    return E_INVALIDARG;

  self = new IGstMFVideoBuffer ();

  if (!self)
    return E_OUTOFMEMORY;

  hr = self->InitializeWrapped (info, data, length);

  if (SUCCEEDED (hr))
    hr = self->QueryInterface (IID_PPV_ARGS (buffer));

  self->Release ();

  return hr;
}

HRESULT
IGstMFVideoBuffer::Initialize (GstVideoInfo * info)
{
  if (!info)
    return E_INVALIDARG;

  info_ = gst_video_info_copy (info);
  contiguous_info_ = gst_video_info_new ();

  /* check if padding is required */
  gst_video_info_set_format (contiguous_info_, GST_VIDEO_INFO_FORMAT (info_),
      GST_VIDEO_INFO_WIDTH (info_), GST_VIDEO_INFO_HEIGHT (info_));

  contiguous_ = GST_VIDEO_INFO_SIZE (info_) ==
      GST_VIDEO_INFO_SIZE (contiguous_info_);

  contiguous_len_ = GST_VIDEO_INFO_SIZE (contiguous_info_);
  /* NOTE: {Set,Get}CurrentLength will not be applied for
   * IMF2DBuffer interface */

  current_len_ = contiguous_len_;

  data_ = (BYTE *) g_malloc0 (GST_VIDEO_INFO_SIZE (info_));

  if (!data_)
    return E_OUTOFMEMORY;

  return S_OK;
}

HRESULT
IGstMFVideoBuffer::InitializeWrapped (GstVideoInfo * info, BYTE * data,
    DWORD length)
{
  if (!info || !data || length == 0)
    return E_INVALIDARG;

  if (length < GST_VIDEO_INFO_SIZE (info))
    return E_INVALIDARG;

  info_ = gst_video_info_copy (info);
  contiguous_info_ = gst_video_info_new ();

  /* check if padding is required */
  gst_video_info_set_format (contiguous_info_, GST_VIDEO_INFO_FORMAT (info_),
      GST_VIDEO_INFO_WIDTH (info_), GST_VIDEO_INFO_HEIGHT (info_));

  contiguous_ = GST_VIDEO_INFO_SIZE (info_) ==
      GST_VIDEO_INFO_SIZE (contiguous_info_);

  contiguous_len_ = GST_VIDEO_INFO_SIZE (contiguous_info_);

  current_len_ = contiguous_len_;

  data_ = data;
  wrapped_ = true;

  return S_OK;
}

HRESULT
IGstMFVideoBuffer::SetUserData (gpointer user_data, GDestroyNotify notify)
{
  GDestroyNotify old_notify = notify_;
  gpointer old_user_data = user_data_;

  if (old_notify)
    old_notify (old_user_data);

  user_data_ = user_data;
  notify_ = notify;

  return S_OK;
}

HRESULT
IGstMFVideoBuffer::GetUserData (gpointer * user_data)
{
  if (!user_data)
    return E_INVALIDARG;

  *user_data = user_data_;

  return S_OK;
}

/* IUnknown interface */
STDMETHODIMP_ (ULONG)
IGstMFVideoBuffer::AddRef (void)
{
  GST_TRACE ("%p, %d", this, (guint) ref_count_);
  return InterlockedIncrement (&ref_count_);
}

STDMETHODIMP_ (ULONG)
IGstMFVideoBuffer::Release (void)
{
  ULONG ref_count;

  GST_TRACE ("%p, %d", this, (guint) ref_count_);
  ref_count = InterlockedDecrement (&ref_count_);

  if (ref_count == 0) {
    GDestroyNotify old_notify = notify_;
    gpointer old_user_data = user_data_;

    GST_TRACE ("Delete instance %p", this);
    delete this;

    if (old_notify)
      old_notify (old_user_data);
  }

  return ref_count;
}

STDMETHODIMP
IGstMFVideoBuffer::QueryInterface (REFIID riid, void ** object)
{
  if (!object)
    return E_POINTER;

  if (riid == IID_IUnknown) {
    GST_TRACE ("query IUnknown interface %p", this);
    *object = static_cast<IUnknown *> (static_cast<IMFMediaBuffer *> (this));
  } else if (riid == __uuidof(IMFMediaBuffer)) {
    GST_TRACE ("query IMFMediaBuffer interface %p", this);
    *object = static_cast<IMFMediaBuffer *> (this);
  } else if (riid == __uuidof(IMF2DBuffer)) {
    GST_TRACE ("query IMF2DBuffer interface %p", this);
    *object = static_cast<IMF2DBuffer *> (this);
  } else if (riid == IID_IGstMFVideoBuffer) {
    GST_TRACE ("query IGstMFVideoBuffer interface %p", this);
    *object = this;
  } else {
    *object = nullptr;
    return E_NOINTERFACE;
  }

  AddRef();

  return S_OK;
}

/* IMFMediaBuffer interface */
STDMETHODIMP
IGstMFVideoBuffer::Lock (BYTE ** buffer, DWORD * max_length,
    DWORD * current_length)
{
  std::lock_guard<std::mutex> lock(lock_);

  GST_TRACE ("%p", this);

  if (locked_) {
    GST_LOG ("%p, Already locked", this);
    return S_OK;
  }

  locked_ = true;

  if (contiguous_) {
    *buffer = data_;
    goto done;
  }

  /* IMFMediaBuffer::Lock method should return contiguous memory */
  if (!contiguous_data_)
    contiguous_data_ = (BYTE *) g_malloc0 (contiguous_len_);

  ContiguousCopyToUnlocked (contiguous_data_, contiguous_len_);
  *buffer = contiguous_data_;

done:
  if (max_length)
    *max_length = contiguous_len_;
  if (current_length)
    *current_length = current_len_;

  return S_OK;
}

STDMETHODIMP
IGstMFVideoBuffer::Unlock (void)
{
  std::lock_guard<std::mutex> lock(lock_);

  GST_TRACE ("%p", this);

  if (!locked_) {
    GST_LOG ("%p, No previous Lock call", this);
    return S_OK;
  }

  locked_ = false;

  if (contiguous_) {
    GST_TRACE ("%p, Have configured contiguous data", this);
    return S_OK;
  }

  /* copy back to original data */
  ContiguousCopyFromUnlocked (contiguous_data_, contiguous_len_);

  return S_OK;
}

STDMETHODIMP
IGstMFVideoBuffer::GetCurrentLength (DWORD * length)
{
  std::lock_guard<std::mutex> lock(lock_);

  *length = current_len_;

  GST_TRACE ("%p, %d", this, (guint) current_len_);

  return S_OK;
}

STDMETHODIMP
IGstMFVideoBuffer::SetCurrentLength (DWORD length)
{
  std::lock_guard<std::mutex> lock(lock_);

  GST_TRACE ("%p %d", this, (guint) length);

  if (length > contiguous_len_) {
    GST_LOG ("%p, Requested length %d is larger than contiguous_len %d",
        this, (guint) length, (guint) contiguous_len_);
    return E_INVALIDARG;
  }

  current_len_ = length;

  return S_OK;
}

STDMETHODIMP
IGstMFVideoBuffer::GetMaxLength (DWORD * length)
{
  std::lock_guard<std::mutex> lock(lock_);

  GST_TRACE ("%p", this);

  *length = contiguous_len_;

  return S_OK;
}

/* IMF2DBuffer */
STDMETHODIMP
IGstMFVideoBuffer::Lock2D (BYTE ** buffer, LONG * pitch)
{
  std::lock_guard<std::mutex> lock(lock_);

  GST_TRACE ("%p", this);

  if (locked_) {
    GST_LOG ("%p, Already locked", this);
    return MF_E_INVALIDREQUEST;
  }

  locked_ = true;

  *buffer = data_;
  *pitch = GST_VIDEO_INFO_PLANE_STRIDE (info_, 0);

  return S_OK;
}

STDMETHODIMP
IGstMFVideoBuffer::Unlock2D (void)
{
  std::lock_guard<std::mutex> lock(lock_);

  GST_TRACE ("%p", this);

  if (!locked_) {
    GST_LOG ("%p, No previous Lock2D call", this);
    return S_OK;
  }

  locked_ = false;

  return S_OK;
}

STDMETHODIMP
IGstMFVideoBuffer::GetScanline0AndPitch (BYTE ** buffer, LONG * pitch)
{
  std::lock_guard<std::mutex> lock(lock_);

  GST_TRACE ("%p", this);

  /* Lock2D must be called before */
  if (!locked_) {
    GST_LOG ("%p, Invalid call, Lock2D must be called before", this);
    return ERROR_INVALID_FUNCTION;
  }

  *buffer = data_;
  *pitch = GST_VIDEO_INFO_PLANE_STRIDE (info_, 0);

  return S_OK;
}

STDMETHODIMP
IGstMFVideoBuffer::IsContiguousFormat (BOOL * contiguous)
{
  std::lock_guard<std::mutex> lock(lock_);

  GST_TRACE ("%p", this);

  *contiguous = contiguous_;

  return S_OK;
}

STDMETHODIMP
IGstMFVideoBuffer::GetContiguousLength (DWORD * length)
{
  std::lock_guard<std::mutex> lock(lock_);

  GST_TRACE ("%p", this);

  *length = contiguous_len_;

  return S_OK;
}

STDMETHODIMP
IGstMFVideoBuffer::ContiguousCopyTo (BYTE * dest_buffer,
    DWORD dest_buffer_length)
{
  std::lock_guard<std::mutex> lock(lock_);

  return ContiguousCopyToUnlocked (dest_buffer, dest_buffer_length);
}

STDMETHODIMP
IGstMFVideoBuffer::ContiguousCopyFrom (const BYTE * src_buffer,
    DWORD src_buffer_length)
{
  std::lock_guard<std::mutex> lock(lock_);

  GST_TRACE ("%p", this);

  return ContiguousCopyFromUnlocked (src_buffer, src_buffer_length);
}

HRESULT
IGstMFVideoBuffer::ContiguousCopyToUnlocked (BYTE * dest_buffer,
    DWORD dest_buffer_length)
{
  GST_TRACE ("%p", this);

  if (!dest_buffer || dest_buffer_length < contiguous_len_)
    return E_INVALIDARG;

  if (contiguous_) {
    memcpy (dest_buffer, data_, current_len_);
    return S_OK;
  }

  for (guint i = 0; i < GST_VIDEO_INFO_N_PLANES (info_); i++) {
    BYTE *src, *dst;
    guint src_stride, dst_stride;
    guint width, height;

    src = data_ + GST_VIDEO_INFO_PLANE_OFFSET (info_, i);
    dst = dest_buffer + GST_VIDEO_INFO_PLANE_OFFSET (contiguous_info_, i);

    src_stride = GST_VIDEO_INFO_PLANE_STRIDE (info_, i);
    dst_stride = GST_VIDEO_INFO_PLANE_STRIDE (contiguous_info_, i);

    width = GST_VIDEO_INFO_COMP_WIDTH (info_, i)
        * GST_VIDEO_INFO_COMP_PSTRIDE (info_, i);
    height = GST_VIDEO_INFO_COMP_HEIGHT (info_, i);

    for (guint j  = 0; j < height; j++) {
      memcpy (dst, src, width);
      src += src_stride;
      dst += dst_stride;
    }
  }

  return S_OK;
}

HRESULT
IGstMFVideoBuffer::ContiguousCopyFromUnlocked (const BYTE * src_buffer,
    DWORD src_buffer_length)
{
  gint offset;

  GST_TRACE ("%p", this);

  if (!src_buffer)
    return E_INVALIDARG;

  /* Nothing to copy */
  if (src_buffer_length == 0)
    return S_OK;

  if (contiguous_) {
    memcpy (data_, src_buffer, src_buffer_length);
    return S_OK;
  }

  for (guint i = 0; i < GST_VIDEO_INFO_N_PLANES (info_); i++) {
    BYTE *dst;
    guint src_stride, dst_stride;
    guint width, height;

    offset = GST_VIDEO_INFO_PLANE_OFFSET (contiguous_info_, i);

    dst = data_ + GST_VIDEO_INFO_PLANE_OFFSET (info_, i);

    src_stride = GST_VIDEO_INFO_PLANE_STRIDE (contiguous_info_, i);
    dst_stride = GST_VIDEO_INFO_PLANE_STRIDE (info_, i);

    width = GST_VIDEO_INFO_COMP_WIDTH (info_, i)
        * GST_VIDEO_INFO_COMP_PSTRIDE (info_, i);
    height = GST_VIDEO_INFO_COMP_HEIGHT (info_, i);

    for (guint j  = 0; j < height; j++) {
      gint to_copy = 0;

      if (offset + width < src_buffer_length)
        to_copy = width;
      else
        to_copy = (gint) src_buffer_length - offset;

      if (to_copy <= 0)
        return S_OK;

      memcpy (dst, src_buffer + offset, to_copy);

      offset += src_stride;
      dst += dst_stride;
    }
  }

  return S_OK;
}

/* *INDENT-ON* */
