/*
 * Copyright (C) 2014 Red Hat Inc, Steven Rostedt <srostedt@redhat.com>
 *
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * 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; version 2 of the License (not later!)
 *
 * 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/licenses>
 *
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 */
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <stdio.h>
#include <dirent.h>
#include <getopt.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>

#include "trace-local.h"

#ifndef BUFSIZ
#define BUFSIZ 1024
#endif

static inline int is_top_instance(struct buffer_instance *instance)
{
	return instance == &top_instance;
}

static int get_instance_file_fd(struct buffer_instance *instance,
				const char *file)
{
	char *path;
	int fd;

	path = get_instance_file(instance, file);
	fd = open(path, O_RDONLY);
	tracecmd_put_tracing_file(path);

	return fd;
}

char *strstrip(char *str)
{
	char *s;

	if (!str)
		return NULL;

	s = str + strlen(str) - 1;
	while (s >= str && isspace(*s))
		s--;
	s++;
	*s = '\0';

	for (s = str; *s && isspace(*s); s++)
		;

	return s;
}

char *append_file(const char *dir, const char *name)
{
	char *file;

	file = malloc_or_die(strlen(dir) + strlen(name) + 2);
	if (!file)
		die("malloc");

	sprintf(file, "%s/%s", dir, name);
	return file;
}

static char *get_fd_content(int fd, const char *file)
{
	char *str = NULL;
	int cnt = 0;
	int ret;

	for (;;) {
		str = realloc(str, BUFSIZ * ++cnt);
		if (!str)
			die("malloc");
		ret = read(fd, str + BUFSIZ * (cnt - 1), BUFSIZ);
		if (ret < 0)
			die("reading %s\n", file);
		if (ret < BUFSIZ)
			break;
	}
	str[BUFSIZ * (cnt-1) + ret] = 0;

	return str;
}

char *get_file_content(const char *file)
{
	char *str;
	int fd;

	fd = open(file, O_RDONLY);
	if (fd < 0)
		return NULL;

	str = get_fd_content(fd, file);
	close(fd);

	return str;
}

static char *get_instance_file_content(struct buffer_instance *instance,
				       const char *file)
{
	char *str = NULL;
	int fd;

	fd = get_instance_file_fd(instance, file);
	if (fd < 0)
		return NULL;

	str = get_fd_content(fd, file);

	close(fd);
	return str;
}

static void report_plugin(struct buffer_instance *instance)
{
	char *str;
	char *cont;

	str = get_instance_file_content(instance, "current_tracer");
	if (!str)
		return;

	cont = strstrip(str);

	/* We only care if the plugin is something other than nop */
	if (strcmp(cont, "nop") == 0)
		goto out;

	printf("\nTracer: %s\n", cont);
 out:
	free(str);
}

struct event_iter *trace_event_iter_alloc(const char *path)
{
	struct event_iter *iter;

	iter = malloc_or_die(sizeof(*iter));
	memset(iter, 0, sizeof(*iter));

	iter->system_dir = opendir(path);
	if (!iter->system_dir)
		die("opendir");

	return iter;
}

enum event_iter_type
trace_event_iter_next(struct event_iter *iter, const char *path, const char *system)
{
	struct dirent *dent;

	if (system && !iter->event_dir) {
		char *event;
		struct stat st;

		event = append_file(path, system);

		stat(event, &st);
		if (!S_ISDIR(st.st_mode)) {
			free(event);
			goto do_system;
		}

		iter->event_dir = opendir(event);
		if (!iter->event_dir)
			die("opendir %s", event);
		free(event);
	}

	if (iter->event_dir) {
		while ((dent = readdir(iter->event_dir))) {
			const char *name = dent->d_name;

			if (strcmp(name, ".") == 0 ||
			    strcmp(name, "..") == 0)
				continue;

			iter->event_dent = dent;
			return EVENT_ITER_EVENT;
		}
		closedir(iter->event_dir);
		iter->event_dir = NULL;
	}

 do_system:
	while ((dent = readdir(iter->system_dir))) {
		const char *name = dent->d_name;

		if (strcmp(name, ".") == 0 ||
		    strcmp(name, "..") == 0)
			continue;

		iter->system_dent = dent;

		return EVENT_ITER_SYSTEM;
	}

	return EVENT_ITER_NONE;
}

void trace_event_iter_free(struct event_iter *iter)
{
	if (!iter)
		return;

	if (iter->event_dir)
		closedir(iter->event_dir);

	closedir(iter->system_dir);
	free(iter);
}

static void reset_event_iter(struct event_iter *iter)
{
	if (iter->event_dir) {
		closedir(iter->event_dir);
		iter->event_dir = NULL;
	}

	rewinddir(iter->system_dir);
}

static int process_individual_events(const char *path, struct event_iter *iter)
{
	struct stat st;
	const char *system = iter->system_dent->d_name;
	char *file;
	char *enable = NULL;
	char *str;
	int ret = 0;

	file = append_file(path, system);

	stat(file, &st);
	if (!S_ISDIR(st.st_mode))
		goto out;

	enable = append_file(file, "enable");
	str = get_file_content(enable);
	if (!str)
		goto out;

	if (*str != '1' && *str != '0')
		ret = 1;
	free(str);

 out:
	free(enable);
	free(file);

	return ret;
}

static void
process_event_enable(char *path, const char *system, const char *name,
		     enum event_process *processed)
{
	struct stat st;
	char *enable = NULL;
	char *file;
	char *str;

	if (system)
		path = append_file(path, system);

	file = append_file(path, name);

	if (system)
		free(path);

	stat(file, &st);
	if (!S_ISDIR(st.st_mode))
		goto out;

	enable = append_file(file, "enable");
	str = get_file_content(enable);
	if (!str)
		goto out;

	if (*str == '1') {
		if (!system) {
			if (!*processed)
				printf(" Individual systems:\n");
			printf( "   %s\n", name);
			*processed = PROCESSED_SYSTEM;
		} else {
			if (!*processed) {
				printf(" Individual events:\n");
				*processed = PROCESSED_SYSTEM;
			}
			if (*processed == PROCESSED_SYSTEM) {
				printf("    %s\n", system);
				*processed = PROCESSED_EVENT;
			}
			printf( "        %s\n", name);
		}
	}
	free(str);

 out:
	free(enable);
	free(file);
}

static void report_events(struct buffer_instance *instance)
{
	struct event_iter *iter;
	char *str;
	char *cont;
	char *path;
	char *system;
	enum event_iter_type type;
	enum event_process processed = PROCESSED_NONE;

	str = get_instance_file_content(instance, "events/enable");
	if (!str)
		return;

	cont = strstrip(str);

	printf("\nEvents:\n");

	switch(*cont) {
	case '1':
		printf(" All enabled\n");
		free(str);
		return;
	case '0':
		printf(" All disabled\n");
		free(str);
		return;
	}

	free(str);

	path = get_instance_file(instance, "events");
	if (!path)
		die("malloc");

	iter = trace_event_iter_alloc(path);

	while (trace_event_iter_next(iter, path, NULL)) {
		process_event_enable(path, NULL, iter->system_dent->d_name, &processed);
	}

	reset_event_iter(iter);

	processed = PROCESSED_NONE;
	system = NULL;
	while ((type = trace_event_iter_next(iter, path, system))) {

		if (type == EVENT_ITER_SYSTEM) {

			/* Only process systems that are not fully enabled */
			if (!process_individual_events(path, iter))
				continue;

			system = iter->system_dent->d_name;
			if (processed)
				processed = PROCESSED_SYSTEM;
			continue;
		}

		process_event_enable(path, iter->system_dent->d_name,
				     iter->event_dent->d_name, &processed);
	}

	trace_event_iter_free(iter);

	if (!processed)
		printf("  (none enabled)\n");

	tracecmd_put_tracing_file(path);
}

static void
process_event_filter(char *path, struct event_iter *iter, enum event_process *processed)
{
	const char *system = iter->system_dent->d_name;
	const char *event = iter->event_dent->d_name;
	struct stat st;
	char *filter = NULL;
	char *file;
	char *str;
	char *cont;

	path = append_file(path, system);
	file = append_file(path, event);
	free(path);

	stat(file, &st);
	if (!S_ISDIR(st.st_mode))
		goto out;

	filter = append_file(file, "filter");
	str = get_file_content(filter);
	if (!str)
		goto out;

	cont = strstrip(str);

	if (strcmp(cont, "none") == 0) {
		free(str);
		goto out;
	}

	if (!*processed)
		printf("\nFilters:\n");
	printf( "  %s:%s \"%s\"\n", system, event, cont);
	*processed = PROCESSED_SYSTEM;
	free(str);

 out:
	free(filter);
	free(file);
}

static void report_event_filters(struct buffer_instance *instance)
{
	struct event_iter *iter;
	char *path;
	char *system;
	enum event_iter_type type;
	enum event_process processed = PROCESSED_NONE;

	path = get_instance_file(instance, "events");
	if (!path)
		die("malloc");

	iter = trace_event_iter_alloc(path);

	processed = PROCESSED_NONE;
	system = NULL;
	while ((type = trace_event_iter_next(iter, path, system))) {

		if (type == EVENT_ITER_SYSTEM) {
			system = iter->system_dent->d_name;
			continue;
		}

		process_event_filter(path, iter, &processed);
	}

	trace_event_iter_free(iter);

	tracecmd_put_tracing_file(path);
}

static void
process_event_trigger(char *path, struct event_iter *iter, enum event_process *processed)
{
	const char *system = iter->system_dent->d_name;
	const char *event = iter->event_dent->d_name;
	struct stat st;
	char *trigger = NULL;
	char *file;
	char *str;
	char *cont;

	path = append_file(path, system);
	file = append_file(path, event);
	free(path);

	stat(file, &st);
	if (!S_ISDIR(st.st_mode))
		goto out;

	trigger = append_file(file, "trigger");
	str = get_file_content(trigger);
	if (!str)
		goto out;

	cont = strstrip(str);

	if (cont[0] == '#') {
		free(str);
		goto out;
	}

	if (!*processed)
		printf("\nTriggers:\n");
	printf( "  %s:%s \"%s\"\n", system, event, cont);
	*processed = PROCESSED_SYSTEM;
	free(str);

 out:
	free(trigger);
	free(file);
}

static void report_event_triggers(struct buffer_instance *instance)
{
	struct event_iter *iter;
	char *path;
	char *system;
	enum event_iter_type type;
	enum event_process processed = PROCESSED_NONE;

	path = get_instance_file(instance, "events");
	if (!path)
		die("malloc");

	iter = trace_event_iter_alloc(path);

	processed = PROCESSED_NONE;
	system = NULL;
	while ((type = trace_event_iter_next(iter, path, system))) {

		if (type == EVENT_ITER_SYSTEM) {
			system = iter->system_dent->d_name;
			continue;
		}

		process_event_trigger(path, iter, &processed);
	}

	trace_event_iter_free(iter);

	tracecmd_put_tracing_file(path);
}

enum func_states {
	FUNC_STATE_START,
	FUNC_STATE_SKIP,
	FUNC_STATE_PRINT,
};

static void list_functions(const char *path, char *string)
{
	enum func_states state;
	struct stat st;
	char *str;
	int ret = 0;
	int len;
	int i;
	int first = 0;

	/* Ignore if it does not exist. */
	ret = stat(path, &st);
	if (ret < 0)
		return;

	str = get_file_content(path);
	if (!str)
		return;

	len = strlen(str);

	state = FUNC_STATE_START;

	/* Skip all lines that start with '#' */
	for (i = 0; i < len; i++) {

		if (state == FUNC_STATE_PRINT)
			putchar(str[i]);

		if (str[i] == '\n') {
			state = FUNC_STATE_START;
			continue;
		}

		if (state == FUNC_STATE_SKIP)
			continue;

		if (state == FUNC_STATE_START && str[i] == '#') {
			state = FUNC_STATE_SKIP;
			continue;
		}

		if (!first) {
			printf("\n%s:\n", string);
			first = 1;
		}

		if (state != FUNC_STATE_PRINT) {
			state = FUNC_STATE_PRINT;
			printf("   ");
			putchar(str[i]);
		}
	}
	free(str);
}

static void report_graph_funcs(struct buffer_instance *instance)
{
	char *path;

	path = get_instance_file(instance, "set_graph_function");
	if (!path)
		die("malloc");

	list_functions(path, "Function Graph Filter");
	
	tracecmd_put_tracing_file(path);

	path = get_instance_file(instance, "set_graph_notrace");
	if (!path)
		die("malloc");

	list_functions(path, "Function Graph No Trace");
	
	tracecmd_put_tracing_file(path);
}

static void report_ftrace_filters(struct buffer_instance *instance)
{
	char *path;

	path = get_instance_file(instance, "set_ftrace_filter");
	if (!path)
		die("malloc");

	list_functions(path, "Function Filter");
	
	tracecmd_put_tracing_file(path);

	path = get_instance_file(instance, "set_ftrace_notrace");
	if (!path)
		die("malloc");

	list_functions(path, "Function No Trace");
	
	tracecmd_put_tracing_file(path);
}

static void report_buffers(struct buffer_instance *instance)
{
#define FILE_SIZE 100
	char *str;
	char *cont;
	char file[FILE_SIZE];
	int cpu;

	str = get_instance_file_content(instance, "buffer_size_kb");
	if (!str)
		return;

	cont = strstrip(str);

	/* If it's not expanded yet, just skip */
	if (strstr(cont, "expanded") != NULL)
		goto out;

	if (strcmp(cont, "X") != 0) {
		printf("\nBuffer size in kilobytes (per cpu):\n");
		printf("   %s\n", str);
		goto total;
	}

	/* Read the sizes of each CPU buffer */
	for (cpu = 0; ; cpu++) {

		snprintf(file, FILE_SIZE, "per_cpu/cpu%d/buffer_size_kb", cpu);
		str = get_instance_file_content(instance, file);
		if (!str)
			break;

		cont = strstrip(str);
		if (!cpu)
			putchar('\n');

		printf("CPU %d buffer size (kb): %s\n", cpu, cont);
		free(str);
	}

 total:
	free(str);

	str = get_instance_file_content(instance, "buffer_total_size_kb");
	if (!str)
		return;

	cont = strstrip(str);
	printf("\nBuffer total size in kilobytes:\n");
	printf("   %s\n", str);

 out:
	free(str);
}

static void report_clock(struct buffer_instance *instance)
{
	char *str;
	char *cont;
	char *clock;

	str = get_instance_file_content(instance, "trace_clock");
	if (!str)
		return;

	clock = strstr(str, "[");
	if (!clock)
		goto out;
	clock++;

	cont = strstr(clock, "]");
	if (!cont) /* should never happen */
		goto out;

	*cont = '\0';

	/* Default clock is "local", only show others */
	if (strcmp(clock, "local") == 0)
		goto out;

	printf("\nClock: %s\n", clock);

 out:
	free(str);
}

static void report_cpumask(struct buffer_instance *instance)
{
	char *str;
	char *cont;
	int cpus;
	int n;
	int i;

	str = get_instance_file_content(instance, "tracing_cpumask");
	if (!str)
		return;

	cont = strstrip(str);

	/* check to make sure all CPUs on this machine are set */
	cpus = count_cpus();

	for (i = strlen(cont) - 1; i >= 0 && cpus > 0; i--) {
		if (cont[i] == ',')
			continue;

		if (cont[i] == 'f') {
			cpus -= 4;
			continue;
		}

		if (cpus >= 4)
			break;

		if (cont[i] >= '0' && cont[i] <= '9')
			n = cont[i] - '0';
		else
			n = 10 + (cont[i] - 'a');

		while (cpus > 0) {
			if (!(n & 1))
				break;
			n >>= 1;
			cpus--;
		}
		break;
	}

	/* If cpus is greater than zero, one isn't set */
	if (cpus > 0)
		printf("\nCPU mask: %s\n", cont);

	free(str);
}

static void report_latency(struct buffer_instance *instance)
{
	char *str;
	char *cont;

	str = get_instance_file_content(instance, "tracing_max_latency");
	if (!str)
		return;

	cont = strstrip(str);

	if (strcmp(cont, "0") != 0)
		printf("\nMax Latency: %s\n", cont);

	free(str);
}

static void report_probes(struct buffer_instance *instance,
			  const char *file, const char *string)
{
	char *str;
	char *cont;
	int newline;
	int i;

	str = get_instance_file_content(instance, file);
	if (!str)
		return;

	cont = strstrip(str);
	if (strlen(cont) == 0)
		goto out;

	printf("\n%s:\n", string);

	newline = 1;
	for (i = 0; cont[i]; i++) {
		if (newline)
			printf("   ");
		putchar(cont[i]);
		if (cont[i] == '\n')
			newline = 1;
		else
			newline = 0;
	}
	putchar('\n');
 out:
	free(str);
}

static void report_kprobes(struct buffer_instance *instance)
{
	report_probes(instance, "kprobe_events", "Kprobe events");
}

static void report_uprobes(struct buffer_instance *instance)
{
	report_probes(instance, "uprobe_events", "Uprobe events");
}

static void report_traceon(struct buffer_instance *instance)
{
	char *str;
	char *cont;

	str = get_instance_file_content(instance, "tracing_on");
	if (!str)
		return;

	cont = strstrip(str);

	/* double newline as this is the last thing printed */
	if (strcmp(cont, "0") == 0)
		printf("\nTracing is disabled\n\n");
	else
		printf("\nTracing is enabled\n\n");

	free(str);
}

static void stat_instance(struct buffer_instance *instance)
{
	if (instance != &top_instance) {
		if (instance != first_instance)
			printf("---------------\n");
		printf("Instance: %s\n", instance->name);
	}

	report_plugin(instance);
	report_events(instance);
	report_event_filters(instance);
	report_event_triggers(instance);
	report_ftrace_filters(instance);
	report_graph_funcs(instance);
	report_buffers(instance);
	report_clock(instance);
	report_cpumask(instance);
	report_latency(instance);
	report_kprobes(instance);
	report_uprobes(instance);
	report_traceon(instance);
}

void trace_stat (int argc, char **argv)
{
	struct buffer_instance *instance = &top_instance;
	int topt = 0;
	int c;

	for (;;) {
		c = getopt(argc-1, argv+1, "tB:");
		if (c == -1)
			break;
		switch (c) {
		case 'h':
			usage(argv);
			break;
		case 'B':
			instance = create_instance(optarg);
			add_instance(instance);
			/* top instance requires direct access */
			if (!topt && is_top_instance(first_instance))
				first_instance = instance;
			break;
		case 't':
			/* Force to use top instance */
			topt = 1;
			instance = &top_instance;
			break;
		default:
			usage(argv);
		}
	}

	update_first_instance(instance, topt);

	for_all_instances(instance) {
		stat_instance(instance);
	}

	exit(0);
}
