/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 * vim: sts=0 sw=8 ts=8 tw=78 noexpandtab
 *
 * Copyright (C) 2009 Pramod Dematagoda <pmd.lotr.gandalf@gmail.com>
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

/*
Udisks2 plugin for the mate-sensors-applet

written by info@cppsp.de using the structure and code of the previous version
from above author

fd1 - from doc1 - dbus-glib documentation
https://dbus.freedesktop.org/doc/dbus-glib/
fd2 - from doc2 - GDBUS documentation
https://developer.gnome.org/gio/stable/index.html

I couldn't figure out debug, so I used syslog
syslog(LOG_ERR, "hellodd");
-> /var/log/syslog
*/




#ifdef HAVE_CONFIG_H
#include "config.h"
#endif /* HAVE_CONFIG_H */

#include <stdio.h>
#include <gio/gio.h>
#include "udisks2-plugin.h"


// remove // from next line for syslog debug
//#define UD2PD              1

#ifdef UD2PD
#include <syslog.h>
#endif


#define UDISKS2_BUS_NAME              "org.freedesktop.UDisks2"
#define UDISKS2_INTERFACE_NAME        "org.freedesktop.DBus.ObjectManager"
#define UDISKS2_DEVICE_INTERFACE_NAME "org.freedesktop.UDisks2.Drive"
#define UDISKS2_DEVICE_INTERFACE2_NAME "org.freedesktop.UDisks2.Drive.Ata"

#define UDISKS2_PROPERTIES_INTERFACE  "org.freedesktop.DBus.Properties"
#define UDISKS2_OBJECT_PATH           "/org/freedesktop/UDisks2"


/*
 * Info about a single sensor
 */
typedef struct _DevInfo{
	gchar *path;
	gchar *id;
	gdouble temp;
	GDBusProxy *sensor_proxy;       // dbus object
	GError *error;
} DevInfo;


const gchar *plugin_name = "udisks2";

// a container for the devices found to have smart enabled
GHashTable *devices = NULL;

/* This is a global variable for convenience */
GDBusConnection *connection = NULL;


static void update_device(DevInfo *info)
{
	GError *error = NULL;
	GVariant *tempgvar = NULL;
	GVariant *tempgvar2 = NULL;
	gdouble temp;

	// check valid input parameter
	g_return_if_fail(info != NULL);

	// check connection too
	g_return_if_fail(connection != NULL);

	g_clear_error(&info->error);

// for the udisks plugin a new sensor_proxy was created here, which seems stupid, as one is already stored in the DevInfo struct
// I only create one direct connect proxy here in this function

	// check for sensor_proxy, which should exist at this point, make one if necessary and save it into DevInfo
	// this is used to get the temp value the direct way
	if(NULL == info->sensor_proxy)
	{
		info->sensor_proxy = g_dbus_proxy_new_sync(connection, G_DBUS_PROXY_FLAGS_NONE, NULL,
						  UDISKS2_BUS_NAME,
						  info->path,
						  UDISKS2_PROPERTIES_INTERFACE,
						  NULL, &error
						);

		// check, just to be sure
		if (NULL == info->sensor_proxy)
		{

#ifdef UD2PD
syslog(LOG_ERR, "Failed to get drive temperature 1");
#endif
			g_debug("Failed to get drive temperature 1: %s",
				   error->message);
			g_clear_error(&error);
			return;
		}
	}

// "DriveAtaSmartTimeCollected" in ud2 is "drive ata SmartUpdated"
// fd: The point in time (seconds since the Unix Epoch) that the SMART status was updated or 0 if never updated.
// should we bother getting this time??

/*
	if (!g_dbus_proxy_call_sync(sensor_proxy, "Get", NULL,
			       G_TYPE_STRING, UDISKS2_BUS_NAME,
			       G_TYPE_STRING, "DriveAtaSmartTimeCollected", G_TYPE_INVALID,
			       G_TYPE_VALUE, &smart_time,
			       G_TYPE_INVALID) ||
	    !g_value_get_uint64(&smart_time))
	{
		g_object_unref(sensor_proxy);
		return;
	}
*/


// reading "DriveAtaSmartBlob" doesn't make it refresh
// it seems to me that smart updates occur automatically every 10 minutes
// mate-sensor-applet has a default refresh of 2 seconds...
// it is possible to force a smart update with udisks2: SmartUpdate (IN  a{sv} options);

// fd: A blob containing the ATA SMART data. This blob can be used with libatasmart to get more information. This property is only valid if DriveAtaSmartTimeCollected is greater than zero.
// this smartblob was needed, bc there was no other way to get the smart temperature from udisks
/*

	if (!g_dbus_proxy_call_sync(sensor_proxy, "Get", &info->error,
			      G_TYPE_STRING, UDISKS2_BUS_NAME,
			      G_TYPE_STRING, "DriveAtaSmartBlob", G_TYPE_INVALID,
			      G_TYPE_VALUE, &smart_blob_val,
			      G_TYPE_INVALID))
	{
		g_debug("Error getting DriveAtaSmartBlob %s",
			info->error ? info->error->message : "NULL");
		g_object_unref(sensor_proxy);
		return;
	}
	smart_blob = g_value_get_boxed(&smart_blob_val);

	sk_disk_open(NULL, &sk_disk);
	sk_disk_set_blob(sk_disk, smart_blob->data, smart_blob->len);
	if (sk_disk_smart_get_temperature(sk_disk, &temperature) < 0)
	{
		g_debug("Error getting temperature from AtaSmartBlob");
		g_free(sk_disk);
		g_array_free(smart_blob, TRUE);
		g_object_unref(sensor_proxy);
		return;
	}
*/

	// directly asking the device's DBus object for the temp
	tempgvar = g_dbus_proxy_call_sync(info->sensor_proxy, "Get",
				g_variant_new ("(ss)",
					UDISKS2_DEVICE_INTERFACE2_NAME,
					"SmartTemperature"),				// parameters
				G_DBUS_CALL_FLAGS_NONE,		// flags
				-1,				// timeout
				NULL,				// cancellable
				&error);

	if (NULL == tempgvar)
	{

#ifdef UD2PD
syslog(LOG_ERR, "Failed to get drive temperature 2");
#endif
		g_debug("Failed to get drive temperature 2: %s",
			   error->message);
		g_clear_error(&error);
		// throw away proxy, maybe next time it will be better
		g_clear_object(&info->sensor_proxy);
		return;
	}
	else
	{

#ifdef UD2PD
syslog(LOG_ERR, "tempgvar value: %s", g_variant_print(tempgvar, TRUE));
// leaks memory!
//syslog(LOG_ERR, "tempgvar value: %s", g_variant_print(g_variant_get_variant(g_variant_get_child_value(tempgvar, 0)), TRUE));
#endif

		// tempgvar comes back as sg along the lines of array(gvariant(tempasdouble))
		// hence unpacking
		// need to free up every param / return value, so can't do it like:
		//temp = g_variant_get_double(g_variant_get_variant(g_variant_get_child_value(tempgvar, 0)));
		tempgvar2 = g_variant_get_child_value(tempgvar, 0);
		g_variant_unref(tempgvar);
		tempgvar = g_variant_get_variant(tempgvar2);
		g_variant_unref(tempgvar2);
		temp = g_variant_get_double(tempgvar);
		g_variant_unref(tempgvar);

		// temp in K
		info->temp = temp - 273.15;

#ifdef UD2PD
syslog(LOG_ERR, "Refresh udisks2 device temp: '%f'\n", info->temp);
#endif

	}


}



/* This is the handler for the Changed() signal emitted by UDisks. */
/*
static void udisks2_changed_signal_cb(GDBusProxy *sensor_proxy) {
	const gchar *path;
	DevInfo *info;

	// fd2: Gets the object path sensor_proxy is for.
	path = g_dbus_proxy_get_object_path(sensor_proxy);
	g_debug("%s changed()", path);
	info = g_hash_table_lookup(devices, path);

//	update_device(info);
}
*/


// in this function we would like to get a list of device (hdd/ssd) paths
// then with each path we get the temperature
// it is possible with udisks2 to get all the above information in one g_dbus_proxy_call_sync(), so that is how I did it
// a better version would be to use GDBusObjectManager Server + Client ??
static void udisks2_plugin_get_sensors(GList **sensors) {

#ifdef UD2PD
syslog(LOG_ERR, "fstart");
#endif

	GDBusProxy *proxy = NULL;
	GError *error = NULL;

	DevInfo *info;

	/* This connection will be used for everything, including the obtaining
	 * of sensor data
	 */
	// fd1: Returns a connection to the given bus. The connection is a global variable shared with other callers of this function.
	// fd2: Synchronously connects to the message bus specified by bus_type . Note that the returned object may shared with other callers, e.g. if two separate parts of a process calls this function with the same bus_type , they will share the same object.
	connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error);
	if (NULL == connection)
	{

#ifdef UD2PD
syslog(LOG_ERR, "dbus conn fail");
#endif

		g_debug("Failed to open connection to DBUS: %s",
			error->message);
		g_clear_error(&error);
		return;
	}

#ifdef UD2PD
syslog(LOG_ERR, "dbus conn success");
#endif

	/* This was the proxy which is only used once during the enumeration of
	 * the device object paths
	 */
	// I use it to get all info of all devices at once
	// fd1: Creates a new proxy for a remote interface exported by a connection on a message bus.
	// fd2: Creates a proxy for accessing interface_name on the remote object at object_path owned by name at connection and synchronously loads D-Bus properties unless the G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES flag is used.
	proxy = g_dbus_proxy_new_sync(connection, G_DBUS_PROXY_FLAGS_NONE, NULL,
					  UDISKS2_BUS_NAME,
					  UDISKS2_OBJECT_PATH,
					  UDISKS2_INTERFACE_NAME,
					  NULL, &error
					);

	if (NULL == proxy)
	{

#ifdef UD2PD
syslog(LOG_ERR, "dbus conn proxy fail");
#endif
		g_debug("dbus conn proxy fail: %s",
			   error->message);
		g_clear_error(&error);
		g_clear_object(&connection);
		return;
	}

#ifdef UD2PD
syslog(LOG_ERR, "dbus conn proxy success");
#endif

	/* The object paths of the disks are enumerated and placed in an array
	 * of object paths
	 */
	// fd1: Function for synchronously invoking a method and receiving reply values.
	// fd2: Synchronously invokes the method_name method on proxy.
	// called "EnumerateDevices" method on UDisks object through dbus
	// which returned an array of objectpaths
	// "GetManagedObjects" returns dict of (objectpath, (dict of (string [ie. if. name], dict of(string [ie. property name], variant [ie. prop. value]))))

	// g_dbus_proxy_call_sync() returns NULL on error, GVariant * otherwise
	// need second param to prevent memory leak
	GVariant *managed_objects, *managed_objects2;


	managed_objects2 = g_dbus_proxy_call_sync(proxy, "GetManagedObjects",
				NULL,				// parameters
				G_DBUS_CALL_FLAGS_NONE,		// flags
				-1,				// timeout
				NULL,				// cancellable
				&error);

	if (NULL == managed_objects2)
	{

#ifdef UD2PD
syslog(LOG_ERR, "Failed to enumerate disk devices");
#endif

		g_debug("Failed to enumerate disk devices: %s",
			   error->message);
		g_clear_error(&error);
		g_clear_object(&proxy);
		g_clear_object(&connection);
		return;
	}

	// the result dictionary is enclosed in an array, unpack
	managed_objects = g_variant_get_child_value(managed_objects2, 0);
	g_variant_unref(managed_objects2);

#ifdef UD2PD
//syslog(LOG_ERR, "managed_objects type: %s", g_variant_print(managed_objects, TRUE));
syslog(LOG_ERR, "success to enumerate disk devices");
#endif

	// iterator for the result dictionary

	// iterator code is based on the g_variant_iter_next() documentation
	// iter is freed if the GVariant is, when using g_variant_iter_init()
	GVariantIter iter;
	gchar *key;	     // object path (like '/org/freedesktop/UDisks2/drives/Samsung_SSD_840_EVO_250GB_*insert drive serial nr.*')
	GVariant *value;

#ifdef UD2PD
// log collection size
syslog(LOG_ERR, "iter init count: %d", (int) g_variant_iter_init(&iter, managed_objects));
#else
	g_variant_iter_init(&iter, managed_objects);
#endif

	// "{sv}"  is a GVariant format string
	// {} dictionary of, s string, v GVariant
	// changed to "{oa{sa{sv}}}" on error message 'the GVariant format string '{sv}' has a type of '{sv}' but the given value has a type of 'a{oa{sa{sv}}}''
	// a is array, o is object path
	// NOO!! the right format string is "{o@*}", which means get an object path into the 1st variable (key)
	// and get 'everything else' (as a GVariant) into the 2nd variable (value)
	// needs the & before the key and value params!
	while (g_variant_iter_next(&iter, "{o@*}", &key, &value))
	{

#ifdef UD2PD
syslog(LOG_ERR, "in iter while loop");
syslog(LOG_ERR, "key value: %s", key);
//syslog(LOG_ERR, "value type: %s", g_variant_print(value, TRUE));
#endif

		// level 2
		// create a dictionary of value
		// the two interface names that we are searching for are known and defined
		// can't use GVariantDict, it only supports '{sv}' but the given value has a type of '{sa{sv}}'
		// using general lookup

		GVariant *propdict;		// drive data
		GVariant *propdict2;	// drive smart data

		// make two dictionaries that contain the properties of the drive interfaces
		propdict = g_variant_lookup_value(value, UDISKS2_DEVICE_INTERFACE_NAME, G_VARIANT_TYPE_DICTIONARY);
		propdict2 = g_variant_lookup_value(value, UDISKS2_DEVICE_INTERFACE2_NAME, G_VARIANT_TYPE_DICTIONARY);


		// do we have the right ifname keys?
		if((NULL != propdict) && (NULL != propdict2))
		{

#ifdef UD2PD
syslog(LOG_ERR, "propdict type: %s", g_variant_print(propdict, TRUE));
syslog(LOG_ERR, "propdict2 type: %s", g_variant_print(propdict2, TRUE));
#endif

			// get data
			gchar *id;
			gchar *model;

			gboolean smartenabled;
			gdouble temp;

			// NULL, bc we don't care about the length of the string
			// typecast bc g_variant_get_string() returns const char*
			id = (gchar *) g_variant_get_string(g_variant_lookup_value(propdict, "Id", G_VARIANT_TYPE_STRING), NULL);
			model = (gchar *) g_variant_get_string(g_variant_lookup_value(propdict, "Model", G_VARIANT_TYPE_STRING), NULL);

			smartenabled = g_variant_get_boolean(g_variant_lookup_value(propdict2, "SmartEnabled", G_VARIANT_TYPE_BOOLEAN));
			temp = g_variant_get_double(g_variant_lookup_value(propdict2, "SmartTemperature", G_VARIANT_TYPE_DOUBLE));

#ifdef UD2PD
syslog(LOG_ERR, "Found udisks2 device id: '%s'\n", id);
syslog(LOG_ERR, "Found udisks2 device model: '%s'\n", model);
syslog(LOG_ERR, "Found udisks2 device smartenabled: '%d'\n", smartenabled);
syslog(LOG_ERR, "Found udisks2 device temp: '%f'\n", temp);
#endif


			// only go on if smart is enabled
			// save data
			if(smartenabled)
			{

				info = g_malloc0(sizeof(DevInfo));
				if (NULL == devices)
				{
					devices = g_hash_table_new(g_str_hash,
								   g_str_equal);
				}

				info->id =  g_strdup(id);
				info->path =  g_strdup(key);

				// temp in K
				// this could be left at 0.0, 2 seconds later it will be refreshed anyway
				info->temp = (gdouble)temp - 273.15;
				g_hash_table_insert(devices, info->id, info);

				// Write the sensor data
				sensors_applet_plugin_add_sensor(sensors,
								 id,
								 "Disk Temperature",
								 model,
								 TEMP_SENSOR,
								 FALSE,
								 HDD_ICON,
								 DEFAULT_GRAPH_COLOR);

				g_debug("Added %s", id);

#ifdef UD2PD
syslog(LOG_ERR, "Added %s", id);
#endif

			}
			else
			{

#ifdef UD2PD
syslog(LOG_ERR, "No temp data for device: %s\n", key);
#endif

				g_debug ("No temp data for device: %s\n", key);
			}

#ifdef UD2PD
syslog(LOG_ERR, "b4 free1");
#endif

			g_free(id);
			g_free(model);

		}

#ifdef UD2PD
syslog(LOG_ERR, "b4 free2");
#endif

		// free propdict, propdict2
		// g_variant_dict_unref() may not work a few times, gives error
		// this one seems to do fine
		if(NULL != propdict) {g_variant_unref(propdict);}
		if(NULL != propdict2) {g_variant_unref(propdict2);}

#ifdef UD2PD
syslog(LOG_ERR, "b4 free3");
#endif

		g_free(key);
		g_variant_unref(value);

	}       // end of while loop


	g_variant_unref(managed_objects);
	g_clear_object(&proxy);
	if (NULL == devices)
	{
		g_clear_object(&connection);
	}
}


// this is the function called every refresh cycle
static gdouble udisks2_plugin_get_sensor_value(const gchar *path,
					      const gchar *id,
					      SensorType type,
					      GError **error) {
	DevInfo *info;

	// get device stuct from data store
	info = (DevInfo *)g_hash_table_lookup(devices, path);
	if (NULL == info)
	{
		g_set_error(error, SENSORS_APPLET_PLUGIN_ERROR, 0,
			    "Error finding disk with path %s", path);
		return 0.0;
	}

	if (info->error)
	{
		*error = info->error;
		info->error = NULL;
		return 0.0;
	}

	// refresh device temp
	/* update value since Changed() signal doesn't fire manually enough so
	 * poll instead */
	update_device(info);
	return info->temp;
}


// API functions
const gchar *sensors_applet_plugin_name(void)
{
	return plugin_name;
}

static GList *udisks2_plugin_init(void) {
	GList *sensors = NULL;

	udisks2_plugin_get_sensors(&sensors);

	return sensors;
}

GList *sensors_applet_plugin_init(void)
{
	return udisks2_plugin_init();
}

gdouble sensors_applet_plugin_get_sensor_value(const gchar *path,
					       const gchar *id,
					       SensorType type,
					       GError **error) {
	return udisks2_plugin_get_sensor_value(path, id, type, error);
}
