/*
 * Copyright (C) 2025 The Phosh.mobi e.V.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 *
 * Author: Guido Günther <agx@sigxcpu.org>
 */

#define G_LOG_DOMAIN "cbd-channel-manager"

#include "cbd-config.h"

#include "cbd-channel-manager.h"
#include "cbd-service-providers.h"

#include "lcb-enums.h"
#include "lcb-enum-types.h"

#include <gio/gio.h>

/**
 * CbdChannelManager:
 *
 * Determine the current channels that should be set on a modem. If
 * the manager is in `country` mode it tracks the channel based on the
 * set country. If it is in `none` mode it ignores any changes to the
 * country.
 *
 * TODO: As per 3GPP TS 22.268 we should also take CBMs language into account
 * but that information is not in any released modem manager version yet
 * (see https://gitlab.freedesktop.org/mobile-broadband/ModemManager/-/merge_requests/1361)
 * and we hence ignore that information for now.
 */

enum {
  PROP_0,
  PROP_SERVICE_PROVIDERS_DATABASE,
  PROP_MODE,
  PROP_COUNTRY,
  PROP_CHANNELS,
  PROP_LAST_PROP
};
static GParamSpec *props[PROP_LAST_PROP];

struct _CbdChannelManager {
  GObject         parent;

  char           *service_providers_database;

  GSettings      *settings;
  LcbChannelMode  mode;
  char           *country;
  GArray         *channels; /* element-type: CbdChannelsRange */
};

G_DEFINE_TYPE (CbdChannelManager, cbd_channel_manager, G_TYPE_OBJECT)


static const CbdChannelsRange default_channels[] = {
  {
    .level = LCB_SEVERITY_LEVEL_PRESIDENTIAL,
    .start = 4370,
    .end = 4370,
  },
  {
    .level = LCB_SEVERITY_LEVEL_EXTREME,
    .start = 4371,
    .end = 4372,
  },
  {
    .level = LCB_SEVERITY_LEVEL_SEVERE,
    .start = 4373,
    .end = 4378,
  },
  {
    .level = LCB_SEVERITY_LEVEL_PUBLIC_SAFETY,
    .start = 4397,
    .end = 4397,
  },
  {
    .level = LCB_SEVERITY_LEVEL_AMBER,
    .start = 4379,
    .end = 4379,
  },
};


static void
set_default_channels (CbdChannelManager *self)
{
  g_debug ("Setting default channel list");
  g_clear_pointer (&self->channels, g_array_unref);
  self->channels = g_array_new_take (g_memdup2 (default_channels, sizeof (default_channels)),
                                     G_N_ELEMENTS (default_channels),
                                     FALSE,
                                     sizeof (CbdChannelsRange));
}


static void
cbd_channel_manager_update_channels (CbdChannelManager *self)
{
  g_autoptr (GError) err = NULL;

  if (self->mode != LCB_CHANNEL_MODE_COUNTRY)
    return;

  if (!self->service_providers_database) {
    set_default_channels (self);
    goto out;
  }

  g_clear_pointer (&self->channels, g_array_unref);
  self->channels = cbd_service_providers_get_channels_sync (self->service_providers_database,
                                                            self->country,
                                                            &err);
  if (!self->channels || self->channels->len == 0) {
    g_warning ("Failed to get channels: %s, using built-in defaults", err->message);
    set_default_channels (self);
  }

 out:
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHANNELS]);
}


static void
cbd_channel_manager_set_property (GObject      *object,
                                  guint         property_id,
                                  const GValue *value,
                                  GParamSpec   *pspec)
{
  CbdChannelManager *self = CBD_CHANNEL_MANAGER (object);

  switch (property_id) {
  case PROP_SERVICE_PROVIDERS_DATABASE:
    self->service_providers_database = g_value_dup_string (value);
    break;
  case PROP_MODE:
    cbd_channel_manager_set_mode (self, g_value_get_enum (value));
    break;
  case PROP_COUNTRY:
    cbd_channel_manager_set_country (self, g_value_get_string (value));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}


static void
cbd_channel_manager_get_property (GObject    *object,
                                  guint       property_id,
                                  GValue     *value,
                                  GParamSpec *pspec)
{
  CbdChannelManager *self = CBD_CHANNEL_MANAGER (object);

  switch (property_id) {
  case PROP_SERVICE_PROVIDERS_DATABASE:
    g_value_set_string (value, self->service_providers_database);
    break;
  case PROP_MODE:
    g_value_set_enum (value, cbd_channel_manager_get_mode (self));
    break;
  case PROP_COUNTRY:
    g_value_set_string (value, cbd_channel_manager_get_country (self));
    break;
  case PROP_CHANNELS:
    g_value_set_boxed (value, cbd_channel_manager_get_channels (self));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}


static void
cbd_channel_manager_finalize (GObject *object)
{
  CbdChannelManager *self = CBD_CHANNEL_MANAGER (object);

  g_clear_pointer (&self->channels, g_array_unref);
  g_clear_object (&self->settings);
  g_clear_pointer (&self->service_providers_database, g_free);
  g_clear_pointer (&self->country, g_free);

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


static void
cbd_channel_manager_class_init (CbdChannelManagerClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->get_property = cbd_channel_manager_get_property;
  object_class->set_property = cbd_channel_manager_set_property;
  object_class->finalize = cbd_channel_manager_finalize;

  /**
   * CbdChannelManager:service-providers-database:
   *
   * Path to the service provider database
   */
  props[PROP_SERVICE_PROVIDERS_DATABASE] =
    g_param_spec_string ("service-providers-database", "", "",
                          CBD_SERVICE_PROVIDER_DATABASE,
                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
  /**
   * CbdChannelManager:mode:
   *
   * How the current list of channels should be determined
   */
  props[PROP_MODE] =
    g_param_spec_enum ("mode", "", "",
                       LCB_TYPE_CHANNEL_MODE,
                       LCB_CHANNEL_MODE_NONE,
                       G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
  /**
   * CbdChannelManager:country:
   *
   * The country to determine the channel list for
   */
  props[PROP_COUNTRY] =
    g_param_spec_string ("country", "", "",
                         NULL,
                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
  /**
   * CbdChannelManager:channels:
   *
   * The current channel list suitable for country
   */
  props[PROP_CHANNELS] =
    g_param_spec_boxed ("channels", "", "",
                        G_TYPE_ARRAY,
                        G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
}


static void
cbd_channel_manager_init (CbdChannelManager *self)
{
  set_default_channels (self);

  self->settings = g_settings_new ("org.freedesktop.cbd");
  g_settings_bind (self->settings,
                   "channel-mode",
                   self,
                   "mode",
                   G_SETTINGS_BIND_GET);
}


CbdChannelManager *
cbd_channel_manager_new (const char *service_providers_database)
{
  return g_object_new (CBD_TYPE_CHANNEL_MANAGER,
                       "service-providers-database", service_providers_database,
                       NULL);
}


const char *
cbd_channel_manager_get_service_providers_database (CbdChannelManager *self)
{
  g_assert (CBD_IS_CHANNEL_MANAGER (self));

  return self->service_providers_database;
}


LcbChannelMode
cbd_channel_manager_get_mode (CbdChannelManager *self)
{
  g_assert (CBD_IS_CHANNEL_MANAGER (self));

  return self->mode;
}


void
cbd_channel_manager_set_mode (CbdChannelManager *self, LcbChannelMode mode)
{
  g_assert (CBD_IS_CHANNEL_MANAGER (self));

  if (self->mode == mode)
    return;

  self->mode = mode;
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MODE]);

  cbd_channel_manager_update_channels (self);
}


const char *
cbd_channel_manager_get_country (CbdChannelManager *self)
{
  g_assert (CBD_IS_CHANNEL_MANAGER (self));

  return self->country;
}


void
cbd_channel_manager_set_country (CbdChannelManager *self, const char *country)
{
  g_assert (CBD_IS_CHANNEL_MANAGER (self));

  if (!g_strcmp0 (self->country, country))
      return;

  g_set_str (&self->country, country);
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_COUNTRY]);

  cbd_channel_manager_update_channels (self);
}


LcbSeverityLevel
cbd_channel_manager_lookup_level (CbdChannelManager *self, int channel)
{
  g_assert (CBD_IS_CHANNEL_MANAGER (self));

  if (!self->channels)
    return LCB_SEVERITY_LEVEL_UNKNOWN;

  for (int i = 0; i < self->channels->len; i++) {
    CbdChannelsRange range;

    range = g_array_index (self->channels, CbdChannelsRange, i);
    if (range.start <= channel && channel <= range.end)
      return range.level;
  }

  return LCB_SEVERITY_LEVEL_UNKNOWN;
}

/**
 * cbd_channel_manager_get_channels:
 * @self: The channel manager
 *
 * Get the current channels
 *
 * Returns: (transfer none): The channels
 */
GArray *
cbd_channel_manager_get_channels (CbdChannelManager *self)
{
  g_assert (CBD_IS_CHANNEL_MANAGER (self));

  return self->channels;
}
