/*****************************************************************************\
 *  extra_constraints.c
 *****************************************************************************
 *  Copyright (C) SchedMD LLC.
 *
 *  This file is part of Slurm, a resource management program.
 *  For details, see <https://slurm.schedmd.com/>.
 *  Please also read the included file: DISCLAIMER.
 *
 *  Slurm is free software; you can redistribute it and/or modify it under
 *  the terms of the GNU General Public License as published by the Free
 *  Software Foundation; either version 2 of the License, or (at your option)
 *  any later version.
 *
 *  In addition, as a special exception, the copyright holders give permission
 *  to link the code of portions of this program with the OpenSSL library under
 *  certain conditions as described in each individual source file, and
 *  distribute linked combinations including the two. You must obey the GNU
 *  General Public License in all respects for all of the code used other than
 *  OpenSSL. If you modify file(s) with this exception, you may extend this
 *  exception to your version of the file(s), but you are not obligated to do
 *  so. If you do not wish to do so, delete this exception statement from your
 *  version.  If you delete this exception statement from all source files in
 *  the program, then also delete it here.
 *
 *  Slurm is distributed in the hope that it will be useful, but WITHOUT ANY
 *  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 *  FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with Slurm; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA.
\*****************************************************************************/

#include <string.h>

#include "src/common/extra_constraints.h"
#include "src/common/log.h"
#include "src/common/macros.h"
#include "src/common/xassert.h"
#include "src/common/xmalloc.h"
#include "src/common/xstring.h"

#define _DEBUG 0 /* Set this to non-zero to see detailed debugging */

#define OP_BEGIN OP_CHILD_AND

typedef enum {
	CMP_INVALID = -2,
	CMP_LT = -1,
	CMP_EQ = 0,
	CMP_GT = 1
} cmp_t;

typedef struct {
	op_t op;
	char *op_str;
} op_tbl_t;

#define CHILDREN_LEN 2 /* Starting length of the children array */

static op_tbl_t op_table[] = {
	{ OP_NONE, NULL },
	{ OP_CHILD_AND, "&" },
	{ OP_CHILD_AND_COMMA, "," },
	{ OP_CHILD_OR, "|" },
	{ OP_LEAF_EQ, "=" },
	{ OP_LEAF_NE, "!=" },
	{ OP_LEAF_GT, ">" },
	{ OP_LEAF_GTE, ">=" },
	{ OP_LEAF_LT, "<" },
	{ OP_LEAF_LTE, "<=" },
};
static const int op_table_len = ARRAY_SIZE(op_table);

static const char *child_op_chars = ",&|";
static const char *leaf_op_chars = "<>=!";
static const char *op_chars = ",&|<>=!";

static bool extra_constraints_parsing = false;

static char *_op2str(op_t op)
{
	return op_table[op].op_str;
}

static void _element2str(elem_t *el, int indent, char **str, char **pos)
{
	char *newline = *pos ? "\n" : "";

	if (el->children) {
		xstrfmtcatat(*str, pos, "%s%*s{key:\"%s\", value:\"%s\", operator:\"%s\"(%d), num_children:%d, children:%p}",
			     newline, indent, "", el->key, el->value,
			     op_table[el->operator].op_str, el->operator,
			     el->num_children, el->children);
	} else {
		xstrfmtcatat(*str, pos, "%s%*s{key:\"%s\", value:\"%s\", operator:\"%s\"(%d)}",
			     newline, indent, "", el->key, el->value,
			     op_table[el->operator].op_str, el->operator);
	}
}

static void _tree2str_recursive(elem_t *el, int indent, char **str, char **pos)
{
	if (!el)
		return;
	if (!el->num_children) {
		/* leaf */
		_element2str(el, indent, str, pos);
		return;
	}
	xassert(el->children);
	_element2str(el, indent, str, pos);
	for (int i = 0; i < el->num_children; i++) {
		_tree2str_recursive(el->children[i], indent + 4, str, pos);
	}
}

extern char *extra_constraints_2str(elem_t *el)
{
	char *pos = NULL;
	char *output_str = NULL;

	_tree2str_recursive(el, 0, &output_str, &pos);
	return output_str;
}

#if _DEBUG
static void _log_element(elem_t *el)
{
	char *str = NULL;
	char *pos = NULL;

	_element2str(el, 0, &str, &pos);
	info("%s", str);
	xfree(str);
}
#endif

static void _free_null_elem(elem_t **el)
{
	if (*el) {
		xfree((*el)->children);
		xfree((*el)->key);
		xfree((*el)->value);
		xfree((*el));
	}
}
#define _free_element(el) _free_null_elem(&el)

extern bool extra_constraints_enabled(void)
{
	return extra_constraints_parsing;
}

extern void extra_constraints_free_null(elem_t **el)
{
	if (!(*el))
		return;
	if (!(*el)->num_children) {
		/* leaf */
		_free_element(*el);
		return;
	} else {
		xassert((*el)->children);
		for (int i = 0; i < (*el)->num_children; i++) {
			_free_element((*el)->children[i]);
		}
		_free_element((*el));
	}
	xfree((*el));
}

static elem_t *_alloc_tree(void)
{
	return xmalloc(sizeof(elem_t));
}

static void _add_child(elem_t *parent, elem_t *child)
{
	int num_children;
	int curr_max;

	xassert(parent);
	xassert(child);

	num_children = parent->num_children;
	curr_max = parent->curr_max_children;
	if (!parent->children) {
		parent->children =
			xcalloc(CHILDREN_LEN,
				sizeof(*parent->children));
		curr_max = CHILDREN_LEN;
	} else if (num_children == curr_max) {
		curr_max = num_children * 2;
		xrecalloc(parent->children, curr_max,
			  sizeof(*parent->children));
	}
	parent->children[num_children] = child;
	parent->num_children++;
	parent->curr_max_children = curr_max;
}

/*
 * Given a string and a set of valid operator characters, return the matching
 * operator. Also set *end_out to point to the first invalid character.
 * Return OP_NONE if the operator was not found.
 */
static op_t _str2op(char *str, const char *valid_chars, char **end_out)
{
	op_t op = OP_NONE;
	char save_char;
	char *end = str;

	xassert(xstrchr(valid_chars, *str));

	while (*end) {
		if (!xstrchr(valid_chars, *end))
			break;
		end++;
	}
	save_char = *end;
	*end = '\0';

	for (int i = OP_BEGIN; i < op_table_len; i++) {
		op_t tmp = op_table[i].op;
		if (!xstrcmp(str, _op2str(tmp))) {
			op = tmp;
			break;
		}
	}
	/* Automatically convert ',' to '&' */
	if (op == OP_CHILD_AND_COMMA)
		op = OP_CHILD_AND;

	*end = save_char;
	*end_out = end;
	return op;
}

/*
 * Return true if the key or value is valid, false otherwise.
 * A valid key/value is not an empty string and does not have any operator
 * characters.
 */
static bool _valid_key_value(char *str)
{
	char *p;

	if (!str || (*str == '\0'))
		return false;

	while (*str) {
		if ((p = xstrchr(op_chars, *str)))
			return false;
		str++;
	}
	return true;
}

/*
 * Leaf:
 * <key><op><value>
 *
 * Return: SLURM_SUCCESS or SLURM_ERROR
 */
static elem_t *_parse_leaf(char *str)
{
	char *key;
	char *val = NULL;
	char *op_ptr = NULL;
	op_t op;
	elem_t *leaf;

	if (!str)
		return NULL;

	/* This is not a leaf if there are paren */
	xassert(!xstrchr(str, '(') && !xstrchr(str, ')'));

	if (*str == '\0') {
#if _DEBUG
		error("Leaf is empty");
#endif
		return NULL;
	}

	key = xstrdup(str);

	/* Find the first leaf operator character */
	op_ptr = key;
	while (*op_ptr) {
		if (xstrchr(leaf_op_chars, *op_ptr))
			break;
		op_ptr++;
	}
	if (*op_ptr == '\0') {
#if _DEBUG
		error("Could not find a leaf operator \"%s\" in \"%s\"",
		      leaf_op_chars, str);
#endif
		xfree(key);
		return NULL;
	}

	/* Get the operator from the string and a pointer to value */
	op = _str2op(op_ptr, leaf_op_chars, &val);

	if (op == OP_NONE) {
		/*
		 * The xstrchr check verified that an operator
		 * character exists, but not that the whole
		 * operator string is valid. For example, there
		 * could be repeating operator characters:
		 * xstrchr would return a pointer to the first
		 * one, but _str2op would correctly identify
		 * that the operator is invalid.
		 */
#if _DEBUG
		{
			char save_char;
			save_char = *val;
			*val = '\0';
			error("Invalid operator string: \"%s\"", op_ptr);
			*val = save_char;
		}
#endif
		xfree(key);
		return NULL;
	}

	/* NULL-terminate key */
	*op_ptr = '\0';

	/* Check for invalid characters in key and value: operators */
	if (!_valid_key_value(key) || !_valid_key_value(val)) {
#if _DEBUG
		error("Invalid key-op-value: %s", str);
#endif
		xfree(key);
		return NULL;
	}

	/* Build an element */
	leaf = xmalloc(sizeof(*leaf));
	leaf->operator = op;
	leaf->key = key; /* Already malloc'd */
	leaf->value = xstrdup(val);

#if _DEBUG
	_log_element(leaf);
#endif

	return leaf;
}

static char *_find_leaf_end(char *str)
{
	char *ptr = str;

	xassert(str);

	/* None of the following characters are allowed in a leaf */
	while (*ptr) {
		if (xstrchr(child_op_chars, *ptr) ||
		    (*ptr == '(') || (*ptr == ')'))
			break;
		ptr++;
	}
	return ptr;
}

/*
 * Parse a string like the following:
 *   (a=23&(b<=42|c=foo)&d>50)&e=bar
 *
 * Parentheses denote a level of the tree.
 * There are two kinds of operators: child and leaf operators. They are defined
 * in op_table.
 * Any particular level of the tree has only one child operator.
 * Leaves are:
 *   <key><leaf_op><value>
 *
 * Operators are not allowed in a key or value.
 *
 * The following should succeed:
 *
 * a=1
 * a=1,b=2
 * a=3&(b=asdf|c<24)
 * (a=1|(b>=2))
 * zed<yam,(a=23&(b<=42|c=foo)&d>50)&e=bar
 *
 * Spaces are allowed and are considered part of the string:
 * a=   b
 *
 *
 * The following should fail:
 *
 * Invalid leaf operator (',')
 * a,<=6
 *
 * Trailing operator:
 * a<=6<=
 *
 * Multiple child operators in a row:
 * a=5&&&b=5
 * a=5|||b=5
 *
 * Multiple leaf operators in a row:
 * a====5
 * b<=<=5
 *
 * Paren without anything inside
 * a=5&()
 *
 * Different operators at a single level
 * a=5&b=5|c=5
 * (a=1)&(b=2)|(c=3)
 *
 * No operator given:
 * a=1(b=2)
 * (a=1)(b=2)
 * (((a=1)b=2))
 */
static void _recurse(char **str_ptr, int *level, elem_t *parent, int *rc)
{
	elem_t *child;
	bool got_leaf = false;
	/* Implied operator before the first leaf on any level of the tree */
	bool got_op = true;

	xassert(str_ptr);
	xassert(level);
	xassert(parent);
	xassert(rc);

	while (*rc == SLURM_SUCCESS) {
		char save_char;
		char *next;

#if _DEBUG
		info("level=%d, string=\"%s\"", *level, *str_ptr);
#endif
		if (**str_ptr == '\0') {
			/* End of string. */
			if (got_op || !got_leaf) {
#if _DEBUG
				error("Did not end in a leaf. Invalid string");
#endif
				*rc = SLURM_ERROR;
			}
			break;
		}

		/*
		 * These first two checks go deeper or shallower in the tree.
		 * Make sure to update str_ptr.
		 *
		 * We can have multiple '(' or ')' in a row.
		 */
		if (**str_ptr == '(') {
			elem_t *child;

			if (!got_op) {
#if _DEBUG
				error("No boolean operator before leaf. Invalid string: \"%s\"",
				      *str_ptr);
#endif
				*rc = SLURM_ERROR;
				break;
			}

			/* Create a child for this new level and recurse */
			child = xmalloc(sizeof(*child));
			_add_child(parent, child);
			*level = *level + 1;
			(*str_ptr) = *str_ptr + 1;
			_recurse(str_ptr, level, child, rc);
			/*
			 * Returned after a ')' is found.
			 * Treat the contents within the parentheses as a leaf
			 * since we expect an operator to come after the
			 * closing paren.
			 */
			got_leaf = true;
			got_op = false;
			continue;
		} else if (**str_ptr == ')') {
			(*str_ptr) = *str_ptr + 1;
			if (*level) {
				*level = *level - 1;
			} else {
#if _DEBUG
				error("Unbalanced parentheses");
#endif
				*rc = SLURM_ERROR;
				/*
				 * Do not return because we were not recursively
				 * called to get here.
				 */
				break;
			}
			if (got_op || !got_leaf) {
#if _DEBUG
				error("Invalid expression inside parentheses, did not end in leaf");
#endif
				*rc = SLURM_ERROR;
			}

			return;
		}

		/* Check if we are at a child operator. */
		if (xstrchr(child_op_chars, **str_ptr)) {
			char *tmp_end = NULL;
			op_t op;

			if (!got_leaf) {
#if _DEBUG
				error("Got boolean operator without a leaf. Invalid string: \"%s\"",
				      *str_ptr);
#endif
				*rc = SLURM_ERROR;
				break;
			}

			op = _str2op(*str_ptr, child_op_chars, &tmp_end);

			if (op == OP_NONE) {
				/*
				 * The xstrchr check verified that an operator
				 * character exists, but not that the whole
				 * operator string is valid. For example, there
				 * could be repeating operator characters:
				 * xstrchr would return a pointer to the first
				 * one, but _str2op would correctly identify
				 * that the operator is invalid.
				 */
#if _DEBUG
				save_char = *tmp_end;
				*tmp_end = '\0';
				error("Invalid operator string: \"%s\"",
				      *str_ptr);
				*tmp_end = save_char;
#endif
				*rc = SLURM_ERROR;
				break;
			}

			/* All operators in a single level must be the same. */
			if ((parent->operator != OP_NONE) &&
			    (parent->operator != op)) {
#if _DEBUG
				error("Operators at a single level must be the same. Got \"%s\" but parent op is \"%s\"",
				      _op2str(op),
				      _op2str(parent->operator));
#endif
				*rc = SLURM_ERROR;
				break;
			} else {
				parent->operator = op;
			}
			*str_ptr = tmp_end;
			got_op = true;
			got_leaf = false;
			continue;
		}

		if (!got_op) {
#if _DEBUG
			error("No boolean operator before leaf. Invalid string: \"%s\"",
			      *str_ptr);
#endif
			*rc = SLURM_ERROR;
			break;
		}

		/*
		 * This is a leaf.
		 * NULL-terminate the leaf string and create the leaf at the
		 * the next paren or child operator, or end of string.
		 * Then continue parsing at the next paren or child operator.
		 */
		next = _find_leaf_end(*str_ptr);
		xassert(next);

		save_char = *next;
		*next = '\0';

		if (!(child = _parse_leaf(*str_ptr))) {
			xfree(child);
			*rc = SLURM_ERROR;
			break;
		} else
			_add_child(parent, child);

		*next = save_char;
		*str_ptr = next;
		got_op = false;
		got_leaf = true;
	}

	/*
	 * Underflow should not happen - we should be catching potential
	 * underflow conditions and return an error instead.
	 */
	xassert(*level >= 0);

	if (*level) {
		/* Unbalanced parentheses or parsing error */
#if _DEBUG
		if (*rc != SLURM_ERROR)
			error("Unbalanced parentheses");
#endif
		*rc = SLURM_ERROR;
	}
}

#define NUMBER_COMPARE(a,b,fuzzy,result) \
do {\
	if (fuzzy && fuzzy_equal(a, b))\
		result = CMP_EQ;\
	else if (!fuzzy && (a == b))\
		result = CMP_EQ;\
	else if (a < b)\
		result = CMP_LT;\
	else\
		result = CMP_GT;\
} while(0);


/*
 * Test if "data" equals, is less than, or is greater than "value"
 * data.c already has data_check_match(); however, that only checks for
 * equality and is stricter than we want to be here.
 */
static cmp_t _compare(data_t *data, char *value)
{
	cmp_t comparison;
	data_type_t data_type;
	data_t *value_d;

	xassert(value);
	xassert(data);

	value_d = data_new();
	if (!data_set_string(value_d, value)) {
		data_free(value_d);
#if _DEBUG
		error("%s: Couldn't convert %s to data_t", __func__, value);
#endif
		return CMP_INVALID;
	}
	data_type = data_get_type(data);

	switch (data_type) {
	case DATA_TYPE_INT_64:
	{
		/*
		 * We always do floating point comparison to be less strict on
		 * the user, and if the node data sometimes swaps between
		 * integer and floating point on node updates.
		 */
		double tmp1 = (double) data_get_int(data);
		double tmp2;

		if (data_convert_type(value_d, DATA_TYPE_FLOAT) !=
		    DATA_TYPE_FLOAT) {
			comparison = CMP_INVALID;
		} else {
			tmp2 = data_get_float(value_d);
			NUMBER_COMPARE(tmp1, tmp2, true, comparison);
		}
		break;
	}
	case DATA_TYPE_STRING:
		/*
		 * NOTE: strcmp is not guaranteed to return -1, 0, or 1. It
		 * is guaranteed to return a negative number, zero, or a
		 * positive number. Convert those to our CMP_* values.
		 */
		comparison = xstrcmp(data_get_string(data), value);
		if (comparison < 0)
			comparison = CMP_LT;
		else if (comparison > 0)
			comparison = CMP_GT;
		else
			comparison = CMP_EQ;
		break;
	case DATA_TYPE_FLOAT:
	{
		double tmp1 = data_get_float(data);
		double tmp2;

		if (data_convert_type(value_d, DATA_TYPE_FLOAT) !=
		    DATA_TYPE_FLOAT) {
			comparison = CMP_INVALID;
		} else {
			tmp2 = data_get_float(value_d);
			NUMBER_COMPARE(tmp1, tmp2, true, comparison);
		}
		break;
	}
	case DATA_TYPE_BOOL:
	{
		bool tmp1 = data_get_bool(data);
		bool tmp2;

		if (data_convert_type(value_d, DATA_TYPE_BOOL) !=
		    DATA_TYPE_BOOL) {
			comparison = CMP_INVALID;
		} else {
			tmp2 = data_get_bool(value_d);
			NUMBER_COMPARE(tmp1, tmp2, false, comparison);
		}
		break;
	}
	default:
		comparison = CMP_INVALID;
#if _DEBUG
		info("%s: Data type: %s is invalid",
		     __func__, data_type_to_string(data_type));
#endif
		break;
	}
	FREE_NULL_DATA(value_d);
	return comparison;
}

static bool _test(cmp_t comparison, op_t op)
{
	bool rc;

	if (op == OP_LEAF_EQ) {
		rc = (comparison == CMP_EQ);
	} else if (op == OP_LEAF_NE) {
		rc = (comparison != CMP_EQ);
	} else if (op == OP_LEAF_GT) {
		rc = (comparison == CMP_GT);
	} else if (op == OP_LEAF_GTE) {
		rc = (comparison >= CMP_EQ);
	} else if (op == OP_LEAF_LT) {
		rc = (comparison == CMP_LT);
	} else if (op == OP_LEAF_LTE) {
		rc = (comparison <= CMP_EQ);
	} else {
		error("%s: Undefined leaf operator %d", __func__, op);
		rc = false;
	}

	return rc;
}

/*
 * Test each leaf: the test is true if <data_value> <leaf_op> <leaf_value>
 * For each test, the key needs to exist in the data_t structure.
 */
static bool _test_extra_constraints(elem_t *parent, elem_t *el, data_t *data)
{
	bool test_result = false;

	if (!el)
		return false;
	if (!el->num_children) {
		/* leaf */
		data_t *data_ptr = NULL;
		cmp_t comparison = CMP_INVALID;
#if _DEBUG
		char *data_str = NULL;
#endif

		/* Check that key is in data_t */
		data_ptr = data_key_get(data, el->key);
		if (!data_ptr) {
#if _DEBUG
			info("%s: Key %s not found", __func__, el->key);
#endif
			return false;
		}
#if _DEBUG
		if (data_get_string_converted(data_ptr, &data_str) !=
		    SLURM_SUCCESS) {
			/* Couldn't convert to string. */
			data_str = xstrdup_printf("<Couldn't convert data to string>");
		}
#endif
		comparison = _compare(data_ptr, el->value);
		if (comparison == CMP_INVALID) {
#if _DEBUG
			info("%s: Invalid comparison: \"%s\" %s \"%s\"",
			     __func__, el->value, _op2str(el->operator),
			     data_str);
			xfree(data_str);
#endif
			return false;
		}
		test_result = _test(comparison, el->operator);
#if _DEBUG
		info("%s: Comparison result=%s: \"%s\" %s \"%s\"",
		     __func__, test_result ? "true" : "false",
		     data_str, _op2str(el->operator), el->value);
		xfree(data_str);
#endif

		return test_result;
	}

	xassert(el->children);
	for (int i = 0; i < el->num_children; i++) {
		test_result = _test_extra_constraints(el, el->children[i],
						      data);
		if (el->operator == OP_CHILD_OR) {
			/* OR: At least one child must pass. */
			if (test_result)
				break;
			else
				continue;
		} else {
			/*
			 * OP_CHILD_AND or OP_CHILD_NONE which is treated the
			 * same as OP_CHILD_AND.
			 * AND: All children must pass.
			 */
			if (test_result)
				continue;
			else
				break;
		}
	}
	return test_result;
}

/*
 * Parse a string into a tree
 */
extern int extra_constraints_parse(char *extra, elem_t **head)
{
	int rc = SLURM_SUCCESS;
	int level = 0;
	char *copy, *copy_start;
	elem_t *tree_head;

	xassert(head);

	if (!extra)
		return SLURM_SUCCESS;
	if (!extra_constraints_parsing)
		return SLURM_SUCCESS;

#if _DEBUG
	info("%s: parse %s", __func__, extra);
#endif

	copy = xstrdup(extra);
	tree_head = _alloc_tree();
	/*
	 * _recurse modifies the string pointer, so save a copy of the pointer
	 * to the beginning of the string so it can be free'd.
	 */
	copy_start = copy; /* Save a pointer to the beginning of the string */
	_recurse(&copy, &level, tree_head, &rc);
	if (rc != SLURM_SUCCESS) {
		error("%s: Parsing %s failed", __func__, extra);
		FREE_NULL_EXTRA_CONSTRAINTS(tree_head);
		rc = ESLURM_INVALID_EXTRA;
	} else {
		if (tree_head->operator == OP_NONE) {
			/*
			 * This should only happen if the request was
			 * structured such that the parent has only one child.
			 * In that case, set the operator to AND as the
			 * default.
			 */
			xassert(tree_head->num_children == 1);
			tree_head->operator = OP_CHILD_AND;
		}
#if _DEBUG
		info("%s: Succeeded parsing %s", __func__, extra);
#endif
	}

#if _DEBUG
	{
		char *str = extra_constraints_2str(tree_head);
		info("\n%s", str);
		xfree(str);
	}
#endif

	*head = tree_head;
	xfree(copy_start);
	return rc;
}

extern void extra_constraints_set_parsing(bool set)
{
	extra_constraints_parsing = set;
}

extern bool extra_constraints_test(elem_t *head, data_t *data)
{
	if (!extra_constraints_parsing)
		return true;
	if (!head)
		return true;
	if (!data) {
		return false;
	}

	return _test_extra_constraints(NULL, head, data);
}
