/*
 * 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 <errno.h>

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

#define PROP_CLIENT_SUBSYS_SPEC_CAPSULE "prop_client.subsys_spec"

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

/* Polaris AIS Frame object */
typedef struct {
	PyObject_HEAD
	uint8_t type;
	uint8_t error;
	uint32_t seq;
	uint64_t time;
	uint8_t channel;
	uint16_t scale;
	int16_t mean;
	uint8_t length;
	PyObject *data;
} PolarisAISFrameObject;

static PyTypeObject PolarisAISFrameType;

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

static int Polaris_AIS_store_mirror_recv_cb(sl_polaris_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;
		PolarisAIS_store_mirror_recv_cbmap *cbmap = arg;

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

		/* Create constructor args */
		objargs = Py_BuildValue("bbIkbIibO", frame->type, frame->error, frame->seq, frame->time, frame->channel,
					frame->scale, frame->mean, frame->length, databytes);

		/* Call the class object */
		frameobj = PyObject_CallObject((PyObject *)&PolarisAISFrameType, 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(ais_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 None).\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 *PolarisAIS_store_mirror_recv(PolarisAISObject *self, PyObject *args, PyObject *kwds)
{
	int ret;
	uint32_t timeout = 2500;
	uint8_t mirror_port = SL_POLARIS_PORT_STORE_MIRROR_RECV;

	static const char *kwlist[] = { "cb", "cbarg", "mirror_port", "timeout", NULL };
	PolarisAIS_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_store_mirror_recv(self->node, mirror_port, timeout, Polaris_AIS_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(ais_sample_doc,
	     "Sample IQ data from an AIS channel or directly from the ADC.\n\n"
	     "Parameters:\n"
	     "timeout (int) (optional): Timeout of the command in milliseconds (default 2500).\n"
	     "samples (int) (optional): Number of samples to store, must always be 8192 if source\n"
	     "is SL_POLARIS_SAMPLEBUF_SOURCE_RAW (default 8192).\n"
	     "source (int) (optional): Sample source identifier. Must be one of:\n"
	     "    polaris_ais_client.SAMPLE_SOURCE_RAW\n"
	     "    polaris_ais_client.SAMPLE_SOURCE_AIS1_CH87B\n"
	     "    polaris_ais_client.SAMPLE_SOURCE_AIS2_CH88B\n"
	     "    polaris_ais_client.SAMPLE_SOURCE_AIS3_CH75\n"
	     "    polaris_ais_client.SAMPLE_SOURCE_AIS4_CH76\n"
	     "(default polaris_ais_client.SAMPLE_SOURCE_RAW).\n"
	     "wait_pps (bool) (optional): Start sample capture on next PPS pulse (default False).\n\n"
	     "Returns:\n"
	     "16bit I/Q (4 bytes per sample) samples on success, raises exception on failure.\n\n");

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

	uint32_t timeout = 2500;
	size_t samples = 8192;

	uint8_t source = SL_POLARIS_SAMPLEBUF_SOURCE_RAW;
	bool wait_pps = false;

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

	if (!PyArg_ParseTupleAndKeywords(args, kwds, "|IIbp", (char **)kwlist, &timeout, &samples, &source, &wait_pps))
		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_sample_read(self->node, timeout, source, sample_mem,
					samples * sizeof(*sample_mem), wait_pps);
	Py_END_ALLOW_THREADS
	if (ret < 0) {
		free(sample_mem);
		PyErr_Format(PyExc_RuntimeError, "Failed to trigger sample: %s", strerror(-ret));
		return NULL;
	}

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

	free(sample_mem);

	return byte_mem;
}

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

	self->node = SL_POLARIS_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_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 PolarisAIS_members[] = {
	{ "prop", T_OBJECT_EX, offsetof(PolarisAISObject, prop), 0, "PropNode" },
	{ "btp", T_OBJECT_EX, offsetof(PolarisAISObject, btp), 0, "BTPNode" },
	{ "csp", T_OBJECT_EX, offsetof(PolarisAISObject, csp), 0, "CSPNode" },
	{ NULL } /* Sentinel */
};

static PyMethodDef PolarisAIS_methods[] = {
	{"store_mirror_recv", (PyCFunction)(void(*)(void))PolarisAIS_store_mirror_recv, METH_VARARGS | METH_KEYWORDS, ais_store_mirror_doc},
	{"sample",            (PyCFunction)(void(*)(void))PolarisAIS_sample,	        METH_VARARGS | METH_KEYWORDS, ais_sample_doc},
	{NULL}  /* Sentinel */
};

static PyTypeObject PolarisAISType = {
	PyVarObject_HEAD_INIT(NULL, 0)
	.tp_name = "polaris_ais_client.Polaris",
	.tp_doc = "Polaris AIS object",
	.tp_basicsize = sizeof(PolarisAISObject),
	.tp_itemsize = 0,
	.tp_flags = Py_TPFLAGS_DEFAULT,
	.tp_new = PyType_GenericNew,
	.tp_init = (initproc)PolarisAIS_init,
	.tp_members = PolarisAIS_members,
	.tp_methods = PolarisAIS_methods,
};

static int PolarisAISFrame_init(PolarisAISFrameObject *self, PyObject *args, PyObject *kwds)
{
	static const char *kwlist[] = {"type",
				       "error",
				       "seq",
				       "time",
				       "channel",
				       "scale",
				       "mean",
				       "length",
				       "data",
				       NULL};

	if (!PyArg_ParseTupleAndKeywords(args, kwds, "bbIkbIibO", (char **)kwlist,
					 &self->type,
					 &self->error,
					 &self->seq,
					 &self->time,
					 &self->channel,
					 &self->scale,
					 &self->mean,
					 &self->length,
					 &self->data))
		return -1;

	Py_INCREF(self->data);

	return 0;
}


static PyMemberDef PolarisAISFrame_members[] = {
	{ "type",       T_UBYTE, offsetof(PolarisAISFrameObject, type), 0, "type" },
	{ "error",      T_UBYTE, offsetof(PolarisAISFrameObject, error), 0, "error" },
	{ "seq",        T_UINT, offsetof(PolarisAISFrameObject, seq), 0, "seq" },
	{ "time",       T_ULONG, offsetof(PolarisAISFrameObject, time), 0, "time" },
	{ "channel",    T_UBYTE, offsetof(PolarisAISFrameObject, channel), 0, "channel" },
	{ "scale",      T_USHORT, offsetof(PolarisAISFrameObject, scale), 0, "scale" },
	{ "mean",       T_SHORT, offsetof(PolarisAISFrameObject, mean), 0, "mean" },
	{ "length",     T_UBYTE, offsetof(PolarisAISFrameObject, length), 0, "length" },
	{ "data",       T_OBJECT_EX, offsetof(PolarisAISFrameObject, data), 0, "data" },
	{ NULL } /* Sentinel */
};

static PyTypeObject PolarisAISFrameType = {
	PyVarObject_HEAD_INIT(NULL, 0)
	.tp_name = "polaris_ais_client.PolarisFrame",
	.tp_doc = "Polaris AIS Frame object",
	.tp_basicsize = sizeof(PolarisAISFrameObject),
	.tp_itemsize = 0,
	.tp_flags = Py_TPFLAGS_DEFAULT,
	.tp_new = PyType_GenericNew,
	.tp_init = (initproc)PolarisAISFrame_init,
	.tp_members = PolarisAISFrame_members,
};

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

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

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

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

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

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

	PyModule_AddIntConstant(m, "DEFAULT_ADDRESS", SL_POLARIS_DEFAULT_ADDRESS);
	PyModule_AddIntConstant(m, "SAMPLE_SOURCE_RAW", SL_POLARIS_SAMPLEBUF_SOURCE_RAW);
	PyModule_AddIntConstant(m, "SAMPLE_SOURCE_AIS1_CH87B", SL_POLARIS_SAMPLEBUF_SOURCE_AIS1_CH87B);
	PyModule_AddIntConstant(m, "SAMPLE_SOURCE_AIS2_CH88B", SL_POLARIS_SAMPLEBUF_SOURCE_AIS2_CH88B);
	PyModule_AddIntConstant(m, "SAMPLE_SOURCE_AIS3_CH75", SL_POLARIS_SAMPLEBUF_SOURCE_AIS3_CH75);
	PyModule_AddIntConstant(m, "SAMPLE_SOURCE_AIS4_CH76", SL_POLARIS_SAMPLEBUF_SOURCE_AIS4_CH76);

	return m;
}
