/*
     This file is part of anastasis-gtk.
     Copyright (C) 2025 Anastasis SARL

     Anastasis 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, or (at your
     option) any later version.

     Anastasis 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 Anastasis; see the file COPYING.  If not, write to the
     Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
     Boston, MA 02110-1301, USA.
*/
/**
 * @file src/anastasis/anastasis-gtk_rows.c
 * @brief Rows of the GtkColumnViews and the models holding them
 * @author Christian Grothoff
 */
#include <gnunet/gnunet_util_lib.h>
#include "anastasis-gtk_helper.h"
#include "anastasis-gtk_rows.h"


/**
 * Key under which the row a cell shows is attached to the widget of that
 * cell.  This is how #AG_row_at() finds the row that was clicked.
 */
#define ROW_KEY "anastasis-row"


/**
 * One row of one of our lists.
 */
struct _AGRow
{
  /**
   * We are a GObject.
   */
  GObject parent_instance;

  /**
   * The values of this row.
   */
  json_t *fields;

  /**
   * Image to render this row with, or NULL.
   */
  GdkPaintable *image;

  /**
   * Rows nested under this one, or NULL if there are none.
   */
  GListStore *children;
};

G_DEFINE_TYPE (AGRow,
               AG_row,
               G_TYPE_OBJECT);


static void
AG_row_finalize (GObject *object)
{
  AGRow *row = AG_ROW (object);

  json_decref (row->fields);
  row->fields = NULL;
  g_clear_object (&row->image);
  g_clear_object (&row->children);
  G_OBJECT_CLASS (AG_row_parent_class)->finalize (object);
}


static void
AG_row_class_init (AGRowClass *klass)
{
  G_OBJECT_CLASS (klass)->finalize = &AG_row_finalize;
}


static void
AG_row_init (AGRow *row)
{
  row->fields = json_object ();
  GNUNET_assert (NULL != row->fields);
}


AGRow *
AG_row_new (json_t *fields)
{
  AGRow *row;

  GNUNET_assert (json_is_object (fields));
  row = g_object_new (AG_TYPE_ROW,
                      NULL);
  json_decref (row->fields);
  row->fields = fields;
  return row;
}


void
AG_row_set (AGRow *row,
            const char *field,
            json_t *value)
{
  GNUNET_assert (0 ==
                 json_object_set_new (row->fields,
                                      field,
                                      value));
}


const char *
AG_row_string (AGRow *row,
               const char *field)
{
  return json_string_value (json_object_get (row->fields,
                                             field));
}


bool
AG_row_bool (AGRow *row,
             const char *field)
{
  return json_boolean_value (json_object_get (row->fields,
                                              field));
}


uint64_t
AG_row_uint (AGRow *row,
             const char *field)
{
  return (uint64_t) json_integer_value (json_object_get (row->fields,
                                                         field));
}


json_t *
AG_row_json (AGRow *row,
             const char *field)
{
  return json_object_get (row->fields,
                          field);
}


void
AG_row_set_image (AGRow *row,
                  GdkPaintable *image)
{
  g_clear_object (&row->image);
  row->image = image;
}


GdkPaintable *
AG_row_image (AGRow *row)
{
  return row->image;
}


GListStore *
AG_row_children (AGRow *row)
{
  if (NULL == row->children)
    row->children = g_list_store_new (AG_TYPE_ROW);
  return row->children;
}


AGRow *
AG_row_of (gpointer item)
{
  if (NULL == item)
    return NULL;
  if (GTK_IS_TREE_LIST_ROW (item))
  {
    AGRow *row;

    row = gtk_tree_list_row_get_item (GTK_TREE_LIST_ROW (item));
    if (NULL == row)
      return NULL;
    /* we hand out a borrowed reference; the tree list row holds one of
       its own for as long as it exists */
    g_object_unref (row);
    return AG_ROW (row);
  }
  return AG_ROW (item);
}


GListStore *
AG_list_of (GtkBuilder *builder,
            const char *name)
{
  GObject *model;

  model = gtk_builder_get_object (builder,
                                  name);
  if (NULL == model)
  {
    GNUNET_break (0);
    return NULL;
  }
  return G_LIST_STORE (model);
}


GListStore *
AG_list (const char *name)
{
  GObject *model;

  model = GCG_get_main_window_object (name);
  if (NULL == model)
  {
    GNUNET_break (0);
    return NULL;
  }
  return G_LIST_STORE (model);
}


void
AG_list_clear (const char *name)
{
  GListStore *list = AG_list (name);

  if (NULL == list)
    return;
  g_list_store_remove_all (list);
}


void
AG_list_add_row (GListStore *list,
                 AGRow *row)
{
  g_list_store_append (list,
                       row);
  g_object_unref (row);
}


AGRow *
AG_list_append (GListStore *list,
                json_t *fields)
{
  AGRow *row = AG_row_new (fields);

  g_list_store_append (list,
                       row);
  g_object_unref (row);
  return row;
}


AGRow *
AG_list_add (const char *name,
             json_t *fields)
{
  GListStore *list = AG_list (name);

  if (NULL == list)
  {
    json_decref (fields);
    return NULL;
  }
  return AG_list_append (list,
                         fields);
}


AGRow *
AG_selected (GtkSingleSelection *selection)
{
  return AG_row_of (gtk_single_selection_get_selected_item (selection));
}


/**
 * Return the selection called @a name of the main window.
 *
 * @param name name of the selection in the UI file
 * @return NULL on error
 */
static GtkSingleSelection *
main_window_selection (const char *name)
{
  GObject *sel;

  sel = GCG_get_main_window_object (name);
  if (NULL == sel)
  {
    GNUNET_break (0);
    return NULL;
  }
  return GTK_SINGLE_SELECTION (sel);
}


AGRow *
AG_selected_row (const char *name)
{
  GtkSingleSelection *sel = main_window_selection (name);

  if (NULL == sel)
    return NULL;
  return AG_selected (sel);
}


void
AG_select_row (const char *name,
               guint position)
{
  GtkSingleSelection *sel = main_window_selection (name);

  if (NULL == sel)
    return;
  gtk_selection_model_select_item (GTK_SELECTION_MODEL (sel),
                                   position,
                                   TRUE);
}


/**
 * Search the descendants of @a widget for the cell of a row that covers
 * @a y in the coordinates of @a view.
 *
 * @param widget widget to search below
 * @param view the column view @a y refers to
 * @param y vertical position to look for
 * @return NULL if no cell of any row covers @a y
 */
static AGRow *
find_row_at (GtkWidget *widget,
             GtkWidget *view,
             double y)
{
  for (GtkWidget *child = gtk_widget_get_first_child (widget);
       NULL != child;
       child = gtk_widget_get_next_sibling (child))
  {
    AGRow *row;
    GtkWidget *cell;
    graphene_rect_t bounds;

    row = g_object_get_data (G_OBJECT (child),
                             ROW_KEY);
    if (NULL == row)
    {
      row = find_row_at (child,
                         view,
                         y);
      if (NULL != row)
        return row;
      continue;
    }
    /* our widget does not fill the cell it was put into, so we measure
       the cell, which does tile the list without gaps */
    cell = gtk_widget_get_parent (child);
    if (! gtk_widget_compute_bounds ((NULL != cell) ? cell : child,
                                     view,
                                     &bounds))
      continue;
    if ( (y >= bounds.origin.y) &&
         (y < bounds.origin.y + bounds.size.height) )
      return row;
  }
  return NULL;
}


AGRow *
AG_row_at (GtkWidget *view,
           double y)
{
  return find_row_at (view,
                      view,
                      y);
}


/**
 * Return the @a field of @a row as a number, mapping true to 1.
 *
 * @param row the row to inspect
 * @param field name of the field
 * @return 0 if there is no such field
 */
static json_int_t
row_number (AGRow *row,
            const char *field)
{
  json_t *val = AG_row_json (row,
                             field);

  if (json_is_boolean (val))
    return json_is_true (val) ? 1 : 0;
  return json_integer_value (val);
}


/**
 * Compare two rows by the field named in @a cls.
 *
 * @param a first row
 * @param b second row
 * @param cls the `struct AG_SortSpec` of the column being sorted
 * @return -1, 0 or 1
 */
static gint
compare_rows (gconstpointer a,
              gconstpointer b,
              gpointer cls)
{
  const struct AG_SortSpec *spec = cls;
  AGRow *ra = AG_row_of ((gpointer) a);
  AGRow *rb = AG_row_of ((gpointer) b);

  if (spec->numeric)
  {
    json_int_t na = row_number (ra,
                                spec->field);
    json_int_t nb = row_number (rb,
                                spec->field);

    if (na < nb)
      return -1;
    if (na > nb)
      return 1;
    return 0;
  }
  {
    const char *sa = AG_row_string (ra,
                                    spec->field);
    const char *sb = AG_row_string (rb,
                                    spec->field);

    if (NULL == sa)
      sa = "";
    if (NULL == sb)
      sb = "";
    return g_utf8_collate (sa,
                           sb);
  }
}


void
AG_list_make_sortable (GtkBuilder *builder,
                       const char *view,
                       const char *sort_model,
                       const struct AG_SortSpec *specs)
{
  GtkColumnView *cv;
  GtkSortListModel *slm;

  cv = GTK_COLUMN_VIEW (gtk_builder_get_object (builder,
                                                view));
  slm = GTK_SORT_LIST_MODEL (gtk_builder_get_object (builder,
                                                     sort_model));
  if ( (NULL == cv) ||
       (NULL == slm) )
  {
    GNUNET_break (0);
    return;
  }
  for (unsigned int i = 0; NULL != specs[i].column; i++)
  {
    GtkColumnViewColumn *col;
    GtkSorter *sorter;

    col = GTK_COLUMN_VIEW_COLUMN (gtk_builder_get_object (builder,
                                                          specs[i].column));
    if (NULL == col)
    {
      GNUNET_break (0);
      continue;
    }
    sorter = GTK_SORTER (gtk_custom_sorter_new (&compare_rows,
                                                (gpointer) &specs[i],
                                                NULL));
    gtk_column_view_column_set_sorter (col,
                                       sorter);
    g_object_unref (sorter);
  }
  /* clicking a column header now sorts the rows the view shows */
  gtk_sort_list_model_set_sorter (slm,
                                  gtk_column_view_get_sorter (cv));
}


/**
 * Return the rows nested under @a item, if any.
 *
 * @param item the parent row
 * @param cls NULL
 * @return NULL if @a item has no children
 */
static GListModel *
create_child_model (gpointer item,
                    gpointer cls)
{
  AGRow *row = AG_ROW (item);

  (void) cls;
  if (NULL == row->children)
    return NULL;
  return G_LIST_MODEL (g_object_ref (row->children));
}


/**
 * Columns of the list of authentication methods that can be sorted by
 * clicking their header.
 */
static const struct AG_SortSpec auth_method_sorts[] = {
  { .column = "auth_method_type",
    .field = "type" },
  { .column = NULL }
};


/**
 * Columns of the list of secrets that can be sorted by clicking their
 * header.
 */
static const struct AG_SortSpec secret_sorts[] = {
  { .column = "secret_name_column",
    .field = "secret_name" },
  { .column = "backup_time_column",
    .field = "date",
    .numeric = true },
  { .column = NULL }
};


void
AG_lists_setup (void)
{
  GtkBuilder *builder = ANASTASIS_GTK_main_loop_get_builder (AG_ml);

  /* the policy review shows a tree, which the UI file cannot express */
  {
    GListStore *policies;
    GtkTreeListModel *tlm;
    GtkSingleSelection *sel;

    policies = AG_list ("policy_review_treestore");
    sel = main_window_selection ("anastasis_gtk_review_policy_selection");
    if ( (NULL == policies) ||
         (NULL == sel) )
    {
      GNUNET_break (0);
      return;
    }
    tlm = gtk_tree_list_model_new (G_LIST_MODEL (g_object_ref (policies)),
                                   FALSE, /* we want the GtkTreeListRows */
                                   TRUE, /* expand all */
                                   &create_child_model,
                                   NULL,
                                   NULL);
    gtk_single_selection_set_model (sel,
                                    G_LIST_MODEL (tlm));
    g_object_unref (tlm);
  }
  AG_list_make_sortable (builder,
                         "anastasis_gtk_authentication_methods_list",
                         "authentication_methods_sorted",
                         auth_method_sorts);
  AG_list_make_sortable (builder,
                         "anastasis_secret_selection_treeview",
                         "secret_selection_sorted",
                         secret_sorts);
}


void
AG_cell_set_row (GtkWidget *widget,
                 AGRow *row)
{
  g_object_set_data (G_OBJECT (widget),
                     ROW_KEY,
                     row);
}


AGRow *
AG_cell_get_row (GtkWidget *widget)
{
  return g_object_get_data (G_OBJECT (widget),
                            ROW_KEY);
}


/* end of anastasis-gtk_rows.c */
