/*
 * This file is part of libmodulemd
 * Copyright (C) 2020 Red Hat, Inc.
 *
 * Fedora-License-Identifier: MIT
 * SPDX-2.0-License-Identifier: MIT
 * SPDX-3.0-License-Identifier: MIT
 *
 * This program is free software.
 * For more information on the license, see COPYING.
 * For more information on free software, see <https://www.gnu.org/philosophy/free-sw.en.html>.
 */

#include "modulemd-2.0/modulemd-buildopts.h"
#include "modulemd-2.0/modulemd-errors.h"
#include "modulemd-2.0/modulemd-module-stream.h"

#include "private/modulemd-build-config-private.h"
#include "private/modulemd-buildopts-private.h"
#include "private/modulemd-util.h"
#include "private/modulemd-yaml.h"

struct _ModulemdBuildConfig
{
  GObject parent_instance;

  gchar *context;
  gchar *platform;
  GHashTable *requires; /* hashtable<string, string> */
  GHashTable *buildrequires; /* hashtable<string, string> */
  ModulemdBuildopts *buildopts;
};

G_DEFINE_TYPE (ModulemdBuildConfig, modulemd_build_config, G_TYPE_OBJECT)

ModulemdBuildConfig *
modulemd_build_config_new (void)
{
  return g_object_new (MODULEMD_TYPE_BUILD_CONFIG, NULL);
}

static void
modulemd_build_config_finalize (GObject *object)
{
  ModulemdBuildConfig *self = MODULEMD_BUILD_CONFIG (object);

  g_clear_pointer (&self->context, g_free);
  g_clear_pointer (&self->platform, g_free);
  g_clear_pointer (&self->requires, g_hash_table_unref);
  g_clear_pointer (&self->buildrequires, g_hash_table_unref);
  g_clear_object (&self->buildopts);

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

static void
modulemd_build_config_class_init (ModulemdBuildConfigClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = modulemd_build_config_finalize;
}

static void
modulemd_build_config_init (ModulemdBuildConfig *self)
{
  self->requires =
    g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
  self->buildrequires =
    g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
}


void
modulemd_build_config_set_context (ModulemdBuildConfig *self,
                                   const gchar *context)
{
  g_return_if_fail (MODULEMD_IS_BUILD_CONFIG (self));

  g_clear_pointer (&self->context, g_free);

  if (context)
    {
      self->context = g_strdup (context);
    }
}


const gchar *
modulemd_build_config_get_context (ModulemdBuildConfig *self)
{
  g_return_val_if_fail (MODULEMD_IS_BUILD_CONFIG (self), NULL);

  return self->context;
}


void
modulemd_build_config_set_platform (ModulemdBuildConfig *self,
                                    const gchar *platform)
{
  g_return_if_fail (MODULEMD_IS_BUILD_CONFIG (self));

  g_clear_pointer (&self->platform, g_free);

  if (platform)
    {
      self->platform = g_strdup (platform);
    }
}


const gchar *
modulemd_build_config_get_platform (ModulemdBuildConfig *self)
{
  g_return_val_if_fail (MODULEMD_IS_BUILD_CONFIG (self), NULL);

  return self->platform;
}


void
modulemd_build_config_add_runtime_requirement (ModulemdBuildConfig *self,
                                               const gchar *module_name,
                                               const gchar *stream_name)
{
  g_return_if_fail (MODULEMD_IS_BUILD_CONFIG (self));
  g_return_if_fail (module_name && stream_name);

  g_hash_table_replace (
    self->requires, g_strdup (module_name), g_strdup (stream_name));
}


void
modulemd_build_config_remove_runtime_requirement (ModulemdBuildConfig *self,
                                                  const gchar *module_name)
{
  g_return_if_fail (MODULEMD_IS_BUILD_CONFIG (self));
  g_return_if_fail (module_name);

  g_hash_table_remove (self->requires, module_name);
}


void
modulemd_build_config_clear_runtime_requirements (ModulemdBuildConfig *self)
{
  g_return_if_fail (MODULEMD_IS_BUILD_CONFIG (self));

  g_hash_table_remove_all (self->requires);
}


const gchar *
modulemd_build_config_get_runtime_requirement_stream (
  ModulemdBuildConfig *self, const gchar *module_name)
{
  g_return_val_if_fail (MODULEMD_IS_BUILD_CONFIG (self), NULL);

  return g_hash_table_lookup (self->requires, module_name);
}


GStrv
modulemd_build_config_get_runtime_modules_as_strv (ModulemdBuildConfig *self)
{
  g_return_val_if_fail (MODULEMD_IS_BUILD_CONFIG (self), NULL);

  return modulemd_ordered_str_keys_as_strv (self->requires);
}


void
modulemd_build_config_add_buildtime_requirement (ModulemdBuildConfig *self,
                                                 const gchar *module_name,
                                                 const gchar *stream_name)
{
  g_return_if_fail (MODULEMD_IS_BUILD_CONFIG (self));
  g_return_if_fail (module_name && stream_name);

  g_hash_table_replace (
    self->buildrequires, g_strdup (module_name), g_strdup (stream_name));
}


void
modulemd_build_config_remove_buildtime_requirement (ModulemdBuildConfig *self,
                                                    const gchar *module_name)
{
  g_return_if_fail (MODULEMD_IS_BUILD_CONFIG (self));
  g_return_if_fail (module_name);

  g_hash_table_remove (self->buildrequires, module_name);
}


void
modulemd_build_config_clear_buildtime_requirements (ModulemdBuildConfig *self)
{
  g_return_if_fail (MODULEMD_IS_BUILD_CONFIG (self));

  g_hash_table_remove_all (self->buildrequires);
}


const gchar *
modulemd_build_config_get_buildtime_requirement_stream (
  ModulemdBuildConfig *self, const gchar *module_name)
{
  g_return_val_if_fail (MODULEMD_IS_BUILD_CONFIG (self), NULL);

  return g_hash_table_lookup (self->buildrequires, module_name);
}


GStrv
modulemd_build_config_get_buildtime_modules_as_strv (ModulemdBuildConfig *self)
{
  g_return_val_if_fail (MODULEMD_IS_BUILD_CONFIG (self), NULL);

  return modulemd_ordered_str_keys_as_strv (self->buildrequires);
}


void
modulemd_build_config_set_buildopts (ModulemdBuildConfig *self,
                                     ModulemdBuildopts *buildopts)
{
  g_return_if_fail (MODULEMD_IS_BUILD_CONFIG (self));

  g_clear_object (&self->buildopts);
  if (buildopts)
    {
      self->buildopts = modulemd_buildopts_copy (buildopts);
    }
}


ModulemdBuildopts *
modulemd_build_config_get_buildopts (ModulemdBuildConfig *self)
{
  g_return_val_if_fail (MODULEMD_IS_BUILD_CONFIG (self), NULL);

  return self->buildopts;
}


static void
modulemd_build_config_replace_runtime_deps (ModulemdBuildConfig *self,
                                            GHashTable *deps);
static void
modulemd_build_config_replace_buildtime_deps (ModulemdBuildConfig *self,
                                              GHashTable *deps);

static GHashTable *
modulemd_build_config_parse_deptable (yaml_parser_t *parser, GError **error)
{
  MODULEMD_INIT_TRACE ();
  MMD_INIT_YAML_EVENT (event);
  g_autoptr (GError) nested_error = NULL;
  g_autoptr (GHashTable) nested_set = NULL;
  g_autoptr (GHashTable) deptable = NULL;
  g_auto (GStrv) stream_names = NULL;
  GHashTableIter iter;
  gpointer key;
  gpointer value;
  gchar *module_name;


  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  nested_set = modulemd_yaml_parse_nested_set (parser, &nested_error);
  if (!nested_set)
    {
      g_propagate_error (error, g_steal_pointer (&nested_error));
      return NULL;
    }

  deptable = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);

  g_hash_table_iter_init (&iter, nested_set);
  while (g_hash_table_iter_next (&iter, &key, &value))
    {
      module_name = (gchar *)key;
      stream_names = modulemd_ordered_str_keys_as_strv (value);

      if (g_strv_length (stream_names) != 1)
        {
          MMD_YAML_ERROR_EVENT_EXIT (
            error,
            event,
            "BuildConfig dependency %s must specify a single stream",
            module_name);
        }

      g_hash_table_replace (
        deptable, g_strdup (module_name), g_strdup (stream_names[0]));

      g_clear_pointer (&stream_names, g_strfreev);
    }

  g_clear_pointer (&nested_set, g_hash_table_unref);

  return g_steal_pointer (&deptable);
}

ModulemdBuildConfig *
modulemd_build_config_parse_yaml (yaml_parser_t *parser,
                                  gboolean strict,
                                  GError **error)
{
  MODULEMD_INIT_TRACE ();
  MMD_INIT_YAML_EVENT (event);
  gboolean done = FALSE;
  g_autoptr (ModulemdBuildConfig) buildconfig = NULL;
  g_autoptr (ModulemdBuildopts) buildopts = NULL;
  g_autoptr (GError) nested_error = NULL;
  g_autoptr (GHashTable) deptable = NULL;

  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  buildconfig = modulemd_build_config_new ();

  /* Read in attributes of this config */
  while (!done)
    {
      YAML_PARSER_PARSE_WITH_EXIT (parser, &event, error);

      switch (event.type)
        {
        case YAML_MAPPING_END_EVENT: done = TRUE; break;

        case YAML_SCALAR_EVENT:
          if (g_str_equal ((const gchar *)event.data.scalar.value, "context"))
            MMD_SET_PARSED_YAML_STRING (
              parser, error, modulemd_build_config_set_context, buildconfig);

          else if (g_str_equal ((const gchar *)event.data.scalar.value,
                                "platform"))
            MMD_SET_PARSED_YAML_STRING (
              parser, error, modulemd_build_config_set_platform, buildconfig);

          else if (g_str_equal ((const gchar *)event.data.scalar.value,
                                "buildrequires"))
            {
              deptable =
                modulemd_build_config_parse_deptable (parser, &nested_error);
              if (!deptable)
                {
                  g_propagate_error (error, g_steal_pointer (&nested_error));
                  return FALSE;
                }
              modulemd_build_config_replace_buildtime_deps (buildconfig,
                                                            deptable);
              g_clear_pointer (&deptable, g_hash_table_unref);
            }

          else if (g_str_equal ((const gchar *)event.data.scalar.value,
                                "requires"))
            {
              deptable =
                modulemd_build_config_parse_deptable (parser, &nested_error);
              if (!deptable)
                {
                  g_propagate_error (error, g_steal_pointer (&nested_error));
                  return FALSE;
                }
              modulemd_build_config_replace_runtime_deps (buildconfig,
                                                          deptable);
              g_clear_pointer (&deptable, g_hash_table_unref);
            }

          else if (g_str_equal ((const gchar *)event.data.scalar.value,
                                "buildopts"))
            {
              buildopts =
                modulemd_buildopts_parse_yaml (parser, strict, &nested_error);
              if (!buildopts)
                {
                  g_propagate_error (error, g_steal_pointer (&nested_error));
                  return NULL;
                }

              modulemd_build_config_set_buildopts (buildconfig, buildopts);
              g_clear_object (&buildopts);
            }

          else
            {
              SKIP_UNKNOWN (parser,
                            FALSE,
                            "Unexpected key in build config: %s",
                            (const gchar *)event.data.scalar.value);
              break;
            }

          break;

        default:
          MMD_YAML_ERROR_EVENT_EXIT (
            error, event, "Unexpected YAML event in profile");
          break;
        }
      yaml_event_delete (&event);
    }

  /* Validate the input */
  if (!modulemd_build_config_validate (buildconfig, &nested_error))
    {
      g_propagate_error (error, g_steal_pointer (&nested_error));
      return NULL;
    }

  return g_steal_pointer (&buildconfig);
}

gboolean
modulemd_build_config_emit_deptable (GHashTable *deptable,
                                     const char *table_key,
                                     yaml_emitter_t *emitter,
                                     GError **error);

gboolean
modulemd_build_config_emit_yaml (ModulemdBuildConfig *self,
                                 yaml_emitter_t *emitter,
                                 GError **error)
{
  MODULEMD_INIT_TRACE ();
  int ret;
  g_autoptr (GError) nested_error = NULL;
  MMD_INIT_YAML_EVENT (event);

  ret = mmd_emitter_start_mapping (
    emitter, YAML_BLOCK_MAPPING_STYLE, &nested_error);
  if (!ret)
    {
      g_propagate_prefixed_error (error,
                                  g_steal_pointer (&nested_error),
                                  "Failed to start BuildConfig mapping: ");
      return FALSE;
    }

  EMIT_KEY_VALUE_STRING_IF_SET (emitter, error, "context", self->context);
  EMIT_KEY_VALUE_STRING_IF_SET (emitter, error, "platform", self->platform);

  if (!modulemd_build_config_emit_deptable (
        self->buildrequires, "buildrequires", emitter, error))
    {
      g_propagate_error (error, g_steal_pointer (&nested_error));
      return FALSE;
    }

  if (!modulemd_build_config_emit_deptable (
        self->requires, "requires", emitter, error))
    {
      g_propagate_error (error, g_steal_pointer (&nested_error));
      return FALSE;
    }

  if (self->buildopts != NULL)
    {
      EMIT_SCALAR (emitter, error, "buildopts");
      EMIT_MAPPING_START (emitter, error);
      if (!modulemd_buildopts_emit_yaml (
            self->buildopts, emitter, &nested_error))
        {
          g_propagate_prefixed_error (
            error,
            g_steal_pointer (&nested_error),
            "Failed to emit BuildConfig buildopts: ");
          return FALSE;
        }
      EMIT_MAPPING_END (emitter, error);
    }

  ret = mmd_emitter_end_mapping (emitter, &nested_error);
  if (!ret)
    {
      g_propagate_prefixed_error (error,
                                  g_steal_pointer (&nested_error),
                                  "Failed to end BuildConfig mapping");
      return FALSE;
    }
  return TRUE;
}

gboolean
modulemd_build_config_emit_deptable (GHashTable *deptable,
                                     const char *table_key,
                                     yaml_emitter_t *emitter,
                                     GError **error)
{
  MODULEMD_INIT_TRACE ();
  MMD_INIT_YAML_EVENT (event);
  g_autoptr (GError) nested_error = NULL;
  g_autoptr (GHashTable) nested_set = NULL;
  g_autoptr (GHashTable) stream_table = NULL;
  int ret;
  GHashTableIter iter;
  gpointer key;
  gpointer value;
  gchar *module_name;
  gchar *stream_name;

  if (deptable == NULL || g_hash_table_size (deptable) == 0)
    {
      return TRUE;
    }

  nested_set = g_hash_table_new_full (
    g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_hash_table_unref);

  g_hash_table_iter_init (&iter, deptable);
  while (g_hash_table_iter_next (&iter, &key, &value))
    {
      module_name = (gchar *)key;
      stream_name = (gchar *)value;

      /* stuff the stream name into a sub-table */
      stream_table =
        g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
      g_hash_table_add (stream_table, g_strdup (stream_name));

      g_hash_table_insert (
        nested_set, g_strdup (module_name), g_steal_pointer (&stream_table));
    }

  ret = mmd_emitter_scalar_string (emitter, table_key, &nested_error);
  if (!ret)
    {
      g_propagate_prefixed_error (error,
                                  g_steal_pointer (&nested_error),
                                  "Failed to emit %s dependencies key: ",
                                  table_key);
      return FALSE;
    }

  ret = modulemd_yaml_emit_nested_set (emitter, nested_set, &nested_error);
  if (!ret)
    {
      g_propagate_prefixed_error (error,
                                  g_steal_pointer (&nested_error),
                                  "Failed to emit %s dependencies values: ",
                                  table_key);
      return FALSE;
    }

  g_clear_pointer (&nested_set, g_hash_table_unref);
  return TRUE;
}

static void
modulemd_build_config_replace_runtime_deps (ModulemdBuildConfig *self,
                                            GHashTable *deps)
{
  g_return_if_fail (MODULEMD_IS_BUILD_CONFIG (self));

  if (deps)
    {
      g_clear_pointer (&self->requires, g_hash_table_unref);
      self->requires = modulemd_hash_table_deep_str_copy (deps);
    }
  else
    {
      g_hash_table_remove_all (self->requires);
    }
}


static void
modulemd_build_config_replace_buildtime_deps (ModulemdBuildConfig *self,
                                              GHashTable *deps)
{
  g_return_if_fail (MODULEMD_IS_BUILD_CONFIG (self));

  if (deps)
    {
      g_clear_pointer (&self->buildrequires, g_hash_table_unref);
      self->buildrequires = modulemd_hash_table_deep_str_copy (deps);
    }
  else
    {
      g_hash_table_remove_all (self->buildrequires);
    }
}


gboolean
modulemd_build_config_validate (ModulemdBuildConfig *self, GError **error)
{
  gsize i;

  /* Context must be present and be between 1 and MMD_MAXCONTEXTLEN
   * alphanumeric characters
   */
  if (self->context == NULL || self->context[0] == '\0')
    {
      g_set_error (error,
                   MODULEMD_ERROR,
                   MMD_ERROR_VALIDATE,
                   "Empty context in BuildConfig");
      return FALSE;
    }

  for (i = 0; i < MMD_MAXCONTEXTLEN; i++)
    {
      if (self->context[i] == '\0')
        break;

      if (!(g_ascii_isalnum (self->context[i])))
        {
          g_set_error (error,
                       MODULEMD_ERROR,
                       MMD_ERROR_VALIDATE,
                       "Non-alphanumeric character in BuildConfig context");
          return FALSE;
        }
    }

  if (self->context[i] != '\0')
    {
      /* We passed the maximum length without encountering a
       * NULL-terminator
       */
      g_set_error (error,
                   MODULEMD_ERROR,
                   MMD_ERROR_VALIDATE,
                   "BuildConfig context exceeds maximum characters");
      return FALSE;
    }

  /* The platform value must be set.
   * In the future, we should probably validate its contents, but this is
   * not currently defined in the specification.
   */

  if (!self->platform)
    {
      g_set_error (error,
                   MODULEMD_ERROR,
                   MMD_ERROR_VALIDATE,
                   "Unset platform in BuildConfig");
      return FALSE;
    }

  return TRUE;
}


ModulemdBuildConfig *
modulemd_build_config_copy (ModulemdBuildConfig *self)
{
  g_autoptr (ModulemdBuildConfig) copy = modulemd_build_config_new ();

  modulemd_build_config_set_context (copy,
                                     modulemd_build_config_get_context (self));
  modulemd_build_config_set_platform (
    copy, modulemd_build_config_get_platform (self));

  if (self->requires)
    {
      modulemd_build_config_replace_runtime_deps (copy, self->requires);
    }

  if (self->buildrequires)
    {
      modulemd_build_config_replace_buildtime_deps (copy, self->buildrequires);
    }

  modulemd_build_config_set_buildopts (
    copy, modulemd_build_config_get_buildopts (self));

  return g_steal_pointer (&copy);
}


gboolean
modulemd_build_config_equals (ModulemdBuildConfig *self_1,
                              ModulemdBuildConfig *self_2)
{
  if (!self_1 && !self_2)
    {
      return TRUE;
    }

  if (!self_1 || !self_2)
    {
      return FALSE;
    }

  g_return_val_if_fail (MODULEMD_IS_BUILD_CONFIG (self_1), FALSE);
  g_return_val_if_fail (MODULEMD_IS_BUILD_CONFIG (self_2), FALSE);

  if (g_strcmp0 (self_1->context, self_2->context) != 0)
    {
      return FALSE;
    }

  if (g_strcmp0 (self_1->platform, self_2->platform) != 0)
    {
      return FALSE;
    }

  if (!modulemd_hash_table_equals (
        self_1->requires, self_2->requires, g_str_equal))
    {
      return FALSE;
    }

  if (!modulemd_hash_table_equals (
        self_1->buildrequires, self_2->buildrequires, g_str_equal))
    {
      return FALSE;
    }

  if (!modulemd_buildopts_equals (self_1->buildopts, self_2->buildopts))
    {
      return FALSE;
    }

  return TRUE;
}


/* return less than zero if first arg is less than second arg,
 * zero for equal,
 * greater than zero if first arg is greater than second arg.
 */
gint
modulemd_build_config_compare (ModulemdBuildConfig *self_1,
                               ModulemdBuildConfig *self_2)
{
  gint cmp;

  if (!self_1 && !self_2)
    {
      return 0;
    }

  if (!self_1)
    {
      return -1;
    }

  if (!self_2)
    {
      return 1;
    }

  g_return_val_if_fail (MODULEMD_IS_BUILD_CONFIG (self_1), 1);
  g_return_val_if_fail (MODULEMD_IS_BUILD_CONFIG (self_2), -1);

  cmp = g_strcmp0 (self_1->context, self_2->context);
  if (cmp != 0)
    {
      return cmp;
    }

  cmp = g_strcmp0 (self_1->platform, self_2->platform);
  if (cmp != 0)
    {
      return cmp;
    }

  cmp = modulemd_hash_table_compare (
    self_1->buildrequires, self_2->buildrequires, modulemd_strcmp_wrapper);
  if (cmp != 0)
    {
      return cmp;
    }

  cmp = modulemd_hash_table_compare (
    self_1->requires, self_2->requires, modulemd_strcmp_wrapper);
  if (cmp != 0)
    {
      return cmp;
    }

  cmp = modulemd_buildopts_compare (self_1->buildopts, self_2->buildopts);
  if (cmp != 0)
    {
      return cmp;
    }

  return 0;
}
