/*
 * Copyright (c) 2007, 2008 University of Tsukuba
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of the University of Tsukuba nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
/*
 * Copyright (c) 2010-2014 Yuichi Watanabe
 */

#include <core/assert.h>
#include <core/initfunc.h>
#include <core/printf.h>
#include <core/mm.h>
#include <core/mmio.h>
#include <core/spinlock.h>
#include <core/vm.h>
#include <io/io.h>
#include <io/pci.h>
#include "pci_internal.h"

static spinlock_t	pci_config_lock = SPINLOCK_INITIALIZER;
LIST_DEFINE_HEAD(pci_mmconfig_list);

static pci_config_address_t
pci_make_config_address(u8 bus, u8 devfn, int reg)
{
	pci_config_address_t addr;

	addr.allow = 1;
	addr.reserved = 0;
	addr.bus_no = bus;
	addr.devfn = devfn;
	addr.reg_no = reg;
	addr.type = 0;
	return addr;
}

#define DEFINE_pci_read_config_data_without_lock(size)		\
static inline u##size pci_read_config_data##size##_without_lock(pci_config_address_t addr, int offset) \
{								\
	u##size data;						\
	out32(PCI_CONFIG_ADDR_PORT, addr.value);		\
	in##size(PCI_CONFIG_DATA_PORT + offset, &data);		\
	return data;						\
}

#define DEFINE_pci_write_config_data_without_lock(size)		\
static inline void pci_write_config_data##size##_without_lock(pci_config_address_t addr, int offset, u##size data) \
{								\
	out32(PCI_CONFIG_ADDR_PORT, addr.value);		\
	out##size(PCI_CONFIG_DATA_PORT + offset, data);		\
}

DEFINE_pci_read_config_data_without_lock(8)
DEFINE_pci_read_config_data_without_lock(16)
DEFINE_pci_read_config_data_without_lock(32)
DEFINE_pci_write_config_data_without_lock(8)
DEFINE_pci_write_config_data_without_lock(16)
DEFINE_pci_write_config_data_without_lock(32)

#define MMCONFIG_OFFSET(start_bus_no, bus_no, devfn, offset) \
	((bus_no - start_bus_no) * 32 * 8 * 4096 + devfn * 4096 + offset) \

#define DEFINE_pci_read_mmconfig(size)					\
	u##size pci_read_mmconfig_##size(u16 seg, u8 bus, u8 devfn,	\
					 pci_off_t offset)		\
	{								\
		struct pci_mmconfig *mmconfig;				\
		LIST_FOREACH(pci_mmconfig_list, mmconfig) {		\
			if (mmconfig->seg != seg ||			\
			    mmconfig->start_bus_no > bus ||		\
			    mmconfig->end_bus_no < bus) {		\
				continue;				\
			}						\
			return *(vu##size*)((ulong)mmconfig->virt_base + \
				MMCONFIG_OFFSET(mmconfig->start_bus_no, \
					bus, devfn, offset));		\
		}							\
		printf("Out of range of MMCONFIG: %04x:%02x:%02x.%01x %x\n", \
		       seg, bus, PCI_DEV_NO(devfn), PCI_FUNC_NO(devfn),	\
		       offset);						\
		return (u##size)((i##size)-1);				\
	}

#define DEFINE_pci_write_mmconfig(size)					\
	void pci_write_mmconfig_##size(u16 seg, u8 bus, u8 devfn,	\
					pci_off_t offset, u##size data)	\
	{								\
		struct pci_mmconfig *mmconfig;				\
		LIST_FOREACH(pci_mmconfig_list, mmconfig) {		\
			if (mmconfig->seg != seg ||			\
			    mmconfig->start_bus_no > bus ||		\
			    mmconfig->end_bus_no < bus) {		\
				continue;				\
			}						\
			*(vu##size*)((ulong)mmconfig->virt_base +	\
				MMCONFIG_OFFSET(mmconfig->start_bus_no, \
					bus, devfn, offset)) = data;	\
			return;						\
		}							\
		printf("Out of range of MMCONFIG: %04x:%02x:%02x.%01x %x\n", \
		       seg, bus, PCI_DEV_NO(devfn), PCI_FUNC_NO(devfn),	\
		       offset);						\
	}

DEFINE_pci_read_mmconfig(8)
DEFINE_pci_read_mmconfig(16)
DEFINE_pci_read_mmconfig(32)
DEFINE_pci_read_mmconfig(64)
DEFINE_pci_write_mmconfig(8)
DEFINE_pci_write_mmconfig(16)
DEFINE_pci_write_mmconfig(32)
DEFINE_pci_write_mmconfig(64)

#define DEFINE_pci_read_legacy_config(size)				\
	u##size pci_read_legacy_config_##size(u8 bus, u8 devfn,		\
					      pci_off_t offset)		\
	{								\
		u##size data;						\
		pci_config_address_t addr;				\
		if (offset > 0xff)					\
			return (u##size)((i##size)-1);			\
		addr =  pci_make_config_address(bus, devfn, offset >> 2); \
		offset &= 0x03;						\
		ASSERT(offset + size / 8 <= 4);				\
		spinlock_lock(&pci_config_lock);			\
		data = pci_read_config_data##size##_without_lock(addr,	\
							       offset);	\
		spinlock_unlock(&pci_config_lock);			\
		return data;						\
	}

#define DEFINE_pci_write_legacy_config(size)				\
	void pci_write_legacy_config_##size(u8 bus, u8 devfn,		\
					    pci_off_t offset,		\
					    u##size data)		\
	{								\
		pci_config_address_t addr;				\
		if (offset > 0xff)					\
			return;						\
		addr =  pci_make_config_address(bus, devfn, offset >> 2); \
		offset &= 0x03;						\
		ASSERT(offset + size / 8 <= 4);			\
		spinlock_lock(&pci_config_lock);			\
		pci_write_config_data##size##_without_lock(addr, offset,\
							   data);	\
		spinlock_unlock(&pci_config_lock);			\
	}

DEFINE_pci_read_legacy_config(8)
DEFINE_pci_read_legacy_config(16)
DEFINE_pci_read_legacy_config(32)
DEFINE_pci_write_legacy_config(8)
DEFINE_pci_write_legacy_config(16)
DEFINE_pci_write_legacy_config(32)

#define DEFINE_pci_read_config(size)					\
	u##size pci_read_config_##size(/* u16 seg, */ u8 bus, u8 devfn,	\
				       pci_off_t offset)		\
	{								\
		u16 seg = 0;						\
		if (LIST_HEAD(pci_mmconfig_list)) {			\
			return pci_read_mmconfig_##size(seg, bus, devfn,\
							offset);	\
		} else {						\
			ASSERT(seg == 0);				\
			return pci_read_legacy_config_##size(bus,	\
							     devfn,	\
							     offset);	\
		}							\
	}

#define DEFINE_pci_write_config(size)					\
	void pci_write_config_##size(/* u16 seg, */ u8 bus, u8 devfn,	\
				     pci_off_t offset, u##size data)	\
	{								\
		u16 seg = 0;						\
		if (LIST_HEAD(pci_mmconfig_list)) {			\
			pci_write_mmconfig_##size(seg, bus, devfn,	\
						 offset, data);		\
		} else {						\
			ASSERT(seg == 0);				\
			pci_write_legacy_config_##size(bus, devfn,	\
						       offset, data);	\
		}							\
	}

DEFINE_pci_read_config(8)
DEFINE_pci_read_config(16)
DEFINE_pci_read_config(32)
DEFINE_pci_write_config(8)
DEFINE_pci_write_config(16)
DEFINE_pci_write_config(32)

u64
pci_read_config_64(/* u16 seg, */ u8 bus, u8 devfn, pci_off_t offset)
{
	u16 seg = 0;
	if (LIST_HEAD(pci_mmconfig_list)) {
		return pci_read_mmconfig_64(seg, bus, devfn, offset);
	}
	ASSERT(seg == 0);
	return (((u64)pci_read_config_32(bus, devfn, offset + 4) << 32) |
		pci_read_config_32(bus, devfn, offset));
}

void
pci_write_config_64(/* u16 seg, */ u8 bus, u8 devfn, pci_off_t offset, u64 data)
{
	u16 seg = 0;
	if (LIST_HEAD(pci_mmconfig_list)) {
		pci_write_mmconfig_64(seg, bus, devfn, offset, data);
	}
	ASSERT(seg == 0);
	pci_write_config_32(bus, devfn, offset + 4, data >> 32);
	pci_write_config_32(bus, devfn, offset, data);
}

u8
pci_read_config8(struct pci_device *dev, pci_off_t offset)
{
	return pci_read_config_8(dev->bus_no, dev->devfn, offset);
}

u16
pci_read_config16(struct pci_device *dev, pci_off_t offset)
{
	return pci_read_config_16(dev->bus_no, dev->devfn, offset);
}

u32
pci_read_config32(struct pci_device *dev, pci_off_t offset)
{
	return pci_read_config_32(dev->bus_no, dev->devfn, offset);
}

u64
pci_read_config64(struct pci_device *dev, pci_off_t offset)
{
	return pci_read_config_64(dev->bus_no, dev->devfn, offset);
}

void
pci_write_config8(struct pci_device *dev, pci_off_t offset, u8 data)
{
	pci_write_config_8(dev->bus_no, dev->devfn, offset, data);
}

void
pci_write_config16(struct pci_device *dev, pci_off_t offset, u16 data)
{
	pci_write_config_16(dev->bus_no, dev->devfn, offset, data);
}

void
pci_write_config32(struct pci_device *dev, pci_off_t offset, u32 data)
{
	pci_write_config_32(dev->bus_no, dev->devfn, offset, data);
}

void
pci_write_config64(struct pci_device *dev, pci_off_t offset, u64 data)
{
	pci_write_config_64(dev->bus_no, dev->devfn, offset, data);
}

size_t
pci_config_size(void)
{
	return LIST_HEAD(pci_mmconfig_list) ? PCI_MMCONFIG_SIZE : PCI_LEGACY_CONFIG_SIZE;
}

void
pci_init_config(void)
{
	struct acpi_mmconfig* acpi_mmconfig;
	struct pci_mmconfig* pci_mmconfig;

	for (acpi_mmconfig = acpi_get_next_mmconfig(NULL); acpi_mmconfig;
	     acpi_mmconfig = acpi_get_next_mmconfig(acpi_mmconfig)) {
		pci_mmconfig = (struct pci_mmconfig *)alloc(
			sizeof(* pci_mmconfig));
		if (!pci_mmconfig) {
			printf("pci_init_mmconfig: Can't alloc memory.\n");
			return;
		}
		pci_mmconfig->phys_base = acpi_mmconfig->base;
		pci_mmconfig->seg = acpi_mmconfig->seg;
		pci_mmconfig->start_bus_no = acpi_mmconfig->start_bus_no;
		pci_mmconfig->end_bus_no = acpi_mmconfig->end_bus_no;
		pci_mmconfig->size = (pci_mmconfig->end_bus_no
				      - pci_mmconfig->start_bus_no + 1)
				      * 4096 * 32 * 8;
		pci_mmconfig->virt_base = mapmem(MAPMEM_HPHYS | MAPMEM_WRITE
						 | MAPMEM_UC,
						 pci_mmconfig->phys_base,
						 pci_mmconfig->size);
		if (!pci_mmconfig->virt_base) {
			printf("pci_init_mmconfig: Can't map mmconfig area.\n");
			free(pci_mmconfig);
		}
		printf("MMCONFIG enabled: phys 0x%llx virt 0x%lx size 0x%llx "
		       "seg 0x%04x start bus 0x%02x end bus 0x%02x\n",
		       pci_mmconfig->phys_base,
		       (unsigned long)pci_mmconfig->virt_base,
		       pci_mmconfig->size,
		       pci_mmconfig->seg,
		       pci_mmconfig->start_bus_no,
		       pci_mmconfig->end_bus_no);
		LIST_APPEND(pci_mmconfig_list, pci_mmconfig);
	}
}

static int
pci_config_addr_handler(iotype_t type, ioport_t port, void *data)
{
	struct pci_vm_data *pci_data;

	pci_data = (struct pci_vm_data *)vm_get_driver_data(pci_handle);

	switch(type) {
	case IOTYPE_OUTL:
		pci_data->config_addr.value = *(u32 *)data;
		break;
	case IOTYPE_INL:
		*(u32 *)data = pci_data->config_addr.value;
		break;
	default:
		break;
	}
	return 1; /* no pass */
}

static int
pci_config_data_handler(iotype_t type, ioport_t port, void *data)
{
	struct pci_vm_data *pci_data;
	bool wr;
	ioport_t size;
	int ret;

	pci_data = (struct pci_vm_data *)vm_get_driver_data(pci_handle);

	if (!pci_data->config_addr.allow) {
		return 1; /* no pass */
	}
	ret = pci_check_assignment(pci_data->config_addr.bus_no,
				   pci_data->config_addr.devfn);
	switch (ret) {
	case PCI_NOT_ASSIGNED:
		/* nothing to do */
		break;
	case PCI_ASSIGNED:
		spinlock_lock(&pci_config_lock);
		out32(PCI_CONFIG_ADDR_PORT, pci_data->config_addr.value);
		do_io_pass(type, port, data);
		spinlock_unlock(&pci_config_lock);
		break;
	case PCI_DUMMY_FUNC0:
		iotype_to_size_wr(type, &size, &wr);
		return pci_dummy_func0((pci_data->config_addr.reg_no << 2)
				       + (port & 0x3), size, wr, data);
	}
	return 1; /* no pass */
}

static int
pci_mmconfig_access(void *data, phys_t gphys, bool wr, void *buf,
		     uint len, u32 flags)
{
	struct pci_mmconfig *mmconfig = (struct pci_mmconfig *)data;
	u16 bus, devfn;
	phys_t mmoffset;
	pci_off_t off;
	int ret;

	ASSERT(mmconfig->seg == 0);
	mmoffset = gphys - mmconfig->phys_base;
	bus = mmconfig->start_bus_no + ((mmoffset >> 20) & 0xff);
	devfn = mmoffset >> 12;
	off = mmoffset & 0xfff;

	ret = pci_check_assignment(bus, devfn);
	switch (ret) {
	case PCI_NOT_ASSIGNED:
		mmio_do_nothing(data, gphys, wr, buf, len, flags);
		break;
	case PCI_ASSIGNED:
		if (wr) {
			mmio_memcpy(mmconfig->virt_base + mmoffset, buf,
				    len);
		} else {
			mmio_memcpy(buf, mmconfig->virt_base + mmoffset,
				    len);
		}
		break;
	case PCI_DUMMY_FUNC0:
		return pci_dummy_func0(off, len, wr, buf);
	}
	return 1; /* emulated */
}

static void
pci_setup_config(void)
{
	struct pci_vm_data *pci_data;
	struct pci_mmconfig *mmconfig;
	int i;

	pci_data = vm_get_driver_data(pci_handle);
	pci_data->config_addr.value = 0;

	set_iofunc(PCI_CONFIG_ADDR_PORT, pci_config_addr_handler);
	for (i = 0; i < 4; i++) {
		set_iofunc(PCI_CONFIG_DATA_PORT + i,
			   pci_config_data_handler);
	}

	if (LIST_HEAD(pci_mmconfig_list)) {
		LIST_FOREACH(pci_mmconfig_list, mmconfig) {
			if (mmconfig->seg != 0) {
				/*
				 * Currently we does not support multi
				 * segments.
				 */
				continue;
			}
			mmio_register(mmconfig->phys_base, mmconfig->size,
				      pci_mmconfig_access, mmconfig);
		}
	}
}

BUS_SETUPVM(pci_setup_config);
