/*
 * uterm - Linux User-Space Terminal
 *
 * Copyright (c) 2011-2012 David Herrmann <dh.herrmann@googlemail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/*
 * FBDEV Video backend
 */

#include <errno.h>
#include <fcntl.h>
#include <linux/fb.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#include "log.h"
#include "uterm.h"
#include "uterm_video.h"

#define LOG_SUBSYSTEM "video_fbdev"

static const char *mode_get_name(const struct uterm_mode *mode)
{
	return "<default>";
}

static unsigned int mode_get_width(const struct uterm_mode *mode)
{
	return mode->fbdev.width;
}

static unsigned int mode_get_height(const struct uterm_mode *mode)
{
	return mode->fbdev.height;
}

static int refresh_info(struct uterm_display *disp)
{
	int ret;

	ret = ioctl(disp->fbdev.fd, FBIOGET_FSCREENINFO, &disp->fbdev.finfo);
	if (ret) {
		log_err("cannot get finfo (%d): %m", errno);
		return -EFAULT;
	}

	ret = ioctl(disp->fbdev.fd, FBIOGET_VSCREENINFO, &disp->fbdev.vinfo);
	if (ret) {
		log_err("cannot get vinfo (%d): %m", errno);
		return -EFAULT;
	}

	return 0;
}

static int display_activate_force(struct uterm_display *disp,
				  struct uterm_mode *mode,
				  bool force)
{
	/* TODO: Add support for 24-bpp. However, we need to check how 3-bytes
	 * integers are assembled in big/little/mixed endian systems. */
	static const char depths[] = { 32, 16, 0 };
	struct fb_var_screeninfo *vinfo;
	struct fb_fix_screeninfo *finfo;
	int ret, i;
	uint64_t quot;
	size_t len;
	unsigned int val;

	if (!disp->video || !video_is_awake(disp->video))
		return -EINVAL;
	if (!force && (disp->flags & DISPLAY_ONLINE))
		return 0;

	/* TODO: We do not support explicit modesetting in fbdev, so we require
	 * @mode to be NULL. You can still switch modes via "fbset" on the
	 * console and then restart the app. It will automatically adapt to the
	 * new mode. The only values changed here are bpp and color mode. */
	if (mode)
		return -EINVAL;

	ret = refresh_info(disp);
	if (ret)
		return ret;

	finfo = &disp->fbdev.finfo;
	vinfo = &disp->fbdev.vinfo;

	vinfo->xoffset = 0;
	vinfo->yoffset = 0;
	vinfo->activate = FB_ACTIVATE_NOW | FB_ACTIVATE_FORCE;
	vinfo->xres_virtual = vinfo->xres;
	vinfo->yres_virtual = vinfo->yres * 2;
	disp->flags |= DISPLAY_DBUF;

	/* udlfb is broken as it reports the sizes of the virtual framebuffer
	 * (even mmap() accepts it) but the actual size that we can access
	 * without segfaults is the _real_ framebuffer. Therefore, disable
	 * double-buffering for it.
	 * TODO: fix this kernel-side!
	 * TODO: There are so many broken fbdev drivers that just accept any
	 * virtual FB sizes and then break mmap that we now disable
	 * double-buffering entirely. We might instead add a white-list or
	 * optional command-line argument to re-enable it. */
	if (true || !strcmp(finfo->id, "udlfb")) {
		disp->flags &= ~DISPLAY_DBUF;
		vinfo->yres_virtual = vinfo->yres;
	}

	ret = ioctl(disp->fbdev.fd, FBIOPUT_VSCREENINFO, vinfo);
	if (ret) {
		disp->flags &= ~DISPLAY_DBUF;
		vinfo->yres_virtual = vinfo->yres;
		ret = ioctl(disp->fbdev.fd, FBIOPUT_VSCREENINFO, vinfo);
		if (ret) {
			log_debug("cannot reset fb offsets (%d): %m", errno);
			return -EFAULT;
		}
	}

	if (disp->flags & DISPLAY_DBUF)
		log_debug("enabling double buffering");
	else
		log_debug("disabling double buffering");

	ret = refresh_info(disp);
	if (ret)
		return ret;

	/* We require TRUECOLOR mode here. That is, each pixel has a color value
	 * that is split into rgba values that we can set directly. Other visual
	 * modes like pseudocolor or direct-color do not provide this. As I have
	 * never seen a device that does not support TRUECOLOR, I think we can
	 * ignore them here. */
	if (finfo->visual != FB_VISUAL_TRUECOLOR ||
	    vinfo->bits_per_pixel != 32) {
		for (i = 0; depths[i]; ++i) {
			vinfo->bits_per_pixel = depths[i];
			vinfo->activate = FB_ACTIVATE_NOW | FB_ACTIVATE_FORCE;

			ret = ioctl(disp->fbdev.fd, FBIOPUT_VSCREENINFO,
				    vinfo);
			if (ret < 0)
				continue;

			ret = refresh_info(disp);
			if (ret)
				return ret;

			if (finfo->visual == FB_VISUAL_TRUECOLOR)
				break;
		}
	}

	if (vinfo->xres_virtual < vinfo->xres ||
	    (disp->flags & DISPLAY_DBUF &&
	     vinfo->yres_virtual < vinfo->yres * 2) ||
	    vinfo->yres_virtual < vinfo->yres) {
		log_warning("device %s has weird virtual buffer sizes (%d %d %d %d)",
			    disp->fbdev.node, vinfo->xres, vinfo->xres_virtual,
			    vinfo->yres, vinfo->yres_virtual);
	}

	if (vinfo->bits_per_pixel != 32 &&
	    vinfo->bits_per_pixel != 16) {
		log_error("device %s does not support 16/32 bpp but: %u",
			  disp->fbdev.node, vinfo->bits_per_pixel);
		return -EFAULT;
	}

	if (finfo->visual != FB_VISUAL_TRUECOLOR) {
		log_error("device %s does not support true-color",
			  disp->fbdev.node);
		return -EFAULT;
	}

	if (vinfo->red.length > 8 ||
	    vinfo->green.length > 8 ||
	    vinfo->blue.length > 8) {
		log_error("device %s uses unusual color-ranges",
			  disp->fbdev.node);
		return -EFAULT;
	}

	log_info("activating display %s to %ux%u %u bpp", disp->fbdev.node,
		 vinfo->xres, vinfo->yres, vinfo->bits_per_pixel);

	/* calculate monitor rate, default is 60 Hz */
	quot = (vinfo->upper_margin + vinfo->lower_margin + vinfo->yres);
	quot *= (vinfo->left_margin + vinfo->right_margin + vinfo->xres);
	quot *= vinfo->pixclock;
	if (quot) {
		disp->fbdev.rate = 1000000000000000LLU / quot;
	} else {
		disp->fbdev.rate = 60 * 1000;
		log_warning("cannot read monitor refresh rate, forcing 60 Hz");
	}

	if (disp->fbdev.rate == 0) {
		log_warning("monitor refresh rate is 0 Hz, forcing it to 1 Hz");
		disp->fbdev.rate = 1;
	} else if (disp->fbdev.rate > 200000) {
		log_warning("monitor refresh rate is >200 Hz (%u Hz), forcing it to 200 Hz",
			    disp->fbdev.rate / 1000);
		disp->fbdev.rate = 200000;
	}

	val = 1000000 / disp->fbdev.rate;
	display_set_vblank_timer(disp, val);
	log_debug("vblank timer: %u ms, monitor refresh rate: %u Hz", val,
		  disp->fbdev.rate / 1000);

	len = finfo->line_length * vinfo->yres;
	if (disp->flags & DISPLAY_DBUF)
		len *= 2;

	disp->fbdev.map = mmap(0, len, PROT_WRITE, MAP_SHARED,
			       disp->fbdev.fd, 0);
	if (disp->fbdev.map == MAP_FAILED) {
		log_error("cannot mmap device %s (%d): %m", disp->fbdev.node,
			  errno);
		return -EFAULT;
	}

	memset(disp->fbdev.map, 0, len);
	disp->fbdev.xres = vinfo->xres;
	disp->fbdev.yres = vinfo->yres;
	disp->fbdev.len = len;
	disp->fbdev.stride = finfo->line_length;
	disp->fbdev.bufid = 0;
	disp->fbdev.Bpp = vinfo->bits_per_pixel / 8;
	disp->fbdev.off_r = vinfo->red.offset;
	disp->fbdev.len_r = vinfo->red.length;
	disp->fbdev.off_g = vinfo->green.offset;
	disp->fbdev.len_g = vinfo->green.length;
	disp->fbdev.off_b = vinfo->blue.offset;
	disp->fbdev.len_b = vinfo->blue.length;
	disp->fbdev.dither_r = 0;
	disp->fbdev.dither_g = 0;
	disp->fbdev.dither_b = 0;
	disp->fbdev.xrgb32 = false;
	if (disp->fbdev.len_r == 8 &&
	    disp->fbdev.len_g == 8 &&
	    disp->fbdev.len_b == 8 &&
	    disp->fbdev.off_r == 16 &&
	    disp->fbdev.off_g ==  8 &&
	    disp->fbdev.off_b ==  0 &&
	    disp->fbdev.Bpp == 4)
		disp->fbdev.xrgb32 = true;

	/* TODO: make dithering configurable */
	disp->flags |= DISPLAY_DITHERING;

	if (!disp->current_mode) {
		ret = mode_new(&disp->modes, &fbdev_mode_ops);
		if (ret) {
			munmap(disp->fbdev.map, disp->fbdev.len);
			return ret;
		}
		disp->modes->fbdev.width = disp->fbdev.xres;
		disp->modes->fbdev.height = disp->fbdev.yres;
		disp->current_mode = disp->modes;
	}

	disp->flags |= DISPLAY_ONLINE;
	return 0;
}

static int display_activate(struct uterm_display *disp, struct uterm_mode *mode)
{
	return display_activate_force(disp, mode, false);
}

static void display_deactivate_force(struct uterm_display *disp, bool force)
{
	if (!disp->video || !(disp->flags & DISPLAY_ONLINE))
		return;

	log_info("deactivating device %s", disp->fbdev.node);

	if (!force)
		uterm_mode_unref(disp->current_mode);
	disp->modes = NULL;
	disp->current_mode = NULL;
	memset(disp->fbdev.map, 0, disp->fbdev.len);
	munmap(disp->fbdev.map, disp->fbdev.len);

	if (!force)
		disp->flags &= ~DISPLAY_ONLINE;
}

static void display_deactivate(struct uterm_display *disp)
{
	return display_deactivate_force(disp, false);
}

static int display_set_dpms(struct uterm_display *disp, int state)
{
	int set, ret;

	if (!disp->video || !(disp->flags & DISPLAY_ONLINE))
		return -EINVAL;

	switch (state) {
	case UTERM_DPMS_ON:
		set = FB_BLANK_UNBLANK;
		break;
	case UTERM_DPMS_STANDBY:
		set = FB_BLANK_NORMAL;
		break;
	case UTERM_DPMS_SUSPEND:
		set = FB_BLANK_NORMAL;
		break;
	case UTERM_DPMS_OFF:
		set = FB_BLANK_POWERDOWN;
		break;
	default:
		return -EINVAL;
	}

	log_info("setting DPMS of device %p to %s", disp->fbdev.node,
		 uterm_dpms_to_name(state));

	ret = ioctl(disp->fbdev.fd, FBIOBLANK, set);
	if (ret) {
		log_error("cannot set DPMS on %s (%d): %m", disp->fbdev.node,
			  errno);
		return -EFAULT;
	}

	disp->dpms = state;
	return 0;
}

static int display_swap(struct uterm_display *disp)
{
	struct fb_var_screeninfo *vinfo;
	int ret;

	if (!disp->video || !video_is_awake(disp->video))
		return -EINVAL;
	if (!(disp->flags & DISPLAY_ONLINE))
		return -EINVAL;

	if (!(disp->flags & DISPLAY_DBUF)) {
		return display_schedule_vblank_timer(disp);
	}

	vinfo = &disp->fbdev.vinfo;
	vinfo->activate = FB_ACTIVATE_VBL;

	if (!disp->fbdev.bufid)
		vinfo->yoffset = disp->fbdev.yres;
	else
		vinfo->yoffset = 0;

	ret = ioctl(disp->fbdev.fd, FBIOPUT_VSCREENINFO, vinfo);
	if (ret) {
		log_warning("cannot swap buffers on %s (%d): %m",
			    disp->fbdev.node, errno);
		return -EFAULT;
	}

	disp->fbdev.bufid ^= 1;
	return display_schedule_vblank_timer(disp);
}

static int clamp_value(int val, int low, int up)
{
	if (val < low)
		return low;
	else if (val > up)
		return up;
	else
		return val;
}

static uint_fast32_t xrgb32_to_device(struct uterm_display *disp,
				      uint32_t pixel)
{
	uint8_t r, g, b, nr, ng, nb;
	int i;
	uint_fast32_t res;

	r = (pixel >> 16) & 0xff;
	g = (pixel >>  8) & 0xff;
	b = (pixel >>  0) & 0xff;

	if (disp->flags & DISPLAY_DITHERING) {
		/* This is some very basic dithering which simply does small
		 * rotations in the lower pixel bits. TODO: Let's take a look
		 * at Floyd-Steinberg dithering which should give much better
		 * results. It is slightly slower, though.
		 * Or even better would be some Sierra filter like the Sierra
		 * LITE. */
		disp->fbdev.dither_r = r - disp->fbdev.dither_r;
		disp->fbdev.dither_g = g - disp->fbdev.dither_g;
		disp->fbdev.dither_b = b - disp->fbdev.dither_b;
		r = clamp_value(disp->fbdev.dither_r, 0, 255) >> (8 - disp->fbdev.len_r);
		g = clamp_value(disp->fbdev.dither_g, 0, 255) >> (8 - disp->fbdev.len_g);
		b = clamp_value(disp->fbdev.dither_b, 0, 255) >> (8 - disp->fbdev.len_b);
		nr = r << (8 - disp->fbdev.len_r);
		ng = g << (8 - disp->fbdev.len_g);
		nb = b << (8 - disp->fbdev.len_b);

		for (i = disp->fbdev.len_r; i < 8; i <<= 1)
			nr |= nr >> i;
		for (i = disp->fbdev.len_g; i < 8; i <<= 1)
			ng |= ng >> i;
		for (i = disp->fbdev.len_b; i < 8; i <<= 1)
			nb |= nb >> i;

		disp->fbdev.dither_r = nr - disp->fbdev.dither_r;
		disp->fbdev.dither_g = ng - disp->fbdev.dither_g;
		disp->fbdev.dither_b = nb - disp->fbdev.dither_b;

		res  = r << disp->fbdev.off_r;
		res |= g << disp->fbdev.off_g;
		res |= b << disp->fbdev.off_b;
	} else {
		res  = (r >> (8 - disp->fbdev.len_r)) << disp->fbdev.off_r;
		res |= (g >> (8 - disp->fbdev.len_g)) << disp->fbdev.off_g;
		res |= (b >> (8 - disp->fbdev.len_b)) << disp->fbdev.off_b;
	}

	return res;
}

static int display_blit(struct uterm_display *disp,
			const struct uterm_video_buffer *buf,
			unsigned int x, unsigned int y)
{
	unsigned int tmp;
	uint8_t *dst, *src;
	unsigned int width, height, i;
	uint32_t val;

	if (!disp->video || !(disp->flags & DISPLAY_ONLINE))
		return -EINVAL;
	if (!buf || !video_is_awake(disp->video))
		return -EINVAL;
	if (buf->format != UTERM_FORMAT_XRGB32)
		return -EINVAL;

	tmp = x + buf->width;
	if (tmp < x || x >= disp->fbdev.xres)
		return -EINVAL;
	if (tmp > disp->fbdev.xres)
		width = disp->fbdev.xres - x;
	else
		width = buf->width;

	tmp = y + buf->height;
	if (tmp < y || y >= disp->fbdev.yres)
		return -EINVAL;
	if (tmp > disp->fbdev.yres)
		height = disp->fbdev.yres - y;
	else
		height = buf->height;

	if (!(disp->flags & DISPLAY_DBUF) || disp->fbdev.bufid)
		dst = disp->fbdev.map;
	else
		dst = &disp->fbdev.map[disp->fbdev.yres * disp->fbdev.stride];
	dst = &dst[y * disp->fbdev.stride + x * disp->fbdev.Bpp];
	src = buf->data;

	if (disp->fbdev.xrgb32) {
		while (height--) {
			memcpy(dst, src, 4 * width);
			dst += disp->fbdev.stride;
			src += buf->stride;
		}
	} else if (disp->fbdev.Bpp == 2) {
		while (height--) {
			for (i = 0; i < width; ++i) {
				val = ((uint32_t*)src)[i];
				((uint16_t*)dst)[i] = xrgb32_to_device(disp, val);
			}
			dst += disp->fbdev.stride;
			src += buf->stride;
		}
	} else if (disp->fbdev.Bpp == 4) {
		while (height--) {
			for (i = 0; i < width; ++i) {
				val = ((uint32_t*)src)[i];
				((uint32_t*)dst)[i] = xrgb32_to_device(disp, val);
			}
			dst += disp->fbdev.stride;
			src += buf->stride;
		}
	} else {
		log_debug("invalid Bpp");
	}

	return 0;
}

static int display_blend(struct uterm_display *disp,
			 const struct uterm_video_buffer *buf,
			 unsigned int x, unsigned int y,
			 uint8_t fr, uint8_t fg, uint8_t fb,
			 uint8_t br, uint8_t bg, uint8_t bb)
{
	unsigned int tmp;
	uint8_t *dst, *src;
	unsigned int width, height, i;
	unsigned int r, g, b;
	uint32_t val;

	if (!disp->video || !(disp->flags & DISPLAY_ONLINE))
		return -EINVAL;
	if (!buf || !video_is_awake(disp->video))
		return -EINVAL;
	if (buf->format != UTERM_FORMAT_GREY)
		return -EINVAL;

	tmp = x + buf->width;
	if (tmp < x || x >= disp->fbdev.xres)
		return -EINVAL;
	if (tmp > disp->fbdev.xres)
		width = disp->fbdev.xres - x;
	else
		width = buf->width;

	tmp = y + buf->height;
	if (tmp < y || y >= disp->fbdev.yres)
		return -EINVAL;
	if (tmp > disp->fbdev.yres)
		height = disp->fbdev.yres - y;
	else
		height = buf->height;

	if (!(disp->flags & DISPLAY_DBUF) || disp->fbdev.bufid)
		dst = disp->fbdev.map;
	else
		dst = &disp->fbdev.map[disp->fbdev.yres * disp->fbdev.stride];
	dst = &dst[y * disp->fbdev.stride + x * disp->fbdev.Bpp];
	src = buf->data;

	/* Division by 256 instead of 255 increases
	 * speed by like 20% on slower machines.
	 * Downside is, full white is 254/254/254
	 * instead of 255/255/255. */
	if (disp->fbdev.xrgb32) {
		while (height--) {
			for (i = 0; i < width; ++i) {
				if (src[i] == 0) {
					r = br;
					g = bg;
					b = bb;
				} else if (src[i] == 255) {
					r = fr;
					g = fg;
					b = fb;
				} else {
					r = fr * src[i] +
					    br * (255 - src[i]);
					r /= 256;
					g = fg * src[i] +
					    bg * (255 - src[i]);
					g /= 256;
					b = fb * src[i] +
					    bb * (255 - src[i]);
					b /= 256;
				}
				val = (r << 16) | (g << 8) | b;
				((uint32_t*)dst)[i] = val;
			}
			dst += disp->fbdev.stride;
			src += buf->stride;
		}
	} else if (disp->fbdev.Bpp == 2) {
		while (height--) {
			for (i = 0; i < width; ++i) {
				if (src[i] == 0) {
					r = br;
					g = bg;
					b = bb;
				} else if (src[i] == 255) {
					r = fr;
					g = fg;
					b = fb;
				} else {
					r = fr * src[i] +
					    br * (255 - src[i]);
					r /= 256;
					g = fg * src[i] +
					    bg * (255 - src[i]);
					g /= 256;
					b = fb * src[i] +
					    bb * (255 - src[i]);
					b /= 256;
				}
				val = (r << 16) | (g << 8) | b;
				((uint16_t*)dst)[i] = xrgb32_to_device(disp,
								       val);
			}
			dst += disp->fbdev.stride;
			src += buf->stride;
		}
	} else if (disp->fbdev.Bpp == 4) {
		while (height--) {
			for (i = 0; i < width; ++i) {
				if (src[i] == 0) {
					r = br;
					g = bg;
					b = bb;
				} else if (src[i] == 255) {
					r = fr;
					g = fg;
					b = fb;
				} else {
					r = fr * src[i] +
					    br * (255 - src[i]);
					r /= 256;
					g = fg * src[i] +
					    bg * (255 - src[i]);
					g /= 256;
					b = fb * src[i] +
					    bb * (255 - src[i]);
					b /= 256;
				}
				val = (r << 16) | (g << 8) | b;
				((uint32_t*)dst)[i] = xrgb32_to_device(disp,
								       val);
			}
			dst += disp->fbdev.stride;
			src += buf->stride;
		}
	} else {
		log_warning("invalid Bpp");
	}

	return 0;
}

static int display_fake_blendv(struct uterm_display *disp,
			       const struct uterm_video_blend_req *req,
			       size_t num)
{
	unsigned int tmp;
	uint8_t *dst, *src;
	unsigned int width, height, i, j;
	unsigned int r, g, b;
	uint32_t val;

	if (!disp->video || !(disp->flags & DISPLAY_ONLINE))
		return -EINVAL;
	if (!req || !video_is_awake(disp->video))
		return -EINVAL;

	for (j = 0; j < num; ++j, ++req) {
		if (!req->buf)
			continue;

		if (req->buf->format != UTERM_FORMAT_GREY)
			return -EOPNOTSUPP;

		tmp = req->x + req->buf->width;
		if (tmp < req->x || req->x >= disp->fbdev.xres)
			return -EINVAL;
		if (tmp > disp->fbdev.xres)
			width = disp->fbdev.xres - req->x;
		else
			width = req->buf->width;

		tmp = req->y + req->buf->height;
		if (tmp < req->y || req->y >= disp->fbdev.yres)
			return -EINVAL;
		if (tmp > disp->fbdev.yres)
			height = disp->fbdev.yres - req->y;
		else
			height = req->buf->height;

		if (!(disp->flags & DISPLAY_DBUF) || disp->fbdev.bufid)
			dst = disp->fbdev.map;
		else
			dst = &disp->fbdev.map[disp->fbdev.yres * disp->fbdev.stride];
		dst = &dst[req->y * disp->fbdev.stride + req->x * disp->fbdev.Bpp];
		src = req->buf->data;

		/* Division by 256 instead of 255 increases
		 * speed by like 20% on slower machines.
		 * Downside is, full white is 254/254/254
		 * instead of 255/255/255. */
		if (disp->fbdev.xrgb32) {
			while (height--) {
				for (i = 0; i < width; ++i) {
					if (src[i] == 0) {
						r = req->br;
						g = req->bg;
						b = req->bb;
					} else if (src[i] == 255) {
						r = req->fr;
						g = req->fg;
						b = req->fb;
					} else {
						r = req->fr * src[i] +
						    req->br * (255 - src[i]);
						r /= 256;
						g = req->fg * src[i] +
						    req->bg * (255 - src[i]);
						g /= 256;
						b = req->fb * src[i] +
						    req->bb * (255 - src[i]);
						b /= 256;
					}
					val = (r << 16) | (g << 8) | b;
					((uint32_t*)dst)[i] = val;
				}
				dst += disp->fbdev.stride;
				src += req->buf->stride;
			}
		} else if (disp->fbdev.Bpp == 2) {
			while (height--) {
				for (i = 0; i < width; ++i) {
					if (src[i] == 0) {
						r = req->br;
						g = req->bg;
						b = req->bb;
					} else if (src[i] == 255) {
						r = req->fr;
						g = req->fg;
						b = req->fb;
					} else {
						r = req->fr * src[i] +
						    req->br * (255 - src[i]);
						r /= 256;
						g = req->fg * src[i] +
						    req->bg * (255 - src[i]);
						g /= 256;
						b = req->fb * src[i] +
						    req->bb * (255 - src[i]);
						b /= 256;
					}
					val = (r << 16) | (g << 8) | b;
					((uint16_t*)dst)[i] =
						xrgb32_to_device(disp, val);
				}
				dst += disp->fbdev.stride;
				src += req->buf->stride;
			}
		} else if (disp->fbdev.Bpp == 4) {
			while (height--) {
				for (i = 0; i < width; ++i) {
					if (src[i] == 0) {
						r = req->br;
						g = req->bg;
						b = req->bb;
					} else if (src[i] == 255) {
						r = req->fr;
						g = req->fg;
						b = req->fb;
					} else {
						r = req->fr * src[i] +
						    req->br * (255 - src[i]);
						r /= 256;
						g = req->fg * src[i] +
						    req->bg * (255 - src[i]);
						g /= 256;
						b = req->fb * src[i] +
						    req->bb * (255 - src[i]);
						b /= 256;
					}
					val = (r << 16) | (g << 8) | b;
					((uint32_t*)dst)[i] =
						xrgb32_to_device(disp, val);
				}
				dst += disp->fbdev.stride;
				src += req->buf->stride;
			}
		} else {
			log_warning("invalid Bpp");
		}
	}

	return 0;
}

static int display_fill(struct uterm_display *disp,
			uint8_t r, uint8_t g, uint8_t b,
			unsigned int x, unsigned int y,
			unsigned int width, unsigned int height)
{
	unsigned int tmp, i;
	uint8_t *dst;
	uint32_t full_val, rgb32;

	if (!disp->video || !(disp->flags & DISPLAY_ONLINE))
		return -EINVAL;
	if (!video_is_awake(disp->video))
		return -EINVAL;

	tmp = x + width;
	if (tmp < x || x >= disp->fbdev.xres)
		return -EINVAL;
	if (tmp > disp->fbdev.xres)
		width = disp->fbdev.xres - x;
	tmp = y + height;
	if (tmp < y || y >= disp->fbdev.yres)
		return -EINVAL;
	if (tmp > disp->fbdev.yres)
		height = disp->fbdev.yres - y;

	if (!(disp->flags & DISPLAY_DBUF) || disp->fbdev.bufid)
		dst = disp->fbdev.map;
	else
		dst = &disp->fbdev.map[disp->fbdev.yres * disp->fbdev.stride];
	dst = &dst[y * disp->fbdev.stride + x * disp->fbdev.Bpp];

	full_val  = ((r & 0xff) >> (8 - disp->fbdev.len_r)) << disp->fbdev.off_r;
	full_val |= ((g & 0xff) >> (8 - disp->fbdev.len_g)) << disp->fbdev.off_g;
	full_val |= ((b & 0xff) >> (8 - disp->fbdev.len_b)) << disp->fbdev.off_b;

	if (disp->fbdev.Bpp == 2) {
		if (disp->flags & DISPLAY_DITHERING) {
			rgb32  = (r & 0xff) << 16;
			rgb32 |= (g & 0xff) <<  8;
			rgb32 |= (b & 0xff) <<  0;
			while (height--) {
				for (i = 0; i < width; ++i)
					((uint16_t*)dst)[i] = xrgb32_to_device(disp, rgb32);
				dst += disp->fbdev.stride;
			}
		} else {
			full_val &= 0xffff;
			while (height--) {
				for (i = 0; i < width; ++i)
					((uint16_t*)dst)[i] = full_val;
				dst += disp->fbdev.stride;
			}
		}
	} else if (disp->fbdev.Bpp == 4) {
		while (height--) {
			for (i = 0; i < width; ++i)
				((uint32_t*)dst)[i] = full_val;
			dst += disp->fbdev.stride;
		}
	} else {
		log_error("invalid Bpp");
		return -EFAULT;
	}

	return 0;
}

static void intro_idle_event(struct ev_eloop *eloop, void *unused, void *data)
{
	struct uterm_display *disp = data;

	if (!disp->fbdev.pending_intro)
		return;

	disp->fbdev.pending_intro = false;
	ev_eloop_unregister_idle_cb(eloop, intro_idle_event, disp);

	if (!disp->video)
		return;

	VIDEO_CB(disp->video, disp, UTERM_NEW);
}

static int video_init(struct uterm_video *video, const char *node)
{
	int ret;
	struct uterm_display *disp;

	ret = display_new(&disp, &fbdev_display_ops, video);
	if (ret)
		return ret;

	ret = ev_eloop_register_idle_cb(video->eloop, intro_idle_event, disp);
	if (ret) {
		log_error("cannot register idle event: %d", ret);
		goto err_free;
	}
	disp->fbdev.pending_intro = true;

	disp->fbdev.node = strdup(node);
	if (!disp->fbdev.node) {
		log_err("cannot dup node name");
		ret = -ENOMEM;
		goto err_idle;
	}

	disp->fbdev.fd = open(node, O_RDWR | O_CLOEXEC);
	if (disp->fbdev.fd < 0) {
		log_err("cannot open %s (%d): %m", node, errno);
		ret = -EFAULT;
		goto err_node;
	}

	disp->dpms = UTERM_DPMS_UNKNOWN;
	video->displays = disp;

	log_info("new device on %s", disp->fbdev.node);
	return 0;

err_node:
	free(disp->fbdev.node);
err_idle:
	ev_eloop_register_idle_cb(video->eloop, intro_idle_event, disp);
err_free:
	uterm_display_unref(disp);
	return ret;
}

static void video_destroy(struct uterm_video *video)
{
	struct uterm_display *disp;

	log_info("free device %p", video);
	disp = video->displays;
	video->displays = disp->next;

	if (disp->fbdev.pending_intro)
		ev_eloop_unregister_idle_cb(video->eloop, intro_idle_event,
					    disp);
	else
		VIDEO_CB(video, disp, UTERM_GONE);

	close(disp->fbdev.fd);
	free(disp->fbdev.node);
	uterm_display_unref(disp);
}

static void video_sleep(struct uterm_video *video)
{
	if (!(video->flags & VIDEO_AWAKE))
		return;

	display_deactivate_force(video->displays, true);
	video->flags &= ~VIDEO_AWAKE;
}

static int video_wake_up(struct uterm_video *video)
{
	int ret;

	if (video->flags & VIDEO_AWAKE)
		return 0;

	video->flags |= VIDEO_AWAKE;
	if (video->displays->flags & DISPLAY_ONLINE) {
		ret = display_activate_force(video->displays, NULL, true);
		if (ret) {
			video->flags &= ~VIDEO_AWAKE;
			return ret;
		}
	}

	return 0;
}

const struct mode_ops fbdev_mode_ops = {
	.init = NULL,
	.destroy = NULL,
	.get_name = mode_get_name,
	.get_width = mode_get_width,
	.get_height = mode_get_height,
};

const struct display_ops fbdev_display_ops = {
	.init = NULL,
	.destroy = NULL,
	.activate = display_activate,
	.deactivate = display_deactivate,
	.set_dpms = display_set_dpms,
	.use = NULL,
	.swap = display_swap,
	.blit = display_blit,
	.blend = display_blend,
	.blendv = display_fake_blendv,
	.fake_blendv = display_fake_blendv,
	.fill = display_fill,
};

const struct video_ops fbdev_video_ops = {
	.init = video_init,
	.destroy = video_destroy,
	.segfault = NULL, /* TODO */
	.use = NULL,
	.poll = NULL,
	.sleep = video_sleep,
	.wake_up = video_wake_up,
};
