/* speccy xscreensaver, Copyright (c) 2004
 *  Arttu Ylarakkola <arttu@solvalou.com>
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation.  No representations are made about the suitability of this
 * software for any purpose.  It is provided "as is" without express or 
 * implied warranty.
 */

#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/cursorfont.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <stdlib.h>
#include <time.h>

#define DEFAULT_IDLE_TIME		600
#define DEFAULT_DRAW_DELAY		50
#define DEFAULT_CHANGE_DELAY		10

/* how long to sleep() between XQueryPointer() */
#define IDLE_DELAY_WAIT			15

/* Spectrum colors
 * You can tweak these by changing the color names according to rgb.txt.
 * Mine is located in /usr/X11R6/lib/X11/rgb.txt
 */
#define SPECCY_COLOR_BLUE		"blue2"
#define SPECCY_COLOR_RED		"red2"
#define SPECCY_COLOR_MAGENTA		"magenta2"
#define SPECCY_COLOR_GREEN		"green2"
#define SPECCY_COLOR_CYAN		"cyan2"
#define SPECCY_COLOR_YELLOW		"yellow2"
#define SPECCY_COLOR_WHITE		"gray94"
#define SPECCY_COLOR_BLACK		"black"
#define SPECCY_COLOR_BRIGHTBLUE		"blue1"
#define SPECCY_COLOR_BRIGHTRED		"red1"
#define SPECCY_COLOR_BRIGHTMAGENTA	"magenta1"
#define SPECCY_COLOR_BRIGHTGREEN	"green1"
#define SPECCY_COLOR_BRIGHTCYAN		"cyan1"
#define SPECCY_COLOR_BRIGHTYELLOW	"yellow1"
#define SPECCY_COLOR_BRIGHTWHITE	"grey98"

#define MAX_NUMBER_OF_SCR_FILES	5000

/* Calculate Y coordinate (0-192) from Spectrum screen memory location */
int calcY(int offset){
	return ((offset>>11)<<6)				// sector start
	     +((offset%2048)>>8)				// pixel rows
	     +((((offset%2048)>>5)-((offset%2048)>>8<<3))<<3);	// character rows
}

/* Calculate X coordinate (0-255) from Spectrum screen memory location */
int calcX(int offset){
	return (offset%32)<<3;
}

/* Check if user is active (=fiddles with keyboard or mouse) */
int userActive(Display *dpy,int win){

	XEvent event;
	if(XCheckWindowEvent(dpy,win,ButtonPressMask|PointerMotionMask|KeyPressMask,&event)!=False){
		return 1; /* no events */
	}
	
	return 0;
}

/* Makes the (already configured) screen saver window visible */
void showScreenSaverWindow(Display *dpy,int win,XColor *anycolor){
	/* show screen - on top of the stack, of course*/
	XMapRaised(dpy, win);

	/* grab mouse */
	XGrabPointer(dpy,win,
			True,
			ButtonPressMask|PointerMotionMask,
			GrabModeAsync,
			GrabModeAsync,
			None,
			None,
			CurrentTime);	

	/* grab keyboard */
	XGrabKeyboard(dpy,win,
			True,
			GrabModeAsync,
			GrabModeAsync,
			CurrentTime);

	/* hide mouse */
	Pixmap empty=XCreatePixmap(dpy,DefaultRootWindow(dpy),1,1,1);
	XDefineCursor(dpy,win,XCreatePixmapCursor(dpy,empty,empty,anycolor,anycolor,0,0));
	XFreePixmap(dpy,empty);
	
	XFlush(dpy);
}

/* Makes the screen saver window NOT visible */
void hideScreenSaverWindow(Display *dpy,int win){
	/* hide screen */
	XUnmapWindow(dpy,win);

	/* restore cursor - not neccessary, really */
	XUndefineCursor(dpy,win);

	/* restore inputs */
	XUngrabKeyboard(dpy,CurrentTime);
	XUngrabPointer(dpy,CurrentTime);
	
	XFlush(dpy);
	
	/* wait 'till user does nothing, just in case */
	while(userActive(dpy,win)){}
}

int main(int argc, char **argv){
	
	int n;
	char scrDirectory[100];
	int drawDelay=DEFAULT_DRAW_DELAY;
	int changeDelay=DEFAULT_CHANGE_DELAY;
	int initialInk=0;
	int initialPaper=7;
	int initialBrightness=0;
	int clearBetween=0;

	int saverWait=DEFAULT_IDLE_TIME;
	int saverIdleness=0;
	int mouseX;
	int mouseY;
	int previousMouseX=0;
	int previousMouseY=0;
	int returnInt;
	Window returnWindow;

	DIR *dp;
	struct dirent *ep;
	char *scrFileNames[MAX_NUMBER_OF_SCR_FILES];	
	char currentFile[200];
	char dirAdd[2]={'/',0};
	int selectedScr,previousScr;
	int scrFileCount=0;
	FILE *fp;

	int userIsActive=0;
	int screensaverIsRunning=1;
	
	Display *dpy;
	int width;
	int height;
	int scr;
	Colormap cm;
	XColor	wantedColor;
	XColor colors[16];
	char colorShr;

	int offset,data,currentByte,x,y,lengthX,lengthY;
	unsigned char pixmap[32][192];
	unsigned char attrmap[32][24];

	n=1;
	scrDirectory[0]='\0';
	while(n<argc){
	
		if(!strcmp(argv[n],"-d")){
			if(strlen(argv[n+1])<100){
				strcpy(scrDirectory,argv[n+1]);
			}
		}

		if(!strcmp(argv[n],"-t")){
			saverWait=strtol(argv[n+1],NULL,0)*60;
		}

		if(!strcmp(argv[n],"-s")){
			drawDelay=strtol(argv[n+1],NULL,0);
			if(!drawDelay) drawDelay++;
		}

		if(!strcmp(argv[n],"-w")){
			changeDelay=strtol(argv[n+1],NULL,0);
		}

		if(!strcmp(argv[n],"-c")){
			clearBetween=1;
		}

		if(!strcmp(argv[n],"-b")){
			initialBrightness=1;
		}

		if(!strcmp(argv[n],"-i")){
			initialInk=strtol(argv[n+1],NULL,0);
			if((initialInk<0)&&(initialInk>7)) {
				fprintf(stdout,"Invalid initial INK color specified\nUse speccy --help for help.\n");
				return 1;
			}
		}

		if(!strcmp(argv[n],"-p")){
			initialPaper=strtol(argv[n+1],NULL,0);
			if((initialPaper<0)&&(initialPaper>7)) {
				fprintf(stdout,"Invalid initial PAPER color specified\nUse speccy --help for help.\n");
				return 1;
			}
		}

		if((!strcmp(argv[n],"--help"))||(!strcmp(argv[n],"-h"))){
			fprintf(stdout,"Usage: speccy [OPTION(S)] -scr [DIRECTORY]\n");
			fprintf(stdout,"Display Spectrum .scr screen dumps in a screensaver-like fashion.\n");
			fprintf(stdout,"\n");
			fprintf(stdout," -d DIRECTORY  choose random .scr files from DIRECTORY (mandatory)\n");
			fprintf(stdout," -t NUMBER     number of minutes of idleness before running saver\n");
			fprintf(stdout,"\n");
			fprintf(stdout," -s NUMBER     drawing speed (smaller is slower)\n");
			fprintf(stdout," -w NUMBER     number of seconds to wait between images\n");
			fprintf(stdout,"\n");
			fprintf(stdout," -i COLOR      initial INK color\n");
			fprintf(stdout," -p COLOR      initial PAPER color\n");
			fprintf(stdout," -b            use BRIGHT initial colors\n");
			fprintf(stdout," -c            clear screen to initial color before new image\n");
			fprintf(stdout,"\n");
			fprintf(stdout,"COLOR values:\n");
			fprintf(stdout,"0=black, 1=blue, 2=red, 3=magenta, 4=green, 5=cyan, 6=yellow, 7=white\n");
			fprintf(stdout,"\n");
			fprintf(stdout,"If -t 0 (idle wait is zero), the program will instantly start to display\n");
			fprintf(stdout,".scr files and will exit after the user touches mouse or keyboard.\n");
			fprintf(stdout,"\n");
			fprintf(stdout,"Report bugs to <arttu@solvalou.com>.\n");
			return 0;
		}

		if((!strcmp(argv[n],"--version"))||(!strcmp(argv[n],"--v"))){
			fprintf(stdout,"Speccy screensaver v1.0 - Arttu Ylarakkola (arttu@solvalou.com)\n");
			return 0;
		}

		n++;
	}

	if(scrDirectory[0]=='\0'){
		fprintf(stdout,".scr file directory was not specified.\nUse speccy --help for help.\n");
		return 1;
	}

	initialInk=initialInk&7;
	initialPaper=initialPaper&7;
	initialBrightness=initialBrightness&1;
	
	/* load .scr file list */
	dp=opendir(scrDirectory);
	if (dp!=NULL){
		while ((ep=readdir(dp))){
			int len=strlen(ep->d_name);
			if((len>4)
			 &&(((ep->d_name)[len-4])=='.')
		 	 &&(((ep->d_name)[len-3])=='s')
			 &&(((ep->d_name)[len-2])=='c')
			 &&(((ep->d_name)[len-1])=='r')){

				char *p;
				if((scrFileCount<MAX_NUMBER_OF_SCR_FILES)&&(p=malloc(len))){
					strcpy(p,(ep->d_name));
					scrFileNames[scrFileCount++]=p;
				}
			}
		}
		closedir(dp);	
	}

	if(scrFileCount==0){
		fprintf(stderr,"No .scr files were found from directory %s\n",scrDirectory);
		return 1;
	}

	srand(time(NULL));
  
  	/* init X */	
	dpy=XOpenDisplay("");
	if(dpy==NULL){
		fprintf(stderr,"XOpenDisplay returned NULL!\n");
		return 1;
	}

	scr=DefaultScreen(dpy);

	if(DefaultDepth(dpy,scr)==1){
		fprintf(stderr,"Can't run in this color depth!\n");
		return 1;	
	}

	cm=DefaultColormap(dpy,scr);

	width=DisplayWidth(dpy,scr);
	height=DisplayHeight(dpy,scr);

	XSetWindowAttributes attr;
	attr.override_redirect = True;

	Window win = XCreateWindow(dpy,RootWindow(dpy,scr),
			0,0,
			width,height,		
			0,
			DefaultDepth(dpy,scr),
			InputOutput,
			CopyFromParent,
			CWBackPixel|CWBorderPixel|CWOverrideRedirect,
			&attr);

	XGCValues gcValues;
	GC gc = XCreateGC(dpy, win, (unsigned long)0, &gcValues);
	if(gc==0){
		fprintf(stderr,"Can not create Graphics Context!\n");
		return 1;
	}

	// init colors
	if((XLookupColor(dpy,cm,SPECCY_COLOR_BLACK,&wantedColor,&colors[0])==0)||(XAllocColor(dpy,cm,&colors[0])==0)){
		fprintf(stderr,"Cannot get color %s\n",SPECCY_COLOR_BLACK);
	}
	if((XLookupColor(dpy,cm,SPECCY_COLOR_BLUE,&wantedColor,&colors[1])==0)||(XAllocColor(dpy,cm,&colors[1])==0)){
		fprintf(stderr,"Cannot get color %s\n",SPECCY_COLOR_BLUE);
	}
	if((XLookupColor(dpy,cm,SPECCY_COLOR_RED,&wantedColor,&colors[2])==0)||(XAllocColor(dpy,cm,&colors[2])==0)){
		fprintf(stderr,"Cannot get color %s\n",SPECCY_COLOR_RED);
	}
	if((XLookupColor(dpy,cm,SPECCY_COLOR_MAGENTA,&wantedColor,&colors[3])==0)||(XAllocColor(dpy,cm,&colors[3])==0)){
		fprintf(stderr,"Cannot get color %s\n",SPECCY_COLOR_MAGENTA);
	}
	if((XLookupColor(dpy,cm,SPECCY_COLOR_GREEN,&wantedColor,&colors[4])==0)||(XAllocColor(dpy,cm,&colors[4])==0)){
		fprintf(stderr,"Cannot get color %s\n",SPECCY_COLOR_GREEN);
	}
	if((XLookupColor(dpy,cm,SPECCY_COLOR_CYAN,&wantedColor,&colors[5])==0)||(XAllocColor(dpy,cm,&colors[5])==0)){
		fprintf(stderr,"Cannot get color %s\n",SPECCY_COLOR_CYAN);
	}
	if((XLookupColor(dpy,cm,SPECCY_COLOR_YELLOW,&wantedColor,&colors[6])==0)||(XAllocColor(dpy,cm,&colors[6])==0)){
		fprintf(stderr,"Cannot get color %s\n",SPECCY_COLOR_YELLOW);
	}
	if((XLookupColor(dpy,cm,SPECCY_COLOR_WHITE,&wantedColor,&colors[7])==0)||(XAllocColor(dpy,cm,&colors[7])==0)){
		fprintf(stderr,"Cannot get color %s\n",SPECCY_COLOR_WHITE);
	}
	/* bright black is same as normal black */
	colors[8]=colors[0];
	if((XLookupColor(dpy,cm,SPECCY_COLOR_BRIGHTBLUE,&wantedColor,&colors[9])==0)||(XAllocColor(dpy,cm,&colors[9])==0)){
		fprintf(stderr,"Cannot get color %s\n",SPECCY_COLOR_BRIGHTBLUE);
	}
	if((XLookupColor(dpy,cm,SPECCY_COLOR_BRIGHTRED,&wantedColor,&colors[10])==0)||(XAllocColor(dpy,cm,&colors[10])==0)){
		fprintf(stderr,"Cannot get color %s\n",SPECCY_COLOR_BRIGHTRED);
	}
	if((XLookupColor(dpy,cm,SPECCY_COLOR_BRIGHTMAGENTA,&wantedColor,&colors[11])==0)||(XAllocColor(dpy,cm,&colors[11])==0)){
		fprintf(stderr,"Cannot get color %s\n",SPECCY_COLOR_BRIGHTMAGENTA);
	}
	if((XLookupColor(dpy,cm,SPECCY_COLOR_BRIGHTGREEN,&wantedColor,&colors[12])==0)||(XAllocColor(dpy,cm,&colors[12])==0)){
		fprintf(stderr,"Cannot get color %s\n",SPECCY_COLOR_BRIGHTGREEN);
	}
	if((XLookupColor(dpy,cm,SPECCY_COLOR_BRIGHTCYAN,&wantedColor,&colors[13])==0)||(XAllocColor(dpy,cm,&colors[13])==0)){
		fprintf(stderr,"Cannot get color %s\n",SPECCY_COLOR_BRIGHTCYAN);
	}
	if((XLookupColor(dpy,cm,SPECCY_COLOR_BRIGHTYELLOW,&wantedColor,&colors[14])==0)||(XAllocColor(dpy,cm,&colors[14])==0)){
		fprintf(stderr,"Cannot get color %s\n",SPECCY_COLOR_BRIGHTYELLOW);
	}
	if((XLookupColor(dpy,cm,SPECCY_COLOR_BRIGHTWHITE,&wantedColor,&colors[15])==0)||(XAllocColor(dpy,cm,&colors[15])==0)){
		fprintf(stderr,"Cannot get color %s\n",SPECCY_COLOR_BRIGHTWHITE);
	}

	if(saverWait>0) {
		fprintf(stdout,"Now waiting for the user to go idle...\n");fflush(stdout);
	}

	XQueryPointer(dpy,win,&returnWindow,&returnWindow,&previousMouseX,&previousMouseY,&returnInt,&returnInt,&returnInt);
	
	do { /* we're inside this loop as long as the screensaver is running */

		/* wait for the user to go idle (=mouse remains stationary) */
		saverIdleness=0;
		while(saverIdleness<saverWait){
			sleep(IDLE_DELAY_WAIT);
			XQueryPointer(dpy,win,&returnWindow,&returnWindow,&mouseX,&mouseY,&returnInt,&returnInt,&returnInt);
			if((previousMouseX==mouseX)&&(previousMouseY==mouseY)){
				saverIdleness+=IDLE_DELAY_WAIT;
			} else {
				saverIdleness=0;
			}
			previousMouseX=mouseX;
			previousMouseY=mouseY;
		}

		/* ok, user is now idle so grab pointer, display the window and start displaying screens */
		showScreenSaverWindow(dpy,win,&colors[0]);

		userIsActive=0;
		previousScr=-1;
		n=-1;
	
		do { /* we're inside this loop when the user is idle and pictures are being displayed */

			/* if this is the first screen to be drawn or screen must be cleared between every pic, do so */
			if((clearBetween)||(n==-1)){
				for(y=0;y<24;y++){
					for(x=0;x<32;x++){
						attrmap[x][y]=(initialBrightness<<6)|(initialPaper<<3)|initialInk;
					}
				}

				XSetForeground(dpy,gc,colors[initialPaper+8*initialBrightness].pixel);
				XFillRectangle(dpy,win,gc,0,0,width,height);
				XSetForeground(dpy,gc,colors[initialInk+8*initialBrightness].pixel);
			
				n=0;
			}

			/* pick a random file to be displayed */
			selectedScr=rand()%scrFileCount;
			if((scrFileCount>1)&&(selectedScr==previousScr)){
				selectedScr=(selectedScr+1)%scrFileCount;
			}
			previousScr=selectedScr;
				
			strcpy(currentFile,scrDirectory);
			if(currentFile[strlen(currentFile)-1]!='/'){
				strcat(currentFile,dirAdd);
			}			
			strcat(currentFile,scrFileNames[selectedScr]);

			/* properly terminated directory entries */
			n=strlen(currentFile);
			while(currentFile[n]!='r'){
				currentFile[n]='\0';
				n--;
			}

			if((fp=fopen(currentFile,"r"))==NULL){
				fprintf(stderr,"Can not open SCR file %s\n",currentFile);
				return 1;
			}

			/* draw bitmap */
			for(offset=0;(offset<6144)&&(!userIsActive);offset++){
				data=fgetc(fp);		
				pixmap[calcX(offset)>>3][calcY(offset)]=data;
				for(currentByte=0;currentByte<8;currentByte++){

					x=width*(calcX(offset)+currentByte)/256;
					y=height*calcY(offset)/192;

					lengthX=(width*((calcX(offset)+currentByte+1))/256)-x;
					lengthY=((height*(calcY(offset)+1))/192)-y;
			
					if((data&(128>>currentByte))){
						colorShr=0;
					} else {
						colorShr=3;
					}	
			
					XSetForeground(dpy,gc,colors[
									((attrmap[calcX(offset)>>3][calcY(offset)>>3]>>colorShr)&7)
									+8*(attrmap[calcX(offset)>>3][calcY(offset)>>3]>>6&1)
								].pixel);

					XFillRectangle(dpy,win,gc,x,y,lengthX,lengthY);

				}
		
				XFlush(dpy);
		
				if(userActive(dpy,win)){
					userIsActive=1;
				}
			
				if(offset%drawDelay==0) usleep(25000);
			}
      
		      	/* re-draw by using attribute data */
			for(offset=6144;(offset<6912)&&(!userIsActive);offset++){			
							
				data=fgetc(fp);
				attrmap[(offset-6144)%32][(offset-6144)>>5]=data;
		
				for(y=0;y<8;y++){
					for(x=0;x<8;x++){
						if((pixmap[((offset-6144)%32)][(((offset-6144)>>5)<<3)+y])&(128>>x)){
							XSetForeground(dpy,gc,colors[(data&7)+8*(data>>6&1)].pixel);
						} else {
							XSetForeground(dpy,gc,colors[(data>>3&7)+8*(data>>6&1)].pixel);
						}				
				
						XFillRectangle(dpy,win,gc,
							((((offset-6144)%32)<<3)+x)*width/256,
							((((offset-6144)>>5)<<3)+y)*height/192,
							((((offset-6144)%32)<<3)+x+1)*width/256
								-((((offset-6144)%32)<<3)+x)*width/256,
							((((offset-6144)/32)<<3)+y+1)*height/192
								-((((offset-6144)>>5)<<3)+y)*height/192
						);
					}
				}
		
				XFlush(dpy);

				if(userActive(dpy,win)||userIsActive){
					userIsActive=1;
				}
		
				if(offset%drawDelay==0) usleep(25000);
			}
      
			fclose(fp);      

			/* wait for a while so the user can view at the picture */
			for(n=0;n<(changeDelay*4)&&(!userIsActive);n++){
				if(userActive(dpy,win)){
					userIsActive=1;
				} else {
					usleep(250000); /* small, so the de-activation does not feel sluggish */
				}
			}

			/* image was displayed immediately (saverWait==0) and user wants to quit, so exit */
			if((userIsActive)&&(saverWait==0)){
				screensaverIsRunning=0;
			}

		} while(!userIsActive); /* display another image if user is idle */

		/* user de-activated screensaver, remove window */
		hideScreenSaverWindow(dpy,win);
		
	} while(screensaverIsRunning); /* loop to wait for idleness again if user is not exiting the screen saver */

	/* shutdown */      
	XDestroyWindow(dpy,win);	
	XCloseDisplay(dpy);

	return 0;
}
