/*
 * video driver for framebuffer device
 * copyright (C) 2003 Joey Parrish <joey@nicewarrior.org>
 *
 * This file is part of MPlayer.
 *
 * MPlayer 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.
 *
 * MPlayer 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 MPlayer; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#include <sys/mman.h>
#include <sys/ioctl.h>
#include <linux/fb.h>

#include "config.h"
#include "video_out.h"
#include "video_out_internal.h"
#include "fastmemcpy.h"
#include "sub/sub.h"
#include "mp_msg.h"
#include "aspect.h"
#include "libavutil/common.h"

static const vo_info_t info = {
	"Framebuffer Device",
	"fbdev2",
	"Joey Parrish <joey@nicewarrior.org>",
	""
};

const LIBVO_EXTERN(fbdev2)

static void set_bpp(struct fb_var_screeninfo *p, int bpp)
{
	p->bits_per_pixel = (bpp + 1) & ~1;
	p->red.msb_right = p->green.msb_right = p->blue.msb_right = p->transp.msb_right = 0;
	p->transp.offset = p->transp.length = 0;
	p->blue.offset = 0;
	switch (bpp) {
		case 32:
			p->transp.offset = 24;
			p->transp.length = 8;
		case 24:
			p->red.offset = 16;
			p->red.length = 8;
			p->green.offset = 8;
			p->green.length = 8;
			p->blue.length = 8;
			break;
		case 16:
			p->red.offset = 11;
			p->green.length = 6;
			p->red.length = 5;
			p->green.offset = 5;
			p->blue.length = 5;
			break;
		case 15:
			p->red.offset = 10;
			p->green.length = 5;
			p->red.length = 5;
			p->green.offset = 5;
			p->blue.length = 5;
			break;
		case 12:
			p->red.offset   = 8;
			p->green.length = 4;
			p->red.length   = 4;
			p->green.offset = 4;
			p->blue.length  = 4;
			break;
	}
}

static char *fb_dev_name = NULL; // such as /dev/fb0
static int fb_dev_fd; // handle for fb_dev_name
static uint8_t *frame_buffer = NULL; // mmap'd access to fbdev
static uint8_t *center = NULL; // where to begin writing our image (centered?)
static struct fb_fix_screeninfo fb_finfo; // fixed info
static struct fb_var_screeninfo fb_vinfo; // variable info
static struct fb_var_screeninfo fb_orig_vinfo; // variable info to restore later
static unsigned short fb_ored[256], fb_ogreen[256], fb_oblue[256];
static struct fb_cmap fb_oldcmap = { 0, 256, fb_ored, fb_ogreen, fb_oblue };
static int fb_cmap_changed = 0; //  to restore map
static int fb_pixel_size;	// 32:  4  24:  3  16:  2  15:  2
static int fb_bpp;		// 32: 32  24: 24  16: 16  15: 15
static size_t fb_size; // size of frame_buffer
static int fb_line_len; // length of one line in bytes
static void (*draw_alpha_p)(int w, int h, unsigned char *src,
		unsigned char *srca, int stride, unsigned char *dst,
		int dstride);

static uint8_t *next_frame = NULL; // for double buffering
static int in_width;
static int in_height;

static struct fb_cmap *make_directcolor_cmap(struct fb_var_screeninfo *var)
{
  int i, cols, rcols, gcols, bcols;
  uint16_t *red, *green, *blue;
  struct fb_cmap *cmap;

  rcols = 1 << var->red.length;
  gcols = 1 << var->green.length;
  bcols = 1 << var->blue.length;

  /* Make our palette the length of the deepest color */
  cols = FFMAX3(rcols, gcols, bcols);

  red = malloc(3 * cols * sizeof(red[0]));
  if(!red) {
	  mp_msg(MSGT_VO, MSGL_ERR, "[fbdev2] Can't allocate red palette with %d entries.\n", cols);
	  return NULL;
  }
  green = red   + cols;
  blue  = green + cols;
  for (i = 0; i < cols; i++) {
    red[i]   = (65535/(rcols-1)) * i;
    green[i] = (65535/(gcols-1)) * i;
    blue[i]  = (65535/(bcols-1)) * i;
  }

  cmap = malloc(sizeof(struct fb_cmap));
  if(!cmap) {
	  mp_msg(MSGT_VO, MSGL_ERR, "[fbdev2] Can't allocate color map\n");
	  free(red);
	  return NULL;
  }
  cmap->start = 0;
  cmap->transp = 0;
  cmap->len = cols;
  cmap->red = red;
  cmap->blue = blue;
  cmap->green = green;
  cmap->transp = NULL;

  return cmap;
}

static int fb_preinit(int reset)
{
	static int fb_preinit_done = 0;
	static int fb_err = -1;

	if (reset) {
		fb_preinit_done = 0;
		return 0;
	}

	if (fb_preinit_done)
		return fb_err;
	fb_preinit_done = 1;

	if (!fb_dev_name && !(fb_dev_name = getenv("FRAMEBUFFER")))
		fb_dev_name = strdup("/dev/fb0");

	mp_msg(MSGT_VO, MSGL_V, "[fbdev2] Using device %s\n", fb_dev_name);

	if ((fb_dev_fd = open(fb_dev_name, O_RDWR)) == -1) {
		mp_msg(MSGT_VO, MSGL_ERR, "[fbdev2] Can't open %s: %s\n", fb_dev_name, strerror(errno));
		goto err_out;
	}
	if (ioctl(fb_dev_fd, FBIOGET_VSCREENINFO, &fb_vinfo)) {
		mp_msg(MSGT_VO, MSGL_ERR, "[fbdev2] Can't get VSCREENINFO: %s\n", strerror(errno));
		goto err_out;
	}
	fb_orig_vinfo = fb_vinfo;

	fb_bpp = fb_vinfo.bits_per_pixel;

	/* 16 and 15 bpp is reported as 16 bpp */
	if (fb_bpp == 16)
		fb_bpp = fb_vinfo.red.length + fb_vinfo.green.length +
			fb_vinfo.blue.length;

	fb_err = 0;
	return 0;
err_out:
	if (fb_dev_fd >= 0) close(fb_dev_fd);
	fb_dev_fd = -1;
	fb_err = -1;
	return -1;
}

static int preinit(const char *subdevice)
{
	if (subdevice)
	{
	    free(fb_dev_name);
	    fb_dev_name = strdup(subdevice);
	}
	return fb_preinit(0);
}

static int config(uint32_t width, uint32_t height, uint32_t d_width,
		uint32_t d_height, uint32_t flags, char *title,
		uint32_t format)
{
	struct fb_cmap *cmap;
	int fs = flags & VOFLAG_FULLSCREEN;
	int x_offset = vo_dx + (d_width  - width ) / 2;
	int y_offset = vo_dy + (d_height - height) / 2;
	x_offset = av_clip(x_offset, 0, fb_vinfo.xres - width);
	y_offset = av_clip(y_offset, 0, fb_vinfo.yres - height);

	in_width = width;
	in_height = height;

	if (fb_vinfo.xres < in_width || fb_vinfo.yres < in_height) {
		mp_msg(MSGT_VO, MSGL_ERR, "[fbdev2] Screensize is smaller than video size (%dx%d < %dx%d)\n",
		    fb_vinfo.xres, fb_vinfo.yres, in_width, in_height);
		return 1;
	}

	switch (fb_bpp) {
		case 32: draw_alpha_p = vo_draw_alpha_rgb32; break;
		case 24: draw_alpha_p = vo_draw_alpha_rgb24; break;
		case 16: draw_alpha_p = vo_draw_alpha_rgb16; break;
		case 15: draw_alpha_p = vo_draw_alpha_rgb15; break;
		case 12: draw_alpha_p = vo_draw_alpha_rgb12; break;
		default: return 1;
	}

	if (vo_config_count == 0) {
		if (ioctl(fb_dev_fd, FBIOGET_FSCREENINFO, &fb_finfo)) {
			mp_msg(MSGT_VO, MSGL_ERR, "[fbdev2] Can't get FSCREENINFO: %s\n", strerror(errno));
			return 1;
		}

		if (fb_finfo.type != FB_TYPE_PACKED_PIXELS) {
			mp_msg(MSGT_VO, MSGL_ERR, "[fbdev2] type %d not supported\n", fb_finfo.type);
			return 1;
		}

		switch (fb_finfo.visual) {
			case FB_VISUAL_TRUECOLOR:
				break;
			case FB_VISUAL_DIRECTCOLOR:
				mp_msg(MSGT_VO, MSGL_V, "[fbdev2] creating cmap for directcolor\n");
				if (ioctl(fb_dev_fd, FBIOGETCMAP, &fb_oldcmap)) {
					mp_msg(MSGT_VO, MSGL_ERR, "[fbdev2] can't get cmap: %s\n", strerror(errno));
					return 1;
				}
				if (!(cmap = make_directcolor_cmap(&fb_vinfo)))
					return 1;
				if (ioctl(fb_dev_fd, FBIOPUTCMAP, cmap)) {
					mp_msg(MSGT_VO, MSGL_ERR, "[fbdev2] can't put cmap: %s\n", strerror(errno));
					free(cmap->red);
					free(cmap);
					return 1;
				}
				fb_cmap_changed = 1;
				free(cmap->red);
				free(cmap);
				break;
			default:
				mp_msg(MSGT_VO, MSGL_ERR, "[fbdev2] visual: %d not yet supported\n", fb_finfo.visual);
				return 1;
		}

		fb_size = fb_finfo.smem_len;
		fb_line_len = fb_finfo.line_length;
		if ((frame_buffer = (uint8_t *) mmap(0, fb_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_dev_fd, 0)) == (uint8_t *) -1) {
			mp_msg(MSGT_VO, MSGL_ERR, "[fbdev2] Can't mmap %s: %s\n", fb_dev_name, strerror(errno));
			return 1;
		}
	}

	center = frame_buffer +
	         x_offset * fb_pixel_size +
		 y_offset * fb_line_len;

#ifndef USE_CONVERT2FB
	if (!(next_frame = realloc(next_frame, in_width * in_height * fb_pixel_size))) {
		mp_msg(MSGT_VO, MSGL_ERR, "[fbdev2] Can't malloc next_frame: %s\n", strerror(errno));
		return 1;
	}
#endif
	if (fs) memset(frame_buffer, '\0', fb_line_len * fb_vinfo.yres);

	return 0;
}

static int query_format(uint32_t format)
{
	// open the device, etc.
	if (fb_preinit(0)) return 0;
	if ((format & IMGFMT_BGR_MASK) == IMGFMT_BGR) {
		int fb_target_bpp = format & 0xff;
		set_bpp(&fb_vinfo, fb_target_bpp);
		fb_vinfo.xres_virtual = fb_vinfo.xres;
		fb_vinfo.yres_virtual = fb_vinfo.yres;
		if (ioctl(fb_dev_fd, FBIOPUT_VSCREENINFO, &fb_vinfo))
			// Needed for Intel framebuffer with 32 bpp
			fb_vinfo.transp.length = fb_vinfo.transp.offset = 0;
		if (ioctl(fb_dev_fd, FBIOPUT_VSCREENINFO, &fb_vinfo)) {
			mp_msg(MSGT_VO, MSGL_ERR, "[fbdev2] Can't put VSCREENINFO: %s\n", strerror(errno));
			return 0;
		}
		fb_pixel_size = fb_vinfo.bits_per_pixel / 8;
		fb_bpp = fb_vinfo.bits_per_pixel;
		if (fb_bpp == 16)
			fb_bpp = fb_vinfo.red.length + fb_vinfo.green.length + fb_vinfo.blue.length;
		if (fb_bpp == fb_target_bpp)
			return VFCAP_CSP_SUPPORTED|VFCAP_CSP_SUPPORTED_BY_HW|VFCAP_ACCEPT_STRIDE;
	}
	return 0;
}

static void draw_alpha(int x0, int y0, int w, int h, unsigned char *src,
		unsigned char *srca, int stride)
{
	unsigned char *dst;
	int dstride;

#ifdef USE_CONVERT2FB
	dst = center + (fb_line_len * y0) + (x0 * fb_pixel_size);
	dstride = fb_line_len;
#else
	dst = next_frame + (in_width * y0 + x0) * fb_pixel_size;
	dstride = in_width * fb_pixel_size;
#endif
	(*draw_alpha_p)(w, h, src, srca, stride, dst, dstride);
}

static void draw_osd(void)
{
	vo_draw_text(in_width, in_height, draw_alpha);
}

// all csp support stride
static int draw_frame(uint8_t *src[]) { return 1; }

static int draw_slice(uint8_t *src[], int stride[], int w, int h, int x, int y)
{
	uint8_t *in = src[0];
#ifdef USE_CONVERT2FB
	uint8_t *dest = center + (fb_line_len * y) + (x * fb_pixel_size);
	int next = fb_line_len;
#else
	uint8_t *dest = next_frame + (in_width * y + x) * fb_pixel_size;
	int next = in_width * fb_pixel_size;
#endif

	memcpy_pic(dest, in, w * fb_pixel_size, h, next, stride[0]);
	return 0;
}

static void check_events(void)
{
}

static void flip_page(void)
{
#ifndef USE_CONVERT2FB
	int out_offset = 0, in_offset = 0;

	memcpy_pic(center + out_offset, next_frame + in_offset,
	           in_width * fb_pixel_size, in_height,
	           fb_line_len, in_width * fb_pixel_size);
#endif
}

static void uninit(void)
{
	if (fb_cmap_changed) {
		if (ioctl(fb_dev_fd, FBIOPUTCMAP, &fb_oldcmap))
			mp_msg(MSGT_VO, MSGL_ERR, "[fbdev2] Can't restore original cmap\n");
		fb_cmap_changed = 0;
	}
	free(next_frame);
	if (fb_dev_fd >= 0) {
		if (ioctl(fb_dev_fd, FBIOPUT_VSCREENINFO, &fb_orig_vinfo))
			mp_msg(MSGT_VO, MSGL_ERR, "[fbdev2] Can't reset original fb_var_screeninfo: %s\n", strerror(errno));
		close(fb_dev_fd);
		fb_dev_fd = -1;
	}
	if(frame_buffer) munmap(frame_buffer, fb_size);
	next_frame = frame_buffer = NULL;
	fb_preinit(1); // so that later calls to preinit don't fail
}

static int control(uint32_t request, void *data)
{
  switch (request) {
  case VOCTRL_QUERY_FORMAT:
    return query_format(*((uint32_t*)data));
  case VOCTRL_UPDATE_SCREENINFO:
    vo_screenwidth  = fb_vinfo.xres;
    vo_screenheight = fb_vinfo.yres;
    aspect_save_screenres(vo_screenwidth, vo_screenheight);
    return VO_TRUE;
  }
  return VO_NOTIMPL;
}
