/****************************************************************************

giftool.c - GIF transformation tool.

SPDX-License-Identifier: MIT

****************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <stdbool.h>

#include "getopt.h"
#include "gif_lib.h"
#include "getarg.h"

#define PROGRAM_NAME	"giftool"

#define MAX_OPERATIONS	256
#define MAX_IMAGES	2048

enum boolmode {numeric, onoff, tf, yesno};

char *putbool(bool flag, enum boolmode mode)
{
    if (flag)
	switch (mode) {
	case numeric: return "1"; break;
	case onoff:   return "on"; break;
	case tf:      return "true"; break;
	case yesno:   return "yes"; break;
	}
    else
	switch (mode) {
	case numeric: return "0"; break;
	case onoff:   return "off"; break;
	case tf:      return "false"; break;
	case yesno:   return "no"; break;
	}

    return "FAIL";	/* should never happen */
}

bool getbool(char *from)
{
    struct valmap {char *name; bool val;} 
    boolnames[] = {
	{"yes", true},
	{"on",  true},
	{"1",   true},
	{"t",   true},
	{"no",  false},
	{"off", false},
	{"0",   false},
	{"f",   false},
	{NULL,  false},
    }, *sp;

    for (sp = boolnames; sp->name; sp++)
	if (strcmp(sp->name, from) == 0)
	    return sp->val;

    (void)fprintf(stderr, 
		  "giftool: %s is not a valid boolean argument.\n",
		  sp->name);
    exit(EXIT_FAILURE);
}

struct operation {
    enum {
	aspect,
	delaytime,
	background,
	info,
	interlace,
	position,
	screensize,
	transparent,
	userinput,
	disposal,
    } mode;    
    union {
	GifByteType numerator;
	int delay;
	int color;
	int dispose;
	char *format;
	bool flag;
	struct {
	    int x, y;
	} p;
    };
};

int main(int argc, char **argv)
{
    extern char	*optarg;	/* set by getopt */
    extern int	optind;		/* set by getopt */
    struct operation operations[MAX_OPERATIONS];
    struct operation *top = operations;
    int selected[MAX_IMAGES], nselected = 0;
    bool have_selection = false;
    char *cp;
    int	i, status, ErrorCode;
    GifFileType *GifFileIn, *GifFileOut = (GifFileType *)NULL;
    struct operation *op;

    /*
     * Gather operations from the command line.  We use regular
     * getopt(3) here rather than Gershom's argument getter because
     * preserving the order of operations is important.
     */
    while ((status = getopt(argc, argv, "a:b:d:f:i:n:p:s:u:x:")) != EOF)
    {
	if (top >= operations + MAX_OPERATIONS) {
	    (void)fprintf(stderr, "giftool: too many operations.");
	    exit(EXIT_FAILURE);
	}

	switch (status)
	{
	case 'a':
	    top->mode = aspect;
	    top->numerator = (GifByteType)atoi(optarg);
	    break;

	case 'b':
	    top->mode = background;
	    top->color = atoi(optarg);
	    break;

	case 'd':
	    top->mode = delaytime;
	    top->delay = atoi(optarg);
	    break;

	case 'f':
	    top->mode = info;
	    top->format = optarg;
	    break;

	case 'i':
	    top->mode = interlace;
	    top->flag = getbool(optarg);
	    break;

	case 'n':
	    have_selection = true;
	    nselected = 0;
	    cp = optarg;
	    for (;;)
	    {
		size_t span = strspn(cp, "0123456789");

		if (span > 0)
		{
		    selected[nselected++] = atoi(cp)-1;
		    cp += span;
		    if (*cp == '\0')
			break;
		    else if (*cp == ',')
			continue;
		}

		(void) fprintf(stderr, "giftool: bad selection.\n");
		exit(EXIT_FAILURE);
	    }
	    break;

	case 'p':
	case 's':
	    if (status == 'p')
		top->mode = position;
	    else
		top->mode = screensize;
	    cp = strchr(optarg, ',');
	    if (cp == NULL)
	    {
		(void) fprintf(stderr, "giftool: missing comma in coordinate pair.\n");
		exit(EXIT_FAILURE);
	    }
	    top->p.x = atoi(optarg);
	    top->p.y = atoi(cp+1);
	    if (top->p.x < 0 || top->p.y < 0)
	    {
		(void) fprintf(stderr, "giftool: negative coordinate.\n");
		exit(EXIT_FAILURE);
	    }
	    break;

	case 'u':
	    top->mode = userinput;
	    top->flag = getbool(optarg);
	    break;

	case 'x':
	    top->mode = disposal;
	    top->dispose = atoi(optarg);
	    break;

	default:
	    fprintf(stderr, "usage: giftool [-b color] [-d delay] [-iI] [-t color] -[uU] [-x disposal]\n");
	    break;
	}

	++top;
    }	

    /* read in a GIF */
    if ((GifFileIn = DGifOpenFileHandle(0, &ErrorCode)) == NULL) {
	PrintGifError(ErrorCode);
	exit(EXIT_FAILURE);
    }
    if (DGifSlurp(GifFileIn) == GIF_ERROR) {
	PrintGifError(GifFileIn->Error);
	exit(EXIT_FAILURE);
    }
    if ((GifFileOut = EGifOpenFileHandle(1, &ErrorCode)) == NULL) {
	PrintGifError(ErrorCode);
	exit(EXIT_FAILURE);
    }

    /* if the selection is defaulted, compute it; otherwise bounds-check it */
    if (!have_selection)
	for (i = nselected = 0; i < GifFileIn->ImageCount; i++)
	    selected[nselected++] = i;
    else
	for (i = 0; i < nselected; i++)
	    if (selected[i] >= GifFileIn->ImageCount || selected[i] < 0)
	    {
		(void) fprintf(stderr,
			       "giftool: selection index out of bounds.\n");
		exit(EXIT_FAILURE);
	    }

    /* perform the operations we've gathered */
    for (op = operations; op < top; op++)
	switch (op->mode)
	{
	case background:
	    GifFileIn->SBackGroundColor = op->color; 
	    break;

	case delaytime:
	    for (i = 0; i < nselected; i++)
	    {
		GraphicsControlBlock gcb;

		DGifSavedExtensionToGCB(GifFileIn, selected[i], &gcb);
		gcb.DelayTime = op->delay;
		EGifGCBToSavedExtension(&gcb, GifFileIn, selected[i]);
	    }
	    break;

	case info:
	    for (i = 0; i < nselected; i++) {
		SavedImage *ip = &GifFileIn->SavedImages[selected[i]];
		GraphicsControlBlock gcb;
		for (cp = op->format; *cp; cp++) {
		    if (*cp == '\\') 
		    {
			char c;
			switch (*++cp) 
			{
			case 'b':
			    (void)putchar('\b');
			    break;
			case 'e':
			    (void)putchar(0x1b);
			    break;
			case 'f':
			    (void)putchar('\f');
			    break;
			case 'n':
			    (void)putchar('\n');
			    break;
			case 'r':
			    (void)putchar('\r');
			    break;
			case 't':
			    (void)putchar('\t');
			    break;
			case 'v':
			    (void)putchar('\v');
			    break;
			case 'x':
			    switch (*++cp) {
			    case '0':
				c = (char)0x00;
				break;
			    case '1':
				c = (char)0x10;
				break;
			    case '2':
				c = (char)0x20;
				break;
			    case '3':
				c = (char)0x30;
				break;
			    case '4':
				c = (char)0x40;
				break;
			    case '5':
				c = (char)0x50;
				break;
			    case '6':
				c = (char)0x60;
				break;
			    case '7':
				c = (char)0x70;
				break;
			    case '8':
				c = (char)0x80;
				break;
			    case '9':
				c = (char)0x90;
				break;
			    case 'A':
			    case 'a':
				c = (char)0xa0;
				break;
			    case 'B':
			    case 'b':
				c = (char)0xb0;
				break;
			    case 'C':
			    case 'c':
				c = (char)0xc0;
				break;
			    case 'D':
			    case 'd':
				c = (char)0xd0;
				break;
			    case 'E':
			    case 'e':
				c = (char)0xe0;
				break;
			    case 'F':
			    case 'f':
				c = (char)0xf0;
				break;
			    default:
				return -1;
			    }
			    switch (*++cp) {
			    case '0':
				c += 0x00;
				break;
			    case '1':
				c += 0x01;
				break;
			    case '2':
				c += 0x02;
				break;
			    case '3':
				c += 0x03;
				break;
			    case '4':
				c += 0x04;
				break;
			    case '5':
				c += 0x05;
				break;
			    case '6':
				c += 0x06;
				break;
			    case '7':
				c += 0x07;
				break;
			    case '8':
				c += 0x08;
				break;
			    case '9':
				c += 0x09;
				break;
			    case 'A':
			    case 'a':
				c += 0x0a;
				break;
			    case 'B':
			    case 'b':
				c += 0x0b;
				break;
			    case 'C':
			    case 'c':
				c += 0x0c;
				break;
			    case 'D':
			    case 'd':
				c += 0x0d;
				break;
			    case 'E':
			    case 'e':
				c += 0x0e;
				break;
			    case 'F':
			    case 'f':
				c += 0x0f;
				break;
			    default:
				return -2;
			    }
			    putchar(c);
			    break;
			default:
			    putchar(*cp);
			    break;
			}
		    }
	    	    else if (*cp == '%')
		    {
			enum boolmode  boolfmt;
			SavedImage *sp = &GifFileIn->SavedImages[i];

			if (cp[1] == 't') {
			    boolfmt = tf;
			    ++cp;
			} else if (cp[1] == 'o') {
			    boolfmt = onoff;
			    ++cp;
			} else if (cp[1] == 'y') {
			    boolfmt = yesno;
			    ++cp;
			} else if (cp[1] == '1') {
			    boolfmt = numeric;
			    ++cp;
			} else
			    boolfmt = numeric;

			switch (*++cp) 
			{
			case '%':
			    putchar('%');
			    break;
			case 'a':
			    (void)printf("%d", GifFileIn->AspectByte);
			    break;
			case 'b':
			    (void)printf("%d", GifFileIn->SBackGroundColor);
			    break;
			case 'd':
			    DGifSavedExtensionToGCB(GifFileIn, 
						    selected[i], 
						    &gcb);
			    (void)printf("%d", gcb.DelayTime);
			    break;
			case 'h':
			    (void)printf("%d", ip->ImageDesc.Height);
			    break;
			case 'n':
			    (void)printf("%d", selected[i]+1);
			    break;
			case 'p':
			    (void)printf("%d,%d", 
					 ip->ImageDesc.Left, ip->ImageDesc.Top);
			    break;
			case 's':
			    (void)printf("%d,%d", 
					 GifFileIn->SWidth, 
					 GifFileIn->SHeight);
			    break;
			case 'w':
			    (void)printf("%d", ip->ImageDesc.Width);
			    break;
			case 't':
			    DGifSavedExtensionToGCB(GifFileIn, 
						    selected[i], 
						    &gcb);
			    (void)printf("%d", gcb.TransparentColor);
			    break;
			case 'u':
			    DGifSavedExtensionToGCB(GifFileIn, 
						    selected[i], 
						    &gcb);
			    (void)printf("%s", putbool(gcb.UserInputFlag, boolfmt));
			    break;
			case 'v':
			    fputs(EGifGetGifVersion(GifFileIn), stdout);
			    break;
			case 'x':
			    DGifSavedExtensionToGCB(GifFileIn, 
						    selected[i], 
						    &gcb);
			    (void)printf("%d", gcb.DisposalMode);
			    break;
			case 'z':
			    (void) printf("%s", putbool(sp->ImageDesc.ColorMap && sp->ImageDesc.ColorMap->SortFlag, boolfmt));
			    break;
			default:
			    (void)fprintf(stderr, 
					  "giftool: bad format %%%c\n", *cp);
			}
		    }
	    	    else
			(void)putchar(*cp);
		}
	    }
	    exit(EXIT_SUCCESS);
	    break;

	case interlace:
	    for (i = 0; i < nselected; i++)
		GifFileIn->SavedImages[selected[i]].ImageDesc.Interlace = op->flag;
	    break;

	case position:
	    for (i = 0; i < nselected; i++) {
		GifFileIn->SavedImages[selected[i]].ImageDesc.Left = op->p.x;
		GifFileIn->SavedImages[selected[i]].ImageDesc.Top  = op->p.y;
	    }
	    break;

	case screensize:
	    GifFileIn->SWidth = op->p.x;
	    GifFileIn->SHeight = op->p.y;
	    break;

	case transparent:
	    for (i = 0; i < nselected; i++)
	    {
		GraphicsControlBlock gcb;

		DGifSavedExtensionToGCB(GifFileIn, selected[i], &gcb);
		gcb.TransparentColor = op->color;
		EGifGCBToSavedExtension(&gcb, GifFileIn, selected[i]);
	    }
	    break;

	case userinput:
	    for (i = 0; i < nselected; i++)
	    {
		GraphicsControlBlock gcb;

		DGifSavedExtensionToGCB(GifFileIn, selected[i], &gcb);
		gcb.UserInputFlag = op->flag;
		EGifGCBToSavedExtension(&gcb, GifFileIn, selected[i]);
	    }
	    break;

	case disposal:
	    for (i = 0; i < nselected; i++)
	    {
		GraphicsControlBlock gcb;

		DGifSavedExtensionToGCB(GifFileIn, selected[i], &gcb);
		gcb.DisposalMode = op->dispose;
		EGifGCBToSavedExtension(&gcb, GifFileIn, selected[i]);
	    }
	    break;

	default:
	    (void)fprintf(stderr, "giftool: unknown operation mode\n");
	    exit(EXIT_FAILURE);
	}

    /* write out the results */
    GifFileOut->SWidth = GifFileIn->SWidth;
    GifFileOut->SHeight = GifFileIn->SHeight;
    GifFileOut->SColorResolution = GifFileIn->SColorResolution;
    GifFileOut->SBackGroundColor = GifFileIn->SBackGroundColor;
    if (GifFileIn->SColorMap != NULL)
	GifFileOut->SColorMap = GifMakeMapObject(
	    GifFileIn->SColorMap->ColorCount,
	    GifFileIn->SColorMap->Colors);

    for (i = 0; i < GifFileIn->ImageCount; i++)
	(void) GifMakeSavedImage(GifFileOut, &GifFileIn->SavedImages[i]);

    if (EGifSpew(GifFileOut) == GIF_ERROR)
	PrintGifError(GifFileOut->Error);
    else if (DGifCloseFile(GifFileIn, &ErrorCode) == GIF_ERROR)
	PrintGifError(ErrorCode);

    return 0;
}

/* end */
