/*
 * Copyright (c) 2008, Intel Corporation.
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms and conditions of the GNU Lesser General Public License,
 * version 2.1, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.
 *
 */

#include "utils.h"
#include "adapt_impl.h"
#include <linux/pci_regs.h>
#include <pciaccess.h>
#include <byteswap.h>

static void
get_device_serial_number(struct pci_device *dev, struct hba_info *hba_info)
{
	pciaddr_t offset;
	u_int32_t pcie_cap_header;
	u_int16_t pcie_cap_id;
	u_int16_t status;
	u_int8_t cap_ptr;
	u_int32_t dword_low = 0;
	u_int32_t dword_high = 0;
	int rc;

	/* Default */
	snprintf(hba_info->SerialNumber,
		 sizeof(hba_info->SerialNumber),
		 "Unknown");
	/*
	 * Read the Status Register in the PCIe configuration
	 * header space to see if the PCI Capability List is
	 * supported by this device.
	 */
	rc = pci_device_cfg_read_u16(dev, &status, PCI_STATUS);
	if (rc) {
		fprintf(stderr, "Failed reading PCI Status Register\n");
		return;
	}
	if (!(status & PCI_STATUS_CAP_LIST)) {
		printf("PCI capabilities are not supported\n");
		return;
	}

	/*
	 * Read the offset (cap_ptr) of first entry in the capability list in
	 * the PCI configuration space.
	 */
	rc = pci_device_cfg_read_u8(dev, &cap_ptr, PCI_CAPABILITY_LIST);
	if (rc) {
		fprintf(stderr,
			"Failed reading PCI Capability List Register\n");
		return;
	}
	offset = cap_ptr;

	/* Search for the PCIe capability */
	while (offset) {
		u_int8_t cap_id;
		u_int8_t next_cap;

		rc = pci_device_cfg_read_u8(dev, &cap_id,
					    offset + PCI_CAP_LIST_ID);
		if (rc) {
#if defined(__x86_64__)
			fprintf(stderr,
				"Failed reading capability ID at 0x%lx\n",
				offset + PCI_CAP_LIST_ID);
#elif defined(__i386__)
			fprintf(stderr,
				"Failed reading capability ID at 0x%llx\n",
				offset + PCI_CAP_LIST_ID);
#endif
			return;
		}

		if (cap_id != PCI_CAP_ID_EXP) {
			rc = pci_device_cfg_read_u8(dev, &next_cap,
						    offset + PCI_CAP_LIST_NEXT);
			if (rc) {
#if defined(__x86_64__)
				fprintf(stderr, "Failed reading next capability "
					"offset at 0x%lx\n",
					offset + PCI_CAP_LIST_NEXT);
#elif defined(__i386__)
				fprintf(stderr, "Failed reading next capability "
					"offset at 0x%llx\n",
					offset + PCI_CAP_LIST_NEXT);
#endif
				return;
			}
			offset = (pciaddr_t)next_cap;
			continue;
		}

		/*
		 * PCIe Capability Structure exists!
		 */

		/*
		 * The first PCIe extended capability is located at
		 * offset 0x100 in the device configuration space.
		 */
		offset = 0x100;

		do {
			rc = pci_device_cfg_read_u32(dev, &pcie_cap_header,
						     offset);
			if (rc) {
				fprintf(stderr,
					"Failed reading PCIe config header\n");
				return;
			}

			/* Get the PCIe Extended Capability ID */
			pcie_cap_id = pcie_cap_header & 0xffff;

			if (pcie_cap_id != PCI_EXT_CAP_ID_DSN) {
				/* Get the offset of the next capability */
				offset = (pciaddr_t)pcie_cap_header >> 20;
				continue;
			}

			/*
			 * Found the serial number register!
			 */

			rc = pci_device_cfg_read_u32(dev,
						     &dword_low, offset + 4);
			rc = pci_device_cfg_read_u32(dev,
						     &dword_high, offset + 8);
			snprintf(hba_info->SerialNumber,
				 sizeof(hba_info->SerialNumber),
				 "%02X%02X%02X%02X%02X%02X\n",
				 dword_high >> 24, (dword_high >> 16) & 0xff,
				 (dword_high >> 8) & 0xff, (dword_low >> 16) & 0xff,
				 (dword_low >> 8) & 0xff, dword_low & 0xff);
			break;
		} while (offset);
		break;
	}
}

static void
get_pci_device_info(struct pci_device *dev, struct hba_info *hba_info)
{
	const char *name;
	u_int8_t revision;
	char *unknown = "Unknown";

	name = pci_device_get_vendor_name(dev);
	if (!name)
		name = unknown;
	sa_strncpy_safe(hba_info->Manufacturer,
			sizeof(hba_info->Manufacturer),
			name, sizeof(hba_info->Manufacturer));

	name = pci_device_get_device_name(dev);
	if (!name)
		name = unknown;
	sa_strncpy_safe(hba_info->ModelDescription,
			sizeof(hba_info->ModelDescription),
			name, sizeof(hba_info->ModelDescription));

	/*
	 * Reading hardware revision from PCIe
	 * configuration header space.
	 */
	pci_device_cfg_read_u8(dev, &revision, PCI_REVISION_ID);
	snprintf(hba_info->HardwareVersion,
		 sizeof(hba_info->HardwareVersion),
		 "%02x", revision);

	hba_info->NumberOfPorts = 1;

	/*
	 * Searching for serial number in PCIe extended
	 * capabilities space
	 */
	get_device_serial_number(dev, hba_info);
}

HBA_STATUS
find_pci_device(struct hba_info *hba_info)
{
	struct pci_device_iterator *iterator;
	struct pci_device *dev;
	struct pci_slot_match match;

	int rc;

	rc = pci_system_init();
	if (rc) {
		fprintf(stderr, "pci_system_init failed\n");
		return HBA_STATUS_ERROR;
	}

	match.domain = hba_info->domain;
	match.bus = hba_info->bus;
	match.dev = hba_info->dev;
	match.func = hba_info->func;

	iterator = pci_slot_match_iterator_create(&match);

	for (;;) {
		dev = pci_device_next(iterator);
		if (!dev)
			break;
		get_pci_device_info(dev, hba_info);
	}

	pci_system_cleanup();
	return HBA_STATUS_OK;
}

