/*
 * PMU support for the CPU-measurement facilities
 *
 * Copyright IBM Corp. 2012, 2014
 * Contributed by Hendrik Brueckner <brueckner@linux.vnet.ibm.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
 * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
 * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
#include <ctype.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

/* private library and arch headers */
#include "pfmlib_priv.h"
#include "pfmlib_s390x_priv.h"
#include "pfmlib_perf_event_priv.h"
#include "events/s390x_cpumf_events.h"


#define CPUM_CF_DEVICE_DIR  "/sys/bus/event_source/devices/cpum_cf"
#define CPUM_SF_DEVICE_DIR  "/sys/bus/event_source/devices/cpum_sf"
#define SYS_INFO	    "/proc/sysinfo"
#define SERVICE_LEVEL	    "/proc/service_levels"
#define CF_VERSION_STR	    "CPU-MF: Counter facility: version="


/* CPU-measurement counter list (pmu events) */
static pme_cpumf_ctr_t *cpumcf_pe = NULL;

/* Detect the CPU-measurement counter and sampling facilities */
static int pfm_cpumcf_detect(void *this)
{
	if (access(CPUM_CF_DEVICE_DIR, R_OK))
		return PFM_ERR_NOTSUPP;
	return PFM_SUCCESS;
}

static int pfm_cpumsf_detect(void *this)
{
	if (access(CPUM_SF_DEVICE_DIR, R_OK))
		return PFM_ERR_NOTSUPP;
	return PFM_SUCCESS;
}

/* Parses the machine type that identifies an IBM mainframe.
 * These kind of information are from /proc/sysinfo.
 */
static long get_machine_type(void)
{
	long machine_type;
	size_t buflen, len;
	char *buffer, *tmp;
	FILE *fp;

	machine_type = 0;
	fp = fopen(SYS_INFO, "r");
	if (fp == NULL)
		goto out;

	buffer = NULL;
	while (pfmlib_getl(&buffer, &buflen, fp) != -1) {
		/* skip empty lines */
		if (*buffer == '\n')
			continue;

		/* look for 'Type:' entry */
		if (!strncmp("Type:", buffer, 5)) {
			tmp = buffer + 5;    /* set ptr after ':' */
			/* skip leading blanks */
			while (isspace(*tmp))
				tmp++;
			/* skip trailing blanks */
			len = strlen(tmp);
			while (len > 0 && isspace(tmp[len]))
				len--;
			tmp[len+1] = '\0';
			machine_type = strtol(tmp, NULL, 10);
			break;
		}
	}
	fclose(fp);
	free(buffer);
out:
	return machine_type;
}

static void get_cf_version(unsigned int *cfvn, unsigned int *csvn)
{
	int rc;
	FILE *fp;
	char *buffer;
	size_t buflen;

	*cfvn = *csvn = 0;
	fp = fopen(SERVICE_LEVEL, "r");
	if (fp == NULL)
		return;

	buffer = NULL;
	while (pfmlib_getl(&buffer, &buflen, fp) != -1) {
		/* skip empty lines */
		if (*buffer == '\n')
			continue;

		/* look for 'CPU-MF: Counter facility: version=' entry */
		if (!strncmp(CF_VERSION_STR, buffer, strlen(CF_VERSION_STR))) {
			rc = sscanf(buffer + strlen(CF_VERSION_STR), "%u.%u",
				    cfvn, csvn);
			if (rc != 2)
				*cfvn = *csvn = 0;
			break;
		}
	}
	fclose(fp);
	free(buffer);
}

/* Initialize the PMU representation for CPUMF.
 *
 * Set up the PMU events array based on
 *  - generic (basic, problem-state, and crypto-activaty) counter sets
 *  - the extended counter depending on the machine type
 */
static int pfm_cpumcf_init(void *this)
{
	pfmlib_pmu_t *pmu = this;
	unsigned int cfvn, csvn;
	const pme_cpumf_ctr_t *cfvn_set, *csvn_set, *ext_set;
	size_t cfvn_set_count, csvn_set_count, ext_set_count, pme_count;

	/* obtain counter first/second version number */
	get_cf_version(&cfvn, &csvn);

	/* counters based on first version number */
	switch (cfvn)
	{
	case 1:
		cfvn_set = cpumcf_fvn1_counters;
		cfvn_set_count = LIBPFM_ARRAY_SIZE(cpumcf_fvn1_counters);
		break;
	case 3:
		cfvn_set = cpumcf_fvn3_counters;
		cfvn_set_count = LIBPFM_ARRAY_SIZE(cpumcf_fvn3_counters);
		break;
	default:
		cfvn_set = NULL;
		cfvn_set_count = 0;
		break;
	}

	/* counters based on second version number */
	csvn_set = cpumcf_svn_generic_counters;
	csvn_set_count = LIBPFM_ARRAY_SIZE(cpumcf_svn_generic_counters);

	/* check and assign a machine-specific extended counter set */
	switch (get_machine_type()) {
	case 2097:  /* IBM System z10 EC */
	case 2098:  /* IBM System z10 BC */
		ext_set = cpumcf_z10_counters,
		ext_set_count = LIBPFM_ARRAY_SIZE(cpumcf_z10_counters);
		break;
	case 2817:  /* IBM zEnterprise 196 */
	case 2818:  /* IBM zEnterprise 114 */
		ext_set = cpumcf_z196_counters;
		ext_set_count = LIBPFM_ARRAY_SIZE(cpumcf_z196_counters);
		break;
	case 2827:  /* IBM zEnterprise EC12 */
	case 2828:  /* IBM zEnterprise BC12 */
		ext_set = cpumcf_zec12_counters;
		ext_set_count = LIBPFM_ARRAY_SIZE(cpumcf_zec12_counters);
		break;
	case 2964:  /* IBM z13  */
	case 2965:  /* IBM z13s */
		ext_set = cpumcf_z13_counters;
		ext_set_count = LIBPFM_ARRAY_SIZE(cpumcf_z13_counters);
		break;
	case 3906:  /* IBM z14  */
		ext_set = cpumcf_z14_counters;
		ext_set_count = LIBPFM_ARRAY_SIZE(cpumcf_z14_counters);
		break;
	default:
		/* No extended counter set for this machine type or there
		 * was an error retrieving the machine type */
		ext_set = NULL;
		ext_set_count = 0;
		break;
	}

	cpumcf_pe = calloc(sizeof(*cpumcf_pe),
			   cfvn_set_count + csvn_set_count + ext_set_count);
	if (cpumcf_pe == NULL)
		return PFM_ERR_NOMEM;

	pme_count = 0;
	memcpy(cpumcf_pe, cfvn_set, sizeof(*cpumcf_pe) * cfvn_set_count);
	pme_count += cfvn_set_count;
	memcpy((void *) (cpumcf_pe + pme_count), csvn_set,
	       sizeof(*cpumcf_pe) * csvn_set_count);
	pme_count += csvn_set_count;
	if (ext_set_count)
		memcpy((void *) (cpumcf_pe + pme_count),
		       ext_set, sizeof(*cpumcf_pe) * ext_set_count);
	pme_count += ext_set_count;

	pmu->pe = cpumcf_pe;
	pmu->pme_count = pme_count;

	/* CPUM-CF provides fixed counters only. The number of installed
	 * counters depends on the version and hardware model up to
	 * CPUMF_COUNTER_MAX.
	 */
	pmu->num_fixed_cntrs = pme_count;

	return PFM_SUCCESS;
}

static void pfm_cpumcf_exit(void *this)
{
	pfmlib_pmu_t *pmu = this;

	pmu->pme_count = 0;
	pmu->pe = NULL;

	free(cpumcf_pe);
}

static int pfm_cpumf_get_encoding(void *this, pfmlib_event_desc_t *e)
{
	const pme_cpumf_ctr_t *pe = this_pe(this);

	e->count = 1;	  /* number of encoded entries in e->codes */
	e->codes[0] = pe[e->event].ctrnum;
	evt_strcat(e->fstr, "%s", pe[e->event].name);

	return PFM_SUCCESS;
}

static int pfm_cpumf_get_event_first(void *this)
{
	pfmlib_pmu_t *pmu = this;

	return !!pmu->pme_count ? 0 : -1;
}

static int pfm_cpumf_get_event_next(void *this, int idx)
{
	pfmlib_pmu_t *pmu = this;

	if (idx >= (pmu->pme_count - 1))
		return -1;
	return idx + 1;
}

static int pfm_cpumf_event_is_valid(void *this, int idx)
{
	pfmlib_pmu_t *pmu = this;

	return (idx >= 0 && idx < pmu->pme_count);
}

static int pfm_cpumf_validate_table(void *this, FILE *fp)
{
	pfmlib_pmu_t *pmu = this;
	const pme_cpumf_ctr_t *pe = this_pe(this);
	int i, rc;

	rc = PFM_ERR_INVAL;
	for (i = 0; i < pmu->pme_count; i++) {
		if (!pe[i].name) {
			fprintf(fp, "pmu: %s event: %i: No name\n",
				pmu->name, i);
			goto failed;
		}
		if (!pe[i].desc) {
			fprintf(fp, "pmu: %s event: %i: No description\n",
				pmu->name, i);
			goto failed;
		}
	}

	rc = PFM_SUCCESS;
failed:
	return rc;
}

static int pfm_cpumcf_validate_table(void *this, FILE *fp)
{
	pfmlib_pmu_t *pmu = this;

	if (pmu->pme_count > CPUMF_COUNTER_MAX) {
		fprintf(fp, "pmu: %s: pme number exceeded maximum\n",
			pmu->name);
		return PFM_ERR_INVAL;
	}

	return pfm_cpumf_validate_table(this, fp);
}

static int pfm_cpumf_get_event_info(void *this, int idx,
				    pfm_event_info_t *info)
{
	pfmlib_pmu_t *pmu = this;
	const pme_cpumf_ctr_t *pe = this_pe(this);

	if (idx >= pmu->pme_count)
		return PFM_ERR_INVAL;

	info->name = pe[idx].name;
	info->desc = pe[idx].desc;
	info->code = pe[idx].ctrnum;
	info->equiv = NULL;
	info->idx = idx;
	info->pmu = pmu->pmu;
	info->is_precise = 0;

	info->nattrs = 0;	/* attributes are not supported */

	return PFM_SUCCESS;
}

static int pfm_cpumf_get_event_attr_info(void *this, int idx, int umask_idx,
					 pfmlib_event_attr_info_t *info)
{
	/* Attributes are not supported */
	return PFM_ERR_ATTR;
}

pfmlib_pmu_t s390x_cpum_cf_support = {
	.desc	   = "CPU-measurement counter facility",
	.name	   = "cpum_cf",
	.pmu	   = PFM_PMU_S390X_CPUM_CF,
	.type	   = PFM_PMU_TYPE_CORE,
	.flags	   = PFMLIB_PMU_FL_ARCH_DFL,

	.num_cntrs	 = 0,	  /* no general-purpose counters */
	.num_fixed_cntrs = CPUMF_COUNTER_MAX,	/* fixed counters only */
	.max_encoding	 = 1,

	.pe		 = NULL,
	.pme_count	 = 0,

	.pmu_detect    = pfm_cpumcf_detect,
	.pmu_init      = pfm_cpumcf_init,
	.pmu_terminate = pfm_cpumcf_exit,

	.get_event_encoding[PFM_OS_NONE] = pfm_cpumf_get_encoding,
		PFMLIB_ENCODE_PERF(pfm_s390x_get_perf_encoding),
	.get_event_first	= pfm_cpumf_get_event_first,
	.get_event_next		= pfm_cpumf_get_event_next,
	.event_is_valid		= pfm_cpumf_event_is_valid,
	.validate_table		= pfm_cpumcf_validate_table,
	.get_event_info		= pfm_cpumf_get_event_info,
	.get_event_attr_info	= pfm_cpumf_get_event_attr_info,
};

pfmlib_pmu_t s390x_cpum_sf_support = {
	.desc	   = "CPU-measurement sampling facility",
	.name	   = "cpum_sf",
	.pmu	   = PFM_PMU_S390X_CPUM_SF,
	.type	   = PFM_PMU_TYPE_CORE,
	.flags	   = PFMLIB_PMU_FL_ARCH_DFL,

	.num_cntrs	 = 0,	/* no general-purpose counters */
	.num_fixed_cntrs = 2,	/* fixed counters only */
	.max_encoding	 = 1,

	.pe	   = cpumsf_counters,
	.pme_count = LIBPFM_ARRAY_SIZE(cpumsf_counters),

	.pmu_detect = pfm_cpumsf_detect,

	.get_event_encoding[PFM_OS_NONE] = pfm_cpumf_get_encoding,
		PFMLIB_ENCODE_PERF(pfm_s390x_get_perf_encoding),
	.get_event_first	= pfm_cpumf_get_event_first,
	.get_event_next		= pfm_cpumf_get_event_next,
	.event_is_valid		= pfm_cpumf_event_is_valid,
	.validate_table		= pfm_cpumf_validate_table,
	.get_event_info		= pfm_cpumf_get_event_info,
	.get_event_attr_info	= pfm_cpumf_get_event_attr_info,
};
