/*
 * Copyright 2010, Intel Corporation
 *
 * This file 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.
 *
 * Authors:
 *	Arjan van de Ven <arjan@linux.intel.com>
 */

#include <fstream>

#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <cstring>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>

#include <fcntl.h>

#include "perf_event.h"
#include "perf.h"
#include "../lib.h"
#include "../display.h"

struct tep_handle *perf_event::tep;

static inline int sys_perf_event_open(struct perf_event_attr *attr,
                      pid_t pid, int cpu, int group_fd,
                      unsigned long flags)
{
	attr->size = sizeof(*attr);
	return syscall(__NR_perf_event_open, attr, pid, cpu,
			group_fd, flags);
}

void perf_event::create_perf_event([[maybe_unused]] const std::string &eventname, int _cpu)
{
	struct perf_event_attr attr;
	int ret;
	int err;

	struct {
		__u64 count;
		__u64 time_enabled;
		__u64 time_running;
		__u64 id;
	} read_data;

	if (perf_fd != -1)
		clear();

	memset(&attr, 0, sizeof(attr));

	attr.read_format	= PERF_FORMAT_TOTAL_TIME_ENABLED |
				  PERF_FORMAT_TOTAL_TIME_RUNNING |
				  PERF_FORMAT_ID;

	attr.sample_freq	= 0;
	attr.sample_period	= 1;
	attr.sample_type	|= PERF_SAMPLE_RAW | PERF_SAMPLE_CPU | PERF_SAMPLE_TIME;

	attr.mmap		= 1;
	attr.comm		= 1;
	attr.inherit		= 0;
	attr.disabled		= 1;

	attr.type		= PERF_TYPE_TRACEPOINT;
	attr.config		= trace_type;

	if (attr.config <= 0)
		return;

	perf_fd = sys_perf_event_open(&attr, -1, _cpu, -1, 0);

	if (perf_fd < 0) {
		err = errno;
		reset_display();
		if (err == EMFILE)
			fprintf(stderr, _("Too many open files, please increase the limit of open file descriptors.\n"));
		else {
			fprintf(stderr, _("PowerTOP %s needs the kernel to support the 'perf' subsystem\n"), PACKAGE_VERSION);
			fprintf(stderr, _("as well as support for trace points in the kernel:\n"));
			fprintf(stderr, "CONFIG_PERF_EVENTS=y\nCONFIG_PERF_COUNTERS=y\nCONFIG_TRACEPOINTS=y\nCONFIG_TRACING=y\n");
		}
		exit(EXIT_FAILURE);
	}
	if (read(perf_fd, &read_data, sizeof(read_data)) == -1) {
		reset_display();
		perror("Unable to read perf file descriptor\n");
		exit(-1);
	}

	fcntl(perf_fd, F_SETFL, O_NONBLOCK);

	perf_mmap = mmap(nullptr, (bufsize+1)*getpagesize(),
				PROT_READ | PROT_WRITE, MAP_SHARED, perf_fd, 0);
	if (perf_mmap == MAP_FAILED) {
		fprintf(stderr, _("failed to mmap with %d (%s)\n"), errno, strerror(errno));
		close(perf_fd);
		perf_fd = -1;
		return;
	}

	ret = ioctl(perf_fd, PERF_EVENT_IOC_ENABLE, 0);

	if (ret < 0) {
		fprintf(stderr, _("failed to enable perf\n"));
	}

	pc = (perf_event_mmap_page *)perf_mmap;
	data_mmap = (unsigned char *)perf_mmap + getpagesize();


}

static int parse_event_format(const std::string &system_name, const std::string &event_name)
{
	char *buf;
	int size;

	buf = tracefs_event_file_read(nullptr, system_name.c_str(), event_name.c_str(), "format", &size);
	if (!buf)
		return -1;

	tep_parse_event(perf_event::tep, buf, size, system_name.c_str());
	free(buf);
	return 0;
}

void perf_event::set_event_name(const std::string &system_name, const std::string &event_name)
{
	struct tep_event *event;
	int ret;

	event = tep_find_event_by_name(perf_event::tep, system_name.c_str(), event_name.c_str());
	if (!event) {
		ret = parse_event_format(system_name, event_name);
		if (ret < 0) {
			trace_type = -1;
			return;
		}
		event = tep_find_event_by_name(perf_event::tep, system_name.c_str(), event_name.c_str());
		if (!event) {
			trace_type = -1;
			return;
		}
	}
	trace_type = event->id;
}

perf_event::~perf_event(void)
{
	clear();
	if (tep_get_ref(perf_event::tep) == 1) {
		tep_free(perf_event::tep);
		perf_event::tep = nullptr;
	} else
		tep_unref(perf_event::tep);
}

void perf_event::set_cpu(int _cpu)
{
	cpu = _cpu;
}

static void allocate_tep(void)
{
	if (!perf_event::tep)
		perf_event::tep = tep_alloc();
	else
		tep_ref(perf_event::tep);
}

perf_event::perf_event(const std::string &system_name, const std::string &event_name, int _cpu, int buffer_size)
{
	allocate_tep();
	perf_fd = -1;
	bufsize = buffer_size;
	cpu = _cpu;
	perf_mmap = nullptr;
	pc = nullptr;
	data_mmap = nullptr;
	trace_type = 0;
	set_event_name(system_name, event_name);
}

perf_event::perf_event(void)
{
	allocate_tep();
	perf_fd = -1;
	bufsize = 128;
	perf_mmap = nullptr;
	pc = nullptr;
	data_mmap = nullptr;
	cpu = 0;
	trace_type = 0;
}

void perf_event::start(void)
{
	create_perf_event(name, cpu);
}

void perf_event::stop(void)
{
	int ret;
	if (perf_fd < 0)
		return;
	ret = ioctl(perf_fd, PERF_EVENT_IOC_DISABLE, 0);
	if (ret)
		fprintf(stderr, _("stop failing\n"));
}

void perf_event::process(void *cookie)
{
	struct perf_event_header *header;

	if (perf_fd < 0)
		return;

	const uint64_t head = pc->data_head;
	__sync_synchronize(); /* required rmb() before reading ring buffer data */

	while (pc->data_tail != head) {
		while (pc->data_tail >= (unsigned int)bufsize * getpagesize())
			pc->data_tail -= bufsize * getpagesize();

		header = (struct perf_event_header *)( (unsigned char *)data_mmap + pc->data_tail);

		if (header->size == 0)
			break;

		pc->data_tail += header->size;

		while (pc->data_tail >= (unsigned int)bufsize * getpagesize())
			pc->data_tail -= bufsize * getpagesize();

		if (header->type == PERF_RECORD_SAMPLE)
			handle_event(header, cookie);
	}
	pc->data_tail = head;
}

void perf_event::clear(void)
{
	if (perf_mmap) {
//		memset(perf_mmap, 0, (bufsize)*getpagesize());
		munmap(perf_mmap, (bufsize+1)*getpagesize());
		perf_mmap = nullptr;
	}
	if (perf_fd != -1)
		close(perf_fd);
	perf_fd = -1;
}
