/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* cc-wwan-device-page.c
 *
 * Copyright 2019 Purism SPC
 *
 * 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 <http://www.gnu.org/licenses/>.
 *
 * Author(s):
 *   Mohammed Sadiq <sadiq@sadiqpk.org>
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#undef G_LOG_DOMAIN
#define G_LOG_DOMAIN "cc-wwan-device-page"

#include <config.h>
#include <glib/gi18n.h>
#include <libmm-glib.h>
#define GCR_API_SUBJECT_TO_CHANGE
#include <gcr/gcr.h>

#include "list-box-helper.h"
#include "cc-list-row.h"
#include "cc-wwan-data.h"
#include "cc-wwan-mode-dialog.h"
#include "cc-wwan-network-dialog.h"
#include "cc-wwan-details-dialog.h"
#include "cc-wwan-sim-lock-dialog.h"
#include "cc-wwan-apn-dialog.h"
#include "cc-wwan-device-page.h"
#include "cc-wwan-resources.h"

#include "shell/cc-application.h"
#include "shell/cc-debug.h"
#include "shell/cc-object-storage.h"

/**
 * @short_description: Device settings page
 * @include: "cc-wwan-device-page.h"
 *
 * The Device page allows users to configure device
 * settings.  Please note that there is no one-to-one
 * maping for a device settings page and a physical
 * device.  Say, if a device have two SIM card slots,
 * there should be two device pages, one for each SIM.
 */

struct _CcWwanDevicePage
{
  GtkBox         parent_instance;

  GtkListBox    *advanced_settings_list;
  CcListRow     *apn_settings_row;
  CcListRow     *data_enable_row;
  CcListRow     *data_roaming_row;
  GtkListBox    *data_settings_list;
  CcListRow     *details_row;
  GtkStack      *main_stack;
  CcListRow     *network_mode_row;
  CcListRow     *network_name_row;
  GtkListBox    *network_settings_list;
  CcListRow     *sim_lock_row;
  GtkButton     *unlock_button;

  GtkLabel      *notification_label;

  CcWwanDevice  *device;
  CcWwanData    *wwan_data;
  GDBusProxy    *wwan_proxy;

  CcWwanApnDialog     *apn_dialog;
  CcWwanDetailsDialog *details_dialog;
  CcWwanModeDialog    *network_mode_dialog;
  CcWwanNetworkDialog *network_dialog;
  CcWwanSimLockDialog *sim_lock_dialog;

  gint                 sim_index;
  /* Set if a change is triggered in a signal’s callback,
   * to avoid re-triggering of callback.  This is used
   * instead of blocking handlers where the signal may be
   * emitted async and the block/unblock may not work right
   */
  gboolean is_self_change;
  gboolean is_retry;
};

G_DEFINE_TYPE (CcWwanDevicePage, cc_wwan_device_page, GTK_TYPE_BOX)

enum {
  PROP_0,
  PROP_DEVICE,
  N_PROPS
};

static GParamSpec *properties[N_PROPS];

static void
wwan_device_page_handle_data_row (CcWwanDevicePage *self,
                                  CcListRow        *data_row)
{
  gboolean active;

  /* The user dismissed the dialog for selecting default APN */
  if (cc_wwan_data_get_default_apn (self->wwan_data) == NULL)
    {
      self->is_self_change = TRUE;
      gtk_widget_activate (GTK_WIDGET (data_row));

      return;
    }

  active = cc_list_row_get_active (data_row);

  if (data_row == self->data_enable_row)
    cc_wwan_data_set_enabled (self->wwan_data, active);
  else
    cc_wwan_data_set_roaming_enabled (self->wwan_data, active);

  cc_wwan_data_save_settings (self->wwan_data, NULL, NULL, NULL);
}

static gboolean
wwan_apn_dialog_closed_cb (CcWwanDevicePage *self)
{
  CcListRow *data_row;

  if (gtk_widget_in_destruction (GTK_WIDGET (self)))
    return FALSE;

  data_row = g_object_get_data (G_OBJECT (self->apn_dialog), "row");
  g_object_set_data (G_OBJECT (self->apn_dialog), "row", NULL);

  if (data_row)
    wwan_device_page_handle_data_row (self, data_row);

  return FALSE;
}

static void
wwan_data_show_apn_dialog (CcWwanDevicePage *self)
{
  GtkWindow *top_level;

  top_level = GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self)));

  if (!self->apn_dialog)
    {
      self->apn_dialog = cc_wwan_apn_dialog_new (top_level, self->device);
      g_signal_connect_object (self->apn_dialog, "unmap",
                               G_CALLBACK (wwan_apn_dialog_closed_cb),
                               self, G_CONNECT_SWAPPED);
    }

  gtk_widget_show (GTK_WIDGET (self->apn_dialog));
}

static GcrPrompt *
cc_wwan_device_page_new_prompt (CcWwanDevicePage *self,
                                MMModemLock       lock)
{
  GcrPrompt *prompt;
  g_autoptr(GError) error = NULL;
  g_autofree gchar *description = NULL;
  g_autofree gchar *warning = NULL;
  const gchar *message = NULL;
  guint num;

  prompt = GCR_PROMPT (gcr_system_prompt_open (-1, NULL, &error));

  if (error)
    {
      g_warning ("Error opening Prompt: %s", error->message);
      return NULL;
    }

  gcr_prompt_set_title (prompt, _("Unlock SIM card"));
  gcr_prompt_set_continue_label (prompt, _("Unlock"));
  gcr_prompt_set_cancel_label (prompt, _("Cancel"));

  if (lock == MM_MODEM_LOCK_SIM_PIN)
    {
      description = g_strdup_printf (_("Please provide PIN code for SIM %d"), self->sim_index);
      message = _("Enter PIN to unlock your SIM card");
    }
  else if (lock == MM_MODEM_LOCK_SIM_PUK)
    {
      description = g_strdup_printf (_("Please provide PUK code for SIM %d"), self->sim_index);
      message = _("Enter PUK to unlock your SIM card");
    }
  else
    {
      g_warn_if_reached ();
      g_object_unref (prompt);

      return NULL;
    }

  gcr_prompt_set_description (prompt, description);
  gcr_prompt_set_message (prompt, message);

  num = cc_wwan_device_get_unlock_retries (self->device, lock);

  if (num != MM_UNLOCK_RETRIES_UNKNOWN)
    {
      if (self->is_retry)
        warning = g_strdup_printf (ngettext ("Wrong password entered. You have %1$u try left",
                                             "Wrong password entered. You have %1$u tries left", num), num);
      else
        warning = g_strdup_printf (ngettext ("You have %u try left",
                                             "You have %u tries left", num), num);
    }
  else if (self->is_retry)
    {
      warning = g_strdup (_("Wrong password entered."));
    }

  gcr_prompt_set_warning (prompt, warning);

  return prompt;
}

static void
wwan_update_unlock_button (CcWwanDevicePage *self)
{
  gtk_button_set_label (self->unlock_button, _("Unlock"));
  gtk_widget_set_sensitive (GTK_WIDGET (self->unlock_button), TRUE);
}

static void
cc_wwan_device_page_unlocked_cb (GObject      *object,
                                 GAsyncResult *result,
                                 gpointer      user_data)
{
  CcWwanDevicePage *self = user_data;
  wwan_update_unlock_button (self);
}

static void
wwan_device_unlock_clicked_cb (CcWwanDevicePage *self)
{
  g_autoptr(GError) error = NULL;
  GcrPrompt *prompt;
  const gchar *password, *warning;
  const gchar *pin = "";
  const gchar *puk = "";
  MMModemLock lock;

  lock = cc_wwan_device_get_lock (self->device);
  password = "";

  if (lock != MM_MODEM_LOCK_SIM_PIN &&
      lock != MM_MODEM_LOCK_SIM_PUK)
    g_return_if_reached ();

  if (lock == MM_MODEM_LOCK_SIM_PUK)
    {
      prompt = cc_wwan_device_page_new_prompt (self, lock);

      warning = _("PUK code should be an 8 digit number");
      while (password && !cc_wwan_device_pin_valid (password, lock))
        {
          password = gcr_prompt_password (prompt, NULL, &error);
          gcr_prompt_set_warning (prompt, warning);
        }

      puk = g_strdup (password);
      password = "";
      gcr_prompt_close (prompt);
      g_object_unref (prompt);

      if (error)
        g_warning ("Error: %s", error->message);

      /* Error or User cancelled PUK */
      if (!puk)
        return;
    }

  prompt = cc_wwan_device_page_new_prompt (self, MM_MODEM_LOCK_SIM_PIN);
  if (lock == MM_MODEM_LOCK_SIM_PUK)
    {
      gcr_prompt_set_password_new (prompt, TRUE);
      gcr_prompt_set_message (prompt, _("Enter New PIN"));
      gcr_prompt_set_warning (prompt, "");
    }

  warning = _("PIN code should be a 4-8 digit number");
  while (password && !cc_wwan_device_pin_valid (password, MM_MODEM_LOCK_SIM_PIN))
    {
      password = gcr_prompt_password (prompt, NULL, &error);
      gcr_prompt_set_warning (prompt, warning);
    }

  pin = g_strdup (password);
  gcr_prompt_close (prompt);
  g_object_unref (prompt);

  if (error)
    g_warning ("Error: %s", error->message);

  /* Error or User cancelled PIN */
  if (!pin)
    return;

  gtk_button_set_label (self->unlock_button, _("Unlocking…"));
  gtk_widget_set_sensitive (GTK_WIDGET (self->unlock_button), FALSE);

  if (lock == MM_MODEM_LOCK_SIM_PIN)
    cc_wwan_device_send_pin (self->device, pin,
                             NULL, /* cancellable */
                             cc_wwan_device_page_unlocked_cb,
                             self);
  else if (lock == MM_MODEM_LOCK_SIM_PUK)
    {
      cc_wwan_device_send_puk (self->device, puk, pin,
                               NULL, /* Cancellable */
                               cc_wwan_device_page_unlocked_cb,
                               self);
    }
  else
    {
      g_warn_if_reached ();
    }
}

static void
wwan_data_settings_changed_cb (CcWwanDevicePage *self,
                               GParamSpec       *pspec,
                               CcListRow        *data_row)
{
  if (self->is_self_change)
    {
      self->is_self_change = FALSE;
      return;
    }

  if (cc_wwan_data_get_default_apn (self->wwan_data) == NULL)
    {
      wwan_data_show_apn_dialog (self);
      g_object_set_data (G_OBJECT (self->apn_dialog), "row", data_row);
    }
  else
    {
      wwan_device_page_handle_data_row (self, data_row);
    }
}

static void
wwan_network_settings_activated_cb (CcWwanDevicePage *self,
                                    CcListRow        *row)
{
  GtkWidget *dialog;
  GtkWindow *top_level;

  top_level = GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self)));

  if (row == self->network_mode_row)
    {
      if (!self->network_mode_dialog)
        self->network_mode_dialog = cc_wwan_mode_dialog_new (top_level, self->device);

      dialog = GTK_WIDGET (self->network_mode_dialog);
    }
  else if (row == self->network_name_row)
    {
      if (!self->network_dialog)
        self->network_dialog = cc_wwan_network_dialog_new (top_level, self->device);

      dialog = GTK_WIDGET (self->network_dialog);
    }
  else
    {
      return;
    }

  gtk_widget_show (dialog);
}

static void
wwan_advanced_settings_activated_cb (CcWwanDevicePage *self,
                                     CcListRow        *row)
{
  GtkWindow *top_level;

  top_level = GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self)));

  if (row == self->sim_lock_row)
    {
      if (!self->sim_lock_dialog)
        self->sim_lock_dialog = cc_wwan_sim_lock_dialog_new (top_level, self->device);
      gtk_widget_show (GTK_WIDGET (self->sim_lock_dialog));
    }
  else if (row == self->details_row)
    {
      if (!self->details_dialog)
        self->details_dialog = cc_wwan_details_dialog_new (top_level, self->device);
      gtk_widget_show (GTK_WIDGET (self->details_dialog));
    }
  else if (row == self->apn_settings_row)
    {
      wwan_data_show_apn_dialog (self);
    }
  else
    {
      g_return_if_reached ();
    }
}

static void
cc_wwan_device_page_update_data (CcWwanDevicePage *self)
{
  gboolean has_data;

  if (self->wwan_data == cc_wwan_device_get_data (self->device))
    return;

  self->wwan_data = cc_wwan_device_get_data (self->device);
  has_data = self->wwan_data != NULL;

  gtk_widget_set_sensitive (GTK_WIDGET (self->data_settings_list), has_data);
  gtk_widget_set_sensitive (GTK_WIDGET (self->apn_settings_row), has_data);

  if (!has_data)
    return;

  g_signal_handlers_block_by_func (self->data_roaming_row,
                                   wwan_data_settings_changed_cb, self);
  g_signal_handlers_block_by_func (self->data_enable_row,
                                   wwan_data_settings_changed_cb, self);

  g_object_set (self->data_roaming_row, "active",
                cc_wwan_data_get_roaming_enabled (self->wwan_data), NULL);

  g_object_set (self->data_enable_row, "active",
                cc_wwan_data_get_enabled (self->wwan_data), NULL);

  g_signal_handlers_unblock_by_func (self->data_roaming_row,
                                     wwan_data_settings_changed_cb, self);
  g_signal_handlers_unblock_by_func (self->data_enable_row,
                                     wwan_data_settings_changed_cb, self);
}

static void
cc_wwan_device_page_update (CcWwanDevicePage *self)
{
  GtkStack *main_stack;
  MMModemLock lock;

  main_stack = self->main_stack;
  if (!cc_wwan_device_has_sim (self->device))
    gtk_stack_set_visible_child_name (main_stack, "no-sim-view");
  else if ((lock = cc_wwan_device_get_lock (self->device)) == MM_MODEM_LOCK_SIM_PIN ||
           lock == MM_MODEM_LOCK_SIM_PUK)
    gtk_stack_set_visible_child_name (main_stack, "sim-lock-view");
  else
    gtk_stack_set_visible_child_name (main_stack, "settings-view");
}

static void
cc_wwan_locks_changed_cb (CcWwanDevicePage *self)
{
  const gchar *label;

  if (cc_wwan_device_get_sim_lock (self->device))
    label = _("Enabled");
  else
    label = _("Disabled");

  cc_list_row_set_secondary_label (self->sim_lock_row, label);
  cc_wwan_device_page_update (self);
}

static void
cc_wwan_device_page_set_property (GObject      *object,
                                  guint         prop_id,
                                  const GValue *value,
                                  GParamSpec   *pspec)
{
  CcWwanDevicePage *self = (CcWwanDevicePage *)object;

  switch (prop_id)
    {
    case PROP_DEVICE:
      self->device = g_value_dup_object (value);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
cc_wwan_device_page_constructed (GObject *object)
{
  CcWwanDevicePage *self = (CcWwanDevicePage *)object;

  G_OBJECT_CLASS (cc_wwan_device_page_parent_class)->constructed (object);

  cc_wwan_device_page_update_data (self);

  g_object_bind_property (self->device, "operator-name",
                          self->network_name_row, "secondary-label",
                          G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);

  g_object_bind_property (self->device, "network-mode",
                          self->network_mode_row, "secondary-label",
                          G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);

  g_signal_connect_object (self->device, "notify::enabled-locks",
                           (GCallback)cc_wwan_locks_changed_cb,
                           self, G_CONNECT_SWAPPED);

  g_signal_connect_object (self->device, "notify::has-data",
                           (GCallback)cc_wwan_device_page_update_data,
                           self, G_CONNECT_SWAPPED);

  cc_wwan_device_page_update (self);
  cc_wwan_locks_changed_cb (self);
}

static void
cc_wwan_device_page_dispose (GObject *object)
{
  CcWwanDevicePage *self = (CcWwanDevicePage *)object;

  g_clear_pointer ((GtkWidget **)&self->apn_dialog, gtk_widget_destroy);
  g_clear_pointer ((GtkWidget **)&self->details_dialog, gtk_widget_destroy);
  g_clear_pointer ((GtkWidget **)&self->network_mode_dialog, gtk_widget_destroy);
  g_clear_pointer ((GtkWidget **)&self->network_dialog, gtk_widget_destroy);
  g_clear_pointer ((GtkWidget **)&self->sim_lock_dialog, gtk_widget_destroy);

  g_clear_object (&self->wwan_proxy);
  g_clear_object (&self->device);

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

static void
cc_wwan_device_page_class_init (CcWwanDevicePageClass *klass)
{
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->set_property = cc_wwan_device_page_set_property;
  object_class->constructed  = cc_wwan_device_page_constructed;
  object_class->dispose = cc_wwan_device_page_dispose;

  g_type_ensure (CC_TYPE_WWAN_DEVICE);

  properties[PROP_DEVICE] =
    g_param_spec_object ("device",
                         "Device",
                         "The WWAN Device",
                         CC_TYPE_WWAN_DEVICE,
                         G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY);

  g_object_class_install_properties (object_class, N_PROPS, properties);

  gtk_widget_class_set_template_from_resource (widget_class,
                                               "/org/gnome/control-center/wwan/cc-wwan-device-page.ui");

  gtk_widget_class_bind_template_child (widget_class, CcWwanDevicePage, advanced_settings_list);
  gtk_widget_class_bind_template_child (widget_class, CcWwanDevicePage, apn_settings_row);
  gtk_widget_class_bind_template_child (widget_class, CcWwanDevicePage, data_enable_row);
  gtk_widget_class_bind_template_child (widget_class, CcWwanDevicePage, data_roaming_row);
  gtk_widget_class_bind_template_child (widget_class, CcWwanDevicePage, data_settings_list);
  gtk_widget_class_bind_template_child (widget_class, CcWwanDevicePage, details_row);
  gtk_widget_class_bind_template_child (widget_class, CcWwanDevicePage, main_stack);
  gtk_widget_class_bind_template_child (widget_class, CcWwanDevicePage, network_mode_row);
  gtk_widget_class_bind_template_child (widget_class, CcWwanDevicePage, network_name_row);
  gtk_widget_class_bind_template_child (widget_class, CcWwanDevicePage, network_settings_list);
  gtk_widget_class_bind_template_child (widget_class, CcWwanDevicePage, sim_lock_row);
  gtk_widget_class_bind_template_child (widget_class, CcWwanDevicePage, unlock_button);

  gtk_widget_class_bind_template_callback (widget_class, wwan_device_unlock_clicked_cb);
  gtk_widget_class_bind_template_callback (widget_class, wwan_data_settings_changed_cb);
  gtk_widget_class_bind_template_callback (widget_class, wwan_network_settings_activated_cb);
  gtk_widget_class_bind_template_callback (widget_class, wwan_advanced_settings_activated_cb);
}

static void
cc_wwan_device_page_init (CcWwanDevicePage *self)
{
  gtk_widget_init_template (GTK_WIDGET (self));

  gtk_list_box_set_header_func (self->data_settings_list,
                                cc_list_box_update_header_func,
                                NULL, NULL);

  gtk_list_box_set_header_func (self->network_settings_list,
                                cc_list_box_update_header_func,
                                NULL, NULL);

  gtk_list_box_set_header_func (self->advanced_settings_list,
                                cc_list_box_update_header_func,
                                NULL, NULL);
}

static void
cc_wwan_error_changed_cb (CcWwanDevicePage *self)
{
  const gchar *message;

  message = cc_wwan_device_get_simple_error (self->device);

  if (!message)
    return;

  /*
   * The label is first set to empty, which will result in
   * the revealer to be closed.  Then the real label is
   * set.  This will animate the revealer which can bring
   * the user's attention.
   */
  gtk_label_set_label (self->notification_label, "");
  gtk_label_set_label (self->notification_label, message);
}

CcWwanDevicePage *
cc_wwan_device_page_new (CcWwanDevice *device,
                         GtkWidget    *notification_label)
{
  CcWwanDevicePage *self;

  g_return_val_if_fail (CC_IS_WWAN_DEVICE (device), NULL);

  self = g_object_new (CC_TYPE_WWAN_DEVICE_PAGE,
                       "device", device,
                       NULL);

  self->notification_label = GTK_LABEL (notification_label);

  g_signal_connect_object (self->device, "notify::error",
                           G_CALLBACK (cc_wwan_error_changed_cb),
                           self, G_CONNECT_SWAPPED);

  return self;
}

CcWwanDevice *
cc_wwan_device_page_get_device (CcWwanDevicePage *self)
{
  g_return_val_if_fail (CC_IS_WWAN_DEVICE_PAGE (self), NULL);

  return self->device;
}

void
cc_wwan_device_page_set_sim_index (CcWwanDevicePage *self,
                                   gint              sim_index)
{
  g_return_if_fail (CC_IS_WWAN_DEVICE_PAGE (self));
  g_return_if_fail (sim_index >= 1);

  self->sim_index = sim_index;
}
