/*
 * Copyright © 2023 Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: Matthias Clasen <mclasen@redhat.com>
 */

#include "config.h"

#include "a11yoverlay.h"

#include "gtkwidget.h"
#include "gtkroot.h"
#include "gtknative.h"
#include "gtkwidgetprivate.h"
#include "gtkatcontextprivate.h"
#include "gtkaccessibleprivate.h"
#include "gtktypebuiltins.h"

struct _GtkA11yOverlay
{
  GtkInspectorOverlay parent_instance;

  GdkRGBA recommend_color;
  GdkRGBA error_color;

  GArray *context;
};

struct _GtkA11yOverlayClass
{
  GtkInspectorOverlayClass parent_class;
};

G_DEFINE_TYPE (GtkA11yOverlay, gtk_a11y_overlay, GTK_TYPE_INSPECTOR_OVERLAY)

typedef enum
{
  FIX_SEVERITY_GOOD,
  FIX_SEVERITY_RECOMMENDATION,
  FIX_SEVERITY_ERROR
} FixSeverity;

typedef enum
{
  ATTRIBUTE_STATE,
  ATTRIBUTE_PROPERTY,
  ATTRIBUTE_RELATION
} AttributeType;

static struct {
  GtkAccessibleRole role;
  AttributeType type;
  int id;
} required_attributes[] = {
  { GTK_ACCESSIBLE_ROLE_CHECKBOX, ATTRIBUTE_STATE, GTK_ACCESSIBLE_STATE_CHECKED },
  { GTK_ACCESSIBLE_ROLE_COMBO_BOX, ATTRIBUTE_STATE, GTK_ACCESSIBLE_STATE_EXPANDED },
  { GTK_ACCESSIBLE_ROLE_COMBO_BOX, ATTRIBUTE_RELATION, GTK_ACCESSIBLE_RELATION_CONTROLS },
  { GTK_ACCESSIBLE_ROLE_HEADING, ATTRIBUTE_PROPERTY, GTK_ACCESSIBLE_PROPERTY_LEVEL },
  { GTK_ACCESSIBLE_ROLE_SCROLLBAR, ATTRIBUTE_RELATION, GTK_ACCESSIBLE_RELATION_CONTROLS },
  { GTK_ACCESSIBLE_ROLE_SCROLLBAR, ATTRIBUTE_PROPERTY, GTK_ACCESSIBLE_PROPERTY_VALUE_NOW },
  { GTK_ACCESSIBLE_ROLE_SWITCH, ATTRIBUTE_STATE, GTK_ACCESSIBLE_STATE_CHECKED },
};

static struct {
  GtkAccessibleRole role;
  GtkAccessibleRole context;
} required_context[] = {
  { GTK_ACCESSIBLE_ROLE_CAPTION, GTK_ACCESSIBLE_ROLE_GRID },
  { GTK_ACCESSIBLE_ROLE_CAPTION, GTK_ACCESSIBLE_ROLE_TABLE },
  { GTK_ACCESSIBLE_ROLE_CAPTION, GTK_ACCESSIBLE_ROLE_TREE_GRID },
  { GTK_ACCESSIBLE_ROLE_CELL, GTK_ACCESSIBLE_ROLE_ROW },
  { GTK_ACCESSIBLE_ROLE_COLUMN_HEADER, GTK_ACCESSIBLE_ROLE_ROW },
  { GTK_ACCESSIBLE_ROLE_GRID_CELL, GTK_ACCESSIBLE_ROLE_ROW },
  { GTK_ACCESSIBLE_ROLE_LIST_ITEM, GTK_ACCESSIBLE_ROLE_LIST },
  { GTK_ACCESSIBLE_ROLE_MENU_ITEM, GTK_ACCESSIBLE_ROLE_GROUP },
  { GTK_ACCESSIBLE_ROLE_MENU_ITEM, GTK_ACCESSIBLE_ROLE_MENU },
  { GTK_ACCESSIBLE_ROLE_MENU_ITEM, GTK_ACCESSIBLE_ROLE_MENU_BAR },
  { GTK_ACCESSIBLE_ROLE_MENU_ITEM_CHECKBOX, GTK_ACCESSIBLE_ROLE_GROUP },
  { GTK_ACCESSIBLE_ROLE_MENU_ITEM_CHECKBOX, GTK_ACCESSIBLE_ROLE_MENU },
  { GTK_ACCESSIBLE_ROLE_MENU_ITEM_CHECKBOX, GTK_ACCESSIBLE_ROLE_MENU_BAR },
  { GTK_ACCESSIBLE_ROLE_MENU_ITEM_RADIO, GTK_ACCESSIBLE_ROLE_GROUP },
  { GTK_ACCESSIBLE_ROLE_MENU_ITEM_RADIO, GTK_ACCESSIBLE_ROLE_MENU },
  { GTK_ACCESSIBLE_ROLE_MENU_ITEM_RADIO, GTK_ACCESSIBLE_ROLE_MENU_BAR },
  { GTK_ACCESSIBLE_ROLE_OPTION, GTK_ACCESSIBLE_ROLE_GROUP },
  { GTK_ACCESSIBLE_ROLE_OPTION, GTK_ACCESSIBLE_ROLE_LIST_BOX },
  { GTK_ACCESSIBLE_ROLE_ROW, GTK_ACCESSIBLE_ROLE_GRID },
  { GTK_ACCESSIBLE_ROLE_ROW, GTK_ACCESSIBLE_ROLE_ROW_GROUP },
  { GTK_ACCESSIBLE_ROLE_ROW, GTK_ACCESSIBLE_ROLE_TABLE },
  { GTK_ACCESSIBLE_ROLE_ROW, GTK_ACCESSIBLE_ROLE_TREE_GRID },
  { GTK_ACCESSIBLE_ROLE_ROW_GROUP, GTK_ACCESSIBLE_ROLE_GRID },
  { GTK_ACCESSIBLE_ROLE_ROW_GROUP, GTK_ACCESSIBLE_ROLE_TABLE },
  { GTK_ACCESSIBLE_ROLE_ROW_GROUP, GTK_ACCESSIBLE_ROLE_TREE_GRID },
  { GTK_ACCESSIBLE_ROLE_ROW_HEADER, GTK_ACCESSIBLE_ROLE_ROW },
  { GTK_ACCESSIBLE_ROLE_TAB, GTK_ACCESSIBLE_ROLE_TAB_LIST },
  { GTK_ACCESSIBLE_ROLE_TREE_ITEM, GTK_ACCESSIBLE_ROLE_GROUP },
  { GTK_ACCESSIBLE_ROLE_TREE_ITEM, GTK_ACCESSIBLE_ROLE_TREE },
};

static FixSeverity
check_accessibility_errors (GtkATContext       *context,
                            GtkAccessibleRole   role,
                            GArray             *context_elements,
                            char              **hint)
{
  gboolean label_set;
  const char *role_name;
  GEnumClass *states;
  GEnumClass *properties;
  GEnumClass *relations;
  gboolean has_context;

  *hint = NULL;
  role_name = gtk_accessible_role_to_name (role, NULL);

  if (!gtk_at_context_is_realized (context))
    gtk_at_context_realize (context);

  /* Check for abstract roles */
  if (gtk_accessible_role_is_abstract (role))
    {
      *hint = g_strdup_printf ("%s is an abstract role", role_name);
      return FIX_SEVERITY_ERROR;
    }

  /* Check for name and description */
  label_set = gtk_at_context_has_accessible_property (context, GTK_ACCESSIBLE_PROPERTY_LABEL) ||
              gtk_at_context_has_accessible_relation (context, GTK_ACCESSIBLE_RELATION_LABELLED_BY);

  switch (gtk_accessible_role_get_naming (role))
    {
    case GTK_ACCESSIBLE_NAME_ALLOWED:
      break;

    case GTK_ACCESSIBLE_NAME_REQUIRED:
      if (!label_set)
        {
          if (gtk_accessible_role_supports_name_from_author (role))
            {
              char *name = gtk_at_context_get_name (context);

              if (strcmp (name, "") == 0)
                {
                  g_free (name);
                  *hint = g_strdup_printf ("%s must have text content or label", role_name);

                  return FIX_SEVERITY_ERROR;
                }
            }
          else
            {
              *hint = g_strdup_printf ("%s must have label", role_name);

              return FIX_SEVERITY_ERROR;
            }
        }
      break;

    case GTK_ACCESSIBLE_NAME_PROHIBITED:
      if (label_set)
        {
          *hint = g_strdup_printf ("%s can't have label", role_name);

          return FIX_SEVERITY_ERROR;
        }
      break;

    case GTK_ACCESSIBLE_NAME_RECOMMENDED:
      if (!label_set)
        {
          *hint = g_strdup_printf ("label recommended for %s", role_name);

          return FIX_SEVERITY_RECOMMENDATION;
        }
      break;

    case GTK_ACCESSIBLE_NAME_NOT_RECOMMENDED:
      if (label_set)
        {
          *hint = g_strdup_printf ("label not recommended for %s", role_name);

          return FIX_SEVERITY_RECOMMENDATION;
        }
      break;

    default:
      g_assert_not_reached ();
    }

  /* Check for required attributes */
  states = g_type_class_peek (GTK_TYPE_ACCESSIBLE_STATE);
  properties = g_type_class_peek (GTK_TYPE_ACCESSIBLE_PROPERTY);
  relations = g_type_class_peek (GTK_TYPE_ACCESSIBLE_RELATION);

  for (unsigned int i = 0; i < G_N_ELEMENTS (required_attributes); i++)
    {
      if (role == required_attributes[i].role)
        {
          switch (required_attributes[i].type)
            {
            case ATTRIBUTE_STATE:
              if (!gtk_at_context_has_accessible_state (context, required_attributes[i].id))
                {
                  *hint = g_strdup_printf ("%s must have state %s", role_name, g_enum_get_value (states, required_attributes[i].id)->value_nick);
                  return FIX_SEVERITY_ERROR;
                }
              break;

            case ATTRIBUTE_PROPERTY:
              if (!gtk_at_context_has_accessible_property (context, required_attributes[i].id))
                {
                  *hint = g_strdup_printf ("%s must have property %s", role_name, g_enum_get_value (properties, required_attributes[i].id)->value_nick);
                  return FIX_SEVERITY_ERROR;
                }
              break;

            case ATTRIBUTE_RELATION:
              if (!gtk_at_context_has_accessible_relation (context, required_attributes[i].id))
                {
                  *hint = g_strdup_printf ("%s must have relation %s", role_name, g_enum_get_value (relations, required_attributes[i].id)->value_nick);
                  return FIX_SEVERITY_ERROR;
                }
              break;

            default:
              g_assert_not_reached ();
            }
        }
    }

  /* Check for required context */
  has_context = TRUE;

  for (unsigned int i = 0; i < G_N_ELEMENTS (required_context); i++)
    {
      if (required_context[i].role != role)
        continue;

      has_context = FALSE;

      for (unsigned int j = 0; j < context_elements->len; j++)
        {
          GtkAccessibleRole elt = g_array_index (context_elements, GtkAccessibleRole, j);

          if (required_context[i].context == elt)
            {
              has_context = TRUE;
              break;
            }
        }

      if (has_context)
        break;
    }

  if (!has_context)
    {
      GString *s = g_string_new ("");

      for (unsigned int i = 0; i < G_N_ELEMENTS (required_context); i++)
        {
          if (required_context[i].role != role)
            continue;

          if (s->len > 0)
            g_string_append (s, ", ");

          g_string_append (s, gtk_accessible_role_to_name (required_context[i].context, NULL));
        }

      *hint = g_strdup_printf ("%s requires context: %s", role_name, s->str);

      g_string_free (s, TRUE);

      return FIX_SEVERITY_ERROR;
    }

  if (role == GTK_ACCESSIBLE_ROLE_BUTTON)
    {
      GtkAccessible *accessible = gtk_at_context_get_accessible (context);

      if (GTK_IS_WIDGET (accessible))
        {
          int width = gtk_widget_get_width (GTK_WIDGET (accessible));
          int height = gtk_widget_get_height (GTK_WIDGET (accessible));

          if (width < 24 || height < 24)
            {
              *hint = g_strdup_printf ("Button is too small: %dx%d", width, height);
              return FIX_SEVERITY_ERROR;
            }
        }
    }

  return FIX_SEVERITY_GOOD;
}

static FixSeverity
check_widget_accessibility_errors (GtkWidget  *widget,
                                   GArray     *context_elements,
                                   char      **hint)
{
  GtkAccessibleRole role;
  GtkATContext *context;
  FixSeverity ret;

  role = gtk_accessible_get_accessible_role (GTK_ACCESSIBLE (widget));
  context = gtk_accessible_get_at_context (GTK_ACCESSIBLE (widget));
  ret = check_accessibility_errors (context, role, context_elements, hint);
  g_object_unref (context);

  return ret;
}

static void
center_over_within (graphene_rect_t       *rect,
                    const graphene_rect_t *over,
                    const graphene_rect_t *within)
{
  graphene_point_t center;

  graphene_rect_get_center (over, &center);

  rect->origin.x = CLAMP (center.x - 0.5 * rect->size.width, within->origin.x, within->origin.x + within->size.width - rect->size.width);
  rect->origin.y = CLAMP (center.y - 0.5 * rect->size.height, within->origin.y, within->origin.y + within->size.height - rect->size.height);
}

static void
recurse_child_widgets (GtkA11yOverlay *self,
                       GtkWidget      *widget,
                       GtkSnapshot    *snapshot)
{
  GtkWidget *child;
  char *hint;
  FixSeverity severity;
  GtkAccessibleRole role;

  if (!gtk_widget_get_mapped (widget))
    return;

  severity = check_widget_accessibility_errors (widget, self->context, &hint);

  if (severity != FIX_SEVERITY_GOOD)
    {
      int width, height;
      GdkRGBA color;

      width = gtk_widget_get_width (widget);
      height = gtk_widget_get_height (widget);

      if (severity == FIX_SEVERITY_ERROR)
        color = self->error_color;
      else
        color = self->recommend_color;

      gtk_snapshot_save (snapshot);
      gtk_snapshot_push_debug (snapshot, "Widget a11y debugging");

      gtk_snapshot_append_color (snapshot, &color,
                                 &GRAPHENE_RECT_INIT (0, 0, width, height));

      if (hint)
        {
          int toplevel_width, toplevel_height;
          PangoLayout *layout;
          PangoRectangle extents;
          GdkRGBA black = { 0, 0, 0, 1 };
          float widths[4] = { 1, 1, 1, 1 };
          GdkRGBA colors[4] = {
            { 0, 0, 0, 1 },
            { 0, 0, 0, 1 },
            { 0, 0, 0, 1 },
            { 0, 0, 0, 1 },
          };
          GtkNative *native;
          graphene_rect_t over, within, bounds;
          gboolean ret G_GNUC_UNUSED;

          native = gtk_widget_get_native (widget);
          toplevel_width = gtk_widget_get_width (GTK_WIDGET (native));
          toplevel_height = gtk_widget_get_height (GTK_WIDGET (native));

          gtk_snapshot_save (snapshot);

          layout = gtk_widget_create_pango_layout (widget, hint);
          pango_layout_set_width (layout, toplevel_width * PANGO_SCALE);

          pango_layout_get_pixel_extents (layout, NULL, &extents);

          extents.x -= 5;
          extents.y -= 5;
          extents.width += 10;
          extents.height += 10;

          ret = gtk_widget_compute_point (widget, GTK_WIDGET (native), &GRAPHENE_POINT_INIT (0, 0), &over.origin);
          over.size.width = width;
          over.size.height = height;

          graphene_rect_init (&within, 0, 0, toplevel_width, toplevel_height);

          graphene_rect_init (&bounds, 0, 0, extents.width, extents.height);
          center_over_within (&bounds, &over, &within);

          color.alpha = 0.8f;

          gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (bounds.origin.x - over.origin.x,
                                                                  bounds.origin.y - over.origin.y));

          gtk_snapshot_append_border (snapshot,
                                       &GSK_ROUNDED_RECT_INIT (0, 0,
                                                               extents.width, extents.height),
                                      widths, colors);
          gtk_snapshot_append_color (snapshot, &color,
                                     &GRAPHENE_RECT_INIT (0, 0,
                                                          extents.width, extents.height));

          gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (5, 5));
          gtk_snapshot_append_layout (snapshot, layout, &black);
          g_object_unref (layout);

          gtk_snapshot_restore (snapshot);
        }

      gtk_snapshot_pop (snapshot);
      gtk_snapshot_restore (snapshot);
   }

  g_free (hint);

  /* Recurse into child widgets */

  role = gtk_accessible_get_accessible_role (GTK_ACCESSIBLE (widget));
  g_array_append_val (self->context, role);

  for (child = gtk_widget_get_first_child (widget);
       child != NULL;
       child = gtk_widget_get_next_sibling (child))
    {
      gtk_snapshot_save (snapshot);
      gtk_snapshot_transform (snapshot, child->priv->transform);

      recurse_child_widgets (self, child, snapshot);

      gtk_snapshot_restore (snapshot);
    }

  g_array_remove_index (self->context, self->context->len - 1);
}

static void
gtk_a11y_overlay_snapshot (GtkInspectorOverlay *overlay,
                           GtkSnapshot         *snapshot,
                           GskRenderNode       *node,
                           GtkWidget           *widget)
{
  GtkA11yOverlay *self = GTK_A11Y_OVERLAY (overlay);

  g_assert (self->context->len == 0);

  recurse_child_widgets (self, widget, snapshot);

  g_assert (self->context->len == 0);
}

static void
gtk_a11y_overlay_finalize (GObject *object)
{
  GtkA11yOverlay *self = GTK_A11Y_OVERLAY (object);

  g_array_free (self->context, TRUE);

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

static void
gtk_a11y_overlay_class_init (GtkA11yOverlayClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkInspectorOverlayClass *overlay_class = GTK_INSPECTOR_OVERLAY_CLASS (klass);

  object_class->finalize = gtk_a11y_overlay_finalize;

  overlay_class->snapshot = gtk_a11y_overlay_snapshot;
}

static void
gtk_a11y_overlay_init (GtkA11yOverlay *self)
{
  self->recommend_color = (GdkRGBA) { 0.0, 0.5, 1.0, 0.2 };
  self->error_color = (GdkRGBA) { 1.0, 0.0, 0.0, 0.2 };

  self->context = g_array_new (FALSE, FALSE, sizeof (GtkAccessibleRole));
}

GtkInspectorOverlay *
gtk_a11y_overlay_new (void)
{
  GtkA11yOverlay *self;

  self = g_object_new (GTK_TYPE_A11Y_OVERLAY, NULL);

  return GTK_INSPECTOR_OVERLAY (self);
}
