/*
 Author: José Bollo <jobol@nonadev.net>

 https://gitlab.com/jobol/mustach

 SPDX-License-Identifier: ISC
*/

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#ifdef _WIN32
#include <malloc.h>
#endif

#include "mustach.h"
#include "mustach-wrap.h"

/*
* It was stated that allowing to include files
* through template is not safe when the mustache
* template is open to any value because it could
* create leaks (example: {{>/etc/passwd}}).
*/
#if MUSTACH_SAFE
# undef MUSTACH_LOAD_TEMPLATE
#elif !defined(MUSTACH_LOAD_TEMPLATE)
# define MUSTACH_LOAD_TEMPLATE 1
#endif

#if !defined(INCLUDE_PARTIAL_EXTENSION)
# define INCLUDE_PARTIAL_EXTENSION ".mustache"
#endif

/* global hook for partials */
int (*mustach_wrap_get_partial)(const char *name, struct mustach_sbuf *sbuf) = NULL;

/* internal structure for wrapping */
struct wrap {
	/* original interface */
	const struct mustach_wrap_itf *itf;

	/* original closure */
	void *closure;

	/* flags */
	int flags;

	/* emiter callback */
	mustach_emit_cb_t *emitcb;

	/* write callback */
	mustach_write_cb_t *writecb;
};

/* length given by masking with 3 */
enum comp {
	C_no = 0,
	C_eq = 1,
	C_lt = 5,
	C_le = 6,
	C_gt = 9,
	C_ge = 10
};

enum sel {
	S_none = 0,
	S_ok = 1,
	S_objiter = 2,
	S_ok_or_objiter = S_ok | S_objiter
};

static enum comp getcomp(char *head, int sflags)
{
	return (head[0] == '=' && (sflags & Mustach_With_Equal)) ? C_eq
		: (head[0] == '<' && (sflags & Mustach_With_Compare)) ? (head[1] == '=' ? C_le : C_lt)
		: (head[0] == '>' && (sflags & Mustach_With_Compare)) ? (head[1] == '=' ? C_ge : C_gt)
		: C_no;
}

static char *keyval(char *head, int sflags, enum comp *comp)
{
	char *w, car, escaped;
	enum comp k;

	k = C_no;
	w = head;
	car = *head;
	escaped = (sflags & Mustach_With_EscFirstCmp) && (getcomp(head, sflags) != C_no);
	while (car && (escaped || (k = getcomp(head, sflags)) == C_no)) {
		if (escaped)
			escaped = 0;
		else
			escaped = ((sflags & Mustach_With_JsonPointer) ? car == '~' : car == '\\')
			    && (getcomp(head + 1, sflags) != C_no);
		if (!escaped)
			*w++ = car;
		head++;
		car = *head;
	}
	*w = 0;
	*comp = k;
	return k == C_no ? NULL : &head[k & 3];
}

static char *getkey(char **head, int sflags)
{
	char *result, *iter, *write, car;

	car = *(iter = *head);
	if (!car)
		result = NULL;
	else {
		result = write = iter;
		if (sflags & Mustach_With_JsonPointer)
		{
			while (car && car != '/') {
				if (car == '~')
					switch (iter[1]) {
					case '1': car = '/'; /*@fallthrough@*/
					case '0': iter++;
					}
				*write++ = car;
				car = *++iter;
			}
			*write = 0;
			while (car == '/')
				car = *++iter;
		}
		else
		{
			while (car && car != '.') {
				if (car == '\\' && (iter[1] == '.' || iter[1] == '\\'))
					car = *++iter;
				*write++ = car;
				car = *++iter;
			}
			*write = 0;
			while (car == '.')
				car = *++iter;
		}
		*head = iter;
	}
	return result;
}

static enum sel sel(struct wrap *w, const char *name)
{
	enum sel result;
	int i, j, sflags, scmp;
	char *key, *value;
	enum comp k;

	/* make a local writeable copy */
	size_t lenname = 1 + strlen(name);
	char buffer[lenname];
	char *copy = buffer;
	memcpy(copy, name, lenname);

	/* check if matches json pointer selection */
	sflags = w->flags;
	if (sflags & Mustach_With_JsonPointer) {
		if (copy[0] == '/')
			copy++;
		else
			sflags ^= Mustach_With_JsonPointer;
	}

	/* extract the value, translate the key and get the comparator */
	if (sflags & (Mustach_With_Equal | Mustach_With_Compare))
		value = keyval(copy, sflags, &k);
	else {
		k = C_no;
		value = NULL;
	}

	/* case of . alone if Mustach_With_SingleDot? */
	if (copy[0] == '.' && copy[1] == 0 /*&& (sflags & Mustach_With_SingleDot)*/)
		/* yes, select current */
		result = w->itf->sel(w->closure, NULL) ? S_ok : S_none;
	else
	{
		/* not the single dot, extract the first key */
		key = getkey(&copy, sflags);
		if (key == NULL)
			return 0;

		/* select the root item */
		if (w->itf->sel(w->closure, key))
			result = S_ok;
		else if (key[0] == '*'
		      && !key[1]
		      && !value
		      && !*copy
		      && (w->flags & Mustach_With_ObjectIter)
		      && w->itf->sel(w->closure, NULL))
			result = S_ok_or_objiter;
		else
			result = S_none;
		if (result == S_ok) {
			/* iterate the selection of sub items */
			key = getkey(&copy, sflags);
			while(result == S_ok && key) {
				if (w->itf->subsel(w->closure, key))
					/* nothing */;
				else if (key[0] == '*'
				      && !key[1]
				      && !value
				      && !*copy
				      && (w->flags & Mustach_With_ObjectIter))
					result = S_objiter;
				else
					result = S_none;
				key = getkey(&copy, sflags);
			}
		}
	}
	/* should it be compared? */
	if (result == S_ok && value) {
		if (!w->itf->compare)
			result = S_none;
		else {
			i = value[0] == '!';
			scmp = w->itf->compare(w->closure, &value[i]);
			switch (k) {
			case C_eq: j = scmp == 0; break;
			case C_lt: j = scmp < 0; break;
			case C_le: j = scmp <= 0; break;
			case C_gt: j = scmp > 0; break;
			case C_ge: j = scmp >= 0; break;
			default: j = i; break;
			}
			if (i == j)
				result = S_none;
		}
	}
	return result;
}

static int start_callback(void *closure)
{
	struct wrap *w = closure;
	return w->itf->start ? w->itf->start(w->closure) : MUSTACH_OK;
}

static void stop_callback(void *closure, int status)
{
	struct wrap *w = closure;
	if (w->itf->stop)
		w->itf->stop(w->closure, status);
}

static int emit(struct wrap *w, const char *buffer, size_t size, FILE *file)
{
	int r;

	if (w->writecb)
		r = w->writecb(file, buffer, size);
	else
		r = fwrite(buffer, 1, size, file) == size ? MUSTACH_OK : MUSTACH_ERROR_SYSTEM;
	return r;
}

static int emit_callback(void *closure, const char *buffer, size_t size, int escape, FILE *file)
{
	struct wrap *w = closure;
	int r;
	size_t s, i;
	char car;

	if (w->emitcb)
		r = w->emitcb(file, buffer, size, escape);
	else if (!escape)
		r = emit(w, buffer, size, file);
	else {
		i = 0;
		r = MUSTACH_OK;
		while(i < size && r == MUSTACH_OK) {
			s = i;
			while (i < size && (car = buffer[i]) != '<' && car != '>' && car != '&' && car != '"')
				i++;
			if (i != s)
				r = emit(w, &buffer[s], i - s, file);
			if (i < size && r == MUSTACH_OK) {
				switch(car) {
				case '<': r = emit(w, "&lt;", 4, file); break;
				case '>': r = emit(w, "&gt;", 4, file); break;
				case '&': r = emit(w, "&amp;", 5, file); break;
				case '"': r = emit(w, "&quot;", 6, file); break;
				}
				i++;
			}
		}
	}
	return r;
}

static int enter_callback(void *closure, const char *name)
{
	struct wrap *w = closure;
	enum sel s = sel(w, name);
	return s == S_none ? 0 : w->itf->enter(w->closure, s & S_objiter);
}

static int next_callback(void *closure)
{
	struct wrap *w = closure;
	return w->itf->next(w->closure);
}

static int leave_callback(void *closure)
{
	struct wrap *w = closure;
	return w->itf->leave(w->closure);
}

static int getoptional(struct wrap *w, const char *name, struct mustach_sbuf *sbuf)
{
	enum sel s = sel(w, name);
	if (!(s & S_ok))
		return 0;
	return w->itf->get(w->closure, sbuf, s & S_objiter);
}

static int get_callback(void *closure, const char *name, struct mustach_sbuf *sbuf)
{
	struct wrap *w = closure;
	if (getoptional(w, name, sbuf) <= 0) {
		if (w->flags & Mustach_With_ErrorUndefined)
			return MUSTACH_ERROR_UNDEFINED_TAG;
		sbuf->value = "";
	}
	return MUSTACH_OK;
}

#if MUSTACH_LOAD_TEMPLATE
static int get_partial_from_file(const char *name, struct mustach_sbuf *sbuf)
{
	static char extension[] = INCLUDE_PARTIAL_EXTENSION;
	size_t s;
	long pos;
	FILE *file;
	char *path, *buffer;

	/* allocate path */
	s = strlen(name);
	path = malloc(s + sizeof extension);
	if (path == NULL)
		return MUSTACH_ERROR_SYSTEM;

	/* try without extension first */
	memcpy(path, name, s + 1);
	file = fopen(path, "r");
	if (file == NULL) {
		memcpy(&path[s], extension, sizeof extension);
		file = fopen(path, "r");
	}
	free(path);

	/* if file opened */
	if (file == NULL)
		return MUSTACH_ERROR_PARTIAL_NOT_FOUND;

	/* compute file size */
	if (fseek(file, 0, SEEK_END) >= 0
	 && (pos = ftell(file)) >= 0
	 && fseek(file, 0, SEEK_SET) >= 0) {
		/* allocate value */
		s = (size_t)pos;
		buffer = malloc(s + 1);
		if (buffer != NULL) {
			/* read value */
			if (1 == fread(buffer, s, 1, file)) {
				/* force zero at end */
				sbuf->value = buffer;
				buffer[s] = 0;
				sbuf->freecb = free;
				fclose(file);
				return MUSTACH_OK;
			}
			free(buffer);
		}
	}
	fclose(file);
	return MUSTACH_ERROR_SYSTEM;
}
#endif

static int partial_callback(void *closure, const char *name, struct mustach_sbuf *sbuf)
{
	struct wrap *w = closure;
	int rc;
	if (mustach_wrap_get_partial != NULL) {
		rc = mustach_wrap_get_partial(name, sbuf);
		if (rc != MUSTACH_ERROR_PARTIAL_NOT_FOUND) {
			if (rc != MUSTACH_OK)
				sbuf->value = "";
			return rc;
		}
	}
#if MUSTACH_LOAD_TEMPLATE
	if (w->flags & Mustach_With_PartialDataFirst) {
		if (getoptional(w, name, sbuf) > 0)
			rc = MUSTACH_OK;
		else
			rc = get_partial_from_file(name, sbuf);
	}
	else {
		rc = get_partial_from_file(name, sbuf);
		if (rc != MUSTACH_OK &&  getoptional(w, name, sbuf) > 0)
			rc = MUSTACH_OK;
	}
#else
	rc = getoptional(w, name, sbuf) > 0 ?  MUSTACH_OK : MUSTACH_ERROR_PARTIAL_NOT_FOUND;
#endif
	if (rc != MUSTACH_OK)
		sbuf->value = "";
	return MUSTACH_OK;
}

const struct mustach_itf mustach_wrap_itf = {
	.start = start_callback,
	.put = NULL,
	.enter = enter_callback,
	.next = next_callback,
	.leave = leave_callback,
	.partial = partial_callback,
	.get = get_callback,
	.emit = emit_callback,
	.stop = stop_callback
};

static void wrap_init(struct wrap *wrap, const struct mustach_wrap_itf *itf, void *closure, int flags, mustach_emit_cb_t *emitcb, mustach_write_cb_t *writecb)
{
	if (flags & Mustach_With_Compare)
		flags |= Mustach_With_Equal;
	wrap->closure = closure;
	wrap->itf = itf;
	wrap->flags = flags;
	wrap->emitcb = emitcb;
	wrap->writecb = writecb;
}

int mustach_wrap_file(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, FILE *file)
{
	struct wrap w;
	wrap_init(&w, itf, closure, flags, NULL, NULL);
	return mustach_file(template, length, &mustach_wrap_itf, &w, flags, file);
}

int mustach_wrap_fd(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, int fd)
{
	struct wrap w;
	wrap_init(&w, itf, closure, flags, NULL, NULL);
	return mustach_fd(template, length, &mustach_wrap_itf, &w, flags, fd);
}

int mustach_wrap_mem(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, char **result, size_t *size)
{
	struct wrap w;
	wrap_init(&w, itf, closure, flags, NULL, NULL);
	return mustach_mem(template, length, &mustach_wrap_itf, &w, flags, result, size);
}

int mustach_wrap_write(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, mustach_write_cb_t *writecb, void *writeclosure)
{
	struct wrap w;
	wrap_init(&w, itf, closure, flags, NULL, writecb);
	return mustach_file(template, length, &mustach_wrap_itf, &w, flags, writeclosure);
}

int mustach_wrap_emit(const char *template, size_t length, const struct mustach_wrap_itf *itf, void *closure, int flags, mustach_emit_cb_t *emitcb, void *emitclosure)
{
	struct wrap w;
	wrap_init(&w, itf, closure, flags, emitcb, NULL);
	return mustach_file(template, length, &mustach_wrap_itf, &w, flags, emitclosure);
}

