/**
 * jinja._speedups
 * ~~~~~~~~~~~~~~~
 *
 * This module implements the BaseContext, a c implementation of the
 * Context baseclass. If this extension is not compiled the datastructure
 * module implements a class in python.
 *
 * Note that if you change semantics here you have to edit the _native.py
 * to in order to support those changes for jinja setups without the
 * speedup module too.
 *
 * :copyright: 2007 by Armin Ronacher.
 * :license: BSD, see LICENSE for more details.
 */

#include <Python.h>
#include <structmember.h>

/* Set by init_constants to real values */
static PyObject *Deferred;

/**
 * Internal struct used by BaseContext to store the
 * stacked namespaces.
 */
struct StackLayer {
	PyObject *dict;			/* current value, a dict */
	struct StackLayer *prev;	/* lower struct layer or NULL */
};

/**
 * BaseContext python class.
 */
typedef struct {
	PyObject_HEAD
	struct StackLayer *globals;	/* the dict for the globals */
	struct StackLayer *initial;	/* initial values */
	struct StackLayer *current;	/* current values */
	long stacksize;			/* current size of the stack */
	PyObject *undefined_singleton;	/* the singleton returned on missing values */
} BaseContext;

/**
 * Called by init_speedups in order to retrieve references
 * to some exceptions and classes defined in jinja python modules
 */
static int
init_constants(void)
{
	PyObject *datastructure = PyImport_ImportModule("jinja.datastructure");
	if (!datastructure)
		return 0;
	Deferred = PyObject_GetAttrString(datastructure, "Deferred");
	Py_DECREF(datastructure);
	return 1;
}

/**
 * GC Helper
 */
static int
BaseContext_clear(BaseContext *self)
{
	struct StackLayer *current = self->current, *tmp;
	while (current) {
		tmp = current;
		Py_XDECREF(current->dict);
		current->dict = NULL;
		current = tmp->prev;
		PyMem_Free(tmp);
	}
	self->current = NULL;
	return 0;
}

/**
 * Deallocator for BaseContext.
 *
 * Frees the memory for the stack layers before freeing the object.
 */
static void
BaseContext_dealloc(BaseContext *self)
{
	BaseContext_clear(self);
	self->ob_type->tp_free((PyObject*)self);
}

/**
 * GC Helper
 */
static int
BaseContext_traverse(BaseContext *self, visitproc visit, void *args)
{
	int vret;
	struct StackLayer *layer = self->current;

	while (layer) {
		vret = visit(layer->dict, args);
		if (vret != 0)
			return vret;
		layer = layer->prev;
	}

	return 0;
}

/**
 * Initializes the BaseContext.
 *
 * Like the native python class it takes a reference to the undefined
 * singleton which will be used for undefined values.
 * The other two arguments are the global namespace and the initial
 * namespace which usually contains the values passed to the render
 * function of the template. Both must be dicts.
 */
static int
BaseContext_init(BaseContext *self, PyObject *args, PyObject *kwds)
{
	PyObject *undefined = NULL, *globals = NULL, *initial = NULL;

	if (!PyArg_ParseTuple(args, "OOO", &undefined, &globals, &initial))
		return -1;
	if (!PyDict_Check(globals) || !PyDict_Check(initial)) {
		PyErr_SetString(PyExc_TypeError, "stack layers must be dicts.");
		return -1;
	}

	self->current = PyMem_Malloc(sizeof(struct StackLayer));
	self->current->prev = NULL;
	self->current->dict = PyDict_New();
	if (!self->current->dict)
		return -1;

	self->initial = PyMem_Malloc(sizeof(struct StackLayer));
	self->initial->prev = NULL;
	self->initial->dict = initial;
	Py_INCREF(initial);
	self->current->prev = self->initial;

	self->globals = PyMem_Malloc(sizeof(struct StackLayer));
	self->globals->prev = NULL;
	self->globals->dict = globals;
	Py_INCREF(globals);
	self->initial->prev = self->globals;

	self->undefined_singleton = undefined;
	Py_INCREF(undefined);

	self->stacksize = 3;
	return 0;
}

/**
 * Pop the highest layer from the stack and return it
 */
static PyObject*
BaseContext_pop(BaseContext *self)
{
	PyObject *result;
	struct StackLayer *tmp = self->current;

	if (self->stacksize <= 3) {
		PyErr_SetString(PyExc_IndexError, "stack too small.");
		return NULL;
	}
	result = self->current->dict;
	assert(result);
	self->current = tmp->prev;
	PyMem_Free(tmp);
	self->stacksize--;
	/* Took the reference to result from the struct. */
	return result;
}

/**
 * Push a new layer to the stack and return it. If no parameter
 * is provided an empty dict is created. Otherwise the dict passed
 * to it is used as new layer.
 */
static PyObject*
BaseContext_push(BaseContext *self, PyObject *args)
{
	PyObject *value = NULL;
	struct StackLayer *new;

	if (!PyArg_ParseTuple(args, "|O:push", &value))
		return NULL;
	if (!value) {
		value = PyDict_New();
		if (!value)
			return NULL;
	}
	else if (!PyDict_Check(value)) {
		PyErr_SetString(PyExc_TypeError, "dict required.");
		return NULL;
	}
	else
		Py_INCREF(value);
	new = PyMem_Malloc(sizeof(struct StackLayer));
	if (!new) {
		Py_DECREF(value);
		return NULL;
	}
	new->dict = value;
	new->prev = self->current;
	self->current = new;
	self->stacksize++;
	Py_INCREF(value);
	return value;
}

/**
 * Getter that creates a list representation of the internal
 * stack. Used for compatibility with the native python implementation.
 */
static PyObject*
BaseContext_getstack(BaseContext *self, void *closure)
{
	int idx = 0;
	struct StackLayer *current = self->current;
	PyObject *result = PyList_New(self->stacksize);
	if (!result)
		return NULL;
	while (current) {
		Py_INCREF(current->dict);
		PyList_SET_ITEM(result, idx++, current->dict);
		current = current->prev;
	}
	PyList_Reverse(result);
	return result;
}

/**
 * Getter that returns a reference to the current layer in the context.
 */
static PyObject*
BaseContext_getcurrent(BaseContext *self, void *closure)
{
	Py_INCREF(self->current->dict);
	return self->current->dict;
}

/**
 * Getter that returns a reference to the initial layer in the context.
 */
static PyObject*
BaseContext_getinitial(BaseContext *self, void *closure)
{
	Py_INCREF(self->initial->dict);
	return self->initial->dict;
}

/**
 * Getter that returns a reference to the global layer in the context.
 */
static PyObject*
BaseContext_getglobals(BaseContext *self, void *closure)
{
	Py_INCREF(self->globals->dict);
	return self->globals->dict;
}

/**
 * Implements the context lookup.
 *
 * This works exactly like the native implementation but a lot
 * faster. It disallows access to internal names (names that start
 * with "::") and resolves Deferred values.
 */
static PyObject*
BaseContext_getitem(BaseContext *self, PyObject *item)
{
	PyObject *result;
	char *name = NULL;
	int isdeferred;
	struct StackLayer *current = self->current;
	
	/* allow unicode keys as long as they are ascii keys */
	if (PyUnicode_CheckExact(item)) {
		item = PyUnicode_AsASCIIString(item);
		if (!item)
			goto missing;
	}
	else if (!PyString_Check(item))
		goto missing;

	/* disallow access to internal jinja values */
	name = PyString_AS_STRING(item);
	if (name[0] == ':' && name[1] == ':')
		goto missing;

	while (current) {
		/* GetItemString just builds a new string from "name" again... */
		result = PyDict_GetItem(current->dict, item);
		if (!result) {
			current = current->prev;
			continue;
		}
		isdeferred = PyObject_IsInstance(result, Deferred);
		if (isdeferred == -1)
			return NULL;
		else if (isdeferred) {
			PyObject *namespace;
			PyObject *resolved = PyObject_CallFunctionObjArgs(
						result, self, item, NULL);
			if (!resolved)
				return NULL;

			/* never touch the globals */
			if (current == self->globals)
				namespace = self->initial->dict;
			else
				namespace = current->dict;
			if (PyDict_SetItem(namespace, item, resolved) < 0)
				return NULL;
			Py_INCREF(resolved);
			return resolved;
		}
		Py_INCREF(result);
		return result;
	}

missing:
	Py_INCREF(self->undefined_singleton);
	return self->undefined_singleton;
}

/**
 * Check if the context contains a given value.
 */
static int
BaseContext_contains(BaseContext *self, PyObject *item)
{
	char *name;
	struct StackLayer *current = self->current;

	/* allow unicode objects as keys as long as they are ASCII */
	if (PyUnicode_CheckExact(item)) {
		item = PyUnicode_AsASCIIString(item);
		if (!item)
			return 0;
	}
	else if (!PyString_Check(item))
		return 0;

	name = PyString_AS_STRING(item);
	if (name[0] == ':' && name[1] == ':')
		return 0;

	while (current) {
		/* XXX: for 2.4 and newer, use PyDict_Contains */
		if (!PyMapping_HasKey(current->dict, item)) {
			current = current->prev;
			continue;
		}
		return 1;
	}

	return 0;
}

/**
 * Set an value in the highest layer or delete one.
 */
static int
BaseContext_setitem(BaseContext *self, PyObject *item, PyObject *value)
{
	/* allow unicode objects as keys as long as they are ASCII */
	if (PyUnicode_CheckExact(item)) {
		item = PyUnicode_AsASCIIString(item);
		if (!item) {
			PyErr_Clear();
			goto error;
		}
	}
	else if (!PyString_Check(item))
		goto error;
	if (!value)
		return PyDict_DelItem(self->current->dict, item);
	return PyDict_SetItem(self->current->dict, item, value);

error:
	PyErr_SetString(PyExc_TypeError, "expected string argument");
	return -1;
}


static PyGetSetDef BaseContext_getsetters[] = {
	{"stack", (getter)BaseContext_getstack, NULL,
	 "a read only copy of the internal stack", NULL},
	{"current", (getter)BaseContext_getcurrent, NULL,
	 "reference to the current layer on the stack", NULL},
	{"initial", (getter)BaseContext_getinitial, NULL,
	 "reference to the initial layer on the stack", NULL},
	{"globals", (getter)BaseContext_getglobals, NULL,
	 "reference to the global layer on the stack", NULL},
	{NULL}				/* Sentinel */
};

static PyMethodDef BaseContext_methods[] = {
	{"pop", (PyCFunction)BaseContext_pop, METH_NOARGS,
	 "ctx.pop() -> dict\n\n"
	 "Pop the last layer from the stack and return it."},
	{"push", (PyCFunction)BaseContext_push, METH_VARARGS,
	 "ctx.push([layer]) -> layer\n\n"
	 "Push one layer to the stack. Layer must be a dict "
	 "or omitted."},
	{NULL}				/* Sentinel */
};

static PySequenceMethods BaseContext_as_sequence = {
	0,				/* sq_length */
	0,				/* sq_concat */
	0,				/* sq_repeat */
	0,				/* sq_item */
	0,				/* sq_slice */
	0,				/* sq_ass_item */
	0,				/* sq_ass_slice */
	(objobjproc)BaseContext_contains,/* sq_contains */
	0,				/* sq_inplace_concat */
	0				/* sq_inplace_repeat */
};

static PyMappingMethods BaseContext_as_mapping = {
	NULL,
	(binaryfunc)BaseContext_getitem,
	(objobjargproc)BaseContext_setitem
};

static PyTypeObject BaseContextType = {
	PyObject_HEAD_INIT(NULL)
	0,				/* ob_size */
	"jinja._speedups.BaseContext",	/* tp_name */
	sizeof(BaseContext),		/* tp_basicsize */
	0,				/* tp_itemsize */
	(destructor)BaseContext_dealloc,/* tp_dealloc */
	0,				/* tp_print */
	0,				/* tp_getattr */
	0,				/* tp_setattr */
	0,				/* tp_compare */
	0,				/* tp_repr */
	0,				/* tp_as_number */
	&BaseContext_as_sequence,	/* tp_as_sequence */
	&BaseContext_as_mapping,	/* tp_as_mapping */
	0,				/* tp_hash */
	0,				/* tp_call */
	0,				/* tp_str */
	0,				/* tp_getattro */
	0,				/* tp_setattro */
	0,				/* tp_as_buffer */
	Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
					/*tp_flags*/
	"",				/* tp_doc */
	(traverseproc)BaseContext_traverse, /* tp_traverse */
	(inquiry)BaseContext_clear,	/* tp_clear */
	0,				/* tp_richcompare */
	0,				/* tp_weaklistoffset */
	0,				/* tp_iter */
	0,				/* tp_iternext */
	BaseContext_methods,		/* tp_methods */
	0,				/* tp_members */
	BaseContext_getsetters,		/* tp_getset */
	0,				/* tp_base */
	0,				/* tp_dict */
	0,				/* tp_descr_get */
	0,				/* tp_descr_set */
	0,				/* tp_dictoffset */
	(initproc)BaseContext_init,	/* tp_init */
	0,				/* tp_alloc */
	0				/* tp_new */
};

static PyMethodDef module_methods[] = {
	{NULL, NULL, 0, NULL}		/* Sentinel */
};

#ifndef PyMODINIT_FUNC	/* declarations for DLL import/export */
#define PyMODINIT_FUNC void
#endif
PyMODINIT_FUNC
init_speedups(void)
{
	PyObject *module;

	BaseContextType.tp_new = (newfunc)PyType_GenericNew;
	if (PyType_Ready(&BaseContextType) < 0)
		return;

	if (!init_constants())
		return;

	module = Py_InitModule3("_speedups", module_methods, "");
	if (!module)
		return;

	Py_INCREF(&BaseContextType);
	PyModule_AddObject(module, "BaseContext", (PyObject*)&BaseContextType);
}
