/*
 This is displ.c, part of the source code for

 BMV version 1.1
 copyright by Jan Kybic, 26th July 1994

 Jan Kybic, Prosecka 681, Praha 9, Czech Republic, <kybic@earn.cvut.cz>

 BMV is a very simple viewer of images in the pbm(5)-raw format
     and front end for GhostScript		
     based on Svgalib library for Linux

 BMV is distributed under GNU GPL (General Public License),
   you can obtain a copy from many FTP sites if you are interested for details
*/


#include "bmv.h"
#include <sys/time.h>       /* because of FD_SETSIZE */
#include <string.h>
#include <ctype.h>
#include <linux/vt.h>

/* the following variables local to this file contain the information about
   the videomode being used and what colours to use */

static int inkcol,papercol,bitsperpix,pixperbyte ;

vga_modeinfo *modeinfo ;

void (*oldreadyhandler)() ;

#ifdef M_UNIX
#define sig_atomic_t int
#endif


/* displaying is 1 if we are currently in the middle of displaying
   if user presses a key, it is detected by backround routine which
   sets (through signal) displaying=0 and this causes displaying to stop */

static volatile sig_atomic_t displaying=0 ;


pid_t kbdpid=-1 ; /* pid of the keyboard monitoring process */

unsigned char *outbuff=NULL,*readbuff=NULL,*inbuff=NULL ;
unsigned int *gray_inbuff=NULL;
unsigned int *pix_inbuff=NULL;
unsigned char *filebuff=NULL;
static long filebuff_size = 0;
                 /* buffers we need for showwindow */


void sighandler(int sig) ; /* receives SIGREADY and resets displaying */


/* opendisplay initializes graphic screen and sets the appropriate globals
   If the requested mode is not available, it tries to find other. */

void opendisplay()
{
  vga_init() ;
  
  /* Disable console switching */
  vga_lockvc() ;
  	
  /* try to find default mode, if user has not set it with the -v switch */
  if (vidmode==-1) vidmode=vga_getdefaultmode() ;
  if (vidmode==-1) vidmode=DVGAMODE ;

  /* If it is not available, find other */
  if (!vga_hasmode(vidmode))
    printf("Required videomode %d not available, trying others ...\n",vidmode) ;
  for(;vidmode>0 && !vga_hasmode(vidmode);vidmode--) ;

  vga_setmode(vidmode) ; vidmode=vga_getcurrentmode() ;
  if (vidmode==0) leave(1,"Failed to set any VGA mode",NULL) ;

  printf("Using VGA mode number %d.\n",vidmode) ;

  /* Get all the information */
  modeinfo=vga_getmodeinfo(vidmode) ;

  /* If we have too many colours, say sorry and exit. You can change it
     so that even in this case we would try to find an alternate mode,
     but I believe it is better to make user to do the choice */

  if (modeinfo->bytesperpixel>1)
   leave(1,"Videomodes with more than 1 byte per pixel not supported, sorry",
           "Try -v to set some other mode",NULL) ;
  if (modeinfo->colors==256)
      { inkcol=INKEIGHT ; papercol=PAPEREIGHT ;
        bitsperpix=8 ; pixperbyte=1 ;}
   else if (modeinfo->colors==16)
      { inkcol=INKFOUR ; papercol=PAPERFOUR ;
        bitsperpix=4 ; pixperbyte=2 ;}
   else if (modeinfo->colors==2)
      { inkcol=INKONE ; papercol=PAPERONE ;
        bitsperpix=1 ; pixperbyte=8 ;}
   else leave(1,"Unsupported number of colors, sorry",
                "Try -v to set some other mode",NULL) ;

  screenw=modeinfo->width ; screenh=modeinfo->height ;
  vgaon=1 ;
}



/* closedisplay resets screen to text mode and kills the keyboard monitor
   if necessary */

void closedisplay()
{
  if (leaving) { puts("Recursive call to closedisplay ignored") ; return ; }
  leaving = 1;
  if (kbdpid>0 && waitpid(kbdpid,NULL,WNOHANG)==0) kill(kbdpid,SIGTERM) ;
  if (!vgaon) return ;
	
  vga_unlockvc() ;	
  if (vga_getcurrentmode()) { puts("Returning to text mode") ;
                              vga_setmode(TEXT) ; }
  vgaon=0 ;
}



/* showwindow assumes the screen is set, file is open and buffers are
   allocated. It then displays a part of the PBM file with a given upper
   left corner and magnification */

void showwindow(int mag,int windowx,int windowy)
{ int scrline ;   /* current screen line for main loop */
  unsigned char col ;   /* colour */
  unsigned char gray_inkcol, gray_papercol;
  int scrbits,imbits ;  /* a square of imbits x imbits in the image
                           corresponds to scrbits x scrbits in the screen */
  int windoww,windowh ; /* image window dimensions */
  int i,j,k,lim ;       /* various use */
  unsigned char *p,*r ; /* pointers to buffers */
  unsigned int *b ;	/* pointer to gray and pix buffers */
  unsigned char *imagebuff; /* pointer to image buffer */
  long imbit,imbyte ;   /* position in bits and bytes from the beginning
                           of the image */
  int thresh ;		/* threshold to display a dot */
  int max_accum ;	/* max accumulator value */
  long accum ;		/* bit value accumulator */
  long accum_red, accum_green, accum_blue;
  static int last_mag, last_windowx = -1, last_windowy = -1; /* saved params */

  /* check for repaint using last positions */
  if (windowx < 0)
    { windowx = last_windowx; windowy = last_windowy; mag = last_mag; }
  if (windowx < 0) return;
  last_windowx = windowx; last_windowy = windowy; last_mag = mag;


  displaying=1 ;          /* set flag - we are displaying */

  kill(kbdpid,SIGREADY) ;  /* start the keyboard monitor, if user presses a key
                             displaying will be set to 0 */

  /* compute size and scale factors */
  scrbits= (mag>1) ? mag : 1 ;
  imbits= (mag<-1) ? -mag : 1 ;
  windowx=(windowx/8)*8 ;
  windoww=screenw/scrbits*imbits ;
  windowh=screenh/scrbits*imbits ;
  if (windoww+windowx>imagew) windoww=imagew-windowx ;
  if (windowh+windowy>imageh) windowh=imageh-windowy ;
  max_accum = image_max_component * imbits * imbits;
  thresh = (image_max_component * imbits * imbits * pixthresh) / 100;

  /* in pgm, 0=black, max=white */
  /* draw in larger of ink or paper color to keep from inverting */
  gray_inkcol = inkcol;
  gray_papercol = papercol;
  if (image_magic == PGMRAW_MAGIC && papercol > inkcol) {
    gray_inkcol = papercol;
    gray_papercol = inkcol;
  }

  if (filebuff == NULL) {
    lim = windoww * (long) windowh * image_num_components /
		image_pixels_per_byte;
    if (lim >= filebuff_size / 2 || filebuff_size <= 1L << 16) {
      if ((filebuff = malloc(filebuff_size)) != NULL) {
        if (fseek(file, offset, SEEK_SET) == -1)
          leave(1, "Cannot seek PBM file", strerror(errno), NULL) ;
        if (fread(filebuff, 1, filebuff_size, file) != filebuff_size)
          leave(1, "Cannot read PBM file", strerror(errno), NULL) ;
      }
    }
  }

  scrline=0 ;
  while(scrline+scrbits<=screenh && windowy+imbits<=imageh && displaying)
   /* now for every line do: */
   {
     /* initialize input buffer */
     memset(inbuff,0,screenw) ;

#if COLOUR
     if (image_magic == PGMRAW_MAGIC)
       memset(gray_inbuff, 0, (screenw + 1) * sizeof(int));
     if (image_magic == PPMRAW_MAGIC)
       memset(pix_inbuff, 0, (screenw + 1) * sizeof(int) * NUM_PIX_COMPONENTS);
#endif

     /* initialize output buffer - 0 here means that out of paper regions
        will be black */
     memset(outbuff,0,screenw) ;

     for(i=0;i<imbits && windowy<imageh;i++,windowy++)
       {
         /* now for every line that should contribute to given screen line */
         imbit = (windowy * imagew + windowx) * image_num_components ;
         imbyte = imbit / image_pixels_per_byte ;
	 lim = windoww * image_num_components / image_pixels_per_byte;

	 if (filebuff) {
           imagebuff = &filebuff[ imbyte ];
	 } else {
           /* read image line from file */
           if (fseek(file,offset+imbyte,SEEK_SET)==-1)
             leave(1,"Cannot seek PBM file",strerror(errno),NULL) ;

	   /* there should be a test for an error, but it is difficult to
	      know, how many bytes we should expect. I hope you forgive */
           fread(readbuff, lim + 1, 1, file) ;

           imagebuff = readbuff;
	 }

         /* now group pixels by imbits and add to inbuff   */

#if COLOUR
         if (image_magic == PBMRAW_MAGIC) {
#endif

           for(k = 128, p = imagebuff; p < imagebuff + lim; p++)
             for(j=0;j<8;j++,k= (k==1) ? 128 : k>>1)
               if (*p & k) inbuff[((p-imagebuff)*8+j)/imbits]++ ;
#if COLOUR
	 } else if (image_magic == PGMRAW_MAGIC) {

	   for (j = 0; j < windoww; j++) {
	     gray_inbuff[ j / imbits ] += imagebuff[j];
	   }

	 } else if (image_magic == PPMRAW_MAGIC) {

	   p = imagebuff;
	   for (j = 0; j < windoww; j++) {
	     b = &pix_inbuff[ (j / imbits) * NUM_PIX_COMPONENTS ];
	     b[0] += *p++;
	     b[1] += *p++;
	     b[2] += *p++;
	   }

	 } else {
	   printf("Error -- invalid image magic number %d\n", image_magic);
	   return;
	 }
#endif
       }

     /* now in inbuff we have counts, how many pixels from the image
        corresponding to particular screen pixels are black */

#if COLOUR
     if (image_magic == PBMRAW_MAGIC) {
#endif

	/* we have to distinguish between monochrome modes and colour modes */
	if (pixperbyte != 8) {
	/* for colour modes - each byte in outbuff represents one screen pixel */
	  for(p=inbuff,r=outbuff;p<inbuff+windoww/imbits;p++)
	    { col = (*p > thresh) ? inkcol : papercol ;
	      for (k=0;k<scrbits;k++) *r++=col ; }
	} else {
	/* monochrome modes -each byte in outbuff represents eight screen pixels */
	  for(p=inbuff,j=0;p<(unsigned char *)inbuff+windoww/imbits;p++)
	    { col= (*p > thresh) ? inkcol : papercol ;
	     for (k=0;k<scrbits;k++,j++)
	       *(outbuff+(j/8)) |=col ? (128>>(j%8)) : 0 ; }
	}
#if COLOUR
     } else if (image_magic == PGMRAW_MAGIC) {

	if (pixperbyte != 8) {
	  /* color mode */
	  accum = 0;
          for(b=gray_inbuff, r=outbuff; b<gray_inbuff+windoww/imbits; b++) {
	    accum += *b;
	    if (accum > thresh) {
	      col = gray_inkcol;
	      accum -= max_accum;
	    } else {
	      col = gray_papercol;
	    }
	    for (k=0; k<scrbits; k++) *r++ = col;
	  }
	} else {
	  /* monochrome mode */
          for(b=gray_inbuff,r=outbuff,j=0; b<gray_inbuff+windoww/imbits; b++) {
	    col = (*b > thresh) ? gray_inkcol : gray_papercol;
            for (k=0;k<scrbits;k++,j++)
              *(outbuff+(j/8)) |=col ? (128>>(j%8)) : 0 ;
	  }
	}

      } else if (image_magic == PPMRAW_MAGIC) {

	if (pixperbyte != 8) {
	  /* color mode */
          b = pix_inbuff;
          r = outbuff;
          accum_red = accum_green = accum_blue = 0;

          while (b < pix_inbuff + (windoww * NUM_PIX_COMPONENTS) / imbits) {

            accum_red += *b++;
            if (accum_red > thresh) {
	      col = 4;
	      accum_red -= max_accum;
	    } else {
	      col = 0;
	    }

	    accum_green += *b++;
	    if (accum_green > thresh) {
	      col |= 2;
	      accum_green -= max_accum;
	    }

	    accum_blue += *b++;
	    if (accum_blue > thresh) {
	      col |= 1;
	      accum_blue -= max_accum;
	    }

            for (k = 0; k < scrbits; k++)
              *r++ = col;
	  }
	} else {
	  /* monochrome mode */
          for(b = pix_inbuff, r = outbuff, j = 0;
              b < pix_inbuff + (windoww * NUM_PIX_COMPONENTS) / imbits;
              b += NUM_PIX_COMPONENTS) {
	    col = (b[0] + b[1] + b[2] > NUM_PIX_COMPONENTS * thresh) ?
			inkcol : papercol;
            for (k=0;k<scrbits;k++,j++)
               *(outbuff+(j/8)) |=col ? (128>>(j%8)) : 0 ;
	  }
	}
      } else {
	printf("Error -- invalid image magic number %d\n", image_magic);
	return;
      }
#endif

      /* and now just draw the line scrbits-times */
      for(j=0;j<scrbits;j++)
        vga_drawscanline(scrline++,outbuff) ;
   }
   displaying=0 ;
   kill(kbdpid,SIGDONE) ;  /* stop the keyboard monitor */
}


#define PAGNOLEN 3   /* number of digits for a page number */

#ifdef M_UNIX
#ifdef getchar
#undef getchar
#endif
#define	getchar	vgasco_getchar
#endif



/* Display file gets a name of a rawPBM file as its argument and displays it
   on the screen. Then it expects user to press a key and acts accordingly.
   It is possible to change magnification, move a round and if we are
   displaying PostScript, we can move to other pages */

void displayfile(const char *filename)
{ int windowx,windowy,step=STEP ;
      /* upper left corner of the viewing window, step size */
  char c ;   /* character read from the keyboard */
  int cont ; /* shall we continue with this file(flag) */
  int efstep ;  /* step size in image pixels */
  int numpage=1,nextpage=1 ;
                /* current page number, any more pages to display(flag) */

#ifdef M_UNIX
  int ms_center_x, ms_center_y;
#endif
  windowx=0 ; windowy=0 ; /* set the upper left corner */

  while(nextpage) {

  /* if we are in PS mode, ask GS for the page */
  if (psview) makegsdisplay(numpage) ;

  openpbm(filename) ; /* open PBM file */
  printf("Image size: %u x %u\n",imagew,imageh) ;

  /* allocate buffers */
  if ((outbuff=malloc(screenw))==NULL)
    leave(1,"Cannot allocate memory for output buffer",NULL) ;
  if ((readbuff=malloc((screenw * MAXIMBITS * image_num_components) /
	image_pixels_per_byte + 1))==NULL)
    leave(1,"Cannot allocate memory for read buffer",NULL) ;
  if ((inbuff=malloc(screenw))==NULL)
    leave(1,"Cannot allocate memory for intermediate buffer",NULL) ;
  if (image_magic == PGMRAW_MAGIC) {
    if ((gray_inbuff=malloc((screenw + 1) * sizeof(int)))==NULL)
      leave(1,"Cannot allocate memory for intermediate gray buffer",NULL) ;
  } else if (image_magic == PPMRAW_MAGIC) {
    if ((pix_inbuff=malloc((screenw + 1) * sizeof(int) *
 NUM_PIX_COMPONENTS))==NULL)
      leave(1,"Cannot allocate memory for intermediate color buffer",NULL) ;
  }
  filebuff_size = (imagew * (long) imageh * image_num_components) /
			image_pixels_per_byte;
  if (filebuff != NULL) { free(filebuff) ; filebuff = NULL ; }

  cont=1 ;
  /* in this loop we process one page */
  while (cont)
  { /* clear the screen, so that the user knows, that something is happening */
    vga_clear() ;

    /* display window */
    showwindow(mag,windowx,windowy) ;


    readchar: /* that is the place to jump, if no redisply is needed */

    /* calculate effective step size in image pixels */
    efstep=(mag>0) ? step/mag : -step * mag ;

#ifdef M_UNIX
    ms_center_x = ms_center_y = -1;
#endif

    /* read character and process it */
    c=getchar() ;
    switch (c) {
          /* q - quit */
          case 'q' : cont=0 ; nextpage=0 ; break ;

	  /* f/r - increase/reduce step size */
          case 'f' : if (step<MAXSTEP) step*=2 ; goto readchar ;
          case 'r' : if (step>MINSTEP) step /=2 ; goto readchar ;

	  /* +/- - increase/reduce magnification ration
	           we try not to move the middle of the picture */
          case '+' :
          case '-' :
	  case '=' :	/* unshifted '+' */
	  case '_' :	/* shifted '-' */
          case ' ' : {	int cx, cy;
			cx=screenw;
			cy=screenh;
			if (c == '=') c = '+';
			if (c == '_') c = '-';
#ifdef M_UNIX
			/* center at mouse instead of middle of screen */
			if (mouse_pixel_x >= 0)
			  { cx = 2 * mouse_pixel_x; cy = 2 * mouse_pixel_y; }
#endif
			cx=windowx+(mag>0 ? cx/mag : -cx*mag )/2 ;
			cy=windowy+(mag>0 ? cy/mag : -cy*mag )/2 ;
#ifdef M_UNIX
			/* save image location of mouse */
			if (mouse_pixel_x >= 0)
			  { ms_center_x = cx; ms_center_y = cy; }
#endif
                       if (c=='+' && mag<MAXIMBITS)
                         if (mag==-1) mag=2 ; else mag++ ;
                       if (c=='-' && mag>-MAXIMBITS)
                         if (mag==1) mag=-2 ; else mag-- ;
                       windowx=cx-(mag>0 ? screenw/mag : -screenw*mag )/2 ;
                       windowy=cy-(mag>0 ? screenh/mag : -screenh*mag )/2 ;
                       break ; }

	  /* h,j,k,l - move left, down, up, right */
          case 'h' : windowx-=efstep ;
                     break ;
          case 'j' : windowy+=efstep ;
                     break ;
          case 'k' : windowy-=efstep ;
                     break ;
          case 'l' : windowx+=efstep ;
                     break ;

          case 'H' : windowx=0 ;
                     break ;
          case 'J' : windowy=imagew ;
                     break ;
          case 'K' : windowy=0 ;
                     break ;
          case 'L' : windowx=imagew ;
                     break ;

          /* n,p - go to the next/previous page */
          case 'n' : if (psview && usedsc && numpage<pages.numpages)
                                { numpage++ ; cont=0 ; break ; }
                     goto readchar ;

          case 'p' : if (psview && usedsc && numpage>1)
                                { numpage-- ; cont=0 ; break ; }
                     goto readchar ;

          case 'N' : if (psview && usedsc && numpage<pages.numpages)
                                { numpage=pages.numpages ; cont=0 ; break ; }
                     goto readchar ;

          case 'P' : if (psview && usedsc && numpage>1)
                                { numpage=0 ; cont=0 ; break ; }
                     goto readchar ;

          /* g reads PAGNOLEN digits and goes to that page */
          case 'g' : { int temp,i ; char c ;
                       for(temp=0,i=0;i<PAGNOLEN;i++) {
                       	 c = getchar() ;
                         if (isdigit(c)) temp=temp*10+c-'0' ;
                         else if (isspace(c)) break ;
                         else goto readchar ;
		       }
		       if (temp > 0 && temp <= pages.numpages)
		                { numpage=temp ; cont=0 ; break ; }
		           else goto readchar ;
		     }
		     
          /* c switches to other virtual consoles */		     
	  case 's' : { char c ; int consolefd ;
 	  	       c=getchar() ;
	  	       if (!isdigit(c)) goto readchar ; 
	  	       vga_setmode(TEXT) ;

		       signal(SIGREADY,oldreadyhandler) ;			  	       
	  	       vga_unlockvc() ;
                       
                       printf("Switching to virtual console %d.\n",c-'0') ;
                       if ((consolefd=open("/dev/console",O_RDWR))==-1)
                        leave(1,"Cannot open /dev/console",strerror(errno),NULL) ;
                           
	  	       ioctl(consolefd,VT_ACTIVATE,c-'0') ;
	  	       vga_lockvc() ;
	  	       signal(SIGREADY,sighandler) ;
		       puts("Back in my virtual console.") ;	
	  	       vga_setmode(vidmode) ;
	  	       break ;
	  	     }  	     	
	  
	  default : goto readchar ;
                    }

    /* adjust window position if it exceeds limits */
    windowy=max(0,min(windowy,imageh-(mag>0 ? screenh/mag-mag*2 :
 -screenh*mag))) ;
    windowx=max(0,min(windowx,imagew-(mag>0 ? screenw/mag-mag*2 :
 -screenw*mag))) ;

#ifdef M_UNIX
    /* recenter mouse */
    if (ms_center_x >= 0) {
    	int cx, cy;
    	if (mag > 0) {
	    cx = (ms_center_x - windowx) * mag;
	    cy = (ms_center_y - windowy) * mag;
	} else {
	    cx = (windowx - ms_center_x) / mag;
	    cy = (windowy - ms_center_y) / mag;
	}
	ms_set_pix_location(cy, cx);
    }
#endif
  }


/* Displaying is over, let us do the clean up */
if (readbuff) { free(readbuff) ; readbuff = NULL ; }
if (outbuff) { free(outbuff) ; outbuff = NULL ; }
if (inbuff) { free(inbuff) ; inbuff = NULL ; }
if (gray_inbuff) { free(gray_inbuff) ; gray_inbuff = NULL ; }
if (pix_inbuff) { free(pix_inbuff) ; pix_inbuff = NULL ; }
if (filebuff) { free(filebuff) ; filebuff = NULL ; }
if (fclose(file)!=0)
  leave(1,"Cannot close PBM file",strerror(errno),NULL) ;
 }
}


/* bkgdisplayfile should appears from outside to be the same as displayfile.
   However, it first of all starts a background process monitoring keyboard,
   the effect being that displaying is interrupted whenever user presses key
*/


void bkgdisplayfile(const char *filename)
{
   /* Flush output buffers before forking */
   fflush(stdout); fflush(stderr);


   kbdpid = fork();

   if (kbdpid < 0)
     leave(1,"Cannot fork",NULL) ;

   /* we shall use SIGREADY for communication */

   if (kbdpid)
     { /* Parent */

       /* we install handler that resets displaying when invoked */

       oldreadyhandler=signal(SIGREADY,sighandler) ;

        /* do the display */

       displayfile(filename) ;

       /* if child is running, kill it */
       if (kbdpid>0) kill(kbdpid,SIGTERM) ;

       /* claim terminal back */
       tcsetpgrp(STDIN_FILENO,getpgrp()) ; 


       /* we do not need SIGREADY any more */
       signal(SIGREADY,oldreadyhandler) ;
     }
    else { /* This is the child */

      fd_set f ; /* f will contain a mask for stdin */

      /* if we receive SIGTERM, terminate silently */
      signal(SIGTERM,SIG_DFL) ;

      /* if we receive SIGINT, terminate and make parent terminate too */
      signal(SIGINT,siginthandler) ;

      /* SIGREADY will do nothing, but we use it to synchronize */
      signal(SIGREADY,emptyhandler) ;
      signal(SIGDONE,emptyhandler) ;

      /* we need an access to the terminal */
      tcsetpgrp(STDIN_FILENO,getpgrp()) ;


      /* this is an infinite loop, it ends when we receive SIGTERM */
      for(;;) {
                /* If parent is not displaying, wait for SIGREADY that
                    it sends when it starts */
		while (!displaying) pause() ;

                /* wait until character is available, or until parent
                   starts displaying again */
                FD_ZERO(&f) ; FD_SET(STDIN_FILENO,&f) ;
                select(FD_SETSIZE,&f,NULL,NULL,NULL) ;

                /* if character is available, notify parent */
	        if (FD_ISSET(STDIN_FILENO,&f) && displaying)
                   kill(getppid(),SIGREADY) ;

		/* Avoid a polling loop */
		displaying = 0 ;
                }
         }

   kbdpid=-1 ;
}


/* very simple signal handlers */

void sighandler(int sig)
{ displaying=0 ;
  signal(sig,sighandler) ;
}

void emptyhandler(int sig)
{ displaying = (sig == SIGREADY) ;
  signal(sig,emptyhandler) ;
}

void siginthandler(int sig)
{ kill(getppid(),SIGTERM) ;
  kill(getpid(),SIGTERM) ;
}

/* ********** end of displ.c ************ */

