🏡 index : quine-global-labs/bazaar.git

/* bz-flathub-state.c
 *
 * Copyright 2025 Adam Masciola
 *
 * 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 <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#define G_LOG_DOMAIN                 "BAZAAR::FLATHUB"
#define COLLECTION_FETCH_SIZE        192
#define CATEGORY_FETCH_SIZE          48
#define QUALITY_MODERATION_PAGE_SIZE 300
#define KEYWORD_SEARCH_PAGE_SIZE     48

#include <json-glib/json-glib.h>
#include <libdex.h>

#include "bz-flathub-category.h"
#include "bz-flathub-curated-selection.h"
#include "bz-flathub-state.h"
#include "bz-serializable.h"
#include "env.h"
#include "global-net.h"
#include "io.h"
#include "util.h"

struct _BzFlathubState
{
  GObject parent_instance;

  char                    *for_day;
  BzApplicationMapFactory *map_factory;
  char                    *app_of_the_day;
  GtkStringList           *apps_of_the_week;
  GListStore              *categories;
  GListStore              *curated_selections;

  DexFuture *initializing;
};

typedef enum
{
  QUALITY_MODE_NONE,
  QUALITY_MODE_FIRST,
  QUALITY_MODE_RANDOM
} QualityMode;

static void
serializable_iface_init (BzSerializableInterface *iface);

G_DEFINE_FINAL_TYPE_WITH_CODE (
    BzFlathubState,
    bz_flathub_state,
    G_TYPE_OBJECT,
    G_IMPLEMENT_INTERFACE (BZ_TYPE_SERIALIZABLE, serializable_iface_init))

static GListModel *bz_flathub_state_dup_apps_of_the_day_week (BzFlathubState *self);

enum
{
  PROP_0,

  PROP_FOR_DAY,
  PROP_MAP_FACTORY,
  PROP_APP_OF_THE_DAY,
  PROP_APPS_OF_THE_WEEK,
  PROP_APPS_OF_THE_DAY_WEEK,
  PROP_CATEGORIES,
  PROP_CURATED_SELECTIONS,

  LAST_PROP
};
static GParamSpec *props[LAST_PROP] = { 0 };

static DexFuture *
initialize_fiber (GWeakRef *wr);
static DexFuture *
initialize_finally (DexFuture *future,
                    GWeakRef  *wr);

static void
notify_all (BzFlathubState *self);

static void
clear (BzFlathubState *self);

static void
bz_flathub_state_dispose (GObject *object)
{
  BzFlathubState *self = BZ_FLATHUB_STATE (object);

  dex_clear (&self->initializing);
  g_clear_pointer (&self->map_factory, g_object_unref);
  clear (self);

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

static void
bz_flathub_state_get_property (GObject    *object,
                               guint       prop_id,
                               GValue     *value,
                               GParamSpec *pspec)
{
  BzFlathubState *self = BZ_FLATHUB_STATE (object);

  switch (prop_id)
    {
    case PROP_FOR_DAY:
      g_value_set_string (value, bz_flathub_state_get_for_day (self));
      break;
    case PROP_MAP_FACTORY:
      g_value_set_object (value, bz_flathub_state_get_map_factory (self));
      break;
    case PROP_APP_OF_THE_DAY:
      g_value_set_string (value, bz_flathub_state_get_app_of_the_day (self));
      break;
    case PROP_APPS_OF_THE_WEEK:
      g_value_take_object (value, bz_flathub_state_dup_apps_of_the_week (self));
      break;
    case PROP_APPS_OF_THE_DAY_WEEK:
      g_value_take_object (value, bz_flathub_state_dup_apps_of_the_day_week (self));
      break;
    case PROP_CATEGORIES:
      g_value_set_object (value, bz_flathub_state_get_categories (self));
      break;
    case PROP_CURATED_SELECTIONS:
      g_value_set_object (value, bz_flathub_state_get_curated_selections (self));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
bz_flathub_state_set_property (GObject      *object,
                               guint         prop_id,
                               const GValue *value,
                               GParamSpec   *pspec)
{
  BzFlathubState *self = BZ_FLATHUB_STATE (object);

  switch (prop_id)
    {
    case PROP_FOR_DAY:
      dex_future_disown (bz_flathub_state_set_for_day (self, g_value_get_string (value)));
      break;
    case PROP_MAP_FACTORY:
      bz_flathub_state_set_map_factory (self, g_value_get_object (value));
      break;
    case PROP_APP_OF_THE_DAY:
    case PROP_APPS_OF_THE_WEEK:
    case PROP_APPS_OF_THE_DAY_WEEK:
    case PROP_CATEGORIES:
    case PROP_CURATED_SELECTIONS:
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
bz_flathub_state_class_init (BzFlathubStateClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->set_property = bz_flathub_state_set_property;
  object_class->get_property = bz_flathub_state_get_property;
  object_class->dispose      = bz_flathub_state_dispose;

  props[PROP_FOR_DAY] =
      g_param_spec_string (
          "for-day",
          NULL, NULL, NULL,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  props[PROP_MAP_FACTORY] =
      g_param_spec_object (
          "map-factory",
          NULL, NULL,
          BZ_TYPE_APPLICATION_MAP_FACTORY,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  props[PROP_APP_OF_THE_DAY] =
      g_param_spec_string (
          "app-of-the-day",
          NULL, NULL, NULL,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  props[PROP_APPS_OF_THE_WEEK] =
      g_param_spec_object (
          "apps-of-the-week",
          NULL, NULL,
          G_TYPE_LIST_MODEL,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  props[PROP_APPS_OF_THE_DAY_WEEK] =
      g_param_spec_object (
          "apps-of-the-day-week",
          NULL, NULL,
          G_TYPE_LIST_MODEL,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  props[PROP_CATEGORIES] =
      g_param_spec_object (
          "categories",
          NULL, NULL,
          G_TYPE_LIST_MODEL,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  props[PROP_CURATED_SELECTIONS] =
      g_param_spec_object (
          "curated-selections",
          NULL, NULL,
          G_TYPE_LIST_MODEL,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  g_object_class_install_properties (object_class, LAST_PROP, props);
}

static void
bz_flathub_state_init (BzFlathubState *self)
{
}

static void
bz_flathub_state_real_serialize (BzSerializable  *serializable,
                                 GVariantBuilder *builder)
{
  BzFlathubState *self = BZ_FLATHUB_STATE (serializable);

  if (self->initializing != NULL &&
      dex_future_is_pending (self->initializing))
    return;

  if (self->for_day != NULL)
    g_variant_builder_add (builder, "{sv}", "for-day", g_variant_new_string (self->for_day));
  if (self->app_of_the_day != NULL)
    g_variant_builder_add (builder, "{sv}", "app-of-the-day", g_variant_new_string (self->app_of_the_day));
  if (self->apps_of_the_week != NULL)
    {
      guint n_items = 0;

      n_items = g_list_model_get_n_items (G_LIST_MODEL (self->apps_of_the_week));
      if (n_items > 0)
        {
          g_autoptr (GVariantBuilder) sub_builder = NULL;

          sub_builder = g_variant_builder_new (G_VARIANT_TYPE ("as"));
          for (guint i = 0; i < n_items; i++)
            {
              const char *string = NULL;

              string = gtk_string_list_get_string (self->apps_of_the_week, i);
              g_variant_builder_add (sub_builder, "s", string);
            }

          g_variant_builder_add (builder, "{sv}", "apps-of-the-week", g_variant_builder_end (sub_builder));
        }
    }
  if (self->categories != NULL)
    {
      guint n_items = 0;

      n_items = g_list_model_get_n_items (G_LIST_MODEL (self->categories));
      if (n_items > 0)
        {
          g_autoptr (GVariantBuilder) sub_builder = NULL;

          sub_builder = g_variant_builder_new (G_VARIANT_TYPE ("av"));
          for (guint i = 0; i < n_items; i++)
            {
              g_autoptr (BzFlathubCategory) category       = NULL;
              g_autoptr (GVariantBuilder) category_builder = NULL;

              category         = g_list_model_get_item (G_LIST_MODEL (self->categories), i);
              category_builder = g_variant_builder_new (G_VARIANT_TYPE_VARDICT);

              bz_serializable_serialize (BZ_SERIALIZABLE (category), category_builder);
              g_variant_builder_add (sub_builder, "v", g_variant_builder_end (category_builder));
            }

          g_variant_builder_add (builder, "{sv}", "categories", g_variant_builder_end (sub_builder));
        }
    }
  if (self->curated_selections != NULL)
    {
      guint n_items = 0;

      n_items = g_list_model_get_n_items (G_LIST_MODEL (self->curated_selections));
      if (n_items > 0)
        {
          g_autoptr (GVariantBuilder) sub_builder = NULL;

          sub_builder = g_variant_builder_new (G_VARIANT_TYPE ("av"));
          for (guint i = 0; i < n_items; i++)
            {
              g_autoptr (BzFlathubCuratedSelection) selection = NULL;
              g_autoptr (GVariantBuilder) selection_builder   = NULL;
              const char    *theme_key                        = NULL;
              const char    *slot                             = NULL;
              GtkStringList *apps                             = NULL;

              selection = g_list_model_get_item (G_LIST_MODEL (self->curated_selections), i);

              theme_key = bz_flathub_curated_selection_get_theme_key (selection);
              slot      = bz_flathub_curated_selection_get_slot (selection);
              apps      = bz_flathub_curated_selection_get_apps (selection);

              selection_builder = g_variant_builder_new (G_VARIANT_TYPE_VARDICT);

              if (theme_key != NULL)
                g_variant_builder_add (selection_builder, "{sv}", "theme-key", g_variant_new_string (theme_key));
              if (slot != NULL)
                g_variant_builder_add (selection_builder, "{sv}", "slot", g_variant_new_string (slot));
              if (apps != NULL)
                {
                  guint n_apps = 0;

                  n_apps = g_list_model_get_n_items (G_LIST_MODEL (apps));
                  if (n_apps > 0)
                    {
                      g_autoptr (GVariantBuilder) apps_builder = NULL;

                      apps_builder = g_variant_builder_new (G_VARIANT_TYPE ("as"));
                      for (guint j = 0; j < n_apps; j++)
                        {
                          const char *app_id = NULL;

                          app_id = gtk_string_list_get_string (apps, j);
                          g_variant_builder_add (apps_builder, "s", app_id);
                        }

                      g_variant_builder_add (selection_builder, "{sv}", "apps", g_variant_builder_end (apps_builder));
                    }
                }

              g_variant_builder_add (sub_builder, "v", g_variant_builder_end (selection_builder));
            }

          g_variant_builder_add (builder, "{sv}", "curated-selections", g_variant_builder_end (sub_builder));
        }
    }
}

static gboolean
bz_flathub_state_real_deserialize (BzSerializable *serializable,
                                   GVariant       *import,
                                   GError        **error)
{
  BzFlathubState *self          = BZ_FLATHUB_STATE (serializable);
  gboolean        result        = FALSE;
  g_autoptr (GVariantIter) iter = NULL;

  if (self->initializing != NULL &&
      !dex_future_is_pending (self->initializing))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_BUSY,
                   "Cannot perform serialization operations while initializing!");
      return FALSE;
    }

  clear (self);

  iter = g_variant_iter_new (import);
  for (;;)
    {
      g_autofree char *key       = NULL;
      g_autoptr (GVariant) value = NULL;

      /* TODO automate this, this is awful */
      if (!g_variant_iter_next (iter, "{sv}", &key, &value))
        break;

      if (g_strcmp0 (key, "for-day") == 0)
        self->for_day = g_variant_dup_string (value, NULL);
      else if (g_strcmp0 (key, "app-of-the-day") == 0)
        self->app_of_the_day = g_variant_dup_string (value, NULL);
      else if (g_strcmp0 (key, "apps-of-the-week") == 0)
        {
          g_autoptr (GtkStringList) list     = NULL;
          g_autoptr (GVariantIter) list_iter = NULL;

          list = gtk_string_list_new (NULL);

          list_iter = g_variant_iter_new (value);
          for (;;)
            {
              g_autofree char *id = NULL;

              if (!g_variant_iter_next (list_iter, "s", &id))
                break;
              gtk_string_list_append (list, id);
            }

          self->apps_of_the_week = g_steal_pointer (&list);
        }
      else if (g_strcmp0 (key, "categories") == 0)
        {
          g_autoptr (GListStore) categories        = NULL;
          g_autoptr (GVariantIter) categories_iter = NULL;

          categories = g_list_store_new (BZ_TYPE_FLATHUB_CATEGORY);

          categories_iter = g_variant_iter_new (value);
          for (;;)
            {
              g_autoptr (GVariant) category_import   = NULL;
              g_autoptr (BzFlathubCategory) category = NULL;

              if (!g_variant_iter_next (categories_iter, "v", &category_import))
                break;

              category = bz_flathub_category_new ();
              result   = bz_serializable_deserialize (
                  BZ_SERIALIZABLE (category), category_import, error);
              if (!result)
                return FALSE;

              g_object_bind_property (self, "map-factory", category, "map-factory", G_BINDING_SYNC_CREATE);
              g_list_store_append (categories, category);
            }

          self->categories = g_steal_pointer (&categories);
        }
      else if (g_strcmp0 (key, "curated-selections") == 0)
        {
          g_autoptr (GListStore) curated_selections        = NULL;
          g_autoptr (GVariantIter) curated_selections_iter = NULL;

          curated_selections = g_list_store_new (BZ_TYPE_FLATHUB_CURATED_SELECTION);

          curated_selections_iter = g_variant_iter_new (value);
          for (;;)
            {
              g_autoptr (GVariant) selection_import           = NULL;
              g_autoptr (BzFlathubCuratedSelection) selection = NULL;
              g_autoptr (GVariantIter) selection_iter         = NULL;
              g_autoptr (GtkStringList) apps                  = NULL;
              g_autofree char *theme_key                      = NULL;
              g_autofree char *slot                           = NULL;

              if (!g_variant_iter_next (curated_selections_iter, "v", &selection_import))
                break;

              selection_iter = g_variant_iter_new (selection_import);
              for (;;)
                {
                  g_autofree char *sel_key     = NULL;
                  g_autoptr (GVariant) sel_val = NULL;

                  if (!g_variant_iter_next (selection_iter, "{sv}", &sel_key, &sel_val))
                    break;

                  if (g_strcmp0 (sel_key, "theme-key") == 0)
                    theme_key = g_variant_dup_string (sel_val, NULL);
                  else if (g_strcmp0 (sel_key, "slot") == 0)
                    slot = g_variant_dup_string (sel_val, NULL);
                  else if (g_strcmp0 (sel_key, "apps") == 0)
                    {
                      g_autoptr (GVariantIter) apps_iter = NULL;

                      apps      = gtk_string_list_new (NULL);
                      apps_iter = g_variant_iter_new (sel_val);
                      for (;;)
                        {
                          g_autofree char *app_id = NULL;

                          if (!g_variant_iter_next (apps_iter, "s", &app_id))
                            break;
                          gtk_string_list_append (apps, app_id);
                        }
                    }
                }

              selection = bz_flathub_curated_selection_new ();
              if (theme_key != NULL)
                bz_flathub_curated_selection_set_theme_key (selection, theme_key);
              if (slot != NULL)
                bz_flathub_curated_selection_set_slot (selection, slot);
              if (apps != NULL)
                bz_flathub_curated_selection_set_apps (selection, apps);

              g_object_bind_property (self, "map-factory", selection, "map-factory", G_BINDING_SYNC_CREATE);
              g_list_store_append (curated_selections, selection);
            }

          self->curated_selections = g_steal_pointer (&curated_selections);
        }
    }

  notify_all (self);
  return TRUE;
}

static void
serializable_iface_init (BzSerializableInterface *iface)
{
  iface->serialize   = bz_flathub_state_real_serialize;
  iface->deserialize = bz_flathub_state_real_deserialize;
}

BzFlathubState *
bz_flathub_state_new (void)
{
  return g_object_new (BZ_TYPE_FLATHUB_STATE, NULL);
}

const char *
bz_flathub_state_get_for_day (BzFlathubState *self)
{
  g_return_val_if_fail (BZ_IS_FLATHUB_STATE (self), NULL);
  return self->for_day;
}

BzApplicationMapFactory *
bz_flathub_state_get_map_factory (BzFlathubState *self)
{
  g_return_val_if_fail (BZ_IS_FLATHUB_STATE (self), NULL);
  return self->map_factory;
}

const char *
bz_flathub_state_get_app_of_the_day (BzFlathubState *self)
{
  g_return_val_if_fail (BZ_IS_FLATHUB_STATE (self), NULL);
  if (self->initializing != NULL &&
      dex_future_is_pending (self->initializing))
    return NULL;
  return self->app_of_the_day;
}

BzEntryGroup *
bz_flathub_state_dup_app_of_the_day_group (BzFlathubState *self)
{
  g_autoptr (GtkStringObject) string = NULL;

  g_return_val_if_fail (BZ_IS_FLATHUB_STATE (self), NULL);
  if (self->initializing != NULL &&
      dex_future_is_pending (self->initializing))
    return NULL;
  g_return_val_if_fail (self->map_factory != NULL, NULL);

  string = gtk_string_object_new (self->app_of_the_day);
  return bz_application_map_factory_convert_one (self->map_factory, string);
}

GListModel *
bz_flathub_state_dup_apps_of_the_week (BzFlathubState *self)
{
  g_return_val_if_fail (BZ_IS_FLATHUB_STATE (self), NULL);
  if (self->initializing != NULL &&
      dex_future_is_pending (self->initializing))
    return NULL;

  if (self->apps_of_the_week != NULL)
    {
      if (self->map_factory != NULL)
        return bz_application_map_factory_generate (
            self->map_factory, G_LIST_MODEL (self->apps_of_the_week));
      else
        return G_LIST_MODEL (g_object_ref (self->apps_of_the_week));
    }
  else
    return NULL;
}

GListModel *
bz_flathub_state_dup_apps_of_the_day_week (BzFlathubState *self)
{
  g_autoptr (GtkStringList) combined_list = NULL;

  g_return_val_if_fail (BZ_IS_FLATHUB_STATE (self), NULL);
  if (self->initializing != NULL &&
      dex_future_is_pending (self->initializing))
    return NULL;

  combined_list = gtk_string_list_new (NULL);

  if (self->app_of_the_day != NULL)
    gtk_string_list_append (combined_list, self->app_of_the_day);

  if (self->apps_of_the_week != NULL)
    {
      guint n_items = g_list_model_get_n_items (G_LIST_MODEL (self->apps_of_the_week));
      for (guint i = 0; i < n_items; i++)
        {
          const char *app_id = gtk_string_list_get_string (self->apps_of_the_week, i);
          gtk_string_list_append (combined_list, app_id);
        }
    }

  if (self->map_factory != NULL)
    return bz_application_map_factory_generate (self->map_factory, G_LIST_MODEL (combined_list));
  else
    return G_LIST_MODEL (g_object_ref (combined_list));
}

GListModel *
bz_flathub_state_get_categories (BzFlathubState *self)
{
  g_return_val_if_fail (BZ_IS_FLATHUB_STATE (self), NULL);
  if (self->initializing != NULL &&
      dex_future_is_pending (self->initializing))
    return NULL;
  return G_LIST_MODEL (self->categories);
}

GListModel *
bz_flathub_state_get_curated_selections (BzFlathubState *self)
{
  g_return_val_if_fail (BZ_IS_FLATHUB_STATE (self), NULL);
  if (self->initializing != NULL &&
      dex_future_is_pending (self->initializing))
    return NULL;
  return G_LIST_MODEL (self->curated_selections);
}

DexFuture *
bz_flathub_state_set_for_day (BzFlathubState *self,
                              const char     *for_day)
{
  dex_return_error_if_fail (BZ_IS_FLATHUB_STATE (self));

  dex_clear (&self->initializing);
  clear (self);
  if (for_day != NULL)
    {
      g_autoptr (DexFuture) future = NULL;

      self->for_day            = g_strdup (for_day);
      self->apps_of_the_week   = gtk_string_list_new (NULL);
      self->categories         = g_list_store_new (BZ_TYPE_FLATHUB_CATEGORY);
      self->curated_selections = g_list_store_new (BZ_TYPE_FLATHUB_CURATED_SELECTION);

      future = dex_scheduler_spawn (
          bz_get_io_scheduler (),
          bz_get_dex_stack_size (),
          (DexFiberFunc) initialize_fiber,
          bz_track_weak (self), bz_weak_release);
      future = dex_future_finally (
          future,
          (DexFutureCallback) initialize_finally,
          bz_track_weak (self), bz_weak_release);
      self->initializing = g_steal_pointer (&future);
      return dex_ref (self->initializing);
    }
  else
    {
      notify_all (self);
      return dex_future_new_false ();
    }
}

DexFuture *
bz_flathub_state_update_to_today (BzFlathubState *self)
{
  g_autoptr (GDateTime) datetime = NULL;
  g_autofree gchar *for_day      = NULL;

  dex_return_error_if_fail (BZ_IS_FLATHUB_STATE (self));

  datetime = g_date_time_new_now_utc ();
  for_day  = g_date_time_format (datetime, "%F");

  g_debug ("Syncing with flathub for day: %s", for_day);
  return bz_flathub_state_set_for_day (self, for_day);
}

void
bz_flathub_state_set_map_factory (BzFlathubState          *self,
                                  BzApplicationMapFactory *map_factory)
{
  g_return_if_fail (BZ_IS_FLATHUB_STATE (self));
  g_return_if_fail (map_factory == NULL || BZ_IS_APPLICATION_MAP_FACTORY (map_factory));

  g_clear_object (&self->map_factory);
  if (map_factory != NULL)
    self->map_factory = g_object_ref (map_factory);

  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MAP_FACTORY]);
}

static const char *
get_de (void)
{
  const char *desktop = NULL;

  desktop = g_getenv ("XDG_CURRENT_DESKTOP");
  if (desktop == NULL)
    return "adwaita";

  if (g_strstr_len (desktop, -1, "KDE") != NULL)
    return "kde";

  if (g_strstr_len (desktop, -1, "COSMIC") != NULL)
    return "cosmic";

  return "adwaita";
}

static void
add_category (BzFlathubState *self,
              const char     *name,
              JsonNode       *node,
              GHashTable     *quality_set,
              gboolean        is_toplevel_json,
              QualityMode     quality_mode,
              gboolean        is_spotlight)
{
  JsonObject    *object                   = NULL;
  JsonArray     *hits_array               = NULL;
  JsonArray     *toplevel_array           = NULL;
  const char    *app                      = NULL;
  g_autoptr (BzFlathubCategory) category  = NULL;
  g_autoptr (GtkStringList) store         = NULL;
  g_autoptr (GtkStringList) quality_store = NULL;
  g_autoptr (GPtrArray) quality_apps      = NULL;
  guint app_count                         = 0;
  guint quality_count                     = 0;
  guint random_index                      = 0;
  guint i                                 = 0;
  int   total_entries                     = 0;

  category      = bz_flathub_category_new ();
  store         = gtk_string_list_new (NULL);
  quality_store = gtk_string_list_new (NULL);

  bz_flathub_category_set_name (category, name);
  bz_flathub_category_set_is_spotlight (category, is_spotlight);
  bz_flathub_category_set_applications (category, G_LIST_MODEL (store));

  if (quality_mode == QUALITY_MODE_RANDOM)
    quality_apps = g_ptr_array_new_with_free_func (g_free);

  if (is_toplevel_json)
    {
      toplevel_array = json_node_get_array (node);
      app_count      = json_array_get_length (toplevel_array);

      for (i = 0; i < app_count; i++)
        {
          const char *app_id = NULL;

          app_id = json_array_get_string_element (toplevel_array, i);
          gtk_string_list_append (store, app_id);

          if (g_hash_table_contains (quality_set, app_id))
            {
              if (quality_mode == QUALITY_MODE_RANDOM)
                g_ptr_array_add (quality_apps, g_strdup (app_id));
              else if (quality_mode == QUALITY_MODE_FIRST)
                gtk_string_list_append (quality_store, app_id);
            }
        }
      total_entries = app_count;
    }
  else
    {
      object     = json_node_get_object (node);
      hits_array = json_object_get_array_member (object, "hits");
      app_count  = json_array_get_length (hits_array);

      for (i = 0; i < app_count; i++)
        {
          JsonObject *element = NULL;
          const char *app_id  = NULL;

          element = json_array_get_object_element (hits_array, i);
          app_id  = json_object_get_string_member (element, "app_id");
          gtk_string_list_append (store, app_id);

          if (g_hash_table_contains (quality_set, app_id))
            {
              if (quality_mode == QUALITY_MODE_RANDOM)
                g_ptr_array_add (quality_apps, g_strdup (app_id));
              else if (quality_mode == QUALITY_MODE_FIRST)
                gtk_string_list_append (quality_store, app_id);
            }
        }
      total_entries = json_object_get_int_member (object, "totalHits");
    }

  if (quality_mode == QUALITY_MODE_RANDOM && quality_apps != NULL)
    {
      quality_count = MIN (7, quality_apps->len);
      for (i = 0; i < quality_count; i++)
        {
          random_index = g_random_int_range (0, quality_apps->len);
          app          = g_ptr_array_index (quality_apps, random_index);
          gtk_string_list_append (quality_store, app);
          g_ptr_array_remove_index_fast (quality_apps, random_index);
        }
    }

  bz_flathub_category_set_total_entries (category, total_entries);
  bz_flathub_category_set_quality_applications (category, G_LIST_MODEL (quality_store));
  g_list_store_append (self->categories, category);
}

static void
add_curated_selection (BzFlathubState *self,
                       JsonObject     *object)
{
  g_autoptr (BzFlathubCuratedSelection) selection = NULL;
  g_autoptr (GtkStringList) apps                  = NULL;
  JsonArray *apps_array                           = NULL;
  guint      length                               = 0;

  selection = bz_flathub_curated_selection_new ();
  apps      = gtk_string_list_new (NULL);

  bz_flathub_curated_selection_set_theme_key (selection, json_object_get_string_member (object, "theme_key"));
  bz_flathub_curated_selection_set_slot (selection, json_object_get_string_member (object, "slot"));

  apps_array = json_object_get_array_member (object, "apps");
  length     = apps_array != NULL ? json_array_get_length (apps_array) : 0;

  for (guint i = 0; i < length; i++)
    {
      JsonObject *element = NULL;
      const char *app_id  = NULL;

      element = json_array_get_object_element (apps_array, i);
      app_id  = json_object_get_string_member (element, "app_id");

      gtk_string_list_append (apps, app_id);
    }

  bz_flathub_curated_selection_set_apps (selection, apps);
  g_list_store_append (self->curated_selections, selection);
}

static DexFuture *
initialize_fiber (GWeakRef *wr)
{
  g_autoptr (BzFlathubState) self    = NULL;
  g_autoptr (GError) local_error     = NULL;
  gboolean result                    = FALSE;
  const char *de                     = get_de ();
  g_autoptr (GHashTable) quality_set = NULL;

  g_autoptr (DexFuture) aotd_f       = NULL;
  g_autoptr (DexFuture) aotw_f       = NULL;
  g_autoptr (DexFuture) curated_f    = NULL;
  g_autoptr (DexFuture) categories_f = NULL;
  g_autoptr (DexFuture) updated_f    = NULL;
  g_autoptr (DexFuture) added_f      = NULL;
  g_autoptr (DexFuture) popular_f    = NULL;
  g_autoptr (DexFuture) trending_f   = NULL;
  g_autoptr (DexFuture) mobile_f     = NULL;
  g_autoptr (DexFuture) passing_f    = NULL;
  g_autoptr (DexFuture) toolkit_f    = NULL;
  g_autoptr (DexFuture) emulators_f  = NULL;
  g_autoptr (DexFuture) launchers_f  = NULL;
  g_autoptr (DexFuture) game_tools_f = NULL;
  g_autoptr (DexFuture) game_only_f  = NULL;

  bz_weak_get_or_return_reject (self, wr);

  quality_set = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);

#define ADD_REQUEST(_var, ...)                                                         \
  G_STMT_START                                                                         \
  {                                                                                    \
    g_autofree char *_request = NULL;                                                  \
                                                                                       \
    _request = g_strdup_printf (__VA_ARGS__);                                          \
    (_var)   = bz_query_flathub_v2_json_take (g_steal_pointer (&_request));            \
    if (!dex_await (dex_ref ((_var)), &local_error))                                   \
      {                                                                                \
        g_warning ("Failed to complete request to flathub: %s", local_error->message); \
        return dex_future_new_for_error (g_steal_pointer (&local_error));              \
      }                                                                                \
  }                                                                                    \
  G_STMT_END

  {
    g_autofree char *made_for_route = NULL;

    made_for_route = g_strdup_printf ("/made-for/%s", de);
    toolkit_f = bz_query_bazaar_json (made_for_route);
    if (!dex_await (dex_ref (toolkit_f), &local_error))
      {
        g_warning ("Failed to complete request to bazaar %s: %s", made_for_route, local_error->message);
        g_clear_error (&local_error);
        dex_clear (&toolkit_f);
      }
  }

  ADD_REQUEST (passing_f, "/quality-moderation/passing-apps?page=1&page_size=%d", QUALITY_MODERATION_PAGE_SIZE);
  ADD_REQUEST (aotd_f, "/app-picks/app-of-the-day/%s", self->for_day);
  ADD_REQUEST (aotw_f, "/app-picks/apps-of-the-week/%s", self->for_day);
  ADD_REQUEST (curated_f, "/app-picks/curated-app-selections/%s", self->for_day);
  ADD_REQUEST (categories_f, "/collection/category");
  ADD_REQUEST (updated_f, "/collection/recently-updated?page=0&per_page=%d", COLLECTION_FETCH_SIZE);
  ADD_REQUEST (added_f, "/collection/recently-added?page=0&per_page=%d", COLLECTION_FETCH_SIZE);
  ADD_REQUEST (popular_f, "/collection/popular?page=0&per_page=%d", COLLECTION_FETCH_SIZE);
  ADD_REQUEST (trending_f, "/collection/trending?page=0&per_page=%d", COLLECTION_FETCH_SIZE);
  ADD_REQUEST (mobile_f, "/collection/mobile?sort_by=trending&page=0&per_page=%d", CATEGORY_FETCH_SIZE);
  ADD_REQUEST (game_only_f, "/collection/category/game?exclude_subcategories=emulator&exclude_subcategories=packageManager&exclude_subcategories=utility&exclude_subcategories=network&exclude_subcategories=gameTool&exclude_subcategories=launcherStore&sort_by=trending&page=0&per_page=%d", CATEGORY_FETCH_SIZE);
  ADD_REQUEST (emulators_f, "/collection/category/game/subcategories?subcategory=emulator&sort_by=trending&page=0&per_page=%d", CATEGORY_FETCH_SIZE);
  ADD_REQUEST (launchers_f, "/collection/category/game/subcategories?subcategory=packageManager&subcategory=launcherStore&sort_by=trending&page=0&per_page=%d", CATEGORY_FETCH_SIZE);
  ADD_REQUEST (game_tools_f, "/collection/category/game/subcategories?subcategory=utility&subcategory=network&subcategory=gameTool&sort_by=trending&page=0&per_page=%d", CATEGORY_FETCH_SIZE);

#undef ADD_REQUEST

#define GET_BOXED(_future) g_value_get_boxed (dex_future_get_value ((_future), NULL))

  {
    JsonObject *object = NULL;
    JsonArray  *array  = NULL;
    guint       length = 0;

    object = json_node_get_object (GET_BOXED (passing_f));
    array  = json_object_get_array_member (object, "apps");
    length = json_array_get_length (array);

    for (guint i = 0; i < length; i++)
      {
        const char *app_id = NULL;

        app_id = json_array_get_string_element (array, i);
        g_hash_table_replace (quality_set, g_strdup (app_id), NULL);
      }
  }
  {
    JsonObject *object = NULL;

    object               = json_node_get_object (GET_BOXED (aotd_f));
    self->app_of_the_day = g_strdup (json_object_get_string_member (object, "app_id"));
  }
  {
    JsonObject *root   = NULL;
    JsonArray  *array  = NULL;
    guint       length = 0;

    root   = json_node_get_object (GET_BOXED (curated_f));
    array  = json_object_get_array_member (root, "selections");
    length = json_array_get_length (array);

    for (guint i = 0; i < length; i++)
      add_curated_selection (self, json_array_get_object_element (array, i));
  }
  {
    JsonObject *object = NULL;
    JsonArray  *array  = NULL;
    guint       length = 0;

    object = json_node_get_object (GET_BOXED (aotw_f));
    array  = json_object_get_array_member (object, "apps");
    length = json_array_get_length (array);

    for (guint i = 0; i < length; i++)
      {
        JsonObject *element = NULL;

        element = json_array_get_object_element (array, i);
        gtk_string_list_append (
            self->apps_of_the_week,
            json_object_get_string_member (element, "app_id"));
      }
  }

  add_category (self, "trending", GET_BOXED (trending_f), quality_set, FALSE, QUALITY_MODE_NONE, TRUE);
  add_category (self, "popular", GET_BOXED (popular_f), quality_set, FALSE, QUALITY_MODE_NONE, TRUE);
  add_category (self, "recently-added", GET_BOXED (added_f), quality_set, FALSE, QUALITY_MODE_NONE, TRUE);
  add_category (self, "recently-updated", GET_BOXED (updated_f), quality_set, FALSE, QUALITY_MODE_NONE, TRUE);
  add_category (self, "mobile", GET_BOXED (mobile_f), quality_set, FALSE, QUALITY_MODE_NONE, TRUE);
  add_category (self, "game-only", GET_BOXED (game_only_f), quality_set, FALSE, QUALITY_MODE_NONE, FALSE);
  add_category (self, "emulators", GET_BOXED (emulators_f), quality_set, FALSE, QUALITY_MODE_NONE, FALSE);
  add_category (self, "launchers", GET_BOXED (launchers_f), quality_set, FALSE, QUALITY_MODE_NONE, FALSE);
  add_category (self, "game-tools", GET_BOXED (game_tools_f), quality_set, FALSE, QUALITY_MODE_NONE, FALSE);

  {
    static const char *categories[] = {
      "audiovideo", "development", "education", "game", "graphics",
      "network", "office", "science", "system", "utility"
    };
    g_autoptr (GPtrArray) category_futures = NULL;

    category_futures = g_ptr_array_new_with_free_func (dex_unref);

    for (guint i = 0; i < G_N_ELEMENTS (categories); i++)
      {
        g_autofree char *request     = NULL;
        g_autoptr (DexFuture) future = NULL;

        request = g_strdup_printf (
            "/collection/category/%s?sort_by=trending&page=0&per_page=%d",
            categories[i], CATEGORY_FETCH_SIZE);

        future = bz_query_flathub_v2_json_take (g_steal_pointer (&request));
        result = dex_await (dex_ref (future), &local_error);
        if (!result)
          {
            g_warning ("Failed to complete request to flathub: %s", local_error->message);
            return dex_future_new_for_error (g_steal_pointer (&local_error));
          }
        g_ptr_array_add (category_futures, dex_ref (future));
      }

    for (guint i = 0; i < G_N_ELEMENTS (categories); i++)
      {
        DexFuture *future = NULL;
        JsonNode  *node   = NULL;

        future = g_ptr_array_index (category_futures, i);
        node   = GET_BOXED (future);

        add_category (self, categories[i], node, quality_set, FALSE, QUALITY_MODE_FIRST, FALSE);
      }
  }

  if (toolkit_f != NULL)
    add_category (self, de, GET_BOXED (toolkit_f), quality_set, TRUE, QUALITY_MODE_RANDOM, FALSE);

  return dex_future_new_true ();
}

static DexFuture *
initialize_finally (DexFuture *future,
                    GWeakRef  *wr)
{
  g_autoptr (BzFlathubState) self = NULL;

  bz_weak_get_or_return_reject (self, wr);

  if (dex_future_is_resolved (future))
    {
      guint n_categories = 0;
      guint n_selections = 0;

      n_categories = g_list_model_get_n_items (G_LIST_MODEL (self->categories));
      for (guint i = 0; i < n_categories; i++)
        {
          g_autoptr (BzFlathubCategory) category = NULL;

          category = g_list_model_get_item (G_LIST_MODEL (self->categories), i);
          g_object_bind_property (self, "map-factory", category, "map-factory", G_BINDING_SYNC_CREATE);
        }

      n_selections = g_list_model_get_n_items (G_LIST_MODEL (self->curated_selections));
      for (guint i = 0; i < n_selections; i++)
        {
          g_autoptr (BzFlathubCuratedSelection) selection = NULL;

          selection = g_list_model_get_item (G_LIST_MODEL (self->curated_selections), i);
          g_object_bind_property (self, "map-factory", selection, "map-factory", G_BINDING_SYNC_CREATE);
        }

      g_debug ("Done syncing flathub state; notifying property listeners...");
      notify_all (self);
    }
  else
    clear (self);

  return dex_ref (future);
}

static DexFuture *
search_collection_fiber (char *route)
{
  g_autoptr (GError) local_error    = NULL;
  g_autoptr (GtkStringList) results = NULL;
  g_autoptr (JsonNode) node         = NULL;
  g_autofree char *request          = NULL;
  JsonObject      *object           = NULL;
  JsonArray       *array            = NULL;
  guint            length           = 0;

  request = g_strdup_printf ("%s&page=1&per_page=%d&locale=en",
                             route, KEYWORD_SEARCH_PAGE_SIZE);

  node = dex_await_boxed (
      bz_query_flathub_v2_json_take (
          g_steal_pointer (&request)),
      &local_error);
  if (node == NULL)
    return dex_future_new_for_error (g_steal_pointer (&local_error));

  results = gtk_string_list_new (NULL);

  object = json_node_get_object (node);
  array  = json_object_get_array_member (object, "hits");
  length = json_array_get_length (array);

  for (guint i = 0; i < length; i++)
    {
      JsonObject *element = NULL;
      const char *app_id  = NULL;

      element = json_array_get_object_element (array, i);
      app_id  = json_object_get_string_member (element, "app_id");
      gtk_string_list_append (results, app_id);
    }
  return dex_future_new_take_object (g_steal_pointer (&results));
}

static DexFuture *
search_collection_finally (DexFuture *future,
                           GWeakRef  *wr)
{
  g_autoptr (BzFlathubState) self = NULL;
  const GValue *value             = NULL;
  GListModel   *model             = NULL;

  bz_weak_get_or_return_reject (self, wr);

  value = dex_future_get_value (future, NULL);
  if (value == NULL)
    return dex_ref (future);

  model = g_value_get_object (value);

  if (self->map_factory != NULL)
    return dex_future_new_take_object (
        bz_application_map_factory_generate (self->map_factory, model));

  return dex_ref (future);
}

DexFuture *
bz_flathub_state_search_collection (BzFlathubState *self,
                                    const char     *route)
{
  g_autoptr (DexFuture) future = NULL;

  dex_return_error_if_fail (BZ_IS_FLATHUB_STATE (self));
  dex_return_error_if_fail (route != NULL);

  future = dex_scheduler_spawn (
      bz_get_io_scheduler (),
      bz_get_dex_stack_size (),
      (DexFiberFunc) search_collection_fiber,
      g_strdup (route),
      g_free);
  future = dex_future_finally (
      future,
      (DexFutureCallback) search_collection_finally,
      bz_track_weak (self),
      bz_weak_release);
  return g_steal_pointer (&future);
}

static void
notify_all (BzFlathubState *self)
{
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_FOR_DAY]);
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_APP_OF_THE_DAY]);
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_APPS_OF_THE_WEEK]);
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_APPS_OF_THE_DAY_WEEK]);
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CATEGORIES]);
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CURATED_SELECTIONS]);
}

static void
clear (BzFlathubState *self)
{
  g_clear_pointer (&self->for_day, g_free);
  g_clear_pointer (&self->app_of_the_day, g_free);
  g_clear_pointer (&self->apps_of_the_week, g_object_unref);
  g_clear_pointer (&self->categories, g_object_unref);
  g_clear_pointer (&self->curated_selections, g_object_unref);
}

/* End of bz-flathub-state.c */