/* ide-diagnostic.c
 *
 * Copyright 2018-2019 Christian Hergert <chergert@redhat.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#define G_LOG_DOMAIN "ide-diagnostic"

#include "config.h"

#include "ide-code-enums.h"
#include "ide-diagnostic.h"
#include "ide-location.h"
#include "ide-range.h"
#include "ide-text-edit.h"

typedef struct
{
  IdeDiagnosticSeverity  severity;
  guint                  hash;
  gchar                 *text;
  IdeLocation           *location;
  GPtrArray             *ranges;
  GPtrArray             *fixits;
} IdeDiagnosticPrivate;

enum {
  PROP_0,
  PROP_LOCATION,
  PROP_SEVERITY,
  PROP_TEXT,
  PROP_DISPLAY_TEXT,
  N_PROPS
};

G_DEFINE_TYPE_WITH_PRIVATE (IdeDiagnostic, ide_diagnostic, IDE_TYPE_OBJECT)

static GParamSpec *properties [N_PROPS];

static void
ide_diagnostic_set_location (IdeDiagnostic *self,
                             IdeLocation   *location)
{
  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);

  g_return_if_fail (IDE_IS_DIAGNOSTIC (self));

  g_set_object (&priv->location, location);
}

static void
ide_diagnostic_set_text (IdeDiagnostic *self,
                         const gchar   *text)
{
  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);

  g_return_if_fail (IDE_IS_DIAGNOSTIC (self));

  priv->text = g_strdup (text);
}

static void
ide_diagnostic_set_severity (IdeDiagnostic         *self,
                             IdeDiagnosticSeverity  severity)
{
  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);

  g_return_if_fail (IDE_IS_DIAGNOSTIC (self));

  priv->severity = severity;
}

static void
ide_diagnostic_finalize (GObject *object)
{
  IdeDiagnostic *self = (IdeDiagnostic *)object;
  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);

  g_clear_pointer (&priv->text, g_free);
  g_clear_pointer (&priv->ranges, g_ptr_array_unref);
  g_clear_pointer (&priv->fixits, g_ptr_array_unref);
  g_clear_object (&priv->location);

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

static void
ide_diagnostic_get_property (GObject    *object,
                             guint       prop_id,
                             GValue     *value,
                             GParamSpec *pspec)
{
  IdeDiagnostic *self = IDE_DIAGNOSTIC (object);

  switch (prop_id)
    {
    case PROP_LOCATION:
      g_value_set_object (value, ide_diagnostic_get_location (self));
      break;

    case PROP_SEVERITY:
      g_value_set_enum (value, ide_diagnostic_get_severity (self));
      break;

    case PROP_DISPLAY_TEXT:
      g_value_take_string (value, ide_diagnostic_get_text_for_display (self));
      break;

    case PROP_TEXT:
      g_value_set_string (value, ide_diagnostic_get_text (self));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
ide_diagnostic_set_property (GObject      *object,
                             guint         prop_id,
                             const GValue *value,
                             GParamSpec   *pspec)
{
  IdeDiagnostic *self = IDE_DIAGNOSTIC (object);

  switch (prop_id)
    {
    case PROP_LOCATION:
      ide_diagnostic_set_location (self, g_value_get_object (value));
      break;

    case PROP_SEVERITY:
      ide_diagnostic_set_severity (self, g_value_get_enum (value));
      break;

    case PROP_TEXT:
      ide_diagnostic_set_text (self, g_value_get_string (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
ide_diagnostic_class_init (IdeDiagnosticClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = ide_diagnostic_finalize;
  object_class->get_property = ide_diagnostic_get_property;
  object_class->set_property = ide_diagnostic_set_property;

  properties [PROP_LOCATION] =
    g_param_spec_object ("location",
                         "Location",
                         "The location of the diagnostic",
                         IDE_TYPE_LOCATION,
                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  properties [PROP_SEVERITY] =
    g_param_spec_enum ("severity",
                       "Severity",
                       "The severity of the diagnostic",
                       IDE_TYPE_DIAGNOSTIC_SEVERITY,
                       IDE_DIAGNOSTIC_IGNORED,
                       (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  properties [PROP_TEXT] =
    g_param_spec_string ("text",
                         "Text",
                         "The text of the diagnostic",
                         NULL,
                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  properties [PROP_DISPLAY_TEXT] =
    g_param_spec_string ("display-text",
                         "Display Text",
                         "The text formatted for display",
                         NULL,
                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_properties (object_class, N_PROPS, properties);
}

static void
ide_diagnostic_init (IdeDiagnostic *self)
{
}

/**
 * ide_diagnostic_get_location:
 * @self: a #IdeDiagnostic
 *
 * Gets the location of the diagnostic.
 *
 * See also: ide_diagnostic_get_range().
 *
 * Returns: (transfer none) (nullable): an #IdeLocation or %NULL
 *
 * Since: 3.32
 */
IdeLocation *
ide_diagnostic_get_location (IdeDiagnostic *self)
{
  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);

  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), NULL);

  if (priv->location != NULL)
    return priv->location;

  if (priv->ranges != NULL && priv->ranges->len > 0)
    {
      IdeRange *range = g_ptr_array_index (priv->ranges, 0);
      return ide_range_get_begin (range);
    }

  return NULL;
}

/**
 * ide_diagnostic_get_file:
 * @self: a #IdeDiagnostic
 *
 * Gets the file containing the diagnostic, if any.
 *
 * Returns: (transfer none) (nullable): an #IdeLocation or %NULL
 *
 * Since: 3.32
 */
GFile *
ide_diagnostic_get_file (IdeDiagnostic *self)
{
  IdeLocation *location;

  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), NULL);

  if ((location = ide_diagnostic_get_location (self)))
    return ide_location_get_file (location);

  return NULL;
}

/**
 * ide_diagnostic_get_text_for_display:
 * @self: an #IdeDiagnostic
 *
 * This creates a new string that is formatted using the diagnostics
 * line number, column, severity, and message text in the format
 * "line:column: severity: message".
 *
 * This can be convenient when wanting to quickly display a
 * diagnostic such as in a tooltip.
 *
 * Returns: (transfer full): string containing the text formatted for
 *   display.
 *
 * Since: 3.32
 */
gchar *
ide_diagnostic_get_text_for_display (IdeDiagnostic *self)
{
  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);
  IdeLocation *location;
  const gchar *severity;
  guint line = 0;
  guint column = 0;

  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), NULL);

  severity = ide_diagnostic_severity_to_string (priv->severity);
  location = ide_diagnostic_get_location (self);

  if (location != NULL)
    {
      line = ide_location_get_line (location) + 1;
      column = ide_location_get_line_offset (location) + 1;
    }

  return g_strdup_printf ("%u:%u: %s: %s", line, column, severity, priv->text);
}

/**
 * ide_diagnostic_severity_to_string:
 * @severity: a #IdeDiagnosticSeverity
 *
 * Returns a string suitable to represent the diagnsotic severity.
 *
 * Returns: a string
 *
 * Since: 3.32
 */
const gchar *
ide_diagnostic_severity_to_string (IdeDiagnosticSeverity severity)
{
  switch (severity)
    {
    case IDE_DIAGNOSTIC_IGNORED:
      return "ignored";

    case IDE_DIAGNOSTIC_NOTE:
      return "note";

    case IDE_DIAGNOSTIC_UNUSED:
      return "unused";

    case IDE_DIAGNOSTIC_DEPRECATED:
      return "deprecated";

    case IDE_DIAGNOSTIC_WARNING:
      return "warning";

    case IDE_DIAGNOSTIC_ERROR:
      return "error";

    case IDE_DIAGNOSTIC_FATAL:
      return "fatal";

    default:
      return "unknown";
    }
}

guint
ide_diagnostic_get_n_ranges (IdeDiagnostic *self)
{
  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);

  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), 0);

  return priv->ranges ? priv->ranges->len : 0;
}

/**
 * ide_diagnostic_get_range:
 *
 * Retrieves the range found at @index. It is a programming error to call this
 * function with a value greater or equal to ide_diagnostic_get_n_ranges().
 *
 * Returns: (transfer none) (nullable): An #IdeRange
 *
 * Since: 3.32
 */
IdeRange *
ide_diagnostic_get_range (IdeDiagnostic *self,
                          guint          index)
{
  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);

  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), NULL);

  if (priv->ranges)
    {
      if (index < priv->ranges->len)
        return g_ptr_array_index (priv->ranges, index);
    }

  return NULL;
}

guint
ide_diagnostic_get_n_fixits (IdeDiagnostic *self)
{
  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);

  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), 0);

  return (priv->fixits != NULL) ? priv->fixits->len : 0;
}

/**
 * ide_diagnostic_get_fixit:
 * @self: an #IdeDiagnostic.
 * @index: The index of the fixit.
 *
 * Gets the fixit denoted by @index. This value should be less than the value
 * returned from ide_diagnostic_get_n_fixits().
 *
 * Returns: (transfer none) (nullable): An #IdeTextEdit
 *
 * Since: 3.32
 */
IdeTextEdit *
ide_diagnostic_get_fixit (IdeDiagnostic *self,
                          guint          index)
{
  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);

  g_return_val_if_fail (self, NULL);
  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), NULL);
  g_return_val_if_fail (priv->fixits, NULL);

  if (priv->fixits != NULL)
    {
      if (index < priv->fixits->len)
        return g_ptr_array_index (priv->fixits, index);
    }

  return NULL;
}

gint
ide_diagnostic_compare (IdeDiagnostic *a,
                        IdeDiagnostic *b)
{
  IdeDiagnosticPrivate *priv_a = ide_diagnostic_get_instance_private (a);
  IdeDiagnosticPrivate *priv_b = ide_diagnostic_get_instance_private (b);
  gint ret;

  g_assert (IDE_IS_DIAGNOSTIC (a));
  g_assert (IDE_IS_DIAGNOSTIC (b));

  /* Severity is 0..N where N is more important. So reverse comparator. */
  if (0 != (ret = (gint)priv_b->severity - (gint)priv_a->severity))
    return ret;

  if (priv_a->location && priv_b->location)
    {
      if (0 != (ret = ide_location_compare (priv_a->location, priv_b->location)))
        return ret;
    }

  return g_strcmp0 (priv_a->text, priv_b->text);
}

const gchar *
ide_diagnostic_get_text (IdeDiagnostic *self)
{
  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);

  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), NULL);

  return priv->text;
}

IdeDiagnosticSeverity
ide_diagnostic_get_severity (IdeDiagnostic *self)
{
  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);

  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), 0);

  return priv->severity;
}

/**
 * ide_diagnostic_add_range:
 * @self: a #IdeDiagnostic
 * @range: an #IdeRange
 *
 * Adds a source range to the diagnostic.
 *
 * Since: 3.32
 */
void
ide_diagnostic_add_range (IdeDiagnostic *self,
                          IdeRange      *range)
{
  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);

  g_return_if_fail (IDE_IS_DIAGNOSTIC (self));
  g_return_if_fail (IDE_IS_RANGE (range));

  if (priv->ranges == NULL)
    priv->ranges = g_ptr_array_new_with_free_func (g_object_unref);
  g_ptr_array_add (priv->ranges, g_object_ref (range));
}

/**
 * ide_diagnostic_take_range:
 * @self: a #IdeDiagnostic
 * @range: (transfer full): an #IdeRange
 *
 * Adds a source range to the diagnostic, but does not increment the
 * reference count of @range.
 *
 * Since: 3.32
 */
void
ide_diagnostic_take_range (IdeDiagnostic *self,
                           IdeRange      *range)
{
  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);

  g_return_if_fail (IDE_IS_DIAGNOSTIC (self));
  g_return_if_fail (IDE_IS_RANGE (range));

  if (priv->ranges == NULL)
    priv->ranges = g_ptr_array_new_with_free_func (g_object_unref);
  g_ptr_array_add (priv->ranges, g_steal_pointer (&range));
}

/**
 * ide_diagnostic_add_fixit:
 * @self: a #IdeDiagnostic
 * @fixit: an #IdeTextEdit
 *
 * Adds a source fixit to the diagnostic.
 *
 * Since: 3.32
 */
void
ide_diagnostic_add_fixit (IdeDiagnostic *self,
                          IdeTextEdit   *fixit)
{
  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);

  g_return_if_fail (IDE_IS_DIAGNOSTIC (self));
  g_return_if_fail (IDE_IS_TEXT_EDIT (fixit));

  if (priv->fixits == NULL)
    priv->fixits = g_ptr_array_new_with_free_func (g_object_unref);
  g_ptr_array_add (priv->fixits, g_object_ref (fixit));
}

/**
 * ide_diagnostic_take_fixit:
 * @self: a #IdeDiagnostic
 * @fixit: (transfer full): an #IdeTextEdit
 *
 * Adds a source fixit to the diagnostic, but does not increment the
 * reference count of @fixit.
 *
 * Since: 3.32
 */
void
ide_diagnostic_take_fixit (IdeDiagnostic *self,
                           IdeTextEdit   *fixit)
{
  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);

  g_return_if_fail (IDE_IS_DIAGNOSTIC (self));
  g_return_if_fail (IDE_IS_TEXT_EDIT (fixit));

  if (priv->fixits == NULL)
    priv->fixits = g_ptr_array_new_with_free_func (g_object_unref);
  g_ptr_array_add (priv->fixits, g_steal_pointer (&fixit));
}

IdeDiagnostic *
ide_diagnostic_new (IdeDiagnosticSeverity  severity,
                    const gchar           *message,
                    IdeLocation           *location)
{
  return g_object_new (IDE_TYPE_DIAGNOSTIC,
                       "severity", severity,
                       "location", location,
                       "text", message,
                       NULL);
}

guint
ide_diagnostic_hash (IdeDiagnostic *self)
{
  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);

  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), 0);

  if (priv->hash == 0)
    {
      guint hash = g_str_hash (priv->text ?: "");
      if (priv->location)
        hash ^= ide_location_hash (priv->location);
      if (priv->fixits)
        hash ^= g_int_hash (&priv->fixits->len);
      if (priv->ranges)
        hash ^= g_int_hash (&priv->ranges->len);
      priv->hash = hash;
    }

  return priv->hash;
}

/**
 * ide_diagnostic_to_variant:
 * @self: a #IdeDiagnostic
 *
 * Creates a #GVariant to represent the diagnostic. This can be useful when
 * working in subprocesses to serialize the diagnostic.
 *
 * This function will never return a floating variant.
 *
 * Returns: (transfer full): a #GVariant
 *
 * Since: 3.32
 */
GVariant *
ide_diagnostic_to_variant (IdeDiagnostic *self)
{
  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);
  GVariantDict dict;

  g_return_val_if_fail (self != NULL, NULL);
  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), NULL);

  g_variant_dict_init (&dict, NULL);

  g_variant_dict_insert (&dict, "text", "s", priv->text ?: "");
  g_variant_dict_insert (&dict, "severity", "u", priv->severity);

  if (priv->location != NULL)
    {
      g_autoptr(GVariant) vloc = ide_location_to_variant (priv->location);

      if (vloc != NULL)
        g_variant_dict_insert_value (&dict, "location", vloc);
    }

  if (priv->ranges != NULL && priv->ranges->len > 0)
    {
      GVariantBuilder builder;

      g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}"));

      for (guint i = 0; i < priv->ranges->len; i++)
        {
          IdeRange *range = g_ptr_array_index (priv->ranges, i);
          g_autoptr(GVariant) vrange = ide_range_to_variant (range);

          g_variant_builder_add_value (&builder, vrange);
        }

      g_variant_dict_insert_value (&dict, "ranges", g_variant_builder_end (&builder));
    }

  if (priv->fixits != NULL && priv->fixits->len > 0)
    {
      GVariantBuilder builder;

      g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}"));

      for (guint i = 0; i < priv->fixits->len; i++)
        {
          IdeTextEdit *fixit = g_ptr_array_index (priv->fixits, i);
          g_autoptr(GVariant) vfixit = ide_text_edit_to_variant (fixit);

          g_variant_builder_add_value (&builder, vfixit);
        }

      g_variant_dict_insert_value (&dict, "fixits", g_variant_builder_end (&builder));
    }

  return g_variant_take_ref (g_variant_dict_end (&dict));
}

/**
 * ide_diagnostic_new_from_variant:
 * @variant: (nullable): a #GVariant or %NULL
 *
 * Creates a new #GVariant using the data contained in @variant.
 *
 * If @variant is %NULL or Upon failure, %NULL is returned.
 *
 * Returns: (nullable) (transfer full): a #IdeDiagnostic or %NULL
 *
 * Since: 3.32
 */
IdeDiagnostic *
ide_diagnostic_new_from_variant (GVariant *variant)
{
  g_autoptr(IdeLocation) loc = NULL;
  g_autoptr(GVariant) vloc = NULL;
  g_autoptr(GVariant) unboxed = NULL;
  g_autoptr(GVariant) ranges = NULL;
  g_autoptr(GVariant) fixits = NULL;
  IdeDiagnostic *self;
  GVariantDict dict;
  GVariantIter iter;
  const gchar *text;
  guint32 severity;

  if (variant == NULL)
    return NULL;

  if (g_variant_is_of_type (variant, G_VARIANT_TYPE_VARIANT))
    variant = unboxed = g_variant_get_variant (variant);

  if (!g_variant_is_of_type (variant, G_VARIANT_TYPE_VARDICT))
    return NULL;

  g_variant_dict_init (&dict, variant);

  if (!g_variant_dict_lookup (&dict, "text", "&s", &text))
    text = NULL;

  if (!g_variant_dict_lookup (&dict, "severity", "u", &severity))
    severity = 0;

  if ((vloc = g_variant_dict_lookup_value (&dict, "location", NULL)))
    loc = ide_location_new_from_variant (vloc);

  if (!(self = ide_diagnostic_new (severity, text, loc)))
    goto failure;

  /* Ranges */
  if ((ranges = g_variant_dict_lookup_value (&dict, "ranges", NULL)))
    {
      GVariant *vrange;

      g_variant_iter_init (&iter, ranges);

      while ((vrange = g_variant_iter_next_value (&iter)))
        {
          IdeRange *range;

          if ((range = ide_range_new_from_variant (vrange)))
            ide_diagnostic_take_range (self, g_steal_pointer (&range));

          g_variant_unref (vrange);
        }
    }

  /* Fixits */
  if ((fixits = g_variant_dict_lookup_value (&dict, "fixits", NULL)))
    {
      GVariant *vfixit;

      g_variant_iter_init (&iter, fixits);

      while ((vfixit = g_variant_iter_next_value (&iter)))
        {
          IdeTextEdit *fixit;

          if ((fixit = ide_text_edit_new_from_variant (vfixit)))
            ide_diagnostic_take_fixit (self, g_steal_pointer (&fixit));

          g_variant_unref (vfixit);
        }
    }

failure:

  g_variant_dict_clear (&dict);

  return self;
}
