/*
 * Copyright 2010, Intel Corporation
 *
 * This is part of PowerTOP
 *
 * This program file 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.
 *
 * 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 in a file named COPYING; if not, write to the
 * Free Software Foundation, Inc,
 * 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301 USA
 * or just google for it.
 *
 * getopt code is taken from "The GNU C Library" reference manual,
 * section 24.2 "Parsing program options using getopt"
 * http://www.gnu.org/s/libc/manual/html_node/Getopt-Long-Option-Example.html
 * Manual published under the terms of the Free Documentation License.
 *
 * Authors:
 *	Arjan van de Ven <arjan@linux.intel.com>
 */
#include <iostream>
#include <fstream>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/statfs.h>
#include <getopt.h>
#include <unistd.h>
#include <locale.h>
#include <sys/resource.h>
#include <limits.h>
#include <pthread.h>

#include "cpu/cpu.h"
#include "process/process.h"
#include "perf/perf.h"
#include "perf/perf_bundle.h"
#include "lib.h"
#include "../config.h"


#include "devices/device.h"
#include "devices/devfreq.h"
#include "devices/usb.h"
#include "devices/ahci.h"
#include "measurement/measurement.h"
#include "parameters/parameters.h"
#include "calibrate/calibrate.h"


#include "tuning/tuning.h"
#include "wakeup/wakeup.h"

#include "display.h"
#include "devlist.h"
#include "report/report.h"

#define DEBUGFS_MAGIC          0x64626720

#define NR_OPEN_DEF 1024 * 1024

int debug_learning = 0;
unsigned time_out = 20;
int leave_powertop = 0;
void (*ui_notify_user) (const char *frmt, ...);

enum {
	OPT_AUTO_TUNE = CHAR_MAX + 1,
	OPT_AUTO_TUNE_DUMP,
	OPT_EXTECH,
	OPT_DEBUG
};

static const struct option long_options[] =
{
	/* These options set a flag. */
	{"auto-tune",	no_argument,		NULL,		 OPT_AUTO_TUNE},
	{"auto-tune-dump",	no_argument,	NULL,		 OPT_AUTO_TUNE_DUMP},
	{"calibrate",	no_argument,		NULL,		 'c'},
	{"csv",		optional_argument,	NULL,		 'C'},
	{"debug",	no_argument,		&debug_learning, OPT_DEBUG},
	{"extech",	optional_argument,	NULL,		 OPT_EXTECH},
	{"html",	optional_argument,	NULL,		 'r'},
	{"iteration",	optional_argument,	NULL,		 'i'},
	{"quiet",	no_argument,		NULL,		 'q'},
	{"sample",	optional_argument,	NULL,		 's'},
	{"time",	optional_argument,	NULL,		 't'},
	{"workload",	optional_argument,	NULL,		 'w'},
	{"version",	no_argument,		NULL,		 'V'},
	{"help",	no_argument,		NULL,		 'h'},
	{NULL,		0,			NULL,		 0}
};

static void print_version()
{
	printf(_("PowerTOP version " PACKAGE_VERSION "\n"));
}

static bool set_refresh_timeout()
{
	static char buf[4];
	mvprintw(1, 0, "%s (currently %u): ", _("Set refresh time out"), time_out);
	memset(buf, '\0', sizeof(buf));
	get_user_input(buf, sizeof(buf) - 1);
	show_tab(0);
	unsigned time = strtoul(buf, NULL, 0);
	if (!time) return 0;
	if (time > 32) time = 32;
	time_out = time;
	return 1;
}

static void print_usage()
{
	printf("%s\n\n", _("Usage: powertop [OPTIONS]"));
	printf("     --auto-tune\t %s\n", _("sets all tunable options to their GOOD setting"));
	printf("     --auto-tune-dump\t %s\n", _("print auto-tune commands to STDOUT *instead* of executing them"));
	printf(" -c, --calibrate\t %s\n", _("runs powertop in calibration mode"));
	printf(" -C, --csv%s\t %s\n", _("[=filename]"), _("generate a csv report"));
	printf("     --debug\t\t %s\n", _("run in \"debug\" mode"));
	printf("     --extech%s\t %s\n", _("[=devnode]"), _("uses an Extech Power Analyzer for measurements"));
	printf(" -r, --html%s\t %s\n", _("[=filename]"), _("generate a html report"));
	printf(" -i, --iteration%s\n", _("[=iterations] number of times to run each test"));
	printf(" -q, --quiet\t\t %s\n", _("suppress stderr output"));
	printf(" -s, --sample%s\t %s\n", _("[=seconds]"), _("interval for power consumption measurement"));
	printf(" -t, --time%s\t %s\n", _("[=seconds]"), _("generate a report for 'x' seconds"));
	printf(" -w, --workload%s %s\n", _("[=workload]"), _("file to execute for workload"));
	printf(" -V, --version\t\t %s\n", _("print version information"));
	printf(" -h, --help\t\t %s\n", _("print this help menu"));
	printf("\n");
	printf("%s\n\n", _("For more help please refer to the 'man 8 powertop'"));
}

static void do_sleep(int seconds)
{
	time_t target;
	int delta;

	if (!ncurses_initialized()) {
		sleep(seconds);
		return;
	}
	target = time(NULL) + seconds;
	delta = seconds;
	do {
		int c;
		usleep(6000);
		halfdelay(delta * 10);

		c = getch();
		switch (c) {
		case KEY_BTAB:
			show_prev_tab();
			break;
		case '\t':
			show_next_tab(); 
			break;
		case KEY_RIGHT:
			cursor_right(); 
			break;
		case KEY_LEFT:
			cursor_left(); 
			break;
		case KEY_NPAGE:
		case KEY_DOWN:
			cursor_down();
			break;
		case KEY_PPAGE:
		case KEY_UP:
			cursor_up();
			break;
		case ' ':
		case '\n':
			cursor_enter();
			break;
		case 's':
			if (set_refresh_timeout())
				return;
			break;
		case 'r':
			window_refresh();
			return;
		case KEY_EXIT:
		case 'q':
		case 27:	// Escape
			leave_powertop = 1;
			return;
		}

		delta = target - time(NULL);
		if (delta <= 0)
			break;

	} while (1);
}

extern "C" {
	static volatile bool end_thread;
	void* measure_background_thread(void *arg)
	{
		int sleep_time = *((int *) arg);
		while (!end_thread) {
			do_sleep(sleep_time);
			global_sample_power();
		}
		return 0;
	}
}

void one_measurement(int seconds, int sample_interval, char *workload)
{
	create_all_usb_devices();
	start_power_measurement();
	devices_start_measurement();
	start_devfreq_measurement();
	start_process_measurement();
	start_cpu_measurement();

	if (workload && workload[0]) {
		pthread_t thread = 0UL;
		end_thread = false;
		if (pthread_create(&thread, NULL, measure_background_thread, &sample_interval))
			fprintf(stderr, "ERROR: workload measurement thread creation failed\n");

		if (system(workload))
			fprintf(stderr, _("Unknown issue running workload!\n"));

		if (thread)
		{
			end_thread = true;
			pthread_join( thread, NULL);
		}
		global_sample_power();
	} else {
		while (seconds > 0)
		{
			do_sleep(sample_interval > seconds ? seconds : sample_interval);
			seconds -= sample_interval;
			global_sample_power();
		}
	}
	end_cpu_measurement();
	end_process_measurement();
	collect_open_devices();
	end_devfreq_measurement();
	devices_end_measurement();
	end_power_measurement();

	process_cpu_data();
	process_process_data();

	/* output stats */
	process_update_display();
	report_summary();
	w_display_cpu_cstates();
	w_display_cpu_pstates();
	if (reporttype != REPORT_OFF) {
		report_display_cpu_cstates();
		report_display_cpu_pstates();
	}
	report_process_update_display();
	tuning_update_display();
	wakeup_update_display();
	end_process_data();

	global_power();
	compute_bundle();

	show_report_devices();
	report_show_open_devices();

	report_devices();
	display_devfreq_devices();
	report_devfreq_devices();
	ahci_create_device_stats_table();
	store_results(measurement_time);
	end_cpu_data();
}

void out_of_memory()
{
	reset_display();
	printf("%s...\n",_("PowerTOP is out of memory. PowerTOP is Aborting"));
	abort();
}

void make_report(int time, char *workload, int iterations, int sample_interval, char *file)
{

	/* one to warm up everything */
	fprintf(stderr, _("Preparing to take measurements\n"));
	utf_ok = 0;
	one_measurement(1, sample_interval, NULL);

	if (!workload[0])
	  fprintf(stderr, _("Taking %d measurement(s) for a duration of %d second(s) each.\n"),iterations,time);
	else
	   fprintf(stderr, _("Measuring workload %s.\n"), workload);
	for (int i=0; i != iterations; i++){
		init_report_output(file, iterations);
		initialize_tuning();
		initialize_wakeup();
		/* and then the real measurement */
		one_measurement(time, sample_interval, workload);
		report_show_tunables();
		report_show_wakeup();
		finish_report_output();
		clear_tuning();
	}
	/* and wrap up */
	learn_parameters(50, 0);
	save_all_results("saved_results.powertop");
	save_parameters("saved_parameters.powertop");
	end_pci_access();
	exit(0);
}

static void checkroot() {
	int uid;
	uid = getuid();

	if (uid != 0) {
		printf(_("PowerTOP " PACKAGE_VERSION " must be run with root privileges.\n"));
		printf(_("exiting...\n"));
		exit(EXIT_FAILURE);
	}

}

static int get_nr_open(void) {
	int nr_open = NR_OPEN_DEF;
	ifstream file;

	file.open("/proc/sys/fs/nr_open", ios::in);
	if (file) {
		file >> nr_open;
		file.close();
	}
	return nr_open;
}

static void powertop_init(int auto_tune)
{
	static char initialized = 0;
	int ret;
	struct statfs st_fs;
	struct rlimit rlmt;

	if (initialized)
		return;

	checkroot();

	rlmt.rlim_cur = rlmt.rlim_max = get_nr_open();
	setrlimit (RLIMIT_NOFILE, &rlmt);

	if (system("/sbin/modprobe cpufreq_stats > /dev/null 2>&1"))
		fprintf(stderr, _("modprobe cpufreq_stats failed\n"));
#if defined(__i386__) || defined(__x86_64__)
	if (system("/sbin/modprobe msr > /dev/null 2>&1"))
		fprintf(stderr, _("modprobe msr failed\n"));
#endif
	statfs("/sys/kernel/debug", &st_fs);

	if (st_fs.f_type != (long) DEBUGFS_MAGIC) {
		if (access("/bin/mount", X_OK) == 0) {
			ret = system("/bin/mount -t debugfs debugfs /sys/kernel/debug > /dev/null 2>&1");
		} else {
			ret = system("mount -t debugfs debugfs /sys/kernel/debug > /dev/null 2>&1");
		}
		if (ret != 0) {
			if (!auto_tune) {
				fprintf(stderr, _("Failed to mount debugfs!\n"));
				fprintf(stderr, _("exiting...\n"));
				exit(EXIT_FAILURE);
			} else {
				fprintf(stderr, _("Failed to mount debugfs!\n"));
				fprintf(stderr, _("Should still be able to auto tune...\n"));
			}
		}
	}

	srand(time(NULL));

	if (access("/var/cache/", W_OK) == 0)
		mkdir("/var/cache/powertop", 0600);
	else
		mkdir("/data/local/powertop", 0600);

	load_results("saved_results.powertop");
	load_parameters("saved_parameters.powertop");

	enumerate_cpus();
	create_all_devices();
	create_all_devfreq_devices();
	detect_power_meters();

	register_parameter("base power", 100, 0.5);
	register_parameter("cpu-wakeups", 39.5);
	register_parameter("cpu-consumption", 1.56);
	register_parameter("gpu-operations", 0.5576);
	register_parameter("disk-operations-hard", 0.2);
	register_parameter("disk-operations", 0.0);
	register_parameter("xwakes", 0.1);

        load_parameters("saved_parameters.powertop");

	initialized = 1;
}

void clean_shutdown()
{
	close_results();
	clean_open_devices();
	clear_all_devices();
	clear_all_devfreq();
	clear_all_cpus();

	return;
}


int main(int argc, char **argv)
{
	int option_index;
	int c;
	char filename[PATH_MAX];
	char workload[PATH_MAX] = {0};
	int  iterations = 1, auto_tune = 0, sample_interval = 5;
	bool auto_tune_dump = false;

	set_new_handler(out_of_memory);

	setlocale (LC_ALL, "");

#ifdef ENABLE_NLS
	bindtextdomain (PACKAGE, LOCALEDIR);
	textdomain (PACKAGE);
#endif
	ui_notify_user = ui_notify_user_ncurses;
	while (1) { /* parse commandline options */
		c = getopt_long(argc, argv, "cC::r::i:qt:w:Vh", long_options, &option_index);
		/* Detect the end of the options. */
		if (c == -1)
			break;
		switch (c) {
		case OPT_AUTO_TUNE_DUMP:
			auto_tune_dump = true; // do NOT `break;` here!
		case OPT_AUTO_TUNE:
			auto_tune = 1;
			leave_powertop = 1;
			ui_notify_user = ui_notify_user_console;
			break;
		case 'c':
			powertop_init(0);
			calibrate();
			break;
		case 'C':		/* csv report */
			reporttype = REPORT_CSV;
			snprintf(filename, sizeof(filename), "%s", optarg ? optarg : "powertop.csv");
			if (!strlen(filename))
			{
				fprintf(stderr, _("Invalid CSV filename\n"));
				exit(1);
			}
			break;
		case OPT_DEBUG:
			/* implemented using getopt_long(3) flag */
			break;
		case OPT_EXTECH:	/* Extech power analyzer support */
			checkroot();
			extech_power_meter(optarg ? optarg : "/dev/ttyUSB0");
			break;
		case 'r':		/* html report */
			reporttype = REPORT_HTML;
			snprintf(filename, sizeof(filename), "%s", optarg ? optarg : "powertop.html");
			if (!strlen(filename))
			{
				fprintf(stderr, _("Invalid HTML filename\n"));
				exit(1);
			}
			break;
		case 'i':
			iterations = (optarg ? atoi(optarg) : 1);
			break;
		case 'q':
			if (freopen("/dev/null", "a", stderr))
				fprintf(stderr, _("Quiet mode failed!\n"));
			break;
		case 's':
			sample_interval = (optarg ? atoi(optarg) : 5);
			break;
		case 't':
			time_out = (optarg ? atoi(optarg) : 20);
			break;
		case 'w':		/* measure workload */
			snprintf(workload, sizeof(workload), "%s", optarg ? optarg : "");
			break;
		case 'V':
			print_version();
			exit(0);
			break;
		case 'h':
			print_usage();
			exit(0);
			break;
		case '?':		/* Unknown option */
			/* getopt_long already printed an error message. */
			exit(1);
			break;
		}
	}

	powertop_init(auto_tune);

	if (reporttype != REPORT_OFF)
		make_report(time_out, workload, iterations, sample_interval, filename);

	if (debug_learning)
		printf("Learning debugging enabled\n");

	learn_parameters(250, 0);
	save_parameters("saved_parameters.powertop");


	if (debug_learning) {
	        learn_parameters(1000, 1);
		dump_parameter_bundle();
		end_pci_access();
		exit(0);
	}
	if (!auto_tune)
		init_display();

	initialize_devfreq();
	initialize_tuning();
	initialize_wakeup();
	/* first one is short to not let the user wait too long */
	one_measurement(1, sample_interval, NULL);

	if (!auto_tune) {
		tuning_update_display();
		show_tab(0);
	} else {
		auto_toggle_tuning(auto_tune_dump);
	}

	while (!leave_powertop) {
		if (!auto_tune)
			show_cur_tab();
		one_measurement(time_out, sample_interval, NULL);
		learn_parameters(15, 0);
	}
	if (!auto_tune)
		endwin();
	fprintf(stderr, "%s\n", _("Leaving PowerTOP"));

	end_process_data();
	clear_process_data();
	end_cpu_data();
	clear_cpu_data();

	save_all_results("saved_results.powertop");
	save_parameters("saved_parameters.powertop");
	learn_parameters(500, 0);
	save_parameters("saved_parameters.powertop");
	end_pci_access();
	clear_tuning();
	reset_display();

	clean_shutdown();

	return 0;
}
