/*
 * 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 <algorithm>
#include <iostream>
#include <fstream>
#include <new>
#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 "test_framework.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 "gpu-tab.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 std::string &msg);

enum {
	OPT_AUTO_TUNE = CHAR_MAX + 1,
	OPT_AUTO_TUNE_DUMP,
	OPT_BYPASS_ROOT,
	OPT_EXTECH,
	OPT_DEBUG,
	OPT_ONCE,
	OPT_RECORD,
	OPT_REPLAY
};

static bool bypass_root_check = false;

static const struct option long_options[] =
{
	/* These options set a flag. */
	{"auto-tune",		no_argument,		nullptr,	 OPT_AUTO_TUNE},
	{"auto-tune-dump",	no_argument,		nullptr,	 OPT_AUTO_TUNE_DUMP},
	{"bypass-root-check",	no_argument,		nullptr,	 OPT_BYPASS_ROOT},
	{"calibrate",		no_argument,		nullptr,	 'c'},
	{"markdown",	optional_argument,	nullptr,		 'M'},
	{"csv",		optional_argument,	nullptr,		 'C'},
	{"debug",	no_argument,		&debug_learning, OPT_DEBUG},
	{"extech",	optional_argument,	nullptr,		 OPT_EXTECH},
	{"html",	optional_argument,	nullptr,		 'r'},
	{"iteration",	optional_argument,	nullptr,		 'i'},
	{"once",	no_argument,		nullptr,		 OPT_ONCE},
	{"quiet",	no_argument,		nullptr,		 'q'},
	{"sample",	optional_argument,	nullptr,		 's'},
	{"time",	optional_argument,	nullptr,		 't'},
	{"workload",	optional_argument,	nullptr,		 'w'},
	{"version",	no_argument,		nullptr,		 'V'},
	{"help",	no_argument,		nullptr,		 'h'},
#ifdef ENABLE_TEST_FRAMEWORK
	{"record",	required_argument,	nullptr,		 OPT_RECORD},
	{"replay",	required_argument,	nullptr,		 OPT_REPLAY},
#endif
	{nullptr,		0,			nullptr,		 0}
};

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

static bool set_refresh_timeout()
{
	std::string buf;
	mvprintw(1, 0, "%s (currently %u): ", _("Set refresh time out"), time_out);
	buf = get_user_input(3);
	show_tab(0);
	unsigned time = strtoul(buf.c_str(), nullptr, 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("     --bypass-root-check %s\n", _("skip root privilege check (for testing only)"));
	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("     --once\t\t %s\n", _("run measurements only once, then exit"));
	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(nullptr) + seconds;
	delta = seconds;
	do {
		int c;
		usleep(6000);
		halfdelay(std::clamp(delta * 10, 1, 255));

		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_RESIZE:
			endwin();
			clearok(stdscr, TRUE);
			refresh();
			window_refresh();
			break;
		case KEY_EXIT:
		case 'q':
		case 27:	// Escape
			leave_powertop = 1;
			return;
		}

		delta = target - time(nullptr);
		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, const std::string &workload)
{
	create_all_usb_devices();
	start_power_measurement();
	devices_start_measurement();
	start_devfreq_measurement();
	start_process_measurement();
	start_cpu_measurement();

	if (!workload.empty()) {
		pthread_t thread = 0UL;
		end_thread = false;
		if (pthread_create(&thread, nullptr, measure_background_thread, &sample_interval))
			fprintf(stderr, _("ERROR: workload measurement thread creation failed\n"));

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

		if (thread)
		{
			end_thread = true;
			pthread_join( thread, nullptr);
		}
		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();
	if (!test_framework_manager::get().is_replaying())
		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_gpu_stats();
	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 clean_shutdown();

void make_report(const int time, const std::string &workload, const int iterations, const int sample_interval, const std::string &file)
{

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

	if (workload.empty())
	  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.c_str());
	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_wakeup();
		shutdown_tuning();
	}
	/* and wrap up */
	learn_parameters(50, 0);
	save_all_results("saved_results.powertop");
	save_parameters("saved_parameters.powertop");
	end_pci_access();

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

	exit(0);
}

static void checkroot() {
	if (bypass_root_check)
		return;
	if (test_framework_manager::get().is_replaying())
		return;
	const int uid = geteuid();

	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 = read_sysfs("/proc/sys/fs/nr_open");
	if (nr_open <= 0)
		nr_open = NR_OPEN_DEF;
	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 (!test_framework_manager::get().is_replaying()) {
		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 (pt_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"));
				} else {
					fprintf(stderr, _("Failed to mount debugfs!\n"));
					fprintf(stderr, _("Should still be able to auto tune...\n"));
				}
			}
		}
	}

	srand(time(nullptr));

	if (pt_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);

	initialized = 1;
}

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

	return;
}


int main(int argc, char **argv)
{
	try {
		int option_index;
		int c;
		std::string filename, workload;
		int  iterations = 1, auto_tune = 0, sample_interval = 5;
		bool auto_tune_dump = false, run_once = false;

		std::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::M::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!
				[[fallthrough]];
			case OPT_AUTO_TUNE:
				auto_tune = 1;
				leave_powertop = 1;
				ui_notify_user = ui_notify_user_console;
				break;
			case OPT_BYPASS_ROOT:
				bypass_root_check = true;
				break;
			case 'c':
				powertop_init(0);
				calibrate();
				exit(0);
			case 'M':
				reporttype = REPORT_MD;
				filename = optarg ? optarg : "powertop.md";
				if (filename.empty())
				{
					fprintf(stderr, _("Invalid Markdown filename\n"));
					exit(1);
				}
				break;
			case 'C':		/* csv report */
				reporttype = REPORT_CSV;
				filename = optarg ? optarg : "powertop.csv";
				if (filename.empty())
				{
					fprintf(stderr, _("Invalid CSV filename\n"));
					exit(1);
				}
				break;
#ifdef ENABLE_TEST_FRAMEWORK
			case OPT_RECORD:
				test_framework_manager::get().set_record(optarg);
				break;
			case OPT_REPLAY:
				test_framework_manager::get().set_replay(optarg);
				break;
#endif
			case OPT_DEBUG:
				/* implemented using getopt_long(3) flag */
				break;
			case OPT_ONCE:
				run_once = true;
				break;
			case OPT_EXTECH:	/* Extech power analyzer support */
				checkroot();
				extech_power_meter(optarg ? optarg : "/dev/ttyUSB0");
				break;
			case 'r':		/* html report */
				reporttype = REPORT_HTML;
				filename = optarg ? optarg : "powertop.html";
				if (filename.empty())
				{
					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 */
				workload = 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)
			fprintf(stderr, _("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();

		if (!auto_tune) {
			initialize_gpu_tab();
			create_device_stats_tab();
		}
		initialize_devfreq();
		initialize_tuning();
		initialize_wakeup();
		/* first one is short to not let the user wait too long */
		one_measurement(1, sample_interval, "");

		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, "");
			learn_parameters(15, 0);
			if (run_once)
				leave_powertop = 1;
		}
		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();
		shutdown_tuning();
		shutdown_wakeup();
		reset_display();

		clean_shutdown();

	} catch (const test_exception& e) {
		fprintf(stderr, "%s\n", e.what());
		return EXIT_FAILURE;
	}
	return 0;
}
