/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
 *
 * Copyright © 2019 Endless Mobile, Inc.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 * 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 2 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 <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *  - Philip Withnall <withnall@endlessm.com>
 */

#include "config.h"

#include <glib.h>
#include <glib-object.h>
#include <glib/gi18n-lib.h>
#include <gio/gio.h>
#include <gtk/gtk.h>
#include <adwaita.h>
#include <locale.h>
#include <polkit/polkit.h>

#include "application.h"
#include "user-selector.h"
#include "user-page.h"
#include "access-page.h"
#include "time-page.h"


static void user_selector_notify_selected_user_cb (GObject    *obj,
                                                   GParamSpec *pspec,
                                                   gpointer    user_data);
static void user_selector_notify_n_users_cb (GObject    *obj,
                                             GParamSpec *pspec,
                                             gpointer    user_data);
static void user_manager_notify_is_loaded_cb (GObject    *obj,
                                              GParamSpec *pspec,
                                              void       *user_data);
static void user_manager_loaded_cb (GObject      *object,
                                    GAsyncResult *result,
                                    void         *user_data);
static void user_manager_loaded_user_cb (GObject      *object,
                                         GAsyncResult *result,
                                         void         *user_data);
static void user_manager_user_removed_cb (MctUserManager *user_manager,
                                          MctUser        *removed_user,
                                          void           *user_data);
static void permission_new_cb (GObject      *source_object,
                               GAsyncResult *result,
                               gpointer      user_data);
static void permission_notify_allowed_cb (GObject    *obj,
                                          GParamSpec *pspec,
                                          gpointer    user_data);
static void command_line_permission_notify_allowed_cb (GObject    *obj,
                                                       GParamSpec *pspec,
                                                       gpointer    user_data);
static void user_accounts_panel_button_clicked_cb (GtkButton *button,
                                                   gpointer   user_data);
static void about_action_cb (GSimpleAction *action, GVariant *parameters, gpointer user_data);
static void help_action_cb (GSimpleAction *action, GVariant *parameters, gpointer user_data);
static void quit_action_cb (GSimpleAction *action, GVariant *parameters, gpointer user_data);
static void update_main_stack (MctApplication *self);
static void select_user_by_command_line_username (MctApplication *self);


/**
 * MctApplication:
 *
 * A top-level object representing the parental controls application.
 *
 * Since: 0.5.0
 */
struct _MctApplication
{
  GtkApplication parent_instance;

  GCancellable *cancellable;  /* (owned) */

  GDBusConnection *dbus_connection;  /* (owned) */
  MctManager *policy_manager;  /* (owned) */
  MctUserManager *user_manager;  /* (owned) */
  GError *user_manager_load_error;  /* (nullable) (owned) */

  GPermission *permission;  /* (owned) */
  GError *permission_error;  /* (nullable) (owned) */

  char *command_line_username; /* (owned) (nullable) */
  unsigned long user_manager_notify_is_loaded_id;
  unsigned long user_manager_user_removed_id;
  unsigned long command_line_permission_notify_allowed_id;

  AdwNavigationView *navigation;
  MctUserSelector *user_selector;
  MctAccessPage *access_page;
  MctTimePage *time_page;
  GtkStack *main_stack;
  AdwStatusPage *error_page;
  GtkLockButton *lock_button;
  GtkButton *user_accounts_panel_button;
  GtkLabel *help_label;
};

G_DEFINE_TYPE (MctApplication, mct_application, GTK_TYPE_APPLICATION)

static void
mct_application_init (MctApplication *self)
{
  self->cancellable = g_cancellable_new ();
}

static void
mct_application_constructed (GObject *object)
{
  GApplication *application = G_APPLICATION (object);
  const GOptionEntry options[] =
    {
      { "user", 'u', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, NULL,
        /* Translators: This documents the --user command line option to malcontent-control: */
        N_("User to select in the UI"),
        /* Translators: This is a placeholder for a command line argument value: */
        N_("USERNAME") },
      { NULL, },
    };

  g_application_set_application_id (application, "org.freedesktop.MalcontentControl");

  g_application_add_main_option_entries (application, options);
  g_application_set_flags (application, g_application_get_flags (application) | G_APPLICATION_HANDLES_COMMAND_LINE);

  /* Translators: This is a summary of what the application does, displayed when
   * it’s run with --help: */
  g_application_set_option_context_parameter_string (application,
                                                     N_("— view and edit parental controls"));

  /* Localisation */
  setlocale (LC_ALL, "");
  bindtextdomain ("malcontent", PACKAGE_LOCALE_DIR);
  bind_textdomain_codeset ("malcontent", "UTF-8");
  textdomain ("malcontent");

  g_set_application_name (_("Parental Controls"));
  gtk_window_set_default_icon_name ("org.freedesktop.MalcontentControl");

  G_OBJECT_CLASS (mct_application_parent_class)->constructed (object);
}

static void
mct_application_dispose (GObject *object)
{
  MctApplication *self = MCT_APPLICATION (object);

  g_cancellable_cancel (self->cancellable);

  g_clear_signal_handler (&self->user_manager_notify_is_loaded_id, self->user_manager);
  g_clear_signal_handler (&self->user_manager_user_removed_id, self->user_manager);
  g_clear_object (&self->policy_manager);
  g_clear_object (&self->user_manager);
  g_clear_pointer (&self->command_line_username, g_free);
  g_clear_signal_handler (&self->command_line_permission_notify_allowed_id,
                          self->permission);

  if (self->permission != NULL)
    {
      g_signal_handlers_disconnect_by_func (self->permission,
                                            permission_notify_allowed_cb, self);
      g_clear_object (&self->permission);
    }

  g_clear_object (&self->dbus_connection);
  g_clear_error (&self->permission_error);
  g_clear_error (&self->user_manager_load_error);
  g_clear_object (&self->cancellable);

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

static GtkWindow *
mct_application_get_main_window (MctApplication *self)
{
  return gtk_application_get_active_window (GTK_APPLICATION (self));
}

static void
mct_application_activate (GApplication *application)
{
  MctApplication *self = MCT_APPLICATION (application);
  GtkWindow *window = NULL;

  window = mct_application_get_main_window (self);

  if (window == NULL)
    {
      g_autoptr(GtkBuilder) builder = NULL;
      g_autoptr(GError) local_error = NULL;

      /* Ensure the types used in the UI are registered. */
      g_type_ensure (MCT_TYPE_USER_SELECTOR);
      g_type_ensure (MCT_TYPE_ACCESS_PAGE);
      g_type_ensure (MCT_TYPE_TIME_PAGE);

      /* Start loading the permission */
      polkit_permission_new ("org.freedesktop.MalcontentControl.administration",
                             NULL, self->cancellable,
                             permission_new_cb, self);

      builder = gtk_builder_new ();

      g_assert (self->dbus_connection == NULL);
      self->dbus_connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, self->cancellable, &local_error);
      if (self->dbus_connection == NULL)
        {
          g_error ("Error getting system bus: %s", local_error->message);
          return;
        }

      g_assert (self->policy_manager == NULL);
      self->policy_manager = mct_manager_new (self->dbus_connection);

      g_assert (self->user_manager == NULL);
      self->user_manager = mct_user_manager_new (self->dbus_connection);
      self->user_manager_user_removed_id = g_signal_connect (self->user_manager,
                                                             "user-removed",
                                                             G_CALLBACK (user_manager_user_removed_cb),
                                                             self);

      gtk_builder_set_translation_domain (builder, "malcontent");
      gtk_builder_expose_object (builder, "policy_manager", G_OBJECT (self->policy_manager));
      gtk_builder_expose_object (builder, "user_manager", G_OBJECT (self->user_manager));
      gtk_builder_expose_object (builder, "dbus_connection", G_OBJECT (self->dbus_connection));

      gtk_builder_add_from_resource (builder, "/org/freedesktop/MalcontentControl/ui/main.ui", &local_error);
      g_assert (local_error == NULL);

      /* Set up the main window. */
      window = GTK_WINDOW (gtk_builder_get_object (builder, "main_window"));
      gtk_window_set_application (window, GTK_APPLICATION (application));

      self->navigation = ADW_NAVIGATION_VIEW (gtk_builder_get_object (builder, "navigation"));
      self->main_stack = GTK_STACK (gtk_builder_get_object (builder, "main_stack"));
      self->user_selector = MCT_USER_SELECTOR (gtk_builder_get_object (builder, "user_selector"));
      self->access_page = MCT_ACCESS_PAGE (gtk_builder_get_object (builder, "access_page"));
      self->time_page = MCT_TIME_PAGE (gtk_builder_get_object (builder, "time_page"));
      self->error_page = ADW_STATUS_PAGE (gtk_builder_get_object (builder, "error_page"));
      self->lock_button = GTK_LOCK_BUTTON (gtk_builder_get_object (builder, "lock_button"));
      self->user_accounts_panel_button = GTK_BUTTON (gtk_builder_get_object (builder, "user_accounts_panel_button"));

      /* Connect signals. */
      g_signal_connect_object (self->user_selector, "notify::selected-user",
                               G_CALLBACK (user_selector_notify_selected_user_cb),
                               self, G_CONNECT_DEFAULT);
      g_signal_connect_object (self->user_selector, "notify::n-users",
                               G_CALLBACK (user_selector_notify_n_users_cb),
                               self, G_CONNECT_DEFAULT);
      g_signal_connect_object (self->user_accounts_panel_button, "clicked",
                               G_CALLBACK (user_accounts_panel_button_clicked_cb),
                               self, G_CONNECT_DEFAULT);

      /* Show the loading page and start loading the users. */
      update_main_stack (self);
      mct_user_manager_load_async (self->user_manager, self->cancellable,
                                   user_manager_loaded_cb, self);

      gtk_window_present (GTK_WINDOW (window));
    }

  /* Bring the window to the front. */
  gtk_window_present (window);
}

static void
mct_application_startup (GApplication *application)
{
  const GActionEntry app_entries[] =
    {
      { "about", about_action_cb, NULL, NULL, NULL, { 0, } },
      { "help", help_action_cb, NULL, NULL, NULL, { 0, } },
      { "quit", quit_action_cb, NULL, NULL, NULL, { 0, } },
    };
  GtkIconTheme *theme;

  /* Chain up. */
  G_APPLICATION_CLASS (mct_application_parent_class)->startup (application);

  adw_init ();

  theme = gtk_icon_theme_get_for_display (gdk_display_get_default ());
  gtk_icon_theme_add_resource_path (theme, "/org/freedesktop/MalcontentUi/icons");

  g_action_map_add_action_entries (G_ACTION_MAP (application), app_entries,
                                   G_N_ELEMENTS (app_entries), application);

  gtk_application_set_accels_for_action (GTK_APPLICATION (application),
                                         "app.help", (const gchar * const[]) { "F1", NULL });
  gtk_application_set_accels_for_action (GTK_APPLICATION (application),
                                         "app.quit", (const gchar * const[]) { "<Primary>q", "<Primary>w", NULL });
}

static gint
mct_application_command_line (GApplication            *application,
                              GApplicationCommandLine *command_line)
{
  MctApplication *self = MCT_APPLICATION (application);
  GVariantDict *options = g_application_command_line_get_options_dict (command_line);
  const gchar *username;

  /* Show the application. */
  g_application_activate (application);

  /* Select a user if requested. */
  if (g_variant_dict_lookup (options, "user", "&s", &username))
    {
      self->command_line_username = g_strdup (username);
      if (!mct_user_manager_get_is_loaded (self->user_manager))
        {
          self->user_manager_notify_is_loaded_id =
                    g_signal_connect (self->user_manager,
                                      "notify::is-loaded",
                                      G_CALLBACK (user_manager_notify_is_loaded_cb),
                                      self);
        }
      else if (!g_permission_get_allowed (self->permission))
        {
          g_clear_signal_handler (&self->command_line_permission_notify_allowed_id,
                                  self->permission);

          self->command_line_permission_notify_allowed_id =
            g_signal_connect (self->permission,
                              "notify::allowed",
                              G_CALLBACK (command_line_permission_notify_allowed_cb),
                              self);
        }
      else
        {
          select_user_by_command_line_username (self);
        }
    }

  return 0;  /* exit status */
}

static void
mct_application_class_init (MctApplicationClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GApplicationClass *application_class = G_APPLICATION_CLASS (klass);

  object_class->constructed = mct_application_constructed;
  object_class->dispose = mct_application_dispose;

  application_class->activate = mct_application_activate;
  application_class->startup = mct_application_startup;
  application_class->command_line = mct_application_command_line;
}

static void
about_action_cb (GSimpleAction *action, GVariant *parameters, gpointer user_data)
{
  MctApplication *self = MCT_APPLICATION (user_data);
  const gchar *developers[] =
    {
      "Philip Withnall <withnall@endlessm.com>",
      "Georges Basile Stavracas Neto <georges@endlessm.com>",
      "Andre Moreira Magalhaes <andre@endlessm.com>",
      NULL
    };

  adw_show_about_dialog (GTK_WIDGET (mct_application_get_main_window (self)),
	                       "application-name", g_get_application_name (),
	                       "application-icon", "org.freedesktop.MalcontentControl",
	                       "version", VERSION,
                               "website", "https://gitlab.freedesktop.org/pwithnall/malcontent",
	                       "developers", developers,
                               "translator-credits", _("translator-credits"),
                               "license-type", GTK_LICENSE_GPL_2_0,
                         "copyright", _("Copyright © 2019, 2021–2022 Endless Mobile, Inc.\n"
                                        "Copyright © 2022–2025 GNOME Foundation, Inc. and malcontent contributors"),
	                       NULL);
}

static void
on_malcontent_help_shown_finished_cb (GObject      *source,
                                      GAsyncResult *result,
                                      gpointer      user_data)
{
  MctApplication *self = MCT_APPLICATION (user_data);
  GtkUriLauncher *launcher = GTK_URI_LAUNCHER (source);
  g_autoptr(GError) local_error = NULL;

  if (!gtk_uri_launcher_launch_finish (launcher,
                                       result,
                                       &local_error))
    {
      g_autoptr(GtkAlertDialog) dialog = NULL;

      dialog = gtk_alert_dialog_new (_("The help contents could not be displayed"));
      gtk_alert_dialog_set_detail (dialog, local_error->message);
      gtk_alert_dialog_set_modal (dialog, TRUE);
      gtk_alert_dialog_show (dialog, mct_application_get_main_window (self));
    }
}

static void
help_action_cb (GSimpleAction *action, GVariant *parameters, gpointer user_data)
{
  MctApplication *self = MCT_APPLICATION (user_data);
  g_autoptr(GtkUriLauncher) launcher = NULL;

  launcher = gtk_uri_launcher_new ("help:malcontent");
  gtk_uri_launcher_launch (launcher,
                           mct_application_get_main_window (self),
                           NULL,
                           on_malcontent_help_shown_finished_cb,
                           self);
}

static void
quit_action_cb (GSimpleAction *action, GVariant *parameters, gpointer user_data)
{
  MctApplication *self = MCT_APPLICATION (user_data);

  g_application_quit (G_APPLICATION (self));
}

static void select_user_by_command_line_username_cb (GObject      *object,
                                                     GAsyncResult *result,
                                                     void         *user_data);

static void
select_user_by_command_line_username (MctApplication *self)
{
  mct_user_manager_get_user_by_username_async (self->user_manager,
                                               self->command_line_username,
                                               self->cancellable,
                                               select_user_by_command_line_username_cb,
                                               self);
}

static void
select_user_by_command_line_username_cb (GObject      *object,
                                         GAsyncResult *result,
                                         void         *user_data)
{
  MctUserManager *user_manager = MCT_USER_MANAGER (object);
  MctApplication *self = MCT_APPLICATION (user_data);
  g_autoptr(GError) local_error = NULL;
  g_autoptr(MctUser) user = NULL;

  user = mct_user_manager_get_user_by_username_finish (user_manager,
                                                       result, &local_error);
  if (user == NULL)
    {
      g_warning ("Failed to select user ‘%s’: %s", self->command_line_username, local_error->message);
      return;
    }

  adw_navigation_view_set_animate_transitions (self->navigation, FALSE);
  adw_navigation_view_pop_to_tag (self->navigation, "main_page");

  if (!mct_user_selector_select_user (self->user_selector, user))
    g_warning ("Failed to select user ‘%s’", self->command_line_username);

  adw_navigation_view_set_animate_transitions (self->navigation, TRUE);
}

static void
update_main_stack (MctApplication *self)
{
  gboolean is_user_manager_loaded, is_permission_loaded, has_permission;
  const gchar *new_page_name, *old_page_name;
  GtkWidget *new_focus_widget;
  guint n_users;

  /* The implementation of #MctUserManager guarantees that once is-loaded is
   * true, it is never reset to false. */
  is_user_manager_loaded = (mct_user_manager_get_is_loaded (self->user_manager) || self->user_manager_load_error != NULL);
  is_permission_loaded = (self->permission != NULL || self->permission_error != NULL);
  has_permission = (self->permission != NULL && g_permission_get_allowed (self->permission));
  n_users = mct_user_selector_get_n_users (self->user_selector);

  /* Handle any loading errors (including those from getting the permission). */
  if (self->user_manager_load_error != NULL || self->permission_error != NULL)
    {
      adw_status_page_set_title (self->error_page,
                                 _("Failed to load user data from the system"));
      adw_status_page_set_description (self->error_page,
                                       _("Please make sure that the AccountsService is installed and enabled."));

      new_page_name = "error";
      new_focus_widget = NULL;
    }
  else if (is_user_manager_loaded && n_users == 0)
    {
      new_page_name = "no-other-users";
      new_focus_widget = GTK_WIDGET (self->user_accounts_panel_button);
    }
  else if (is_permission_loaded && !has_permission)
    {
      G_GNUC_BEGIN_IGNORE_DEPRECATIONS
      gtk_lock_button_set_permission (self->lock_button, self->permission);
      G_GNUC_END_IGNORE_DEPRECATIONS

      new_page_name = "unlock";
      new_focus_widget = GTK_WIDGET (self->lock_button);
    }
  else if (is_permission_loaded && is_user_manager_loaded)
    {
      new_page_name = "controls";
      new_focus_widget = GTK_WIDGET (self->user_selector);
    }
  else
    {
      new_page_name = "loading";
      new_focus_widget = NULL;
    }

  old_page_name = gtk_stack_get_visible_child_name (self->main_stack);
  gtk_stack_set_visible_child_name (self->main_stack, new_page_name);

  if (new_focus_widget != NULL && !g_str_equal (old_page_name, new_page_name))
    gtk_widget_grab_focus (new_focus_widget);
}

static void
user_selector_notify_selected_user_cb (GObject    *obj,
                                       GParamSpec *pspec,
                                       gpointer    user_data)
{
  MctApplication *self = MCT_APPLICATION (user_data);

  MctUser *user;
  MctUserPage *user_page;

  user = mct_user_selector_get_selected_user (self->user_selector);

  mct_access_page_set_user (self->access_page, user, self->permission);
  mct_time_page_set_user (self->time_page, user);

  user_page = mct_user_page_new (self->dbus_connection, self->policy_manager);
  mct_user_page_set_user (user_page, user, self->permission);
  adw_navigation_view_push (self->navigation, ADW_NAVIGATION_PAGE (user_page));

  update_main_stack (self);
}

static void
user_selector_notify_n_users_cb (GObject    *obj,
                                 GParamSpec *pspec,
                                 gpointer    user_data)
{
  MctApplication *self = MCT_APPLICATION (user_data);

  update_main_stack (self);
}

static void
user_manager_notify_is_loaded_cb (GObject    *obj,
                                  GParamSpec *pspec,
                                  void       *user_data)
{
  MctApplication *self = MCT_APPLICATION (user_data);

  g_clear_signal_handler (&self->user_manager_notify_is_loaded_id, self->user_manager);

  if (!g_permission_get_allowed (self->permission))
    {
      self->command_line_permission_notify_allowed_id =
          g_signal_connect (self->permission,
                            "notify::allowed",
                            G_CALLBACK (command_line_permission_notify_allowed_cb),
                            self);
    }
  else
    {
      select_user_by_command_line_username (self);
    }
}

static void
user_manager_loaded_cb (GObject      *object,
                        GAsyncResult *result,
                        void         *user_data)
{
  MctUserManager *user_manager = MCT_USER_MANAGER (object);
  MctApplication *self = MCT_APPLICATION (user_data);
  g_autoptr(GError) local_error = NULL;

  if (!mct_user_manager_load_finish (user_manager, result, &local_error))
    {
      g_assert (self->user_manager_load_error == NULL);
      self->user_manager_load_error = g_steal_pointer (&local_error);
      g_debug ("Error loading users: %s", self->user_manager_load_error->message);
    }

  mct_user_manager_get_user_by_uid_async (self->user_manager, getuid (), self->cancellable,
                                          user_manager_loaded_user_cb, self);
}

static void
user_manager_loaded_user_cb (GObject      *object,
                             GAsyncResult *result,
                             void         *user_data)
{
  MctUserManager *user_manager = MCT_USER_MANAGER (object);
  MctApplication *self = MCT_APPLICATION (user_data);
  g_autoptr(MctUser) current_user = NULL;
  g_autoptr(GError) local_error = NULL;

  current_user = mct_user_manager_get_user_by_uid_finish (user_manager, result, &local_error);
  if (current_user != NULL)
    mct_user_selector_set_current_user (self->user_selector, current_user);
  else
    g_debug ("Error loading current user: %s", local_error->message);

  update_main_stack (self);
}

static void
user_manager_user_removed_cb (MctUserManager *user_manager,
                              MctUser        *removed_user,
                              void           *user_data)
{
  MctApplication *self = MCT_APPLICATION (user_data);

  if (mct_user_equal (mct_access_page_get_user (self->access_page), removed_user))
    adw_navigation_view_pop_to_tag (self->navigation, "main_page");

  update_main_stack (self);
}

static void
permission_new_cb (GObject      *source_object,
                   GAsyncResult *result,
                   gpointer      user_data)
{
  MctApplication *self = MCT_APPLICATION (user_data);
  g_autoptr(GPermission) permission = NULL;
  g_autoptr(GError) local_error = NULL;

  permission = polkit_permission_new_finish (result, &local_error);
  if (permission == NULL)
    {
      g_assert (self->permission_error == NULL);
      self->permission_error = g_steal_pointer (&local_error);
      g_debug ("Error getting permission: %s", self->permission_error->message);
    }
  else
    {
      g_assert (self->permission == NULL);
      self->permission = g_steal_pointer (&permission);

      g_signal_connect (self->permission, "notify::allowed",
                        G_CALLBACK (permission_notify_allowed_cb), self);
    }

  /* Recalculate the UI. */
  update_main_stack (self);
}

static void
permission_notify_allowed_cb (GObject    *obj,
                              GParamSpec *pspec,
                              gpointer    user_data)
{
  MctApplication *self = MCT_APPLICATION (user_data);

  update_main_stack (self);
}

static void
command_line_permission_notify_allowed_cb (GObject    *obj,
                                           GParamSpec *pspec,
                                           gpointer    user_data)
{
  MctApplication *self = MCT_APPLICATION (user_data);

  g_clear_signal_handler (&self->command_line_permission_notify_allowed_id,
                          self->permission);

  select_user_by_command_line_username (self);
}

static void
user_accounts_panel_button_clicked_cb (GtkButton *button,
                                       gpointer   user_data)
{
  g_autoptr(GError) local_error = NULL;

  if (!g_spawn_command_line_async ("gnome-control-center system users", &local_error))
    {
      g_warning ("Error opening GNOME Control Center: %s",
                 local_error->message);
      return;
    }
}

/**
 * mct_application_new:
 *
 * Create a new [class@Malcontent.Application].
 *
 * Returns: (transfer full): a new application object
 * Since: 0.5.0
 */
MctApplication *
mct_application_new (void)
{
  return g_object_new (MCT_TYPE_APPLICATION, NULL);
}
