/*
 * xdl 2.0 -- display a DL animation in an X-window.
 *
 *
 * Author:
 *      Jonas Yngvesson <jonas-y@isy.liu.se>
 *
 * Derived from dltogl.c by:
 *	George Phillips <phillips@cs.ubc.ca>
 *
 * Support for user defined animation speed:
 *      Per Beremark <per.beremark@telelogic.se>
 */

#include <stdio.h>
#include <signal.h>
#ifdef __convex__
#include <stdlib.h>
#else
#include <malloc.h>
#endif
#include <sys/types.h>
#include <sys/time.h>

#include <X11/Xos.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>

#define isneg16(x)	((x) & 0x8000)
#define neg16(x)	((~(x) + 1) & 0x7fff)


typedef struct {
    int   version;
    int   format;
    int   images_per_screen;
    char  title[21];
    char  author[21];
    int   num_screen;
    int   num_command;
} DL_info;


Display *x_display;
Window   x_window;
int      x_depth;
u_long   pixels[256];
Pixmap  *pixmap;
XImage  *x_image;
GC       gc_clear;


/*
 * Initialize the colormap. I use a private one for PseudoColor,
 * I was too tired to fiddle with allocating shared colors.
 */
static void
colormap_setup(fp, version)
    FILE *fp;
    int   version;
{
    Colormap cmap;
    XColor   color;
    u_char   pal[768];
    int      i;

    /* Is this the border colour? */
    if (version == 2)
        for (i = 0; i < 3; i++)
            fgetc(fp);
    else
        fgetc(fp);
    
    /*
     * Here comes the colormap.
     */
    fread(pal, 1, 768, fp);
    
    /*
     * Set up for grayscale conversion on a monochrome display.
     */
    if (x_depth == 1) {
        for (i = 0; i < 256; i++) {
            pixels[i] = (u_long)((pal[3 * i] << 2) * 0.30 
                                 + (pal[3 * i + 1] << 2) * 0.59
                                 + (pal[3 * i + 2] << 2) * 0.11);
        }
        return;
    }
        
    /*
     * Allocate colors on color displays.
     */
    if (x_depth == 8) {
        cmap = XCreateColormap(x_display, x_window, 
			       DefaultVisual(x_display, 
					     DefaultScreen(x_display)),
                               AllocNone);
    } else {
        cmap = DefaultColormap(x_display, DefaultScreen(x_display));
    }

    for (i = 0; i < 256; i++) {
        /*
         * X wants 16 bit color specs and VGA uses 6 ==> shift 10 bits.
         */
        color.red   = pal[3 * i    ] << 10;
        color.green = pal[3 * i + 1] << 10;
        color.blue  = pal[3 * i + 2] << 10;
        XAllocColor(x_display, cmap, &color);
        pixels[i] = color.pixel;
    }

    if (x_depth == 8) {        
        XSetWindowColormap(x_display, x_window, cmap);
    }
}



/*
 * Wait for a key to be pressed. In "dltogl" it was
 * printed as a "waitkey" command to GL with a numeric
 * argument. I don't know any GL commands so I don't know
 * if the argument was a request to wait for a specific key
 * or a timeout for how long to wait or anything else.
 * I just wait forever for any key to be pressed.
 */
static void
wait_for_key()
{
    XEvent event;
    Bool   done;

    done = False;
    while (!done) {
        XNextEvent(x_display, &event);
        if (event.type == KeyPress) {
            done = True;
        }
    }
}



/*
 * Set up the X window.
 */
static void
x_window_setup(title, author)
    char *title;
    char *author;
{
    XSetWindowAttributes  win_attr;    /* storage for "window attributes" */
    XSizeHints            hints;       /* storage for "window hints" */
    XGCValues             gcval;
    int                   screen;
    char                  label[256];

    /*
     * Open the display.
     */
    if (NULL == (x_display = XOpenDisplay(NULL))) {
        fputs("Can't open display.\n", stderr);
        exit(1);
    }

    /*
     * Create the window.
     */
    screen = DefaultScreen(x_display);
    x_depth = DefaultDepth(x_display, DefaultScreen(x_display));
    x_window = XCreateSimpleWindow(x_display, DefaultRootWindow(x_display),  
                                   100, 100, 320, 200, 0, 
                                   BlackPixel(x_display, screen), 
                                   BlackPixel(x_display, screen));
 
    /* 
     * set up "window hints" so that we won't be allowed to
     * resize the window while it's running 
     */
    sprintf(label, "%s %s %s", title, (author[0] ? "by" : ""), author);
    hints.flags = PSize | PMinSize | PMaxSize;
    hints.width = hints.min_width = hints.max_width = 320;
    hints.height = hints.min_height = hints.max_height = 200;
    XSetStandardProperties(x_display, x_window, label, title, 
                           None, NULL, 0, &hints);
 
    /*
     * Tell the server which events we want to recieve.
     * Enable backing store so we don't have to worry
     * about exposes (yes I know, backing store is not
     * guaranteed, but it works for me, hah).
     */
    win_attr.event_mask = KeyPressMask;
    win_attr.backing_store = Always;
    XChangeWindowAttributes(x_display, x_window, 
                            CWEventMask | CWBackingStore, &win_attr);

    gcval.foreground = BlackPixel(x_display, screen);
    gc_clear = XCreateGC(x_display, x_window, GCForeground, &gcval);

    XMapWindow(x_display, x_window);
    XSync(x_display, False);
}


static void die(s)char*s;{fprintf(stderr,"%s\n",s);exit(1);}


void
main(argc, argv)
    int		argc;
    char*	argv[];
{
    DL_info           dlinfo;
    struct itimerval  timer;
    struct timeval    timeout;
    char             *filename;
    FILE             *fp;
    u_char           *image_data;
    char             *err1, *err2, *tmp;
    short             alpha[256], beta[256], gamma[256], delta[256];
    short             gray, err;
    u_long            black, white;
    int               dx, dy;
    int               width, height;
    int              *cmd;
    int               labelpos, label;
    int               cmdnum;
    int               frame_freq;
    int               fps = 25;
    int               zoomflag = 0;
    int               errflg = 0;
    int               i, j;
    extern char      *optarg;
    extern int        optind;


    /*
     * Lets see what option we got from the user.
     */
    while ((i = getopt(argc, argv, "zhr:")) != -1) {
	switch (i) {
	  case 'z':
	    zoomflag = 1;
  	    break;

	  case 'h':
	    errflg++;
	    break;

	  case 'r':
	    fps = atoi(optarg);
	    if (fps < 2) {
	        printf("Minimum value is 2 frames per second.\n");
	        fps = 2;
	    }
	    break;

	  case '?':
	  default:
	    errflg++;
	}
    }
    if (errflg) { 
        fputs("usage: xdl [-z] [-h] [-r frames/second] [file.dl]\n", stderr);
	exit (2);
    }
    
    if (argv[optind] == NULL) {
        fp = stdin;
        filename = "stdin";
    } else if (NULL == (fp = fopen(argv[optind], "r"))) {        
        fprintf(stderr, "xdl: can't open %s\n", argv[1]);
        exit(1);
    } else {
        filename = argv[optind];
    }


    /*
     * Check the version number...
     */
    if (1 != (dlinfo.version = fgetc(fp)) && 2 != dlinfo.version) {
        fprintf(stderr, "xdl: This file is in an unknown format.\n");
        fprintf(stderr, "     I can only do .DL version 1 and 2.\n", 
                dlinfo.version);
        exit(1);
    }
    

    /*
     * ...and the format.
     */
    if (dlinfo.version == 1)
        dlinfo.format = 1;
    else
        dlinfo.format = fgetc(fp);
    
    switch (dlinfo.format) {
      case 0: /* large */
        dx = dy = 0;
        width = 320;
        height = 200;
        dlinfo.images_per_screen = 1;
        zoomflag = 0;
        break;
      case 1: /* medium */
        if (zoomflag) {
            dx = dy = 0;
            width = 320;
            height = 200;
        } else {
            dx = 80;
            dy = 50;
            width = 160;
            height = 100;
        }
        dlinfo.images_per_screen = 4;
        break;
      default:
        die("xdl: only large and medium formats are handled");
        break;
    }
    

    /*
     * Get title and author (if any).
     */
    dlinfo.title[20] = dlinfo.author[20] = 0;
    for (i = 0; i < 20; i++) {
        dlinfo.title[i] = fgetc(fp) ^ 255;
        if ((u_char)dlinfo.title[i] == 255) {
            dlinfo.title[i] = 0;
        }
    }
    for (i = 0; i < 20; i++) {
        if (dlinfo.version == 2) {
            dlinfo.author[i] = fgetc(fp) ^ 255;
            if ((u_char)dlinfo.author[i] == 255) {
                dlinfo.author[i] = 0;
            }
        } else {
            dlinfo.author[i] = 0;
        }
    }
    

    /*
     * Read number of screens and commands.
     */
    dlinfo.num_screen = fgetc(fp);
    dlinfo.num_command = fgetc(fp);
    

    /*
     * Display what we know so far.
     */
    printf("%s is a %s sized version %d .DL file.\n", filename, 
           (dlinfo.format == 0 ? "large" : "medium"), 
           dlinfo.version);
    printf("It containes %d images in a %d frame loop.\n",
           dlinfo.num_screen * dlinfo.images_per_screen, dlinfo.num_command);
    if (dlinfo.format == 1 && zoomflag) {
        puts("Zooming images to 320x200.");
    }
    printf("Displaying animation at %d frames per second.\n",fps);
    

    /*
     * Kick X into action.
     */
    x_window_setup(dlinfo.title, dlinfo.author);
    colormap_setup(fp, dlinfo.version);
    

    /*
     * Allocate memory for the commands, the image data
     * and all the pixmaps.
     */
    if (!(cmd = (int *)malloc(dlinfo.num_command * sizeof(int))))
        die("xdl: out of memory");
    if (NULL == (image_data = (u_char *)malloc(320 * 200))) {
        die("xdl: not enough memory.");
    }
    if (NULL == (pixmap = (Pixmap *)malloc(dlinfo.num_screen 
                                           * dlinfo.images_per_screen
                                           * sizeof(Pixmap)))) {
        die("xdl: not enough memory.");
    }
    

    /*
     * Set up for error distribution
     * on monochrome displays.
     */
    if (x_depth == 1) {
        for (i = 0; i < 256; i++) {
            alpha[i] = ((i - 128) * 7) / 16;
            beta[i] = ((i - 128) * 3) / 16;
            gamma[i] = ((i - 128) * 5) / 16;
            delta[i] = ((i - 128) * 1) / 16;
        }
        err1 = malloc(322);
        err2 = malloc(322);
        black = BlackPixel(x_display, DefaultScreen(x_display));
        white = WhitePixel(x_display, DefaultScreen(x_display));
    }


    /*
     * Build the pixmaps for the animation.
     */
    printf("Building pixmaps, wait..."); fflush(stdout);
    for (j = 0; j < dlinfo.num_screen; j++) {
        u_char	*src;
        int      row;
        int      col;


        /*
         * Read one screen of data.
         */
        fread(image_data, 1, 320 * 200, fp);


        /*
         * Each screen can hold several images.
         */
        for (i = 0; i < dlinfo.images_per_screen; i++) {
            if (x_depth == 1) {
                bzero(err1, 322);
            }


            /*
             * Get a pixmap for the image.
             */
            pixmap[j * (dlinfo.format * 3 + 1) + i] 
                = XCreatePixmap(x_display, x_window, 320, 200, x_depth);
            XFillRectangle(x_display, pixmap[j * (dlinfo.format * 3 + 1) + i],
                           gc_clear, 0, 0, 320, 200);
            x_image = XGetImage(x_display, 
                                pixmap[j * (dlinfo.format * 3 + 1) + i], 
                                dx, dy, width, height, AllPlanes, ZPixmap);


            /*
             * Get a pointer to the beginning of this image.
             * Put the pixels in the x-image and perform
             * error distribution if needed. We do zooming
             * by reading the same data several times, together
             * with error distribution this gives us some smoothing
             * for free! :-)
             */
            src = image_data + (i % 2) * 160 + (i / 2) * 100 * 320;
            for (row = 0; row < height; row++) {
                if (x_depth == 1) {
                    bzero(err2, 322);
                }
                for (col = 0; col < width; col++) {

                    if (x_depth == 1) {                                
                        gray = pixels[*src] + err1[col + 1];
                        if (gray < 128) {
                            err = gray;
                            XPutPixel(x_image, col, row, black);
                        } else {
                            err = gray - 256;
                            XPutPixel(x_image, col, row, white);
                        }
                        err1[col + 2] += alpha[err + 128];
                        err2[col    ] += beta[err + 128];
                        err2[col + 1] += gamma[err + 128];
                        err2[col + 2] += delta[err + 128];

                    } else {
                        XPutPixel(x_image, col, row, pixels[*src]);
                    }
                    src += (zoomflag) ? (col & 1) : 1;
                }

                if (x_depth == 1) {
                    tmp = err1;
                    err1 = err2;
                    err2 = tmp;
                }

                if (dlinfo.format) {
                    if (zoomflag) {
                        src += (row & 1) ? 160 : -160;
                    } else {
                        src += 160;
                    }
                }
            }


            /*
             * Put the image in the pixmap.
             */
            XPutImage(x_display, pixmap[j * (dlinfo.format * 3 + 1) + i], 
                      DefaultGC(x_display, DefaultScreen(x_display)), 
                      x_image, 0, 0, dx, dy, width, height);
        }
    }
    printf("done.\n");


    /*
     * Read the commands.
     */
    for (i = 0; i < dlinfo.num_command; i++) {
        if (dlinfo.version == 2) {
            j = fgetc(fp);
            j += fgetc(fp) << 8;
            cmd[i] = j;
        } else {
            j = fgetc(fp);
            cmd[i] = (j % 10) - 1 + ((j / 10) - 1) * 4;
        }
    }
    
    labelpos = 0;
    if (isneg16(cmd[dlinfo.num_command - 1])) {
        labelpos = (neg16(cmd[dlinfo.num_command - 1]) 
                    + 1); /* Correct? Why add 1 ?? */
        dlinfo.num_command--;	/* ignore that last command */
    }
    

    /*
     * Now for the animation. I use setitimer() and getitimer()
     * to try to keep 25 frames/sec. (Not that all DL-files are
     * adjusted to that but it seem natural)
     * However, as Per Beremark noted, most files seem to be tuned
     * for 10 - 12 frames/sec so it is possible to change the
     * speed with the -r switch.
     */
    signal(SIGALRM, SIG_IGN);
    frame_freq = 1000000/fps;
    i = 0;
    cmdnum = 0;
    label = -1;
    while (1) {
        for (; i < dlinfo.num_command; i++, cmdnum++) {
            if (cmdnum == labelpos && label == -1) {
                label = i;
            }
            if (isneg16(cmd[i])) {
                i++;              /* Skip argument to waitkey, see above */
                wait_for_key();
            } else {
                timer.it_interval.tv_sec = 0;
                timer.it_interval.tv_usec = frame_freq;
                timer.it_value.tv_sec = 0;
                timer.it_value.tv_usec = frame_freq;
                setitimer(ITIMER_REAL, &timer, NULL);
                XCopyArea(x_display, pixmap[cmd[i]], x_window, 
                          DefaultGC(x_display, DefaultScreen(x_display)), 
                          0, 0, 320, 200, 0, 0);
                XSync(x_display, False);
                getitimer(ITIMER_REAL, &timer);
                timeout = timer.it_value;
                select(0, NULL, NULL, NULL, &timeout);
            }
        }
        i = cmdnum = label;
    }
}
