/*
 * Copyright (C) 2013 Felix Fietkau <nbd@openwrt.org>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */
#include <sys/stat.h>
#include <regex.h>

#include "avl-cmp.h"
#include "json_script.h"

struct json_call {
	struct json_script_ctx *ctx;
	struct blob_attr *vars;
	unsigned int seq;
};

struct json_handler {
	const char *name;
	int (*cb)(struct json_call *call, struct blob_attr *cur);
};

static int json_process_expr(struct json_call *call, struct blob_attr *cur);
static int json_process_cmd(struct json_call *call, struct blob_attr *cur);
static int eval_string(struct json_call *call, struct blob_buf *buf, const char *name, const char *pattern);

struct json_script_file *
json_script_file_from_blobmsg(const char *name, void *data, int len)
{
	struct json_script_file *f;
	char *new_name;
	int name_len = 0;

	if (name)
		name_len = strlen(name) + 1;

	f = calloc_a(sizeof(*f) + len, &new_name, name_len);
	if (!f)
		return NULL;

	memcpy(f->data, data, len);
	if (name)
		f->avl.key = strcpy(new_name, name);

	return f;
}

static struct json_script_file *
json_script_get_file(struct json_script_ctx *ctx, const char *filename)
{
	struct json_script_file *f;

	f = avl_find_element(&ctx->files, filename, f, avl);
	if (f)
		return f;

	f = ctx->handle_file(ctx, filename);
	if (!f)
		return NULL;

	avl_insert(&ctx->files, &f->avl);
	return f;
}

static void __json_script_run(struct json_call *call, struct json_script_file *file,
			      struct blob_attr *context)
{
	struct json_script_ctx *ctx = call->ctx;

	if (file->seq == call->seq) {
		if (context)
			ctx->handle_error(ctx, "Recursive include", context);

		return;
	}

	file->seq = call->seq;
	while (file) {
		json_process_cmd(call, file->data);
		file = file->next;
	}
}

const char *json_script_find_var(struct json_script_ctx *ctx, struct blob_attr *vars,
				 const char *name)
{
	struct blob_attr *cur;
	size_t rem;

	blobmsg_for_each_attr(cur, vars, rem) {
		if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING)
			continue;

		if (strcmp(blobmsg_name(cur), name) != 0)
			continue;

		return blobmsg_data(cur);
	}

	return ctx->handle_var(ctx, name, vars);
}

static const char *
msg_find_var(struct json_call *call, const char *name)
{
	return json_script_find_var(call->ctx, call->vars, name);
}

static void
json_get_tuple(struct blob_attr *cur, struct blob_attr **tb, int t1, int t2)
{
	static struct blobmsg_policy expr_tuple[3] = {
		{ .type = BLOBMSG_TYPE_STRING },
		{},
		{},
	};

	expr_tuple[1].type = t1;
	expr_tuple[2].type = t2;
	blobmsg_parse_array(expr_tuple, 3, tb, blobmsg_data(cur), blobmsg_data_len(cur));
}

static int handle_if(struct json_call *call, struct blob_attr *expr)
{
	struct blob_attr *tb[4];
	int ret;

	static const struct blobmsg_policy if_tuple[4] = {
		{ .type = BLOBMSG_TYPE_STRING },
		{ .type = BLOBMSG_TYPE_ARRAY },
		{ .type = BLOBMSG_TYPE_ARRAY },
		{ .type = BLOBMSG_TYPE_ARRAY },
	};

	blobmsg_parse_array(if_tuple, 4, tb, blobmsg_data(expr), blobmsg_data_len(expr));

	if (!tb[1] || !tb[2])
		return 0;

	ret = json_process_expr(call, tb[1]);
	if (ret < 0)
		return 0;

	if (ret)
		return json_process_cmd(call, tb[2]);

	if (!tb[3])
		return 0;

	return json_process_cmd(call, tb[3]);
}

static int handle_case(struct json_call *call, struct blob_attr *expr)
{
	struct blob_attr *tb[3], *cur;
	const char *var;
	size_t rem;

	json_get_tuple(expr, tb, BLOBMSG_TYPE_STRING, BLOBMSG_TYPE_TABLE);
	if (!tb[1] || !tb[2])
		return 0;

	var = msg_find_var(call, blobmsg_data(tb[1]));
	if (!var)
		return 0;

	blobmsg_for_each_attr(cur, tb[2], rem) {
		if (!strcmp(var, blobmsg_name(cur)))
			return json_process_cmd(call, cur);
	}

	return 0;
}

static int handle_return(struct json_call *call, struct blob_attr *expr)
{
	return -2;
}

static int handle_include(struct json_call *call, struct blob_attr *expr)
{
	struct blob_attr *tb[3];
	struct json_script_file *f;

	json_get_tuple(expr, tb, BLOBMSG_TYPE_STRING, 0);
	if (!tb[1])
		return 0;

	f = json_script_get_file(call->ctx, blobmsg_data(tb[1]));
	if (!f)
		return 0;

	__json_script_run(call, f, expr);
	return 0;
}

static const struct json_handler cmd[] = {
	{ "if", handle_if },
	{ "case", handle_case },
	{ "return", handle_return },
	{ "include", handle_include },
};

static int eq_regex_cmp(const char *str, const char *pattern, bool regex)
{
	regex_t reg;
	int ret;

	if (!regex)
		return !strcmp(str, pattern);

	if (regcomp(&reg, pattern, REG_EXTENDED | REG_NOSUB))
		return 0;

	ret = !regexec(&reg, str, 0, NULL, 0);
	regfree(&reg);

	return ret;
}

static int expr_eq_regex(struct json_call *call, struct blob_attr *expr, bool regex)
{
	struct json_script_ctx *ctx = call->ctx;
	struct blob_attr *tb[3], *cur;
	const char *var;
	size_t rem;

	json_get_tuple(expr, tb, BLOBMSG_TYPE_STRING, 0);
	if (!tb[1] || !tb[2])
		return -1;

	var = msg_find_var(call, blobmsg_data(tb[1]));
	if (!var)
		return 0;

	switch(blobmsg_type(tb[2])) {
	case BLOBMSG_TYPE_STRING:
		return eq_regex_cmp(var, blobmsg_data(tb[2]), regex);
	case BLOBMSG_TYPE_ARRAY:
		blobmsg_for_each_attr(cur, tb[2], rem) {
			if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING) {
				ctx->handle_error(ctx, "Unexpected element type", cur);
				return -1;
			}

			if (eq_regex_cmp(var, blobmsg_data(cur), regex))
				return 1;
		}
		return 0;
	default:
		ctx->handle_error(ctx, "Unexpected element type", tb[2]);
		return -1;
	}
}

static int handle_expr_eq(struct json_call *call, struct blob_attr *expr)
{
	return expr_eq_regex(call, expr, false);
}

static int handle_expr_regex(struct json_call *call, struct blob_attr *expr)
{
	return expr_eq_regex(call, expr, true);
}

static int handle_expr_has(struct json_call *call, struct blob_attr *expr)
{
	struct json_script_ctx *ctx = call->ctx;
	struct blob_attr *tb[3], *cur;
	size_t rem;

	json_get_tuple(expr, tb, 0, 0);
	if (!tb[1])
		return -1;

	switch(blobmsg_type(tb[1])) {
	case BLOBMSG_TYPE_STRING:
		return !!msg_find_var(call, blobmsg_data(tb[1]));
	case BLOBMSG_TYPE_ARRAY:
		blobmsg_for_each_attr(cur, tb[1], rem) {
			if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING) {
				ctx->handle_error(ctx, "Unexpected element type", cur);
				return -1;
			}

			if (msg_find_var(call, blobmsg_data(cur)))
				return 1;
		}
		return 0;
	default:
		ctx->handle_error(ctx, "Unexpected element type", tb[1]);
		return -1;
	}
}

static int expr_and_or(struct json_call *call, struct blob_attr *expr, bool and)
{
	struct blob_attr *cur;
	int ret;
	size_t rem;
	int i = 0;

	blobmsg_for_each_attr(cur, expr, rem) {
		if (i++ < 1)
			continue;

		ret = json_process_expr(call, cur);
		if (ret < 0)
			return ret;

		if (ret != and)
			return ret;
	}

	return and;
}

static int handle_expr_and(struct json_call *call, struct blob_attr *expr)
{
	return expr_and_or(call, expr, 1);
}

static int handle_expr_or(struct json_call *call, struct blob_attr *expr)
{
	return expr_and_or(call, expr, 0);
}

static int handle_expr_not(struct json_call *call, struct blob_attr *expr)
{
	struct blob_attr *tb[3];
	int ret;

	json_get_tuple(expr, tb, BLOBMSG_TYPE_ARRAY, 0);
	if (!tb[1])
		return -1;

	ret = json_process_expr(call, tb[1]);
	if (ret < 0)
		return ret;
	return !ret;
}

static int handle_expr_isdir(struct json_call *call, struct blob_attr *expr)
{
	static struct blob_buf b;
	struct blob_attr *tb[3];
	const char *pattern, *path;
	struct stat s;
	int ret;

	json_get_tuple(expr, tb, BLOBMSG_TYPE_STRING, 0);
	if (!tb[1] || blobmsg_type(tb[1]) != BLOBMSG_TYPE_STRING)
		return -1;
	pattern = blobmsg_data(tb[1]);

	blob_buf_init(&b, 0);
	ret = eval_string(call, &b, NULL, pattern);
	if (ret < 0)
		return ret;
	path = blobmsg_data(blob_data(b.head));
	ret = stat(path, &s);
	if (ret < 0)
		return 0;
	return S_ISDIR(s.st_mode);
}

static const struct json_handler expr[] = {
	{ "eq", handle_expr_eq },
	{ "regex", handle_expr_regex },
	{ "has", handle_expr_has },
	{ "and", handle_expr_and },
	{ "or", handle_expr_or },
	{ "not", handle_expr_not },
	{ "isdir", handle_expr_isdir },
};

static int
__json_process_type(struct json_call *call, struct blob_attr *cur,
		    const struct json_handler *h, int n, bool *found)
{
	const char *name = blobmsg_data(blobmsg_data(cur));
	int i;

	for (i = 0; i < n; i++) {
		if (strcmp(name, h[i].name) != 0)
			continue;

		*found = true;
		return h[i].cb(call, cur);
	}

	*found = false;
	return -1;
}

static int json_process_expr(struct json_call *call, struct blob_attr *cur)
{
	struct json_script_ctx *ctx = call->ctx;
	bool found;
	int ret;

	if (blobmsg_type(cur) != BLOBMSG_TYPE_ARRAY ||
	    blobmsg_type(blobmsg_data(cur)) != BLOBMSG_TYPE_STRING) {
		ctx->handle_error(ctx, "Unexpected element type", cur);
		return -1;
	}

	ret = __json_process_type(call, cur, expr, ARRAY_SIZE(expr), &found);
	if (!found) {
		const char *name = blobmsg_data(blobmsg_data(cur));
		ctx->handle_expr(ctx, name, cur, call->vars);
	}

	return ret;
}

static int eval_string(struct json_call *call, struct blob_buf *buf, const char *name, const char *pattern)
{
	char *dest, *next, *str;
	int len = 0;
	bool var = false;
	char c = '%';

	dest = blobmsg_alloc_string_buffer(buf, name, 0);
	if (!dest)
		return -1;

	next = alloca(strlen(pattern) + 1);
	strcpy(next, pattern);

	for (str = next; str; str = next) {
		const char *cur;
		char *end, *new_buf;
		int cur_len = 0;
		bool cur_var = var;

		end = strchr(str, '%');
		if (end) {
			*end = 0;
			next = end + 1;
			var = !var;
		} else {
			end = str + strlen(str);
			next = NULL;
		}

		if (cur_var) {
			if (end > str) {
				cur = msg_find_var(call, str);
				if (!cur)
					continue;

				cur_len = strlen(cur);
			} else {
				cur = &c;
				cur_len = 1;
			}
		} else {
			if (str == end)
				continue;

			cur = str;
			cur_len = end - str;
		}

		new_buf = blobmsg_realloc_string_buffer(buf, len + cur_len);
		if (!new_buf) {
			/* Make eval_string return -1 */
			var = true;
			break;
		}

		dest = new_buf;
		memcpy(dest + len, cur, cur_len);
		len += cur_len;
	}

	dest[len] = 0;
	blobmsg_add_string_buffer(buf);

	if (var)
		return -1;

	return 0;
}

static int cmd_add_string(struct json_call *call, const char *pattern)
{
	return eval_string(call, &call->ctx->buf, NULL, pattern);
}

int json_script_eval_string(struct json_script_ctx *ctx, struct blob_attr *vars,
			    struct blob_buf *buf, const char *name,
			    const char *pattern)
{
	struct json_call call = {
		.ctx = ctx,
		.vars = vars,
	};

	return eval_string(&call, buf, name, pattern);
}

static int cmd_process_strings(struct json_call *call, struct blob_attr *attr)
{
	struct json_script_ctx *ctx = call->ctx;
	struct blob_attr *cur;
	int args = -1;
	int ret;
	size_t rem;
	void *c;

	blob_buf_init(&ctx->buf, 0);
	c = blobmsg_open_array(&ctx->buf, NULL);
	blobmsg_for_each_attr(cur, attr, rem) {
		if (args++ < 0)
			continue;

		if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING) {
			blobmsg_add_blob(&ctx->buf, cur);
			continue;
		}

		ret = cmd_add_string(call, blobmsg_data(cur));
		if (ret) {
			ctx->handle_error(ctx, "Unterminated variable reference in string", attr);
			return ret;
		}
	}

	blobmsg_close_array(&ctx->buf, c);

	return 0;
}

static int __json_process_cmd(struct json_call *call, struct blob_attr *cur)
{
	struct json_script_ctx *ctx = call->ctx;
	const char *name;
	bool found;
	int ret;

	if (blobmsg_type(cur) != BLOBMSG_TYPE_ARRAY ||
	    blobmsg_type(blobmsg_data(cur)) != BLOBMSG_TYPE_STRING) {
		ctx->handle_error(ctx, "Unexpected element type", cur);
		return -1;
	}

	ret = __json_process_type(call, cur, cmd, ARRAY_SIZE(cmd), &found);
	if (found)
		return ret;

	name = blobmsg_data(blobmsg_data(cur));
	ret = cmd_process_strings(call, cur);
	if (ret)
		return ret;

	ctx->handle_command(ctx, name, blob_data(ctx->buf.head), call->vars);

	return 0;
}

static int json_process_cmd(struct json_call *call, struct blob_attr *block)
{
	struct json_script_ctx *ctx = call->ctx;
	struct blob_attr *cur;
	size_t rem;
	int ret;
	int i = 0;

	if (blobmsg_type(block) != BLOBMSG_TYPE_ARRAY) {
		ctx->handle_error(ctx, "Unexpected element type", block);
		return -1;
	}

	blobmsg_for_each_attr(cur, block, rem) {
		if (ctx->abort)
			break;

		switch(blobmsg_type(cur)) {
		case BLOBMSG_TYPE_STRING:
			if (!i)
				return __json_process_cmd(call, block);
			fallthrough;
		default:
			ret = json_process_cmd(call, cur);
			if (ret < -1)
				return ret;
			break;
		}
		i++;
	}

	return 0;
}

void json_script_run_file(struct json_script_ctx *ctx, struct json_script_file *file,
			  struct blob_attr *vars)
{
	static unsigned int _seq = 0;
	struct json_call call = {
		.ctx = ctx,
		.vars = vars,
		.seq = ++_seq,
	};

	/* overflow */
	if (!call.seq)
		call.seq = ++_seq;

	ctx->abort = false;

	__json_script_run(&call, file, NULL);
}

void json_script_run(struct json_script_ctx *ctx, const char *name,
		     struct blob_attr *vars)
{
	struct json_script_file *file;

	file = json_script_get_file(ctx, name);
	if (!file)
		return;

	json_script_run_file(ctx, file, vars);
}

static void __json_script_file_free(struct json_script_file *f)
{
	struct json_script_file *next;

	if (!f)
		return;

	next = f->next;
	free(f);

	__json_script_file_free(next);
}

void
json_script_free(struct json_script_ctx *ctx)
{
	struct json_script_file *f, *next;

	avl_remove_all_elements(&ctx->files, f, avl, next)
		__json_script_file_free(f);

	blob_buf_free(&ctx->buf);
}

static void
__default_handle_error(struct json_script_ctx *ctx, const char *msg,
		       struct blob_attr *context)
{
}

static const char *
__default_handle_var(struct json_script_ctx *ctx, const char *name,
		     struct blob_attr *vars)
{
	return NULL;
}

static int
__default_handle_expr(struct json_script_ctx *ctx, const char *name,
		      struct blob_attr *expr, struct blob_attr *vars)
{
	ctx->handle_error(ctx, "Unknown expression type", expr);
	return -1;
}

static struct json_script_file *
__default_handle_file(struct json_script_ctx *ctx, const char *name)
{
	return NULL;
}

void json_script_init(struct json_script_ctx *ctx)
{
	avl_init(&ctx->files, avl_strcmp, false, NULL);

	if (!ctx->handle_error)
		ctx->handle_error = __default_handle_error;

	if (!ctx->handle_var)
		ctx->handle_var = __default_handle_var;

	if (!ctx->handle_expr)
		ctx->handle_expr = __default_handle_expr;

	if (!ctx->handle_file)
		ctx->handle_file = __default_handle_file;
}
