/*
 * Copyright © 2018 Red Hat Inc.
 *
 * 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/>.
 */

#include "cc-wifi-connection-list.h"
#include "cc-wifi-connection-row.h"

struct _CcWifiConnectionList
{
  AdwBin         parent_instance;

  GtkListBox    *listbox;

  guint          freeze_count;
  gboolean       updating;

  gboolean       checkable;
  gboolean       hide_unavailable;
  gboolean       show_aps;

  NMClient      *client;
  NMDeviceWifi  *device;

  NMConnection  *last_active;

  GPtrArray     *connections;
  GPtrArray     *connections_row;

  /* AP SSID cache stores the APs SSID used for assigning it to a row.
   * This is necessary to efficiently remove it when its SSID changes.
   *
   * Note that we only group APs that cannot be assigned to a connection
   * by the SSID. In principle this is wrong, because other attributes may
   * be different rendering them separate networks.
   * In practice this will almost never happen, and if it does, we just
   * show and select the strongest AP.
   */
  GHashTable    *ap_ssid_cache;
  GHashTable    *ssid_to_row;
};

static void on_device_ap_added_cb   (CcWifiConnectionList *self,
                                     NMAccessPoint        *ap,
                                     NMDeviceWifi         *device);
static void on_device_ap_removed_cb (CcWifiConnectionList *self,
                                     NMAccessPoint        *ap,
                                     NMDeviceWifi         *device);
static void on_row_configured_cb    (CcWifiConnectionList *self,
                                     CcWifiConnectionRow  *row);

G_DEFINE_TYPE (CcWifiConnectionList, cc_wifi_connection_list, ADW_TYPE_BIN)

enum
{
  PROP_0,
  PROP_CHECKABLE,
  PROP_HIDE_UNAVAILABLE,
  PROP_SHOW_APS,
  PROP_CLIENT,
  PROP_DEVICE,
  PROP_LAST
};

static GParamSpec *props [PROP_LAST];

static GBytes*
new_hashable_ssid (GBytes *ssid)
{
  GBytes *res;
  const guint8 *data;
  gsize size;

  /* This is what nm_utils_same_ssid does, but returning it so that we can
   * use the result in other ways (i.e. hash table lookups). */
  data = g_bytes_get_data ((GBytes*) ssid, &size);
  if (data[size-1] == '\0')
    size -= 1;
  res = g_bytes_new (data, size);

  return res;
}

static gboolean
connection_ignored (NMConnection *connection)
{
  NMSettingWireless *sw;

  /* Ignore AP and adhoc modes (i.e. accept infrastructure or empty) */
  sw = nm_connection_get_setting_wireless (connection);
  if (!sw)
    return TRUE;
  if (g_strcmp0 (nm_setting_wireless_get_mode (sw), "adhoc") == 0 ||
      g_strcmp0 (nm_setting_wireless_get_mode (sw), "ap") == 0)
    {
      return TRUE;
    }

  return FALSE;
}

static CcWifiConnectionRow*
cc_wifi_connection_list_row_add (CcWifiConnectionList *self,
                                 NMConnection         *connection,
                                 NMAccessPoint        *ap)
{
  CcWifiConnectionRow *res;
  g_autoptr(GPtrArray) aps = NULL;

  if (ap)
    {
      aps = g_ptr_array_new ();
      g_ptr_array_add (aps, ap);
    }

  res = cc_wifi_connection_row_new (self->device,
                                    connection,
                                    aps,
                                    self->checkable);
  gtk_list_box_append (self->listbox, GTK_WIDGET (res));

  g_signal_connect_object (res, "configure", G_CALLBACK (on_row_configured_cb), self, G_CONNECT_SWAPPED);

  return res;
}

static void
clear_widget (CcWifiConnectionList *self)
{
  const GPtrArray *aps;
  GHashTableIter iter;
  CcWifiConnectionRow *row;
  gint i;

  /* Clear everything; disconnect all AP signals first */
  aps = nm_device_wifi_get_access_points (self->device);
  for (i = 0; i < aps->len; i++)
    g_signal_handlers_disconnect_by_data (g_ptr_array_index (aps, i), self);

  /* Remove all AP only rows */
  g_hash_table_iter_init (&iter, self->ssid_to_row);
  while (g_hash_table_iter_next (&iter, NULL, (gpointer*) &row))
    {
      g_hash_table_iter_remove (&iter);
      gtk_list_box_remove (self->listbox, GTK_WIDGET (row));
    }

  /* Remove all connection rows */
  for (i = 0; i < self->connections_row->len; i++)
    {
      if (!g_ptr_array_index (self->connections_row, i))
        continue;

      row = g_ptr_array_index (self->connections_row, i);
      g_ptr_array_index (self->connections_row, i) = NULL;
      gtk_list_box_remove (self->listbox, GTK_WIDGET (row));
     }

  /* Reset the internal state */
  g_ptr_array_set_size (self->connections, 0);
  g_ptr_array_set_size (self->connections_row, 0);
  g_hash_table_remove_all (self->ssid_to_row);
  g_hash_table_remove_all (self->ap_ssid_cache);
}

static void
update_connections (CcWifiConnectionList *self)
{
  const GPtrArray *aps;
  const GPtrArray *acs_client;
  g_autoptr(GPtrArray) acs = NULL;
  NMActiveConnection *ac;
  NMConnection *ac_con = NULL;
  gint i;

  /* We don't want full UI rebuilds during some UI interactions, so allow freezing the list. */
  if (self->freeze_count > 0)
    return;

  /* Prevent recursion (maybe move this into an idle handler instead?) */
  if (self->updating)
    return;
  self->updating = TRUE;

  clear_widget (self);

  /* Copy the new connections; also create a row if we show unavailable
   * connections */
  acs_client = nm_client_get_connections (self->client);

  acs = g_ptr_array_new_full (acs_client->len + 1, NULL);
  for (i = 0; i < acs_client->len; i++)
    g_ptr_array_add (acs, g_ptr_array_index (acs_client, i));

  ac = nm_device_get_active_connection (NM_DEVICE (self->device));
  if (ac)
    ac_con = NM_CONNECTION (nm_active_connection_get_connection (ac));

  if (ac_con && !g_ptr_array_find (acs, ac_con, NULL))
    {
      g_debug ("Adding remote connection for active connection");
      g_ptr_array_add (acs, g_object_ref (ac_con));
    }

  for (i = 0; i < acs->len; i++)
    {
      NMConnection *con;

      con = g_ptr_array_index (acs, i);
      if (connection_ignored (con))
        continue;

      g_ptr_array_add (self->connections, g_object_ref (con));
      if (self->hide_unavailable && con != ac_con)
        g_ptr_array_add (self->connections_row, NULL);
      else
        g_ptr_array_add (self->connections_row,
                         cc_wifi_connection_list_row_add (self, con,
                         NULL));
    }

  /* Coldplug all known APs again */
  aps = nm_device_wifi_get_access_points (self->device);
  for (i = 0; i < aps->len; i++)
    on_device_ap_added_cb (self, g_ptr_array_index (aps, i), self->device);

  self->updating = FALSE;
}

static void
on_row_configured_cb (CcWifiConnectionList *self, CcWifiConnectionRow *row)
{
  g_signal_emit_by_name (self, "configure", row);
}

static void
on_access_point_property_changed (CcWifiConnectionList *self,
                                  GParamSpec           *pspec,
                                  NMAccessPoint        *ap)
{
  CcWifiConnectionRow *row;
  GBytes *ssid;
  gboolean has_connection = FALSE;
  gint i;

  /* If the SSID changed then the AP needs to be added/removed from rows.
   * Do this by simulating an AP addition/removal.  */
  if (g_str_equal (pspec->name, NM_ACCESS_POINT_SSID))
    {
      g_debug ("Simulating add/remove for SSID change");
      on_device_ap_removed_cb (self, ap, self->device);
      on_device_ap_added_cb (self, ap, self->device);
      return;
    }

  /* Otherwise, find all rows that contain the AP and update it. Do this by
   * first searching all rows with connections, and then looking it up in the
   * SSID rows if not found. */
  for (i = 0; i < self->connections_row->len; i++)
    {
      row = g_ptr_array_index (self->connections_row, i);
      if (row && cc_wifi_connection_row_has_access_point (row, ap))
        {
          cc_wifi_connection_row_update (row);
          has_connection = TRUE;
        }
    }

  if (!self->show_aps || has_connection)
    return;

  ssid = g_hash_table_lookup (self->ap_ssid_cache, ap);
  if (!ssid)
    return;

  row = g_hash_table_lookup (self->ssid_to_row, ssid);
  if (!row)
    g_assert_not_reached ();
  else
    cc_wifi_connection_row_update (row);
}

static void
on_device_ap_added_cb (CcWifiConnectionList *self,
                       NMAccessPoint        *ap,
                       NMDeviceWifi         *device)
{
  g_autoptr(GPtrArray) connections = NULL;
  CcWifiConnectionRow *row;
  GBytes *ap_ssid;
  g_autoptr(GBytes) ssid = NULL;
  guint i, j;

  g_signal_connect_object (ap, "notify",
                           G_CALLBACK (on_access_point_property_changed),
                           self, G_CONNECT_SWAPPED);

  connections = nm_access_point_filter_connections (ap, self->connections);

  /* If this is the active AP, then add the active connection to the list. This
   * is a workaround because nm_access_pointer_filter_connections() will not
   * include it otherwise.
   * So it seems like the dummy AP entry that NM creates internally is not actually
   * compatible with the connection that is being activated.
   */
  if (ap == nm_device_wifi_get_active_access_point (device))
    {
      NMActiveConnection *ac;
      NMConnection *ac_con;

      ac = nm_device_get_active_connection (NM_DEVICE (self->device));

      if (ac)
        {
          guint idx;

          ac_con = NM_CONNECTION (nm_active_connection_get_connection (ac));

          if (!g_ptr_array_find (connections, ac_con, NULL) &&
              g_ptr_array_find (self->connections, ac_con, &idx))
            {
              g_debug ("Adding active connection to list of valid connections for AP");
              g_ptr_array_add (connections, g_object_ref (ac_con));
            }
        }
    }

  /* Add the AP to all connection related rows, creating the row if neccessary. */
  for (i = 0; i < connections->len; i++)
    {
      gboolean found = g_ptr_array_find (self->connections, g_ptr_array_index (connections, i), &j);

      g_assert (found);

      row = g_ptr_array_index (self->connections_row, j);
      if (!row)
        row = cc_wifi_connection_list_row_add (self, g_ptr_array_index (connections, i), NULL);
      cc_wifi_connection_row_add_access_point (row, ap);
      g_ptr_array_index (self->connections_row, j) = row;
    }

  if (connections->len > 0)
    return;

  if (!self->show_aps)
    return;

  /* The AP is not compatible to any known connection, generate an entry for the
   * SSID or add to existing one. However, not for hidden APs that don't have an SSID.
   */
  ap_ssid = nm_access_point_get_ssid (ap);
  if (ap_ssid == NULL)
    return;
  ssid = new_hashable_ssid (ap_ssid);

  g_hash_table_insert (self->ap_ssid_cache, ap, g_bytes_ref (ssid));

  row = g_hash_table_lookup (self->ssid_to_row, ssid);
  if (!row)
    {
      row = cc_wifi_connection_list_row_add (self, NULL, ap);

      g_hash_table_insert (self->ssid_to_row, g_bytes_ref (ssid), row);
    }
  else
    {
      cc_wifi_connection_row_add_access_point (row, ap);
    }
}

static void
on_device_ap_removed_cb (CcWifiConnectionList *self,
                         NMAccessPoint        *ap,
                         NMDeviceWifi         *device)
{
  CcWifiConnectionRow *row;
  g_autoptr(GBytes) ssid = NULL;
  gboolean found = FALSE;
  gint i;

  g_signal_handlers_disconnect_by_data (ap, self);

  /* Find any connection related row with the AP and remove the AP from it. Remove the
   * row if it was the last AP and we are hiding unavailable connections. */
  for (i = 0; i < self->connections_row->len; i++)
    {
      row = g_ptr_array_index (self->connections_row, i);
      if (row && cc_wifi_connection_row_remove_access_point (row, ap))
        {
          found = TRUE;

          if (self->hide_unavailable)
            {
              g_ptr_array_index (self->connections_row, i) = NULL;
              gtk_list_box_remove (self->listbox, GTK_WIDGET (row));
            }
        }
    }

  if (found || !self->show_aps)
    return;

  /* If the AP was inserted into a row without a connection, then we will get an
   * SSID for it here. */
  g_hash_table_steal_extended (self->ap_ssid_cache, ap, NULL, (gpointer*) &ssid);
  if (!ssid)
    return;

  /* And we can update the row (possibly removing it) */
  row = g_hash_table_lookup (self->ssid_to_row, ssid);
  g_assert (row != NULL);

  if (cc_wifi_connection_row_remove_access_point (row, ap))
    {
      g_hash_table_remove (self->ssid_to_row, ssid);
      gtk_list_box_remove (self->listbox, GTK_WIDGET (row));
    }
}

static void
on_client_connection_added_cb (CcWifiConnectionList *self,
                               NMConnection         *connection,
                               NMClient             *client)
{
  if (!nm_device_connection_compatible (NM_DEVICE (self->device), connection, NULL))
    return;

  if (connection_ignored (connection))
    return;

  /* The approach we take to handle connection changes is to do a full rebuild.
   * It happens seldom enough to make this feasible.
   */
  update_connections (self);
}

static void
on_client_connection_removed_cb (CcWifiConnectionList *self,
                                 NMConnection         *connection,
                                 NMClient             *client)
{
  if (!g_ptr_array_find (self->connections, connection, NULL))
    return;

  /* The approach we take to handle connection changes is to do a full rebuild.
   * It happens seldom enough to make this feasible.
   */
  update_connections (self);
}

static void
on_device_state_changed_cb (CcWifiConnectionList *self,
                            GParamSpec           *pspec,
                            NMDeviceWifi         *device)
{
  NMActiveConnection *ac;
  NMConnection *connection = NULL;
  guint idx;

  ac = nm_device_get_active_connection (NM_DEVICE (self->device));
  if (ac)
    connection = NM_CONNECTION (nm_active_connection_get_connection (ac));

  /* Just update the corresponding row if the AC is still the same. */
  if (self->last_active == connection &&
      g_ptr_array_find (self->connections, connection, &idx) &&
      g_ptr_array_index (self->connections_row, idx))
    {
      cc_wifi_connection_row_update (g_ptr_array_index (self->connections_row, idx));
      return;
    }

  /* Give up and do a full update. */
  update_connections (self);
  self->last_active = connection;
}

static void
on_device_active_ap_changed_cb (CcWifiConnectionList *self,
                                GParamSpec           *pspec,
                                NMDeviceWifi         *device)
{
  NMAccessPoint *ap;
  /* We need to make sure the active AP is grouped with the active connection.
   * Do so by simply removing and adding it.
   *
   * This is necessary because the AP is added before this property
   * is updated. */
  ap = nm_device_wifi_get_active_access_point (self->device);
  if (ap)
    {
      g_debug ("Simulating add/remove for active AP change");
      on_device_ap_removed_cb (self, ap, self->device);
      on_device_ap_added_cb (self, ap, self->device);
    }
}

static void
cc_wifi_connection_list_dispose (GObject *object)
{
  CcWifiConnectionList *self = (CcWifiConnectionList *)object;

  /* Prevent any further updates; clear_widget must not indirectly recurse
   * through updates_connections */
  self->updating = TRUE;

  /* Drop all external references */
  clear_widget (self);

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

static void
cc_wifi_connection_list_finalize (GObject *object)
{
  CcWifiConnectionList *self = (CcWifiConnectionList *)object;

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

  g_clear_pointer (&self->connections, g_ptr_array_unref);
  g_clear_pointer (&self->connections_row, g_ptr_array_unref);
  g_clear_pointer (&self->ssid_to_row, g_hash_table_unref);
  g_clear_pointer (&self->ap_ssid_cache, g_hash_table_unref);

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

static void
cc_wifi_connection_list_constructed (GObject *object)
{
  CcWifiConnectionList *self = (CcWifiConnectionList *)object;

  G_OBJECT_CLASS (cc_wifi_connection_list_parent_class)->constructed (object);

  g_assert (self->client);
  g_assert (self->device);

  g_signal_connect_object (self->client, "connection-added",
                           G_CALLBACK (on_client_connection_added_cb),
                           self, G_CONNECT_SWAPPED);
  g_signal_connect_object (self->client, "connection-removed",
                           G_CALLBACK (on_client_connection_removed_cb),
                           self, G_CONNECT_SWAPPED);

  g_signal_connect_object (self->device, "access-point-added",
                           G_CALLBACK (on_device_ap_added_cb),
                           self, G_CONNECT_SWAPPED);
  g_signal_connect_object (self->device, "access-point-removed",
                           G_CALLBACK (on_device_ap_removed_cb),
                           self, G_CONNECT_SWAPPED);

  g_signal_connect_object (self->device, "notify::state",
                           G_CALLBACK (on_device_state_changed_cb),
                           self, G_CONNECT_SWAPPED);
  g_signal_connect_object (self->device, "notify::active-connection",
                           G_CALLBACK (on_device_state_changed_cb),
                           self, G_CONNECT_SWAPPED);
  g_signal_connect_object (self->device, "notify::active-access-point",
                           G_CALLBACK (on_device_active_ap_changed_cb),
                           self, G_CONNECT_SWAPPED);
  on_device_state_changed_cb (self, NULL, self->device);

  /* Simulate a change notification on the available connections.
   * This uses the implementation detail that the list is rebuild
   * completely in this case. */
  update_connections (self);
}

static void
cc_wifi_connection_list_get_property (GObject    *object,
                                      guint       prop_id,
                                      GValue     *value,
                                      GParamSpec *pspec)
{
  CcWifiConnectionList *self = CC_WIFI_CONNECTION_LIST (object);

  switch (prop_id)
    {
    case PROP_CHECKABLE:
      g_value_set_boolean (value, self->checkable);
      break;

    case PROP_HIDE_UNAVAILABLE:
      g_value_set_boolean (value, self->hide_unavailable);
      break;

    case PROP_SHOW_APS:
      g_value_set_boolean (value, self->show_aps);
      break;

    case PROP_CLIENT:
      g_value_set_object (value, self->client);
      break;

    case PROP_DEVICE:
      g_value_set_object (value, self->device);
      break;

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

static void
cc_wifi_connection_list_set_property (GObject      *object,
                                      guint         prop_id,
                                      const GValue *value,
                                      GParamSpec   *pspec)
{
  CcWifiConnectionList *self = CC_WIFI_CONNECTION_LIST (object);

  switch (prop_id)
    {
    case PROP_CHECKABLE:
      self->checkable = g_value_get_boolean (value);
      break;

    case PROP_HIDE_UNAVAILABLE:
      self->hide_unavailable = g_value_get_boolean (value);
      break;

    case PROP_SHOW_APS:
      self->show_aps = g_value_get_boolean (value);
      break;

    case PROP_CLIENT:
      self->client = g_value_dup_object (value);
      break;

    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_wifi_connection_list_class_init (CcWifiConnectionListClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->constructed = cc_wifi_connection_list_constructed;
  object_class->dispose = cc_wifi_connection_list_dispose;
  object_class->finalize = cc_wifi_connection_list_finalize;
  object_class->get_property = cc_wifi_connection_list_get_property;
  object_class->set_property = cc_wifi_connection_list_set_property;

  props[PROP_CHECKABLE] =
    g_param_spec_boolean ("checkable", "checkable",
                          "Passed to the created rows to show/hide the checkbox for deletion",
                          FALSE,
                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

  props[PROP_HIDE_UNAVAILABLE] =
    g_param_spec_boolean ("hide-unavailable", "HideUnavailable",
                          "Whether to show or hide unavailable connections",
                          TRUE,
                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

  props[PROP_SHOW_APS] =
    g_param_spec_boolean ("show-aps", "ShowAPs",
                          "Whether to show available SSIDs/APs without a connection",
                          TRUE,
                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

  props[PROP_CLIENT] =
    g_param_spec_object ("client", "NMClient",
                         "The NM Client",
                         NM_TYPE_CLIENT,
                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

  props[PROP_DEVICE] =
    g_param_spec_object ("device", "WiFi Device",
                         "The WiFi Device for this connection list",
                         NM_TYPE_DEVICE_WIFI,
                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (object_class,
                                     PROP_LAST,
                                     props);

  g_signal_new ("configure",
                CC_TYPE_WIFI_CONNECTION_LIST,
                G_SIGNAL_RUN_LAST,
                0, NULL, NULL, NULL,
                G_TYPE_NONE, 1, CC_TYPE_WIFI_CONNECTION_ROW);
}

static void
cc_wifi_connection_list_init (CcWifiConnectionList *self)
{
  self->listbox = GTK_LIST_BOX (gtk_list_box_new ());
  gtk_list_box_set_selection_mode (GTK_LIST_BOX (self->listbox), GTK_SELECTION_NONE);
  gtk_widget_add_css_class (GTK_WIDGET (self->listbox), "boxed-list");
  adw_bin_set_child (ADW_BIN (self), GTK_WIDGET (self->listbox));

  self->hide_unavailable = TRUE;
  self->show_aps = TRUE;

  self->connections = g_ptr_array_new_with_free_func (g_object_unref);
  self->connections_row = g_ptr_array_new ();
  self->ssid_to_row = g_hash_table_new_full (g_bytes_hash, g_bytes_equal,
                                             (GDestroyNotify) g_bytes_unref, NULL);
  self->ap_ssid_cache = g_hash_table_new_full (g_direct_hash, g_direct_equal,
                                               NULL, (GDestroyNotify) g_bytes_unref);
}

CcWifiConnectionList *
cc_wifi_connection_list_new (NMClient     *client,
                             NMDeviceWifi *device,
                             gboolean      hide_unavailable,
                             gboolean      show_aps,
                             gboolean      checkable)
{
  return g_object_new (CC_TYPE_WIFI_CONNECTION_LIST,
                       "client", client,
                       "device", device,
                       "hide-unavailable", hide_unavailable,
                       "show-aps", show_aps,
                       "checkable", checkable,
                       NULL);
}

void
cc_wifi_connection_list_freeze (CcWifiConnectionList *self)
{
  g_return_if_fail (CC_WIFI_CONNECTION_LIST (self));

  if (self->freeze_count == 0)
    g_debug ("wifi connection list has been frozen");

  self->freeze_count += 1;
}

void
cc_wifi_connection_list_thaw (CcWifiConnectionList *self)
{
  g_return_if_fail (CC_WIFI_CONNECTION_LIST (self));

  g_return_if_fail (self->freeze_count > 0);

  self->freeze_count -= 1;

  if (self->freeze_count == 0)
    {
      g_debug ("wifi connection list has been thawed");
      update_connections (self);
    }
}

GtkListBox *
cc_wifi_connection_list_get_list_box (CcWifiConnectionList *self)
{
  g_return_val_if_fail (CC_IS_WIFI_CONNECTION_LIST (self), NULL);

  return self->listbox;
}
