/*
 * This file is part of the coreboot project.
 *
 * Copyright 2018       Facebook, Inc.
 * Copyright 2003-2017  Cavium Inc.  <support@cavium.com>
 *
 * 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; 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.
 *
 * Derived from Cavium's BSD-3 Clause OCTEONTX-SDK-6.2.0.
 */

#include <console/console.h>
#include <arch/io.h>
#include <device/pci.h>
#include <device/pci_ops.h>
#include <soc/addressmap.h>
#include <soc/cavium/common/pci/chip.h>
#include <assert.h>

#define CAVM_PCCPF_XXX_VSEC_CTL 0x108
#define CAVM_PCCPF_XXX_VSEC_SCTL 0x10c

/*
 * Hide PCI device function on BUS 1 in non secure world.
 */
static void disable_func(unsigned int devfn)
{
	u64 *addr;
	printk(BIOS_DEBUG, "PCI: 01:%02x.%x is secure\n", devfn >> 3,
	       devfn & 7);

	/* disable function */
	addr = (void *)ECAM0_RSLX_SDIS;
	u64 reg = read64(&addr[devfn]);
	reg &= ~3;
	reg |= 2;
	write64(&addr[devfn], reg);
}

/*
 * Show PCI device function on BUS 1 in non secure world.
 */
static void enable_func(unsigned int devfn)
{
	u64 *addr;

	printk(BIOS_DEBUG, "PCI: 01:%02x.%x is insecure\n", devfn >> 3,
	       devfn & 7);

	/* enable function */
	addr = (void *)ECAM0_RSLX_SDIS;
	u64 reg = read64(&addr[devfn]);
	reg &= ~3;
	write64(&addr[devfn], reg);

	addr = (void *)ECAM0_RSLX_NSDIS;
	reg = read64(&addr[devfn]);
	reg &= ~1;
	write64(&addr[devfn], reg);
}

/*
 * Hide PCI device on BUS 0 in non secure world.
 */
static void disable_device(unsigned int dev)
{
	u64 *addr;

	printk(BIOS_DEBUG, "PCI: 00:%02x.0 is secure\n", dev);

	/* disable function */
	addr = (void *)ECAM0_DEVX_SDIS;
	u64 reg = read64(&addr[dev]);
	reg &= ~3;
	write64(&addr[dev], reg);

	addr = (void *)ECAM0_DEVX_NSDIS;
	reg = read64(&addr[dev]);
	reg |= 1;
	write64(&addr[dev], reg);
}

/*
 * Show PCI device on BUS 0 in non secure world.
 */
static void enable_device(unsigned int dev)
{
	u64 *addr;

	printk(BIOS_DEBUG, "PCI: 00:%02x.0 is insecure\n", dev);

	/* enable function */
	addr = (void *)ECAM0_DEVX_SDIS;
	u64 reg = read64(&addr[dev]);
	reg &= ~3;
	write64(&addr[dev], reg);

	addr = (void *)ECAM0_DEVX_NSDIS;
	reg = read64(&addr[dev]);
	reg &= ~1;
	write64(&addr[dev], reg);
}

static void ecam0_read_resources(struct device *dev)
{
	/* There are no dynamic PCI resources on Cavium SoC */
}

static void ecam0_fix_missing_devices(struct bus *link)
{
	size_t i;

	/**
	 * Cavium thinks it's a good idea to violate the PCI spec.
	 * Disabled multi-function PCI devices might have active functions.
	 * Add devices here manually, as coreboot's PCI allocator won't find
	 * them otherwise...
	 */
	for (i = 0; i <= PCI_DEVFN(0x1f, 7); i++) {
		struct device_path pci_path;
		struct device *child;

		pci_path.type = DEVICE_PATH_PCI;
		pci_path.pci.devfn = i;

		child = find_dev_path(link, &pci_path);
		if (!child)
			pci_probe_dev(NULL, link, i);
	}
}

/**
 * Get PCI BAR address from cavium specific extended capability.
 * Use regular BAR if not found in extended capability space.
 *
 * @return The pyhsical address of the BAR, zero on error
 */
static uint64_t get_bar_val(struct device *dev, u8 bar)
{
	size_t cap_offset = pci_find_capability(dev, 0x14);
	uint64_t h, l, ret = 0;
	if (cap_offset) {
		/* Found EA */
		u8 es, bei;
		u8 ne = pci_read_config8(dev, cap_offset + 2) & 0x3f;

		cap_offset += 4;
		while (ne) {
			uint32_t dw0 = pci_read_config32(dev, cap_offset);

			es = dw0 & 7;
			bei = (dw0 >> 4) & 0xf;
			if (bei == bar) {
				h = 0;
				l = pci_read_config32(dev, cap_offset + 4);
				if (l & 2)
					h = pci_read_config32(dev,
							      cap_offset + 12);
				ret = (h << 32) | (l & ~0xfull);
				break;
			}
			cap_offset += (es + 1) * 4;
			ne--;
		}
	} else {
		h = 0;
		l = pci_read_config32(dev, bar * 4 + PCI_BASE_ADDRESS_0);
		if (l & 4)
			h = pci_read_config32(dev, bar * 4 + PCI_BASE_ADDRESS_0
					      + 4);
		ret = (h << 32) | (l & ~0xfull);
	}
	return ret;
}

/**
 * pci_enable_msix - configure device's MSI-X capability structure
 * @dev: pointer to the pci_dev data structure of MSI-X device function
 * @entries: pointer to an array of MSI-X entries
 * @nvec: number of MSI-X irqs requested for allocation by device driver
 *
 * Setup the MSI-X capability structure of device function with the number
 * of requested irqs upon its software driver call to request for
 * MSI-X mode enabled on its hardware device function. A return of zero
 * indicates the successful configuration of MSI-X capability structure.
 * A return of < 0 indicates a failure.
 * Or a return of > 0 indicates that driver request is exceeding the number
 * of irqs or MSI-X vectors available. Driver should use the returned value to
 * re-send its request.
 **/
static size_t ecam0_pci_enable_msix(struct device *dev,
				    struct msix_entry *entries, size_t nvec)
{
	struct msix_entry *msixtable;
	u32 offset;
	u8 bar_idx;
	u64 bar;
	size_t nr_entries;
	size_t i;
	u16 control;

	if (!entries) {
		printk(BIOS_ERR, "%s: No entries specified\n", __func__);
		return -1;
	}

	const size_t pos = pci_find_capability(dev, PCI_CAP_ID_MSIX);
	if (!pos) {
		printk(BIOS_ERR, "%s: Device not MSI-X capable\n",
		       dev_path(dev));
		return -1;
	}
	nr_entries = pci_msix_table_size(dev);
	if (nvec > nr_entries) {
		printk(BIOS_ERR, "ERROR: %s: Specified to many table entries\n",
		       dev_path(dev));
		return nr_entries;
	}

	/* Ensure MSI-X is disabled while it is set up */
	control = pci_read_config16(dev, pos + PCI_MSIX_FLAGS);
	control &= ~PCI_MSIX_FLAGS_ENABLE;
	pci_write_config16(dev, pos + PCI_MSIX_FLAGS, control);

	/* Find MSI-X table region */
	offset = 0;
	bar_idx = 0;
	if (pci_msix_table_bar(dev, &offset, &bar_idx)) {
		printk(BIOS_ERR, "ERROR: %s: Failed to find MSI-X entry\n",
		       dev_path(dev));
		return -1;
	}
	bar = get_bar_val(dev, bar_idx);
	if (!bar) {
		printk(BIOS_ERR, "ERROR: %s: Failed to find MSI-X bar\n",
		       dev_path(dev));
		return -1;
	}
	msixtable = (struct msix_entry *)((void *)bar + offset);

	/*
	 * Some devices require MSI-X to be enabled before we can touch the
	 * MSI-X registers.  We need to mask all the vectors to prevent
	 * interrupts coming in before they're fully set up.
	 */
	control |= PCI_MSIX_FLAGS_MASKALL | PCI_MSIX_FLAGS_ENABLE;
	pci_write_config16(dev, pos + PCI_MSIX_FLAGS, control);

	for (i = 0; i < nvec; i++) {
		write64(&msixtable[i].addr, entries[i].addr);
		write32(&msixtable[i].data, entries[i].data);
		write32(&msixtable[i].vec_control, entries[i].vec_control);
	}

	control &= ~PCI_MSIX_FLAGS_MASKALL;
	pci_write_config16(dev, pos + PCI_MSIX_FLAGS, control);

	return 0;
}

static void ecam0_init(struct device *dev)
{
	struct soc_cavium_common_pci_config *config;
	struct device *child, *child_last;
	size_t i;
	u32 reg32;

	printk(BIOS_INFO, "ECAM0: init\n");
	const struct device *bridge = dev_find_slot(0, PCI_DEVFN(1, 0));
	if (!bridge) {
		printk(BIOS_INFO, "ECAM0: ERROR: PCI 00:01.0 not found.\n");
		return;
	}
	/**
	 * Search for missing devices on BUS 1.
	 * Only required for ARI capability programming.
	 */
	ecam0_fix_missing_devices(bridge->link_list);

	/* Program secure ARI capability on bus 1 */
	child_last = NULL;
	for (i = 0; i <= PCI_DEVFN(0x1f, 7); i++) {
		child = dev_find_slot(bridge->link_list->secondary, i);
		if (!child || !child->enabled)
			continue;

		if (child_last) {
			/* Program ARI capability of the previous device */
			reg32 = pci_read_config32(child_last,
						  CAVM_PCCPF_XXX_VSEC_SCTL);
			reg32 &= ~(0xffU << 24);
			reg32 |= child->path.pci.devfn << 24;
			pci_write_config32(child_last, CAVM_PCCPF_XXX_VSEC_SCTL,
					   reg32);
		}
		child_last = child;
	}

	/* Program insecure ARI capability on bus 1 */
	child_last = NULL;
	for (i = 0; i <= PCI_DEVFN(0x1f, 7); i++) {
		child = dev_find_slot(bridge->link_list->secondary, i);
		if (!child)
			continue;
		config = child->chip_info;
		if (!child->enabled || (config && config->secure))
			continue;

		if (child_last) {
			/* Program ARI capability of the previous device */
			reg32 = pci_read_config32(child_last,
						  CAVM_PCCPF_XXX_VSEC_CTL);
			reg32 &= ~(0xffU << 24);
			reg32 |= child->path.pci.devfn << 24;
			pci_write_config32(child_last, CAVM_PCCPF_XXX_VSEC_CTL,
					   reg32);
		}
		child_last = child;
	}

	/* Enable / disable devices on bus 0 */
	for (i = 0; i <= 0x1f; i++) {
		child = dev_find_slot(0, PCI_DEVFN(i, 0));
		config = child ? child->chip_info : NULL;
		if (child && child->enabled && config && !config->secure)
			enable_device(i);
		else
			disable_device(i);
	}

	/* Enable / disable devices and functions on bus 1 */
	for (i = 0; i <= PCI_DEVFN(0x1f, 7); i++) {
		child = dev_find_slot(bridge->link_list->secondary, i);
		config = child ? child->chip_info : NULL;
		if (child && child->enabled &&
		    ((config && !config->secure) || !config))
			enable_func(i);
		else
			disable_func(i);
	}

	/* Apply IRQ on PCI devices */
	/* UUA */
	for (i = 0; i < 4; i++) {
		child = dev_find_slot(bridge->link_list->secondary,
				      PCI_DEVFN(8, i));
		if (!child)
			continue;

		struct msix_entry entry[2] = {
			{.addr = CAVM_GICD_SETSPI_NSR, .data = 37 + i},
			{.addr = CAVM_GICD_CLRSPI_NSR, .data = 37 + i},
		};

		ecam0_pci_enable_msix(child, entry, 2);
	}

	printk(BIOS_INFO, "ECAM0: done\n");
}

struct device_operations pci_domain_ops_ecam0 = {
	.set_resources    = NULL,
	.enable_resources = NULL,
	.read_resources   = ecam0_read_resources,
	.init             = ecam0_init,
	.scan_bus         = pci_domain_scan_bus,
};