/*
 * Copyright (c) International Business Machines  Corp., 2001
 * Copyright (c) 2013 Oracle and/or its affiliates. All Rights Reserved.
 *
 * 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; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 * This pci and pci-express testing kernel module will allow test calls
 * to be driven through various ioctl calls in a
 * user space program that has attained the appropriate
 * file descriptor for this device. For the functions of
 * this module to work correctly there must be a pci / pci-express
 * device somewhere in the system. The tests do not need
 * a specific device, and the first pci device available
 * will be grabbed.
 *
 * author: Sean Ruyle (srruyle@us.ibm.com)
 * date:   5/20/2003
 * PCI-Express test scripts author: Amit Khanna (amit.khanna@intel.com)
 * date:   8/20/2004
 *
 * file:   tpci.c,
 * module: tpci
 */

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/ioctl.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/pci.h>

#include "tpci.h"

MODULE_AUTHOR("Sean Ruyle <srruyle@us.ibm.com>");
MODULE_AUTHOR("Amit Khanna <amit.khanna@intel.com>");
MODULE_AUTHOR("Copyright (c) 2013 Oracle and/or its affiliates");
MODULE_DESCRIPTION("LTP PCI Test");
MODULE_LICENSE("GPL");

#define prk_err(fmt, ...) \
	pr_err(PCI_DEVICE_NAME ": " fmt "\n", ##__VA_ARGS__)
#define prk_info(fmt, ...) \
	pr_info(PCI_DEVICE_NAME ": " fmt "\n", ##__VA_ARGS__)
#define prk_debug(fmt, ...) \
	pr_debug(PCI_DEVICE_NAME ": " fmt "\n", ##__VA_ARGS__)

#define TPASS	0
#define TFAIL	1
#define TSKIP	32

static DEFINE_PCI_DEVICE_TABLE(ltp_pci_tbl) = {
	{ PCI_DEVICE(PCI_ANY_ID, PCI_ANY_ID) },
	{ 0, }
};

MODULE_DEVICE_TABLE(pci, ltp_pci_tbl);

static int ltp_pci_probe(struct pci_dev *pci_dev,
	const struct pci_device_id *pci_ent)
{
	return 0;
}

static struct pci_driver ltp_pci_driver = {
	.name     = "LTP_PCI_DRIVER",
	.id_table = ltp_pci_tbl,
	.probe    = ltp_pci_probe,
};

static int pci_registered;

struct tpci_user {
	struct pci_dev		*dev;
	struct pci_bus		*bus;
	struct pci_driver	*drv;
	uint32_t		state[16];
};
static struct tpci_user ltp_pci;

/*
 * probe_pci_dev
 *	find a pci device that can be used for other test
 *	calls in this kernel module.
 */
static int probe_pci_dev(unsigned int bus, unsigned int slot)
{
	struct pci_dev *dev;

	if (ltp_pci.dev) {
		pci_dev_put(ltp_pci.dev);
		ltp_pci.dev = NULL;
	}

	dev = pci_get_bus_and_slot(bus, slot);
	if (!dev || !dev->driver)
		return -ENODEV;

	prk_info("found pci_dev '%s', bus %u, devfn %u",
		pci_name(dev), bus, slot);

	ltp_pci.dev = dev;
	ltp_pci.bus = dev->bus;
	prk_info("Bus number: %d", dev->bus->number);
	return 0;
}

/*
 * pci_enable
 *	enable a pci device so that it may be used in
 *	later testing in the user test program
 */
static int pci_enable(void)
{
	struct pci_dev *dev = ltp_pci.dev;

	prk_info("enable pci device");

	/* check if can enable the device pointer */
	if (!dev) {
		prk_err("dev is NULL");
		return TFAIL;
	}

	if (pci_enable_device(dev)) {
		prk_err("failed to enable pci device");
		return TFAIL;
	}

	prk_info("enabled pci device");
	return TPASS;
}

static int pci_disable(void)
{
	struct pci_dev *dev = ltp_pci.dev;

	prk_info("disable pci device");

	/* check if device pointer exists */
	if (!dev) {
		prk_err("dev is NULL");
		return TFAIL;
	}

	prk_info("is pci enabled '%d', is managed '%d'",
		pci_is_enabled(dev), pci_is_managed(dev));

	pci_release_regions(dev);
	pci_disable_device(dev);

	if (dev->current_state == PCI_D3hot ||
		dev->current_state == PCI_D3cold) {

		prk_info("disabled pci device, state '%s'",
			pci_power_name(dev->current_state));
		return TPASS;

	}

	prk_err("failed to disable pci device, state '%s'",
		pci_power_name(dev->current_state));
	return TFAIL;
}

/*
 * find_bus
 *	call to pci_find_bus, use values from bus
 *	pointer in ltp_pci, make sure that returns
 *	bus with same values
 */
static int test_find_bus(void)
{
	int num = ltp_pci.bus->number;
	struct pci_bus *temp = NULL;

	prk_info("find bus");

	temp = pci_find_bus(pci_domain_nr(ltp_pci.bus), num);

	if (!temp) {
		prk_info("pci_find_bus failed");
		return TFAIL;
	} else if (temp->number != num) {
		prk_err("returned bus pointer w/ wrong bus number");
		return TFAIL;
	}

	prk_info("success returned bus pointer");
	return TPASS;
}

/*
 * find_class
 *	call to pci_find_class, using values from the
 *	pci_dev pointer in ltp_pci structure
 */
static int test_find_class(void)
{
	unsigned int num = ltp_pci.dev->class;
	struct pci_dev *temp = NULL;

	prk_info("find pci class");

	temp = pci_get_class(num, NULL);

	if (!temp) {
		prk_err("failed to find pci device from class number");
		return TFAIL;
	}

	prk_info("found pci device from class number");
	pci_dev_put(temp);

	return TPASS;
}

/*
 * find_device
 *	call to pci_find_device, using values for
 *	parameters from pci_dev pointer in the
 *	ltp_pci structure
 */
static int test_find_device(void)
{
	struct pci_dev *temp = NULL;
	unsigned short ven = ltp_pci.dev->vendor, dev = ltp_pci.dev->device;

	prk_info("get pci device");

	temp = pci_get_device(ven, dev, NULL);

	if (!temp) {
		prk_err("failed to find pci device from device info");
		return TFAIL;
	}

	prk_info("found pci device from device info");
	pci_dev_put(temp);

	return TPASS;
}

/*
 * find_subsys
 *	call to pci_find_subsys, use valued from
 *	pci_dev pointer in ltp_pci structure to
 *	find pci_dev from subsys info
 */
static int test_find_subsys(void)
{
	struct pci_dev *temp;
	unsigned short ven = ltp_pci.dev->vendor,
		dev = ltp_pci.dev->device,
		ss_ven = ltp_pci.dev->subsystem_vendor,
		ss_dev = ltp_pci.dev->subsystem_device;

	prk_info("get pci subsys");
	temp = pci_get_subsys(ven, dev, ss_ven, ss_dev, NULL);

	if (!temp) {
		prk_err("failed to find pci device from subsys info");
		return TFAIL;
	}

	prk_info("found pci device from subsys info");
	pci_dev_put(temp);

	return TPASS;
}

/*
 * test_scan_bus
 *	call to pci_do_scan_bus,  which takes
 *	a struct pci_bus pointer, which will
 *	return an integer for how far the
 *	function got in scanning bus
 */
static int test_scan_bus(void)
{
#ifdef CONFIG_HOTPLUG
	int num;
	struct pci_bus *bus = ltp_pci.bus;

	prk_info("scan pci bus");

	num = pci_rescan_bus(bus);
	/*
	 * check if returned number is greater than
	 * max number of bus or less than 0
	 */
	if (num > MAX_BUS || num < 0) {
		prk_err("failed scan bus");
		return TFAIL;
	}
	prk_info("success scan bus");
	return TPASS;
#else
	prk_info("pci_rescan_bus() is not supported");
	return TSKIP;
#endif
}

/*
 * test_slot_scan
 *	make call to pci_scan_slot, which will
 *	find the device pointer and setup the
 *	device info
 */
static int test_slot_scan(void)
{
	int ret, num = ltp_pci.dev->devfn;
	struct pci_bus *bus = ltp_pci.bus;

	prk_info("scan pci slot");

	ret = pci_scan_slot(bus, num);
	if (ret >= 0) {
		prk_info("found '%d' devices from scan slot", ret);
		return TPASS;
	}

	prk_err("pci_scan_slot failed");
	return TFAIL;
}

/*
 * test_bus_add_devices
 *	make call to pci_bus_add_devices,
 *	which will check the device pointer
 *	that is passed in for more devices
 *	that it can add
 */
static int test_bus_add_devices(void)
{
	struct pci_bus *bus = ltp_pci.bus;

	prk_info("add bus device");

	pci_bus_add_devices(bus);

	if (bus) {
		prk_info("called bus_add_device");
		return TPASS;
	}

	prk_err("bus_add_device failed");
	return TFAIL;
}

/*
 * test_enable_bridges
 *	make call to pci_enable_bridges,
 *	use bus pointer from the ltp_pci
 *	structure
 */
static int test_enable_bridges(void)
{
	struct pci_bus *bus = ltp_pci.bus;

	prk_info("enable bridges");

	pci_enable_bridges(bus);

	if (bus) {
		prk_info("called enable bridges");
		return TPASS;
	}

	prk_err("enable_bridges failed");
	return TFAIL;
}

/*
 * test_match_device
 *	make call to pci_match_device, returns a
 *	pci_device_id pointer
 */
static int test_match_device(void)
{
	struct pci_dev *dev = ltp_pci.dev;
	struct pci_driver *drv;
	const struct pci_device_id *id;

	prk_info("test pci_device_id()");

	drv = pci_dev_driver(dev);

	if (!drv) {
		prk_err("driver pointer not allocated for pci_dev");
		return TFAIL;
	}

	id = pci_match_id(drv->id_table, dev);

	if (id) {
		prk_info("match device success");
		return TPASS;
	}

	prk_err("failed return pci_device_id");
	return TFAIL;
}


/*
 * test_reg_driver
 *	make call to pci_register_driver, which will
 *	register the driver for a device with the
 *	system
 */
static int test_reg_driver(void)
{
	prk_info("test pci_register_driver");
	if (pci_register_driver(&ltp_pci_driver)) {
		prk_err("unsuccessful registering pci driver");
		return TFAIL;
	}
	pci_registered = 1;
	prk_info("success driver register");
	return TPASS;
}

/*
 * test_unreg_driver
 *	make call to pci_unregister_driver, which will
 *	unregister the driver for a device from the system
 */
static int test_unreg_driver(void)
{
	pci_unregister_driver(&ltp_pci_driver);
	pci_registered = 0;
	return TPASS;
}

/*
 * test_assign_resources
 *	make calls to pci_assign_resource, will need
 *	to setup a dev pointer and resource pointer,
 */
static int test_assign_resources(void)
{
	int i, ret, rc = 0;
	struct pci_dev *dev = ltp_pci.dev;
	struct resource *r;

	prk_info("assign resources");

	for (i = 0; i < 7; ++i) {
		prk_info("assign resource #%d", i);
		r = &dev->resource[i];
		prk_info("name = %s, flags = %lu, start 0x%lx, end 0x%lx",
			r->name, r->flags,
			(unsigned long)r->start, (unsigned long)r->end);

		if (r->flags & IORESOURCE_MEM &&
			r->flags & IORESOURCE_PREFETCH) {
			ret = pci_assign_resource(dev, i);
			prk_info("assign resource to '%d', ret '%d'", i, ret);
			rc |= (ret < 0 && ret != -EBUSY) ? TFAIL : TPASS;
		}
	}

	/*
	 * enable device after call to assign resource
	 * because might error if (!r->start && r->end)
	 */
	if (pci_enable_device(dev))
		return TFAIL;

	return rc;
}

/*
 * test_save_state
 *	make call to pci_save_state, takes in a u32*
 *	buffer
 */
static int test_save_state(void)
{
	struct pci_dev *dev = ltp_pci.dev;

	prk_info("save state");

	if (pci_save_state(dev)) {
		prk_err("failed save state");
		return TFAIL;
	}

	prk_info("saved state of device");
	return TPASS;
}

/*
 * test_restore_state
 *	make call to pci_restore_state, get the state buffer
 *	should have been previously filled out by save state
 */
static int test_restore_state(void)
{
	struct pci_dev *dev = ltp_pci.dev;

	prk_info("restore state");

	pci_restore_state(dev);

	return TPASS;
}

/*
 * test_find_cap
 *	make call to pci_find_capability, which
 *	will determine if a device has a certain
 *	capability, use second parameter to specify
 *	which capability you are looking for
 */
static int test_find_cap(void)
{
	struct pci_dev *dev = ltp_pci.dev;

	prk_info("find device capability");

	if (pci_find_capability(dev, PCI_CAP_ID_PM))
		prk_info("does not have tested capability");
	else
		prk_info("device has PM capability");

	return TPASS;
}

/*
 * test_read_pci_exp_config
 *	make call to pci_config_read and determine if
 *	the PCI-Express enhanced config space of this
 *	device can be read successfully.
 */
static int test_read_pci_exp_config(void)
{
	int pos;
	u32 header;
	struct pci_dev *dev = ltp_pci.dev;

	/* skip the test if device doesn't have PCIe capability */
	pos = pci_pcie_cap(dev);
	if (!pos) {
		prk_info("device doesn't have PCI-EXP capability");
		return TSKIP;
	}
	prk_info("read the PCI Express configuration registers at 0x%x", pos);

	if (pci_read_config_dword(dev, pos, &header)) {
		prk_err("failed to read config dword");
		return TFAIL;
	}

	/* comparing the value read with PCI_CAP_ID_EXP macro */
	if ((header & 0x000000ff) == PCI_CAP_ID_EXP) {
		prk_info("correct val read using PCIE driver installed: 0x%x",
			header);
		return TPASS;
	}

	prk_err("incorrect val read. PCIE driver/device not installed: 0x%x",
		header);
	return TFAIL;
}

static int test_case(unsigned int cmd)
{
	int rc = TSKIP;

	switch (cmd) {
	case PCI_ENABLE:
		rc = pci_enable();
		break;
	case PCI_DISABLE:
		rc = pci_disable();
		break;
	case FIND_BUS:
		rc = test_find_bus();
		break;
	case FIND_CLASS:
		rc = test_find_class();
		break;
	case FIND_DEVICE:
		rc = test_find_device();
		break;
	case FIND_SUBSYS:
		rc = test_find_subsys();
		break;
	case BUS_SCAN:
		rc = test_scan_bus();
		break;
	case SLOT_SCAN:
		rc = test_slot_scan();
		break;
	case BUS_ADD_DEVICES:
		rc = test_bus_add_devices();
		break;
	case ENABLE_BRIDGES:
		rc = test_enable_bridges();
		break;
	case MATCH_DEVICE:
		rc = test_match_device();
		break;
	case REG_DRIVER:
		rc = test_reg_driver();
		break;
	case UNREG_DRIVER:
		rc = test_unreg_driver();
		break;
	case PCI_RESOURCES:
		rc = test_assign_resources();
		break;
	case SAVE_STATE:
		rc = test_save_state();
		break;
	case RESTORE_STATE:
		rc = test_restore_state();
		break;
	case FIND_CAP:
		rc = test_find_cap();
		break;
	case PCI_EXP_CAP_CONFIG:
		rc = test_read_pci_exp_config();
		break;
	default:
		prk_info("mismatching test-case command %d", cmd);
		break;
	}

	return rc;
}

/*
 * Test-case result,
 * if test is passed, value will be set to 0
 */
static int test_result;

static void device_release(struct device *dev)
{
	prk_info("device released\n");
}

static struct device tdev = {
	.init_name	= PCI_DEVICE_NAME,
	.release	= device_release,
};

/* print test result to sysfs file */
static ssize_t sys_result(struct device *dev,
	struct device_attribute *attr, char *buf)
{
	return scnprintf(buf, PAGE_SIZE, "%d\n", test_result);
}
static DEVICE_ATTR(result, S_IRUSR, sys_result, NULL);

static ssize_t sys_tcase(struct device *dev,
	struct device_attribute *attr,  const char *buf, size_t count)
{
	int tc = 0;

	sscanf(buf, "%d", &tc);
	prk_info("test-case %d", tc);

	test_result = test_case(tc);

	return count;
}
static DEVICE_ATTR(tcase, S_IWUSR, NULL, sys_tcase);

static ssize_t sys_bus_slot(struct device *dev,
	struct device_attribute *attr,  const char *buf, size_t count)
{
	unsigned int res, bus, slot;
	int ret;

	sscanf(buf, "%u", &res);

	bus = res >> 8 & 0xFF;
	slot = res & 0xFF;

	ret = probe_pci_dev(bus, slot);
	if (ret)
		return ret;

	return count;
}
static DEVICE_ATTR(bus_slot, S_IWUSR, NULL, sys_bus_slot);

static int tpci_init_module(void)
{
	int err = 0;
	prk_info("Starting module");

	err = device_register(&tdev);
	if (err) {
		prk_err("Unable to register device");
		goto err0;
	}
	prk_info("device registered\n");

	err = device_create_file(&tdev, &dev_attr_result);
	if (err) {
		prk_err("Can't create sysfs file 'result'");
		goto err1;
	}

	err = device_create_file(&tdev, &dev_attr_tcase);
	if (err) {
		prk_err(": Can't create sysfs file 'tc'");
		goto err2;
	}

	err = device_create_file(&tdev, &dev_attr_bus_slot);
	if (err) {
		prk_err(": Can't create sysfs file 'bus_slot'");
		goto err3;
	}

	return 0;

err3:
	device_remove_file(&tdev, &dev_attr_tcase);
err2:
	device_remove_file(&tdev, &dev_attr_result);
err1:
	device_unregister(&tdev);
err0:
	return err;
}
module_init(tpci_init_module)

static void tpci_exit_module(void)
{
	prk_debug("Unloading module\n");
	if (ltp_pci.dev)
		pci_dev_put(ltp_pci.dev);

	if (pci_registered)
		pci_unregister_driver(&ltp_pci_driver);

	device_remove_file(&tdev, &dev_attr_result);
	device_remove_file(&tdev, &dev_attr_tcase);
	device_remove_file(&tdev, &dev_attr_bus_slot);
	device_unregister(&tdev);
}
module_exit(tpci_exit_module)