/*
 * Copyright (C) 2019 Purism SPC
 * SPDX-License-Identifier: GPL-3.0+
 * Author: Guido Günther <agx@sigxcpu.org>
 */

#define G_LOG_DOMAIN "gthd-thermal-zone"

#include "gth-names.h"
#include "gthd.h"
#include "gthd-data.h"
#include "gthd-thermal-zone.h"
#include "sysfs-utils.h"

#include <gio/gio.h>
#include <glib-unix.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

#define GTHD_THERMAL_ZONES_DBUS_PATH GTH_DBUS_PATH "/ThermalZone"

enum {
  PROP_0,
  PROP_NAME,
  PROP_LAST_PROP,
};
static GParamSpec *props[PROP_LAST_PROP];

typedef struct _TripPoint {
  char *type;
  gint temp;
} TripPoint;


typedef struct _GthdThermalZone {
  GthGdbusThermalZoneSkeleton parent;

  gchar *name;
  int id;

  guint temp_id;
  GHashTable *trip_points;

} GthdThermalZone;

static void initable_iface_init (GInitableIface *iface);

G_DEFINE_TYPE_EXTENDED (GthdThermalZone, gthd_thermal_zone,
			GTH_GDBUS_TYPE_THERMAL_ZONE_SKELETON, 0,
			G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init))

static void
gthd_thermal_zone_set_property (GObject      *object,
				guint         property_id,
				const GValue *value,
				GParamSpec   *pspec)
{
  GthdThermalZone *self = GTHD_THERMAL_ZONE (object);

  switch (property_id) {
  case PROP_NAME:
    g_free (self->name);
    self->name = g_value_dup_string (value);
    g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NAME]);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}


static void
gthd_thermal_zone_get_property (GObject  *object,
			      guint       property_id,
			      GValue     *value,
			      GParamSpec *pspec)
{
  switch (property_id) {
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}


static void
tp_free (TripPoint *tp)
{
  g_free (tp->type);
  g_free (tp);
}


static void
update_trip_points (GthdThermalZone *self)
{
  g_autoptr (GError) err = NULL;
  g_autofree gchar *tz_path =
    g_build_path ("/", GTHD_THERMAL_SYSFS, self->name, NULL);
  g_autoptr (GDir) dir = g_dir_open (tz_path, 0, &err);
  GVariantBuilder builder;
  const gchar *fname;
  gboolean changed = FALSE;

  if (!dir) {
    g_warning ("Failed to read %s: %s", tz_path, err->message);
    return;
  }

  while ((fname = g_dir_read_name (dir))) {
    g_auto(GStrv) parts = NULL;
    gchar *name, *param;
    TripPoint *tp;

    if (!g_str_has_prefix (fname, "trip_point_"))
      continue;

    parts = g_strsplit (fname, "_", -1);
    if (g_strv_length (parts) != 4) {
      g_warning ("%s: %s does not match trip point name pattern", tz_path, fname);
      continue;
    }

    name = parts[2];
    param = parts[3];
    tp = g_hash_table_lookup (self->trip_points, name);
    if (tp == NULL) {
      changed = TRUE;
      g_debug ("%s: new trip point %s", self->name, name);
      tp = g_malloc0 (sizeof (TripPoint));
      g_hash_table_insert (self->trip_points, g_strdup (name), tp);
    }

    if (!g_strcmp0 (param, "type")) {
      gchar *pos;
      g_autofree gchar *type = NULL;

      type = sysfs_get_string (tz_path, fname);
      if ((pos = strchr(type, '\n')) != NULL)
	*pos = '\0';
      if (g_strcmp0(type, tp->type)) {
	changed = TRUE;
	g_free (tp->type);
	tp->type = g_steal_pointer(&type);
      }
    } else if (!g_strcmp0 (param, "temp")) {
      gint temp;

      temp = sysfs_get_int (tz_path, fname);
      if (temp != tp->temp) {
	changed = TRUE;
	tp->temp = temp;
      }
    } else {
      g_debug ("%s: unknown param %s", tz_path, param);
    }
  }

  if (!changed) {
    /* FIXME: can trip points go away? If so we need to check that here */
    return;
  } else {
    GHashTableIter iter;
    gchar *name;
    TripPoint *tp;

    g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{s(si)}"));
    g_hash_table_iter_init (&iter, self->trip_points);
    while (g_hash_table_iter_next (&iter, (gpointer)&name, (gpointer)&tp)) {
      g_debug ("%s: adding trip point %s %s %d",
	       self->name, name, tp->type, tp->temp);
      g_variant_builder_add (&builder, "{s(si)}", name, tp->type ?: "", tp->temp);
    }
    g_object_set (self, "trip-points", g_variant_builder_end(&builder), NULL);
  }
}


static void
update_cooling_devices (GthdThermalZone *self)
{
  g_autoptr (GError) err = NULL;
  g_autoptr (GDir) dir = NULL;
  g_autofree gchar *tz_path = NULL;
  g_autoptr(GSList) cdev_list = NULL;
  GSList *slist;
  const gchar *fname;
  g_auto(GStrv) paths = NULL;
  int n = 0;

  tz_path = g_build_path ("/", GTHD_THERMAL_SYSFS, self->name, NULL);
  dir = g_dir_open (tz_path, 0, &err);
  if (!dir) {
    g_warning ("Failed to read %s: %s", tz_path, err->message);
    return;
  }

  while ((fname = g_dir_read_name (dir))) {
    g_autofree gchar *path = NULL;
    g_autofree gchar *link = NULL;
    g_autofree gchar *cdev_name = NULL;
    GthdCoolingDevice *cdev;

    if (!g_str_has_prefix (fname, "cdev"))
      continue;

    if (strlen (fname) < 5) {
      g_warning ("%s: %s does not match thermal device link name pattern",
		 tz_path, fname);
      continue;
    }

    /* weight and trip point of the cdev */
    if (strstr (fname, "_"))
      continue;

    path = g_build_path ("/", tz_path, fname, NULL);
    link = g_file_read_link (path, &err);
    if (link == NULL)
      g_warning ("%s: error reading %s (%s)", self->name, link, err->message);
    cdev_name = g_path_get_basename (link);

    cdev = gthd_data_get_cooling_device (gthd_data_get_default (), cdev_name);
    if (cdev == NULL) {
      g_warning ("%s: no cooling device found for %s", tz_path, cdev_name);
      continue;
    }

    cdev_list = g_slist_append (cdev_list, gthd_cooling_device_get_dbus_path (cdev));
  }

  paths = g_new0 (gchar*, g_slist_length (cdev_list) + 1);
  for (slist = cdev_list; slist; slist = slist->next) {
    paths[n++] = slist->data;
  }

  gth_gdbus_thermal_zone_set_cooling_devices (GTH_GDBUS_THERMAL_ZONE (self),
					      (const gchar *const *)paths);
}


static gboolean
update_tz (GthdThermalZone *self)
{
  g_autofree gchar *dir = NULL;
  gint new_temp, old_temp;

  dir = g_build_filename (GTHD_THERMAL_SYSFS, self->name, NULL);

  new_temp = sysfs_get_int (dir, "temp");
  g_object_get (self, "temperature", &old_temp, NULL);

  if (old_temp != new_temp) {
    g_object_set (G_OBJECT (self), "temperature", new_temp, NULL);
    g_debug ("%s: temperature %d C", self->name, new_temp / 1000);
  }

  update_trip_points (self);
  update_cooling_devices (self);

  return G_SOURCE_CONTINUE;
}

static gboolean
init_tz (GthdThermalZone *self)
{
  update_tz (self);
  return G_SOURCE_REMOVE;
}

static gboolean
initable_init (GInitable     *initable,
	       GCancellable  *cancellable,
	       GError       **error)
{
  GthdThermalZone *self = GTHD_THERMAL_ZONE (initable);
  g_autoptr(GError) err = NULL;
  gchar *endptr = NULL;
  gint64 id;
  gchar *pos;
  g_autofree gchar *dir = NULL;
  g_autofree gchar *tempfile = NULL;
  g_autofree gchar *type = NULL;

  errno = 0;
  id = g_ascii_strtoll (&self->name[strlen(GTHD_THERMAL_ZONE_SYSFS)], &endptr, 10);
  if (id == 0 && &self->name[strlen(GTHD_THERMAL_ZONE_SYSFS)] == endptr) {
    g_set_error (error, gthd_error_quark(), GTHD_ERROR_FAILED,
		 "Failed to parse thermal zone '%s'", self->name);
    return FALSE;
  }

  if (id == 0 && (errno == EINVAL || errno == ERANGE)) {
    g_set_error (error, gthd_error_quark(), GTHD_ERROR_FAILED,
		 "Failed to parse thermal zone '%s'", self->name);
    return FALSE;
  }

  if (id > G_MAXINT) {
    g_set_error (error, gthd_error_quark(), GTHD_ERROR_FAILED,
		 "Failed to parse thermal zone '%s'", self->name);
    return FALSE;
  }

  self->id = id;
  dir = g_build_filename (GTHD_THERMAL_SYSFS, self->name, NULL);
  type = sysfs_get_string (dir, "type");
  if ((pos = strchr(type, '\n')) != NULL)
    *pos = '\0';
  g_object_set (self, "type", type, NULL);

  g_debug("%s: id: %d, type: %s", self->name, self->id, type);

  /* need to poll since 'temp' attribute doesn't use sysfs_notify */
  self->temp_id = g_timeout_add_seconds (GTHD_THERMAL_SYSFS_INTERVAL,
					 (GSourceFunc)update_tz,
					 self);
  /* schedule in idle loop to get data initially quickly */
  g_idle_add ((GSourceFunc)init_tz, self);

  return TRUE;
}

static void
initable_iface_init (GInitableIface *iface)
{
    iface->init = initable_init;
}

static void
gthd_thermal_zone_dispose (GObject *object)
{
  GthdThermalZone *self = GTHD_THERMAL_ZONE (object);

  if (self->temp_id) {
    g_source_remove (self->temp_id);
    self->temp_id = 0;
  }
  G_OBJECT_CLASS (gthd_thermal_zone_parent_class)->dispose (object);
}

static void
gthd_thermal_zone_finalize (GObject *object)
{
  GthdThermalZone *self = GTHD_THERMAL_ZONE (object);

  g_free (self->name);
  g_hash_table_destroy (self->trip_points);

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

static void
gthd_thermal_zone_class_init (GthdThermalZoneClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->set_property = gthd_thermal_zone_set_property;
  object_class->get_property = gthd_thermal_zone_get_property;

  object_class->dispose = gthd_thermal_zone_dispose;
  object_class->finalize = gthd_thermal_zone_finalize;

  props[PROP_NAME] =
    g_param_spec_string (
      "name",
      "Name",
      "The themal zone's name",
      NULL,
      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
}

static void
gthd_thermal_zone_init (GthdThermalZone *self)
{
  self->trip_points = g_hash_table_new_full (g_str_hash,
					     g_str_equal,
					     (GDestroyNotify)g_free,
					     (GDestroyNotify)tp_free);
}

GthdThermalZone *
gthd_thermal_zone_new (const gchar *name, GError **error)
{
  return GTHD_THERMAL_ZONE (g_initable_new (GTHD_TYPE_THERMAL_ZONE,
					    NULL,
					    error,
					    "name", name,
					    NULL));
}

gint
gthd_thermal_zone_get_id (GthdThermalZone *self)
{
  g_return_val_if_fail (GTHD_IS_THERMAL_ZONE (self), -1);

  return self->id;
}

const gchar *
gthd_thermal_zone_get_name (GthdThermalZone *self)
{
  g_return_val_if_fail (GTHD_IS_THERMAL_ZONE (self), NULL);

  return self->name;
}

gchar *
gthd_thermal_zone_get_dbus_path (GthdThermalZone *self)
{
  g_return_val_if_fail (GTHD_IS_THERMAL_ZONE (self), NULL);

  return g_strdup_printf(GTHD_THERMAL_ZONES_DBUS_PATH "/%d", self->id);
}
