/*
 * MultiGIF  V1.0  28-Jun-96  Andy Wardley
 *
 * (C) 1996 Andy Wardley <abw@peritas.com>.  All rights reserved.
 *
 * multigif.c
 *
 * Main program module.  Contains GIF manipulation routines specific to
 * the operation of MultiGIF.  Contains program entry point - main().
 *
 */

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "gif.h"
#include "gifio.h"
#include "multigif.h"
#include "parse.h"


/* global error buffer */
char mg_error[ERROR_BUFSIZ];

/* define fseek whence options if not set */
#ifndef SEEK_SET
#define SEEK_SET	0
#define SEEK_CUR	1
#define SEEK_END	2
#endif  /* SEEK_SET */



/*
 * static FILE *open_gif_file(char *filename, gif_header *gh, gif_colour *gc)
 *
 * Opens the file specified, validates it as a GIF file and attempts to
 * read the GIF header and optional global colour table into the structure
 * and array pointer to by gh and gc respectively.  Returns a FILE
 * pointer to the open file or NULL on failure and writes an error message
 * to the mg_error string.
 */

static FILE *open_gif_file(char *filename, gif_header *gh, gif_colour *gc)
{
	static FILE *fp;
	int coltabsiz;


	/* open file */
	if ((fp = fopen(filename, "rb")) == NULL)
	{
		(void) sprintf(mg_error, "cannot open %s\n", filename); 
		return (fp = (FILE *) NULL);
	}

	/* read GIF header */
	if (read_gif_header(gh, fp) != 0)
	{
		(void) sprintf(mg_error, "cannot read GIF header: %s\n", filename);
		(void) fclose(fp);
		return (fp = (FILE *) NULL);	
	}

	/* see if global colour table exists */
	if (gh->packed & GIF_COLTAB)
	{
		/* read colour table size */
		coltabsiz = 1L << ((gh->packed & GIF_COLTABSIZ) + 1);

		/* read colour table entries */
		if (read_gif_colour_table(gc, coltabsiz, fp) != 0)
		{	
			(void) sprintf(mg_error, "no global colour table: %s\n", filename);
			(void) fclose(fp);
			return (fp = (FILE *) NULL);
		}
	}

	return fp;
}



/*
 * int find_first_image(gif_graphic_control **ggcp, gif_img_desc *gid, 
 *                                                              FILE *fp)
 *
 * Locates the first image in the data stream connected to fp and
 * loads the GIF image descriptor block.  If a GIF graphic control block
 * was found immediately preceeding the GIF image descriptor, this is loaded
 * into a static structure and *gccp is set to point to it.  Otherwise *gccp
 * is set to NULL.  
 * 
 * The data stream should be positioned after the GIF header and any global 
 * colour table.  On completion, the data stream will be left at the start 
 * of the image data or local colour table if present.  Returns 0 on success 
 * or -1 on failure.
 */

int find_first_image(gif_graphic_control **ggcp, gif_img_desc *gid, FILE *fp)
{
	static gif_graphic_control ggc;
	int identifier;
	int label;
	int blocksize;

	/* initialise gccp - no graphic control block */
	*ggcp = NULL;

	/* read each section */
	while ((identifier = read_byte(fp)) != GIF_TRAILER)
	{
		switch (identifier)
		{
		case GIF_IMAGE_DESC:
			gid->seperator = (BYTE) identifier & 0xff;
			return read_gif_img_desc(gid, fp);

			/*NOTREACHED*/
			break;

		case GIF_EXTENSION:
			label = read_byte(fp);

			if (label == GIF_GRAPHIC_EXT)
			{
				/* read gif graphic control block */
				if (read_gif_graphic_control(&ggc, fp) != 0)
					return -1;

				/* set pointer to this block */
				*ggcp = &ggc;
			}
			else
			{
				/* clear pointer to graphic control block */
				*ggcp = NULL;

				/* fast forward past any other blocks */
				while ((blocksize = read_byte(fp)) != 0)
					(void) fseek(fp, (long) blocksize, SEEK_CUR);
			}
			break;

		default:
			(void) fprintf(stderr, "ERROR: Unrecgnised block: %x @ %ld\n", 	
					identifier, ftell(fp));
			return -1;

			/*NOTREACHED*/
			break;
		}
	}

	return -1;
}



/*
 * int get_header_palette(gif_header *gh, gif_colour *gc, char *filename)
 *
 * Opens the file specified, reads the GIF header block and then attempts
 * to locate a colour table.  If an image is present in the file and has
 * a local colour table, this is used in preference over a global colour
 * table.  Returns 0 on success or -1 on failure and writes an error
 * message to the mg_error buffer.
 */

int get_header_palette(gif_header *gh, gif_colour *gc, char *filename)
{
	gif_img_desc gid;
	gif_graphic_control *ggcp;

	FILE *fp;
	int coltabsiz;


	/* open file, read header and (optionally) global colour table */
	if ((fp = open_gif_file(filename, gh, gc)) == NULL)
		/* open_gif_file writes error message */
		return -1;

	/* look for the first image */
	if (find_first_image(&ggcp, &gid, fp) != 0)
	{
		(void) sprintf(mg_error, "no image in %s\n", filename);
		(void) fclose(fp);
		return -1;
	}

	/* see if the image uses a local colour table */
	if (gid.packed & GIF_COLTAB)
	{
		/* read colour table size */
		coltabsiz = 1L << ((gid.packed & GIF_COLTABSIZ) + 1);

		/* read colour table entries */
		if (read_gif_colour_table(gc, coltabsiz, fp) != 0)
		{	
			(void) sprintf(mg_error, "no local colour table: %s\n", filename);
			(void) fclose(fp);
			return -1;
		}

		/* update global colour table header */
		gh->packed |= GIF_COLTAB;

		/* set/unset sorted colour table flag */
		if (gid.packed & GIF_LCOLTABSORT)
			gh->packed |= GIF_GCOLTABSORT;
		else
			gh->packed ^= GIF_GCOLTABSORT;

		/* copy local palette size to global palette */
		gh->packed ^=GIF_COLTABSIZ;
		gh->packed |= (gid.packed & GIF_COLTABSIZ);
	}

	(void) fclose(fp);

	/* return OK if we found (or copied from a local) a global palette */
	if (gh->packed & GIF_COLTAB)
		return 0;
	else
	{
		(void) sprintf(mg_error, "No palette: %s", filename);
		return -1;
	}
}



/*
 * int get_global_palette(gif_header *gh, gif_colour *gct)
 *
 * Searches the global frameset list until the frame is found that 
 * corresponds to the gpalframe value.  This frame is the one whose
 * palette will be used to create a global palette for the destination 
 * GIF.  Having located the frame, get_header_palette() is called to
 * load the GIF header and palette (global, or local if one exists)
 * into the structures pointed to by *gh and *gct.
 *
 * Returns - on success or -1 on failure.
 */

int get_global_palette(gif_header *gh, gif_colour *gct)
{
	frame *f;
	int frameno;

	/* look for the global palette frame */
	frameno = 1;
	f = frameset;
	while (frameno != gpalframe)
	{
		if ((f = f->next) == NULL)
			return -1;
		frameno++;
	}

	/* read the palette from the specified frame file */
	return (get_header_palette(gh, gct, f->filename));
}



/*
 * int create_gif_comment(FILE *fp, const char *message, ...)
 *
 * Creates a comment string from the message and additional parameters
 * using vsprintf and writes it as a GIF comment block to fp.  Total
 * message length should not exceed COMMENT_BUFSIZ bytes - Caveat 
 * Programmer!  Returns 0 on success or -1 on failure.
 */

int create_gif_comment(FILE *fp, const char *message, ...)
{
	gif_comment gc;
	char buffer[COMMENT_BUFSIZ + 1];
	va_list val;

	/* expand comment */
	va_start(val, message);
	(void) vsprintf(buffer + 1, message, val);
	va_end(val);

	/* preface comment with string length (blocksize) */
	*buffer = strlen(buffer + 1);

	/* initialise a GIF comment block */
	gc.introducer = GIF_EXTENSION;
	gc.label = GIF_COMMENT_EXT;
	gc.comment_data = (BYTE *) buffer;
	gc.terminator = 0;

	/* write comment to file */
	return (write_gif_comment(&gc, fp) != 0);
}



/*
 * int create_netscape_loop(FILE *fp, int loop)
 * 
 * Creates a GIF application extension block for specifying a Netscape
 * of 'loop' iterations and writes it to the file fp.  Returns 0 on success
 * or -1 on failure.
 */

int create_netscape_loop(FILE *fp, int loop)
{
	char nsbuffer[5];
	gif_app_ext          gae;

	/* initialise GIF application extension block */
	gae.introducer = GIF_EXTENSION;
	gae.label = GIF_APP_EXT;
	gae.blocksize = 0x0b;
	(void) strncpy((char *) gae.identifier, "NETSCAPE", 8);
	(void) strncpy((char *) gae.authcode, "2.0", 3);
	gae.app_data = (BYTE *) nsbuffer;
	gae.terminator = 0;

	(void) sprintf(nsbuffer, "%c%c%c%c", 3, 1, loop & 0xff, loop >> 8);

	/* write Netscape application extension loop */
	return (write_gif_app_ext(&gae, fp));
}




/*
 * int main(int argc, char **argv)
 *
 * Main program entry point.
 */

int main(int argc, char **argv)
{
	gif_header           src_gh, dest_gh;
	gif_colour           gct[256];
	gif_img_desc         gid;
	gif_graphic_control  ggc, *ggcp;

	FILE *fout, *fin;
	char blockbuf[256];
	frame *f;
	int blocksize;
	int colbits, coltabsiz, coltabsort, coltmp;


	/* parse command line */
	if (parse_cmd_line(argc, argv) != 0)
		exit(1);

	/* initialise destination GIF header */
	(void) strncpy((char *) dest_gh.signature, "GIF", 3);
	(void) strncpy((char *) dest_gh.version, "89a", 3);
	dest_gh.screen_width = dest_gh.screen_height = 0;
	dest_gh.background = (BYTE) bgcol & 0xff;

	/* read the palette from the global palette frame file */
	if (get_global_palette(&src_gh, gct) != 0)
	{
		(void) fprintf(stderr, "ERROR: %s\n", mg_error);
		exit(1);
	}

	/* calculate number of global palette entries and get sort flag */
	coltabsiz = 1L << ((src_gh.packed & GIF_COLTABSIZ) + 1);
	coltabsort = src_gh.packed & GIF_GCOLTABSORT;

	/* fill the remaining elements in the new GIF header */
	dest_gh.packed = src_gh.packed;
	dest_gh.aspect_ratio = src_gh.aspect_ratio;

	/* open output file */
	if ((fout = fopen(outfile, "wb")) == NULL)
	{
		(void) fprintf(stderr, "ERROR: cannot open %s\n",  outfile);
		exit(1);
	}

	/* write the GIF header and palette */
	if (write_gif_header(&dest_gh, fout) != 0 
			|| write_gif_colour_table(gct, coltabsiz, fout) != 0)
	{
		(void) fprintf(stderr, "ERROR: cannot write GIF header: %s\n", 
				outfile);
		(void) fclose(fout);
		exit(1);
	}

	/* write MultiGif comment */
	if (create_gif_comment(fout, "%s V%s.%s (C) %s %s <%s>",
			PROGNAME, VERSION, INCREMENT, CRYEAR, AUTHOR, EMAIL) != 0)
	{
		(void) fprintf(stderr, "ERROR: cannot write comment block: %s\n", 
				outfile);
		(void) fclose(fout);
		exit(1);
	}

	/* add any user-specified comment */
	if (comment != NULL)
	{
		/* write user specified comment */
		if (create_gif_comment(fout, "%.*s", COMMENT_BUFSIZ - 1, comment) != 0)
		{
			(void) fprintf(stderr, "ERROR: cannot write comment block: %s\n", 
					outfile);
			(void) fclose(fout);
			exit(1);
		}
	}

	/* add a Netscape loop if specified */
	if (nsloop >= 0)
	{
		/* warn against infinite loops */
		if (nsloop == 0)
			(void) fprintf(stderr, "WARNING: Netscape loop is infinite (0)\n");

		if (create_netscape_loop(fout, nsloop) != 0)
		{
			(void) fprintf(stderr, "ERROR: cannot write Netscape loop\n");
			(void) fclose(fout);
			exit(1);
		}
	}

	/* step through each frame */
	f = frameset;
	while (f != NULL)
	{
		/* open the source file */
		if ((fin = open_gif_file(f->filename, &src_gh, gct)) == NULL)
		{
			(void) fprintf(stderr, "ERROR: %s\n", mg_error);
			(void) fclose(fout);
			exit(1);
		}

		/* locate the first image */
		if (find_first_image(&ggcp, &gid, fin) != 0)
		{
			(void) fprintf(stderr, "ERROR: cannot locate image in %s\n", 
					f->filename);
			(void) fclose(fout);
			(void) fclose(fin);
			exit(1);
		}

		/* read the local colour table if there is one */
		if (gid.packed & GIF_COLTAB)
		{
			/* read colour table size and sort flag */
			coltabsiz = 1L << ((gid.packed & GIF_COLTABSIZ) + 1);
			coltabsort = gid.packed & GIF_LCOLTABSORT;

			/* read colour table entries */
			if (read_gif_colour_table(gct, coltabsiz, fin) != 0)
			{	
				(void) fprintf(stderr, "ERROR: cannot read local colour table: %s\n", 
					f->filename);
				(void) fclose(fout);
				(void) fclose(fin);
				exit(1);
			}
		}

		/* initialise GIF graphic control block */
		ggc.introducer = GIF_EXTENSION;
		ggc.label = GIF_GRAPHIC_EXT;
		ggc.blocksize = 0x04;
		ggc.packed = 0;
		ggc.delay = f->delay;
		ggc.transparent = 0;
		ggc.terminator = 0;

		/* copy transparency details from source image */
		if (ggcp != NULL)
		{
			if (ggcp->packed & GIF_TRANSPARENT)
			{
				ggc.packed |= GIF_TRANSPARENT;
				ggc.transparent = ggcp->transparent;
			}
		}

		/* set user intervention flag */
		if (f->user)
			ggc.packed |= GIF_USERINPUT;

		/* set disposal method */
		ggc.packed |= ((f->replace << 2) & GIF_DISPOSAL);

		/* write the graphic control block */
		if (write_gif_graphic_control(&ggc, fout) != 0)
		{
			(void) fprintf(stderr, "ERROR: cannot write graphic control\n");
			(void) fclose(fin);
			(void) fclose(fout);
			exit(1);
		}

		/* set X and Y position of frame */
		gid.left = f->xpos;
		gid.top = f->ypos;

		/* ensure destination GIF header has largest frame size */
		if ((int) (gid.left + gid.width)  > (int) dest_gh.screen_width)
			dest_gh.screen_width = gid.left + gid.width;
		if ((int) (gid.top + gid.height) > (int) dest_gh.screen_height)
			dest_gh.screen_height = gid.top + gid.height;

		/* set local palette if requested */
		if (f->locpal)
		{
			/* clear packed field except for interlace flag */
			gid.packed &= GIF_INTERLACE;  
			gid.packed |= GIF_COLTAB;
			gid.packed |= coltabsort ? GIF_LCOLTABSORT : 0;

			/* calculate bit patterns for colour table size */
			colbits = 0;
			coltmp = coltabsiz;

			/* shift until bit 0 is set */
			while (coltmp ^ 0x01)
			{
				coltmp >>= 1;
				colbits++;
			}
			colbits--;
			gid.packed |= colbits;
		}


		/* write the GID image descriptor */
		if (write_gif_img_desc(&gid, fout) != 0)
		{
			(void) fprintf(stderr, "ERROR: cannot write image descriptor\n");
			(void) fclose(fin);
			(void) fclose(fout);
			exit(1);
		}

		/* write the local colour table if it is required  */
		if (f->locpal)
		{
			/* get colour table size */
			coltabsiz = 1L << ((gid.packed & GIF_COLTABSIZ) + 1);

			/* write colour table entries */
			if (write_gif_colour_table(gct, coltabsiz, fout) != 0)
			{	
				(void) fprintf(stderr, "ERROR: cannot write local colour table\n");
				(void) fclose(fout);
				(void) fclose(fin);
				exit(1);
			}
		}

		/* write the LZW block size byte */
		write_byte(read_byte(fin), fout);

		/* write the image itself */
		while ((blocksize = read_byte(fin)) != 0)
		{
			/* write blocksize */
			write_byte(blocksize, fout);

			/* write data block */
			if (fread(blockbuf, 1, blocksize, fin) != blocksize
				|| fwrite(blockbuf, 1, blocksize, fout) != blocksize)
			{
				(void) fprintf(stderr, "ERROR: error copying image blocks\n");
				(void) fclose(fout);
				(void) fclose(fin);
				exit(1);
			}
		}
	
		/* write block terminator */
		write_byte(0, fout);

		/* close file and getr next frame */
		(void) fclose(fin);
		f = f->next;
	}

	/* write GIF terminator to output file */
	write_byte(GIF_TRAILER, fout);

	/* seek the GIF start and re-write the header to update screen size */
	if (fseek(fout, 0L, SEEK_SET) != 0 
					|| write_gif_header(&dest_gh, fout) != 0)
	{
		(void) fprintf(stderr, "ERROR: cannot update GIF header\n");
		(void) fclose(fout);
		(void) fclose(fin);
		exit(1);
	}

	(void) fclose(fout);

	return 0;
}
