/* BOGL - Ben's Own Graphics Library.
   Written by Ben Pfaff <pfaffben@debian.org>.

   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., 59 Temple Place, Suite 330, Boston, MA 02111-1307
   USA. */

#define _GNU_SOURCE 1
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <linux/fb.h>
#include <linux/kd.h>
#include <linux/vt.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(__i386__) || defined(__alpha__)
#include <sys/io.h>
#endif
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <termios.h>
#include <unistd.h>

#if 0 /* stopped being safe.. */
/* Yes, I know, we shouldn't be including headers from the kernel.  But
   XFree86 also uses this one (to get PAGE_MASK) so it's probably safe.  */
#include <asm/page.h>
#endif

#include "bogl.h"
#include "boglP.h"
#if BOGL_VGA16_FB
#include "bogl-vga16.h"
#endif
#if BOGL_CFB_FB
#include "bogl-pcfb.h"
#include "bogl-tcfb.h"
#endif

/* BOGL main code. */

#ifndef unused
#define unused __attribute__((unused))
#endif

/* Global variables. */
int bogl_xres, bogl_yres, bogl_bpp;	/* bogl.h */
int bogl_refresh;
volatile int vt_switch_pending = 0;
volatile char *bogl_frame;		/* boglP.h */
int bogl_drawing;
int bogl_line_len;

/* Device-specific routines. */
void (*bogl_pixel) (int x, int y, int c);
void (*bogl_hline) (int x1, int x2, int y, int c);
void (*bogl_vline) (int x, int y1, int y2, int c);
void (*bogl_text) (int x, int y, const char *s, int n, int fg, int bg, int ul,
 		   const struct bogl_font *font);
void (*bogl_clear) (int x1, int y1, int x2, int y2, int c);
void (*bogl_move) (int sx, int sy, int dx, int dy, int w, int h);
void (*bogl_put) (int x, int y, const struct bogl_pixmap *pixmap,
 		  const int color_map[16]);
void (*bogl_pointer) (int visible, int x, int y,
 		      const struct bogl_pointer *,
 		      int colors[2]);
void (*bogl_set_palette) (int c, int nc, const unsigned char palette[][3]);
void (*bogl_reinit) (void);

/* Static variables. */
static int fb;			/* Framebuffer file handle. */
static int tty;			/* Tty file handle. */

static struct termios save_termios;	/* Saved terminal state. */
static struct vt_mode mode;	/* Terminal mode. */
static int tty_no;		/* Tty that we own. */
static int type;		/* Video type, one of FB_TYPE_*. */
static int visual;		/* Visual type, one of FB_VISUAL_*. */

static int visible;		/* Is our VT visible? */

static int status;		/* 0=never initialized, 1=once initialized,
				   2=currently initialized. */
static char *error;		/* Error message. */

/* Saved color palette. */
static __u16 saved_red[16];
static __u16 saved_green[16];
static __u16 saved_blue[16];

static int gray;		/* Convert colors to grayscale? */

static char *bogl_frame_mapped;	/* Page aligned, while bogl_frame might
				   not be.  */

/* Functions. */

static size_t init_fb (void);
static int draw_enable (void);
static void draw_disable (void);

static void kbd_init (void);
static void kbd_done (void);

static void sigusr2 (int);

static struct fb_fix_screeninfo fb_fix;
/* Initialize BOGL. */
int
bogl_init (void)
{
  unsigned long bogl_frame_offset, bogl_frame_len;
  struct fb_var_screeninfo fb_var;
  struct vt_stat vts;
  int PAGE_MASK = ~(sysconf(_SC_PAGESIZE) - 1);

  assert (status < 2);
  visible = 1;
  
  fb = open ("/dev/fb0", O_RDWR);
  if (fb < 0)
    fb = open ("/dev/fb/0", O_RDWR);
  if (fb < 0)
    return bogl_fail ("opening /dev/fb0: %s", strerror (errno));
  if (bogl_cloexec (fb) < 0)
    return bogl_fail ("setting /dev/fb0 close-on-exec: %s", strerror (errno));

  tty = open ("/dev/tty0", O_RDWR);
  if (tty < 0)
    tty = open ("/dev/vc/0", O_RDWR);
  if (tty < 0)
    return bogl_fail ("opening /dev/tty0: %s", strerror (errno));
  if (bogl_cloexec (tty) < 0)
    return bogl_fail ("setting /dev/tty0 close-on-exec: %s", strerror (errno));

  if (-1 == ioctl (tty, VT_GETSTATE, &vts))
    return bogl_fail ("can't get VT state: %s", strerror (errno));
  tty_no = vts.v_active;

  if (-1 == ioctl (fb, FBIOGET_FSCREENINFO, &fb_fix)
      || -1 == ioctl (fb, FBIOGET_VSCREENINFO, &fb_var))
    return bogl_fail ("reading screen info: %s", strerror (errno));
  
  bogl_xres = fb_var.xres;
  bogl_yres = fb_var.yres;
  bogl_bpp = fb_var.bits_per_pixel;
  bogl_line_len = fb_fix.line_length;
  type = fb_fix.type;
  visual = fb_fix.visual;

  /* Some broken framebuffers (2.4.x virgefb, 2.5 is OK) don't provide
     line_length.  */
  if (bogl_line_len == 0)
    bogl_line_len = fb_var.xres * fb_var.bits_per_pixel / 8;

  if (!draw_enable ())
    return bogl_fail ("don't know screen type %d", type);

  if (ioctl (tty, VT_GETMODE, &mode) == -1)
    return bogl_fail ("can't get VT mode: %s", strerror (errno));

  mode.mode = VT_PROCESS;
  mode.relsig = SIGUSR2;
  mode.acqsig = SIGUSR2;

  bogl_signal (SIGUSR2, sigusr2);

  if (-1 == ioctl (tty, VT_SETMODE, &mode))
    return bogl_fail ("can't set VT mode: %s", strerror (errno));

  if (-1 == ioctl (tty, KDSETMODE, KD_GRAPHICS)) 
    return bogl_fail ("setting graphics mode: %s", strerror (errno));

  if (!init_fb())
    return 0;

  bogl_frame_offset = fb_fix.smem_start & ~PAGE_MASK;
  bogl_frame_len = ((bogl_frame_offset + fb_fix.smem_len + ~PAGE_MASK)
		    & PAGE_MASK);

  bogl_frame_mapped
    = mmap (NULL, bogl_frame_len, PROT_READ | PROT_WRITE, MAP_SHARED, fb, 0);
  if (bogl_frame_mapped == NULL || bogl_frame_mapped == (char *) -1)
     return bogl_fail ("mmaping /dev/fb0: %s", strerror (errno));

  bogl_frame = bogl_frame_mapped + bogl_frame_offset;

  kbd_init ();

  if (visual == FB_VISUAL_PSEUDOCOLOR
      || visual == FB_VISUAL_STATIC_PSEUDOCOLOR)
  {
    static struct fb_cmap cmap;

    cmap.start = 0;
    cmap.len = 16;
    cmap.red = saved_red;
    cmap.green = saved_green;
    cmap.blue = saved_blue;
    cmap.transp = NULL;

    ioctl (fb, FBIOGETCMAP, &cmap);
  }
  
  if (!status)
    atexit (bogl_done);
  status = 2;

  return 1;
}

static size_t
init_fb (void)
{
#if BOGL_VGA16_FB
  #ifndef FB_TYPE_VGA_PLANES
  #define FB_TYPE_VGA_PLANES 4
  #endif

  if (type == FB_TYPE_VGA_PLANES)
    {
      bogl_set_palette = bogl_fb_set_palette;
      return bogl_vga16_init (fb);
    }
#endif

#if BOGL_CFB_FB
  if (type == FB_TYPE_PACKED_PIXELS)
    {
      /* FIXME: what do we do about static pseudocolor visuals such as
       on losing m68k Macs?  (Aside from hoping Apple will release
       programming info for their framebuffers, of course) */
      if (visual == FB_VISUAL_PSEUDOCOLOR
	  || visual == FB_VISUAL_STATIC_PSEUDOCOLOR)
	{
	  bogl_set_palette = bogl_fb_set_palette;
	  return bogl_pcfb_init (fb, bogl_bpp);
	}
      else if (visual == FB_VISUAL_TRUECOLOR
          || visual == FB_VISUAL_DIRECTCOLOR)
	{
	  bogl_set_palette = bogl_tcfb_set_palette;
	  return bogl_tcfb_init (fb, bogl_bpp);
	}
    }
#endif

  return bogl_fail ("unknown or unsupported framebuffer: type %d, "
		    "visual %d, %d bits per pixel", type, visual, bogl_bpp);
}

/* Close down BOGL. */
void
bogl_done (void)
{
  if (status != 2)
    return;
  status = 1;

  bogl_clear (0, 0, bogl_xres, bogl_yres, 0);
  
  if (visual == FB_VISUAL_PSEUDOCOLOR)
  {
    static struct fb_cmap cmap;

    cmap.start = 0;
    cmap.len = 16;
    cmap.red = saved_red;
    cmap.green = saved_green;
    cmap.blue = saved_blue;
    cmap.transp = NULL;

    ioctl (fb, FBIOPUTCMAP, &cmap);
  }
  
  kbd_done ();

  munmap ((void *) bogl_frame, fb_fix.smem_len);
  
  bogl_signal (SIGUSR2, SIG_DFL);

  ioctl (tty, KDSETMODE, KD_TEXT);

  mode.mode = VT_AUTO;
  mode.relsig = 0;
  mode.acqsig = 0;
  ioctl (tty, VT_SETMODE, &mode);
  
  close (tty);
  close (fb);
}

/* Keyboard interface. */

/* Put stdin in raw mode. */
static void
kbd_init (void)
{
  struct termios buf;
  int fd;

  fd = fileno (stdin);
  if (-1 == tcgetattr (fd, &save_termios))
    return;

  /* If you uncomment ISIG and BRKINT below, then ^C will be ignored.
     At least for now, we want it to be processed. */
  buf = save_termios;
  buf.c_lflag &= ~(ECHO | ICANON | IEXTEN/* | ISIG*/);
  buf.c_iflag &= ~(/*BRKINT | */ICRNL | INPCK | ISTRIP | IXON);
  buf.c_cflag &= ~(CSIZE | PARENB);
  buf.c_cflag |= CS8;
  buf.c_oflag &= ~OPOST;
  buf.c_cc[VMIN] = 1;
  buf.c_cc[VTIME] = 0;

  tcsetattr (fd, TCSAFLUSH, &buf);
}

/* Restore stdin to original modes. */
static void
kbd_done (void)
{
  int fd;

  fd = fileno (stdin);
  tcsetattr (fd, TCSAFLUSH, &save_termios);
}

/* Draw a hollow rectangle from (X1,Y1) to (X2,Y2) in color C. */
void
bogl_rectangle (int x1, int y1, int x2, int y2, int c)
{
  bogl_hline (x1, x2, y1, c);
  bogl_vline (x1, y1, y2, c);
  bogl_hline (x1, x2, y2 - 1, c);
  bogl_vline (x2 - 1, y1, y2, c);
}

/* Returns the width of character WC in font FONT and sets *BITMAP. */
int
bogl_font_glyph (const struct bogl_font *font, wchar_t wc, u_int32_t **bitmap)
{
  int mask = font->index_mask;
  int i;

  for (;;)
    {
      for (i = font->offset[wc & mask]; font->index[i]; i += 2)
	{
	  if ((font->index[i] & ~mask) == (wc & ~mask))
	    {
	      if (bitmap != NULL)
		*bitmap = &font->content[font->index[i+1]];
	      return font->index[i] & mask;
	    }
	}
      if (wc != font->default_char)
	wc = font->default_char;
      else
	break;
    }
  return 0;
}

/* Returns whether there is a glyph for WC in font FONT. */
int
bogl_in_font (const struct bogl_font *font, wchar_t wc)
{
  int mask = font->index_mask;
  int i;

  for (i = font->offset[wc & mask]; font->index[i]; i += 2)
    {
      if ((font->index[i] & ~mask) == (wc & ~mask))
	return 1;
    }
  return 0;
}


/* Returns the width of string S when output in font FONT. */
int
bogl_metrics (const char *s, int n, const struct bogl_font *font)
{
  int cx = 0;
  wchar_t wc;
  int k;

  mbtowc (0, 0, 0);
  for (; (k = mbtowc (&wc, s, n)) > 0; s += k, n -= k)
    cx += bogl_font_glyph (font, wc, NULL);
  return cx;
}

/* Set whether to convert colors to grayscale.  You'll need to re-set
   the palette with bogl_set_palette() for this to take effect. */
void
bogl_gray_scale (int make_gray)
{
  gray = make_gray;
}

/* Set NC color palettes values starting at C to red-green-blue value
   specified in PALETTE.  Use 8-bit color values as input. */
void
bogl_fb_set_palette (int c, int nc, const unsigned char palette[][3])
{
  struct fb_cmap cmap;

  __u16 red[nc];
  __u16 green[nc];
  __u16 blue[nc];

  int i;

  for (i = 0; i < nc; i++)
    {
      const unsigned char *e = palette[i];
      
      if (gray)
	red[i] = green[i] = blue[i] = (e[0] * 77 + e[1] * 151 + e[2] * 28);
      else
	{
	  red[i] = e[0] << 8;
	  green[i] = e[1] << 8;
	  blue[i] = e[2] << 8;
	}
    }

  cmap.start = c;
  cmap.len = nc;
  cmap.red = red;
  cmap.green = green;
  cmap.blue = blue;
  cmap.transp = NULL;

  ioctl (fb, FBIOPUTCMAP, &cmap);
}

/* Returns the oldest error message since this function was last
   called.  Clears the error state.  Returns a null pointer if no
   errors have occurred.  The caller must free the returned
   pointer if memory leaks are to be prevented. */
const char *
bogl_error (void)
{
  char *msg = error;
  error = NULL;
  return msg;
}

/* Drawing function setup/disable. */

/* Dummy drawing functions to disable display. */
static void
dummy_pixel (int x unused, int y unused, int c unused)
{}

static void 
dummy_hline (int x1 unused, int x2 unused, int y unused, int c unused)
{}

static void 
dummy_vline (int x unused, int y1 unused, int y2 unused, int c unused)
{}

static void
dummy_text (int x unused, int y unused, const char *s unused, int n unused,
	    int fg unused, int bg unused, int ul unused, const struct bogl_font *font unused)
{}

static void
dummy_clear (int x1 unused, int y1 unused, int x2 unused, int y2 unused,
	     int c unused)
{}

static void
dummy_move (int sx unused, int sy unused, int dx unused, int dy unused,
	    int w unused, int h unused)
{}

static void
dummy_put (int x unused, int y unused, const struct bogl_pixmap *pixmap unused,
	   const int color_map[] unused)
{}

static void
dummy_pointer (int visible unused, int x unused, int y unused,
	       const struct bogl_pointer *pointer unused,
	       int colors[2] unused)
{}


/* Enable drawing by setting the bogl_* device-specific functions to
   their appropriate values for the detected device. */
static int
draw_enable (void)
{
#if BOGL_VGA16_FB
  if (type == FB_TYPE_VGA_PLANES)
    {
      bogl_pixel = bogl_vga16_pixel;
      bogl_hline = bogl_vga16_hline;
      bogl_vline = bogl_vga16_vline;
      bogl_clear = bogl_vga16_clear;
      bogl_text = bogl_vga16_text;
      bogl_put = bogl_vga16_put;
      bogl_pointer = bogl_vga16_pointer;
      bogl_reinit = bogl_vga16_reinit;
      return 1;
    }
#endif

#if BOGL_CFB_FB
  if (type == FB_TYPE_PACKED_PIXELS)
    {
      if (visual == FB_VISUAL_PSEUDOCOLOR
	  || visual == FB_VISUAL_STATIC_PSEUDOCOLOR)
	{
	  bogl_pixel = bogl_pcfb_pixel;
	  bogl_hline = bogl_pcfb_hline;
	  bogl_vline = bogl_pcfb_vline;
	  bogl_clear = bogl_pcfb_clear;
	  bogl_text = bogl_pcfb_text;
	  bogl_put = bogl_pcfb_put;
	  bogl_pointer = bogl_pcfb_pointer;
	  return 1;
	}
      else if (visual == FB_VISUAL_TRUECOLOR
          || visual == FB_VISUAL_DIRECTCOLOR)
	{
	  bogl_pixel = bogl_tcfb_pixel;
	  bogl_hline = bogl_tcfb_hline;
	  bogl_vline = bogl_tcfb_vline;
	  bogl_clear = bogl_tcfb_clear;
	  bogl_text = bogl_tcfb_text;
	  bogl_put = bogl_tcfb_put;
	  bogl_pointer = bogl_tcfb_pointer;
	  return 1;    
	}
    }
  /* FIXME: what about directcolor? */
#endif

  return 0;
}
      
/* Disable drawing by setting all the bogl_* device-specific functions
   to dummy functions. */
static void
draw_disable (void)
{
  bogl_pixel = dummy_pixel;
  bogl_hline = dummy_hline;
  bogl_vline = dummy_vline;
  bogl_text = dummy_text;
  bogl_clear = dummy_clear;
  bogl_move = dummy_move;
  bogl_put = dummy_put;
  bogl_pointer = dummy_pointer;
  bogl_reinit = NULL;
}

/* Signal handler called whenever the kernel wants to switch to or
   from our tty. */
static void
sigusr2 (int sig unused)
{
  vt_switch_pending = 1;
}
void
vt_switch (void)
{
  vt_switch_pending = 0;

  if (bogl_drawing)
    {
      abort();
    }
      
  if (visible)
    {
      visible = 0;
      draw_disable ();

      if (-1 == ioctl (tty, VT_RELDISP, 1))
	bogl_fail ("can't switch away from VT: %s", strerror (errno));
    }
  else
    {
      visible = 1;
      draw_enable ();
      
      if (-1 == ioctl (tty, VT_RELDISP, VT_ACKACQ))
	bogl_fail ("can't acknowledge VT switch: %s", strerror (errno));

      if (bogl_reinit != NULL)
        bogl_reinit ();

      bogl_refresh = 2;
    }
}

/* Sets the BOGL error message to MESSAGE if there is none already
   set.  Returns 0. */
int
bogl_fail (const char *format, ...)
{
  va_list args;

  if (error)
    return 0;

  va_start (args, format);
  vasprintf (&error, format, args);
  va_end (args);

  return 0;
}

/* Set a file descriptor to close-on-exec. */
int
bogl_cloexec(int fd)
{
  int flags;

  flags = fcntl (fd, F_GETFD);
  if (flags < 0)
    return flags;

  if (fcntl (fd, F_SETFD, flags | FD_CLOEXEC) < 0)
    return -1;

  return 0;
}

/* Install signal handler in a portable way (i.e. sigaction wrapper) */
int
bogl_signal(int signum, void (*handler) (int))
{
  struct sigaction sa;

  sa.sa_handler = handler;
  sigemptyset (&sa.sa_mask);
  sa.sa_flags = SA_RESTART;

  return sigaction (signum, &sa, NULL);
}
