/*
Copyright (c) 2012-2016 Ben Croston
Copyright (c) 2018 PHYTEC America, LLC.

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

#include "py_gpio.h"
#include "common.h"
#include "c_gpio.h"

/* TODO: Event support
struct py_callback {
	uint32_t gpio;
	PyObject *py_cb;
	struct py_callback *next;
};

static struct py_callback *py_callbacks = NULL;
*/

static int check_mode_set(void)
{
	if (gpio_mode != BOARD && gpio_mode != BCM) {
		PyErr_SetString(PyExc_RuntimeError,
				"Please set pin numbering mode using "
				"GPIO.setmode(GPIO.BOARD) or "
				"GPIO.setmode(GPIO.BCM)");
		return -1;
	}

	return 0;
}

static int get_bcm_pin(int channel)
{
	int pin;

	pin = channel_to_bcm_pin(channel);
	if (pin < 0)
		PyErr_SetString(PyExc_ValueError,
				"Channel is invalid for the Raspberry "
				"Pi!");

	return pin;
}

static void define_constants(PyObject *module)
{
	high = Py_BuildValue("i", HIGH);
	PyModule_AddObject(module, "HIGH", high);

	low = Py_BuildValue("i", LOW);
	PyModule_AddObject(module, "LOW", low);

	output = Py_BuildValue("i", OUTPUT);
	PyModule_AddObject(module, "OUT", output);

	input = Py_BuildValue("i", INPUT);
	PyModule_AddObject(module, "IN", input);

	board = Py_BuildValue("i", BOARD);
	PyModule_AddObject(module, "BOARD", board);

	bcm = Py_BuildValue("i", BCM);
	PyModule_AddObject(module, "BCM", bcm);

	pud_off = Py_BuildValue("i", PUD_OFF);
	PyModule_AddObject(module, "PUD_OFF", pud_off);

	pud_up = Py_BuildValue("i", PUD_UP);
	PyModule_AddObject(module, "PUD_UP", pud_up);

	pud_down = Py_BuildValue("i", PUD_DOWN);
	PyModule_AddObject(module, "PUD_DOWN", pud_down);
}

/*
 * python function cleanup(channel=None)
 */
static PyObject *py_cleanup(PyObject *self, PyObject *args, PyObject *kwargs)
{
	static char *kwlist[] = {"channel", NULL};
	int chancount;
	PyObject *chanlist;
	int channel;
	PyObject *chantuple;
	int i;
	int pin;
	PyObject *tempobj;

	chancount = -1;
	chanlist = NULL;
	channel = -1;
	chantuple = NULL;

	int cleanup_one(void) {
		pin = get_bcm_pin(channel);
		if (pin < 0)
			return -1;

		/* TODO: Event support
		event_cleanup(board_pin);
		*/

		return cleanup_gpio(pin);
	}

	if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O", kwlist,
					 &chanlist))
		return NULL;

	if (chanlist == NULL) {
		/* do nothing */
#if PY_MAJOR_VERSION >= 3
	} else if (PyLong_Check(chanlist)) {
		channel = (int)PyLong_AsLong(chanlist);
#else
	} else if (PyInt_Check(chanlist)) {
		channel = (int)PyInt_AsLong(chanlist);
#endif
		if (PyErr_Occurred())
			return NULL;
		chanlist = NULL;
	} else if (PyList_Check(chanlist)) {
		chancount = PyList_Size(chanlist);
	} else if (PyTuple_Check(chanlist)) {
		chantuple = chanlist;
		chanlist = NULL;
		chancount = PyTuple_Size(chantuple);
	} else {
		PyErr_SetString(PyExc_ValueError,
				"Channel must be an integer or list/tuple of "
				"integers!");
		return NULL;
	}

	if (module_setup && !setup_error) {
		if (channel == -1 && chancount == -1) {
			/* channel not set: cleanup everything */
			/* TODO: Event support
			event_cleanup_all();
			*/

			/* release all held pins */
			cleanup();
			gpio_mode = MODE_UNKNOWN;
		} else if (channel != -1) {
			if (cleanup_one())
				return NULL;
		} else { /* channel was a list/tuple */
			for (i = 0; i < chancount; i++) {
				if (chanlist) {
					tempobj = PyList_GetItem(chanlist, i);
					if (!tempobj)
						return NULL;
				} else {
					tempobj = PyTuple_GetItem(chantuple,
								  i);
					if (!tempobj)
						return NULL;
				}

#if PY_MAJOR_VERSION >= 3
				if (PyLong_Check(tempobj)) {
					channel = (int)PyLong_AsLong(tempobj);
#else
				if (PyInt_Check(tempobj)) {
					channel = (int)PyInt_AsLong(tempobj);
#endif
					if (PyErr_Occurred())
						return NULL;
				} else {
					PyErr_SetString(PyExc_ValueError,
							"Channel must be an integer!");
					return NULL;
				}

				if (cleanup_one())
					return NULL;
			}
		}
	}

	Py_RETURN_NONE;
}

/*
 * python function setmode(mode)
 */
static PyObject *py_setmode(PyObject *self, PyObject *args)
{
	int new_mode;

	if (!PyArg_ParseTuple(args, "i", &new_mode))
		return NULL;

	if (setup_error) {
		PyErr_SetString(PyExc_RuntimeError,
				"Module not imported correctly!");
		return NULL;
	}

	if (gpio_mode != MODE_UNKNOWN && new_mode != gpio_mode) {
		PyErr_SetString(PyExc_ValueError,
				"A different mode has already been set!");
		return NULL;
	}


	if (new_mode != BOARD && new_mode != BCM) {
		PyErr_SetString(PyExc_ValueError,
				"Mode must be BOARD or BCM!");
		return NULL;
	}

	gpio_mode = new_mode;
	Py_RETURN_NONE;
}

static PyObject *py_setup_channel(PyObject *self, PyObject *args,
				  PyObject *kwargs)
{
	static char *kwlist[] = {"channel", "direction", "pull_up_down",
				 "initial", NULL};
	int chancount;
	PyObject *chanlist;
	int channel;
	PyObject *chantuple;
	int dir;
	int i;
	int initial;
	int pin;
	int pud;
	PyObject *tempobj;

	chanlist = NULL;
	channel = -1;
	chantuple = NULL;
	initial = LOW;
	pud = 0;

	int setup_one(void)
	{
		if (check_mode_set())
			return -1;

		pin = get_bcm_pin(channel);
		if (pin < 0)
			return -1;

		if (setup_gpio(pin, dir, initial)) {
			PyErr_SetString(PyExc_RuntimeError,
					"Unable to configure GPIO!");
			return -1;
		}

		return 0;
	}

	if (!PyArg_ParseTupleAndKeywords(args, kwargs, "Oi|ii", kwlist,
					 &chanlist, &dir, &pud, &initial))
		return NULL;

#if PY_MAJOR_VERSION >= 3
	if (PyLong_Check(chanlist)) {
		channel = (int)PyLong_AsLong(chanlist);
#else
	if (PyInt_Check(chanlist)) {
		channel = (int)PyInt_AsLong(chanlist);
#endif
		if (PyErr_Occurred())
			return NULL;
		chanlist = NULL;
	} else if (PyList_Check(chanlist)) {
		/* do nothing */
	} else if (PyTuple_Check(chanlist)) {
		chantuple = chanlist;
		chanlist = NULL;
	} else {
		PyErr_SetString(PyExc_ValueError,
				"Channel must be an integer or list/tuple of "
				"integers!");
		return NULL;
	}

	if (setup_error) {
		PyErr_SetString(PyExc_RuntimeError,
				"Module not imported correctly!");
		return NULL;
	}

	if (dir != INPUT && dir != OUTPUT) {
		PyErr_SetString(PyExc_ValueError,
				"Direction must be INPUT or OUTPUT!");
		return NULL;
	}

	if (dir == INPUT && initial != LOW) {
		PyErr_SetString(PyExc_ValueError,
				"Initial value is not valid for inputs!");
		return NULL;
	}

	if (pud != PUD_OFF && pud != PUD_DOWN && pud != PUD_UP) {
		PyErr_SetString(PyExc_ValueError,
				"Pull setting must be PUD_OFF, PUD_UP or "
				"PUD_DOWN!");
		return NULL;
	}

	if (pud == PUD_UP || pud == PUD_DOWN)
		if (gpio_warnings)
			PyErr_WarnEx(NULL,
				     "Setting pulls is not supported by this "
				     "module. If you need a pull on a pin, "
				     "please configure it through your "
				     "board's pinmuxing.", 1);

	if (chanlist) {
		chancount = PyList_Size(chanlist);
	} else if (chantuple) {
		chancount = PyTuple_Size(chantuple);
	} else {
		if (setup_one())
			return NULL;

		Py_RETURN_NONE;
	}

	for (i = 0; i < chancount; i++) {
		if (chanlist) {
			tempobj = PyList_GetItem(chanlist, i);
			if (!tempobj)
				return NULL;
		} else {	/* chantuple */
			tempobj = PyTuple_GetItem(chantuple, i);
			if (!tempobj)
				return NULL;
		}

#if PY_MAJOR_VERSION >= 3
		if (PyLong_Check(tempobj)) {
			channel = (int)PyLong_AsLong(tempobj);
#else
		if (PyInt_Check(tempobj)) {
			channel = (int)PyInt_AsLong(tempobj);
#endif
			if (PyErr_Occurred())
				return NULL;
		} else {
			PyErr_SetString(PyExc_ValueError,
					"Channel must be an integer!");
			return NULL;
		}

		if (setup_one())
			return NULL;

	}

	Py_RETURN_NONE;
}

/*
 * python function input(channel)
 */
static PyObject *py_input_gpio(PyObject *self, PyObject *args)
{
	int channel;
	int pin;
	int ret;
	PyObject *value;

	if (!PyArg_ParseTuple(args, "i", &channel))
		return NULL;

	pin = get_bcm_pin(channel);
	if (pin < 0)
		return NULL;

	if (gpio_direction(pin) != INPUT) {
		PyErr_SetString(PyExc_RuntimeError,
				"GPIO channel has not been configured "
				"as INPUT!");
		return NULL;
	}

	ret = input_gpio(pin);
	if (ret < 0)
		return NULL;

	if (ret) {
		value = Py_BuildValue("i", HIGH);
	} else {
		value = Py_BuildValue("i", LOW);
	}

	return value;
}

/*
 * python function output(channel(s), value(s))
 */
static PyObject *py_output_gpio(PyObject *self, PyObject *args)
{
	int chancount;
	PyObject *chanlist;
	int channel;
	PyObject *chantuple;
	int i;
	int pin;
	PyObject *tempobj;
	int value;
	int valuecount;
	PyObject *valuelist;
	PyObject *valuetuple;

	chancount = -1;
	chanlist = NULL;
	channel = -1;
	chantuple = NULL;
	value = -1;
	valuecount = -1;
	valuelist = NULL;
	valuetuple = NULL;

	int output(void) {
		if (check_mode_set())
			return -1;

		pin = get_bcm_pin(channel);
		if (pin < 0)
			return -1;

		if (gpio_direction(pin) != OUTPUT) {
			PyErr_SetString(PyExc_RuntimeError,
					"GPIO channel has not been configured "
					"as OUTPUT!");
			return -1;
		}

		output_gpio(pin, value);

		return 0;
	}

	if (!PyArg_ParseTuple(args, "OO", &chanlist, &valuelist))
		return NULL;

#if PY_MAJOR_VERSION >= 3
	if (PyLong_Check(chanlist)) {
		channel = (int)PyLong_AsLong(chanlist);
#else
	if (PyInt_Check(chanlist)) {
		channel = (int)PyInt_AsLong(chanlist);
#endif
		if (PyErr_Occurred())
			return NULL;
		chanlist = NULL;
	} else if (PyList_Check(chanlist)) {
		/* do nothing */
	} else if (PyTuple_Check(chanlist)) {
		chantuple = chanlist;
		chanlist = NULL;
	} else {
		PyErr_SetString(PyExc_ValueError, "Channel must be an integer "
						  "or list/tuple of integers!");
		return NULL;
	}

#if PY_MAJOR_VERSION >= 3
	if (PyLong_Check(valuelist)) {
		value = (int)PyLong_AsLong(valuelist);
#else
	if (PyInt_Check(valuelist)) {
		value = (int)PyInt_AsLong(valuelist);
#endif
		if (PyErr_Occurred())
			return NULL;
		valuelist = NULL;
	} else if (PyList_Check(valuelist)) {
		/* do nothing */
	} else if (PyTuple_Check(valuelist)) {
		valuetuple = valuelist;
		valuelist = NULL;
	} else {
		PyErr_SetString(PyExc_ValueError, "Value must be an integer/"
						  "boolean or a list/tuple of "
						  "integers/booleans");
		return NULL;
	}

	if (chanlist)
		chancount = PyList_Size(chanlist);
	if (chantuple)
		chancount = PyTuple_Size(chantuple);
	if (valuelist)
		valuecount = PyList_Size(valuelist);
	if (valuetuple)
		valuecount = PyTuple_Size(valuetuple);
	if ((chancount != -1 && chancount != valuecount && valuecount != -1) ||
	    (chancount == -1 && valuecount != -1)) {
		PyErr_SetString(PyExc_RuntimeError,
				"Number of channels must match number of "
				"values!");
		return NULL;
	}

	if (chancount == -1) {
		if (output())
			return NULL;
		Py_RETURN_NONE;
	}

	for (i = 0; i < chancount; i++) {
		if (chanlist) {
			tempobj = PyList_GetItem(chanlist, i);
			if (!tempobj)
				return NULL;
		} else {
			tempobj = PyList_GetItem(chantuple, i);
			if (!tempobj)
				return NULL;
		}
#if PY_MAJOR_VERSION >= 3
		if (PyLong_Check(tempobj)) {
			channel = (int)PyLong_AsLong(tempobj);
#else
		if (PyInt_Check(tempobj)) {
			channel = (int)PyInt_AsLong(tempobj);
#endif
			if (PyErr_Occurred())
				return NULL;
		} else {
			PyErr_SetString(PyExc_ValueError,
					"Channel must be an integer!");
			return NULL;
		}

		if (valuecount > 0) {
			if (valuelist) {
				tempobj = PyList_GetItem(valuelist, i);
				if (!tempobj)
					return NULL;
			} else {
				tempobj = PyTuple_GetItem(valuetuple, i);
				if (!tempobj)
					return NULL;
			}

#if PY_MAJOR_VERSION >= 3
			if (PyLong_Check(tempobj)) {
				value = (int)PyLong_AsLong(tempobj);
#else
			if (PyInt_Check(tempobj)) {
				value = (int)PyInt_AsLong(tempobj);
#endif
				if (PyErr_Occurred())
					return NULL;
			} else {
				PyErr_SetString(PyExc_ValueError,
						"Value must be an integer or "
						"boolean!");
				return NULL;
			}
		}

		if (output())
			return NULL;
	}

	Py_RETURN_NONE;
}

/*
 * python function gpio_function(channel)
 */
static PyObject *py_gpio_function(PyObject *self, PyObject *args)
{
	int dir;
	int pin;
	int channel;
	PyObject *func;

	if (!PyArg_ParseTuple(args, "i", &channel))
		return NULL;

	pin = get_bcm_pin(channel);
	if (pin < 0)
		return NULL;

	dir = gpio_direction(pin);
	switch (dir) {
		case 1 : dir = INPUT; break;
		case 2 : dir = OUTPUT; break;
		default : dir = MODE_UNKNOWN; break;
	}

	func = Py_BuildValue("i", dir);

	return func;
}

/*
 * python function setwarnings(state)
 */
static PyObject *py_setwarnings(PyObject *self, PyObject *args)
{
	if (!PyArg_ParseTuple(args, "i", &gpio_warnings))
		return NULL;

	if (setup_error) {
		PyErr_SetString(PyExc_RuntimeError,
				"Module not imported correctly!");
		return NULL;
	}

	Py_RETURN_NONE;
}

/*
 * python function getmode()
 */
static PyObject *py_getmode(PyObject *self, PyObject *args)
{
	PyObject *value;

	if (setup_error) {
		PyErr_SetString(PyExc_RuntimeError,
				"Module not imported correctly!");
		return NULL;
	}

	if (gpio_mode == MODE_UNKNOWN)
		Py_RETURN_NONE;

	value = Py_BuildValue("i", gpio_mode);

	return value;
}

PyMethodDef rpi_gpio_methods[] = {
	{"setup", (PyCFunction)py_setup_channel, METH_VARARGS | METH_KEYWORDS,
	 "Configure a GPIO channel or list of channels with a direction\n" \
	 "channel\t\t- BCM or RPi pin number depending on which mode is set" \
	 "\ndirection\t- IN or OUT\n[pull_up_down]\t- not supported\n" \
	 "[initial]\t- Initial value for an output channel"
	},
	{"cleanup", (PyCFunction)py_cleanup, METH_VARARGS | METH_KEYWORDS,
	 "Clean up GPIO channel configuration by releasing all requested " \
	 "GPIOs\n[channel]\t- individual channel or list of channels to " \
	 "clean up"
	},
	{"output", py_output_gpio, METH_VARARGS,
	 "Output to a GPIO channel or list of channels\nchannel\t- BCM or " \
	 "RPi pin number depending on which mode is set\nvalue\t- 0, False " \
	 "or LOW for logical low or 1, True, or HIGH for logical high"
	},
	{"input", py_input_gpio, METH_VARARGS,
	 "Read input from a GPIO channel\nchannel\t- BCM or RPi pin number " \
	 "depending on which mode is set\nReturns LOW=0=False or HIGH=1=True"
	},
	{"setmode", py_setmode, METH_VARARGS,
	 "Configure the numbering scheme used for channels\nmode\t- " \
	 "numbering scheme to use: BOARD for RPi numbering or BCM for " \
	 "Broadcom GPIO numbering"
	},
	{"getmode", py_getmode, METH_VARARGS,
	 "Get numbering scheme used for channels\nReturns BOARD, BCM, or None"
	},
	{"gpio_function", py_gpio_function, METH_VARARGS,
	 "Get the current GPIO function\nchannel\t- BCM or RPi pin number " \
	 "depending on which mode is set\nReturns IN or OUT"
	},
	{"setwarnings", py_setwarnings, METH_VARARGS,
	 "Enable or disable warning messages\nstate\t- 0 or False for " \
	 "disabled, 1 or True for enabled"
	},
	{NULL, NULL, 0, NULL}
};

static const char module_doc_string[] = "Drop-in replacement for RPi.GPIO on "
					"PHYTEC hardware using Python and "
					"libgpiod";

#if PY_MAJOR_VERSION >= 3
static strict PyModuleDef rpi_gpio_module = {
	PyModuleDef_HEAD_INIT,
	"RPi._GPIO",
	module_doc_string,
	-1,
	rpi_gpio_methods
};
#endif

#if PY_MAJOR_VERSION >= 3
PyMODINIT_FUNC PyInit__GPIO(void)
#else
PyMODINIT_FUNC init_GPIO(void)
#endif
{
	PyObject *module = NULL;

#if PY_MAJOR_VERSION >= 3
	module = PyModule_Create(&rpi_gpio_module);
	if (!module)
		return NULL;
#else
	module = Py_InitModule3("RPi._GPIO", rpi_gpio_methods, module_doc_string);
	if (!module)
		return;
#endif

	define_constants(module);

	if (!PyEval_ThreadsInitialized())
		PyEval_InitThreads();

	if (Py_AtExit(cleanup)) {
		setup_error = 1;
		cleanup();
#if PY_MAJOR_VERSION >= 3
		return NULL;
#else
		return;
#endif
	}

	/* TODO: Event support
	if (Py_AtExit(event_cleanup_all)) {
		setup_error = 1;
		cleanup()
#if PY_MAJOR_VERSION >= 3
		return NULL;
#else
		return;
#endif
	}
	*/

#if PY_MAJOR_VERSION >= 3
	return module;
#else
	return;
#endif
}