/*
 *
 * Copyright © 1999 Keith Packard
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation, and that the name of Keith Packard not be used in
 * advertising or publicity pertaining to distribution of the software without
 * specific, written prior permission.  Keith Packard makes no
 * representations about the suitability of this software for any purpose.  It
 * is provided "as is" without express or implied warranty.
 *
 * KEITH PACKARD DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
 * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
 * EVENT SHALL KEITH PACKARD BE LIABLE FOR ANY SPECIAL, INDIRECT OR
 * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
 * DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
 * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 * PERFORMANCE OF THIS SOFTWARE.
 */

#ifdef HAVE_CONFIG_H
#include <kdrive-config.h>
#endif
#include "fbdev.h"
#include <sys/ioctl.h>

#include <errno.h>

extern int KdTsPhyScreen;

const char *fbdevDevicePath = NULL;

static Bool fbdevMapFramebuffer(KdScreenInfo * screen);

static Bool fbdevInitialize(KdCardInfo * card, FbdevPriv * priv)
{
	unsigned long off;

	if (fbdevDevicePath == NULL)
		fbdevDevicePath = "/dev/fb0";

	if ((priv->fd = open(fbdevDevicePath, O_RDWR)) < 0) {
		ErrorF("Error opening framebuffer %s: %s\n",
		       fbdevDevicePath, strerror(errno));
		return FALSE;
	}

	/* quiet valgrind */
	memset(&priv->fix, '\0', sizeof(priv->fix));
	if (ioctl(priv->fd, FBIOGET_FSCREENINFO, &priv->fix) < 0) {
		perror("Error with /dev/fb ioctl FIOGET_FSCREENINFO");
		close(priv->fd);
		return FALSE;
	}
	/* quiet valgrind */
	memset(&priv->var, '\0', sizeof(priv->var));
	if (ioctl(priv->fd, FBIOGET_VSCREENINFO, &priv->var) < 0) {
		perror("Error with /dev/fb ioctl FIOGET_VSCREENINFO");
		close(priv->fd);
		return FALSE;
	}

	priv->fb_base = (char *) mmap((caddr_t) NULL,
				     priv->fix.smem_len,
				     PROT_READ | PROT_WRITE,
				     MAP_SHARED, priv->fd, 0);

	if (priv->fb_base == (char *) -1) {
		perror("ERROR: mmap framebuffer fails!");
		close(priv->fd);
		return FALSE;
	}
	off = (unsigned long) priv->fix.smem_start % (unsigned long) getpagesize();
	priv->fb = priv->fb_base + off;
	return TRUE;
}

Bool fbdevCardInit(KdCardInfo * card)
{
	FbdevPriv *priv;

	priv = malloc(sizeof(FbdevPriv));
	if (!priv)
		return FALSE;

	if (!fbdevInitialize(card, priv)) {
		free(priv);
		return FALSE;
	}
	card->driver = priv;

	return TRUE;
}

static Pixel fbdevMakeContig(Pixel orig, Pixel others)
{
	Pixel low;

	low = lowbit(orig) >> 1;
	while (low && (others & low) == 0) {
		orig |= low;
		low >>= 1;
	}
	return orig;
}

static Bool fbdevModeSupported(KdScreenInfo * screen, const KdMonitorTiming * t)
{
	return TRUE;
}

static void
fbdevConvertMonitorTiming(const KdMonitorTiming * t,
			  struct fb_var_screeninfo *var)
{
	memset(var, 0, sizeof(struct fb_var_screeninfo));

	var->xres = t->horizontal;
	var->yres = t->vertical;
	var->xres_virtual = t->horizontal;
	var->yres_virtual = t->vertical;
	var->xoffset = 0;
	var->yoffset = 0;
	var->pixclock = t->clock ? 1000000000 / t->clock : 0;
	var->left_margin = t->hbp;
	var->right_margin = t->hfp;
	var->upper_margin = t->vbp;
	var->lower_margin = t->vfp;
	var->hsync_len = t->hblank - t->hfp - t->hbp;
	var->vsync_len = t->vblank - t->vfp - t->vbp;

	var->sync = 0;
	var->vmode = 0;

	if (t->hpol == KdSyncPositive)
		var->sync |= FB_SYNC_HOR_HIGH_ACT;
	if (t->vpol == KdSyncPositive)
		var->sync |= FB_SYNC_VERT_HIGH_ACT;
}

static Bool fbdevScreenInitialize(KdScreenInfo * screen, FbdevScrPriv * scrpriv)
{
	FbdevPriv *priv = screen->card->driver;
	Pixel allbits;
	int depth;
	Bool gray;
	struct fb_var_screeninfo var;
	const KdMonitorTiming *t;
	int k;

	k = ioctl(priv->fd, FBIOGET_VSCREENINFO, &var);

	if (!screen->width || !screen->height) {
		if (k >= 0) {
			screen->width = var.xres;
			screen->height = var.yres;
		} else {
			screen->width = 1024;
			screen->height = 768;
		}
		screen->rate = 103;	/* FIXME: should get proper value from fb driver */
	}
	if (!screen->fb.depth) {
		if (k >= 0)
			screen->fb.depth = var.bits_per_pixel;
		else
			screen->fb.depth = 16;
	}

	if ((screen->width != var.xres) || (screen->height != var.yres)) {
		t = KdFindMode(screen, fbdevModeSupported);
		screen->rate = t->rate;
		screen->width = t->horizontal;
		screen->height = t->vertical;

		/* Now try setting the mode */
		if (k < 0 || (t->horizontal != var.xres || t->vertical != var.yres))
			fbdevConvertMonitorTiming(t, &var);
	}

	var.activate = FB_ACTIVATE_NOW;
	var.bits_per_pixel = screen->fb.depth;
	var.nonstd = 0;
	var.grayscale = 0;

	k = ioctl(priv->fd, FBIOPUT_VSCREENINFO, &var);

	if (k < 0) {
		fprintf(stderr, "error: %s\n", strerror(errno));
		return FALSE;
	}

	/* Re-get the "fixed" parameters since they might have changed */
	k = ioctl(priv->fd, FBIOGET_FSCREENINFO, &priv->fix);
	if (k < 0)
		perror("FBIOGET_FSCREENINFO");

	/* Now get the new screeninfo */
	ioctl(priv->fd, FBIOGET_VSCREENINFO, &priv->var);
	depth = priv->var.bits_per_pixel;
	gray = priv->var.grayscale;

	/* Calculate fix.line_length if it's zero */
	if (!priv->fix.line_length)
		priv->fix.line_length = (priv->var.xres_virtual * depth + 7) / 8;

	switch (priv->fix.visual) {
	case FB_VISUAL_PSEUDOCOLOR:
		if (gray) {
			screen->fb.visuals = (1 << StaticGray);
			/* could also support GrayScale, but what's the point? */
		} else {
			screen->fb.visuals = ((1 << StaticGray) |
						 (1 << GrayScale) |
						 (1 << StaticColor) |
						 (1 << PseudoColor) |
						 (1 << TrueColor) |
						 (1 << DirectColor));
		}
		screen->fb.blueMask = 0x00;
		screen->fb.greenMask = 0x00;
		screen->fb.redMask = 0x00;
		break;
	case FB_VISUAL_STATIC_PSEUDOCOLOR:
		if (gray) {
			screen->fb.visuals = (1 << StaticGray);
		} else {
			screen->fb.visuals = (1 << StaticColor);
		}
		screen->fb.blueMask = 0x00;
		screen->fb.greenMask = 0x00;
		screen->fb.redMask = 0x00;
		break;
	case FB_VISUAL_TRUECOLOR:
	case FB_VISUAL_DIRECTCOLOR:
		screen->fb.visuals = (1 << TrueColor);
#define Mask(o,l)   (((1 << l) - 1) << o)
		screen->fb.redMask =
		    Mask(priv->var.red.offset, priv->var.red.length);
		screen->fb.greenMask =
		    Mask(priv->var.green.offset, priv->var.green.length);
		screen->fb.blueMask =
		    Mask(priv->var.blue.offset, priv->var.blue.length);
		/*
		 * This is a kludge so that Render will work -- fill in the gaps
		 * in the pixel
		 */
		screen->fb.redMask = fbdevMakeContig(screen->fb.redMask,
							screen->fb.
							greenMask | screen->
							fb.blueMask);

		screen->fb.greenMask =
		    fbdevMakeContig(screen->fb.greenMask,
				    screen->fb.redMask | screen->fb.
				    blueMask);

		screen->fb.blueMask = fbdevMakeContig(screen->fb.blueMask,
							 screen->fb.redMask |
							 screen->fb.
							 greenMask);

		allbits =
		    screen->fb.redMask | screen->fb.greenMask | screen->
		    fb.blueMask;
		depth = 32;
		while (depth && !(allbits & (1 << (depth - 1))))
			depth--;
		break;
	default:
		return FALSE;
		break;
	}
	screen->fb.depth = depth;
	screen->fb.bitsPerPixel = priv->var.bits_per_pixel;

	scrpriv->randr = screen->randr;

	return fbdevMapFramebuffer(screen);
}

Bool fbdevScreenInit(KdScreenInfo * screen)
{
	FbdevScrPriv *scrpriv;

	scrpriv = calloc(1, sizeof(FbdevScrPriv));
	if (!scrpriv)
		return FALSE;
	screen->driver = scrpriv;
	if (!fbdevScreenInitialize(screen, scrpriv)) {
		screen->driver = 0;
		free(scrpriv);
		return FALSE;
	}
	return TRUE;
}

static void *fbdevWindowLinear(ScreenPtr pScreen,
			CARD32 row,
			CARD32 offset, int mode, CARD32 * size, void *closure)
{
	KdScreenPriv(pScreen);
	FbdevPriv *priv = pScreenPriv->card->driver;

	if (!pScreenPriv->enabled)
		return 0;
	*size = priv->fix.line_length;
	return (CARD8 *) priv->fb + row * priv->fix.line_length + offset;
}

static Bool fbdevMapFramebuffer(KdScreenInfo * screen)
{
	FbdevScrPriv *scrpriv = screen->driver;
	KdMouseMatrix m;
	FbdevPriv *priv = screen->card->driver;

	if (scrpriv->randr != RR_Rotate_0 ||
		priv->fix.type != FB_TYPE_PACKED_PIXELS)
		scrpriv->shadow = TRUE;
	else
		scrpriv->shadow = FALSE;

	KdComputeMouseMatrix(&m, scrpriv->randr, screen->width, screen->height);

	KdSetMouseMatrix(&m);

	screen->width = priv->var.xres;
	screen->height = priv->var.yres;
	screen->memory_base = (CARD8 *) (priv->fb);
	screen->memory_size = priv->fix.smem_len;

	if (scrpriv->shadow) {
		if (!KdShadowFbAlloc(screen,
				     scrpriv->
				     randr & (RR_Rotate_90 | RR_Rotate_270)))
			return FALSE;
		screen->off_screen_base = screen->memory_size;
	} else {
		screen->fb.byteStride = priv->fix.line_length;
		screen->fb.pixelStride = (priv->fix.line_length * 8 /
					     priv->var.bits_per_pixel);
		screen->fb.frameBuffer = (CARD8 *) (priv->fb);
		screen->off_screen_base =
		    screen->fb.byteStride * screen->height;
	}

	return TRUE;
}

static void fbdevSetScreenSizes(ScreenPtr pScreen)
{
	KdScreenPriv(pScreen);
	KdScreenInfo *screen = pScreenPriv->screen;
	FbdevScrPriv *scrpriv = screen->driver;
	FbdevPriv *priv = screen->card->driver;

	if (scrpriv->randr & (RR_Rotate_0 | RR_Rotate_180)) {
		pScreen->width = priv->var.xres;
		pScreen->height = priv->var.yres;
		pScreen->mmWidth = screen->width_mm;
		pScreen->mmHeight = screen->height_mm;
	} else {
		pScreen->width = priv->var.yres;
		pScreen->height = priv->var.xres;
		pScreen->mmWidth = screen->height_mm;
		pScreen->mmHeight = screen->width_mm;
	}
}

static Bool fbdevUnmapFramebuffer(KdScreenInfo * screen)
{
	KdShadowFbFree(screen);
	return TRUE;
}

static Bool fbdevSetShadow(ScreenPtr pScreen)
{
	KdScreenPriv(pScreen);
	KdScreenInfo *screen = pScreenPriv->screen;
	FbdevScrPriv *scrpriv = screen->driver;
	FbdevPriv *priv = screen->card->driver;
	ShadowUpdateProc update;
	ShadowWindowProc window;
	int useYX = 0;

#ifdef __arm__
	/* Use variant copy routines that always read left to right in the
	   shadow framebuffer.  Reading vertical strips is exceptionally
	   slow on XScale due to cache effects.  */
	useYX = 1;
#endif

	window = fbdevWindowLinear;
	update = 0;

	if (priv->fix.type != FB_TYPE_PACKED_PIXELS)
		FatalError("Unsupported frame buffer type %u\n", priv->fix.type);

	if (scrpriv->randr)
		if (priv->var.bits_per_pixel == 16) {
			switch (scrpriv->randr) {
			case RR_Rotate_90:
				if (useYX)
					update = shadowUpdateRotate16_90YX;
				else
					update = shadowUpdateRotate16_90;
				break;
			case RR_Rotate_180:
				update = shadowUpdateRotate16_180;
				break;
			case RR_Rotate_270:
				if (useYX)
					update = shadowUpdateRotate16_270YX;
				else
					update = shadowUpdateRotate16_270;
				break;
			default:
				update = shadowUpdateRotate16;
				break;
			}
		} else
			update = shadowUpdateRotatePacked;
	else
		update = shadowUpdatePacked;
	return KdShadowSet(pScreen, scrpriv->randr, update, window);
}

static Bool fbdevRandRGetInfo(ScreenPtr pScreen, Rotation * rotations)
{
	KdScreenPriv(pScreen);
	KdScreenInfo *screen = pScreenPriv->screen;
	FbdevScrPriv *scrpriv = screen->driver;
	RRScreenSizePtr pSize;
	Rotation randr;
	int n;

	*rotations = RR_Rotate_All | RR_Reflect_All;

	for (n = 0; n < pScreen->numDepths; n++)
		if (pScreen->allowedDepths[n].numVids)
			break;
	if (n == pScreen->numDepths)
		return FALSE;

	pSize = RRRegisterSize(pScreen,
			       screen->width,
			       screen->height,
			       screen->width_mm, screen->height_mm);

	randr = KdSubRotation(scrpriv->randr, screen->randr);

	RRSetCurrentConfig(pScreen, randr, 0, pSize);

	return TRUE;
}

static Bool
fbdevRandRSetConfig(ScreenPtr pScreen,
		    Rotation randr, int rate, RRScreenSizePtr pSize)
{
	KdScreenPriv(pScreen);
	KdScreenInfo *screen = pScreenPriv->screen;
	FbdevScrPriv *scrpriv = screen->driver;
	Bool wasEnabled = pScreenPriv->enabled;
	FbdevScrPriv oldscr;
	int oldwidth;
	int oldheight;
	int oldmmwidth;
	int oldmmheight;
	int newwidth, newheight, newmmwidth, newmmheight;

	if (screen->randr & (RR_Rotate_0 | RR_Rotate_180)) {
		newwidth = pSize->width;
		newheight = pSize->height;
		newmmwidth = pSize->mmWidth;
		newmmheight = pSize->mmHeight;
	} else {
		newwidth = pSize->height;
		newheight = pSize->width;
		newmmwidth = pSize->mmHeight;
		newmmheight = pSize->mmWidth;
	}

	if (wasEnabled)
		KdDisableScreen(pScreen);

	oldscr = *scrpriv;

	oldwidth = screen->width;
	oldheight = screen->height;
	oldmmwidth = pScreen->mmWidth;
	oldmmheight = pScreen->mmHeight;

	/*
	 * Set new configuration
	 */

	scrpriv->randr = KdAddRotation(screen->randr, randr);

	pScreen->width = newwidth;
	pScreen->height = newheight;
	pScreen->mmWidth = newmmwidth;
	pScreen->mmHeight = newmmheight;

	fbdevUnmapFramebuffer(screen);

	if (!fbdevMapFramebuffer(screen))
		goto bail4;

	KdShadowUnset(screen->pScreen);

	if (!fbdevSetShadow(screen->pScreen))
		goto bail4;

	fbdevSetScreenSizes(screen->pScreen);

	/*
	 * Set frame buffer mapping
	 */
	(*pScreen->ModifyPixmapHeader) (fbGetScreenPixmap(pScreen),
					pScreen->width,
					pScreen->height,
					screen->fb.depth,
					screen->fb.bitsPerPixel,
					screen->fb.byteStride,
					screen->fb.frameBuffer);

	/* set the subpixel order */

	KdSetSubpixelOrder(pScreen, scrpriv->randr);
	if (wasEnabled)
		KdEnableScreen(pScreen);

	return TRUE;

 bail4:
	fbdevUnmapFramebuffer(screen);
	*scrpriv = oldscr;
	fbdevMapFramebuffer(screen);
	pScreen->width = oldwidth;
	pScreen->height = oldheight;
	pScreen->mmWidth = oldmmwidth;
	pScreen->mmHeight = oldmmheight;

	if (wasEnabled)
		KdEnableScreen(pScreen);
	return FALSE;
}

static Bool fbdevRandRInit(ScreenPtr pScreen)
{
	rrScrPrivPtr pScrPriv;

	if (!RRScreenInit(pScreen))
		return FALSE;

	pScrPriv = rrGetScrPriv(pScreen);
	pScrPriv->rrGetInfo = fbdevRandRGetInfo;
	pScrPriv->rrSetConfig = fbdevRandRSetConfig;
	return TRUE;
}

static Bool fbdevCreateColormap(ColormapPtr pmap)
{
	ScreenPtr pScreen = pmap->pScreen;
	KdScreenPriv(pScreen);
	FbdevPriv *priv = pScreenPriv->card->driver;
	VisualPtr pVisual;
	int i;
	int nent;
	xColorItem *pdefs;

	switch (priv->fix.visual) {
	case FB_VISUAL_STATIC_PSEUDOCOLOR:
		pVisual = pmap->pVisual;
		nent = pVisual->ColormapEntries;
		pdefs = malloc(nent * sizeof(xColorItem));
		if (!pdefs)
			return FALSE;
		for (i = 0; i < nent; i++)
			pdefs[i].pixel = i;
		fbdevGetColors(pScreen, nent, pdefs);
		for (i = 0; i < nent; i++) {
			pmap->red[i].co.local.red = pdefs[i].red;
			pmap->red[i].co.local.green = pdefs[i].green;
			pmap->red[i].co.local.blue = pdefs[i].blue;
		}
		free(pdefs);
		return TRUE;
	default:
		return fbInitializeColormap(pmap);
	}
}

Bool fbdevInitScreen(ScreenPtr pScreen)
{

	pScreen->CreateColormap = fbdevCreateColormap;
	return TRUE;
}

Bool fbdevFinishInitScreen(ScreenPtr pScreen)
{
	if (!shadowSetup(pScreen))
		return FALSE;

	if (!fbdevRandRInit(pScreen))
		return FALSE;

	return TRUE;
}

Bool fbdevCreateResources(ScreenPtr pScreen)
{
	return fbdevSetShadow(pScreen);
}

void fbdevPreserve(KdCardInfo * card)
{
}

static int fbdevUpdateFbColormap(FbdevPriv *priv, int minidx, int maxidx)
{
	struct fb_cmap cmap;

	cmap.start = minidx;
	cmap.len = maxidx - minidx + 1;
	cmap.red = &priv->red[minidx];
	cmap.green = &priv->green[minidx];
	cmap.blue = &priv->blue[minidx];
	cmap.transp = 0;

	return ioctl(priv->fd, FBIOPUTCMAP, &cmap);
}

Bool fbdevEnable(ScreenPtr pScreen)
{
	KdScreenPriv(pScreen);
	FbdevPriv *priv = pScreenPriv->card->driver;
	int k;

	priv->var.activate = FB_ACTIVATE_NOW | FB_CHANGE_CMAP_VBL;

	/* display it on the LCD */
	k = ioctl(priv->fd, FBIOPUT_VSCREENINFO, &priv->var);
	if (k < 0) {
		perror("FBIOPUT_VSCREENINFO");
		return FALSE;
	}

	if (priv->fix.visual == FB_VISUAL_DIRECTCOLOR) {
		int i;

		for (i = 0;
		     i < (1 << priv->var.red.length) ||
		     i < (1 << priv->var.green.length) ||
		     i < (1 << priv->var.blue.length); i++) {
			priv->red[i] =
			    i * 65535 / ((1 << priv->var.red.length) - 1);
			priv->green[i] =
			    i * 65535 / ((1 << priv->var.green.length) - 1);
			priv->blue[i] =
			    i * 65535 / ((1 << priv->var.blue.length) - 1);
		}
		i--;
		fbdevUpdateFbColormap(priv, 0, i);
	}
	return TRUE;
}

Bool fbdevDPMS(ScreenPtr pScreen, int mode)
{
	KdScreenPriv(pScreen);
	FbdevPriv *priv = pScreenPriv->card->driver;
	static int oldmode = -1;

	if (mode == oldmode)
		return TRUE;
#ifdef FBIOPUT_POWERMODE
	if (ioctl(priv->fd, FBIOPUT_POWERMODE, &mode) >= 0) {
		oldmode = mode;
		return TRUE;
	}
#endif
#ifdef FBIOBLANK
	if (ioctl(priv->fd, FBIOBLANK, mode ? mode + 1 : 0) >= 0) {
		oldmode = mode;
		return TRUE;
	}
#endif
	return FALSE;
}

void fbdevDisable(ScreenPtr pScreen)
{
}

void fbdevRestore(KdCardInfo * card)
{
}

void fbdevScreenFini(KdScreenInfo * screen)
{
}

void fbdevCardFini(KdCardInfo * card)
{
	FbdevPriv *priv = card->driver;

	munmap(priv->fb_base, priv->fix.smem_len);
	close(priv->fd);
	free(priv);
}

/*
 * Retrieve actual colormap and return selected n entries in pdefs.
 */

void fbdevGetColors(ScreenPtr pScreen, int n, xColorItem * pdefs)
{
	KdScreenPriv(pScreen);
	FbdevPriv *priv = pScreenPriv->card->driver;
	struct fb_cmap cmap;
	int p;
	int k;
	int min, max;

	min = 256;
	max = 0;
	for (k = 0; k < n; k++) {
		if (pdefs[k].pixel < min)
			min = pdefs[k].pixel;
		if (pdefs[k].pixel > max)
			max = pdefs[k].pixel;
	}
	cmap.start = min;
	cmap.len = max - min + 1;
	cmap.red = &priv->red[min];
	cmap.green = &priv->green[min];
	cmap.blue = &priv->blue[min];
	cmap.transp = 0;
	k = ioctl(priv->fd, FBIOGETCMAP, &cmap);
	if (k < 0) {
		perror("can't get colormap");
		return;
	}
	while (n--) {
		p = pdefs->pixel;
		pdefs->red = priv->red[p];
		pdefs->green = priv->green[p];
		pdefs->blue = priv->blue[p];
		pdefs++;
	}
}

/*
 * Change colormap by updating n entries described in pdefs.
 */
void fbdevPutColors(ScreenPtr pScreen, int n, xColorItem * pdefs)
{
	KdScreenPriv(pScreen);
	FbdevPriv *priv = pScreenPriv->card->driver;
	int p;
	int min, max;

	min = 256;
	max = 0;
	while (n--) {
		p = pdefs->pixel;
		priv->red[p] = pdefs->red;
		priv->green[p] = pdefs->green;
		priv->blue[p] = pdefs->blue;
		if (p < min)
			min = p;
		if (p > max)
			max = p;
		pdefs++;
	}

	fbdevUpdateFbColormap(priv, min, max);
}
