/*
 * Copyright (c) 2014 Google, Inc
 *
 * (C) Copyright 2001 Sysgo Real-Time Solutions, GmbH <www.elinos.com>
 * Andreas Heppel <aheppel@sysgo.de>
 *
 * (C) Copyright 2002, 2003
 * Wolfgang Denk, DENX Software Engineering, wd@denx.de.
 *
 * SPDX-License-Identifier:	GPL-2.0+
 */

#include <common.h>
#include <dm.h>
#include <errno.h>
#include <pci.h>
#include <asm/io.h>

const char *pci_class_str(u8 class)
{
	switch (class) {
	case PCI_CLASS_NOT_DEFINED:
		return "Build before PCI Rev2.0";
		break;
	case PCI_BASE_CLASS_STORAGE:
		return "Mass storage controller";
		break;
	case PCI_BASE_CLASS_NETWORK:
		return "Network controller";
		break;
	case PCI_BASE_CLASS_DISPLAY:
		return "Display controller";
		break;
	case PCI_BASE_CLASS_MULTIMEDIA:
		return "Multimedia device";
		break;
	case PCI_BASE_CLASS_MEMORY:
		return "Memory controller";
		break;
	case PCI_BASE_CLASS_BRIDGE:
		return "Bridge device";
		break;
	case PCI_BASE_CLASS_COMMUNICATION:
		return "Simple comm. controller";
		break;
	case PCI_BASE_CLASS_SYSTEM:
		return "Base system peripheral";
		break;
	case PCI_BASE_CLASS_INPUT:
		return "Input device";
		break;
	case PCI_BASE_CLASS_DOCKING:
		return "Docking station";
		break;
	case PCI_BASE_CLASS_PROCESSOR:
		return "Processor";
		break;
	case PCI_BASE_CLASS_SERIAL:
		return "Serial bus controller";
		break;
	case PCI_BASE_CLASS_INTELLIGENT:
		return "Intelligent controller";
		break;
	case PCI_BASE_CLASS_SATELLITE:
		return "Satellite controller";
		break;
	case PCI_BASE_CLASS_CRYPT:
		return "Cryptographic device";
		break;
	case PCI_BASE_CLASS_SIGNAL_PROCESSING:
		return "DSP";
		break;
	case PCI_CLASS_OTHERS:
		return "Does not fit any class";
		break;
	default:
	return  "???";
		break;
	};
}

__weak int pci_skip_dev(struct pci_controller *hose, pci_dev_t dev)
{
	/*
	 * Check if pci device should be skipped in configuration
	 */
	if (dev == PCI_BDF(hose->first_busno, 0, 0)) {
#if defined(CONFIG_PCI_CONFIG_HOST_BRIDGE) /* don't skip host bridge */
		/*
		 * Only skip configuration if "pciconfighost" is not set
		 */
		if (env_get("pciconfighost") == NULL)
			return 1;
#else
		return 1;
#endif
	}

	return 0;
}

#if !defined(CONFIG_DM_PCI) || defined(CONFIG_DM_PCI_COMPAT)
/* Get a virtual address associated with a BAR region */
void *pci_map_bar(pci_dev_t pdev, int bar, int flags)
{
	pci_addr_t pci_bus_addr;
	u32 bar_response;

	/* read BAR address */
	pci_read_config_dword(pdev, bar, &bar_response);
	pci_bus_addr = (pci_addr_t)(bar_response & ~0xf);

	/*
	 * Pass "0" as the length argument to pci_bus_to_virt.  The arg
	 * isn't actualy used on any platform because u-boot assumes a static
	 * linear mapping.  In the future, this could read the BAR size
	 * and pass that as the size if needed.
	 */
	return pci_bus_to_virt(pdev, pci_bus_addr, flags, 0, MAP_NOCACHE);
}

void pci_write_bar32(struct pci_controller *hose, pci_dev_t dev, int barnum,
		     u32 addr_and_ctrl)
{
	int bar;

	bar = PCI_BASE_ADDRESS_0 + barnum * 4;
	pci_hose_write_config_dword(hose, dev, bar, addr_and_ctrl);
}

u32 pci_read_bar32(struct pci_controller *hose, pci_dev_t dev, int barnum)
{
	u32 addr;
	int bar;

	bar = PCI_BASE_ADDRESS_0 + barnum * 4;
	pci_hose_read_config_dword(hose, dev, bar, &addr);
	if (addr & PCI_BASE_ADDRESS_SPACE_IO)
		return addr & PCI_BASE_ADDRESS_IO_MASK;
	else
		return addr & PCI_BASE_ADDRESS_MEM_MASK;
}

int __pci_hose_bus_to_phys(struct pci_controller *hose,
			   pci_addr_t bus_addr,
			   unsigned long flags,
			   unsigned long skip_mask,
			   phys_addr_t *pa)
{
	struct pci_region *res;
	int i;

	for (i = 0; i < hose->region_count; i++) {
		res = &hose->regions[i];

		if (((res->flags ^ flags) & PCI_REGION_TYPE) != 0)
			continue;

		if (res->flags & skip_mask)
			continue;

		if (bus_addr >= res->bus_start &&
		    (bus_addr - res->bus_start) < res->size) {
			*pa = (bus_addr - res->bus_start + res->phys_start);
			return 0;
		}
	}

	return 1;
}

phys_addr_t pci_hose_bus_to_phys(struct pci_controller *hose,
				 pci_addr_t bus_addr,
				 unsigned long flags)
{
	phys_addr_t phys_addr = 0;
	int ret;

	if (!hose) {
		puts("pci_hose_bus_to_phys: invalid hose\n");
		return phys_addr;
	}

	/*
	 * if PCI_REGION_MEM is set we do a two pass search with preference
	 * on matches that don't have PCI_REGION_SYS_MEMORY set
	 */
	if ((flags & PCI_REGION_TYPE) == PCI_REGION_MEM) {
		ret = __pci_hose_bus_to_phys(hose, bus_addr,
				flags, PCI_REGION_SYS_MEMORY, &phys_addr);
		if (!ret)
			return phys_addr;
	}

	ret = __pci_hose_bus_to_phys(hose, bus_addr, flags, 0, &phys_addr);

	if (ret)
		puts("pci_hose_bus_to_phys: invalid physical address\n");

	return phys_addr;
}

int __pci_hose_phys_to_bus(struct pci_controller *hose,
			   phys_addr_t phys_addr,
			   unsigned long flags,
			   unsigned long skip_mask,
			   pci_addr_t *ba)
{
	struct pci_region *res;
	pci_addr_t bus_addr;
	int i;

	for (i = 0; i < hose->region_count; i++) {
		res = &hose->regions[i];

		if (((res->flags ^ flags) & PCI_REGION_TYPE) != 0)
			continue;

		if (res->flags & skip_mask)
			continue;

		bus_addr = phys_addr - res->phys_start + res->bus_start;

		if (bus_addr >= res->bus_start &&
		    (bus_addr - res->bus_start) < res->size) {
			*ba = bus_addr;
			return 0;
		}
	}

	return 1;
}

/*
 * pci_hose_phys_to_bus(): Convert physical address to bus address
 * @hose:	PCI hose of the root PCI controller
 * @phys_addr:	physical address to convert
 * @flags:	flags of pci regions
 * @return bus address if OK, 0 on error
 */
pci_addr_t pci_hose_phys_to_bus(struct pci_controller *hose,
				phys_addr_t phys_addr,
				unsigned long flags)
{
	pci_addr_t bus_addr = 0;
	int ret;

	if (!hose) {
		puts("pci_hose_phys_to_bus: invalid hose\n");
		return bus_addr;
	}

	/*
	 * if PCI_REGION_MEM is set we do a two pass search with preference
	 * on matches that don't have PCI_REGION_SYS_MEMORY set
	 */
	if ((flags & PCI_REGION_TYPE) == PCI_REGION_MEM) {
		ret = __pci_hose_phys_to_bus(hose, phys_addr,
				flags, PCI_REGION_SYS_MEMORY, &bus_addr);
		if (!ret)
			return bus_addr;
	}

	ret = __pci_hose_phys_to_bus(hose, phys_addr, flags, 0, &bus_addr);

	if (ret)
		puts("pci_hose_phys_to_bus: invalid physical address\n");

	return bus_addr;
}

pci_dev_t pci_find_device(unsigned int vendor, unsigned int device, int index)
{
	struct pci_device_id ids[2] = { {}, {0, 0} };

	ids[0].vendor = vendor;
	ids[0].device = device;

	return pci_find_devices(ids, index);
}

pci_dev_t pci_hose_find_devices(struct pci_controller *hose, int busnum,
				struct pci_device_id *ids, int *indexp)
{
	int found_multi = 0;
	u16 vendor, device;
	u8 header_type;
	pci_dev_t bdf;
	int i;

	for (bdf = PCI_BDF(busnum, 0, 0);
	     bdf < PCI_BDF(busnum + 1, 0, 0);
	     bdf += PCI_BDF(0, 0, 1)) {
		if (pci_skip_dev(hose, bdf))
			continue;

		if (!PCI_FUNC(bdf)) {
			pci_read_config_byte(bdf, PCI_HEADER_TYPE,
					     &header_type);
			found_multi = header_type & 0x80;
		} else {
			if (!found_multi)
				continue;
		}

		pci_read_config_word(bdf, PCI_VENDOR_ID, &vendor);
		pci_read_config_word(bdf, PCI_DEVICE_ID, &device);

		for (i = 0; ids[i].vendor != 0; i++) {
			if (vendor == ids[i].vendor &&
			    device == ids[i].device) {
				if ((*indexp) <= 0)
					return bdf;

				(*indexp)--;
			}
		}
	}

	return -1;
}

pci_dev_t pci_find_class(uint find_class, int index)
{
	int bus;
	int devnum;
	pci_dev_t bdf;
	uint32_t class;

	for (bus = 0; bus <= pci_last_busno(); bus++) {
		for (devnum = 0; devnum < PCI_MAX_PCI_DEVICES - 1; devnum++) {
			pci_read_config_dword(PCI_BDF(bus, devnum, 0),
					      PCI_CLASS_REVISION, &class);
			if (class >> 16 == 0xffff)
				continue;

			for (bdf = PCI_BDF(bus, devnum, 0);
					bdf <= PCI_BDF(bus, devnum,
						PCI_MAX_PCI_FUNCTIONS - 1);
					bdf += PCI_BDF(0, 0, 1)) {
				pci_read_config_dword(bdf, PCI_CLASS_REVISION,
						      &class);
				class >>= 8;

				if (class != find_class)
					continue;
				/*
				 * Decrement the index. We want to return the
				 * correct device, so index is 0 for the first
				 * matching device, 1 for the second, etc.
				 */
				if (index) {
					index--;
					continue;
				}
				/* Return index'th controller. */
				return bdf;
			}
		}
	}

	return -ENODEV;
}
#endif /* !CONFIG_DM_PCI || CONFIG_DM_PCI_COMPAT */
