/*
 * Copyright (c) 2016-2022 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 <fcntl.h>

#include <satlab/polaris_adsb.h>
#include <prop-client/prop_proto.h>

#define PROP_CLIENT_SUBSYS_SPEC_CAPSULE "prop_client.subsys_spec"

/* Polaris ADSB object */
typedef struct {
	PyObject_HEAD
	uint8_t node;
	uint32_t timeout;
	PyObject *prop;
	PyObject *btp;
	PyObject *csp;
} PolarisADSBObject;

/* Polaris ADSB Frame object */
typedef struct {
	PyObject_HEAD
	uint8_t type;
	uint8_t error;
	uint32_t seq;
	uint64_t time;
	int16_t rssi;
	uint16_t flags;
	PyObject *data;
} PolarisADSBFrameObject;

static PyTypeObject PolarisADSBFrameType;

/* Callback mapping */
typedef struct {
	PyObject *cb;
	PyObject *cbarg;
} PolarisADSB_store_mirror_recv_cbmap;

static int Polaris_ADSB_store_mirror_recv_cb(sl_polaris_adsb_store_frame_t *frame, void *arg)
{
	int ret = 0;

	PyGILState_STATE gstate;

	/* Grab GIL before calling back into Python */
	gstate = PyGILState_Ensure();

	if (frame != NULL) {
		PyObject *cbret, *arglist, *frameobj, *objargs, *databytes;
		PolarisADSB_store_mirror_recv_cbmap *cbmap = arg;

		/* Create data bytes object */
		databytes = PyBytes_FromStringAndSize((const char *)frame->data, SL_POLARIS_ADSB_STORE_MAX_FRAMELEN);

		/* Create constructor args */
		objargs = Py_BuildValue("bbIkhHO",
					frame->type,
					frame->error,
					frame->seq,
					frame->time,
					frame->rssi,
					frame->flags,
					databytes);

		/* Call the class object */
		frameobj = PyObject_CallObject((PyObject *)&PolarisADSBFrameType, objargs);

		/* Build argument list */
		arglist = Py_BuildValue("(OO)", frameobj, cbmap->cbarg);

		/* Call Python callback */
		cbret = PyObject_CallObject(cbmap->cb, arglist);
		Py_DECREF(databytes);
		Py_DECREF(objargs);
		Py_DECREF(frameobj);
		Py_DECREF(arglist);

		if (PyErr_Occurred()) {
			PyGILState_Release(gstate);
			return 0;
		} else {
			if (cbret != Py_None)
				ret = PyLong_AsLong(cbret);
			Py_XDECREF(cbret);
		}
	}

	/* Check for ctrl+c */
	PyErr_CheckSignals();
	if (PyErr_Occurred()) {
		if (PyErr_ExceptionMatches(PyExc_KeyboardInterrupt)) {
			ret = -EINTR;
		} else {
			ret = -EIO;
		}
	}

	/* Release GIL */
	PyGILState_Release(gstate);

	return ret;
}

PyDoc_STRVAR(adsb_store_mirror_doc,
	     "Receive mirrored frames from device, calls \'cb\' for each frame received.\n\n"
	     "Parameters:\n"
	     "cb (function): Frame callback function. The return value of the callback function determines what\n"
	     "will happen in the store mirror call:\n"
	     "    0 or None: The store mirror call continues indefinetely.\n"
	     "    Negative int: The store mirror call returns.\n"
	     "    Exceptions: Raised exceptions are re-raised by the store mirror call.\n"
	     "cbarg (-) (optional): Callback argument, passed to the callback function \'cb\' (default NULL).\n"
	     "mirror_port (int) (optional): Mirror frame receive port (default 30).\n"
	     "timeout (int) (optional): Timeout of the command in milliseconds (default 2500).\n\n"
	     "Returns:\n"
	     "None on success, raises exception on failure.\n\n");

static PyObject *PolarisADSB_store_mirror_recv(PolarisADSBObject *self, PyObject *args, PyObject *kwds)
{
	int ret;
	uint32_t timeout = 2500;
	uint8_t mirror_port = SL_POLARIS_ADSB_PORT_STORE_MIRROR_RECV;

	static const char *kwlist[] = { "cb", "cbarg", "mirror_port", "timeout", NULL };
	PolarisADSB_store_mirror_recv_cbmap cbmap;

	if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|ObI", (char **)kwlist, &cbmap.cb, &cbmap.cbarg, &mirror_port, &timeout))
		return NULL;

	if (!PyCallable_Check(cbmap.cb)) {
		PyErr_SetString(PyExc_TypeError, "cb must be callable");
		return NULL;
	}

	Py_BEGIN_ALLOW_THREADS
	ret = sl_polaris_adsb_store_mirror_recv(self->node, mirror_port, timeout,
						Polaris_ADSB_store_mirror_recv_cb, &cbmap);
	Py_END_ALLOW_THREADS
	if (ret < 0) {
		if (!PyErr_Occurred()) {
			if (ret != -EINTR)
				PyErr_Format(PyExc_TimeoutError, "Failed to request store mirror: %d", ret);
		}
		return NULL;
	}

	Py_RETURN_NONE;
}

PyDoc_STRVAR(adsb_sample_doc,
	     "Sample IQ data directly from the ADC.\n\n"
	     "Parameters:\n"
	     "timeout (int) (optional): Timeout of the command in milliseconds (default 2500).\n"
	     "Returns:\n"
	     "8192 16bit I/Q (4 bytes per sample) samples on success, raises exception on failure.\n\n");

static PyObject *PolarisADSB_sample(PolarisADSBObject *self, PyObject *args, PyObject *kwds)
{
	int ret;
	uint32_t *sample_mem;
	PyObject *byte_mem;

	uint32_t timeout = 2500;
	uint8_t source = SL_POLARIS_ADSB_SAMPLEBUF_SOURCE_RAW;
	size_t samples = 8192;

	static const char *kwlist[] = { "timeout", NULL };

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

	sample_mem = calloc(samples, sizeof(*sample_mem));
	if (!sample_mem) {
		PyErr_Format(PyExc_MemoryError, "Failed to allocate memory");
		return NULL;
	}

	Py_BEGIN_ALLOW_THREADS
	ret = sl_polaris_adsb_sample_read(self->node, timeout, source, sample_mem, samples * sizeof(*sample_mem));
	Py_END_ALLOW_THREADS
	if (ret < 0) {
		free(sample_mem);
		printf("Failed to trigger sample: %s\n", strerror(-ret));
		return NULL;
	}

	byte_mem = Py_BuildValue("y#", sample_mem, samples * sizeof(*sample_mem));

	free(sample_mem);

	return byte_mem;
}

static int PolarisADSB_init(PolarisADSBObject *self, PyObject *args, PyObject *kwds)
{
	int ret = -1;
	uint8_t port = SL_PROP_DEFAULT_PORT;

	self->node = SL_POLARIS_ADSB_DEFAULT_ADDRESS;
	self->timeout = 1000;

	static const char *kwlist[] = { "node", "timeout", NULL };
	if (!PyArg_ParseTupleAndKeywords(args, kwds, "|bI", (char **)kwlist, &self->node, &self->timeout))
		goto err;

	/* Get reference to prop client module */
	PyObject *pyprop = PyImport_ImportModule("satlab.prop_client");
	if (!pyprop)
		goto err;

	/* Get reference to PropNode class */
	PyObject *PropNode = PyObject_GetAttrString(pyprop, "PropNode");
	if (!PropNode)
		goto err_prop_node;

	/* Create capsule object with props */
	PyObject *props_capsule = PyCapsule_New((void *)&polaris_adsb_props, PROP_CLIENT_SUBSYS_SPEC_CAPSULE, NULL);
	if (!props_capsule)
		goto err_prop_capsule;

	/* Create PropNode object by calling constructor */
	self->prop = PyObject_CallFunction(PropNode, "bbOI", self->node, port, props_capsule, self->timeout);
	if (!self->prop)
		goto err_prop_ctor;

	/* Get reference to CSP module */
	PyObject *pycsp = PyImport_ImportModule("csp");
	if (!pycsp)
		goto err_csp;

	/* Get reference to CSPNode class */
	PyObject *CSPNode = PyObject_GetAttrString(pycsp, "CSPNode");
	if (!CSPNode)
		goto err_csp_node;

	/* Create CSPNode object by calling constructor */
	self->csp = PyObject_CallFunction(CSPNode, "b", self->node);
	if (!self->csp)
		goto err_csp_ctor;

	/* Get reference to btp module */
	PyObject *pybtp = PyImport_ImportModule("satlab.btp_client");
	if (!pybtp)
		goto err_btp;

	/* Get reference to BTPNode class */
	PyObject *BTPNode = PyObject_GetAttrString(pybtp, "BTPNode");
	if (!BTPNode)
		goto err_btp_node;

	/* Create BTPNode object by calling constructor */
	self->btp = PyObject_CallFunction(BTPNode, "bI", self->node, self->timeout);
	if (!self->btp)
		goto err_btp_ctor;

	ret = 0;

err_btp_ctor:
	Py_DECREF(BTPNode);
err_btp_node:
	Py_DECREF(pybtp);
err_btp:
err_csp_ctor:
	Py_DECREF(CSPNode);
err_csp_node:
	Py_DECREF(pycsp);
err_csp:
err_prop_ctor:
	Py_DECREF(props_capsule);
err_prop_capsule:
	Py_DECREF(PropNode);
err_prop_node:
	Py_DECREF(pyprop);
err:
	return ret;
}

static PyMemberDef Polaris_ADSB_members[] = {
	{ "prop", T_OBJECT_EX, offsetof(PolarisADSBObject, prop), 0, "PropNode" },
	{ "btp", T_OBJECT_EX, offsetof(PolarisADSBObject, btp), 0, "BTPNode" },
	{ "csp", T_OBJECT_EX, offsetof(PolarisADSBObject, csp), 0, "CSPNode" },
	{ NULL } /* Sentinel */
};

static PyMethodDef Polaris_ADSB_methods[] = {
	{ "store_mirror_recv", (PyCFunction)(void(*)(void))PolarisADSB_store_mirror_recv, METH_VARARGS | METH_KEYWORDS, adsb_store_mirror_doc },
	{ "sample",            (PyCFunction)(void(*)(void))PolarisADSB_sample,            METH_VARARGS | METH_KEYWORDS, adsb_sample_doc },
	{ NULL } /* Sentinel */
};

static PyTypeObject PolarisADSBType = {
	PyVarObject_HEAD_INIT(NULL, 0)
	.tp_name = "polaris_adsb_client.Polaris",
	.tp_doc = "Polaris ADSB object",
	.tp_basicsize = sizeof(PolarisADSBObject),
	.tp_itemsize = 0,
	.tp_flags = Py_TPFLAGS_DEFAULT,
	.tp_new = PyType_GenericNew,
	.tp_init = (initproc)PolarisADSB_init,
	.tp_members = Polaris_ADSB_members,
	.tp_methods = Polaris_ADSB_methods,
};

static int PolarisADSBFrame_init(PolarisADSBFrameObject *self, PyObject *args, PyObject *kwds)
{
	static const char *kwlist[] = { "type", "error", "seq", "time", "rssi", "flags", "data", NULL };

	if (!PyArg_ParseTupleAndKeywords(args, kwds, "bbIkhHO", (char **)kwlist, &self->type, &self->error, &self->seq,
					 &self->time, &self->rssi, &self->flags, &self->data))
		return -1;

	Py_INCREF(self->data);

	return 0;
}

static PyMemberDef PolarisADSBFrame_members[] = {
	{ "type", T_UBYTE, offsetof(PolarisADSBFrameObject, type), 0, "type" },
	{ "error", T_UBYTE, offsetof(PolarisADSBFrameObject, error), 0, "error" },
	{ "seq", T_UINT, offsetof(PolarisADSBFrameObject, seq), 0, "seq" },
	{ "time", T_ULONG, offsetof(PolarisADSBFrameObject, time), 0, "time" },
	{ "rssi", T_SHORT, offsetof(PolarisADSBFrameObject, rssi), 0, "rssi" },
	{ "flags", T_USHORT, offsetof(PolarisADSBFrameObject, flags), 0, "flags" },
	{ "data", T_OBJECT_EX, offsetof(PolarisADSBFrameObject, data), 0, "data" },
	{ NULL } /* Sentinel */
};

static PyTypeObject PolarisADSBFrameType = {
	PyVarObject_HEAD_INIT(NULL, 0)
	.tp_name = "polaris_adsb_client.PolarisFrame",
	.tp_doc = "Polaris ADSB Frame object",
	.tp_basicsize = sizeof(PolarisADSBFrameObject),
	.tp_itemsize = 0,
	.tp_flags = Py_TPFLAGS_DEFAULT,
	.tp_new = PyType_GenericNew,
	.tp_init = (initproc)PolarisADSBFrame_init,
	.tp_members = PolarisADSBFrame_members,
};

static struct PyModuleDef polaris_adsb_client_module = {
	.m_base = PyModuleDef_HEAD_INIT,
	.m_name = "polaris_adsb_client",
	.m_size = -1,
};

PyMODINIT_FUNC PyInit_polaris_adsb_client_bindings(void)
{
	/* Prepare objects */
	if (PyType_Ready(&PolarisADSBType) < 0)
		return NULL;

	if (PyType_Ready(&PolarisADSBFrameType) < 0)
		return NULL;

	PyObject *m = PyModule_Create(&polaris_adsb_client_module);
	if (!m)
		return NULL;

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

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

	PyModule_AddIntConstant(m, "DEFAULT_ADDRESS", SL_POLARIS_ADSB_DEFAULT_ADDRESS);

	return m;
}
