#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include "int.h"
#include "types.h"
#include "crtools.h"
#include "parasite-syscall.h"
#include "proc_parse.h"
#include "ptrace.h"
#include "pstree.h"
#include "vma.h"
#include "log.h"
#include "util.h"
#include "kerndat.h"
#include <compel/compel.h>

struct syscall_exec_desc {
	char *name;
	unsigned nr;
};

#define SYSCALL(__name, __nr) { .name = #__name, .nr = __nr, },
#include "sys-exec-tbl.c"
#undef SYSCALL

#ifndef ARCH_HAS_FIND_SYSCALL
struct syscall_exec_desc *
find_syscall(char *name, struct parasite_ctl __always_unused *ctl)
{
	int i;

	for (i = 0; sc_exec_table[i].name != NULL; i++)
		if (!strcmp(sc_exec_table[i].name, name))
			return &sc_exec_table[i];

	return NULL;
}
#endif

#define MAX_ARGS	6

static int execute_syscall(struct parasite_ctl *ctl,
		struct syscall_exec_desc *scd, char **opt)
{
	int i, err;
	unsigned long args[MAX_ARGS] = {}, ret, r_mem_size = 0;
	unsigned int ret_args[MAX_ARGS] = {};
	void *r_mem = NULL;

	for (i = 0; i < MAX_ARGS; i++) {
		if (opt[i] == NULL)
			break;

		/*
		 * &foo -- argument string "foo"
		 * @<size> -- ret-arg of size <size>
		 */

		if ((opt[i][0] == '&') || (opt[i][0] == '@')) {
			int len;

			if (!r_mem) {
				err = compel_map_exchange(ctl, PAGE_SIZE);
				if (err)
					return err;

				r_mem_size = PAGE_SIZE;
				r_mem = ctl->local_map;
			}

			if (opt[i][0] == '&') {
				len = strlen(opt[i]);
				if (r_mem_size < len) {
					pr_err("Arg size overflow\n");
					return -1;
				}

				memcpy(r_mem, opt[i] + 1, len);
			} else {
				len = strtol(opt[i] + 1, NULL, 0);
				if (!len || (r_mem_size < len)) {
					pr_err("Bad argument size %d\n", len);
					return -1;
				}

				ret_args[i] = len;
			}

			args[i] = (unsigned long)ctl->remote_map + (r_mem - ctl->local_map);
			pr_info("Pushing %c mem arg [%s]\n", opt[i][0], (char *)r_mem);
			r_mem_size -= len;
			r_mem += len;
		} else
			args[i] = strtol(opt[i], NULL, 0);
	}

	pr_info("Calling %d with %lu %lu %lu %lu %lu %lu\n", scd->nr,
			args[0], args[1], args[2], args[3], args[4], args[5]);

	err = compel_syscall(ctl, scd->nr, &ret,
			args[0], args[1], args[2], args[3], args[4], args[5]);
	if (err)
		return err;

	pr_msg("Syscall returned %lx(%d)\n", ret, (int)ret);
	for (i = 0; i < MAX_ARGS; i++) {
		unsigned long addr;

		if (!ret_args[i])
			continue;

		pr_msg("Argument %d returns:\n", i);
		addr = (unsigned long)ctl->local_map + (args[i] - (unsigned long)ctl->remote_map);
		print_data(0, (unsigned char *)addr, ret_args[i]);
	}

	return 0;
}

int cr_exec(int pid, char **opt)
{
	char *sys_name = opt[0];
	struct syscall_exec_desc *si;
	struct parasite_ctl *ctl;
	struct vm_area_list vmas;
	int ret, prev_state, exit_code = -1;
	struct proc_status_creds creds;
	unsigned long p_start;

	if (!sys_name) {
		pr_err("Syscall name required\n");
		goto out;
	}

	if (kerndat_init_cr_exec()) {
		pr_err("Failed to init kerndat\n");
		goto out;
	}

	if (compel_interrupt_task(pid))
		goto out;

	/*
	 * We don't seize a task's threads here, so there is no reason to
	 * mess with creds in this use case anyway.
	 */

	prev_state = ret = compel_wait_task(pid, -1, parse_pid_status, &creds.s);
	if (ret < 0) {
		pr_err("Can't seize task %d\n", pid);
		goto out;
	}

	if (!is_alive_state(prev_state)) {
		pr_err("Only can exec on running/stopped tasks\n");
		goto out;
	}

	ret = collect_mappings(pid, &vmas, NULL);
	if (ret) {
		pr_err("Can't collect vmas for %d\n", pid);
		goto out_unseize;
	}

	p_start = get_exec_start(&vmas);
	if (!p_start) {
		pr_err("No suitable VM are found\n");
		goto out_unseize;
	}

	ctl = compel_prepare(pid);
	if (!ctl) {
		pr_err("Can't prep ctl %d\n", pid);
		goto out_unseize;
	}

	ctl->ictx.syscall_ip = p_start;

	si = find_syscall(sys_name, ctl);
	if (!si) {
		pr_err("Unknown syscall [%s]\n", sys_name);
		goto out_cure;
	}

	ret = execute_syscall(ctl, si, opt + 1);
	if (ret < 0) {
		pr_err("Can't execute syscall remotely\n");
		goto out_cure;
	}

	exit_code = 0;
out_cure:
	compel_cure(ctl);
out_unseize:
	compel_resume_task(pid, prev_state, prev_state);
out:
	return exit_code;
}
