/*
 * Copyright (C) 2017 Oracle.
 *
 * This program 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.
 *
 * This program 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 this program; if not, see http://www.gnu.org/copyleft/gpl.txt
 */

/*
 * Basically I see constraints as a way of saying "x <= some_limit".  The
 * problem is that smatch_capped is not granullar enough.
 *
 * This is mostly for finding out of bounds errors.  So there are different
 * types of constraints.  Quite often we have "foo->xxx[i] = 42;" and we want
 * to verify that "i" is less than foo->size.
 *
 * My idea was that we could automatically figure out these constraints.  And we
 * could load them in the DB so that they are the same every time.  As in a
 * constraint could be "< (struct whatever)->size" and give that in ID that
 * would be constant until you completely wiped the DB.  So when you do a normal
 * DB rebuild then the first thing it will do is preserve all the constraints.
 * I guess the reason to do it this way is to save space...  I sometimes suspect
 * that worrying about saving space is premature optimization.
 *
 * The other thing that I want to do a little bit different here is how I merge
 * constraints.  If a constraint is true on both sides, then that's normal.  If
 * we merge constraint 23 and 67 then we get constraint 23|67.  If we merge 23
 * with &undefined then we get &undefined.  We can also have two constraints
 * that are both true so we could have (45&23)|12 which means either both 45 and
 * 23 are true or 12 is true.
 *
 */


#include "smatch.h"
#include "smatch_extra.h"
#include "smatch_slist.h"

static int my_id;

ALLOCATOR(constraint, "constraints");

static void add_constraint(struct constraint_list **list, int op, int constraint)
{
	struct constraint *tmp, *new;

	FOR_EACH_PTR(*list, tmp) {
		if (tmp->id < constraint)
			continue;
		if (tmp->id == constraint) {
			if (tmp->op == '<')
				return;
			if (op == SPECIAL_LTE)
				return;

			new = __alloc_constraint(0);
			new->op = op;
			new->id = constraint;
			REPLACE_CURRENT_PTR(tmp, new);
			return;
		}

		new = __alloc_constraint(0);
		new->op = op;
		new->id = constraint;
		INSERT_CURRENT(new, tmp);
		return;
	} END_FOR_EACH_PTR(tmp);

	new = __alloc_constraint(0);
	new->op = op;
	new->id = constraint;
	add_ptr_list(list, new);
}

static struct constraint_list *merge_constraint_lists(struct constraint_list *one, struct constraint_list *two)
{
	struct constraint_list *ret = NULL;
	struct constraint *tmp;

	// FIXME: not || but &&
	FOR_EACH_PTR(one, tmp) {
		add_constraint(&ret, tmp->op, tmp->id);
	} END_FOR_EACH_PTR(tmp);

	FOR_EACH_PTR(two, tmp) {
		add_constraint(&ret, tmp->op, tmp->id);
	} END_FOR_EACH_PTR(tmp);

	return ret;
}

static struct constraint_list *clone_constraint_list(struct constraint_list *list)
{
	struct constraint_list *ret = NULL;
	struct constraint *tmp;

	FOR_EACH_PTR(list, tmp) {
		add_constraint(&ret, tmp->op, tmp->id);
	} END_FOR_EACH_PTR(tmp);

	return ret;
}

static struct smatch_state *alloc_constraint_state(struct constraint_list *list)
{
	struct smatch_state *state;
	struct constraint *con;
	static char buf[256];
	int cnt = 0;

	FOR_EACH_PTR(list, con) {
		if (cnt != 0)
			cnt += snprintf(buf + cnt, sizeof(buf) - cnt, ", ");
		cnt += snprintf(buf + cnt, sizeof(buf) - cnt, "%s%d",
				show_special(con->op), con->id);
	} END_FOR_EACH_PTR(con);

	state = __alloc_smatch_state(0);
	state->name = alloc_string(buf);
	state->data = list;
	return state;
}

static struct smatch_state *merge_func(struct smatch_state *s1, struct smatch_state *s2)
{
	struct constraint_list *list;

	// FIXME:  use the dead code below instead
	if (strcmp(s1->name, s2->name) == 0)
		return s1;
	return &merged;

	list = merge_constraint_lists(s1->data, s2->data);
	return alloc_constraint_state(list);
}

static int negate_gt(int op)
{
	switch (op) {
	case '>':
	case SPECIAL_UNSIGNED_GT:
	case SPECIAL_GTE:
	case SPECIAL_UNSIGNED_GTE:
		return negate_comparison(op);
	}
	return op;
}

static char *get_func_constraint(struct expression *expr)
{
	char buf[256];
	char *name;

	if (is_fake_call(expr))
		return NULL;
	name = expr_to_str(expr->fn);
	if (!name)
		return NULL;
	snprintf(buf, sizeof(buf), "%s()", name);
	free_string(name);
	return alloc_string(buf);
}

static char *get_toplevel_name(struct expression *expr)
{
	struct symbol *sym;
	char buf[256];

	expr = strip_expr(expr);
	if (expr->type != EXPR_SYMBOL || !expr->symbol || !expr->symbol->ident)
		return NULL;

	sym = expr->symbol;
	if (!(sym->ctype.modifiers & MOD_TOPLEVEL))
		return NULL;

	if (sym->ctype.modifiers & MOD_STATIC)
		snprintf(buf, sizeof(buf), "%s %s", get_base_file(), sym->ident->name);
	else
		snprintf(buf, sizeof(buf), "extern %s", sym->ident->name);

	return alloc_string(buf);
}

char *get_constraint_str(struct expression *expr)
{
	char *name;

	expr = strip_expr(expr);
	if (!expr)
		return NULL;
	if (expr->type == EXPR_CALL)
		return get_func_constraint(expr);
	if (expr->type == EXPR_BINOP)
		return expr_to_str(expr);
	name = get_toplevel_name(expr);
	if (name)
		return name;
	return get_member_name(expr);
}

static int save_int_callback(void *_p, int argc, char **argv, char **azColName)
{
	int *p = _p;

	*p = atoi(argv[0]);
	return 0;
}

static int constraint_str_to_id(const char *str)
{
	int id = -1;

	run_sql(save_int_callback, &id,
		"select id from constraints where str = '%q'", str);

	return id;
}

static int save_constraint_str(void *_str, int argc, char **argv, char **azColName)
{
	char **str = _str;

	*str = alloc_string(argv[0]);
	return 0;
}

static char *constraint_id_to_str(int id)
{
	char *str = NULL;

	run_sql(save_constraint_str, &str,
		"select str from constraints where id = '%d'", id);

	return str;
}

static int save_op_callback(void *_p, int argc, char **argv, char **azColName)
{
	int *p = _p;

	if (argv[0][0] == '<' && argv[0][1] == '=')
		*p = SPECIAL_LTE;
	else
		*p = '<';
	return 0;
}

static int save_str_callback(void *_p, int argc, char **argv, char **azColName)
{
	char **p = _p;

	if (!*p) {
		*p = alloc_string(argv[0]);
	} else {
		char buf[256];

		snprintf(buf, sizeof(buf), "%s, %s", *p, argv[0]);
		*p = alloc_string(buf);
	}
	return 0;
}

char *get_required_constraint(const char *data_str)
{
	char *required = NULL;

	run_sql(save_str_callback, &required,
		"select bound from constraints_required where data = '%q'", data_str);

	return required;
}

static int get_required_op(char *data_str, char *con_str)
{
	int op = 0;

	run_sql(save_op_callback, &op,
		"select op from constraints_required where data = '%q' and bound = '%q'", data_str, con_str);

	return op;
}

char *unmet_constraint(struct expression *data, struct expression *offset)
{
	struct smatch_state *state;
	struct constraint_list *list;
	struct constraint *con;
	char *data_str;
	char *required;
	int req_op;

	data_str = get_constraint_str(data);
	if (!data_str)
		return NULL;

	required = get_required_constraint(data_str);
	if (!required)
		goto free_data;

	state = get_state_expr(my_id, offset);
	if (!state)
		goto free_data;
	list = state->data;

	/* check the list of bounds on our index against the list that work */
	FOR_EACH_PTR(list, con) {
		char *con_str;

		con_str = constraint_id_to_str(con->id);
		if (!con_str) {
			sm_msg("constraint %d not found", con->id);
			continue;
		}

		req_op = get_required_op(data_str, con_str);
		free_string(con_str);
		if (!req_op)
			continue;
		if (con->op == '<' || con->op == req_op) {
			free_string(required);
			required = NULL;
			goto free_data;
		}
	} END_FOR_EACH_PTR(con);

free_data:
	free_string(data_str);
	return required;
}

struct string_list *saved_constraints;
static void save_new_constraint(const char *con)
{
	if (!insert_string(&saved_constraints, con))
		return;
	sql_save_constraint(con);
}

static void handle_comparison(struct expression *left, int op, struct expression *right)
{
	struct constraint_list *constraints;
	struct smatch_state *state;
	char *constraint;
	int constraint_id;
	int orig_op = op;
	sval_t sval;

	/* known values are handled in smatch extra */
	if (get_value(left, &sval) || get_value(right, &sval))
		return;

	constraint = get_constraint_str(right);
	if (!constraint)
		return;
	constraint_id = constraint_str_to_id(constraint);
	if (constraint_id < 0)
		save_new_constraint(constraint);
	free_string(constraint);
	if (constraint_id < 0)
		return;

	constraints = get_constraints(left);
	constraints = clone_constraint_list(constraints);
	op = negate_gt(orig_op);
	add_constraint(&constraints, remove_unsigned_from_comparison(op), constraint_id);
	state = alloc_constraint_state(constraints);

	if (op == orig_op)
		set_true_false_states_expr(my_id, left,	state, NULL);
	else
		set_true_false_states_expr(my_id, left, NULL, state);
}

static void match_condition(struct expression *expr)
{
	if (expr->type != EXPR_COMPARE)
		return;

	if (expr->op == SPECIAL_EQUAL ||
	    expr->op == SPECIAL_NOTEQUAL)
		return;

	handle_comparison(expr->left, expr->op, expr->right);
	handle_comparison(expr->right, flip_comparison(expr->op), expr->left);
}

struct constraint_list *get_constraints(struct expression *expr)
{
	struct smatch_state *state;

	state = get_state_expr(my_id, expr);
	if (!state)
		return NULL;
	return state->data;
}

static void match_caller_info(struct expression *expr)
{
	struct expression *tmp;
	struct smatch_state *state;
	int i;

	i = -1;
	FOR_EACH_PTR(expr->args, tmp) {
		i++;
		state = get_state_expr(my_id, tmp);
		if (!state || state == &merged || state == &undefined)
			continue;
		sql_insert_caller_info(expr, CONSTRAINT, i, "$", state->name);
	} END_FOR_EACH_PTR(tmp);
}

static void struct_member_callback(struct expression *call, int param, char *printed_name, struct sm_state *sm)
{
	if (sm->state == &merged || sm->state == &undefined)
		return;
	sql_insert_caller_info(call, CONSTRAINT, param, printed_name, sm->state->name);
}

static struct smatch_state *constraint_str_to_state(char *value)
{
	struct constraint_list *list = NULL;
	char *p = value;
	int op;
	long long id;

	while (true) {
		op = '<';
		if (*p != '<')
			return &undefined;
		p++;
		if (*p == '=') {
			op = SPECIAL_LTE;
			p++;
		}
		id = strtoll(p, &p, 10);
		add_constraint(&list, op, id);
		if (*p != ',')
			break;
		p++;
		if (*p != ' ')
			return &undefined;
	}

	return alloc_constraint_state(list);
}

static void set_param_constrained(const char *name, struct symbol *sym, char *key, char *value)
{
	char fullname[256];

	if (strcmp(key, "*$") == 0)
		snprintf(fullname, sizeof(fullname), "*%s", name);
	else if (strncmp(key, "$", 1) == 0)
		snprintf(fullname, 256, "%s%s", name, key + 1);
	else
		return;

	set_state(my_id, name, sym, constraint_str_to_state(value));
}

static void print_return_implies_constrained(int return_id, char *return_ranges, struct expression *expr)
{
	struct smatch_state *orig;
	struct sm_state *sm;
	const char *param_name;
	int param;

	FOR_EACH_MY_SM(my_id, __get_cur_stree(), sm) {
		if (sm->state == &merged || sm->state == &undefined)
			continue;

		param = get_param_num_from_sym(sm->sym);
		if (param < 0)
			continue;

		orig = get_state_stree(get_start_states(), my_id, sm->name, sm->sym);
		if (orig && strcmp(sm->state->name, orig->name) == 0)
			continue;

		param_name = get_param_name(sm);
		if (!param_name)
			continue;

		sql_insert_return_states(return_id, return_ranges, CONSTRAINT,
					 param, param_name, sm->state->name);
	} END_FOR_EACH_SM(sm);
}

static void db_returns_constrained(struct expression *expr, int param, char *key, char *value)
{
	char *name;
	struct symbol *sym;

	name = get_name_sym_from_param_key(expr, param, key, &sym);
	if (!name || !sym)
		goto free;

	set_state(my_id, name, sym, constraint_str_to_state(value));
free:
	free_string(name);
}

void register_constraints(int id)
{
	my_id = id;

	set_dynamic_states(my_id);
	add_merge_hook(my_id, &merge_func);
	add_hook(&match_condition, CONDITION_HOOK);

	add_hook(&match_caller_info, FUNCTION_CALL_HOOK);
	add_member_info_callback(my_id, struct_member_callback);
	select_caller_info_hook(&set_param_constrained, CONSTRAINT);

	add_split_return_callback(print_return_implies_constrained);
	select_return_states_hook(CONSTRAINT, &db_returns_constrained);
}
