#include <gtk/gtk.h>

#define TEST_TYPE_OBJECT (test_object_get_type ())
G_DECLARE_FINAL_TYPE (TestObject, test_object, TEST, OBJECT, GObject)

struct _TestObject {
  GObject parent_instance;
  char *string;
  guint number;
  gboolean allow_children;
};

enum {
  PROP_STRING = 1,
  PROP_NUMBER,
  PROP_ALLOW_CHILDREN,
  PROP_NUM_PROPERTIES
};

G_DEFINE_TYPE (TestObject, test_object, G_TYPE_OBJECT);

static void
test_object_init (TestObject *obj)
{
}

static void
test_object_finalize (GObject *object)
{
  TestObject *obj = TEST_OBJECT (object);

  g_free (obj->string);

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

static void
test_object_set_property (GObject      *object,
                          guint         property_id,
                          const GValue *value,
                          GParamSpec   *pspec)
{
  TestObject *obj = TEST_OBJECT (object);

  switch (property_id)
    {
    case PROP_STRING:
      g_free (obj->string);
      obj->string = g_value_dup_string (value);
      break;

    case PROP_NUMBER:
      obj->number = g_value_get_uint (value);
      break;

    case PROP_ALLOW_CHILDREN:
      obj->allow_children = g_value_get_boolean (value);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
test_object_get_property (GObject    *object,
                          guint       property_id,
                          GValue     *value,
                          GParamSpec *pspec)
{
  TestObject *obj = TEST_OBJECT (object);

  switch (property_id)
    {
    case PROP_STRING:
      g_value_set_string (value, obj->string);
      break;

    case PROP_NUMBER:
      g_value_set_uint (value, obj->number);
      break;

    case PROP_ALLOW_CHILDREN:
      g_value_set_boolean (value, obj->allow_children);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
test_object_class_init (TestObjectClass *class)
{
  GObjectClass *object_class = G_OBJECT_CLASS (class);

  object_class->finalize = test_object_finalize;
  object_class->set_property = test_object_set_property;
  object_class->get_property = test_object_get_property;

  g_object_class_install_property (object_class, PROP_STRING,
      g_param_spec_string ("string", "String", "String",
                           NULL,
                           G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_NUMBER,
      g_param_spec_uint ("number", "Number", "Number",
                         0, G_MAXUINT, 0,
                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_ALLOW_CHILDREN,
      g_param_spec_boolean ("allow-children", "Allow children", "Allow children",
                            FALSE,
                            G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
}

static TestObject *
test_object_new (const char *string,
                 guint       number,
                 gboolean    allow_children)
{
  return g_object_new (TEST_TYPE_OBJECT,
                       "string", string,
                       "number", number,
                       "allow-children", allow_children,
                       NULL);
}

static const char *
test_object_get_string (TestObject *obj)
{
  return obj->string;
}

static guint
test_object_get_number (TestObject *obj)
{
  return obj->number;
}

static gboolean
test_object_get_allow_children (TestObject *obj)
{
  return obj->allow_children;
}

/* * * */

static GListModel *
create_model (guint base,
              guint n,
              guint increment,
              gboolean allow_children)
{
  GListStore *store;
  guint i;

  store = g_list_store_new (TEST_TYPE_OBJECT);
  for (i = 0; i < n; i++)
    {
      char *string;
      guint number;
      TestObject *obj;

      number = base + i * increment;
      string = g_strdup_printf ("%u", number);
      obj = test_object_new (string, number, allow_children);
      g_list_store_append (store, obj);
      g_object_unref (obj);
      g_free (string);
    }

  return G_LIST_MODEL (store);
}

static GListModel *
create_child_model (gpointer item,
                    gpointer user_data)
{
  guint size = GPOINTER_TO_UINT (user_data);
  guint base = test_object_get_number (TEST_OBJECT (item));

  if (test_object_get_allow_children (TEST_OBJECT (item)))
    return create_model (base, size, 1, FALSE);
  else
    return NULL;
}

static GListModel *
create_tree_model (guint n, guint m)
{
  return G_LIST_MODEL (gtk_tree_list_model_new (create_model (0, n, m, TRUE),
                                                FALSE,
                                                FALSE,
                                                create_child_model,
                                                GUINT_TO_POINTER (m), NULL));
}

static void
setup_item (GtkSignalListItemFactory *factory,
            GtkListItem              *item)
{
  GtkWidget *entry;

  entry = gtk_editable_label_new ("");
  gtk_editable_set_width_chars (GTK_EDITABLE (entry), 3);
  gtk_list_item_set_child (item, entry);
}

static void
text_changed (GObject    *object,
              GParamSpec *pspec,
              gpointer    data)
{
  const char *text;

  text = gtk_editable_get_text (GTK_EDITABLE (object));
g_print ("text changed to '%s'\n", text);
  g_object_set (data, "string", text, NULL);
}

static void
bind_item (GtkSignalListItemFactory *factory,
           GtkListItem              *item)
{
  TestObject *obj;
  GtkWidget *entry;

  obj = gtk_list_item_get_item (item);
  entry = gtk_list_item_get_child (item);
  gtk_editable_set_text (GTK_EDITABLE (entry), test_object_get_string (obj));
  g_signal_connect (entry, "notify::text", G_CALLBACK (text_changed), obj);
}

static void
unbind_item (GtkSignalListItemFactory *factory,
             GtkListItem              *item)
{
  TestObject *obj;
  GtkWidget *entry;

  obj = gtk_list_item_get_item (item);
  entry = gtk_list_item_get_child (item);
  g_signal_handlers_disconnect_by_func (entry, text_changed, obj);
}

static void
setup_tree_item (GtkSignalListItemFactory *factory,
                 GtkListItem              *item)
{
  GtkWidget *expander;
  GtkWidget *entry;

  entry = gtk_editable_label_new ("");
  gtk_editable_set_width_chars (GTK_EDITABLE (entry), 3);
  expander = gtk_tree_expander_new ();
  gtk_tree_expander_set_child (GTK_TREE_EXPANDER (expander), entry);
  gtk_list_item_set_child (item, expander);
}

static void
bind_tree_item (GtkSignalListItemFactory *factory,
                GtkListItem              *item)
{
  GtkTreeListRow *row;
  GtkTreeExpander *expander;
  TestObject *obj;
  GtkWidget *entry;

  row = gtk_list_item_get_item (item);
  expander = GTK_TREE_EXPANDER (gtk_list_item_get_child (item));
  gtk_tree_expander_set_list_row (expander, row);
  obj = gtk_tree_list_row_get_item (row);
  entry = gtk_tree_expander_get_child (expander);
  gtk_editable_set_text (GTK_EDITABLE (entry), test_object_get_string (obj));

  g_signal_connect (entry, "notify::text", G_CALLBACK (text_changed), obj);
}

static void
unbind_tree_item (GtkSignalListItemFactory *factory,
                  GtkListItem              *item)
{
  GtkTreeListRow *row;
  GtkTreeExpander *expander;
  TestObject *obj;
  GtkWidget *entry;

  row = gtk_list_item_get_item (item);
  expander = GTK_TREE_EXPANDER (gtk_list_item_get_child (item));
  obj = gtk_tree_list_row_get_item (row);
  entry = gtk_tree_expander_get_child (expander);
  g_signal_handlers_disconnect_by_func (entry, text_changed, obj);
}

int
main (int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *box;
  GtkWidget *label;
  GtkWidget *box2;
  GtkWidget *stack;
  GtkWidget *switcher;
  GtkWidget *sw;
  GtkWidget *grid;
  GtkWidget *list;
  GtkWidget *cv;
  GListModel *model;
  GtkListItemFactory *factory;

  gtk_init ();

  window = gtk_window_new ();
  gtk_window_set_default_size (GTK_WINDOW (window), 600, 400);

  box2 = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
  gtk_window_set_child (GTK_WINDOW (window), box2);

  switcher = gtk_stack_switcher_new ();
  gtk_widget_set_halign (GTK_WIDGET (switcher), GTK_ALIGN_CENTER);
  gtk_widget_set_margin_top (GTK_WIDGET (switcher), 10);
  gtk_widget_set_margin_bottom (GTK_WIDGET (switcher), 10);
  gtk_box_append (GTK_BOX (box2), switcher);

  box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
  gtk_box_set_homogeneous (GTK_BOX (box), TRUE);
  gtk_box_append (GTK_BOX (box2), box);

  label = gtk_editable_label_new ("Drag me");
  gtk_box_append (GTK_BOX (box), label);

  stack = gtk_stack_new ();
  gtk_widget_set_vexpand (stack, TRUE);
  gtk_stack_switcher_set_stack (GTK_STACK_SWITCHER (switcher), GTK_STACK (stack));
  gtk_box_append (GTK_BOX (box), stack);

  /* grid */
  sw = gtk_scrolled_window_new ();
  gtk_scrolled_window_set_has_frame (GTK_SCROLLED_WINDOW (sw), TRUE);
  gtk_stack_add_titled (GTK_STACK (stack), sw, "grid", "GtkGridView");

  model = create_model (0, 400, 1, FALSE);
  factory = gtk_signal_list_item_factory_new ();
  g_signal_connect (factory, "setup", G_CALLBACK (setup_item), NULL);
  g_signal_connect (factory, "bind", G_CALLBACK (bind_item), NULL);
  g_signal_connect (factory, "unbind", G_CALLBACK (unbind_item), NULL);

  grid = gtk_grid_view_new_with_factory (model, factory);
  gtk_grid_view_set_min_columns (GTK_GRID_VIEW (grid), 20);
  gtk_grid_view_set_max_columns (GTK_GRID_VIEW (grid), 20);

  gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), grid);

  /* list */
  sw = gtk_scrolled_window_new ();
  gtk_scrolled_window_set_has_frame (GTK_SCROLLED_WINDOW (sw), TRUE);
  gtk_stack_add_titled (GTK_STACK (stack), sw, "list", "GtkListView");

  list = gtk_list_view_new (create_model (0, 400, 1, FALSE));
  gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), list);

  factory = gtk_signal_list_item_factory_new ();
  g_signal_connect (factory, "setup", G_CALLBACK (setup_item), NULL);
  g_signal_connect (factory, "bind", G_CALLBACK (bind_item), NULL);
  g_signal_connect (factory, "unbind", G_CALLBACK (unbind_item), NULL);

  gtk_list_view_set_factory (GTK_LIST_VIEW (list), factory);
  g_object_unref (factory);

  /* columnview */
  sw = gtk_scrolled_window_new ();
  gtk_scrolled_window_set_has_frame (GTK_SCROLLED_WINDOW (sw), TRUE);
  gtk_stack_add_titled (GTK_STACK (stack), sw, "column", "GtkColumnView");

  cv = gtk_column_view_new (create_model (0, 400, 1, FALSE));

  for (guint i = 0; i < 20; i++)
    {
      GtkColumnViewColumn *column;
      char *title;

      factory = gtk_signal_list_item_factory_new ();
      g_signal_connect (factory, "setup", G_CALLBACK (setup_item), NULL);
      g_signal_connect (factory, "bind", G_CALLBACK (bind_item), NULL);
      g_signal_connect (factory, "unbind", G_CALLBACK (unbind_item), NULL);

      title = g_strdup_printf ("Column %u", i);
      column = gtk_column_view_column_new_with_factory (title, factory);
      gtk_column_view_append_column (GTK_COLUMN_VIEW (cv), column);
      g_object_unref (column);
      g_free (title);
    }

  gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), cv);

  /* tree */
  sw = gtk_scrolled_window_new ();
  gtk_scrolled_window_set_has_frame (GTK_SCROLLED_WINDOW (sw), TRUE);
  gtk_stack_add_titled (GTK_STACK (stack), sw, "tree", "Tree");

  list = gtk_list_view_new (create_tree_model (20, 20));
  gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), list);

  factory = gtk_signal_list_item_factory_new ();
  g_signal_connect (factory, "setup", G_CALLBACK (setup_tree_item), NULL);
  g_signal_connect (factory, "bind", G_CALLBACK (bind_tree_item), NULL);
  g_signal_connect (factory, "unbind", G_CALLBACK (unbind_tree_item), NULL);

  gtk_list_view_set_factory (GTK_LIST_VIEW (list), factory);
  g_object_unref (factory);

  gtk_window_present (GTK_WINDOW (window));

  while (g_list_model_get_n_items (gtk_window_get_toplevels ()) > 0)
    g_main_context_iteration (NULL, TRUE);

  gtk_window_destroy (GTK_WINDOW (window));

  return 0;
}