#include <u.h>
#include <libc.h>
#include <thread.h>
#include "dat.h"
#include "fns.h"

PCIDev *pcidevs;
PCIBar membars, iobars;

PCIDev *
mkpcidev(u32int bdf, u32int viddid, u32int clrev, int needirq)
{
	PCIDev *d;
	int n;
	
	d = emalloc(sizeof(PCIDev));
	d->bdf = bdf;
	d->viddid = viddid;
	d->clrev = clrev;
	d->next = pcidevs;
	d->irqno = needirq ? 0 : 0xff;
	for(n = 0; n < nelem(d->bar); n++){
		d->bar[n].d = d;
		d->bar[n].busnext = &d->bar[n];
		d->bar[n].busprev = &d->bar[n];
	}
	d->capalloc = 64;
	pcidevs = d;
	return d;
}

u32int
allocbdf(void)
{
	static int dev = 1;
	
	return BDF(0, dev++, 0);
}

u32int
roundpow2(u32int l)
{
	l = -l;
	l &= (int)l >> 16;
	l &= (int)l >> 8;
	l &= (int)l >> 4;
	l &= (int)l >> 2;
	l &= (int)l >> 1;
	return -l;
}

PCIBar *
mkpcibar(PCIDev *d, u8int t, u32int a, u32int l, void *fn, void *aux)
{
	PCIBar *b;

	assert((t & 1) == 0 || (t & 2) == 0);
	assert((t & 1) != 0 || (t & 6) == 0);
	if((t & 1) != 0 && l < 4) l = 4;
	if((t & 1) == 0 && l < 4096) l = 4096;
	if((l & l-1) != 0)
		l = roundpow2(l);
	for(b = d->bar; b < d->bar + nelem(d->bar); b++)
		if(b->length == 0)
			break;
	if(b == d->bar + nelem(d->bar))
		sysfatal("pci bdf %6ux: too many bars", d->bdf);
	b->addr = a;
	b->type = t;
	b->length = l;
	b->busnext = b;
	b->busprev = b;
	b->d = d;
	if((b->type & 1) != 0)
		b->io = fn;
	b->aux = aux;
	return b;
}

static void
updatebar(PCIBar *b)
{
	b->busnext->busprev = b->busprev;
	b->busprev->busnext = b->busnext;
	b->busnext = b;
	b->busprev = b;
	if(b->length == 0) return;
	if((b->type & 1) == 0){
		if((b->d->ctrl & 2) == 0) return;
		b->busnext = &membars;
		b->busprev = membars.busprev;
		b->busnext->busprev = b;
		b->busprev->busnext = b;
	}else{
		if((b->d->ctrl & 1) == 0 || b->addr == 0 || b->io == nil) return;
		b->busnext = &iobars;
		b->busprev = iobars.busprev;
		b->busnext->busprev = b;
		b->busprev->busnext = b;
	}
}

static void
pciirqupdate(void)
{
	PCIDev *d;
	int irqs, act, i;
	
	irqs = 0;
	act = 0;
	for(d = pcidevs; d != nil; d = d->next){
		if(d->irqno < 16){
			irqs |= 1<<d->irqno;
			act |= d->irqactive<<d->irqno;
		}
	}
	for(i = 0; i < 16; i++)
		if((irqs & 1<<i) != 0)
			irqline(i, ~act>>i & 1);
}

PCICap *
mkpcicap(PCIDev *d, u8int length, u32int (*readf)(PCICap *, u8int), void (*writef)(PCICap *, u8int, u32int, u32int))
{
	PCICap *c, **p;

	assert(readf != nil);
	if(d->capalloc + length > 256)
		sysfatal("mkpcicap (dev %#ux): out of configuration space", d->bdf);
	c = emalloc(sizeof(PCICap));
	c->dev = d;
	c->read = readf;
	c->write = writef;
	c->length = length;
	
	c->addr = d->capalloc;
	d->capalloc += length;
	for(p = &d->cap; *p != nil; p = &(*p)->next)
		;
	*p = c;
	return c;
}

static PCIDev *
findpcidev(u32int bdf)
{
	PCIDev *d;

	for(d = pcidevs; d != nil; d = d->next)
		if(d->bdf == bdf)
			return d;
	return nil;
}

static PCICap *
findpcicap(PCIDev *d, u8int addr)
{
	PCICap *c;
	
	for(c = d->cap; c != nil; c = c->next)
		if((uint)(addr - c->addr) < c->length)
			return c;
	return nil;
}

static u32int
pciread(PCIDev *d, int addr)
{
	u32int val;
	PCICap *c;
	int n;

	switch(addr){
	case 0x00: return d->viddid;
	case 0x04: return 0xa00000 | (d->cap != nil ? 1<<20 : 0) | d->ctrl;
	case 0x08: return d->clrev;
	case 0x0c: return 0; /* BIST, Header Type, Latency Timer, Cache Size */
	case 0x10: case 0x14: case 0x18: case 0x1c: case 0x20: case 0x24:
		n = addr - 0x10 >> 2;
		return d->bar[n].addr | d->bar[n].type;
	case 0x28: return 0; /* Cardbus */
	case 0x2c: return d->subid; /* Subsystem ID */
	case 0x30: return 0; /* Expansion ROM */
	case 0x34: return d->cap != nil ? d->cap->addr : 0; /* Capabilities */
	case 0x38: return 0; /* Reserved */
	case 0x3c: return 1 << 8 | d->irqno; /* Max_Lat, Min_Gnt, IRQ Pin, IRQ Line */
	}
	c = findpcicap(d, addr);
	if(c != nil){
		val = c->read(c, addr - c->addr);
		if(addr == c->addr){
			val &= ~0xff00;
			if(c->next != nil)
				val |= c->next->addr << 8;
		}
		return val;
	}
	vmdebug("pcidev %.6ux: ignoring read from addr %#ux", d->bdf, addr);
	return 0;
}

static void
pciwrite(PCIDev *d, int addr, u32int val, u32int mask)
{
	int n;
	PCICap *c;
	
	switch(addr){
	case 0x04:
		d->ctrl = (d->ctrl & ~mask | val & mask) & 0x21f;
		for(n = 0; n < nelem(d->bar); n++)
			updatebar(&d->bar[n]);
		return;
	case 0x10: case 0x14: case 0x18: case 0x1c: case 0x20: case 0x24:
		n = addr - 0x10 >> 2;
		val &= (d->bar[n].type & 1) != 0 ? ~15 : ~3;
		d->bar[n].addr = (d->bar[n].addr & ~mask | val & mask) & ~(d->bar[n].length - 1);
		updatebar(&d->bar[n]);
		return;
	case 0x30: return;
	case 0x3c: d->irqno = (d->irqno & ~mask | val & mask) & 0xff; pciirqupdate(); return;
	}
	c = findpcicap(d, addr);
	if(c != nil && c->write != nil){
		c->write(c, addr - c->addr, val, mask);
		return;
	}
	vmdebug("pcidev %.6ux: ignoring write to addr %#ux, val %#ux", d->bdf, addr, val);
}

u32int
pciio(int isin, u16int port, u32int val, int sz, void *)
{
	static u32int cfgaddr;
	u32int mask;
	PCIDev *d;

	switch(isin << 16 | port){
	case 0x0cf8: cfgaddr = val; return 0;
	case 0x10cf8: return cfgaddr & ~0x7f000003;
	case 0xcfc: case 0xcfd: case 0xcfe: case 0xcff:
		val <<= 8 * (port & 3);
		mask = -1UL >> 32 - 8 * sz << 8 * (port & 3);
		if((cfgaddr & 1<<31) != 0 && (d = findpcidev(cfgaddr & 0xffff00), d != nil))
			pciwrite(d, cfgaddr & 0xfc, val, mask);
		return 0;
	case 0x10cfc: case 0x10cfd: case 0x10cfe: case 0x10cff:
		if((cfgaddr & 1<<31) == 0 || (d = findpcidev(cfgaddr & 0xffff00), d == nil))
			return -1;
		return pciread(d, cfgaddr & 0xfc) >> 8 * (port & 3);
	}
	return iowhine(isin, port, val, sz, "pci");
}

void
pciirq(PCIDev *d, int status)
{
	d->irqactive = status != 0;
	pciirqupdate();
}

void
pciinit(void)
{
	iobars.busnext = &iobars;
	iobars.busprev = &iobars;
	membars.busprev = &membars;
	membars.busnext = &membars;
	mkpcidev(BDF(0,0,0), 0x01008086, 0x06000000, 0);
}

void
pcibusmap(void)
{
	u16int iop;
	u16int irqs, uirqs;
	PCIDev *d;
	PCIBar *b;
	int irq;
	int i;
	
	iop = 0x1000;
	irqs = 1<<5|1<<7|1<<9|1<<10|1<<11;
	uirqs = 0;
	irq = 0;
	for(d = pcidevs; d != nil; d = d->next){
		d->ctrl |= 3;
		for(b = d->bar; b < d->bar + nelem(d->bar); b++){
			if(b->length == 0 || b->addr != 0)
				continue;
			if((b->type & 1) == 0){
				vmerror("pci device %.6ux: memory bars unsupported", d->bdf);
				continue;
			}
			if(iop + b->length >= 0x10000){
				vmerror("pci device %.6ux: not enough I/O address space for BAR%d (len=%d)", d->bdf, (int)(b - d->bar), b->length);
				continue;
			}
			b->addr = iop;
			iop += b->length;
			updatebar(b);
		}
		if(d->irqno == 0){
			do
				irq = irq + 1 & 15;
			while((irqs & 1<<irq) == 0);
			d->irqno = irq;
			uirqs |= 1<<irq;
		}
	}
	elcr(uirqs);
	for(i = 0; i < 16; i++)
		if((uirqs & 1<<i) != 0)
			irqline(i, 1);
}
