/* GStreamer
 * Copyright (C) 2020 Igalia, S.L.
 *     Author: Víctor Jáquez <vjaquez@igalia.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.
 */
/**
 * plugin-va:
 *
 * Since: 1.18
 */

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

#include "gstvaav1dec.h"
#include "gstvaav1enc.h"
#include "gstvacaps.h"
#include "gstvacompositor.h"
#include "gstvadeinterlace.h"
#include "gstvadevice.h"
#include "gstvafilter.h"
#include "gstvah264dec.h"
#include "gstvah264enc.h"
#include "gstvah265dec.h"
#include "gstvah265enc.h"
#include "gstvah266dec.h"
#include "gstvajpegdec.h"
#include "gstvajpegenc.h"
#include "gstvampeg2dec.h"
#include "gstvaprofile.h"
#include "gstvavp8dec.h"
#include "gstvavp8enc.h"
#include "gstvavp9dec.h"
#include "gstvavp9enc.h"
#include "gstvavpp.h"

#define GST_CAT_DEFAULT gstva_debug
GST_DEBUG_CATEGORY (gstva_debug);

#ifdef G_OS_WIN32
/* Windows support is still experimental */
#define GST_VA_RANK_PRIMARY GST_RANK_NONE
#else
#define GST_VA_RANK_PRIMARY (GST_RANK_PRIMARY + 1)
#endif

static void
plugin_add_dependencies (GstPlugin * plugin)
{
#ifndef G_OS_WIN32
  const gchar *env_vars[] = { "LIBVA_DRIVER_NAME", "GST_VA_ALL_DRIVERS", NULL };
  const gchar *kernel_paths[] = { "/dev/dri", NULL };
  const gchar *kernel_names[] = { "renderD", NULL };

  /* features get updated upon changes in /dev/dri/renderD* */
  gst_plugin_add_dependency (plugin, NULL, kernel_paths, kernel_names,
      GST_PLUGIN_DEPENDENCY_FLAG_FILE_NAME_IS_PREFIX);

  /* features get updated upon changes on LIBVA_DRIVER_NAME and
   * GST_VA_ALL_DRIVERS envvar */
  gst_plugin_add_dependency (plugin, env_vars, NULL, NULL,
      GST_PLUGIN_DEPENDENCY_FLAG_NONE);

  /* features get updated upon changes in default VA drivers
   * directory */
  gst_plugin_add_dependency_simple (plugin, "LIBVA_DRIVERS_PATH",
      LIBVA_DRIVERS_PATH, "_drv_video.so",
      GST_PLUGIN_DEPENDENCY_FLAG_FILE_NAME_IS_SUFFIX |
      GST_PLUGIN_DEPENDENCY_FLAG_PATHS_ARE_DEFAULT_ONLY);
#endif
}

static void
plugin_register_decoders (GstPlugin * plugin, GstVaDevice * device,
    GHashTable * decoders)
{
  GHashTableIter iter;
  gpointer key, value;

  g_hash_table_iter_init (&iter, decoders);
  while (g_hash_table_iter_next (&iter, &key, &value)) {
    guint32 codec = *((gint64 *) key);
    GArray *profiles = (GArray *) value;
    GstCaps *sinkcaps = NULL, *srccaps = NULL;

    if (!profiles || profiles->len == 0)
      continue;

    if (!gst_va_caps_from_profiles (device->display, profiles, VAEntrypointVLD,
            &sinkcaps, &srccaps))
      continue;

    GST_LOG ("%d decoder codec: %" GST_FOURCC_FORMAT, profiles->len,
        GST_FOURCC_ARGS (codec));
    GST_LOG ("sink caps: %" GST_PTR_FORMAT, sinkcaps);
    GST_LOG ("src caps: %" GST_PTR_FORMAT, srccaps);

    switch (codec) {
      case H264:
        if (!gst_va_h264_dec_register (plugin, device, sinkcaps, srccaps,
                GST_VA_RANK_PRIMARY)) {
          GST_WARNING ("Failed to register H264 decoder: %s",
              device->render_device_path);
        }
        break;
      case HEVC:
        if (!gst_va_h265_dec_register (plugin, device, sinkcaps, srccaps,
                GST_VA_RANK_PRIMARY)) {
          GST_WARNING ("Failed to register H265 decoder: %s",
              device->render_device_path);
        }
        break;
#if VA_CHECK_VERSION(1, 22, 0)
      case VVC:
        if (!gst_va_h266_dec_register (plugin, device, sinkcaps, srccaps,
                GST_RANK_NONE)) {
          GST_WARNING ("Failed to register H266 decoder: %s",
              device->render_device_path);
        }
        break;
#endif
      case VP8:
        if (!gst_va_vp8_dec_register (plugin, device, sinkcaps, srccaps,
                GST_VA_RANK_PRIMARY)) {
          GST_WARNING ("Failed to register VP8 decoder: %s",
              device->render_device_path);
        }
        break;
      case VP9:
        if (!gst_va_vp9_dec_register (plugin, device, sinkcaps, srccaps,
                GST_VA_RANK_PRIMARY)) {
          GST_WARNING ("Failed to register VP9 decoder: %s",
              device->render_device_path);
        }
        break;
      case MPEG2:
        if (!gst_va_mpeg2_dec_register (plugin, device, sinkcaps, srccaps,
                GST_VA_RANK_PRIMARY)) {
          GST_WARNING ("Failed to register Mpeg2 decoder: %s",
              device->render_device_path);
        }
        break;
      case AV1:
        if (!gst_va_av1_dec_register (plugin, device, sinkcaps, srccaps,
                GST_VA_RANK_PRIMARY)) {
          GST_WARNING ("Failed to register AV1 decoder: %s",
              device->render_device_path);
        }
        break;
      case JPEG:
        if (!gst_va_jpeg_dec_register (plugin, device, sinkcaps, srccaps,
                GST_RANK_NONE)) {
          GST_WARNING ("Failed to register JPEG decoder: %s",
              device->render_device_path);
        }
        break;
      default:
        GST_DEBUG ("No decoder implementation for %" GST_FOURCC_FORMAT,
            GST_FOURCC_ARGS (codec));
        break;
    }

    gst_caps_unref (srccaps);
    gst_caps_unref (sinkcaps);
  }
}

static void
plugin_register_encoders (GstPlugin * plugin, GstVaDevice * device,
    GHashTable * encoders, VAEntrypoint entrypoint)
{
  GHashTableIter iter;
  gpointer key, value;

  if (GST_VA_DISPLAY_IS_IMPLEMENTATION (device->display, INTEL_I965)
      && g_getenv ("GST_VA_ALL_DRIVERS") == NULL) {
    gst_plugin_add_status_warning (plugin,
        "The Intel i965 VA driver is blocklisted for encoding, use GST_VA_ALL_DRIVERS to overwrite.");
    return;
  }

  g_hash_table_iter_init (&iter, encoders);
  while (g_hash_table_iter_next (&iter, &key, &value)) {
    guint32 codec = *((gint64 *) key);
    GArray *profiles = (GArray *) value;
    GstCaps *sinkcaps = NULL, *srccaps = NULL;

    if (!profiles || profiles->len == 0)
      continue;

    if (!gst_va_caps_from_profiles (device->display, profiles, entrypoint,
            &srccaps, &sinkcaps))
      continue;

    GST_LOG ("%d encoder %scodec: %" GST_FOURCC_FORMAT, profiles->len,
        (entrypoint == VAEntrypointEncSliceLP) ? "low power " : "",
        GST_FOURCC_ARGS (codec));
    GST_LOG ("sink caps: %" GST_PTR_FORMAT, sinkcaps);
    GST_LOG ("src caps: %" GST_PTR_FORMAT, srccaps);

    switch (codec) {
      case H264:
        if (!gst_va_h264_enc_register (plugin, device, sinkcaps, srccaps,
                GST_RANK_NONE, entrypoint)) {
          GST_WARNING ("Failed to register H264 encoder: %s",
              device->render_device_path);
        }
        break;
      case HEVC:
        if (!gst_va_h265_enc_register (plugin, device, sinkcaps, srccaps,
                GST_RANK_NONE, entrypoint)) {
          GST_WARNING ("Failed to register H265 encoder: %s",
              device->render_device_path);
        }
        break;
      case VP8:
        if (!gst_va_vp8_enc_register (plugin, device, sinkcaps, srccaps,
                GST_RANK_NONE, entrypoint)) {
          GST_WARNING ("Failed to register VP8 encoder: %s",
              device->render_device_path);
        }
        break;
      case VP9:
        if (!gst_va_vp9_enc_register (plugin, device, sinkcaps, srccaps,
                GST_RANK_NONE, entrypoint)) {
          GST_WARNING ("Failed to register VP9 encoder: %s",
              device->render_device_path);
        }
        break;
      case JPEG:
        if (!gst_va_jpeg_enc_register (plugin, device, sinkcaps, srccaps,
                GST_RANK_NONE, entrypoint)) {
          GST_WARNING ("Failed to register JPEG encoder: %s",
              device->render_device_path);
        }
        break;
#if VA_CHECK_VERSION(1, 15, 0)
      case AV1:
        if (!gst_va_av1_enc_register (plugin, device, sinkcaps, srccaps,
                GST_RANK_NONE, entrypoint)) {
          GST_WARNING ("Failed to register AV1 encoder: %s",
              device->render_device_path);
        }
        break;
#endif
      default:
        GST_DEBUG ("No encoder implementation for %" GST_FOURCC_FORMAT,
            GST_FOURCC_ARGS (codec));
        break;
    }

    gst_caps_unref (srccaps);
    gst_caps_unref (sinkcaps);
  }
}

static void
plugin_register_vpp (GstPlugin * plugin, GstVaDevice * device)
{
  GstVaFilter *filter;
  gboolean has_colorbalance, has_deinterlace, has_compose;

  has_colorbalance = FALSE;
  has_deinterlace = FALSE;
  has_compose = FALSE;
  filter = gst_va_filter_new (device->display);
  if (gst_va_filter_open (filter)) {
    has_colorbalance =
        gst_va_filter_has_filter (filter, VAProcFilterColorBalance);
    has_deinterlace =
        gst_va_filter_has_filter (filter, VAProcFilterDeinterlacing);
    has_compose = gst_va_filter_has_compose (filter);
  } else {
    GST_WARNING ("Failed open VA filter");
    gst_object_unref (filter);
    return;
  }
  gst_object_unref (filter);

  if (!gst_va_vpp_register (plugin, device, has_colorbalance, GST_RANK_NONE))
    GST_WARNING ("Failed to register postproc: %s", device->render_device_path);

  if (has_deinterlace) {
    if (!gst_va_deinterlace_register (plugin, device, GST_RANK_NONE)) {
      GST_WARNING ("Failed to register deinterlace: %s",
          device->render_device_path);
    }
  }

  if (has_compose) {
    if (!gst_va_compositor_register (plugin, device, GST_RANK_NONE)) {
      GST_WARNING ("Failed to register compositor: %s",
          device->render_device_path);
    }
  }
}

static inline void
_insert_profile_in_table (GHashTable * table, VAProfile profile)
{
  gint64 codec = gst_va_profile_codec (profile);
  GArray *profiles;

  if (codec == GST_MAKE_FOURCC ('N', 'O', 'N', 'E'))
    return;

  profiles = g_hash_table_lookup (table, &codec);
  if (!profiles) {
    gint64 *codec_ptr = g_new (gint64, 1);

    *codec_ptr = codec;
    profiles = g_array_new (FALSE, FALSE, sizeof (VAProfile));
    g_hash_table_insert (table, codec_ptr, profiles);
  }
  g_array_append_val (profiles, profile);
}

static gboolean
plugin_register_elements (GstPlugin * plugin, GstVaDevice * device)
{
  VADisplay dpy = gst_va_display_get_va_dpy (device->display);
  VAEntrypoint *entrypoints = g_new (VAEntrypoint, vaMaxNumEntrypoints (dpy));
  VAProfile *profiles = g_new (VAProfile, vaMaxNumProfiles (dpy));
  VAStatus status;
  GHashTable *decoders, *encoders, *encoderslp, *encodersimg;
  gint i, j, num_entrypoints = 0, num_profiles = 0;
  gboolean has_vpp = FALSE, ret = FALSE;

  decoders = g_hash_table_new_full (g_int64_hash, g_int64_equal,
      (GDestroyNotify) g_free, (GDestroyNotify) g_array_unref);
  encoders = g_hash_table_new_full (g_int64_hash, g_int64_equal,
      (GDestroyNotify) g_free, (GDestroyNotify) g_array_unref);
  encoderslp = g_hash_table_new_full (g_int64_hash, g_int64_equal,
      (GDestroyNotify) g_free, (GDestroyNotify) g_array_unref);
  encodersimg = g_hash_table_new_full (g_int64_hash, g_int64_equal,
      (GDestroyNotify) g_free, (GDestroyNotify) g_array_unref);

  status = vaQueryConfigProfiles (dpy, profiles, &num_profiles);
  if (status != VA_STATUS_SUCCESS) {
    GST_WARNING ("vaQueryConfigProfile: %s", vaErrorStr (status));
    goto bail;
  }

  for (i = 0; i < num_profiles; i++) {
    status = vaQueryConfigEntrypoints (dpy, profiles[i], entrypoints,
        &num_entrypoints);
    if (status != VA_STATUS_SUCCESS) {
      GST_WARNING ("vaQueryConfigEntrypoints: %s", vaErrorStr (status));
      goto bail;
    }

    for (j = 0; j < num_entrypoints; j++) {
      if (entrypoints[j] == VAEntrypointVLD)
        _insert_profile_in_table (decoders, profiles[i]);
      else if (entrypoints[j] == VAEntrypointEncSlice)
        _insert_profile_in_table (encoders, profiles[i]);
      else if (entrypoints[j] == VAEntrypointEncSliceLP)
        _insert_profile_in_table (encoderslp, profiles[i]);
      else if (entrypoints[j] == VAEntrypointEncPicture)
        _insert_profile_in_table (encodersimg, profiles[i]);
      else if (entrypoints[j] == VAEntrypointVideoProc)
        has_vpp = TRUE;
    }
  }

  plugin_register_decoders (plugin, device, decoders);
  plugin_register_encoders (plugin, device, encoders, VAEntrypointEncSlice);
  plugin_register_encoders (plugin, device, encoderslp, VAEntrypointEncSliceLP);
  plugin_register_encoders (plugin, device, encodersimg,
      VAEntrypointEncPicture);
  if (has_vpp)
    plugin_register_vpp (plugin, device);

  ret = TRUE;

bail:
  g_hash_table_unref (encodersimg);
  g_hash_table_unref (encoderslp);
  g_hash_table_unref (encoders);
  g_hash_table_unref (decoders);
  g_free (entrypoints);
  g_free (profiles);

  return ret;
}

static gboolean
plugin_init (GstPlugin * plugin)
{
  GList *devices, *dev;

  GST_DEBUG_CATEGORY_INIT (gstva_debug, "va", 0, "VA general debug");

  plugin_add_dependencies (plugin);

  devices = gst_va_device_find_devices ();
  for (dev = devices; dev; dev = g_list_next (dev))
    plugin_register_elements (plugin, dev->data);

  gst_va_device_list_free (devices);

  return TRUE;
}

GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
    GST_VERSION_MINOR,
    va, "VA-API codecs plugin",
    plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)
