/*
 * Copyright © 2019-2022 Collabora Ltd.
 *
 * SPDX-License-Identifier: MIT
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include "steam-runtime-tools/graphics.h"
#include "steam-runtime-tools/graphics-internal.h"

#include "steam-runtime-tools/glib-backports-internal.h"
#include "steam-runtime-tools/json-glib-backports-internal.h"
#include "steam-runtime-tools/json-utils-internal.h"

/*
 * _srt_dri_driver_get_from_report:
 * @json_obj: (not nullable): A JSON Object used to search for "dri_drivers"
 *  property
 *
 * If the provided @json_obj doesn't have a "dri_drivers" member, or it is
 * malformed, %NULL will be returned.
 *
 * Returns: A list of #SrtDriDriver that have been found, or %NULL if none
 *  has been found.
 */
GList *
_srt_dri_driver_get_from_report (JsonObject *json_obj)
{
  JsonArray *array;
  GList *dri_drivers = NULL;

  g_return_val_if_fail (json_obj != NULL, NULL);

  if (json_object_has_member (json_obj, "dri_drivers"))
    {
      array = json_object_get_array_member (json_obj, "dri_drivers");

      if (array == NULL)
        goto out;

      guint length = json_array_get_length (array);
      for (guint j = 0; j < length; j++)
        {
          const gchar *dri_path = NULL;
          gboolean is_extra = FALSE;
          JsonObject *json_dri_obj = json_array_get_object_element (array, j);
          dri_path = json_object_get_string_member_with_default (json_dri_obj, "library_path",
                                                                 NULL);
          is_extra = json_object_get_boolean_member_with_default (json_dri_obj, "is_extra",
                                                                  FALSE);

          dri_drivers = g_list_prepend (dri_drivers, srt_dri_driver_new (dri_path, is_extra));
        }
    }

out:
  return g_list_reverse (dri_drivers);
}

/*
 * _srt_gbm_backend_get_from_report:
 * @json_obj: (not nullable): A JSON Object used to search for "gbm_backends"
 *  property
 *
 * If the provided @json_obj doesn't have a "gbm_backends" member, or it is
 * malformed, %NULL will be returned.
 *
 * Returns: A list of #SrtGbmBackend that have been found, or %NULL if none
 *  has been found.
 */
GList *
_srt_gbm_backend_get_from_report (JsonObject *json_obj)
{
  JsonArray *array;
  GList *gbm_backends = NULL;

  g_return_val_if_fail (json_obj != NULL, NULL);

  if (json_object_has_member (json_obj, "gbm_backends"))
    {
      array = json_object_get_array_member (json_obj, "gbm_backends");

      if (array == NULL)
        goto out;

      guint length = json_array_get_length (array);
      for (guint j = 0; j < length; j++)
        {
          const gchar *gbm_path = NULL;
          gboolean is_extra = FALSE;
          JsonObject *json_gbm_obj = json_array_get_object_element (array, j);
          gbm_path = json_object_get_string_member_with_default (json_gbm_obj, "library_path",
                                                                 NULL);
          is_extra = json_object_get_boolean_member_with_default (json_gbm_obj, "is_extra",
                                                                  FALSE);

          gbm_backends = g_list_prepend (gbm_backends, srt_gbm_backend_new (gbm_path, is_extra));
        }
    }

out:
  return g_list_reverse (gbm_backends);
}

/*
 * _srt_glx_icd_get_from_report:
 * @json_obj: (not nullable): A JSON Object used to search for "glx_drivers"
 *  property
 *
 * If the provided @json_obj doesn't have a "glx_drivers" member, or it is
 * malformed, %NULL will be returned.
 *
 * Returns: A list of #SrtGlxIcd that have been found, or %NULL if none
 *  has been found.
 */
GList *
_srt_glx_icd_get_from_report (JsonObject *json_obj)
{
  JsonArray *array;
  GList *glx_drivers = NULL;

  g_return_val_if_fail (json_obj != NULL, NULL);

  if (json_object_has_member (json_obj, "glx_drivers"))
    {
      array = json_object_get_array_member (json_obj, "glx_drivers");

      if (array == NULL)
        goto out;

      guint length = json_array_get_length (array);
      for (guint j = 0; j < length; j++)
        {
          const gchar *glx_path = NULL;
          const gchar *glx_soname = NULL;
          JsonObject *json_glx_obj = json_array_get_object_element (array, j);
          glx_path = json_object_get_string_member_with_default (json_glx_obj, "library_path",
                                                                 NULL);
          glx_soname = json_object_get_string_member_with_default (json_glx_obj, "library_soname",
                                                                   NULL);

          glx_drivers = g_list_prepend (glx_drivers, srt_glx_icd_new (glx_soname, glx_path));
        }
    }

out:
  return g_list_reverse (glx_drivers);
}

/*
 * _srt_va_api_driver_get_from_report:
 * @json_obj: (not nullable): A JSON Object used to search for "va-api_drivers"
 *  property
 *
 * If the provided @json_obj doesn't have a "va-api_drivers" member, or it is
 * malformed, %NULL will be returned.
 *
 * Returns: A list of #SrtVaApiDriver that have been found, or %NULL if none
 *  has been found.
 */
GList *
_srt_va_api_driver_get_from_report (JsonObject *json_obj)
{
  JsonArray *array;
  GList *va_api_drivers = NULL;

  g_return_val_if_fail (json_obj != NULL, NULL);

  if (json_object_has_member (json_obj, "va-api_drivers"))
    {
      array = json_object_get_array_member (json_obj, "va-api_drivers");

      if (array == NULL)
        goto out;

      guint length = json_array_get_length (array);
      for (guint j = 0; j < length; j++)
        {
          const gchar *va_api_path = NULL;
          int version = SRT_VA_API_VERSION_UNKNOWN;
          gboolean is_extra = FALSE;
          JsonObject *json_va_api_obj = json_array_get_object_element (array, j);
          va_api_path = json_object_get_string_member_with_default (json_va_api_obj,
                                                                    "library_path",
                                                                    NULL);
          _srt_json_object_get_enum_member (json_va_api_obj, "version",
                                            SRT_TYPE_VA_API_VERSION, &version);
          is_extra = json_object_get_boolean_member_with_default (json_va_api_obj,
                                                                  "is_extra",
                                                                  FALSE);

          va_api_drivers = g_list_prepend (va_api_drivers, srt_va_api_driver_new (va_api_path,
                                                                                  version,
                                                                                  is_extra));
        }
    }

out:
  return g_list_reverse (va_api_drivers);
}

/*
 * _srt_vdpau_driver_get_from_report:
 * @json_obj: (not nullable): A JSON Object used to search for "vdpau_drivers"
 *  property
 *
 * If the provided @json_obj doesn't have a "vdpau_drivers" member, or it is
 * malformed, %NULL will be returned.
 *
 * Returns: A list of #SrtVdpauDriver that have been found, or %NULL if none
 *  has been found.
 */
GList *
_srt_vdpau_driver_get_from_report (JsonObject *json_obj)
{
  JsonArray *array;
  GList *vdpau_drivers = NULL;

  g_return_val_if_fail (json_obj != NULL, NULL);

  if (json_object_has_member (json_obj, "vdpau_drivers"))
    {
      array = json_object_get_array_member (json_obj, "vdpau_drivers");

      if (array == NULL)
        goto out;

      guint length = json_array_get_length (array);
      for (guint j = 0; j < length; j++)
        {
          const gchar *vdpau_path = NULL;
          const gchar *vdpau_link = NULL;
          gboolean is_extra = FALSE;
          JsonObject *json_vdpau_obj = json_array_get_object_element (array, j);
          vdpau_path = json_object_get_string_member_with_default (json_vdpau_obj, "library_path",
                                                                   NULL);
          vdpau_link = json_object_get_string_member_with_default (json_vdpau_obj, "library_link",
                                                                   NULL);
          is_extra = json_object_get_boolean_member_with_default (json_vdpau_obj, "is_extra",
                                                                  FALSE);

          vdpau_drivers = g_list_prepend (vdpau_drivers, srt_vdpau_driver_new (vdpau_path, vdpau_link, is_extra));
        }
    }

out:
  return g_list_reverse (vdpau_drivers);
}

/*
 * get_driver_loadable_from_json_report:
 * @json_obj: (not nullable): A JSON Object used to search for Icd or Layer
 *  properties
 * @which: Used to choose which loadable to search, it can be
 *  %SRT_TYPE_EGL_ICD,
 *  %SRT_TYPE_VULKAN_ICD,
 *  %SRT_TYPE_VULKAN_LAYER,
 *  %SRT_TYPE_EGL_EXTERNAL_PLATFORM,
 *  %SRT_TYPE_OPENXR_1_LAYER
 *  or %SRT_TYPE_OPENXR_1_RUNTIME
 * @debug_member: The name of the parent containing @json_obj, for debugging
 *  purposes
 *
 * Returns: An object of type @which, or %NULL if it could not be loaded.
 */
static GObject *
get_driver_loadable_from_json_report (JsonObject *json_obj,
                                      GType which,
                                      const char *debug_member)
{
  const gchar *json_path = NULL;
  const gchar *json_origin = NULL;
  const gchar *name = NULL;
  const gchar *type = NULL;
  const gchar *description = NULL;
  const gchar *library_path = NULL;
  const gchar *library_arch = NULL;
  const gchar *api_version = NULL;
  const gchar *implementation_version = NULL;
  gboolean portability_driver = FALSE;
  g_auto(GStrv) component_layers = NULL;
  SrtLoadableIssues issues;
  GQuark error_domain;
  gint error_code;
  const gchar *error_message;
  g_autoptr(GError) error = NULL;

  json_path = json_object_get_string_member_with_default (json_obj,
                                                          "json_path",
                                                          NULL);
  if (json_path == NULL)
    {
      g_debug ("The parsed '%s' member is missing the expected 'json_path' member, skipping...",
               debug_member);
      return NULL;
    }

  if (which == SRT_TYPE_VULKAN_LAYER)
    {
      type = json_object_get_string_member_with_default (json_obj, "type", NULL);

      component_layers = _srt_json_object_dup_strv_member (json_obj,
                                                           "component_layers",
                                                           NULL);

      /* Don't distinguish between absent, and present with empty value */
      if (component_layers != NULL && component_layers[0] == NULL)
        g_clear_pointer (&component_layers, g_free);
    }

  description = json_object_get_string_member_with_default (json_obj,
                                                            "description",
                                                            NULL);
  implementation_version = json_object_get_string_member_with_default (json_obj,
                                                                       "implementation_version",
                                                                       NULL);
  json_origin = json_object_get_string_member_with_default (json_obj,
                                                            "json_origin",
                                                            json_path);
  library_path = json_object_get_string_member_with_default (json_obj,
                                                             "library_path",
                                                             NULL);
  library_arch = json_object_get_string_member_with_default (json_obj,
                                                             "library_arch",
                                                             NULL);
  name = json_object_get_string_member_with_default (json_obj, "name", NULL);
  api_version = json_object_get_string_member_with_default (json_obj,
                                                            "api_version",
                                                            NULL);
  portability_driver = json_object_get_boolean_member_with_default (json_obj,
                                                                    "is_portability_driver",
                                                                    FALSE);
  issues = srt_get_flags_from_json_array (SRT_TYPE_LOADABLE_ISSUES,
                                          json_obj,
                                          "issues",
                                          SRT_LOADABLE_ISSUES_UNKNOWN);
  error_domain = g_quark_from_string (json_object_get_string_member_with_default (json_obj,
                                                                                  "error-domain",
                                                                                  NULL));
  error_code = json_object_get_int_member_with_default (json_obj, "error-code", -1);
  error_message = json_object_get_string_member_with_default (json_obj,
                                                              "error",
                                                              "(missing error message)");

  if (which == SRT_TYPE_VULKAN_LAYER &&
      (name != NULL &&
       type != NULL &&
       api_version != NULL &&
       implementation_version != NULL &&
       description != NULL &&
       ( (library_path != NULL && component_layers == NULL) ||
         (library_path == NULL && component_layers != NULL) )))
    {
      return (GObject *) srt_vulkan_layer_new (json_path, name, type,
                                               library_path, library_arch,
                                               api_version, implementation_version,
                                               description, component_layers,
                                               issues);
    }
  else if (which == SRT_TYPE_OPENXR_1_LAYER
           && name != NULL
           && api_version != NULL
           && implementation_version != NULL
           && description != NULL
           && library_path != NULL)
    {
      return (GObject *) srt_openxr_1_layer_new (json_path,
                                                 json_origin,
                                                 name,
                                                 library_path,
                                                 api_version,
                                                 implementation_version,
                                                 description,
                                                 issues);
    }
  else if ((which == SRT_TYPE_EGL_ICD
            || which == SRT_TYPE_OPENXR_1_RUNTIME
            || which == SRT_TYPE_EGL_EXTERNAL_PLATFORM
            || which == SRT_TYPE_VULKAN_ICD
            || which == SRT_TYPE_OPENXR_1_RUNTIME) &&
           library_path != NULL)
    {
      if (which == SRT_TYPE_EGL_ICD)
        return (GObject *) srt_egl_icd_new (json_path, library_path, issues);
      else if (which == SRT_TYPE_EGL_EXTERNAL_PLATFORM)
        return (GObject *) srt_egl_external_platform_new (json_path,
                                                          library_path,
                                                          issues);
      else if (which == SRT_TYPE_VULKAN_ICD)
        return (GObject *) srt_vulkan_icd_new (json_path,
                                               api_version,
                                               library_path,
                                               library_arch,
                                               portability_driver,
                                               issues);
      else if (which == SRT_TYPE_OPENXR_1_RUNTIME)
        return (GObject *) srt_openxr_1_runtime_new (json_path,
                                                     json_origin,
                                                     name,
                                                     library_path,
                                                     issues);
      else
        g_return_val_if_reached (NULL);
    }
  else
    {
      if (error_domain == 0)
        {
          error_domain = G_IO_ERROR;
          error_code = G_IO_ERROR_FAILED;
        }
      g_set_error_literal (&error,
                           error_domain,
                           error_code,
                           error_message);
      if (which == SRT_TYPE_EGL_ICD)
        return (GObject *) srt_egl_icd_new_error (json_path, issues, error);
      else if (which == SRT_TYPE_EGL_EXTERNAL_PLATFORM)
        return (GObject *) srt_egl_external_platform_new_error (json_path, issues, error);
      else if (which == SRT_TYPE_VULKAN_ICD)
        return (GObject *) srt_vulkan_icd_new_error (json_path, issues, error);
      else if (which == SRT_TYPE_VULKAN_LAYER)
        return (GObject *) srt_vulkan_layer_new_error (json_path, issues, error);
      else if (which == SRT_TYPE_OPENXR_1_RUNTIME)
        return (GObject *) srt_openxr_1_runtime_new_error (json_path, json_origin,
                                                           issues, error);
      else if (which == SRT_TYPE_OPENXR_1_LAYER)
        return (GObject *) srt_openxr_1_layer_new_error (json_path, json_origin,
                                                         issues, error);
      else
        g_return_val_if_reached (NULL);
    }
}

/*
 * get_driver_loadables_from_json_report:
 * @json_obj: (not nullable): A JSON Object used to search for Icd or Layer
 *  properties
 * @which: Used to choose which loadable to search, it can be
 *  %SRT_TYPE_EGL_ICD,
 *  %SRT_TYPE_VULKAN_ICD,
 *  %SRT_TYPE_VULKAN_LAYER,
 *  %SRT_TYPE_EGL_EXTERNAL_PLATFORM,
 *  %SRT_TYPE_OPENXR_1_LAYER
 *  or %SRT_TYPE_OPENXR_1_RUNTIME
 * @member: The name of the member of @json_obj that contains @sub_member.
 * @sub_member: The name of the member of @member that contains the array of
 *  loadables.
 *
 * Returns: A list of objects of type @which, or
 *  %NULL if none has been found.
 */
static GList *
get_driver_loadables_from_json_report (JsonObject *json_obj,
                                       GType which,
                                       const char *member,
                                       const char *sub_member)
{
  JsonObject *json_sub_obj;
  JsonArray *array;
  GList *driver_info = NULL;

  g_return_val_if_fail (json_obj != NULL, NULL);

  if (json_object_has_member (json_obj, member))
    {
      json_sub_obj = json_object_get_object_member (json_obj, member);

      /* We are expecting an object here */
      if (json_sub_obj == NULL)
        {
          g_debug ("'%s' is not a JSON object as expected", member);
          goto out;
        }

      if (json_object_has_member (json_sub_obj, sub_member))
        {
          array = json_object_get_array_member (json_sub_obj, sub_member);

          /* We are expecting an array of icds here */
          if (array == NULL)
            {
              g_debug ("'%s' is not an array as expected", sub_member);
              goto out;
            }

          for (guint i = 0; i < json_array_get_length (array); i++)
            {
              JsonObject *json_elem_obj = json_array_get_object_element (array, i);
              GObject *obj = get_driver_loadable_from_json_report (json_elem_obj,
                                                                   which,
                                                                   sub_member);
              if (obj != NULL)
                driver_info = g_list_prepend (driver_info, obj);
            }
        }
    }
out:
  return g_list_reverse (driver_info);
}

/*
 * _srt_get_egl_from_json_report:
 * @json_obj: (not nullable): A JSON Object used to search for "egl" property
 *
 * Returns: A list of #SrtEglIcd that have been found, or %NULL if none
 *  has been found.
 */
GList *
_srt_get_egl_from_json_report (GType which, JsonObject *json_obj)
{
  const char *sub_member;
  if (which == SRT_TYPE_EGL_ICD)
    sub_member = "icds";
  else if (which == SRT_TYPE_EGL_EXTERNAL_PLATFORM)
    sub_member = "external_platforms";
  else
    g_return_val_if_reached (NULL);
  return get_driver_loadables_from_json_report (json_obj, which,
                                                "egl", sub_member);
}

/*
 * _srt_get_explicit_vulkan_layers_from_json_report:
 * @json_obj: (not nullable): A JSON Object used to search for
 *  "explicit_layers" property in "vulkan"
 *
 * Returns: A list of explicit #SrtVulkanLayer that have been found, or %NULL
 *  if none has been found.
 */
GList *
_srt_get_explicit_vulkan_layers_from_json_report (JsonObject *json_obj)
{
  return get_driver_loadables_from_json_report (json_obj, SRT_TYPE_VULKAN_LAYER,
                                                "vulkan", "explicit_layers");
}

/*
 * _srt_get_implicit_vulkan_layers_from_json_report:
 * @json_obj: (not nullable): A JSON Object used to search for
 *  "implicit_layers" property in "vulkan"
 *
 * Returns: A list of implicit #SrtVulkanLayer that have been found, or %NULL
 *  if none has been found.
 */
GList *
_srt_get_implicit_vulkan_layers_from_json_report (JsonObject *json_obj)
{
  return get_driver_loadables_from_json_report (json_obj, SRT_TYPE_VULKAN_LAYER,
                                                "vulkan", "implicit_layers");
}

/*
 * _srt_get_vulkan_from_json_report:
 * @json_obj: (not nullable): A JSON Object used to search for "vulkan" property
 *
 * Returns: A list of #SrtVulkanIcd that have been found, or %NULL if none
 *  has been found.
 */
GList *
_srt_get_vulkan_from_json_report (JsonObject *json_obj)
{
  return get_driver_loadables_from_json_report (json_obj, SRT_TYPE_VULKAN_ICD,
                                                "vulkan", "icds");
}

static SrtOpenXr1Runtime *
get_openxr_1_runtime (JsonObject *json_obj,
                      const char *member)
{
  JsonObject *member_obj;

  if (!json_object_has_member (json_obj, member))
    return NULL;

  member_obj = json_object_get_object_member (json_obj, member);
  if (member_obj == NULL)
    {
      g_debug ("'%s' is not a JSON object as expected", member);
      return NULL;
    }

  return (SrtOpenXr1Runtime *) get_driver_loadable_from_json_report (member_obj,
                                                                     SRT_TYPE_OPENXR_1_RUNTIME,
                                                                     member);
}

/*
 * _srt_openxr_1_runtime_get_active_from_abi_report:
 * @json_obj: (not nullable): A JSON Object holding an ABI-specific section of
 *                            the report, used to search for the
 *                            "openxr-1-runtime" property.
 *
 * Loads an architecture's active OpenXR runtime from the ABI report.
 */
SrtOpenXr1Runtime *
_srt_openxr_1_runtime_get_active_from_abi_report (JsonObject *json_obj)
{
  return get_openxr_1_runtime (json_obj, "openxr-1-runtime");
}

/*
 * _srt_openxr_1_runtimes_get_from_report:
 * @json_obj: (not nullable): A JSON Object used to search for the "openxr-1" property
 * @out_active_fallback: (out): Used to return the "fallback" active runtime.
 * @out_inactive: (out): Used to return a list of inactive runtimes.
 *
 * Loads the OpenXR fallback and inactive runtimes from the report. This does
 * not load the arch-specific active runtimes, which are instead stored in the
 * ABI section and should be loaded with
 * _srt_openxr_1_runtime_get_active_from_abi_report().
 */
void
_srt_openxr_1_runtimes_get_from_report (JsonObject *json_obj,
                                        SrtOpenXr1Runtime **out_active_fallback,
                                        GList **out_inactive)
{
  static const char member_xr[] = "openxr-1";
  static const char member_rt[] = "runtimes";

  JsonObject *obj_xr;
  JsonObject *obj_rt;

  if (!json_object_has_member (json_obj, member_xr))
    return;

  obj_xr = json_object_get_object_member (json_obj, member_xr);
  if (obj_xr == NULL)
    {
      g_debug ("'%s' is not a JSON object as expected", member_xr);
      return;
    }

  if (!json_object_has_member (obj_xr, member_rt))
    return;

  obj_rt = json_object_get_object_member (obj_xr, member_rt);
  if (obj_rt == NULL)
    {
      g_debug ("'%s' is not a JSON object as expected", member_rt);
      return;
    }

  *out_active_fallback = get_openxr_1_runtime (obj_rt, "active-fallback");
  *out_inactive = get_driver_loadables_from_json_report (obj_xr,
                                                         SRT_TYPE_OPENXR_1_RUNTIME,
                                                         "runtimes",
                                                         "inactive");
}

/*
 * _srt_get_explicit_openxr_1_layers_from_json_report:
 * @json_obj: (not nullable): A JSON Object used to search for
 *  "explicit_layers" property in "openxr-1"
 *
 * Returns: A list of explicit #SrtOpenXr1Layer that have been found, or %NULL
 *  if none has been found.
 */
GList *
_srt_get_explicit_openxr_1_layers_from_json_report (JsonObject *json_obj)
{
  return get_driver_loadables_from_json_report (json_obj, SRT_TYPE_OPENXR_1_LAYER,
                                                "openxr-1", "explicit_layers");
}

/*
 * _srt_get_implicit_openxr_1_layers_from_json_report:
 * @json_obj: (not nullable): A JSON Object used to search for
 *  "implicit_layers" property in "openxr-1"
 *
 * Returns: A list of implicit #SrtOpenXr1Layer that have been found, or %NULL
 *  if none has been found.
 */
GList *
_srt_get_implicit_openxr_1_layers_from_json_report (JsonObject *json_obj)
{
  return get_driver_loadables_from_json_report (json_obj, SRT_TYPE_OPENXR_1_LAYER,
                                                "openxr-1", "implicit_layers");
}
