/*
 * Implementation of support type for informal protocols.
 *
 * See the module DOCSTR for more information.
 */
#include "pyobjc.h"

PyDoc_STRVAR(proto_cls_doc,
"objc.informal_protocol(name, selector_list)\n"
"\n"
"This class can be used to specify which methods are supported by an informal\n"
"protocol. Instances of this type can by used while creating subclasses of \n"
"objective-C classes to automaticly specify method signatures et.al."
"");

typedef struct {
	PyObject_HEAD

	PyObject* name;
	PyObject* selectors;
} PyObjCInformalProtocol;


static PyObject* selToProtocolMapping = NULL;


static void
proto_dealloc(PyObject* object)
{
	PyObjCInformalProtocol* self = (PyObjCInformalProtocol*)object;	
#if 0
	/*
	 * For some reason this code causes a crash, while it should
	 * be the reverse of the code in proto_new.
	 */
	Py_ssize_t len = PyTuple_Size(self->selectors);
	Py_ssize_t i;

	for (i = 0; i < len; i++) {
		PyObjCSelector* tmp =
			(PyObjCSelector*)PyTuple_GET_ITEM(
				self->selectors, i);
		
		PyDict_DelItemString(selToProtocolMapping,
			sel_getName(tmp->sel_selector));
	}
#endif

	Py_XDECREF(self->name);
	Py_XDECREF(self->selectors);
	Py_TYPE(object)->tp_free(object);
}

static PyObject*
proto_repr(PyObject* object)
{
	PyObjCInformalProtocol* self = (PyObjCInformalProtocol*)object;	
	PyObject* b = NULL;

	if (PyUnicode_Check(self->name)) {
		b = PyUnicode_AsEncodedString(self->name, NULL, NULL);
#if PY_MAJOR_VERSION == 2
	} else if (PyString_Check(self->name)) {
		b = self->name; Py_INCREF(b);
#endif
	} else {
		b = PyBytes_FromString("<null>");
	}
	if (b == NULL) {
		return NULL;
	}

	PyObject* r = PyText_FromFormat("<%s %s at %p>", Py_TYPE(self)->tp_name, PyBytes_AsString(b), (void*)self);
	Py_XDECREF(b);
	return r;
}

static PyObject*
proto_new(PyTypeObject* type __attribute__((__unused__)), 
	PyObject* args, PyObject* kwds)
{
static	char*	keywords[] = { "name", "selectors", NULL };
	PyObjCInformalProtocol* result;
	PyObject* name;
	PyObject* selectors;
	Py_ssize_t       i, len;

	if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO:informal_protocol",
			keywords, &name, &selectors)) { 
		return NULL;
	}

	if (PyUnicode_Check(name)) {
		/* pass */
#if PY_MAJOR_VERSION == 2
	} else if (PyString_Check(name)) {
		/* pass */
#endif
	} else {
		PyErr_SetString(PyExc_TypeError,
			"Name must be a string");
		return NULL;
	}

	selectors = PySequence_Tuple(selectors);
	if (selectors == NULL) {
		return NULL;
	}

	result = (PyObjCInformalProtocol*)PyObject_New(
			PyObjCInformalProtocol, &PyObjCInformalProtocol_Type);

	result->name = name;
	Py_XINCREF(name);
	result->selectors = selectors;

	len = PyTuple_GET_SIZE(selectors);
	for (i = 0; i < len; i++) {
		if (!PyObjCSelector_Check(
				PyTuple_GET_ITEM(selectors, i))) {
			PyErr_Format(PyExc_TypeError, 
				"Item %"PY_FORMAT_SIZE_T"d is not a selector", i);
			Py_DECREF(result);
			return NULL;
		}
	}

	if (selToProtocolMapping == NULL) {
		selToProtocolMapping = PyDict_New();
		if (selToProtocolMapping == NULL) {
			Py_DECREF(result);
			return NULL;
		}
	}

	for (i = 0; i < len; i++) {
		PyObjCSelector* tmp =
			(PyObjCSelector*)PyTuple_GET_ITEM(selectors, i);
		
		PyDict_SetItemString(selToProtocolMapping,
			(char*)sel_getName(tmp->sel_selector),
			(PyObject*)result);
	}


	return (PyObject*)result;
}

static int 
proto_traverse(PyObject* self, visitproc visit, void* handle)
{
	PyObjCInformalProtocol* me = (PyObjCInformalProtocol*)self;	
	int                   err;

	err = visit(me->name, handle);
	if (err) return err;
	err = visit(me->selectors, handle);	
	if (err) return err;
	return 0;
}

static PyMemberDef proto_members[] = {
	{
		"__name__",
		T_OBJECT,
		offsetof(PyObjCInformalProtocol, name),
		READONLY,
		NULL
	},
	{
		"selectors",
		T_OBJECT,
		offsetof(PyObjCInformalProtocol, selectors),
		READONLY,
		NULL
	},

	{ 0, 0, 0, 0, 0 }
};

PyTypeObject PyObjCInformalProtocol_Type = {
	PyVarObject_HEAD_INIT(&PyType_Type, 0)
	"objc.informal_protocol",		/* tp_name */
	sizeof(PyObjCInformalProtocol),		/* tp_basicsize */
	0,					/* tp_itemsize */
	/* methods */
	proto_dealloc,	 			/* tp_dealloc */
	0,					/* tp_print */
	0,					/* tp_getattr */
	0,					/* tp_setattr */
	0,					/* tp_compare */
	proto_repr,				/* tp_repr */
	0,					/* tp_as_number */
	0,					/* tp_as_sequence */
	0,		       			/* tp_as_mapping */
	0,					/* tp_hash */
	0,					/* tp_call */
	0,					/* tp_str */
	PyObject_GenericGetAttr,		/* tp_getattro */
	0,					/* tp_setattro */
	0,					/* tp_as_buffer */
	Py_TPFLAGS_DEFAULT,			/* tp_flags */
 	proto_cls_doc,				/* tp_doc */
 	proto_traverse,				/* tp_traverse */
 	0,					/* tp_clear */
	0,					/* tp_richcompare */
	0,					/* tp_weaklistoffset */
	0,					/* tp_iter */
	0,					/* tp_iternext */
	0,					/* tp_methods */
	proto_members,				/* tp_members */
	0, /* proto_getset , */			/* tp_getset */
	0,					/* tp_base */
	0,					/* tp_dict */
	0,					/* tp_descr_get */
	0,					/* tp_descr_set */
	0,					/* tp_dictoffset */
	0,					/* tp_init */
	0,					/* tp_alloc */
	proto_new,				/* tp_new */
	0,		        		/* tp_free */
	0,					/* tp_is_gc */
        0,                                      /* tp_bases */
        0,                                      /* tp_mro */
        0,                                      /* tp_cache */
        0,                                      /* tp_subclasses */
        0,                                      /* tp_weaklist */
        0                                       /* tp_del */
#if PY_VERSION_HEX >= 0x02060000
	, 0                                     /* tp_version_tag */
#endif

};


/*
 * Find information about a selector in the protocol.
 *
 * Return NULL if no information can be found, but does not set an
 * exception.
 */
PyObject* 
PyObjCInformalProtocol_FindSelector(PyObject* obj, SEL selector, int isClassMethod)
{
	PyObjCInformalProtocol* self = (PyObjCInformalProtocol*)obj;	
	Py_ssize_t i, len;
	PyObject* cur;
	PyObject* seq;

	if (!PyObjCInformalProtocol_Check(obj)) {
		PyErr_Format(PyExc_TypeError, 
			"First argument is not an 'objc.informal_protocol' "
			"but '%s'", Py_TYPE(obj)->tp_name);
		return 0;
	}

	seq = PySequence_Fast(self->selectors,"selector list not a sequence?");
	if (seq == NULL) {
		return 0;
	}

	len = PySequence_Fast_GET_SIZE(seq);
	for (i = 0; i < len; i++) {
		cur = PySequence_Fast_GET_ITEM(self->selectors, i);
		if (cur == NULL) {
			continue;
		}

		if (PyObjCSelector_Check(cur)) {
			int class_sel = (
				PyObjCSelector_GetFlags(cur) 
				& PyObjCSelector_kCLASS_METHOD) != 0;
			if ((isClassMethod && !class_sel) 
					|| (!isClassMethod && class_sel)) {
				continue;
			}

			if (sel_isEqual(PyObjCSelector_GetSelector(cur), selector)) {
				Py_DECREF(seq);
				return cur;
			}
		}
	}
	Py_DECREF(seq);
	return NULL;
}

PyObject*
findSelInDict(PyObject* clsdict, SEL selector)
{
	PyObject* values;
	PyObject* seq;
	Py_ssize_t       i, len;

	values = PyDict_Values(clsdict);
	if (values == NULL) {
		return NULL;
	}

	seq = PySequence_Fast(values, "PyDict_Values result not a sequence");
	if (seq == NULL) {
		return NULL;
	}
	
	len = PySequence_Fast_GET_SIZE(seq);
	for (i = 0; i < len; i++) {
		PyObject* v = PySequence_Fast_GET_ITEM(seq, i);
		if (!PyObjCSelector_Check(v)) continue;
		if (PyObjCSelector_GetSelector(v) == selector) {
			Py_DECREF(seq);
			Py_DECREF(values);
			Py_INCREF(v);
			return v;
		}
	}
	Py_DECREF(seq);
	Py_DECREF(values);
	return NULL;
}

int 
signaturesEqual(char* sig1, char* sig2)
{
	char buf1[1024];
	char buf2[1024];
	int r;

	/* Return 0 if the two signatures are not equal */
	if (strcmp(sig1, sig2) == 0) return 1;

	/* For some reason compiler-generated signatures contain numbers that
	 * are not used by the runtime. These are irrelevant for our comparison
	 */
	r = PyObjCRT_SimplifySignature(sig1, buf1, sizeof(buf1));
	if (r == -1) { 
		return 0; 
	}

	r = PyObjCRT_SimplifySignature(sig2, buf2, sizeof(buf2));
	if (r == -1) { 
		return 0; 
	}
	return strcmp(buf1, buf2) == 0;
}

/*
 * Verify that 'cls' conforms to the informal protocol
 */
int	
PyObjCInformalProtocol_CheckClass(
	PyObject* obj, char* name, PyObject* super_class, PyObject* clsdict)
{
	PyObjCInformalProtocol* self = (PyObjCInformalProtocol*)obj;	
	Py_ssize_t i, len;
	PyObject* cur;
	PyObject* seq;

	if (!PyObjCInformalProtocol_Check(obj)) {
		PyErr_Format(PyExc_TypeError, 
			"First argument is not an 'objc.informal_protocol' "
			"but '%s'", Py_TYPE(obj)->tp_name);
		return 0;
	}
	if (!PyObjCClass_Check(super_class)) {
		PyErr_Format(PyExc_TypeError, 
			"Third argument is not an 'objc.objc_class' but "
			"'%s'", Py_TYPE(super_class)->tp_name);
		return 0;
	}
	if (!PyDict_Check(clsdict)) {
		PyErr_Format(PyExc_TypeError, 
			"Fourth argument is not a 'dict' but '%s'",
			Py_TYPE(clsdict)->tp_name);
		return 0;
	}

	seq = PySequence_Fast(self->selectors, "selector list not a sequence");
	if (seq == NULL) {
		return 0;
	}

	len = PySequence_Fast_GET_SIZE(seq);
	for (i = 0; i < len; i++) {
		SEL sel;
		PyObject* m;

		cur = PySequence_Fast_GET_ITEM(seq, i);
		if (cur == NULL) {
			continue;
		}

		if (!PyObjCSelector_Check(cur)) {
			continue;
		}

		sel = PyObjCSelector_GetSelector(cur);

		m = findSelInDict(clsdict, sel);
		if (m == NULL) {
			m = PyObjCClass_FindSelector(super_class, sel, PyObjCSelector_IsClassMethod(cur));
		}

		if (m == NULL || !PyObjCSelector_Check(m)) {
			Py_XDECREF(m);
			if (PyObjCSelector_Required(cur)) {
				PyErr_Format(PyExc_TypeError,
					"class %s does not fully implement "
					"protocol %S: no implementation for %s",
					name,
					self->name,
					sel_getName(sel));
				Py_DECREF(seq);
				return 0;
			} else {
				PyErr_Clear();
			}
		} else {
			if (!signaturesEqual(PyObjCSelector_Signature(m),
				PyObjCSelector_Signature(cur)) != 0) {

				PyErr_Format(PyExc_TypeError,
					"class %s does not correctly implement "
					"protocol %S: "
					"the signature for method %s is "
					"%s instead of %s",
					name,
					self->name,
					sel_getName(sel),
					PyObjCSelector_Signature(m),
					PyObjCSelector_Signature(cur)
				);
				Py_DECREF(seq);
				Py_DECREF(m);
				return 0;
			}
			Py_DECREF(m);
		}
	}
	Py_DECREF(seq);
	return 1;
}

PyObject*
PyObjCInformalProtocol_FindProtocol(SEL selector)
{
	PyObject* item;

	if (selToProtocolMapping == NULL) return NULL;

	item = PyDict_GetItemString(selToProtocolMapping, (char*)sel_getName(selector));
	if (item != NULL) {
		return item;
	}

	PyErr_Clear();
	return NULL;
}
