/*
 * Copyright (C) 2016 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
 */

/*
 * What we're doing here is saving all the possible values for static variables.
 * Later on we might do globals as well.
 *
 */

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

static int my_id;
static struct stree *vals;

static int save_rl(void *_rl, int argc, char **argv, char **azColName)
{
	unsigned long *rl = _rl;

	*rl = strtoul(argv[0], NULL, 10);
	return 0;
}

static struct range_list *select_orig(mtag_t tag, int offset)
{
	struct range_list *rl = NULL;

	mem_sql(&save_rl, &rl, "select value from mtag_data where tag = %lld and offset = %d;",
		tag, offset);
	return rl;
}

static int is_kernel_param(const char *name)
{
	struct sm_state *tmp;
	char buf[256];

	/*
	 * I'm ignoring these because otherwise Smatch thinks that kernel
	 * parameters are always set to the default.
	 *
	 */

	if (option_project != PROJ_KERNEL)
		return 0;

	snprintf(buf, sizeof(buf), "__param_%s.arg", name);

	FOR_EACH_SM(vals, tmp) {
		if (strcmp(tmp->name, buf) == 0)
			return 1;
	} END_FOR_EACH_SM(tmp);

	return 0;
}

static bool is_ignored_macro(struct expression *expr)
{
	char *macro;

	macro = get_macro_name(expr->pos);
	if (!macro)
		return false;
	if (strcmp(macro, "EXPORT_SYMBOL") == 0)
		return true;
	return false;
}

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

	/* Smatch thinks head->next == head is always true.  *sad face* */

	if (option_project != PROJ_KERNEL)
		return false;

	if (expr->type != EXPR_DEREF)
		return false;
	if (!expr->member || !expr->member->name ||
	    strcmp(expr->member->name, "next") != 0)
		return false;

	type = get_type(expr->deref);
	if (!type)
		return false;
	if (type->type == SYM_PTR)
		type = get_real_base_type(type);
	if (type->type != SYM_STRUCT)
		return false;
	if (!type->ident || !type->ident->name ||
	    strcmp(type->ident->name, "list_head") != 0)
		return false;
	return true;
}

mtag_t ignored_mtag;
static bool is_ignored_tag(mtag_t tag)
{
	if (tag == ignored_mtag)
		return true;
	return false;
}

static void insert_mtag_data(mtag_t tag, int offset, struct range_list *rl)
{
	if (in_fake_env)
		return;
	if (is_ignored_tag(tag))
		return;

	rl = clone_rl_permanent(rl);

	mem_sql(NULL, NULL, "delete from mtag_data where tag = %lld and offset = %d and type = %d",
		tag, offset, DATA_VALUE);
	mem_sql(NULL, NULL, "insert into mtag_data values (%lld, %d, %d, '%lu');",
		tag, offset, DATA_VALUE, (unsigned long)rl);
}

static bool invalid_type(struct symbol *type)
{
	if (!type)
		return true;
	if (type == &void_ctype)
		return true;
	if (type->type == SYM_STRUCT ||
	    type->type == SYM_ARRAY ||
	    type->type == SYM_UNION)
		return true;
	return false;
}

static bool parent_is_fresh_alloc(struct expression *expr)
{
	struct symbol *sym;

	sym = expr_to_sym(expr);
	if (!sym || !sym->ident)
		return false;
	return is_fresh_alloc_var_sym(sym->ident->name, sym);
}

void update_mtag_data(struct expression *expr, struct smatch_state *state)
{
	struct range_list *orig, *new;
	struct symbol *type;
	char *name;
	mtag_t tag;
	int offset;

	if (!expr)
		return;
	if (is_ignored_macro(expr))
		return;
	if (is_head_next(expr))
		return;
	name = expr_to_var(expr);
	if (is_kernel_param(name)) {
		free_string(name);
		return;
	}
	free_string(name);

	if (!expr_to_mtag_offset(expr, &tag, &offset))
		return;

	type = get_type(expr);
	if (offset == 0 && invalid_type(type))
		return;

	if (parent_is_fresh_alloc(expr))
		orig = NULL;
	else
		orig = select_orig(tag, offset);
	new = rl_union(orig, estate_rl(state));
	insert_mtag_data(tag, offset, new);
}

static void match_global_assign(struct expression *expr)
{
	struct range_list *rl;
	mtag_t tag;
	int offset;
	char *name;

	if (is_ignored_macro(expr))
		return;
	if (is_head_next(expr->left))
		return;
	name = expr_to_var(expr->left);
	if (is_kernel_param(name)) {
		free_string(name);
		return;
	}
	free_string(name);

	if (!expr_to_mtag_offset(expr->left, &tag, &offset))
		return;

	get_absolute_rl(expr->right, &rl);
	insert_mtag_data(tag, offset, rl);
}

static int save_mtag_data(void *_unused, int argc, char **argv, char **azColName)
{
	struct range_list *rl;

	if (argc != 4) {
		sm_msg("Error saving mtag data");
		return 0;
	}
	if (!option_info)
		return 0;

	rl = (struct range_list *)strtoul(argv[3], NULL, 10);
	sm_msg("SQL: insert or ignore into mtag_data values ('%s', '%s', '%s', '%s');",
	       argv[0], argv[1], argv[2], show_rl(rl));

	return 0;
}

static void match_end_file(struct symbol_list *sym_list)
{
	mem_sql(&save_mtag_data, NULL, "select * from mtag_data where type = %d;",
		DATA_VALUE);
}

struct db_info {
	struct symbol *type;
	struct range_list *rl;
};

static int get_vals(void *_db_info, int argc, char **argv, char **azColName)
{
	struct db_info *db_info = _db_info;
	struct range_list *tmp;

	str_to_rl(db_info->type, argv[0], &tmp);
	if (db_info->rl)
		db_info->rl = rl_union(db_info->rl, tmp);
	else
		db_info->rl = tmp;

	return 0;
}

struct db_cache_results {
	mtag_t tag;
	struct range_list *rl;
};
static struct db_cache_results cached_results[8];

static int get_rl_from_mtag_offset(mtag_t tag, int offset, struct symbol *type, struct range_list **rl)
{
	struct db_info db_info = {};
	mtag_t merged = tag | offset;
	struct range_list *mem_rl;
	static int idx;
	int i;

	*rl = NULL;

	for (i = 0; i < ARRAY_SIZE(cached_results); i++) {
		if (merged == cached_results[i].tag) {
			if (cached_results[i].rl) {
				*rl = cached_results[i].rl;
				return 1;
			}
			return 0;
		}
	}

	mem_rl = select_orig(tag, offset);
	if (is_whole_rl(mem_rl))
		goto update_cache;

	db_info.type = type;
	run_sql(get_vals, &db_info,
		"select value from mtag_data where tag = %lld and offset = %d and type = %d;",
		tag, offset, DATA_VALUE);
	if (!db_info.rl)
		goto update_cache;
	db_info.rl = rl_union(mem_rl, db_info.rl);
	if (is_whole_rl(db_info.rl))
		goto update_cache;

	*rl = db_info.rl;

update_cache:
	cached_results[idx].tag = merged;
	cached_results[idx].rl = *rl;
	idx = (idx + 1) % ARRAY_SIZE(cached_results);

	return !!*rl;
}

static void clear_cache(struct symbol *sym)
{
	memset(cached_results, 0, sizeof(cached_results));
}

int get_mtag_rl(struct expression *expr, struct range_list **rl)
{
	struct symbol *type;
	mtag_t tag;
	int offset;

	if (is_local_variable(expr))
		return 0;
	if (!expr_to_mtag_offset(expr, &tag, &offset))
		return 0;
	if (offset >= MTAG_OFFSET_MASK)
		return 0;

	type = get_type(expr);
	if (invalid_type(type))
		return 0;

	return get_rl_from_mtag_offset(tag, offset, type, rl);
}

void register_mtag_data(int id)
{
	my_id = id;

	ignored_mtag = str_to_mtag("extern boot_params");
	add_hook(&clear_cache, FUNC_DEF_HOOK);

	if (!option_info)
		return;
	add_hook(&match_global_assign, GLOBAL_ASSIGNMENT_HOOK);
	add_hook(&match_end_file, END_FILE_HOOK);
}

