/* vim: set noet ts=4:
 *
 * Copyright (c) 2002-2013 Martin A. Godisch <martin@godisch.de>
 */

#include <assert.h>
#include <errno.h>
#include <getopt.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>
#include <X11/xpm.h>

#include "wmgeneral.h"
#include "wmpuzzle.h"

#define TILE_X(N)   ((N) % 4 * 14 + 4)
#define TILE_Y(N)   ((N) / 4 * 14 + 4)
#define BLANK_X     (0)
#define BLANK_Y     (64)
#define LAST_X      (14)
#define LAST_Y      (64)
#define TEMP_X      (28)
#define TEMP_Y      (64)
#define TILE_WIDTH  (14)
#define TILE_HEIGHT (14)

#define BUFF_LEN    (256)

char
#ifdef BSD
	*image     = "daemon",
#else
	*image     = "linux",
#endif
	*rcname    = NULL;
int blank      = 15,
	puzzle[16] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};

int main(int argc, char *argv[])
{
	int n,
		but_stat       = -1,
		auto_save      =  0,
		use_keyboard   =  0,
		shuffle_count  = -1;
	char
		*geometry      = NULL,
		*xdisplay      = NULL,
#ifdef BSD
		**wmpuzzle_xpm = daemon_xpm,
#else
		**wmpuzzle_xpm = linux_xpm,
#endif
		state[16]      = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15},
		wmpuzzle_mask[64*64];
	static int signals[] =
		{SIGALRM, SIGHUP, SIGINT, SIGPIPE, SIGTERM, SIGUSR1, SIGUSR2, 0};
	XEvent
		Event;

	assert(sizeof(char) == 1);

	if (getenv("HOME") == NULL || *getenv("HOME") == 0) {
		fprintf(stderr, "%s: unknown $HOME directory, please check your environment\n", PACKAGE_NAME);
		return -1;
	}

	do_opts(argc, argv, &wmpuzzle_xpm, &auto_save, &use_keyboard, &shuffle_count, &xdisplay, &geometry);

	n = strlen(getenv("HOME")) + strlen("/.wmpuzzlerc") + 1;
	if ((rcname = malloc(n)) == NULL) {
		fprintf(stderr, "%s: %s\n", PACKAGE_NAME, strerror(errno));
		exit(1);
	}
	snprintf(rcname, n, "%s/%s", getenv("HOME"), ".wmpuzzlerc");

	for (n = 0; signals[n]; n++) {
		if (signal(signals[n], handler) == SIG_ERR) {
			fprintf(stderr, "%s: cannot set handler for signal %d\n", PACKAGE_NAME, signals[n]);
			fprintf(stderr, "%s: %s\n", PACKAGE_NAME, strerror(errno));
		}
	}

	initXwindow(xdisplay);
	createXBMfromXPM(wmpuzzle_mask, wmpuzzle_xpm, 64, 64);
	openXwindow(argc, argv, wmpuzzle_xpm, wmpuzzle_mask, 64, 64, geometry, NULL);
	for (n = 0; n < 16; n++)
		AddMouseRegion(n, TILE_X(n), TILE_Y(n), TILE_X(n) + TILE_WIDTH - 1, TILE_Y(n) + TILE_HEIGHT - 1);

	if (shuffle_count >= 0 || !read_state(state))
		shuffle(shuffle_count == -1 ? DEFAULT_SHUFFLE_COUNT : shuffle_count, state);
	if (auto_save)
		atexit(write_state);

	copyXPMArea(TILE_X(15), TILE_Y(15), TILE_WIDTH, TILE_HEIGHT, LAST_X, LAST_Y);
	prepare(state);

	while (1) {
		while (XPending(display)) {
			XNextEvent(display, &Event);
			switch (Event.type) {
			case Expose:
				RedrawWindow();
				break;
			case DestroyNotify:
				XCloseDisplay(display);
				exit(0);
			case ButtonPress:
				but_stat = CheckMouseRegion(Event.xbutton.x, Event.xbutton.y);
				break;
			case ButtonRelease:
				switch (Event.xbutton.button) {
				case Button1:
					n = CheckMouseRegion(Event.xbutton.x, Event.xbutton.y);
					if (but_stat >= 0 && n == but_stat && valid(n)) {
						move(n);
						RedrawWindow();
					}
					break;
				case Button2:
					if (read_state(state)) {
						prepare(state);
						RedrawWindow();
					}
					break;
				case Button3:
					write_state();
					break;
				}
				but_stat = -1;
				break;
			case KeyPress:
				if (!use_keyboard)
					break;
				switch (Event.xkey.keycode) {
				case 27: /* r */
					if (read_state(state)) {
						prepare(state);
						RedrawWindow();
					}
					break;
				case 39: /* s */
					write_state();
					break;
				case  98: /* up */
					if (blank >= 12)
						break;
					move(blank + 4);
					break;
				case 100: /* left */
					if (blank % 4 == 3)
						break;
					move(blank + 1);
					break;
				case 102: /* right */
					if (blank % 4 == 0)
						break;
					move(blank - 1);
					break;
				case 104: /* down */
					if (blank < 4)
						break;
					move(blank - 4);
					break;
				}
				but_stat = -1;
				RedrawWindow();
				break;
			case EnterNotify:
				if (use_keyboard)
					XSetInputFocus(display, PointerRoot, RevertToParent, CurrentTime);
				break;
			case LeaveNotify:
				if (use_keyboard)
					XSetInputFocus(display, PointerRoot, RevertToParent, CurrentTime);
				break;
			}
		}
		usleep(50000L);
	}
}

void do_opts(int argc, char **argv, char ***wmpuzzle, int *autosave, int *keyboard, int *shuffle, char **xdisplay, char **geometry)
{
	char *s;
	int  i;

	static struct option long_opts[] = {
		{"help",      0, NULL, 'h'},
		{"auto-save", 0, NULL, 'a'},
		{"keyboard",  0, NULL, 'k'},
		{"shuffle",   1, NULL, 's'},
		{"version",   0, NULL, 'v'},
		{"display",   1, NULL,  0 },
		{"geometry",  1, NULL,  1 },
		{NULL,        0, NULL,  0}};
	int opt_index = 0;

	while (1) {
		if ((i = getopt_long(argc, argv, "haks:v", long_opts, &opt_index)) == -1)
			break;
		switch (i) {
		case 'a':
			*autosave = 1;
			break;
		case 'k':
			*keyboard = 1;
			break;
		case 's':
			i = strtol(optarg, &s, 10);
			if (*s || i <= 0) {
				fprintf(stderr, "%s: invalid shuffle count '%s'\n", PACKAGE_NAME, optarg);
				exit(1);
			}
			*shuffle = i;
			break;
		case 'h':
			printf("usage: %s [options] [<xpm image file>]\n", PACKAGE_NAME);
			printf("  -h, --help             displays this command line summary,\n");
			printf("  -a, --auto-save        automatically save the puzzle state on program exit,\n");
			printf("  -k, --keyboard         enables the arrow keys on the keyboard,\n");
			printf("  -s, --shuffle <count>  shuffles the image <count> times, default %d,\n", DEFAULT_SHUFFLE_COUNT);
			printf("  -v, --version          displays the version number,\n");
			printf("  --display=<id>         open the mini window on display <id>, e.g. ':0.0',\n");
			printf("  --geometry=<pos>       open the mini window at position <pos>, e.g. '+10+10'.\n");
			exit(0);
		case 'v':
			printf("%s %s\n", PACKAGE_NAME, PACKAGE_VERSION);
			printf("copyright (c) 2002-2013 Martin A. Godisch <martin@godisch.de>\n");
			exit(0);
		case 0:
			*xdisplay = optarg;
			break;
		case 1:
			*geometry = optarg;
			break;
		case '?':
			exit(1);
		}
	}
	if (optind < argc) {
		optarg = argv[optind++];
		if ((errno = XpmReadFileToData(optarg, wmpuzzle)) < 0) {
			fprintf(stderr, "%s: cannot read XPM from '%s'\n", PACKAGE_NAME, optarg);
				fprintf(stderr, "%s: %s\n", PACKAGE_NAME, XpmGetErrorString(errno));
			exit(1);
		}
		if (transform(wmpuzzle) < 0) {
			switch (errno) {
			case EINTERNAL:
				fprintf(stderr, "%s: internal error reading '%s'\n", PACKAGE_NAME, optarg);
				break;
			case EINVALXPM:
				fprintf(stderr, "%s: unknown format of XPM file '%s'\n", PACKAGE_NAME, optarg);
				break;
			case ESMALLXPM:
				fprintf(stderr, "%s: XPM '%s' is too small, minimum is 48x48 pixels\n", PACKAGE_NAME, optarg);
				break;
			default:
				fprintf(stderr, "%s: %s\n", PACKAGE_NAME, strerror(errno));
			}
			exit(1);
		}
		if ((s = strstr(optarg, ".xpm")) != NULL)
			*s = '\0';
		else if ((s = strstr(optarg, ".XPM")) != NULL)
			*s = '\0';
		if ((s = rindex(optarg, '/')) != NULL)
			image = s + 1;
		else
			image = optarg;
	}
	if (optind < argc) {
		fprintf(stderr, "%s: too many arguments -- %s\n", PACKAGE_NAME, argv[optind++]);
		exit(1);
	}
}

static void handler(int signo)
{
	exit(0);
}

void shuffle(int n, char *state)
{
	int    i = blank, j = blank;
	time_t t;
 
	srand(time(&t));
	for (; n; n--) {
		/* FIXME: optimize this! */
		while (!valid(i) || i == j)
			i = rand() % 16;
		j = state[blank];
		state[blank] = state[i];
		state[i] = j;
		j = blank;
		blank = i;
	}
}

void prepare(const char *state)
{
	int i, j, k;

	for (i = 0; i < 16; i++) {
		for (j = 0; puzzle[j] != state[i]; j++);
		if (i == j)
			continue;
		copyXPMArea(TILE_X(i), TILE_Y(i), TILE_WIDTH, TILE_HEIGHT, TEMP_X,    TEMP_Y);
		copyXPMArea(TILE_X(j), TILE_Y(j), TILE_WIDTH, TILE_HEIGHT, TILE_X(i), TILE_Y(i));
		copyXPMArea(TEMP_X,    TEMP_Y,    TILE_WIDTH, TILE_HEIGHT, TILE_X(j), TILE_Y(j));
		k         = puzzle[i];
		puzzle[i] = puzzle[j];
		puzzle[j] = k;
	}
	copyXPMArea(BLANK_X, BLANK_Y, TILE_WIDTH, TILE_HEIGHT, TILE_X(blank), TILE_Y(blank));
}

void move(int n)
{
	int i;

	copyXPMArea(TILE_X(n), TILE_Y(n), TILE_WIDTH, TILE_HEIGHT, TILE_X(blank), TILE_Y(blank));
	copyXPMArea(BLANK_X,   BLANK_Y,   TILE_WIDTH, TILE_HEIGHT, TILE_X(n),     TILE_Y(n));
	puzzle[blank] = puzzle[n];
	puzzle[n] = 15;
	blank = n;
	if (n != 15)
		return;
	for (i = 0; i < 16; i++)
		if (puzzle[i] != i)
			return;
	copyXPMArea(LAST_X, LAST_Y, TILE_WIDTH, TILE_HEIGHT, TILE_X(n), TILE_Y(n));
}

int valid(int n)
{
	return(
		(n / 4 == blank / 4 && (n % 4 == blank % 4 + 1 || n % 4 == blank % 4 - 1)) ||
		(n % 4 == blank % 4 && (n / 4 == blank / 4 + 1 || n / 4 == blank / 4 - 1)));
}

char *read_state(char *state)
{
	FILE *F;
	char buffer[BUFF_LEN], *p, *s, temp[17];
	int  i, line;

	if ((F = fopen(rcname, "r")) == NULL) {
		if (errno != ENOENT) {
			fprintf(stderr, "%s: cannot open rcfile %s\n", PACKAGE_NAME, rcname);
			fprintf(stderr, "%s: %s\n", PACKAGE_NAME, strerror(errno));
		}
		return(NULL);
	}

	for (line = 1;; line++) {
		fgets(buffer, sizeof(buffer), F);
		if (feof(F))
			break;
		if ((s = strchr(buffer, '\n')))
			*s = '\0';
		if (buffer[0] == '#' || buffer[0] == '\0')
			continue;
		s = strchr(buffer, ':');
		if (!s) {
			fprintf(stderr, "%s: missing ':' in %s line %d\n", PACKAGE_NAME, rcname, line);
			continue;
		}
		*s = '\0';
		s++;
		if (strcmp(buffer, image))
			continue;
		if (strlen(s) != 16) {
			fprintf(stderr, "%s: invalid state in %s line %d\n", PACKAGE_NAME, rcname, line);
			fclose(F);
			return(NULL);
		}
		s += 15;
		for (i = 15; i >= 0; i--) {
			temp[i] = strtol(s--, &p, 16);
			if (*p) {
				fprintf(stderr, "%s: invalid state in %s line %d\n", PACKAGE_NAME, rcname, line);
				fclose(F);
				return(NULL);
			}
			if (temp[i] == 15)
				blank = i;
			s[1] = '\0';
		}
		memcpy(state, temp, 16);
		fclose(F);
		return(state);
	}
	fclose(F);
	return(NULL);
}

void write_state(void)
{
	FILE *F, *G;
	char *s, buffer[BUFF_LEN], entry[BUFF_LEN], tmpname[BUFF_LEN + 4];
	int  line, written = 0;

	strcpy(tmpname, rcname);
	strcat(tmpname, ".new");
	umask(0077);
	if ((F = fopen(tmpname, "w")) == NULL) {
		fprintf(stderr, "%s: cannot open tempfile '%s'\n", PACKAGE_NAME, tmpname);
		fprintf(stderr, "%s: %s\n", PACKAGE_NAME, strerror(errno));
		return;
	}

	if ((G = fopen(rcname, "r")) == NULL && errno != ENOENT) {
		fprintf(stderr, "%s: cannot open rcfile '%s'\n", PACKAGE_NAME, rcname);
		fprintf(stderr, "%s: %s\n", PACKAGE_NAME, strerror(errno));
		fclose(F);
		unlink(tmpname);
		return;
	}

	sprintf(entry, "%s:%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x\n", image,
		puzzle[0],  puzzle[1],  puzzle[2],  puzzle[3],
		puzzle[4],  puzzle[5],  puzzle[6],  puzzle[7],
		puzzle[8],  puzzle[9],  puzzle[10], puzzle[11],
		puzzle[12], puzzle[13], puzzle[14], puzzle[15]);

	if (G) {
		for (line = 1;; line++) {
			if (line > 1)
				fputs(buffer, F);
			fgets(buffer, sizeof(buffer), G);
			if (feof(G))
				break;
			if (buffer[0] == '#' || buffer[0] == '\n')
				continue;
			s = strchr(buffer, ':');
			if (!s) {
				fprintf(stderr, "%s: missing ':' in %s line %d\n", PACKAGE_NAME, rcname, line);
				continue;
			}
			if (!strncmp(buffer, image, s - buffer)) {
				strcpy(buffer, entry);
				written = 1;
			}
		}
		fclose(G);
	}
	if (!written)
		fputs(entry, F);
	fclose(F);

	if (rename(tmpname, rcname)) {
		fprintf(stderr, "%s: cannot overwrite rcfile '%s'\n", PACKAGE_NAME, rcname);
		fprintf(stderr, "%s: %s\n", PACKAGE_NAME, strerror(errno));
		return;
	}
}

int transform(char ***puzzle)
{
	char
		**src     = *puzzle,
		**dst     = NULL,
		*s        = NULL,
		*d        = NULL,
		*color[6] = {
			"#00000000FFFF", "#000000000000", "#861782078E38",
			"#F7DEF3CEFFFF", "#C71BC30BC71B", "#AEBAAAAAAEBA"},
		code[64];
	int max_len, width, height, numcol, chars,
		i, j, k, n, si;

	if (sscanf(src[0], "%d %d %d %d", &width, &height, &numcol, &chars) != 4) {
		errno = EINVALXPM;
		return -1;
	}
	if (width < 48 || height < 48) {
		errno = ESMALLXPM;
		return -1;
	}
	if (chars >= sizeof(code)) {
		errno = EINTERNAL;
		return -1;
	}
	numcol += 6;
	n = 1 + numcol + 64 + 14;
	max_len = 64 * chars + 1;
	if ((dst = malloc(n * sizeof(char*))) == NULL)
		return -1;
	for (i = 0; i < n; i++)
		if ((dst[i] = malloc(max_len)) == NULL)
			return -1;
	*puzzle = dst;
	snprintf(dst[0], max_len, "%d %d %d %d", 64, 78, numcol, chars);
	memset(code, 35, chars);
	code[chars] = '\0';
	for (i = 0; i < 6; i++) {
		TRY_AGAIN:
		for (j = 1; j <= numcol - 6; j++) {
			if (strncmp(src[j], code, chars) == 0) {
				for (k = 0; k < chars; k++)
					if (code[k] < 126) {
						code[k]++;
						goto TRY_AGAIN;
					}
				fprintf(stderr, "%s: not enough free color symbols\n", PACKAGE_NAME);
				exit(1);
			}
		}
		snprintf(dst[i+1], max_len, "%s\tc %s", code, color[i]);
		if ((color[i] = strdup(code)) == NULL)
			return -1;
		if (i < 5) {
			for (k = 0; k < chars; k++)
				if (code[k] < 126) {
					code[k]++;
					goto NEXT_COLOR;
				}
			fprintf(stderr, "%s: not enough free color symbols\n", PACKAGE_NAME);
			exit(1);
		}
		NEXT_COLOR:;
	}
	for (i = 7; i <= numcol; i++)
		snprintf(dst[i], max_len, "%s", src[i-6]);

	for (i = numcol + 1; i <= numcol + 3; i++) {
		d = dst[i];
		for (j = 0; j < 64; j++) {
			memcpy(d, color[0], chars);
			d += chars;
		}
		*d++ = '\0';
	}
	d = dst[numcol + 4];
	for (j = 0; j < 64; j++) {
		if (j < 3 || j > 60)
			memcpy(d, color[0], chars);
		else
			memcpy(d, color[1], chars);
		d += chars;
	}
	*d++ = '\0';
	for (i = 4, si = numcol - 5; i < 60; i++) {
		d = dst[i + numcol + 1];
		for (j = 0; j < 3; j++) {
			memcpy(d, color[0], chars);
			d += chars;
		}
		memcpy(d, color[1], chars);
		d += chars;
		switch (i % 14) {
		case 3:
			for (j = 4; j < 60; j++) {
				if (j % 14 == 4)
					memcpy(d, color[2], chars);
				else
					memcpy(d, color[1], chars);
				d += chars;
			}
			break;
		case 4:
			for (j = 4; j < 60; j++) {
				if (j % 14 == 3)
					memcpy(d, color[2], chars);
				else
					memcpy(d, color[3], chars);
				d += chars;
			}
			break;
		default:
			s = src[si++];
			for (j = 4; j < 60; j++) {
				switch (j % 14) {
				case 3:
					memcpy(d, color[1], chars);
					break;
				case 4:
					memcpy(d, color[3], chars);
					break;
				default:
					if (strlen(s) < chars) {
						errno = EINVALXPM;
						return -1;
					}
					memcpy(d, s, chars);
					s += chars;
				}
				d += chars;
			}
		}
		memcpy(d, color[4], chars);
		d += chars;
		for (j = 61; j < 64; j++) {
			memcpy(d, color[0], chars);
			d += chars;
		}
		*d++ = '\0';
	}
	d = dst[numcol + 61];
	for (j = 0; j < 64; j++) {
		if (j < 3 || j > 60)
			memcpy(d, color[0], chars);
		else
			memcpy(d, color[4], chars);
		d += chars;
	}
	*d++ = '\0';
	for (i = numcol + 62; i <= numcol + 64; i++) {
		d = dst[i];
		for (j = 0; j < 64; j++) {
			memcpy(d, color[0], chars);
			d += chars;
		}
		*d++ = '\0';
	}
	d = dst[numcol + 65];
	for (j = 0; j < 13; j++) {
		memcpy(d, color[1], chars);
		d += chars;
	}
	memcpy(d, color[2], chars);
	d += chars;
	for (j = 14; j < 64; j++) {
		memcpy(d, color[0], chars);
		d += chars;
	}
	*d++ = '\0';
	for (i = numcol + 66; i < numcol + 78; i++) {
		d = dst[i];
		memcpy(d, color[1], chars);
		d += chars;
		for (j = 1; j < 13; j++) {
			memcpy(d, color[5], chars);
			d += chars;
		}
		memcpy(d, color[3], chars);
		d += chars;
		for (j = 14; j < 64; j++) {
			memcpy(d, color[0], chars);
			d += chars;
		}
		*d++ = '\0';
	}
	d = dst[numcol + 78];
	memcpy(d, color[2], chars);
	d += chars;
	for (j = 1; j < 14; j++) {
		memcpy(d, color[3], chars);
		d += chars;
	}
	for (j = 14; j < 64; j++) {
		memcpy(d, color[0], chars);
		d += chars;
	}
	*d++ = '\0';
	/* FIXME: free src */
	return 0;
}
