/*
 * 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
 */

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

static int my_id;
static int param_id;

int get_param_from_container_of(struct expression *expr)
{
	struct expression *param_expr;
	struct symbol *type;
	sval_t sval;
	int param;


	type = get_type(expr);
	if (!type || type->type != SYM_PTR)
		return -1;

	expr = strip_expr(expr);
	if (expr->type != EXPR_BINOP || expr->op != '-')
		return -1;

	if (!get_value(expr->right, &sval))
		return -1;
	if (sval.value < 0 || sval.value > 4096)
		return -1;

	param_expr = get_assigned_expr(expr->left);
	if (!param_expr)
		return -1;
	param = get_param_num(param_expr);
	if (param < 0)
		return -1;

	return param;
}

int get_offset_from_container_of(struct expression *expr)
{
	struct expression *param_expr;
	struct symbol *type;
	sval_t sval;

	type = get_type(expr);
	if (!type || type->type != SYM_PTR)
		return -1;

	expr = strip_expr(expr);
	if (expr->type != EXPR_BINOP || expr->op != '-')
		return -1;

	if (!get_value(expr->right, &sval))
		return -1;
	if (sval.value < 0 || sval.value > 4096)
		return -1;

	param_expr = get_assigned_expr(expr->left);
	if (!param_expr)
		return -1;

	return sval.value;
}

static void print_returns_container_of(int return_id, char *return_ranges, struct expression *expr)
{
	int offset;
	int param;
	char key[64];
	char value[64];

	param = get_param_from_container_of(expr);
	if (param < 0)
		return;
	offset = get_offset_from_container_of(expr);
	if (offset < 0)
		return;

	snprintf(key, sizeof(key), "%d", param);
	snprintf(value, sizeof(value), "-%d", offset);

	/* no need to add it to return_implies because it's not really param_used */
	sql_insert_return_states(return_id, return_ranges, CONTAINER, -1,
			key, value);
}

static int get_deref_count(struct expression *expr)
{
	int cnt = 0;

	while (expr && expr->type == EXPR_DEREF) {
		expr = expr->deref;
		if (expr->type == EXPR_PREOP && expr->op == '*')
			expr = expr->unop;
		cnt++;
		if (cnt > 100)
			return -1;
	}
	return cnt;
}

static struct expression *get_partial_deref(struct expression *expr, int cnt)
{
	while (--cnt >= 0) {
		if (!expr || expr->type != EXPR_DEREF)
			return expr;
		expr = expr->deref;
		if (expr->type == EXPR_PREOP && expr->op == '*')
			expr = expr->unop;
	}
	return expr;
}

static int partial_deref_to_offset_str(struct expression *expr, int cnt, char op, char *buf, int size)
{
	int n, offset;

	if (cnt == 0)
		return snprintf(buf, size, "%c0", op);

	n = 0;
	while (--cnt >= 0) {
		offset = get_member_offset_from_deref(expr);
		if (offset < 0)
			return -1;
		n += snprintf(buf + n, size - n, "%c%d", op, offset);
		if (expr->type != EXPR_DEREF)
			return -1;
		expr = expr->deref;
		if (expr->type == EXPR_PREOP && expr->op == '*')
			expr = expr->unop;
	}

	return n;
}

static char *get_shared_str(struct expression *expr, struct expression *container)
{
	struct expression *one, *two;
	int exp, cont, min, ret, n;
	static char buf[48];

	exp = get_deref_count(expr);
	cont = get_deref_count(container);
	if (exp < 0 || cont < 0)
		return NULL;

	min = (exp < cont) ? exp : cont;
	while (min >= 0) {
		one = get_partial_deref(expr, exp - min);
		two = get_partial_deref(container, cont - min);
		if (expr_equiv(one, two))
			goto found;
		min--;
	}

	return NULL;

found:
	ret = partial_deref_to_offset_str(expr, exp - min, '-', buf, sizeof(buf));
	if (ret < 0)
		return NULL;
	n = ret;
	ret = partial_deref_to_offset_str(container, cont - min, '+', buf + ret, sizeof(buf) - ret);
	if (ret < 0)
		return NULL;
	n += ret;
	if (n >= sizeof(buf))
		return NULL;

	return buf;
}

static char *get_stored_container_name(struct expression *container,
				       struct expression *expr)
{
	struct smatch_state *state;
	static char buf[64];
	char *p;
	int param;

	if (!container || container->type != EXPR_SYMBOL)
		return NULL;
	if (!expr || expr->type != EXPR_SYMBOL)
		return NULL;
	state = get_state_expr(param_id, expr);
	if (!state)
		return NULL;

	snprintf(buf, sizeof(buf), "%s", state->name);
	p = strchr(buf, '|');
	if (!p)
		return NULL;
	*p = '\0';
	param = atoi(p + 2);
	if (get_param_sym_from_num(param) == container->symbol)
		return buf;
	return NULL;
}

static char *get_container_name_helper(struct expression *container, struct expression *expr)
{
	struct symbol *container_sym, *sym;
	static char buf[64];
	char *ret, *shared;
	bool star;

	expr = strip_expr(expr);
	container = strip_expr(container);
	if (!expr || !container)
		return NULL;

	ret = get_stored_container_name(container, expr);
	if (ret)
		return ret;

	sym = expr_to_sym(expr);
	container_sym = expr_to_sym(container);
	if (!sym || !container_sym)
		return NULL;
	if (sym != container_sym)
		return NULL;

	if (container->type == EXPR_DEREF)
		star = true;
	else
		star = false;

	if (container->type == EXPR_PREOP && container->op == '&')
		container = strip_expr(container->unop);
	if (expr->type == EXPR_PREOP && expr->op == '&')
		expr = strip_expr(expr->unop);

	shared = get_shared_str(expr, container);
	if (!shared)
		return NULL;
	if (star)
		snprintf(buf, sizeof(buf), "*(%s)", shared);
	else
		snprintf(buf, sizeof(buf), "%s", shared);

	return buf;
}

char *get_container_name(struct expression *container, struct expression *expr)
{
	char *ret;

	ret = get_container_name_helper(container, expr);
	if (ret)
		return ret;

	ret = get_container_name_helper(get_assigned_expr(container), expr);
	if (ret)
		return ret;

	ret = get_container_name_helper(container, get_assigned_expr(expr));
	if (ret)
		return ret;

	ret = get_container_name_helper(get_assigned_expr(container),
					get_assigned_expr(expr));
	if (ret)
		return ret;

	return NULL;
}

static bool is_fn_ptr_arg(struct expression *expr)
{
	struct symbol *type;

	if (!expr)
		return false;
	if (expr->type != EXPR_SYMBOL && expr->type != EXPR_DEREF)
		return false;

	type = get_type(expr);
	if (!type || type->type != SYM_PTR)
		return false;
	type = get_real_base_type(type);
	if (!type || type->type != SYM_FN)
		return false;
	return true;
}

static void match_call(struct expression *call)
{
	struct expression *fn, *arg, *tmp;
	bool found = false;
	int fn_param, param;
	char buf[32];
	char *name;

	/*
	 * We're trying to link the function with the parameter.  There are a
	 * couple ways this can be passed:
	 * foo->func(foo, ...);
	 * foo->func(foo->x, ...);
	 * foo->bar.func(&foo->bar, ...);
	 * foo->bar->baz->func(foo, ...);
	 *
	 * So the method is basically to subtract the offsets until we get to
	 * the common bit, then add the member offsets to get the parameter.
	 *
	 * If we're taking an address then the offset math is not stared,
	 * otherwise it is.  Starred means dereferenced.
	 */
	fn = strip_expr(call->fn);

	param = -1;
	FOR_EACH_PTR(call->args, arg) {
		param++;

		name = get_container_name(arg, fn);
		if (!name)
			continue;

		found = true;
		sql_insert_caller_info(call, CONTAINER, param, name, "$(-1)");
	} END_FOR_EACH_PTR(arg);

	if (found)
		return;

	fn_param = -1;
	FOR_EACH_PTR(call->args, arg) {
		fn_param++;
		if (!is_fn_ptr_arg(arg))
			continue;
		param = -1;
		FOR_EACH_PTR(call->args, tmp) {
			param++;

			/* the function isn't it's own container */
			if (arg == tmp)
				continue;

			name = get_container_name(tmp, arg);
			if (!name)
				continue;

			snprintf(buf, sizeof(buf), "$%d", param);
			sql_insert_caller_info(call, CONTAINER, fn_param, name, buf);
			return;
		} END_FOR_EACH_PTR(tmp);
	} END_FOR_EACH_PTR(arg);
}

static void db_passed_container(const char *name, struct symbol *sym, char *key, char *value)
{
	char buf[64];

	snprintf(buf, sizeof(buf), "%s|%s", key, value);
	set_state(param_id, name, sym, alloc_state_str(buf));
}

struct db_info {
	struct symbol *arg;
	int prev_offset;
	struct range_list *rl;
	int star;
	struct stree *stree;
};

static struct symbol *get_member_from_offset(struct symbol *sym, int offset)
{
	struct symbol *type, *tmp;
	int cur;

	type = get_real_base_type(sym);
	if (!type || type->type != SYM_PTR)
		return NULL;
	type = get_real_base_type(type);
	if (!type || type->type != SYM_STRUCT)
		return NULL;

	cur = 0;
	FOR_EACH_PTR(type->symbol_list, tmp) {
		cur = ALIGN(cur, tmp->ctype.alignment);
		if (offset == cur)
			return tmp;
		cur += type_bytes(tmp);
	} END_FOR_EACH_PTR(tmp);
	return NULL;
}

static struct symbol *get_member_type_from_offset(struct symbol *sym, int offset)
{
	struct symbol *base_type;
	struct symbol *member;

	base_type = get_real_base_type(sym);
	if (base_type && base_type->type == SYM_PTR)
		base_type = get_real_base_type(base_type);
	if (offset == 0 && base_type && base_type->type == SYM_BASETYPE)
		return base_type;

	member = get_member_from_offset(sym, offset);
	if (!member)
		return NULL;
	return get_real_base_type(member);
}

static const char *get_name_from_offset(struct symbol *arg, int offset)
{
	struct symbol *member, *type;
	const char *name;
	static char fullname[256];

	name = arg->ident->name;

	type = get_real_base_type(arg);
	if (!type || type->type != SYM_PTR)
		return name;

	type = get_real_base_type(type);
	if (!type)
		return NULL;
	if (type->type != SYM_STRUCT) {
		snprintf(fullname, sizeof(fullname), "*%s", name);
		return fullname;
	}

	member = get_member_from_offset(arg, offset);
	if (!member || !member->ident)
		return NULL;

	snprintf(fullname, sizeof(fullname), "%s->%s", name, member->ident->name);
	return fullname;
}

static void set_param_value(struct stree **stree, struct symbol *arg, int offset, struct range_list *rl)
{
	const char *name;

	name = get_name_from_offset(arg, offset);
	if (!name)
		return;
	set_state_stree(stree, SMATCH_EXTRA, name, arg, alloc_estate_rl(rl));
}

static int save_vals(void *_db_info, int argc, char **argv, char **azColName)
{
	struct db_info *db_info = _db_info;
	struct symbol *type;
	struct range_list *rl;
	int offset = 0;
	const char *value;

	if (argc == 2) {
		offset = atoi(argv[0]);
		value = argv[1];
	} else {
		value = argv[0];
	}

	if (db_info->prev_offset != -1 &&
	    db_info->prev_offset != offset) {
		set_param_value(&db_info->stree, db_info->arg, db_info->prev_offset, db_info->rl);
		db_info->rl = NULL;
	}

	db_info->prev_offset = offset;

	type = get_real_base_type(db_info->arg);
	if (db_info->star)
		goto found_type;
	if (type->type != SYM_PTR)
		return 0;
	type = get_real_base_type(type);
	if (type->type == SYM_BASETYPE)
		goto found_type;
	type = get_member_type_from_offset(db_info->arg, offset);
found_type:
	str_to_rl(type, (char *)value, &rl);
	if (db_info->rl)
		db_info->rl = rl_union(db_info->rl, rl);
	else
		db_info->rl = rl;

	return 0;
}

static struct stree *load_tag_info_sym(mtag_t tag, struct symbol *arg, int arg_offset, int star)
{
	struct db_info db_info = {
		.arg = arg,
		.prev_offset = -1,
		.star = star,
	};
	struct symbol *type;

	if (!tag || !arg->ident)
		return NULL;

	type = get_real_base_type(arg);
	if (!type)
		return NULL;
	if (!star) {
		if (type->type != SYM_PTR)
			return NULL;
		type = get_real_base_type(type);
		if (!type)
			return NULL;
	}

	if (star || type->type == SYM_BASETYPE) {
		run_sql(save_vals, &db_info,
			"select value from mtag_data where tag = %lld and offset = %d and type = %d;",
			tag, arg_offset, DATA_VALUE);
	} else {  /* presumably the parameter is a struct pointer */
		run_sql(save_vals, &db_info,
			"select offset, value from mtag_data where tag = %lld and type = %d order by offset;",
			tag, DATA_VALUE);
	}

	if (db_info.prev_offset != -1)
		set_param_value(&db_info.stree, arg, db_info.prev_offset, db_info.rl);

	// FIXME: handle an offset correctly
	if (!star && !arg_offset) {
		sval_t sval;

		sval.type = get_real_base_type(arg);
		sval.uvalue = tag;
		set_state_stree(&db_info.stree, SMATCH_EXTRA, arg->ident->name, arg, alloc_estate_sval(sval));
	}
	return db_info.stree;
}

static void load_container_data(struct symbol *arg, const char *info)
{
	mtag_t cur_tag, container_tag, arg_tag;
	int container_offset, arg_offset;
	struct sm_state *sm;
	struct stree *stree;
	char *p, *cont;
	char copy[64];
	bool star = 0;

	snprintf(copy, sizeof(copy), "%s", info);
	p = strchr(copy, '|');
	if (!p)
		return;
	*p = '\0';
	cont = p + 1;
	p = copy;
	if (p[0] == '*') {
		star = 1;
		p += 2;
	}

	if (strcmp(cont, "$(-1)") != 0)
		return;

	if (!get_toplevel_mtag(cur_func_sym, &cur_tag))
		return;

	while (true) {
		container_offset = strtoul(p, &p, 0);
		if (!mtag_map_select_container(cur_tag, -container_offset, &container_tag))
			return;
		cur_tag = container_tag;
		if (!p)
			return;
		if (p[0] != '-')
			break;
		p++;
	}

	if (p[0] != '+')
		return;

	p++;
	arg_offset = strtoul(p, &p, 0);
	if (p && *p && *p != ')')
		return;

	if (!arg_offset || star) {
		arg_tag = container_tag;
	} else {
		if (!mtag_map_select_tag(container_tag, arg_offset, &arg_tag))
			return;
	}

	stree = load_tag_info_sym(arg_tag, arg, arg_offset, star);
	FOR_EACH_SM(stree, sm) {
		set_state(sm->owner, sm->name, sm->sym, sm->state);
	} END_FOR_EACH_SM(sm);
	free_stree(&stree);
}

static void handle_passed_container(struct symbol *sym)
{
	struct symbol *arg;
	struct smatch_state *state;

	FOR_EACH_PTR(cur_func_sym->ctype.base_type->arguments, arg) {
		state = get_state(param_id, arg->ident->name, arg);
		if (!state || state == &merged)
			continue;
		load_container_data(arg, state->name);
	} END_FOR_EACH_PTR(arg);
}

void register_container_of(int id)
{
	my_id = id;

	add_split_return_callback(&print_returns_container_of);
	add_hook(&match_call, FUNCTION_CALL_HOOK);
}

void register_container_of2(int id)
{
	param_id = id;

	set_dynamic_states(param_id);
	select_caller_info_hook(db_passed_container, CONTAINER);
	add_merge_hook(param_id, &merge_str_state);
	add_hook(&handle_passed_container, AFTER_DEF_HOOK);
}

