/*****************************************************************************/

/*
 *      quasar.c -- NetStream2000 EM8400 (quasar) driver
 *
 *      Copyright (C) 1999-2000  Sigma Designs
 *                    written by Emmanuel Michon <emmanuel_michon@sdesigns.com>
 *                           and Howard G. Page
 *
 *      2.3.x/2.4.x compatibility by Thomas Sparr <thomas.sparr@kreatel.se>
 *
 *      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.
 *
 *      This program is distributed in the hope that it will be useful,
 *      but WITHOUT ANY WARRANTY; without even the implied warranty of
 *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *      GNU General Public License for more details.
 *
 *      You should have received a copy of the GNU General Public License
 *      along with this program; if not, write to the Free Software
 *      Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*****************************************************************************/

#include <linux/version.h>

#include <linux/pci.h>
#include <linux/wrapper.h>
#include <linux/pagemap.h>

#include <asm/io.h>
#include <asm/uaccess.h>

#include "common.h"
#include "quasar.h"
#include "realmagicdev.h"

// look in linux/fs.h for types
ssize_t quasar_read(struct file *, char *, size_t, loff_t *);
int quasar_ioctl(struct inode *, struct file *, unsigned int, unsigned long);
int quasar_mmap(struct file *, struct vm_area_struct *);
int quasar_open(struct inode *, struct file *);
int quasar_release(struct inode *, struct file *);

struct file_operations quasar_fops = 
{
	read:quasar_read, 
	ioctl:quasar_ioctl, 
	mmap:quasar_mmap, 
	open:quasar_open, 
	release:quasar_release, 
};

// the same routine handles all REALmagic devices
void quasar_interrupt(int irq, void *dev_t, struct pt_regs * regs);

/* convert virtual user memory address to physical address */
/* (unlike virt_to_phys for kernel virtual space memory) */
/* taken from btty.c */
static inline unsigned long uvirt_to_phys(unsigned long adr)
{
        unsigned long ret = 0UL;
	pmd_t *pmd;
	pte_t *ptep, pte;
        pgd_t *pgd = pgd_offset(current->mm, adr);
  
	if (!pgd_none(*pgd)) {
                pmd = pmd_offset(pgd, adr);
                if (!pmd_none(*pmd)) {
                        ptep = pte_offset(pmd, adr);
                        pte = *ptep;
                        if (pte_present(pte)) {
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,3,23)
                                unsigned long kva;
                                kva = ((unsigned long)(page_address(pte_page(pte)))
				       |((unsigned long)(adr&(PAGE_SIZE-1))));
                                ret = virt_to_phys((void*) kva);
#else
                                ret = virt_to_phys((void*) (pte_page(pte)|(adr&(PAGE_SIZE-1))));
#endif
                        }
                }
        }
	return ret;
}


/* Pure 2^n version of get_order */
static inline int my_get_order(size_t size)
{
        unsigned long order;

        size = (size-1) >> (PAGE_SHIFT-1);
        order = -1;
        do {
                size >>= 1;
                order++;
        } while (size);
        return order;
}

static inline void markpagereserved(unsigned long addr,size_t size) 
{
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,0)
	unsigned long page;
	
	for (page=addr;page<addr+size;page+=PAGE_SIZE)
		SetPageReserved(virt_to_page(page));
#else
	int i;
	
	for (i = MAP_NR(addr); i < MAP_NR(addr+size); i++) 
		mem_map_reserve(i);
#endif 
}

static void markpageunreserved(unsigned long addr,size_t size) 
{
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,0)
	unsigned long page;
	
	for (page=(unsigned long)addr;page<(unsigned long)addr+size;page+=PAGE_SIZE)
		ClearPageReserved(virt_to_page(page));
#else
	int i;
	
	for (i = MAP_NR((unsigned long)addr);
	     i < MAP_NR((unsigned long)addr+size); i++) {
		mem_map_unreserve(i);
	}
#endif		
}

static unsigned long dmaalloc(size_t size)
{
	unsigned long addr = __get_dma_pages(GFP_KERNEL, my_get_order(size));

	printk("dmaalloc called with %d ",size);
	
	if (!addr) {
		printk("(refused)\n");
		return (unsigned long)NULL;
	}
	else {
		markpagereserved(addr,size); // set as not-swappable among other things
		printk("(ok at %p)\n",(char *)addr);
		return addr;
	}
}

static void dmafree(unsigned long addr, size_t size) 
{
	printk("dmafree called with %p, %d\n",(char *)addr,size);

	if (size>0) {
		markpageunreserved(addr,size);
		free_pages(addr, my_get_order(size));
	}
}

int quasar_ioctl(struct inode *i_node, struct file *filp,
		 unsigned int cmd, unsigned long arg)
{
	int idx = MINOR(i_node->i_rdev);
	realmagic_dev_t *dev = &realmagic_dev_tab[idx];
	
	switch (cmd) {
	case IOCTL_GET_PIOMAPSIZE:
		{
			LOGINFO(1,"ioctl device #%d IOCTL_GET_PIOMAPSIZE successful",idx);
			copy_to_user((void *)arg,&dev->mapsize,sizeof(int));
			break;
		}

	case IOCTL_DMABUF_ALLOC:
		{
			int i;

			for (i=0;i<MAX_dmabuf;i++) 
				if (!dev->dmabuf_table[i].inuse) break;
			if (i==MAX_dmabuf) {
				printk("MAX_dmabuf reached\n");
				return -EINVAL;
			}

			DUMPDMABUF(IOCTL_DMABUF_ALLOC)
			
			copy_from_user(&dev->dmabuf_table[i],(void *)arg,sizeof(dmabuf_helper));

			DUMPDMABUF(IOCTL_DMABUF_ALLOC)

			dev->dmabuf_table[i].KernelVirtualAddress=
				(unsigned long)dmaalloc(dev->dmabuf_table[i].Size);

			// dmaalloc failure?
			if (dev->dmabuf_table[i].KernelVirtualAddress==0) return -EINVAL;

			dev->dmabuf_table[i].PhysicalAddress=
				virt_to_phys((char *)dev->dmabuf_table[i].KernelVirtualAddress);
			dev->dmabuf_table[i].inuse=1;
			dev->dmabuf_table[i].ProcessVirtualAddress=0;

			DUMPDMABUF(IOCTL_DMABUF_ALLOC)

			copy_to_user((void *)arg,&dev->dmabuf_table[i],sizeof(dmabuf_helper));
			break;
		}
			
	case IOCTL_DMABUF_FREE:
		{
			/* the user space code must call munmap and put zero in ProcessVirtualAddress 
			   field before getting here */

			int i;
			dmabuf_helper d;

			copy_from_user(&d,(void *)arg,sizeof(dmabuf_helper));
			
			// looking for the (only) index using KernelVirtualAddress
			for (i=0;i<MAX_dmabuf;i++) 
				if (dev->dmabuf_table[i].inuse&&
				    (dev->dmabuf_table[i].KernelVirtualAddress==d.KernelVirtualAddress)) break;
			if (i==MAX_dmabuf) {
				printk("MAX_dmabuf reached\n");
				return -EINVAL;
			}

			DUMPDMABUF(IOCTL_DMABUF_FREE)
				
			if (d.ProcessVirtualAddress) {
				printk("Trying to dmafree mmapped memory\n");
				return -EINVAL;
			}
			else {
				dmafree(dev->dmabuf_table[i].KernelVirtualAddress,dev->dmabuf_table[i].Size);
				dev->dmabuf_table[i].inuse=0;
			}
			
			DUMPDMABUF(IOCTL_DMABUF_FREE)
			
			copy_to_user((void *)arg,&dev->dmabuf_table[i],sizeof(dmabuf_helper));
			break;
		}
			
	case IOCTL_TRANSLATE_PROCESSVIRTUAL_TO_PHYSICAL:
		{
			unsigned long processvirtual,physical;
			copy_from_user(&processvirtual,(void *)arg,sizeof(unsigned long));
			
			physical = (unsigned long)uvirt_to_phys(processvirtual);
			
			copy_to_user((void *)arg,&physical,sizeof(unsigned long));
			break;
		}
		
	case IOCTL_UNRESERVE_PAGE_BY_PHYSICAL_ADDRESS:
		{
			unsigned long physical;
			copy_from_user(&physical,(void *)arg,sizeof(unsigned long));
			
			markpageunreserved((unsigned long)phys_to_virt(physical),PAGE_SIZE);
			break;
		}
		
	case IOCTL_RESERVE_PAGE_BY_UV:
		{
			unsigned long processvirtual,physical;
			copy_from_user(&processvirtual,(void *)arg,sizeof(unsigned long));
			
			physical = (unsigned long)uvirt_to_phys(processvirtual);
			markpagereserved((unsigned long)phys_to_virt(physical),PAGE_SIZE);
			break;
		}

	case IOCTL_UNRESERVE_PAGE_BY_UV:
		{
			unsigned long processvirtual,physical;
			copy_from_user(&processvirtual,(void *)arg,sizeof(unsigned long));
			
			physical = (unsigned long)uvirt_to_phys(processvirtual);
			markpageunreserved((unsigned long)phys_to_virt(physical),PAGE_SIZE);
			break;
		}

	case IOCTL_SEND_RING3_PID:
		{
			copy_from_user(&dev->ring3_pid,(void *)arg,sizeof(unsigned long));
			printk("[Received ring3 pid is %ld]\n",dev->ring3_pid);
			break;
		}
		
	case IOCTL_SEND_IRQ_DATA:
		{
			copy_from_user(&dev->i_d,(void *)arg,sizeof(irq_data));
			printk("[Received irq data: %08lx-%08lx-%08lx-%08lx]\n",
			       dev->i_d.irq_status_register,
			       dev->i_d.irq_mask_register,
			       dev->i_d.irq_status,
			       dev->i_d.irq_mask
			       );
			break;
		}

	case IOCTL_GET_IRQ_COUNT:
		{
			copy_to_user((void *)arg,&dev->ring0_irqcount,sizeof(unsigned long));
			break;
		}
		
	case IOCTL_GET_PCI_ID_SUBSYSTEM_VENDOR:
		{
			unsigned short value;

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,0)
			value=dev->pdev->subsystem_vendor;
#else
			pci_read_config_word(dev->pdev,PCI_SUBSYSTEM_VENDOR_ID,&value);
#endif	
			copy_to_user((void *)arg,&value,sizeof(unsigned short));
			break;
		}

	case IOCTL_GET_PCI_ID_SUBSYSTEM_DEVICE:
		{
			unsigned short value;

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,0)
			value=dev->pdev->subsystem_device;
#else
			pci_read_config_word(dev->pdev,PCI_SUBSYSTEM_ID,&value);
#endif	
			copy_to_user((void *)arg,&value,sizeof(unsigned short));
			break;
		}

	case IOCTL_GET_PCI_DEVICE_ID:
		{
			copy_to_user((void *)arg,&quasar_dev_id,sizeof(unsigned short));
			break;
		}
		
	case IOCTL_GET_SAWEOS:
		{
			copy_to_user((void *)arg,&dev->sawEOS,sizeof(unsigned long));
			if (dev->sawEOS==1) {
				dev->sawEOS=0;
				printk("Resetting EOS detection\n");
			}
			break;
		}
		
	case IOCTL_FORGE_IRQ:
		{
			dev->irqstilltobeserviced++;
#ifdef _STREAMMACHINE_
			wake_up_interruptible(&dev->quasar_queue);
#endif /*_STREAMMACHINE_*/
			break;
		}
	default:
		return -EINVAL;
	}
	
	return 0; // never reached anyways
}

int quasar_mmap(struct file * filp, struct vm_area_struct * vma)
{
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,3,25)
	unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
#else
        unsigned long offset = vma->vm_offset;
#endif
	
	int idx = MINOR(filp->f_dentry->d_inode->i_rdev);
	realmagic_dev_t *dev = &realmagic_dev_tab[idx];

	switch (offset) {
	case REMAP_PIO:
		{
			// for PIO space

			VERBOSECALL(DEBUG,remap_page_range(vma->vm_start,dev->ioaddr, 
							   vma->vm_end - vma->vm_start, 
							   vma->vm_page_prot),
				    -EINVAL,RAS);
			
			LOGINFO(1,"mmap (remap_page_range) device #%d successful",idx);
			
			break;
		}

	case REMAP_DMA:
		{
			// for DMA space

			int i;

			// looking for the (only) dmabuf_table entry used but not yet mapped 
			for (i=0;i<MAX_dmabuf;i++) 
				if (dev->dmabuf_table[i].inuse&!dev->dmabuf_table[i].ProcessVirtualAddress) break;
			if (i==MAX_dmabuf) {
				printk("MAX_dmabuf reached\n");
				return -EINVAL;
			}
			
			DUMPDMABUF(REMAP_DMA);
				
			VERBOSECALL(DEBUG,remap_page_range(vma->vm_start,
							   dev->dmabuf_table[i].PhysicalAddress,
							   vma->vm_end - vma->vm_start, 
							   vma->vm_page_prot),
				    -EINVAL,RAS);	

			dev->dmabuf_table[i].ProcessVirtualAddress=vma->vm_start;

			DUMPDMABUF(REMAP_DMA);
				
			break;
		}
		
	default:
		return -EINVAL;
	}
	
	return 0; // never reached anyways
}

int quasar_open(struct inode *i_node, struct file *filp)
{
	int idx = MINOR(i_node->i_rdev);
	realmagic_dev_t *dev = &realmagic_dev_tab[idx];
	int i;

	// not a good idea to open twice.
	if (dev->opentimes!=0) {
		printk("quasar_open: already open\n");
		return -EINVAL;
	}
	
	dev->opentimes++;
	dev->ring3_pid=0;
	dev->irqstilltobeserviced=0;
	dev->sawEOS=0;
	dev->ring0_irqcount=0;
	dev->i_d.irq_status_register=0;
	
	LOGINFO(1,"opening device #%d successful (doing nothing)",
		MINOR(i_node->i_rdev));
	
	// clean dmabuf_table
	for (i=0;i<MAX_dmabuf;i++) dev->dmabuf_table[i].inuse=0;

	return 0;
}

int quasar_release(struct inode *i_node, struct file *filp)
{
	int idx = MINOR(i_node->i_rdev);
	realmagic_dev_t *dev = &realmagic_dev_tab[idx];

	// not a good idea to close twice.
	if (dev->opentimes==0) {
		printk("quasar_release: already closed\n");
		return -EINVAL;
	}
	
	dev->opentimes--;
	dev->i_d.irq_status_register=0;

	if (dev->opentimes==0) {	
		int i;

		for (i=0;i<MAX_dmabuf;i++) 
			if (dev->dmabuf_table[i].inuse) {
				
				DUMPDMABUF(release)

				if (dev->dmabuf_table[i].ProcessVirtualAddress) 
					printk("Trying to dmafree mmapped memory --- doing it anyways\n");
				dmafree(dev->dmabuf_table[i].KernelVirtualAddress,dev->dmabuf_table[i].Size);
				dev->dmabuf_table[i].inuse=0;

				DUMPDMABUF(release)

			}
	}

	return 0;
}

int pci_scan(void)
{
	struct pci_dev *pdev=NULL;
	int pcipresent;
	
	// check for PCI BIOS
	pcipresent=pci_present();
	ASSERT(DEBUG,pcipresent,
	       -EINVAL,RAS);
	
	// look for Sigma devices
	realmagic_dev_idx=0;

	while ((pdev=pci_find_device(PCI_VENDOR_ID_SIGMAD,
				     quasar_dev_id,pdev))) {
		realmagic_dev_t *dev;
		
		ASSERT(DEBUG,(realmagic_dev_idx<REALMAGIC_MAX_DEV),
		       -EINVAL,RAS);

		dev=&realmagic_dev_tab[realmagic_dev_idx];
		dev->fops=&quasar_fops;
		dev->pdev=pdev;
		
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,3,13)
		dev->ioaddr = dev->pdev->resource[0].start;
#else
                dev->ioaddr = dev->pdev->base_address[0];
#endif	

		{
			int curr;
			unsigned short v;
			
			// see A.Rubini page 353 for details
			
			pci_read_config_dword(dev->pdev,PCI_BASE_ADDRESS_0,&curr);
		
			// get PIO size
			pci_write_config_dword(dev->pdev,PCI_BASE_ADDRESS_0,~0);
			pci_read_config_dword(dev->pdev,PCI_BASE_ADDRESS_0,&dev->mapsize);
			dev->mapsize=~dev->mapsize+1;

			// get configuration space
			pci_write_config_dword(dev->pdev,PCI_BASE_ADDRESS_0,curr);
			pci_read_config_word(dev->pdev,PCI_COMMAND,&v);

			// enable Busmaster in the PCI configuration space
			v |= PCI_COMMAND_MASTER;
			pci_write_config_dword(dev->pdev,PCI_COMMAND,v);
  						
			// check the configuration space
			pci_read_config_word(dev->pdev,PCI_COMMAND,&v);
			LOGINFO(1,"cmd=0x%hx",v);

			// check status
			pci_read_config_word(dev->pdev,PCI_STATUS,&v);
			LOGINFO(1,"stat=0x%hx",v);
		}
		
		LOGINFO(1,"found REALmagic device #%d at PCI "
			"slot=%d devfn=%d ioaddr=%08lx mapsize=%d irq=%d ",
			realmagic_dev_idx,
			PCI_SLOT(dev->pdev->devfn), 
			dev->pdev->devfn,
			dev->ioaddr,
			dev->mapsize,
			dev->pdev->irq);
		
		realmagic_dev_idx++;
	}
	
	ASSERT(1,realmagic_dev_idx,
	       -EINVAL,"found %d REALmagic devices total",realmagic_dev_idx);
	
	return 0;
}

int quasar_init(void)
{
	int idx;

	VERBOSECALL(DEBUG,pci_scan(),
		    -EINVAL,RAS);
	
	for(idx=0;idx<realmagic_dev_idx;idx++) {
		realmagic_dev_t *dev=&realmagic_dev_tab[idx];

		// in pathologic cases device memory can get cached (see MTRR for instance)
#ifndef ioremap_nocache
#define ioremap_nocache(X,Y) ioremap(X,Y)
#endif

		// ioremap
		dev->vioaddr=(unsigned long)ioremap_nocache(dev->ioaddr&PAGE_MASK,dev->mapsize);
		ASSERT(1,dev->vioaddr,
		       -EINVAL,"ioremap device #%d successful at %08lx",idx,dev->vioaddr);
		
		// irq request. ``dev'' is unique: allows irq sharing
		VERBOSECALL(DEBUG,request_irq(dev->pdev->irq,quasar_interrupt,
					      SA_SHIRQ,INTERRUPT_ENTRY_NAME,dev),
			    -EINVAL,"ok for irq %d on device #%d",dev->pdev->irq,idx);

#ifdef _STREAMMACHINE_
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,0)
		init_waitqueue_head(&dev->quasar_queue);
#else
		init_waitqueue(&dev->quasar_queue);
#endif
#endif /*_STREAMMACHINE_*/
		
		// relevant values in struct...
		// init of dmabuf_table is done in open method
		dev->opentimes=0;
	}
	
	return 0;
}

int quasar_cleanup(void)
{
	int idx,nok=0;
	
	// free interrupt
	for(idx=0;idx<realmagic_dev_idx;idx++) {
		realmagic_dev_t *dev=&realmagic_dev_tab[idx];

		if (dev->opentimes==0) {	
			// iounmap
			iounmap((void*)(dev->vioaddr&PAGE_MASK));
			
			LOGINFO(1,"iounmap device #%d executed (no return code)",idx);
			
			// irq free
			free_irq(dev->pdev->irq,dev);
			
			// FIXME CODE TO FREE LAST BUFFERS

			nok++;
		}
		else printk("quasar_cleanup: instance #%d still open\n",idx);
	}
	
	if (nok<realmagic_dev_idx) return -EINVAL;
	
	return 0;
}

#define RISC_IRQ_REGISTER 0x1ffa
#define OLD_CLEAR_RISC 0x2
#define NEW_CLEAR_RISC 0x100

#define writePIO(b,thisregister) writel((b),(dev->vioaddr+((thisregister)<<2)))
#define readPIO(thisregister) readl(dev->vioaddr+((thisregister)<<2))

void quasar_interrupt(int irq, void *dev_t, struct pt_regs * regs)
{
	static int otherirqcount=0;
        realmagic_dev_t* dev = (realmagic_dev_t*)dev_t;
	long keepme=0; // initialized because gcc makes pointless ``might be used uninitialized''
	
	// in case of irq sharing, quick way out
	if (readPIO(RISC_IRQ_REGISTER)==0) {
		otherirqcount++;
		return;
	}
	
	if (quasar_dev_id>=0x8470)
		writePIO(NEW_CLEAR_RISC,RISC_IRQ_REGISTER);
	else
		writePIO(OLD_CLEAR_RISC,RISC_IRQ_REGISTER);

	if (dev->i_d.irq_status_register!=0)
		writePIO(0,dev->i_d.irq_mask_register);
	
#ifdef _STREAMMACHINE_
	if ((keepme&(dev->i_d.irq_mask))==0) return;
#endif /*_STREAMMACHINE_*/
         
	dev->ring0_irqcount++;

	dev->irqstilltobeserviced++;

	// (playback) with 5000 this should log about every minute
	if (dev->ring0_irqcount%5000==0) printk("[ring0 received %ld interrupts so far (skipped %d for other devices sharing line)]\n",dev->ring0_irqcount,otherirqcount);

#ifdef _STREAMMACHINE_
	wake_up_interruptible(&dev->quasar_queue);
#endif /*_STREAMMACHINE_*/

	if (keepme&0x1) {
		printk("EOS detected\n");
		dev->sawEOS=1;
	}

	if (dev->ring3_pid) kill_proc(dev->ring3_pid,SIGUSR2,0);
	
        // do not reenable interrupt: user mode does it for us.
}

ssize_t quasar_read(struct file *filp, char *buf, size_t count, loff_t *pos)
{
#ifdef _STREAMMACHINE_
	static char sentdata='0';

	int rc;
	int idx = MINOR(filp->f_dentry->d_inode->i_rdev);
	realmagic_dev_t *dev = &realmagic_dev_tab[idx];

	rc=wait_event_interruptible(dev->quasar_queue,(dev->irq_to_service>0));
	if (rc!=0) return rc; // most probably -ERESTARTSYS due to signal reception.
	
	copy_to_user(buf,&sentdata,1);
	dev->irqstilltobeserviced--;
	(*pos)++;
	
	// loop 01234567890123456789...
	if (sentdata<'9') 
		sentdata++;
	else
		sentdata='0';
	
	// because we sent one character
	return 1;
#else
	return 0;
#endif /*_STREAMMACHINE_*/
}
