#include <gtk/gtk.h>

#define FILE_INFO_TYPE_SELECTION (file_info_selection_get_type ())

G_DECLARE_FINAL_TYPE (FileInfoSelection, file_info_selection, FILE_INFO, SELECTION, GObject)

struct _FileInfoSelection
{
  GObject parent_instance;

  GListModel *model;
};

struct _FileInfoSelectionClass
{
  GObjectClass parent_class;
};

static GType
file_info_selection_get_item_type (GListModel *list)
{
  FileInfoSelection *self = FILE_INFO_SELECTION (list);

  return g_list_model_get_item_type (self->model);
}

static guint
file_info_selection_get_n_items (GListModel *list)
{
  FileInfoSelection *self = FILE_INFO_SELECTION (list);

  return g_list_model_get_n_items (self->model);
}

static gpointer
file_info_selection_get_item (GListModel *list,
                               guint       position)
{
  FileInfoSelection *self = FILE_INFO_SELECTION (list);

  return g_list_model_get_item (self->model, position);
}

static void
file_info_selection_list_model_init (GListModelInterface *iface)
{
  iface->get_item_type = file_info_selection_get_item_type;
  iface->get_n_items = file_info_selection_get_n_items;
  iface->get_item = file_info_selection_get_item;
}

static gboolean
file_info_selection_is_selected (GtkSelectionModel *model,
                                 guint              position)
{
  FileInfoSelection *self = FILE_INFO_SELECTION (model);
  gpointer item;

  item = g_list_model_get_item (self->model, position);
  if (item == NULL)
    return FALSE;

  if (GTK_IS_TREE_LIST_ROW (item))
    {
      GtkTreeListRow *row = item;
      item = gtk_tree_list_row_get_item (row);
      g_object_unref (row);
    }

  return g_file_info_get_attribute_boolean (item, "filechooser::selected");
}

static void
file_info_selection_set_selected (FileInfoSelection *self,
                                  guint              position,
                                  gboolean           selected)
{
  gpointer item;

  item = g_list_model_get_item (self->model, position);
  if (item == NULL)
    return;

  if (GTK_IS_TREE_LIST_ROW (item))
    {
      GtkTreeListRow *row = item;
      item = gtk_tree_list_row_get_item (row);
      g_object_unref (row);
    }

  g_file_info_set_attribute_boolean (item, "filechooser::selected", selected);
}

static gboolean
file_info_selection_select_item (GtkSelectionModel *model,
                                 guint              position,
                                 gboolean           exclusive)
{
  FileInfoSelection *self = FILE_INFO_SELECTION (model);

  if (exclusive)
    {
      guint i;

      for (i = 0; i < g_list_model_get_n_items (self->model); i++)
        file_info_selection_set_selected (self, i, i == position);

      gtk_selection_model_selection_changed (model, 0, g_list_model_get_n_items (self->model));
    }
  else
    {
      file_info_selection_set_selected (self, position, TRUE);

      gtk_selection_model_selection_changed (model, position, 1);
    }

  return TRUE;
}

static gboolean
file_info_selection_unselect_item (GtkSelectionModel *model,
                                   guint              position)
{
  FileInfoSelection *self = FILE_INFO_SELECTION (model);

  file_info_selection_set_selected (self, position, FALSE);

  gtk_selection_model_selection_changed (model, position, 1);

  return TRUE;
}

static gboolean
file_info_selection_select_range (GtkSelectionModel *model,
                                  guint              position,
                                  guint              n_items,
                                  gboolean           exclusive)
{
  FileInfoSelection *self = FILE_INFO_SELECTION (model);
  guint i;

  if (exclusive)
    for (i = 0; i < position; i++)
      file_info_selection_set_selected (self, i, FALSE);

  for (i = position; i < position + n_items; i++)
    file_info_selection_set_selected (self, i, TRUE);

  if (exclusive)
    for (i = position + n_items; i < g_list_model_get_n_items (self->model); i++)
      file_info_selection_set_selected (self, i, FALSE);

  if (exclusive)
    gtk_selection_model_selection_changed (model, 0, g_list_model_get_n_items (self->model));
  else
    gtk_selection_model_selection_changed (model, position, n_items);

  return TRUE;
}

static gboolean
file_info_selection_unselect_range (GtkSelectionModel *model,
                                    guint              position,
                                    guint              n_items)
{
  FileInfoSelection *self = FILE_INFO_SELECTION (model);
  guint i;

  for (i = position; i < position + n_items; i++)
    file_info_selection_set_selected (self, i, FALSE);

  gtk_selection_model_selection_changed (model, position, n_items);

  return TRUE;
}

static void
file_info_selection_selection_model_init (GtkSelectionModelInterface *iface)
{
  iface->is_selected = file_info_selection_is_selected; 
  iface->select_item = file_info_selection_select_item; 
  iface->unselect_item = file_info_selection_unselect_item; 
  iface->select_range = file_info_selection_select_range; 
  iface->unselect_range = file_info_selection_unselect_range; 
}

G_DEFINE_TYPE_EXTENDED (FileInfoSelection, file_info_selection, G_TYPE_OBJECT, 0,
                        G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL,
                                               file_info_selection_list_model_init)
                        G_IMPLEMENT_INTERFACE (GTK_TYPE_SELECTION_MODEL,
                                               file_info_selection_selection_model_init))

static void
file_info_selection_items_changed_cb (GListModel        *model,
                                      guint              position,
                                      guint              removed,
                                      guint              added,
                                      FileInfoSelection *self)
{
  g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added);
}

static void
file_info_selection_clear_model (FileInfoSelection *self)
{
  if (self->model == NULL)
    return;

  g_signal_handlers_disconnect_by_func (self->model, 
                                        file_info_selection_items_changed_cb,
                                        self);
  g_clear_object (&self->model);
}

static void
file_info_selection_dispose (GObject *object)
{
  FileInfoSelection *self = FILE_INFO_SELECTION (object);

  file_info_selection_clear_model (self);

  G_OBJECT_CLASS (file_info_selection_parent_class)->dispose (object);
}

static void
file_info_selection_class_init (FileInfoSelectionClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

  gobject_class->dispose = file_info_selection_dispose;
}

static void
file_info_selection_init (FileInfoSelection *self)
{
}

static FileInfoSelection *
file_info_selection_new (GListModel *model)
{
  FileInfoSelection *result;

  result = g_object_new (FILE_INFO_TYPE_SELECTION, NULL);

  result->model = g_object_ref (model);
  g_signal_connect (result->model, "items-changed",
                    G_CALLBACK (file_info_selection_items_changed_cb), result);

  return result;
}

/*** ---------------------- ***/

GSList *pending = NULL;
guint active = 0;

static void
loading_cb (GtkDirectoryList *dir,
            GParamSpec       *pspec,
            gpointer          unused)
{
  if (gtk_directory_list_is_loading (dir))
    {
      active++;
      /* HACK: ensure loading finishes and the dir doesn't get destroyed */
      g_object_ref (dir);
    }
  else
    {
      active--;
      g_object_unref (dir);

      while (active < 20 && pending)
        {
          GtkDirectoryList *dir2 = pending->data;
          pending = g_slist_remove (pending, dir2);
          gtk_directory_list_set_file (dir2, g_object_get_data (G_OBJECT (dir2), "file"));
          g_object_unref (dir2);
        }
    }
}

static GtkDirectoryList *
create_directory_list (GFile *file)
{
  GtkDirectoryList *dir;

  dir = gtk_directory_list_new (G_FILE_ATTRIBUTE_STANDARD_TYPE
                                "," G_FILE_ATTRIBUTE_STANDARD_NAME
                                "," G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
                                NULL);
  gtk_directory_list_set_io_priority (dir, G_PRIORITY_DEFAULT_IDLE);
  g_signal_connect (dir, "notify::loading", G_CALLBACK (loading_cb), NULL);
  g_assert (!gtk_directory_list_is_loading (dir));

  if (active > 20)
    {
      g_object_set_data_full (G_OBJECT (dir), "file", g_object_ref (file), g_object_unref);
      pending = g_slist_prepend (pending, g_object_ref (dir));
    }
  else
    {
      gtk_directory_list_set_file (dir, file);
    }

  return dir;
}

static char *
get_file_path (GFileInfo *info)
{
  GFile *file;

  file = G_FILE (g_file_info_get_attribute_object (info, "standard::file"));
  return g_file_get_path (file);
}

static GListModel *
create_list_model_for_directory (gpointer file)
{
  GtkDirectoryList *dir;
  GtkSorter *sorter;

  if (g_file_query_file_type (file, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL) != G_FILE_TYPE_DIRECTORY)
    return NULL;

  dir = create_directory_list (file);
  sorter = gtk_string_sorter_new (gtk_cclosure_expression_new (G_TYPE_STRING, NULL, 0, NULL, (GCallback) get_file_path, NULL, NULL));

  return G_LIST_MODEL (gtk_sort_list_model_new (G_LIST_MODEL (dir), sorter));
}

typedef struct _RowData RowData;
struct _RowData
{
  GtkWidget *expander;
  GtkWidget *icon;
  GtkWidget *name;
  GCancellable *cancellable;

  GtkTreeListRow *current_item;
};

static void row_data_notify_item (GtkListItem *item,
                                  GParamSpec  *pspec,
                                  RowData     *data);
static void
row_data_unbind (RowData *data)
{
  if (data->current_item == NULL)
    return;

  if (data->cancellable)
    {
      g_cancellable_cancel (data->cancellable);
      g_clear_object (&data->cancellable);
    }

  g_clear_object (&data->current_item);
}

static void
row_data_update_info (RowData   *data,
                      GFileInfo *info)
{
  GIcon *icon;
  const char *thumbnail_path;

  thumbnail_path = g_file_info_get_attribute_byte_string (info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH);
  if (thumbnail_path)
    {
      /* XXX: not async */
      GFile *thumbnail_file = g_file_new_for_path (thumbnail_path);
      icon = g_file_icon_new (thumbnail_file);
      g_object_unref (thumbnail_file);
    }
  else
    {
      icon = g_file_info_get_icon (info);
    }

  gtk_widget_set_visible (data->icon, icon != NULL);
  gtk_image_set_from_gicon (GTK_IMAGE (data->icon), icon);
}

static void
copy_attribute (GFileInfo   *to,
                GFileInfo   *from,
                const char *attribute)
{
  GFileAttributeType type;
  gpointer value;

  if (g_file_info_get_attribute_data (from, attribute, &type, &value, NULL))
    g_file_info_set_attribute (to, attribute, type, value);
}

static void
row_data_got_thumbnail_info_cb (GObject      *source,
                                GAsyncResult *res,
                                gpointer      _data)
{
  RowData *data = _data; /* invalid if operation was cancelled */
  GFile *file = G_FILE (source);
  GFileInfo *queried, *info;

  queried = g_file_query_info_finish (file, res, NULL);
  if (queried == NULL)
    return;

  /* now we know row is valid */

  info = gtk_tree_list_row_get_item (data->current_item);

  copy_attribute (info, queried, G_FILE_ATTRIBUTE_THUMBNAIL_PATH);
  copy_attribute (info, queried, G_FILE_ATTRIBUTE_THUMBNAILING_FAILED);
  copy_attribute (info, queried, G_FILE_ATTRIBUTE_STANDARD_ICON);

  g_object_unref (queried);

  row_data_update_info (data, info);
  
  g_clear_object (&data->cancellable);
}

static void
row_data_bind (RowData        *data,
               GtkTreeListRow *item)
{
  GFileInfo *info;

  row_data_unbind (data);

  if (item == NULL)
    return;

  data->current_item = g_object_ref (item);

  gtk_tree_expander_set_list_row (GTK_TREE_EXPANDER (data->expander), item);

  info = gtk_tree_list_row_get_item (item);

  if (!g_file_info_has_attribute (info, "filechooser::queried"))
    {
      data->cancellable = g_cancellable_new ();
      g_file_info_set_attribute_boolean (info, "filechooser::queried", TRUE);
      g_file_query_info_async (G_FILE (g_file_info_get_attribute_object (info, "standard::file")),
                               G_FILE_ATTRIBUTE_THUMBNAIL_PATH ","
                               G_FILE_ATTRIBUTE_THUMBNAILING_FAILED ","
                               G_FILE_ATTRIBUTE_STANDARD_ICON,
                               G_FILE_QUERY_INFO_NONE,
                               G_PRIORITY_DEFAULT,
                               data->cancellable,
                               row_data_got_thumbnail_info_cb,
                               data);
    }

  row_data_update_info (data, info);

  gtk_label_set_label (GTK_LABEL (data->name), g_file_info_get_display_name (info));

  g_object_unref (info);
}

static void
row_data_notify_item (GtkListItem *item,
                      GParamSpec  *pspec,
                      RowData     *data)
{
  row_data_bind (data, gtk_list_item_get_item (item));
}

static void
row_data_free (gpointer _data)
{
  RowData *data = _data;

  row_data_unbind (data);

  g_slice_free (RowData, data);
}

static void
setup_widget (GtkSignalListItemFactory *factory,
              GtkListItem              *list_item)
{
  GtkWidget *box, *child;
  RowData *data;

  data = g_slice_new0 (RowData);

  box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4);
  gtk_list_item_set_child (list_item, box);

  child = gtk_label_new (NULL);
  gtk_label_set_width_chars (GTK_LABEL (child), 5);
  gtk_box_append (GTK_BOX (box), child);

  data->expander = gtk_tree_expander_new ();
  gtk_box_append (GTK_BOX (box), data->expander);

  box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4);
  gtk_tree_expander_set_child (GTK_TREE_EXPANDER (data->expander), box);

  data->icon = gtk_image_new ();
  gtk_box_append (GTK_BOX (box), data->icon);

  data->name = gtk_label_new (NULL);
  gtk_label_set_max_width_chars (GTK_LABEL (data->name), 25);
  gtk_label_set_ellipsize (GTK_LABEL (data->name), PANGO_ELLIPSIZE_END);
  gtk_box_append (GTK_BOX (box), data->name);

  g_signal_connect (list_item, "notify::item", G_CALLBACK (row_data_notify_item), data);
  g_object_set_data_full (G_OBJECT (list_item), "row-data", data, row_data_free);
}

static GListModel *
create_list_model_for_file_info (gpointer file_info,
                                 gpointer unused)
{
  GFile *file = G_FILE (g_file_info_get_attribute_object (file_info, "standard::file"));

  if (file == NULL)
    return NULL;

  return create_list_model_for_directory (file);
}

static gboolean
update_statusbar (GtkStatusbar *statusbar)
{
  GListModel *model = g_object_get_data (G_OBJECT (statusbar), "model");
  GString *string = g_string_new (NULL);
  guint n;
  gboolean result = G_SOURCE_REMOVE;

  gtk_statusbar_remove_all (statusbar, 0);

  n = g_list_model_get_n_items (model);
  g_string_append_printf (string, "%u", n);
  if (GTK_IS_FILTER_LIST_MODEL (model))
    {
      guint n_unfiltered = g_list_model_get_n_items (gtk_filter_list_model_get_model (GTK_FILTER_LIST_MODEL (model)));
      if (n != n_unfiltered)
        g_string_append_printf (string, "/%u", n_unfiltered);
    }
  g_string_append (string, " items");

  if (pending || active)
    {
      g_string_append_printf (string, " (%u directories remaining)", active + g_slist_length (pending));
      result = G_SOURCE_CONTINUE;
    }
      result = G_SOURCE_CONTINUE;

  gtk_statusbar_push (statusbar, 0, string->str);
  g_free (string->str);

  return result;
}

static gboolean
match_file (gpointer item, gpointer data)
{
  GtkWidget *search_entry = data;
  GFileInfo *info = gtk_tree_list_row_get_item (item);
  GFile *file = G_FILE (g_file_info_get_attribute_object (info, "standard::file"));
  char *path;
  gboolean result;
  
  path = g_file_get_path (file);

  result = strstr (path, gtk_editable_get_text (GTK_EDITABLE (search_entry))) != NULL;

  g_object_unref (info);
  g_free (path);

  return result;
}

static void
search_changed_cb (GtkSearchEntry *entry,
                   GtkFilter      *custom_filter)
{
  gtk_filter_changed (custom_filter, GTK_FILTER_CHANGE_DIFFERENT);
}

int
main (int argc, char *argv[])
{
  GtkWidget *win, *vbox, *sw, *listview, *search_entry, *statusbar;
  GtkTreeListModel *tree;
  GtkFilterListModel *filter;
  GtkFilter *custom_filter;
  FileInfoSelection *selectionmodel;
  GFile *root;
  GListModel *toplevels;
  GtkListItemFactory *factory;

  gtk_init ();

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

  vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
  gtk_window_set_child (GTK_WINDOW (win), vbox);

  search_entry = gtk_search_entry_new ();
  gtk_box_append (GTK_BOX (vbox), search_entry);

  sw = gtk_scrolled_window_new ();
  gtk_widget_set_vexpand (sw, TRUE);
  gtk_search_entry_set_key_capture_widget (GTK_SEARCH_ENTRY (search_entry), sw);
  gtk_box_append (GTK_BOX (vbox), sw);

  factory = gtk_signal_list_item_factory_new ();
  g_signal_connect (factory, "setup", G_CALLBACK (setup_widget), NULL);
  listview = gtk_list_view_new (NULL, factory);
  gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), listview);

  if (argc > 1)
    root = g_file_new_for_commandline_arg (argv[1]);
  else
    root = g_file_new_for_path (g_get_current_dir ());
  tree = gtk_tree_list_model_new (create_list_model_for_directory (root),
                                  FALSE,
                                  TRUE,
                                  create_list_model_for_file_info,
                                  NULL, NULL);
  g_object_unref (root);

  custom_filter = gtk_custom_filter_new (match_file, search_entry, NULL);
  filter = gtk_filter_list_model_new (G_LIST_MODEL (tree), custom_filter);
  g_signal_connect (search_entry, "search-changed", G_CALLBACK (search_changed_cb), custom_filter);

  selectionmodel = file_info_selection_new (G_LIST_MODEL (filter));
  g_object_unref (filter);

  gtk_list_view_set_model (GTK_LIST_VIEW (listview), GTK_SELECTION_MODEL (selectionmodel));

  statusbar = gtk_statusbar_new ();
  gtk_widget_add_tick_callback (statusbar, (GtkTickCallback) update_statusbar, NULL, NULL);
  g_object_set_data (G_OBJECT (statusbar), "model", filter);
  g_signal_connect_swapped (filter, "items-changed", G_CALLBACK (update_statusbar), statusbar);
  update_statusbar (GTK_STATUSBAR (statusbar));
  gtk_box_append (GTK_BOX (vbox), statusbar);

  g_object_unref (selectionmodel);

  gtk_widget_show (win);

  toplevels = gtk_window_get_toplevels ();
  while (g_list_model_get_n_items (toplevels))
    g_main_context_iteration (NULL, TRUE);

  return 0;
}
