/* fbdisplay.c: Routines for dealing with the linux fbdev display
   Copyright (c) 2000-2008 Philip Kendall, Matan Ziv-Av, Darren Salt,
			   Witold Filipczyk
   Copyright (c) 2015 Stuart Brady

   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.,
   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

   Author contact information:

   E-mail: philip-fuse@shadowmagic.org.uk

*/

#include "config.h"

#include <errno.h>
#include <math.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <linux/fb.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <string.h>

#include "fbdisplay.h"
#include "fuse.h"
#include "display.h"
#include "screenshot.h"
#include "ui/ui.h"
#include "ui/uidisplay.h"
#include "settings.h"

/* A copy of every pixel on the screen */
libspectrum_word
  fbdisplay_image[ 2 * DISPLAY_SCREEN_HEIGHT ][ DISPLAY_SCREEN_WIDTH ];
ptrdiff_t fbdisplay_pitch = DISPLAY_SCREEN_WIDTH * sizeof( libspectrum_word );

/* The environment variable specifying which device to use */
static const char * const DEVICE_VARIABLE = "FRAMEBUFFER";

/* The device we will use if device_env_variable is not specified */
static const char * const DEFAULT_DEVICE = "/dev/fb0";

/* The size of a 1x1 image in units of
   DISPLAY_ASPECT WIDTH x DISPLAY_SCREEN_HEIGHT */
int image_scale;

/* The height and width of a 1x1 image in pixels */
int image_width, image_height;

/* Are we in a Timex display mode? */
static int hires;

static void register_scalers( void );

/* probably 0rrrrrgggggbbbbb */
static short rgbs[16], greys[16];

static int fb_fd = -1;		/* The framebuffer's file descriptor */
static libspectrum_word *gm = 0;

static struct fb_fix_screeninfo fixed;
static struct fb_var_screeninfo orig_display, display;
static int got_orig_display = 0;
static int changed_palette = 0;

unsigned long fb_resolution; /* == xres << 16 | yres */
#define FB_RES(X,Y) ((X) << 16 | (Y))
#define FB_WIDTH (fb_resolution >> 16)
#define IF_FB_WIDTH(X) ((fb_resolution >> 16) == (X))
#define IF_FB_HEIGHT(Y) ((fb_resolution & 0xFFFF) == (Y))
#define IF_FB_RES(X, Y) (fb_resolution == FB_RES ((X), (Y)))

typedef struct {
  int xres, yres, pixclock;
  int left_margin, right_margin, upper_margin, lower_margin;
  int hsync_len, vsync_len;
  int sync, doublescan;
} fuse_fb_mode_t;

/* Test monitor is a Compaq V50, default settings
 * A = working on ATI RV100 (Radeon 7000) (2.6.x, old radeonfb)
 * M = working on Matrox MGA2064W (Millennium I) (2.6.x, matroxfb)
 * Large/tall/wide = size indication (overscan)
 * The 640x480 modes are from /etc/fb.modes and should work anywhere.
 *
 *    x    y  clock   lm  rm  tm  bm   hl  vl  s  d
 */
static const fuse_fb_mode_t fb_modes_singlescan[] = {
  { 640, 480, 32052,  96, 56, 28,  9,  40,  3, 0, 0 }, /* 640x480-72  72.114 */
  { 640, 480, 39722,  48, 16, 33, 10,  96,  2, 0, 0 }, /* 640x480-60  59.940 std */
  { 0 } /* end of list */
};
static const fuse_fb_mode_t fb_modes_doublescan[] = {
  { 640, 240, 32052,  92, 56, 14,  4,  40,  3, 0, 1 }, /* 640x240-72  72.185 */
  { 640, 480, 32052,  96, 56, 28,  9,  40,  3, 0, 0 }, /* 640x480-72  72.114 */
  { 640, 480, 39722,  48, 16, 33, 10,  96,  2, 0, 0 }, /* 640x480-60  59.940 std */
  { 320, 240, 64104,  46, 28, 14,  4,  20,  3, 3, 1 }, /* 320x240-72  72.185 M wide */
  { 0 } /* end of list */
};
static const fuse_fb_mode_t fb_modes_doublescan_alt[] = {
  { 640, 240, 39722,  36, 12, 18,  7,  96,  2, 1, 1 }, /* 640x240-60  60.133 AM large */
  { 640, 480, 32052,  96, 56, 28,  9,  40,  3, 0, 0 }, /* 640x480-72  72.114 */
  { 640, 480, 39722,  48, 16, 33, 10,  96,  2, 0, 0 }, /* 640x480-60  59.940 std */
  { 320, 240, 79444,  18,  6, 18,  7,  48,  2, 1, 1 }, /* 320x240-60  60.133 AM large */
  { 0 } /* end of list */
};
/* Modes not used but which may work are listed here.
 *    x    y  clock   lm  rm  tm  bm   hl  vl  s  d
  { 640, 480, 22272,  48, 32, 17, 22, 128, 12, 0, 0 }, ** 640x480-100 99.713
  { 640, 480, 25057, 120, 32, 14, 25,  40, 14, 0, 0 }, ** 640x480-90  89.995
  { 640, 480, 31747, 120, 16, 16,  1,  64,  3, 0, 0 }, ** 640x480-75  74.998
  { 320, 240, 80000,  40, 28,  9,  2,  20,  3, 0, 1 }, ** 320x240-60  60.310 M tall
  { 320, 240, 55555,  52, 16, 12,  0,  28,  2, 0, 1 }, ** 320x240-85  85.177 M
 */

static unsigned short red16[256], green16[256], blue16[256], transp16[256];
static struct fb_cmap orig_cmap = {0, 256, red16, green16, blue16, transp16};

static int fb_set_mode( void );

int uidisplay_init( int width, int height )
{
  hires = ( width == 640 ? 1 : 0 );

  image_width = width; image_height = height;
  image_scale = width / DISPLAY_ASPECT_WIDTH;

  register_scalers();

  display_ui_initialised = 1;

  display_refresh_all();

  return 0;
}

static void
register_scalers( void )
{
  scaler_register_clear();
  scaler_select_bitformat( 565 );		/* 16bit always */
  scaler_register( SCALER_NORMAL );
}

static void
linear_palette(struct fb_cmap *p_cmap)
{
  int i;
  int rcols = 1 << display.red.length;
  int gcols = 1 << display.green.length;
  int bcols = 1 << display.blue.length;

  for (i = 0; i < rcols; i++)
    p_cmap->red[i] = (65535 / (rcols - 1)) * i;

  for (i = 0; i < gcols; i++)
    p_cmap->green[i] = (65535 / (gcols - 1)) * i;

  for (i = 0; i < bcols; i++)
    p_cmap->blue[i] = (65535 / (bcols - 1)) * i;
}

int fbdisplay_init(void)
{
  int i;
  const char *dev;

  static libspectrum_word r16[256], g16[256], b16[256];
  static struct fb_cmap fb_cmap = {
    0, 256, r16, g16, b16, NULL
  };

  dev = getenv( DEVICE_VARIABLE );
  if( !dev || !*dev ) dev = DEFAULT_DEVICE;

  fb_fd = open( dev, O_RDWR | O_EXCL );
  if( fb_fd == -1 ) {
    fprintf( stderr, "%s: couldn't open framebuffer device '%s'\n",
	     fuse_progname, dev );
    return 1;
  }
  if( ioctl( fb_fd, FBIOGET_FSCREENINFO, &fixed )        ||
      ioctl( fb_fd, FBIOGET_VSCREENINFO, &orig_display )    ) {
    fprintf( stderr, "%s: couldn't read info from framebuffer device '%s'\n",
	     fuse_progname, dev );
    return 1;
  }
  got_orig_display = 1;

  if( fb_set_mode() ) return 1;

  fputs( "\x1B[H\x1B[J", stdout );	/* clear tty */
  memset( gm, 0, display.xres_virtual * display.yres_virtual * 2 );


  display.activate = FB_ACTIVATE_NOW;
  if( ioctl( fb_fd, FBIOPUT_VSCREENINFO, &display ) ) {
    fprintf( stderr, "%s: couldn't set mode for framebuffer device '%s'\n",
	     fuse_progname, dev );
    return 1;
  }

  ioctl( fb_fd, FBIOGET_VSCREENINFO, &display);
  for( i = 0; i < 16; i++ ) {
    int v = ( i & 8 ) ? 0xff : 0xbf;
    int c;
     rgbs[i] = ( ( i & 1 ) ? (v >> (8 - display.blue.length)) << display.blue.offset  : 0 )
           | ( ( i & 2 ) ? (v >> (8 - display.red.length)) << display.red.offset   : 0 )
           | ( ( i & 4 ) ? (v >> (8 - display.green.length)) << display.green.offset : 0 );

     c = (( i & 1 ) ? (v * 0.114) : 0.0) +
     (( i & 2) ? (v * 0.299) : 0.0) +
     (( i & 4) ? (v * 0.587) : 0.0) + 0.5;
     greys[i] = (c >> (8 - display.red.length) << display.red.offset)
     | (c >> (8 - display.green.length) << display.green.offset)
     | (c >> (8 - display.blue.length) << display.blue.offset);
  }
  linear_palette(&fb_cmap);

  if (orig_display.bits_per_pixel == 8 || fixed.visual == FB_VISUAL_DIRECTCOLOR) {
    ioctl( fb_fd, FBIOGETCMAP, &orig_cmap);
    changed_palette = 1;
  }
  ioctl( fb_fd, FBIOGET_FSCREENINFO, &fixed);
  if ( fixed.visual == FB_VISUAL_DIRECTCOLOR) {
    ioctl( fb_fd, FBIOPUTCMAP, &fb_cmap );
  }
  sleep( 1 ); /* give the monitor time to sync before we start emulating */

  fputs( "\x1B[?25l", stdout );		/* hide cursor */
  fflush( stdout );

  return 0;
}

static int
fb_select_mode( const fuse_fb_mode_t *fb_mode )
{
  memset (&display, 0, sizeof (struct fb_var_screeninfo));
  display.xres_virtual = display.xres = fb_mode->xres;
  display.yres_virtual = display.yres = fb_mode->yres;
  display.xoffset = display.yoffset = 0;
  display.grayscale = 0;
  display.nonstd = 0;
  display.accel_flags = 0;
  display.pixclock = fb_mode->pixclock;
  display.left_margin = fb_mode->left_margin;
  display.right_margin = fb_mode->right_margin;
  display.upper_margin = fb_mode->upper_margin;
  display.lower_margin = fb_mode->lower_margin;
  display.hsync_len = fb_mode->hsync_len;
  display.vsync_len = fb_mode->vsync_len;
  display.sync = fb_mode->sync;
  display.vmode &= ~FB_VMODE_MASK;
  if( fb_mode->doublescan ) display.vmode |= FB_VMODE_DOUBLE;
  display.vmode |= FB_VMODE_CONUPDATE;

  display.bits_per_pixel = 16;
  display.red.length = display.green.length = display.blue.length = 5;

  display.red.offset = 0;
  display.green.offset = 5;
  display.blue.offset = 10;

  gm = mmap( 0, fixed.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0 );
  if( gm == (void*)-1 ) {
    fprintf (stderr, "%s: couldn't mmap for framebuffer: %s\n",
	     fuse_progname, strerror( errno ) );
    return 1;
  }

  display.activate = FB_ACTIVATE_TEST;
  if( ioctl( fb_fd, FBIOPUT_VSCREENINFO, &display ) ) {
    munmap( gm, fixed.smem_len );
    return 1;
  }

  fb_resolution = FB_RES( display.xres, display.yres );
  return 0;			/* success */
}

static int
fb_set_mode( void )
{
  size_t i;

  const fuse_fb_mode_t *fb_modes =
    (settings_current.doublescan_mode == 0) ? fb_modes_singlescan :
    (settings_current.doublescan_mode == 1) ? fb_modes_doublescan :
					      fb_modes_doublescan_alt;

  /* First, try to use our preferred mode */
  for( i=0; fb_modes[i].xres; i++ )
    if( fb_modes[i].xres == settings_current.fb_mode )
      if( !fb_select_mode( fb_modes + i ) )
	return 0;

  /* If that failed, try to use the first available mode */
  for( i=0; fb_modes[i].xres; i++ )
    if( !fb_select_mode( fb_modes + i ) )
      return 0;

  /* If that failed, we can't continue :-( */
  fprintf( stderr, "%s: couldn't find a framebuffer mode to start in\n",
	   fuse_progname );
  return 1;
}

int
uidisplay_hotswap_gfx_mode( void )
{
  return 0;
}

void
uidisplay_frame_end( void ) 
{
  return;
}

void
uidisplay_area( int x, int start, int width, int height)
{
  int y;
  const short *colours = settings_current.bw_tv ? greys : rgbs;

  switch( fb_resolution ) {
  case FB_RES( 640, 480 ):
    for( y = start; y < start + height; y++ )
    {
      int i;
      libspectrum_word *point;

      if( hires ) {

	for( i = 0, point = gm + y * display.xres_virtual + x;
	     i < width;
	     i++, point++ )
	  *point = colours[fbdisplay_image[y][x+i]];

      } else {

	for( i = 0, point = gm + 2 * y * display.xres_virtual + x * 2;
	     i < width;
	     i++, point += 2 )
	  *  point       = *( point +     display.xres_virtual ) =
	  *( point + 1 ) = *( point + 1 + display.xres_virtual ) = 
	    colours[fbdisplay_image[y][x+i]];

      }
    }
    break;

  case FB_RES( 640, 240 ):
    if( hires ) { start >>= 1; height >>= 1; }
    for( y = start; y < start + height; y++ )
    {
      int i;
      libspectrum_word *point;

      if( hires ) {

	for ( i = 0, point = gm + y * display.xres_virtual + x;
	      i < width;
	      i++, point++ )
	  *point = colours[fbdisplay_image[y*2][x+i]];

      } else {

	for( i = 0, point = gm + y * display.xres_virtual + x * 2;
	     i < width;
	     i++, point+=2 )
	  *point = *(point+1) = colours[fbdisplay_image[y][x+i]];

      }
    }
    break;

  case FB_RES( 320, 240 ):
    if( hires ) { start >>= 1; height >>= 1; x >>= 1; width >>= 1; }
    for( y = start; y < start + height; y++ )
    {
      int i;
      libspectrum_word *point;

      if( hires ) {

	/* Drop every second pixel */
	for ( i = 0, point = gm + y * display.xres_virtual + x;
	      i < width;
	      i++, point++ )
	  *point = colours[fbdisplay_image[y*2][(x+i)*2]];

      } else {

	for( i = 0, point = gm + y * display.xres_virtual + x;
	     i < width;
	     i++, point++ )
	  *point = colours[fbdisplay_image[y][x+i]];

      }

    }
    break;

  default:;		/* Shut gcc up */
  }
}

int
uidisplay_end( void )
{
  return 0;
}

int
fbdisplay_end( void )
{
  if( fb_fd != -1 ) {
    if( got_orig_display ) {
      ioctl( fb_fd, FBIOPUT_VSCREENINFO, &orig_display );
      if (changed_palette) {
        ioctl( fb_fd, FBIOPUTCMAP, &orig_cmap);
	changed_palette = 0;
      }
    }
    close( fb_fd );
    fb_fd = -1;
    fputs( "\x1B[H\x1B[J\x1B[?25h", stdout );	/* clear screen, show cursor */
  }

  return 0;
}

/* Set one pixel in the display */
void
uidisplay_putpixel( int x, int y, int colour )
{
  if( machine_current->timex ) {
    x <<= 1; y <<= 1;
    fbdisplay_image[y  ][x  ] = colour;
    fbdisplay_image[y  ][x+1] = colour;
    fbdisplay_image[y+1][x  ] = colour;
    fbdisplay_image[y+1][x+1] = colour;
  } else {
    fbdisplay_image[y][x] = colour;
  }
}

/* Print the 8 pixels in `data' using ink colour `ink' and paper
   colour `paper' to the screen at ( (8*x) , y ) */
void
uidisplay_plot8( int x, int y, libspectrum_byte data,
                libspectrum_byte ink, libspectrum_byte paper )
{
  x <<= 3;

  if( machine_current->timex ) {
    int i;

    x <<= 1; y <<= 1;
    for( i=0; i<2; i++,y++ ) {
      fbdisplay_image[y][x+ 0] = ( data & 0x80 ) ? ink : paper;
      fbdisplay_image[y][x+ 1] = ( data & 0x80 ) ? ink : paper;
      fbdisplay_image[y][x+ 2] = ( data & 0x40 ) ? ink : paper;
      fbdisplay_image[y][x+ 3] = ( data & 0x40 ) ? ink : paper;
      fbdisplay_image[y][x+ 4] = ( data & 0x20 ) ? ink : paper;
      fbdisplay_image[y][x+ 5] = ( data & 0x20 ) ? ink : paper;
      fbdisplay_image[y][x+ 6] = ( data & 0x10 ) ? ink : paper;
      fbdisplay_image[y][x+ 7] = ( data & 0x10 ) ? ink : paper;
      fbdisplay_image[y][x+ 8] = ( data & 0x08 ) ? ink : paper;
      fbdisplay_image[y][x+ 9] = ( data & 0x08 ) ? ink : paper;
      fbdisplay_image[y][x+10] = ( data & 0x04 ) ? ink : paper;
      fbdisplay_image[y][x+11] = ( data & 0x04 ) ? ink : paper;
      fbdisplay_image[y][x+12] = ( data & 0x02 ) ? ink : paper;
      fbdisplay_image[y][x+13] = ( data & 0x02 ) ? ink : paper;
      fbdisplay_image[y][x+14] = ( data & 0x01 ) ? ink : paper;
      fbdisplay_image[y][x+15] = ( data & 0x01 ) ? ink : paper;
    }
  } else {
    fbdisplay_image[y][x+ 0] = ( data & 0x80 ) ? ink : paper;
    fbdisplay_image[y][x+ 1] = ( data & 0x40 ) ? ink : paper;
    fbdisplay_image[y][x+ 2] = ( data & 0x20 ) ? ink : paper;
    fbdisplay_image[y][x+ 3] = ( data & 0x10 ) ? ink : paper;
    fbdisplay_image[y][x+ 4] = ( data & 0x08 ) ? ink : paper;
    fbdisplay_image[y][x+ 5] = ( data & 0x04 ) ? ink : paper;
    fbdisplay_image[y][x+ 6] = ( data & 0x02 ) ? ink : paper;
    fbdisplay_image[y][x+ 7] = ( data & 0x01 ) ? ink : paper;
  }
}

/* Print the 16 pixels in `data' using ink colour `ink' and paper
   colour `paper' to the screen at ( (16*x) , y ) */
void
uidisplay_plot16( int x, int y, libspectrum_word data,
                 libspectrum_byte ink, libspectrum_byte paper )
{
  int i;
  x <<= 4; y <<= 1;

  for( i=0; i<2; i++,y++ ) {
    fbdisplay_image[y][x+ 0] = ( data & 0x8000 ) ? ink : paper;
    fbdisplay_image[y][x+ 1] = ( data & 0x4000 ) ? ink : paper;
    fbdisplay_image[y][x+ 2] = ( data & 0x2000 ) ? ink : paper;
    fbdisplay_image[y][x+ 3] = ( data & 0x1000 ) ? ink : paper;
    fbdisplay_image[y][x+ 4] = ( data & 0x0800 ) ? ink : paper;
    fbdisplay_image[y][x+ 5] = ( data & 0x0400 ) ? ink : paper;
    fbdisplay_image[y][x+ 6] = ( data & 0x0200 ) ? ink : paper;
    fbdisplay_image[y][x+ 7] = ( data & 0x0100 ) ? ink : paper;
    fbdisplay_image[y][x+ 8] = ( data & 0x0080 ) ? ink : paper;
    fbdisplay_image[y][x+ 9] = ( data & 0x0040 ) ? ink : paper;
    fbdisplay_image[y][x+10] = ( data & 0x0020 ) ? ink : paper;
    fbdisplay_image[y][x+11] = ( data & 0x0010 ) ? ink : paper;
    fbdisplay_image[y][x+12] = ( data & 0x0008 ) ? ink : paper;
    fbdisplay_image[y][x+13] = ( data & 0x0004 ) ? ink : paper;
    fbdisplay_image[y][x+14] = ( data & 0x0002 ) ? ink : paper;
    fbdisplay_image[y][x+15] = ( data & 0x0001 ) ? ink : paper;
  }
}

void
uidisplay_frame_save( void )
{
  /* FIXME: Save current framebuffer state as the widget UI wants to scribble
     in here */
}

void
uidisplay_frame_restore( void )
{
  /* FIXME: Restore saved framebuffer state as the widget UI wants to draw a
     new menu */
}
