/* 
 * IBM PowerPC Virtual I/O Infrastructure Support.
 *
 * Dave Engebretsen engebret@us.ibm.com
 *    Copyright (c) 2003 Dave Engebretsen
 *
 *      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; either version
 *      2 of the License, or (at your option) any later version.
 */

#include <linux/init.h>
#include <linux/console.h>
#include <linux/pci.h>
#include <linux/version.h>
#include <asm/rtas.h>
#include <asm/pci_dma.h>
#include <asm/dma.h>
#include <asm/ppcdebug.h>
#include <asm/vio.h>

extern struct TceTable *build_tce_table(struct TceTable *tbl);

extern dma_addr_t get_tces(struct TceTable *, unsigned order, 
			   void *page, unsigned numPages, int direction);
extern void tce_free(struct TceTable *tbl, dma_addr_t dma_addr, 
		     unsigned order, unsigned num_pages);

const struct vio_device_id *
vio_match_device(const struct vio_device_id *ids, const struct vio_dev *dev);

#define VIO_VENDOR_ID_IBM		0x1014
#define VIO_DEVICE_ID_IBM_LAN		0x0001
#define VIO_DEVICE_ID_IBM_SCSI		0x0002
#define VIO_DEVICE_ID_IBM_SCSI_HOST	0x0004
#define VIO_DEVICE_ID_IBM_VTY    	0x0008
#define VIO_DEVICE_ID_IBM_VTY_HOST	0x0010

struct vio_bus vio_bus;

LIST_HEAD(vio_devices);

/*
 * vio_register_driver - register a new vio driver
 *
 * Adds the driver structure to the list of registered drivers
 * Returns the number of vio devices which were claimed by the driver
 * during registration.  The driver remains registered even if the
 * return value is zero.
 */
int
vio_register_driver(struct vio_driver *drv)
{
        int count = 0;
	struct list_head *tmp;

	/* 
	 * Walk the vio_bus, find devices for this driver, and
	 * call back into the driver probe interface.
	 */
	list_for_each(tmp,&(vio_bus.devices)) {
                struct vio_dev *dev = list_entry(tmp, struct vio_dev, bus_list);

		if(drv && vio_match_device(drv->id_table, dev)) {
			dev->driver = drv;
			drv->probe(dev, NULL); 
			count++;
		}
	}

        return count;
}

/*
 * vio_match_device - Tell if a VIO device structure has a matching VIO device 
 * id structure
 * @ids: array of VIO device id structures to search in
 * @dev: the VIO device structure to match against
 *
 * Used by a driver to check whether a VIO device present in the
 * system is in its list of supported devices.Returns the matching
 * vio_device_id structure or NULL if there is no match.
 */
const struct vio_device_id *
vio_match_device(const struct vio_device_id *ids, const struct vio_dev *dev)
{
        while (ids->vendor || ids->subvendor || ids->class_mask) {
                if ((ids->vendor == dev->vendor) &&
                    (ids->device == dev->device))
                        return ids;
                ids++;
        }
        return NULL;
}

int __init
vio_bus_init(void)
{
	struct device_node *node_vroot, *node_vdev;
	int *ip;
	struct vio_dev *dev;
	struct TceTable *newTceTable;
	unsigned int *dma_window;
	unsigned int *ireg, ilen;
	unsigned short devtype;
	char *loccode;
	int loccode_len;
	char *compatible;

	printk("vio_bus_init: start\n");

	INIT_LIST_HEAD(&vio_bus.devices);

	/* 
	 * Create device node entries for each virtual device
	 * identified in the device tree.
	 * Functionally takes the place of pci_scan_bus
	 */
	node_vroot = find_devices("vdevice");
	if(node_vroot) {
		ip = (int *)get_property(node_vroot, "name", NULL);
		if (ip == NULL) {
			printk(KERN_ERR "vio_bus_init: no name\n");
		}
		node_vdev = node_vroot->child;
		while(node_vdev) {
    		        int has_dma_window, has_interrupts;
		        printk(KERN_INFO "vio_bus_init: processing %p\n", node_vdev);

			ip = (int *)get_property(node_vdev, "name", NULL);
			if (ip == NULL) {
				printk(KERN_ERR "vio_bus_init: no name\n");
			        node_vdev = node_vdev->sibling;
				continue;
			}

			if(strncmp((char *)ip, "l-lan", 5) == 0) {
				devtype = VIO_DEVICE_ID_IBM_LAN;
				has_dma_window = has_interrupts = 1;
			} else if (strncmp((char *)ip, "v-scsi-host", 11) == 0) {
				devtype = VIO_DEVICE_ID_IBM_SCSI_HOST;
				has_dma_window = has_interrupts = 1;
			} else if (strncmp((char *)ip, "v-scsi", 5) == 0) {
				devtype = VIO_DEVICE_ID_IBM_SCSI;
				has_dma_window = has_interrupts = 1;
			} else if (strncmp((char *)ip, "vty-server", 10) == 0) {
				devtype = VIO_DEVICE_ID_IBM_VTY_HOST;
				has_dma_window = 0;
				has_interrupts = 1;
			} else if (strncmp((char *)ip, "vty", 3) == 0) {
				devtype = VIO_DEVICE_ID_IBM_VTY;
				has_dma_window = has_interrupts = 0;
			} else {
			        printk(KERN_INFO "vio_bus_init: skipping %s\n", (char*)ip);
			        node_vdev = node_vdev->sibling;
				continue;
			}

			compatible = (char*)get_property(node_vdev, 
							 "compatible", NULL);
			if(compatible == NULL) {
				printk(KERN_ERR "vio_bus_init: no model\n");
				node_vdev = node_vdev->sibling;
				continue;
			}

			loccode = (char*)get_property(node_vdev, 
						      "ibm,loc-code", NULL);
			if(loccode == NULL || ((loccode_len = strlen(loccode)) >= sizeof(dev->name))) {
				printk(KERN_ERR "vio_bus_init: no location code or location code too long\n");
				loccode = NULL;
				loccode_len = 0;
			}

			dev = kmalloc(sizeof(*dev), GFP_KERNEL);
			memset(dev, 0, sizeof(*dev));
			dev->vendor = VIO_VENDOR_ID_IBM;
			dev->device = devtype;
			/* 
			 * Match what the PCI code does - the device name is 
			 * location code + type 
			 */
			memset(dev->name,'\0', sizeof(dev->name));

			if(loccode) {
				strcpy(dev->name, loccode);
			}

			if(loccode_len + 2 < sizeof(dev->name)) {
				/* add on the type if we have room for it */
				dev->name[loccode_len] = ' ';
				strncpy(dev->name + loccode_len + 1, 
					compatible, 
					sizeof(dev->name) - loccode_len - 1);
			}

			ireg = (unsigned int *)get_property(node_vdev, 
							    "reg", &ilen);
			if(!ireg) {
				printk(KERN_WARNING "Can't find %s reg property\n", (char *)ip);
			        node_vdev = node_vdev->sibling;
				continue;
			}

			dev->unit_address = *ireg;

			/* 
			 * The bus_id needs to be unique for all devices 
			 * on the bus - use the reg property
			 */
			sprintf(dev->bus_id, "%x", *ireg);

			if(has_dma_window) {
				dma_window = (unsigned int *)get_property(node_vdev, "ibm,my-dma-window",0);
			}
			if(!dma_window) {
				printk(KERN_WARNING "Can't find %s dma-window property\n", (char*)ip);
				node_vdev = node_vdev->sibling;
				continue;
			}
			dev->liobn = dma_window[0];

			if(has_interrupts) {

				ireg = (unsigned int *) get_property(node_vdev, 
								     "interrupts", 0);
				if(!ireg) {
					printk(KERN_WARNING "Can't find %s interrupts property\n", (char *)ip);
					node_vdev = node_vdev->sibling;
					continue;
				}
				dev->irq = irq_offset_up(*ireg);

			}

			switch(devtype) {
			case VIO_DEVICE_ID_IBM_LAN:

				ireg = (unsigned int *) get_property(node_vdev, 
								     "local-mac-address", 0);
				if(!ireg) {
					printk(KERN_WARNING "Can't find l-lan local-mac-address property\n");
					node_vdev = node_vdev->sibling;
					continue;
				}
				dev->mac_address = 0;
				memcpy(((char *)&(dev->mac_address))+2, ireg, 6);
				printk(KERN_INFO "vio_bus_init: vio_mac %016lx\n", dev->mac_address);

				ireg = (unsigned int *) get_property(node_vdev, 
								     "ibm,mac-address-filters", 0);
				if(!ireg) {
					printk(KERN_WARNING "Can't find l-lan ibm,mac-address-filters property\n");
					node_vdev = node_vdev->sibling;
					continue;
				}
				dev->mcastFilterSize = *(unsigned int *)ireg;
				break;
			case VIO_DEVICE_ID_IBM_SCSI_HOST:
				dev->riobn = dma_window[5];
				break;
			case VIO_DEVICE_ID_IBM_SCSI:
				break;
			case VIO_DEVICE_ID_IBM_VTY_HOST:
				break;
			case VIO_DEVICE_ID_IBM_VTY:
				/* need to check if this device is the console */
#if 0
				/* DRENG TODO new console interface support */
				if(prom.console == node_vdev->node) {
					printk(KERN_INFO "vio_bus_init: this device is the console\n");	
					dev->isConsole=1;
				}
#endif
				break;
			default:
				break;
			}

			if(has_dma_window) {
				unsigned long offset;
				unsigned int size; 

				/* offset for VIO should always be 0 */
				offset = ((((unsigned long)dma_window[1] << 32) | 
					   (unsigned long)dma_window[2]) 
					  >> PAGE_SHIFT);
				size = ((((((unsigned long)dma_window[3] << 32) |
					   (unsigned long)dma_window[4])
					  >> PAGE_SHIFT) << 3) >> PAGE_SHIFT);

				newTceTable = (struct TceTable *)
					kmalloc(sizeof(struct TceTable), GFP_KERNEL);
				/* TCE table size - measured in units of pages of tce table */
				newTceTable->size        = size;
				newTceTable->busNumber   = 0;
				newTceTable->startOffset = offset;
				newTceTable->index       = dev->liobn;
				newTceTable->tceType     = TCE_VB;

				dev->tce_table = build_tce_table(newTceTable);
			}
			
			printk(KERN_INFO "vio_bus_init: registering %s (%s)\n", 
			       dev->name, dev->bus_id);
			list_add_tail(&dev->global_list, &vio_devices);
			list_add_tail(&dev->bus_list, &(vio_bus.devices));
			dev->bus = &vio_bus;

			node_vdev = node_vdev->sibling;
		};
	} else {
		printk("vio_bus_init: no /vdevice node\n");
	}
	
	printk("vio_bus_init: done\n");

	vio_register_driver(NULL);
	return 0;
}

__initcall(vio_bus_init);

dma_addr_t vio_map_single(struct vio_dev *dev, void *vaddr, 
			  size_t size, int direction )
{
	struct TceTable * tbl;
	dma_addr_t dma_handle = NO_TCE;
	unsigned long uaddr;
	unsigned order, nPages;

	if(direction == PCI_DMA_NONE) BUG();
	
	uaddr = (unsigned long)vaddr;
	nPages = PAGE_ALIGN( uaddr + size ) - ( uaddr & PAGE_MASK );
	order = get_order( nPages & PAGE_MASK );
	nPages >>= PAGE_SHIFT;
	
 	/* Client asked for way to much space.  This is checked later anyway */
	/* It is easier to debug here for the drivers than in the tce tables.*/
 	if(order >= NUM_TCE_LEVELS) {
 		printk("VIO_DMA: vio_map_single size to large: 0x%lx \n",size);
 		return NO_TCE;
 	}

	tbl = dev->tce_table;

	if(tbl) {
		dma_handle = get_tces(tbl, order, vaddr, nPages, direction);
		dma_handle |= (uaddr & ~PAGE_MASK);
	}

	return dma_handle;
}

void vio_unmap_single(struct vio_dev *dev, dma_addr_t dma_handle, 
		      size_t size, int direction)
{
	struct TceTable * tbl;
	unsigned order, nPages;
	
	if (direction == PCI_DMA_NONE) BUG();

	nPages = PAGE_ALIGN( dma_handle + size ) - ( dma_handle & PAGE_MASK );
	order = get_order( nPages & PAGE_MASK );
	nPages >>= PAGE_SHIFT;

 	/* Client asked for way to much space.  This is checked later anyway */
	/* It is easier to debug here for the drivers than in the tce tables.*/
 	if(order >= NUM_TCE_LEVELS) {
 		printk("VIO_DMA: vio_unmap_single 0x%lx size to large: 0x%lx \n",(unsigned long)dma_handle,(unsigned long)size);
 		return;
 	}
	
	tbl = dev->tce_table; 
	if(tbl) tce_free(tbl, dma_handle, order, nPages);
}

int vio_map_sg(struct vio_dev *vdev, struct scatterlist *sglist, int nelems,
	       int direction)
{
	int i;

	for (i = 0; i < nelems; i++) {
		void *vaddr = page_address(sglist->page) + sglist->offset;

		sglist->dma_address = vio_map_single(vdev, vaddr,
						     sglist->length,
						     direction);
		sglist->dma_length = sglist->length;
		sglist++;
	}

	return nelems;
}

void vio_unmap_sg(struct vio_dev *vdev, struct scatterlist *sglist, int nelems,
		  int direction)
{
	while (nelems--) {
		vio_unmap_single(vdev, sglist->dma_address,
				 sglist->dma_length, direction);
		sglist++;
	}
}

void *vio_alloc_consistent(struct vio_dev *dev, size_t size,
			   dma_addr_t *dma_handle)
{
	struct TceTable * tbl;
	void *ret = NULL;
	unsigned order, nPages;
	dma_addr_t tce;

	size = PAGE_ALIGN(size);
	order = get_order(size);
	nPages = 1 << order;

 	/* Client asked for way to much space.  This is checked later anyway */
	/* It is easier to debug here for the drivers than in the tce tables.*/
 	if(order >= NUM_TCE_LEVELS) {
 		printk("VIO_DMA: vio_alloc_consistent size to large: 0x%lx \n",size);
 		return (void *)NO_TCE;
 	}

	tbl = dev->tce_table;

	if ( tbl ) {
		/* Alloc enough pages (and possibly more) */
		ret = (void *)__get_free_pages( GFP_ATOMIC, order );
		if ( ret ) {
			/* Page allocation succeeded */
			memset(ret, 0, nPages << PAGE_SHIFT);
			/* Set up tces to cover the allocated range */
			tce = get_tces( tbl, order, ret, nPages, PCI_DMA_BIDIRECTIONAL );
			if ( tce == NO_TCE ) {
				PPCDBG(PPCDBG_TCE, "vio_alloc_consistent: get_tces failed\n" );
				free_pages( (unsigned long)ret, order );
				ret = NULL;
			}
			else
				{
					*dma_handle = tce;
				}
		}
		else PPCDBG(PPCDBG_TCE, "vio_alloc_consistent: __get_free_pages failed for order = %d\n", order);
	}
	else PPCDBG(PPCDBG_TCE, "vio_alloc_consistent: get_tce_table failed for 0x%016lx\n", dev);

	PPCDBG(PPCDBG_TCE, "\tvio_alloc_consistent: dma_handle = 0x%16.16lx\n", *dma_handle);	
	PPCDBG(PPCDBG_TCE, "\tvio_alloc_consistent: return     = 0x%16.16lx\n", ret);	
	return ret;
}

void vio_free_consistent(struct vio_dev *dev, size_t size,
			 void *vaddr, dma_addr_t dma_handle)
{
	struct TceTable * tbl;
	unsigned order, nPages;
	
	PPCDBG(PPCDBG_TCE, "vio_free_consistent:\n");
	PPCDBG(PPCDBG_TCE, "\tdev = 0x%16.16lx, size = 0x%16.16lx, dma_handle = 0x%16.16lx, vaddr = 0x%16.16lx\n", dev, size, dma_handle, vaddr);	

	size = PAGE_ALIGN(size);
	order = get_order(size);
	nPages = 1 << order;

 	/* Client asked for way to much space.  This is checked later anyway */
	/* It is easier to debug here for the drivers than in the tce tables.*/
 	if(order >= NUM_TCE_LEVELS) {
 		printk("PCI_DMA: pci_free_consistent size to large: 0x%lx \n",size);
 		return;
 	}
	
	tbl = dev->tce_table; 

	if ( tbl ) {
		tce_free(tbl, dma_handle, order, nPages);
		free_pages( (unsigned long)vaddr, order );
	}
}

