/*
 * 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.
 */

/**
 * @file	drivers/pci_init.c
 * @brief	PCI driver (init)
 * @author	T. Shinagawa
 */

/*
 * Copyright (c) 2010-2013 Yuichi Watanabe
 */

#include <core/acpi.h>
#include <core/initfunc.h>
#include <core/mm.h>
#include <core/rm.h>
#include <core/panic.h>
#include <core/printf.h>
#include <core/string.h>
#include <core/vm.h>
#include <io/io.h>
#include <io/pci.h>
#include "pci_internal.h"

#define PCI_MAX_BUSES		256
#define PCI_MAX_DEVICES		32
#define PCI_MAX_FUNCS		8

static const char driver_name[] = "pci_driver";

static u8 pci_scan_bus(struct pci_device *parent, u16 seg, u8 bn);

u32
pci_get_base_address_mask(struct pci_device *dev, int offset)
{
	u32 mask;
	u32 tmp;

	tmp = pci_read_config32(dev, offset);
	pci_write_config32(dev, offset, 0xFFFFFFFF);
	mask = pci_read_config32(dev, offset);
	pci_write_config32(dev, offset, tmp);
	return mask;
}

static void
pci_read_config_space(struct pci_device *dev)
{
	dev->vendor_id = pci_read_config16(dev, PCI_CONFIG_VENDOR_ID);
	dev->device_id = pci_read_config16(dev, PCI_CONFIG_DEVICE_ID);
	dev->class_code = pci_read_config32(dev, PCI_CONFIG_CLASS_CODE - 1)
		>> 8;
	dev->header_type = pci_read_config8(dev, PCI_CONFIG_HEADER_TYPE);
	if (dev->type != PCI_CONFIG_HEADER_TYPE_1) {
		return;
	}
	dev->sec_bus = pci_read_config8(dev,
					PCI_CONFIG_SUBORDINATE_BUS_NUMBER);
	dev->sub_bus = pci_read_config8(dev,
					PCI_CONFIG_SUBORDINATE_BUS_NUMBER);
}

static void
pci_init_resource_window(struct pci_device *dev)
{
	struct resource *resource;
	u64 reg_val, base, limit;

	if (dev->type != PCI_CONFIG_HEADER_TYPE_1) {
		return;
	}

	/*
	 * Iitialize I/O window.
	 */
	resource = dev->resource + PCI_RESOURCE_IO_WINDOW_INDEX;
	reg_val = pci_read_config8(dev, PCI_CONFIG_IO_BASE);
	base = (reg_val & PCI_CONFIG_IO_BASE_ADDRESS_MASK) << 8;
	if ((reg_val & PCI_CONFIG_IO_BASE_CAP_MASK)
	    == PCI_CONFIG_IO_BASE_32_BIT) {
		reg_val = pci_read_config16(dev, PCI_CONFIG_IO_BASE_UPPER);
		base |= reg_val << 16;
	}
	reg_val = pci_read_config8(dev, PCI_CONFIG_IO_LIMIT);
	limit = (((reg_val & PCI_CONFIG_IO_LIMIT_ADDRESS_MASK)
		  + PCI_CONFIG_IO_LIMIT_CAP_MASK) << 8) + 0xff;
	if ((reg_val & PCI_CONFIG_IO_LIMIT_CAP_MASK)
	    == PCI_CONFIG_IO_LIMIT_32_BIT) {
		reg_val = pci_read_config16(dev, PCI_CONFIG_IO_LIMIT_UPPER);
		limit |= reg_val << 16;
	}
	if (base <= limit) {
		rm_init_resource(resource, base, limit, RESOURCE_TYPE_IO,
				 PCI_RESOURCE_WINDOW, dev->name);
	}

	/*
	 * Iitialize memory window.
	 */
	resource = dev->resource + PCI_RESOURCE_MEMORY_WINDOW_INDEX;
	reg_val = pci_read_config16(dev, PCI_CONFIG_MEMORY_BASE);
	base = (reg_val & PCI_CONFIG_MEMORY_BASE_ADDRESS_MASK) << 16;
	reg_val = pci_read_config16(dev, PCI_CONFIG_MEMORY_LIMIT);
	limit = (((reg_val & PCI_CONFIG_MEMORY_LIMIT_ADDRESS_MASK)
		  + PCI_CONFIG_MEMORY_LIMIT_RESERVED_MASK) << 16) + 0xffff;
	if (base <= limit) {
		rm_init_resource(resource, base, limit, RESOURCE_TYPE_MMIO,
				 PCI_RESOURCE_WINDOW, dev->name);
	}

	/*
	 * Iitialize prefetchable memory window.
	 */
	resource = dev->resource + PCI_RESOURCE_PREFETCHABLE_WINDOW_INDEX;
	reg_val = pci_read_config16(dev, PCI_CONFIG_PREFETCHABLE_BASE);
	base = ((reg_val & PCI_CONFIG_PREFETCHABLE_BASE_ADDRESS_MASK) << 16);
	if ((reg_val & PCI_CONFIG_PREFETCHABLE_BASE_CAP_MASK)
	    == PCI_CONFIG_PREFETCHABLE_BASE_64_BIT) {
		reg_val = pci_read_config32(dev,
				     PCI_CONFIG_PREFETCHABLE_BASE_UPPER);
		base |= reg_val << 32;
	}
	reg_val = pci_read_config16(dev,
				    PCI_CONFIG_PREFETCHABLE_LIMIT);
	limit = (((reg_val & PCI_CONFIG_PREFETCHABLE_LIMIT_ADDRESS_MASK)
		  + PCI_CONFIG_PREFETCHABLE_LIMIT_CAP_MASK) << 16) + 0xffff;
	if ((reg_val & PCI_CONFIG_PREFETCHABLE_LIMIT_CAP_MASK)
	    == PCI_CONFIG_PREFETCHABLE_LIMIT_64_BIT) {
		reg_val = pci_read_config32(dev,
				     PCI_CONFIG_PREFETCHABLE_LIMIT_UPPER);
		limit |= (reg_val << 32);
	}
	if (base <= limit) {
		rm_init_resource(resource, base, limit, RESOURCE_TYPE_MMIO,
			PCI_RESOURCE_WINDOW | PCI_RESOURCE_PREFETCHABLE,
			dev->name);
	}

	reg_val = pci_read_config16(dev, PCI_CONFIG_BRIDGE_CONTROL);
	if ((reg_val & PCI_CONFIG_VGA_ENABLE) == 0) {
		return;
	}
	resource = dev->resource + PCI_RESOURCE_VGA_MEM;
	rm_init_resource(resource, 0xa0000, 0xbffff,
			 RESOURCE_TYPE_MMIO,
			 PCI_RESOURCE_WINDOW | PCI_RESOURCE_PREFETCHABLE,
			 dev->name);

	resource = dev->resource + PCI_RESOURCE_VGA_IO1;
	rm_init_resource(resource, 0x3b0, 0x3bb,
			 RESOURCE_TYPE_IO, PCI_RESOURCE_WINDOW,
			 dev->name);
	resource = dev->resource + PCI_RESOURCE_VGA_IO2;
	rm_init_resource(resource, 0x3c0, 0x3df,
			 RESOURCE_TYPE_IO, PCI_RESOURCE_WINDOW,
			 dev->name);
}

static void
pci_init_resources(struct pci_device *dev)
{
	int i, bar_num;
	u32 reg_val;
	int offset;
	u32 mask;
	u32 data;
	phys_t start;
	struct resource *resource;

	switch (dev->type) {
	case PCI_CONFIG_HEADER_TYPE_0:
		bar_num = PCI_CONFIG_BAR_NUM;
		break;
	case PCI_CONFIG_HEADER_TYPE_1:
		bar_num = PCI_CONFIG_HEADER1_BAR_NUM;
		break;
	default:
		return;
	}

	for (i = 0; i < bar_num; i++) {
		offset = PCI_CONFIG_BASE_ADDRESS0 + i * 4;
		resource = dev->resource + i;

		reg_val = pci_read_config32(dev, offset);
		if (reg_val == 0x00000000 || reg_val == 0xFFFFFFFF) {
			/* Unimplemented register */
			continue;
		}
		mask = pci_get_base_address_mask(dev, offset);
		if ((reg_val & PCI_CONFIG_BAR_SPACEMASK) ==
		    PCI_CONFIG_BAR_IOSPACE) {
			start = reg_val & PCI_CONFIG_BAR_IOMASK;
			if (start == 0x0) {
				continue;
			}

			mask &= PCI_CONFIG_BAR_IOMASK;
			mask |= 0xFFFF0000;
			rm_init_resource(resource,
					 start,
					 start + (~mask),
					 RESOURCE_TYPE_IO,
					 0,
					 dev->name);
		} else {
			if (reg_val & PCI_CONFIG_BAR_PREFETCHABLE) {
				data = PCI_RESOURCE_PREFETCHABLE;
			} else {
				data = 0;
			}
			start = reg_val & PCI_CONFIG_BAR_MEMMASK;
			if ((reg_val & PCI_CONFIG_BAR_MEMTYPE_MASK) ==
					   PCI_CONFIG_BAR_MEMTYPE_64) {
				i++;
				offset += 4;
				reg_val = pci_read_config32(dev,
							    offset);
				start |= (u64)reg_val << 32;
			}
			if (start == 0x0) {
				continue;
			}

			mask &= PCI_CONFIG_BAR_MEMMASK;

			rm_init_resource(resource,
					 start,
					 start + (~mask),
					 RESOURCE_TYPE_MMIO,
					 data,
					 dev->name);
		}
	}

	pci_init_resource_window(dev);

	resource = dev->resource + PCI_RESOURCE_EXPANTION_ROM_INDEX;
	reg_val = pci_read_config32(dev, PCI_CONFIG_EXPANSION_ROM);
	if (reg_val != 0x00000000 && reg_val != 0xFFFFFFFF) {
		start = reg_val & PCI_CONFIG_ROM_MEMMASK;
		mask = pci_get_base_address_mask(dev,
						 PCI_CONFIG_EXPANSION_ROM) &
			PCI_CONFIG_ROM_MEMMASK;
		rm_init_resource(resource,
				 start,
				 start + (~mask),
				 RESOURCE_TYPE_MMIO,
				 0,
				 dev->name);
	}
}

static struct pci_device
*pci_new_device(u8 bus, u8 devfn)
{
	struct pci_device *dev;

	dev = (struct pci_device *)alloc(sizeof(struct pci_device));
	if (dev != NULL) {
		memset(dev, 0, sizeof(*dev));
		dev->bus_no = bus;
		dev->devfn = devfn;
		dev->driver = NULL;
		snprintf(dev->name, PCI_LOCATION_STR_SIZE, PCI_LOCATION_FORMAT,
			 PCI_LOCATION_VALUE(dev));
		pci_read_config_space(dev);
		pci_init_resources(dev);
	}
	return dev;
}

static u8
pci_scan_buses_behind_bridge(struct pci_device *bridge)
{
	int bn;

	for (bn = bridge->sec_bus; bn <= bridge->sub_bus; bn++) {
		bn = pci_scan_bus(bridge, bridge->seg, bn);
	}

	return bn;
}

static u8
pci_scan_bus(struct pci_device *parent, u16 seg, u8 bn)
{
	int dn, fn;
	u16 data;
	struct pci_device *dev;
	u8 ret = bn;

	if (seg != 0) {
		/*
		 * Currently we does not support multi segments.
		 */
		return bn;
	}

	for (dn = 0; dn < PCI_MAX_DEVICES; dn++) {
		for (fn = 0; fn < PCI_MAX_FUNCS; fn++) {
			data = pci_read_config_16(bn, PCI_DEVFN(dn, fn), 0);
			if (data == 0xFFFF) /* not exist */
				continue;
			dev = pci_new_device(bn, PCI_DEVFN(dn, fn));
			if (dev == NULL) {
				panic("pci_scan_bus: Can't alloc memory");
			}

			pci_register_device(parent, dev);
			if (dev->type == PCI_CONFIG_HEADER_TYPE_1) {
				ret = pci_scan_buses_behind_bridge(dev);
			}

			if (fn == 0 && dev->multi_function == 0)
				break;
		}
	}
	return ret;
}

static void
pci_find_devices(void)
{
	int bn;
	struct pci_mmconfig *mmconfig;

	if (LIST_HEAD(pci_mmconfig_list)) {
		LIST_FOREACH(pci_mmconfig_list, mmconfig) {
			for (bn = mmconfig->start_bus_no;
			     bn <= mmconfig->end_bus_no; bn++) {
				bn = pci_scan_bus(NULL, mmconfig->seg, bn);
			}
		}
	} else {
		for (bn = 0; bn < PCI_MAX_BUSES; bn++) {
			bn = pci_scan_bus(NULL, 0, bn);
		}
	}
	return;
}

static void
pci_init(void)
{
	pci_init_root_resources();
	pci_init_config();
	pci_find_devices();
	pci_handle = vm_alloc_driver_data(sizeof(struct pci_vm_data));
}

BUS_INIT(pci_init);
