/*
 * Copyright (c) 2016-2021 Satlab A/S <satlab@satlab.com>
 * All rights reserved. Do not distribute without permission.
 */

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <structmember.h>
#include <errno.h>

#include <prop-client/prop_client.h>

#define PROP_CLIENT_SUBSYS_SPEC_CAPSULE	"prop_client.subsys_spec"

/* PropNode object */
typedef struct {
	PyObject_HEAD
	uint8_t node;
	uint8_t port;
	uint32_t timeout;
	const struct prop_subsys_spec *spec;
} PropNodeObject;

static int errno_to_exception(const char *msg, int ret)
{
	switch(ret){
	case -ENOMEM:
	case -ENOSPC:
		PyErr_Format(PyExc_MemoryError, "%s ret: %d", msg, ret);
		break;
	case -EINVAL:
		PyErr_Format(PyExc_ValueError, "%s: %d", msg, ret);
		break;
	case -ENOSYS:
		PyErr_Format(PyExc_SystemError, "%s: %d", msg, ret);
		break;
	case -ECONNREFUSED:
		PyErr_Format(PyExc_ConnectionRefusedError, "%s: %d", msg, ret);
		break;
	case -EIO:
		PyErr_Format(PyExc_OSError, "%s: %d", msg, ret);
		break;
	case -ETIMEDOUT:
		PyErr_Format(PyExc_TimeoutError, "%s: %d", msg, ret);
		break;
	case -ENOENT:
		PyErr_Format(PyExc_FileNotFoundError, "%d: %s", msg, ret);
		break;
	default:
		PyErr_Format(PyExc_Exception, "%s: %d", msg, ret);
		break;
	}

	return 0;
}

static int PropNode_init(PropNodeObject *self, PyObject *args, PyObject *kwds)
{
	static const char *kwlist[] = {"node", "port", "spec", "timeout", NULL};
	PyObject *subsys_spec_capsule;

	self->timeout = 1000;

	if (!PyArg_ParseTupleAndKeywords(args, kwds, "bbO|I", (char **)kwlist, &self->node, &self->port, &subsys_spec_capsule, &self->timeout))
		return -1;

	self->spec = PyCapsule_GetPointer(subsys_spec_capsule, PROP_CLIENT_SUBSYS_SPEC_CAPSULE);
	if (!self->spec)
		return -1;

	return 0;
}

static PyObject *PropNode_get_ex(PropNodeObject *self, PyObject *props_arg, uint32_t timeout)
{
	int ret;
	PyObject *value;
	const struct prop_spec *prop;
	struct sl_prop_query query;
	PyObject *propobj, *props_ascii, *propretdict;
	PyObject *proplist = NULL;
	bool unpack_single;

	/* Could be argument, but better to let C interface hide this in the future */
	size_t tx_size = 1024, rx_size = 1024;

	/* Create iterators from argument, the object is traversed twice (once when
	creating the request, and once when unpacking the reply), hence the need for two iterators */
	PyObject *iterreq = NULL;
	PyObject *iterrep = NULL;

	if (PyUnicode_Check(props_arg)) {
		/* Put the string in to a list to avoid iterating over every char in the string */
		/* Create single entry list */
		proplist = PyList_New(0);
		if (!proplist)
			goto err;

		if (PyList_Append(proplist, props_arg) != 0)
			goto err_proplist;

		props_arg = proplist;

		/* Return value object directy */
		unpack_single = true;

	} else {
		/* Return as dict */
		unpack_single = false;
	}

	/* Create iterators */
	iterreq = PyObject_GetIter(props_arg);
	iterrep = PyObject_GetIter(props_arg);
	if (!iterreq || !iterrep) {
		PyErr_Format(PyExc_ValueError, "Could not create iterables");
		goto err_proplist;
	}

	/* Prepare query */
	ret = sl_prop_remote_query_create(&query, tx_size, rx_size);
	if (ret < 0) {
		errno_to_exception("Creation of remote query failed", ret);
		goto err_proplist;
	}

	while ((propobj = PyIter_Next(iterreq))) {
		if (!propobj) {
			goto err_query;
		}

		props_ascii = PyUnicode_AsASCIIString(propobj);
		if (!props_ascii) {
			goto err_query;
		}

		const char *name = PyBytes_AsString(props_ascii);
		if (!name) {
			goto err_query;
		}

		prop = sl_prop_spec_find_by_name(self->spec, name);
		if (!prop) {
			PyErr_Format(PyExc_ValueError, "No such property \"%s\"", name);
			goto err_query;
		}

		/* Add to query */
		ret = sl_prop_remote_query_get(&query, prop->id);
		if (ret < 0)
			goto err_query;

		/* We no longer need reference to ASCII name */
		Py_DECREF(props_ascii);

		/* Remove reference to iterator item */
		Py_DECREF(propobj);
	}

	/* Send query */
	Py_BEGIN_ALLOW_THREADS
	ret = sl_prop_remote_query_send(&query, self->node, self->port, timeout);
	Py_END_ALLOW_THREADS
	if (ret < 0) {
		errno_to_exception("Failed to send property query", ret);
		goto err_query;
	}

	/* Create return dict */
	propretdict = PyDict_New();
	if (!propretdict)
		goto err_query;

	/* Unpack replies */
	while ((propobj = PyIter_Next(iterrep))) {

		props_ascii = PyUnicode_AsASCIIString(propobj);
		if (!props_ascii) {
			goto err_propretdict;
		}

		const char *name = PyBytes_AsString(props_ascii);
		if (!name) {
			goto err_ascii;
		}

		prop = sl_prop_spec_find_by_name(self->spec, name);
		if (!prop) {
			PyErr_Format(PyExc_ValueError, "No such property \"%s\"", name);
			goto err_ascii;
		}

		/* Create value object */
		switch (prop->type.type) {
		case PROP_TYPE_BOOL: {
			bool boolval;
			ret = sl_prop_remote_query_get_reply(&query, prop->id, &boolval, sizeof(boolval));
			if (ret < 0)
				goto err_ascii;

			value = PyBool_FromLong(boolval);
			break;
		}
		case PROP_TYPE_UINT8: {
			uint8_t u8val;
			ret = sl_prop_remote_query_get_reply(&query, prop->id, &u8val, sizeof(u8val));
			if (ret < 0)
				goto err_ascii;

			value = PyLong_FromLong(u8val);
			break;
		}
		case PROP_TYPE_UINT16: {
			uint16_t u16val;
			ret = sl_prop_remote_query_get_reply(&query, prop->id, &u16val, sizeof(u16val));
			if (ret < 0)
				goto err_ascii;

			value = PyLong_FromLong(u16val);
			break;
		}
		case PROP_TYPE_UINT32: {
			uint32_t u32val;
			ret = sl_prop_remote_query_get_reply(&query, prop->id, &u32val, sizeof(u32val));
			if (ret < 0)
				goto err_ascii;

			value = PyLong_FromLong(u32val);
			break;
		}
		case PROP_TYPE_UINT64: {
			uint64_t u64val;
			ret = sl_prop_remote_query_get_reply(&query, prop->id, &u64val, sizeof(u64val));
			if (ret < 0)
				goto err_ascii;

			value = PyLong_FromLong(u64val);
			break;
		}
		case PROP_TYPE_INT8: {
			int8_t i8val;
			ret = sl_prop_remote_query_get_reply(&query, prop->id, &i8val, sizeof(i8val));
			if (ret < 0)
				goto err_ascii;

			value = PyLong_FromLong(i8val);
			break;
		}
		case PROP_TYPE_INT16: {
			int16_t i16val;
			ret = sl_prop_remote_query_get_reply(&query, prop->id, &i16val, sizeof(i16val));
			if (ret < 0)
				goto err_ascii;

			value = PyLong_FromLong(i16val);
			break;
		}
		case PROP_TYPE_INT32: {
			int32_t i32val;
			ret = sl_prop_remote_query_get_reply(&query, prop->id, &i32val, sizeof(i32val));
			if (ret < 0)
				goto err_ascii;

			value = PyLong_FromLong(i32val);
			break;
		}
		case PROP_TYPE_INT64: {
			int64_t i64val;
			ret = sl_prop_remote_query_get_reply(&query, prop->id, &i64val, sizeof(i64val));
			if (ret < 0)
				goto err_ascii;

			value = PyLong_FromLong(i64val);
			break;
		}
		case PROP_TYPE_DOUBLE: {
			double doubleval;
			ret = sl_prop_remote_query_get_reply(&query, prop->id, &doubleval, sizeof(doubleval));
			if (ret < 0)
				goto err_ascii;

			value = PyFloat_FromDouble(doubleval);
			break;
		}
		case PROP_TYPE_FLOAT: {
			float floatval;
			ret = sl_prop_remote_query_get_reply(&query, prop->id, &floatval, sizeof(floatval));
			if (ret < 0)
				goto err_ascii;

			value = PyFloat_FromDouble(floatval);
			break;
		}
		case PROP_TYPE_STRING: {
			char *string = malloc(prop->size);
			if (!string) {
				PyErr_Format(PyExc_MemoryError, "No available memory for string");
				goto err_ascii;
			}

			ret = sl_prop_remote_query_get_reply(&query, prop->id, string, prop->size);
			if (ret < 0) {
				free(string);
				goto err_ascii;
			}

			string[prop->size - 1] = '\0';
			value = PyUnicode_FromString(string);
			free(string);
			break;
		}
		case PROP_TYPE_BINARY: {
			uint8_t *bin = calloc(1, prop->size);
			if (!bin) {
				PyErr_Format(PyExc_MemoryError, "No available memory for bytes");
				goto err_ascii;
			}

			ret = sl_prop_remote_query_get_reply(&query, prop->id, bin, prop->size);
			if (ret < 0) {
				free(bin);
				goto err_ascii;
			}

			value = PyBytes_FromStringAndSize((const char *)bin, prop->size);
			free(bin);
			break;
		}
		default:
			PyErr_Format(PyExc_TypeError, "Unknown type for property \"%s\"", name);
			goto err_ascii;
		}

		/* We no longer need reference to ASCII name */
		Py_DECREF(props_ascii);

		/* Remove reference to iterator item */
		Py_DECREF(propobj);

		/* Check generated object */
		if (!value)
			goto err_propretdict;

		/* Add value object to return dict */
		PyDict_SetItemString(propretdict, name, value);
	}
	/* Free query memory */
	sl_prop_remote_query_destroy(&query);

	Py_DECREF(iterreq);
	Py_DECREF(iterrep);

	/* If getting single value, return value directly instead of as list */
	if (unpack_single) {
		PyObject *key;
		Py_ssize_t pos = 0;

		/* Retrieve first value from the reply */
		PyDict_Next(propretdict, &pos, &key, &value);
		Py_DECREF(propretdict);

		/* Make sure to drop reference to property list */
		Py_DECREF(proplist);

		return value;
	}

	return propretdict;

err_ascii:
	/* We no longer need reference to ASCII name */
	Py_DECREF(props_ascii);
err_propretdict:
	/* Make sure to drop reference to property return list */
	Py_DECREF(propretdict);
err_query:
	/* Free query memory */
	sl_prop_remote_query_destroy(&query);
err_proplist:
	/* Make sure to drop reference to property list */
	Py_XDECREF(proplist);
err:
	Py_XDECREF(iterreq);
	Py_XDECREF(iterrep);

	return NULL;
}

PyDoc_STRVAR(prop_get_doc,
	     "Get remote property value.\n\n"
	     "Parameters:\n"
	     "props (string or iterable): Name(s) of the prop(s) to get.\n"
	     "timeout (int) (optional): Timeout of the command in milliseconds (default 1000).\n\n"
	     "Returns:\n"
	     "Dictionary containing a \'key: value\' pair for each prop if \'props\' is an\n"
	     "iterable or string if \'props\' is a string. Raises exception on failure.\n\n");

static PyObject *PropNode_get(PropNodeObject *self, PyObject *args, PyObject *kwds)
{
	/* Arguments */
	static const char *kwlist[] = {"props", "timeout", NULL};
	PyObject *props_arg;
	uint32_t timeout = self->timeout;

	/* Parse arguments */
	if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|I", (char **)kwlist, &props_arg, &timeout))
		return NULL;

	return PropNode_get_ex(self, props_arg, timeout);
}

static PyObject *PropNode_set_ex(PropNodeObject *self, PyObject *props_arg, PyObject *values_arg, uint32_t timeout)
{
	int ret;
	const struct prop_spec *prop;
	struct sl_prop_query query;
	PyObject *proplist = NULL;
	PyObject *vallist = NULL;
	PyObject *propobj, *props_ascii, *valobj;

	/* Could be argument, but better to let C interface hide this in the future */
	size_t tx_size = 1024, rx_size = 1024;

	/* Create iterators from arguments */
	PyObject *iternames = NULL;
	PyObject *itervalues = NULL;

	/* sets are unordered, and will therefore result in unpredictable results if used */
	if (PyAnySet_Check(props_arg) || PyAnySet_Check(values_arg)) {
		PyErr_Format(PyExc_ValueError, "Set() or Frozenset() not supported");
		goto err;
	}

	if (PyUnicode_Check(props_arg)) {
		/* Put the string in to a list to avoid iterating over every char in the string */
		/* Create single entry list */
		proplist = PyList_New(0);
		if (!proplist)
			goto err;

		if (PyList_Append(proplist, props_arg) != 0)
			goto err_proplist;

		props_arg = proplist;
	}

	/* Create iterator */
	iternames = PyObject_GetIter(props_arg);
	if (!iternames) {
		PyErr_Format(PyExc_ValueError, "Could not create name iterable");
		goto err_proplist;
	}

	if (PyNumber_Check(values_arg) || PyUnicode_Check(values_arg)) {
		/* Put the int or string in to a list to avoid iterating over every char in the string */
		/* Create single entry list */
		vallist = PyList_New(0);
		if (!vallist)
			goto err_proplist;

		if (PyList_Append(vallist, values_arg) != 0)
			goto err_vallist;

		values_arg = vallist;
	}

	itervalues = PyObject_GetIter(values_arg);
	if (!itervalues) {
		PyErr_Format(PyExc_ValueError, "Could not create value iterable");
		goto err_vallist;
	}

	/* Prepare query */
	ret = sl_prop_remote_query_create(&query, tx_size, rx_size);
	if (ret < 0) {
		errno_to_exception("Failed to create remote property query", ret);
		goto err_vallist;
	}

	/* Add properties to query */
	while ((propobj = PyIter_Next(iternames))) {
		if (!propobj) {
			goto err_query;
		}

		valobj = PyIter_Next(itervalues);
		if (!valobj) {
			goto err_query;
		}

		props_ascii = PyUnicode_AsASCIIString(propobj);
		if (!props_ascii) {
			goto err_query;
		}

		const char *name = PyBytes_AsString(props_ascii);
		if (!name) {
			goto err_ascii;
		}

		prop = sl_prop_spec_find_by_name(self->spec, name);
		if (!prop) {
			PyErr_Format(PyExc_ValueError, "No such property \"%s\"", name);
			goto err_ascii;
		}

		switch (prop->type.type) {
		case PROP_TYPE_BOOL: {
			if (!PyBool_Check(valobj)) {
				PyErr_Format(PyExc_ValueError, "Invalid boolean value");
				goto err_ascii;
			}

			bool boolval = (valobj == Py_True);
			ret = sl_prop_remote_query_set(&query, prop->id, &boolval, sizeof(boolval));
			break;
		}
		case PROP_TYPE_UINT8: {
			long longval = PyLong_AsLong(valobj);
			if (PyErr_Occurred() || longval < 0 || longval > UINT8_MAX) {
				PyErr_Format(PyExc_ValueError, "Invalid value for UINT8 property");
				goto err_ascii;
			}

			uint8_t u8val = longval;
			ret = sl_prop_remote_query_set(&query, prop->id, &u8val, sizeof(u8val));
			break;
		}
		case PROP_TYPE_UINT16: {
			long longval = PyLong_AsLong(valobj);
			if (PyErr_Occurred() || longval < 0 || longval > UINT16_MAX) {
				PyErr_Format(PyExc_ValueError, "Invalid value for UINT16 property");
				goto err_ascii;
			}

			uint16_t u16val = longval;
			ret = sl_prop_remote_query_set(&query, prop->id, &u16val, sizeof(u16val));
			break;
		}
		case PROP_TYPE_UINT32: {
			long longval = PyLong_AsLong(valobj);
			if (PyErr_Occurred() || longval < 0 || longval > UINT32_MAX) {
				PyErr_Format(PyExc_ValueError, "Invalid value for UINT32 property");
				goto err_ascii;
			}

			uint32_t u32val = longval;
			ret = sl_prop_remote_query_set(&query, prop->id, &u32val, sizeof(u32val));
			break;
		}
		case PROP_TYPE_UINT64: {
			long longval = PyLong_AsLong(valobj);
			if (PyErr_Occurred() || longval < 0 || longval > UINT64_MAX) {
				PyErr_Format(PyExc_ValueError, "Invalid value for UINT64 property");
				goto err_ascii;
			}

			uint64_t u64val = longval;
			ret = sl_prop_remote_query_set(&query, prop->id, &u64val, sizeof(u64val));
			break;
		}
		case PROP_TYPE_INT8: {
			long longval = PyLong_AsLong(valobj);
			if (PyErr_Occurred() || longval < INT8_MIN || longval > INT8_MAX) {
				PyErr_Format(PyExc_ValueError, "Invalid value for INT8 property");
				goto err_ascii;
			}

			int8_t i8val = longval;
			ret = sl_prop_remote_query_set(&query, prop->id, &i8val, sizeof(i8val));
			break;
		}
		case PROP_TYPE_INT16: {
			long longval = PyLong_AsLong(valobj);
			if (PyErr_Occurred() || longval < INT16_MIN || longval > INT16_MAX) {
				PyErr_Format(PyExc_ValueError, "Invalid value for INT16 property");
				goto err_ascii;
			}

			int16_t i16val = longval;
			ret = sl_prop_remote_query_set(&query, prop->id, &i16val, sizeof(i16val));
			break;
		}
		case PROP_TYPE_INT32: {
			long longval = PyLong_AsLong(valobj);
			if (PyErr_Occurred() || longval < INT32_MIN || longval > INT32_MAX) {
				PyErr_Format(PyExc_ValueError, "Invalid value for INT32 property");
				goto err_ascii;
			}

			int32_t i32val = longval;
			ret = sl_prop_remote_query_set(&query, prop->id, &i32val, sizeof(i32val));
			break;
		}
		case PROP_TYPE_INT64: {
			long longval = PyLong_AsLong(valobj);
			if (PyErr_Occurred() || longval < INT64_MIN || longval > INT64_MAX) {
				PyErr_Format(PyExc_ValueError, "Invalid value for INT64 property");
				goto err_ascii;
			}

			int64_t i64val = longval;
			ret = sl_prop_remote_query_set(&query, prop->id, &i64val, sizeof(i64val));
			break;
		}
		case PROP_TYPE_DOUBLE: {
			double doubleval = PyFloat_AsDouble(valobj);
			if (PyErr_Occurred()) {
				PyErr_Format(PyExc_ValueError, "Invalid value for double property");
				goto err_ascii;
			}

			ret = sl_prop_remote_query_set(&query, prop->id, &doubleval, sizeof(doubleval));
			break;
		}
		case PROP_TYPE_FLOAT: {
			float floatval = PyFloat_AsDouble(valobj);
			if (PyErr_Occurred()) {
				PyErr_Format(PyExc_ValueError, "Invalid value for float property");
				goto err_ascii;
			}

			ret = sl_prop_remote_query_set(&query, prop->id, &floatval, sizeof(floatval));
			break;
		}
		case PROP_TYPE_STRING: {
			PyObject *string_ascii = PyUnicode_AsASCIIString(valobj);
			if (!string_ascii)
				goto err_ascii;

			char *string;
			Py_ssize_t string_size;
			if (PyBytes_AsStringAndSize(string_ascii, &string, &string_size) < 0) {
				Py_DECREF(string_ascii);
				goto err_ascii;
			}

			ret = sl_prop_remote_query_set(&query, prop->id, string, string_size + 1);
			Py_DECREF(string_ascii);
			break;
		}
		case PROP_TYPE_BINARY: {
			char *bin;
			Py_ssize_t bin_size;
			if (PyBytes_AsStringAndSize(valobj, &bin, &bin_size) < 0)
				goto err_ascii;

			ret = sl_prop_remote_query_set(&query, prop->id, (const uint8_t *)bin, bin_size);
			break;
		}
		default:
			PyErr_Format(PyExc_TypeError, "Unknown type for property \"%s\"", name);
			break;
		}

		/* We no longer need reference to ASCII name */
		Py_DECREF(props_ascii);

		/* Remove reference to value iterator */
		Py_DECREF(valobj);

		/* Remove reference to prop iterator */
		Py_DECREF(propobj);

		if (ret < 0) {
			goto err_ascii;
		}
	}

	/* Send query */
	Py_BEGIN_ALLOW_THREADS
	ret = sl_prop_remote_query_send(&query, self->node, self->port, timeout);
	Py_END_ALLOW_THREADS
	if (ret < 0) {
		errno_to_exception("Failed to send property query", ret);
		goto err_query;
	}

	/* Free query memory */
	sl_prop_remote_query_destroy(&query);

	Py_DECREF(iternames);
	Py_DECREF(itervalues);

	/* Make sure to drop reference to property and value list */
	Py_XDECREF(proplist);
	Py_XDECREF(vallist);

	Py_RETURN_NONE;

err_ascii:
	/* We no longer need reference to ASCII name */
	Py_DECREF(props_ascii);
err_query:
	/* Free query memory */
	sl_prop_remote_query_destroy(&query);
err_vallist:
	/* Make sure to drop reference to property value list */
	Py_XDECREF(vallist);
err_proplist:
	/* Make sure to drop reference to property list */
	Py_XDECREF(proplist);
err:
	Py_XDECREF(iternames);
	Py_XDECREF(itervalues);

	return NULL;
}

PyDoc_STRVAR(prop_set_doc,
	     "Set remote property value.\n\n"
	     "Parameters:\n"
	     "props (string, ordered iterable or dict): Name(s) of the prop(s) to set or dict\n"
	     "containing \'key: value\' pairs for each prop.\n"
	     "values (bool|int|float|string|binary or ordered iterable) (optional):\n"
	     "Value(s) of the prop(s) to set. \'values\' should only be speficied when a dict is\n"
	     "not specified in \'props\'. The length of \'props\' and \'values\' must match.\n"
	     "timeout (int) (optional): Timeout of the command in milliseconds (default 1000).\n\n"
	     "Returns:\n"
	     "None on success, raises exception on failure.\n\n");

static PyObject *PropNode_set(PropNodeObject *self, PyObject *args, PyObject *kwds)
{
	/* Arguments */
	static const char *kwlist[] = {"props", "values", "timeout", NULL};
	PyObject *props_arg;
	PyObject *values_arg = NULL;
	uint32_t timeout = self->timeout;

	/* Parse arguments */
	if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OI", (char **)kwlist, &props_arg, &values_arg, &timeout))
		return NULL;

	if (!values_arg) {
		/* Check if a dict is supplied, and split it */
		if (PyDict_Check(props_arg)) {
			PyObject *ret;
			PyObject *props_keys = PyDict_Keys(props_arg);
			PyObject *props_vals = PyDict_Values(props_arg);
			ret = PropNode_set_ex(self, props_keys, props_vals, timeout);
			Py_DECREF(props_keys);
			Py_DECREF(props_vals);
			return ret;
		} else {
			PyErr_Format(PyExc_ValueError, "Could not convert dict to \'prop: value\' pairs");
			return NULL;
		}
	}
	return PropNode_set_ex(self, props_arg, values_arg, timeout);
}

PyDoc_STRVAR(prop_reset_doc,
	     "Resets a property to its default value, or sets it to 0 if no default value is stored.\n\n"
	     "Parameters:\n"
	     "name (string): Name of the prop to reset.\n"
	     "timeout (int) (optional): Timeout of the command in milliseconds (default 1000).\n\n"
	     "Returns:\n"
	     "None on success, raises exception on failure.\n\n");

static PyObject *PropNode_reset(PropNodeObject *self, PyObject *args, PyObject *kwds)
{
	int ret;
	const struct prop_spec *prop;

	static const char *kwlist[] = {"name", "timeout", NULL};
	const char *name;
	uint32_t timeout = self->timeout;

	if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|I", (char **)kwlist, &name, &timeout))
		return NULL;

	prop = sl_prop_spec_find_by_name(self->spec, name);
	if (!prop) {
		PyErr_Format(PyExc_ValueError, "No such property \"%s\"", name);
		return NULL;
	}

	Py_BEGIN_ALLOW_THREADS
	ret = sl_prop_remote_reset(self->node, self->port, timeout, prop->id);
	Py_END_ALLOW_THREADS
	if (ret < 0) {
		errno_to_exception("Failed to reset prop", ret);
		return NULL;
	}

	Py_RETURN_NONE;
}

PyDoc_STRVAR(prop_save_doc,
	     "Save property group on remote system.\n\n"
	     "Warning:\n"
	     "Be very careful with this command. Modifying and saving certain \n"
	     "properties, such as the CSP address, could render the system unresponsible.\n\n"
	     "Parameters:\n"
	     "name (string): Name of the group to save.\n"
	     "fallback (bool) (optional): If true, the group is saved to the fallback store. \n"
	     "Otherwise, it is saved to the boot store (default False).\n"
	     "timeout (int) (optional): Timeout of the command in milliseconds (default 1000).\n\n"
	     "Returns:\n"
	     "None on success, raises exception on failure.\n\n"
	     "Note:\n"
	     "Some groups require unlocking before saving.\n\n");

static PyObject *PropNode_save(PropNodeObject *self, PyObject *args, PyObject *kwds)
{
	int ret;
	const struct prop_group_spec *group;

	static const char *kwlist[] = {"name", "fallback", "timeout", NULL};
	const char *name;
	uint32_t timeout = self->timeout;
	int fallback = false;

	if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|pI", (char **)kwlist, &name, &fallback, &timeout))
		return NULL;

	group = sl_prop_group_spec_find_by_name(self->spec, name);
	if (!group) {
		PyErr_Format(PyExc_ValueError, "No such property group \"%s\"", name);
		return NULL;
	}

	Py_BEGIN_ALLOW_THREADS
	ret = sl_prop_remote_save(self->node, self->port, timeout, group->id, fallback);
	Py_END_ALLOW_THREADS
	if (ret < 0) {
		errno_to_exception("Failed to save group", ret);
		return NULL;
	}

	Py_RETURN_NONE;
}

PyDoc_STRVAR(prop_load_doc,
	     "Load property group on remote system.\n\n"
	     "Parameters:\n"
	     "name (string): Name of the group to load\n"
	     "fallback (bool) (optional): If true, the group is loaded from the fallback store.\n"
	     "Otherwise, it is loaded from the boot store (default False).\n"
	     "timeout (int) (optional): Timeout of the command in milliseconds (default 1000).\n\n"
	     "Returns:\n"
	     "None on success, raises exception on failure.\n\n");

static PyObject *PropNode_load(PropNodeObject *self, PyObject *args, PyObject *kwds)
{
	int ret;
	const struct prop_group_spec *group;

	static const char *kwlist[] = {"name", "fallback", "timeout", NULL};
	const char *name;
	uint32_t timeout = self->timeout;
	int fallback = false;

	if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|pI", (char **)kwlist, &name, &fallback, &timeout))
		return NULL;

	group = sl_prop_group_spec_find_by_name(self->spec, name);
	if (!group) {
		PyErr_Format(PyExc_ValueError, "No such property group \"%s\"", name);
		return NULL;
	}

	Py_BEGIN_ALLOW_THREADS
	ret = sl_prop_remote_load(self->node, self->port, timeout, group->id, fallback);
	Py_END_ALLOW_THREADS
	if (ret < 0) {
		errno_to_exception("Failed to load group", ret);
		return NULL;
	}

	Py_RETURN_NONE;
}

PyDoc_STRVAR(prop_erase_doc,
	     "Erase property group on remote system.\n\n"
	     "Warning:\n"
	     "Be very careful with this command. Modifying and saving certain \n"
	     "properties, such as the CSP address, could render the system unresponsible.\n\n"
	     "Parameters:\n"
	     "name (string): Name of the group to erase.\n"
	     "fallback (bool) (optional): If true, the fallback store is erased. \n"
	     "Otherwise, the boot store is erased (default False).\n"
	     "timeout (int) (optional): Timeout of the command in milliseconds (default 1000).\n\n"
	     "Returns:\n"
	     "None on success, raises exception on failure.\n\n"
	     "Note:\n"
	     "Some groups require unlocking before erasing.\n\n");

static PyObject *PropNode_erase(PropNodeObject *self, PyObject *args, PyObject *kwds)
{
	int ret;
	const struct prop_group_spec *group;

	static const char *kwlist[] = {"name", "fallback", "timeout", NULL};
	const char *name;
	uint32_t timeout = self->timeout;
	int fallback = false;

	if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|pI", (char **)kwlist, &name, &fallback, &timeout))
		return NULL;

	group = sl_prop_group_spec_find_by_name(self->spec, name);
	if (!group) {
		PyErr_Format(PyExc_ValueError, "No such property group \"%s\"", name);
		return NULL;
	}

	Py_BEGIN_ALLOW_THREADS
	ret = sl_prop_remote_erase(self->node, self->port, timeout, group->id, fallback);
	Py_END_ALLOW_THREADS
	if (ret < 0) {
		errno_to_exception("Failed to erase group", ret);
		return NULL;
	}

	Py_RETURN_NONE;
}

PyDoc_STRVAR(prop_lock_doc,
	     "Lock property group on remote system.\n\n"
	     "Parameters:\n"
	     "name (string): Name of the group to lock.\n"
	     "fallback (bool) (optional): If true, the fallback store is locked. \n"
	     "Otherwise, the boot store is locked (default False).\n"
	     "timeout (int) (optional): Timeout of the command in milliseconds (default 1000).\n\n"
	     "Returns:\n"
	     "None on success, raises exception on failure.\n\n");

static PyObject *PropNode_lock(PropNodeObject *self, PyObject *args, PyObject *kwds)
{
	int ret;
	const struct prop_group_spec *group;

	static const char *kwlist[] = {"name", "fallback", "timeout", NULL};
	const char *name;
	uint32_t timeout = self->timeout;
	int fallback = false;

	if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|pI", (char **)kwlist, &name, &fallback, &timeout))
		return NULL;

	group = sl_prop_group_spec_find_by_name(self->spec, name);
	if (!group) {
		PyErr_Format(PyExc_ValueError, "No such property group \"%s\"", name);
		return NULL;
	}

	Py_BEGIN_ALLOW_THREADS
	ret = sl_prop_remote_lock(self->node, self->port, timeout, group->id, fallback);
	Py_END_ALLOW_THREADS
	if (ret < 0) {
		errno_to_exception("Failed to lock group", ret);
		return NULL;
	}

	Py_RETURN_NONE;
}

PyDoc_STRVAR(prop_unlock_doc,
	     "Unlock property group on remote system.\n\n"
	     "Parameters:\n"
	     "name (string): Name of the group to unlock.\n"
	     "fallback (bool) (optional): If true, the fallback store is unlocked. \n"
	     "Otherwise, the boot store is unlocked (default False).\n"
	     "timeout (int) (optional): Timeout of the command in milliseconds (default 1000).\n\n"
	     "Returns:\n"
	     "None on success, raises exception on failure.\n\n");

static PyObject *PropNode_unlock(PropNodeObject *self, PyObject *args, PyObject *kwds)
{
	int ret;
	const struct prop_group_spec *group;

	static const char *kwlist[] = {"name", "fallback", "timeout", NULL};
	const char *name;
	uint32_t timeout = self->timeout;
	int fallback = false;

	if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|pI", (char **)kwlist, &name, &fallback, &timeout))
		return NULL;

	group = sl_prop_group_spec_find_by_name(self->spec, name);
	if (!group) {
		PyErr_Format(PyExc_ValueError, "No such property group \"%s\"", name);
		return NULL;
	}

	Py_BEGIN_ALLOW_THREADS
	ret = sl_prop_remote_unlock(self->node, self->port, timeout, group->id, fallback);
	Py_END_ALLOW_THREADS
	if (ret < 0) {
		errno_to_exception("Failed to unlock group", ret);
		return NULL;
	}

	Py_RETURN_NONE;
}

static PyMethodDef PropNode_methods[] = {
	{"get",    (PyCFunction)(void(*)(void))PropNode_get,    METH_VARARGS | METH_KEYWORDS, prop_get_doc},
	{"set",    (PyCFunction)(void(*)(void))PropNode_set,    METH_VARARGS | METH_KEYWORDS, prop_set_doc},
	{"reset",  (PyCFunction)(void(*)(void))PropNode_reset,  METH_VARARGS | METH_KEYWORDS, prop_reset_doc},
	{"save",   (PyCFunction)(void(*)(void))PropNode_save,   METH_VARARGS | METH_KEYWORDS, prop_save_doc},
	{"load",   (PyCFunction)(void(*)(void))PropNode_load,   METH_VARARGS | METH_KEYWORDS, prop_load_doc},
	{"erase",  (PyCFunction)(void(*)(void))PropNode_erase,  METH_VARARGS | METH_KEYWORDS, prop_erase_doc},
	{"lock",   (PyCFunction)(void(*)(void))PropNode_lock,   METH_VARARGS | METH_KEYWORDS, prop_lock_doc},
	{"unlock", (PyCFunction)(void(*)(void))PropNode_unlock, METH_VARARGS | METH_KEYWORDS, prop_unlock_doc},
	{NULL}     /* Sentinel */
};

PyObject *PropNode_mapping_subscript(PropNodeObject *self, PyObject *key)
{
	return PropNode_get_ex(self, key, self->timeout);
}

int PropNode_mapping_ass_subscript(PropNodeObject *self, PyObject *key, PyObject *value)
{
	/* Deletion is not supported */
	if (!value) {
		PyErr_Format(PyExc_ValueError, "Deletion is not supported");
		return -1;
	}

	PyObject *ret = PropNode_set_ex(self, key, value, self->timeout) ;
	if (!ret)
		return -1;

	/* Decrement reference to None object */
	Py_DECREF(ret);
	return 0;
}

static PyMappingMethods PropNode_mapping_methods = {
	.mp_subscript = (binaryfunc)PropNode_mapping_subscript,
	.mp_ass_subscript = (objobjargproc)PropNode_mapping_ass_subscript,
};

static PyTypeObject PropNodeType = {
	PyVarObject_HEAD_INIT(NULL, 0)
	.tp_name = "prop_client.PropNode",
	.tp_doc = "PropNode object",
	.tp_basicsize = sizeof(PropNodeObject),
	.tp_itemsize = 0,
	.tp_flags = Py_TPFLAGS_DEFAULT,
	.tp_new = PyType_GenericNew,
	.tp_init = (initproc) PropNode_init,
	.tp_methods = PropNode_methods,
	.tp_as_mapping = &PropNode_mapping_methods,
};

/* Module definition */
static struct PyModuleDef prop_client_module = {
	.m_base = PyModuleDef_HEAD_INIT,
	.m_name = "prop_client",
	.m_size = -1,
};

PyMODINIT_FUNC PyInit_prop_client_bindings(void)
{
	if (PyType_Ready(&PropNodeType) < 0)
		return NULL;

	/* Create module */
	PyObject *m = PyModule_Create(&prop_client_module);
	if (!m)
		return NULL;

	/* Add PropNode object */
	Py_INCREF(&PropNodeType);
	if (PyModule_AddObject(m, "PropNode", (PyObject *) &PropNodeType) < 0) {
		Py_DECREF(&PropNodeType);
		Py_DECREF(m);
		return NULL;
	}

	return m;
}
