/*
 *  linux/drivers/char/fbmem.c
 *
 *  Copyright (C) 1994 Martin Schaller
 *
 * This file is subject to the terms and conditions of the GNU General Public
 * License.  See the file COPYING in the main directory of this archive
 * for more details.
 */

#include <linux/types.h>
#include <linux/errno.h>
#include <linux/sched.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/malloc.h>
#include <linux/mman.h>
#include <linux/tty.h>
#include <linux/init.h>

#include <asm/setup.h>
#include <asm/uaccess.h>
#include <asm/page.h>
#include <asm/pgtable.h>

#include <linux/fb.h>

#define FB_MAJOR	29

#define FB_MODES_SHIFT    5	/* 32 modes per framebuffer */
#define FB_NUM_MINORS 	256	/* 256 Minors		    */
#define FB_MAX		(FB_NUM_MINORS / (1 << FB_MODES_SHIFT))

#define GET_INODE(i) MKDEV(FB_MAJOR, (i) << FB_MODES_SHIFT)
#define GET_FB_IDX(node) (MINOR(node) >> FB_MODES_SHIFT)
#define GET_FB_VAR_IDX(node) (MINOR(node) & ((1 << FB_MODES_SHIFT)-1)) 

struct fb_ops *registered_fb[FB_MAX];
struct fb_var_screeninfo *registered_fb_var[FB_MAX];
int registered_fb_var_num[FB_MAX];
int fb_curr_open[FB_MAX];
int fb_open_count[FB_MAX];

static inline int PROC_CONSOLE(void)
{
	if (!current->tty)
		return fg_console;

	if (current->tty->driver.type != TTY_DRIVER_TYPE_CONSOLE)
		/* XXX Should report error here? */
		return fg_console;

	if (MINOR(current->tty->device) < 1)
		return fg_console;

	return MINOR(current->tty->device) - 1;
}

static long 
fb_read(struct inode *inode, struct file *file, char *buf, unsigned long count)
{
	unsigned long p = file->f_pos;
	struct fb_ops *fb = registered_fb[GET_FB_IDX(inode->i_rdev)];
	struct fb_fix_screeninfo fix;
	char *base_addr;
	int copy_size;

	if (! fb)
		return -ENODEV;

	fb->fb_get_fix(&fix,PROC_CONSOLE());
	base_addr=(char *) fix.smem_start;
	copy_size=(count + p <= fix.smem_len ? count : fix.smem_len - p);
	copy_to_user(buf, base_addr+p, copy_size);
	file->f_pos += copy_size;
	return copy_size;
}

static long
fb_write(struct inode *inode, struct file *file, const char *buf,
	 unsigned long count)
{
	unsigned long p = file->f_pos;
	struct fb_ops *fb = registered_fb[GET_FB_IDX(inode->i_rdev)];
	struct fb_fix_screeninfo fix;
	char *base_addr;
	int copy_size;

	if (! fb)
		return -ENODEV;
	fb->fb_get_fix(&fix, PROC_CONSOLE());
	base_addr=(char *) fix.smem_start;
	copy_size=(count + p <= fix.smem_len ? count : fix.smem_len - p);
	copy_from_user(base_addr+p, buf, copy_size); 
	file->f_pos += copy_size;
	return copy_size;
}


static int 
fb_ioctl(struct inode *inode, struct file *file, unsigned int cmd,
	 unsigned long arg)
{
	struct fb_ops *fb = registered_fb[GET_FB_IDX(inode->i_rdev)];
	struct fb_cmap cmap;
	struct fb_var_screeninfo var;
	struct fb_fix_screeninfo fix;

	int i,fbidx,vidx;
	
	if (! fb)
		return -ENODEV;
	switch (cmd) {
	case FBIOGET_VSCREENINFO:
		i = verify_area(VERIFY_WRITE, (void *) arg, 
				sizeof(struct fb_var_screeninfo));
		if (i) return i;
		fbidx=GET_FB_IDX(inode->i_rdev);
		vidx=GET_FB_VAR_IDX(inode->i_rdev);
		if (! vidx) /* ask device driver for current */
			i=fb->fb_get_var(&var, PROC_CONSOLE());
		else
			var=registered_fb_var[fbidx][vidx-1];
		copy_to_user((void *) arg, &var, sizeof(var));
		return i;
	case FBIOPUT_VSCREENINFO:
		i = verify_area(VERIFY_WRITE, (void *) arg, 
				sizeof(struct fb_var_screeninfo));
		if (i) return i;
		copy_from_user(&var, (void *) arg, sizeof(var));
		i=fb->fb_set_var(&var, PROC_CONSOLE());
		copy_to_user((void *) arg, &var, sizeof(var));
		fbidx=GET_FB_IDX(inode->i_rdev);
		vidx=GET_FB_VAR_IDX(inode->i_rdev);
		if (! i && vidx)
			registered_fb_var[fbidx][vidx-1]=var;
		return i;
	case FBIOGET_FSCREENINFO:
		i = verify_area(VERIFY_WRITE, (void *) arg, 
				sizeof(struct fb_fix_screeninfo));
		if (i)	return i;
		i=fb->fb_get_fix(&fix, PROC_CONSOLE());
		copy_to_user((void *) arg, &fix, sizeof(fix));
		return i;
	case FBIOPUTCMAP:
		i = verify_area(VERIFY_READ, (void *) arg,
				sizeof(struct fb_cmap));
		if (i) return i;
		copy_from_user(&cmap, (void *) arg, sizeof(cmap));
		i = verify_area(VERIFY_READ, (void *) cmap.red, 
				cmap.len * sizeof(unsigned short));
		if (i) return i;
		i = verify_area(VERIFY_READ, (void *) cmap.green, 
				cmap.len * sizeof(unsigned short));
		if (i) return i;
		i = verify_area(VERIFY_READ, (void *) cmap.blue, 
				cmap.len * sizeof(unsigned short));
		if (i) return i;
		if (cmap.transp) {
			i = verify_area(VERIFY_READ, (void *) cmap.transp, 
					cmap.len * sizeof(unsigned short));
			if (i) return i;
		}
		return (fb->fb_set_cmap(&cmap, 0, PROC_CONSOLE()));
	case FBIOGETCMAP:
		i = verify_area(VERIFY_READ, (void *) arg,
				sizeof(struct fb_cmap));
		if (i)	return i;
		copy_from_user(&cmap, (void *) arg, sizeof(cmap));
		i = verify_area(VERIFY_WRITE, (void *) cmap.red, 
				cmap.len * sizeof(unsigned short));
		if (i) return i;
		i = verify_area(VERIFY_WRITE, (void *) cmap.green, 
				cmap.len * sizeof(unsigned short));
		if (i) return i;
		i = verify_area(VERIFY_WRITE, (void *) cmap.blue, 
				cmap.len * sizeof(unsigned short));
		if (i) return i;
		if (cmap.transp) {
			i = verify_area(VERIFY_WRITE, (void *) cmap.transp, 
					cmap.len * sizeof(unsigned short));
			if (i) return i;
		}
		return (fb->fb_get_cmap(&cmap, 0, PROC_CONSOLE()));
	case FBIOPAN_DISPLAY:
		i = verify_area(VERIFY_WRITE, (void *) arg, 
				sizeof(struct fb_var_screeninfo));
		if (i) return i;
		copy_from_user(&var, (void *) arg, sizeof(var));
		i=fb->fb_pan_display(&var, PROC_CONSOLE());
		copy_to_user((void *) arg, &var, sizeof(var));
		fbidx=GET_FB_IDX(inode->i_rdev);
		vidx=GET_FB_VAR_IDX(inode->i_rdev);
		if (! i && vidx)
			registered_fb_var[fbidx][vidx-1]=var;
		return i;
	default:
		return (fb->fb_ioctl(inode, file, cmd, arg, PROC_CONSOLE()));
	}
}

static int fb_mmap(struct file *file, struct vm_area_struct * vma)
{
	struct fb_ops *fb = registered_fb[GET_FB_IDX(file->f_dentry->d_inode->i_rdev)];
	struct fb_fix_screeninfo fix;

	if (! fb)
		return -ENODEV;
	fb->fb_get_fix(&fix, PROC_CONSOLE());
	if ((vma->vm_end - vma->vm_start + vma->vm_offset) > fix.smem_len)
		return -EINVAL;
	vma->vm_offset += __pa(fix.smem_start);
	if (vma->vm_offset & ~PAGE_MASK)
		return -ENXIO;
	if (CPU_IS_040_OR_060) {
		pgprot_val(vma->vm_page_prot) &= _CACHEMASK040;
		/* Use write-through cache mode */
		pgprot_val(vma->vm_page_prot) |= _PAGE_NOCACHE_S;
	}
	if (remap_page_range(vma->vm_start, vma->vm_offset,
			     vma->vm_end - vma->vm_start, vma->vm_page_prot))
		return -EAGAIN;
	vma->vm_file = file;
	file->f_count++;
	return 0;
}

static int
fb_open(struct inode *inode, struct file *file)
{
	int fbidx=GET_FB_IDX(inode->i_rdev);
	int vidx=GET_FB_VAR_IDX(inode->i_rdev);
	struct fb_ops *fb = registered_fb[fbidx];
	int err;
	
	if (! vidx)		/* fb?current always succeeds */ 
		return 0;
	if (vidx > registered_fb_var_num[fbidx])
		return -EINVAL;
	if (fb_curr_open[fbidx] && fb_curr_open[fbidx] != vidx)
		return -EBUSY;
 	if (file->f_mode & 2) /* only set parameters if opened writeable */
		if ((err=fb->fb_set_var(registered_fb_var[fbidx] + vidx-1, PROC_CONSOLE())))
			return err;
	fb_curr_open[fbidx] = vidx;
	fb_open_count[fbidx]++;
	return 0;
}

static int 
fb_release(struct inode *inode, struct file *file)
{
	int fbidx=GET_FB_IDX(inode->i_rdev);
	int vidx=GET_FB_VAR_IDX(inode->i_rdev);
	if (! vidx)
		return 0;
	if (! (--fb_open_count[fbidx]))
		fb_curr_open[fbidx]=0;
	return 0;
}

static struct file_operations fb_fops = {
	NULL,		/* lseek	*/
	fb_read,	/* read		*/
	fb_write,	/* write	*/
	NULL,		/* readdir 	*/
	NULL,		/* poll 	*/
	fb_ioctl,	/* ioctl 	*/
	fb_mmap,	/* mmap		*/
	fb_open,	/* open 	*/
	fb_release,	/* release 	*/
	NULL		/* fsync 	*/
};

int
register_framebuffer(char *id, int *node, struct fb_ops *fbops, int fbvar_num, 
		     struct fb_var_screeninfo *fbvar)
{
	int i;
	for (i = 0 ; i < FB_MAX; i++)
		if (! registered_fb[i])
			break;
	if (i == FB_MAX)
		return -ENXIO;
	registered_fb[i]=fbops;
	registered_fb_var[i]=fbvar;
	registered_fb_var_num[i]=fbvar_num;
	*node=GET_INODE(i);
	return 0;
}

int
unregister_framebuffer(int node)
{
	int i=GET_FB_IDX(node);
	if (! registered_fb[i])
		return -EINVAL; 
	registered_fb[i]=NULL;
	registered_fb_var[i]=NULL;
	return 0;
}

__initfunc(void
fbmem_init(void))
{
	if (register_chrdev(FB_MAJOR,"fb",&fb_fops))
		printk("unable to get major %d for fb devs\n", FB_MAJOR);
}

