/*
 * Copyright 2008 Sean Fox <dyntryx@gmail.com>
 * Copyright 2008 James Bursa <james@netsurf-browser.org>
 * Copyright 2022 Michael Drake <tlsa@netsurf-browser.org>
 *
 * This file is part of NetSurf's libnsgif, http://www.netsurf-browser.org/
 * Licenced under the MIT License,
 *                http://www.opensource.org/licenses/mit-license.php
 */

#include <assert.h>
#include <errno.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>

#include "../include/nsgif.h"

#include "cli.h"
#include "cli.c"

#define STR_VAL(_S) STR(_S)
#define STR(_S) #_S

#define BYTES_PER_PIXEL 4

static struct nsgif_options {
	const char *file;
	const char *ppm;
	uint64_t loops;
	bool palette;
	bool version;
	bool info;
	bool help;
} nsgif_options;

static const struct cli_table_entry cli_entries[] = {
	{
		.s = 'h',
		.l = "help",
		.t = CLI_BOOL,
		.no_pos = true,
		.v.b = &nsgif_options.help,
		.d = "Print this text.",
	},
	{
		.s = 'i',
		.l = "info",
		.t = CLI_BOOL,
		.v.b = &nsgif_options.info,
		.d = "Dump GIF info to stdout."
	},
	{
		.s = 'l',
		.l = "loops",
		.t = CLI_UINT,
		.v.u = &nsgif_options.loops,
		.d = "Loop through decoding all frames N times. "
		     "The default is 1."
	},
	{
		.s = 'm',
		.l = "ppm",
		.t = CLI_STRING,
		.v.s = &nsgif_options.ppm,
		.d = "Convert frames to PPM image at given path."
	},
	{
		.s = 'p',
		.l = "palette",
		.t = CLI_BOOL,
		.v.b = &nsgif_options.palette,
		.d = "Save palette images."
	},
	{
		.s = 'V',
		.l = "version",
		.t = CLI_BOOL,
		.no_pos = true,
		.v.b = &nsgif_options.version,
		.d = "Print version number."
	},
	{
		.p = true,
		.l = "FILE",
		.t = CLI_STRING,
		.v.s = &nsgif_options.file,
		.d = "Path to GIF file to load."
	},
};

const struct cli_table cli = {
	.entries = cli_entries,
	.count = (sizeof(cli_entries))/(sizeof(*cli_entries)),
	.min_positional = 1,
	.d = "NSGIF - A utility for inspecting and decoding GIFs with libnsgif",
};

static void *bitmap_create(int width, int height)
{
	/* Ensure a stupidly large bitmap is not created */
	if (width > 4096 || height > 4096) {
		return NULL;
	}

	return calloc(width * height, BYTES_PER_PIXEL);
}

static unsigned char *bitmap_get_buffer(void *bitmap)
{
	return bitmap;
}

static void bitmap_destroy(void *bitmap)
{
	free(bitmap);
}

static uint8_t *load_file(const char *path, size_t *data_size)
{
	FILE *fd;
	struct stat sb;
	unsigned char *buffer;
	size_t size;
	size_t n;

	fd = fopen(path, "rb");
	if (!fd) {
		perror(path);
		exit(EXIT_FAILURE);
	}

	if (stat(path, &sb)) {
		perror(path);
		exit(EXIT_FAILURE);
	}
	size = sb.st_size;

	buffer = malloc(size);
	if (!buffer) {
		fprintf(stderr, "Unable to allocate %lld bytes\n",
			(long long) size);
		exit(EXIT_FAILURE);
	}

	n = fread(buffer, 1, size, fd);
	if (n != size) {
		perror(path);
		exit(EXIT_FAILURE);
	}

	fclose(fd);

	*data_size = size;
	return buffer;
}

static void warning(const char *context, nsgif_error err)
{
	fprintf(stderr, "%s: %s\n", context, nsgif_strerror(err));
}

static void print_gif_info(const nsgif_info_t *info)
{
	const uint8_t *bg = (uint8_t *) &info->background;

	fprintf(stdout, "gif:\n");
	fprintf(stdout, "  width: %"PRIu32"\n", info->width);
	fprintf(stdout, "  height: %"PRIu32"\n", info->height);
	fprintf(stdout, "  max-loops: %"PRIu32"\n", info->loop_max);
	fprintf(stdout, "  frame-count: %"PRIu32"\n", info->frame_count);
	fprintf(stdout, "  global palette: %s\n", info->global_palette ? "yes" : "no");
	fprintf(stdout, "  background:\n");
	fprintf(stdout, "    red: 0x%"PRIx8"\n", bg[0]);
	fprintf(stdout, "    green: 0x%"PRIx8"\n", bg[1]);
	fprintf(stdout, "    blue: 0x%"PRIx8"\n", bg[2]);
	fprintf(stdout, "  frames:\n");
}

static void print_gif_frame_info(const nsgif_frame_info_t *info, uint32_t i)
{
	const char *disposal = nsgif_str_disposal(info->disposal);

	fprintf(stdout, "  - frame: %"PRIu32"\n", i);
	fprintf(stdout, "    local palette: %s\n", info->local_palette ? "yes" : "no");
	fprintf(stdout, "    disposal-method: %s\n", disposal);
	fprintf(stdout, "    transparency: %s\n", info->transparency ? "yes" : "no");
	fprintf(stdout, "    interlaced: %s\n", info->interlaced ? "yes" : "no");
	fprintf(stdout, "    display: %s\n", info->display ? "yes" : "no");
	fprintf(stdout, "    delay: %"PRIu32"\n", info->delay);
	fprintf(stdout, "    rect:\n");
	fprintf(stdout, "      x: %"PRIu32"\n", info->rect.x0);
	fprintf(stdout, "      y: %"PRIu32"\n", info->rect.y0);
	fprintf(stdout, "      w: %"PRIu32"\n", info->rect.x1 - info->rect.x0);
	fprintf(stdout, "      h: %"PRIu32"\n", info->rect.y1 - info->rect.y0);
}

static bool save_palette(
		const char *img_filename,
		const char *palette_filename,
		const uint32_t palette[NSGIF_MAX_COLOURS],
		size_t used_entries)
{
	enum {
		SIZE = 32,
		COUNT = 16,
	};
	FILE *f;
	int size = COUNT * SIZE + 1;

	f = fopen(palette_filename, "w+");
	if (f == NULL) {
		fprintf(stderr, "Unable to open %s for writing\n",
				palette_filename);
		return false;
	}

	fprintf(f, "P3\n");
	fprintf(f, "# %s: %s\n", img_filename, palette_filename);
	fprintf(f, "# Colour count: %zu\n", used_entries);
	fprintf(f, "%u %u 256\n", size, size);

	for (int y = 0; y < size; y++) {
		for (int x = 0; x < size; x++) {
			if (x % SIZE == 0 || y % SIZE == 0) {
				fprintf(f, "0 0 0 ");
			} else {
				size_t offset = y / SIZE * COUNT + x / SIZE;
				uint8_t *entry = (uint8_t *)&palette[offset];

				fprintf(f, "%u %u %u ",
						entry[0],
						entry[1],
						entry[2]);
			}
		}

		fprintf(f, "\n");
	}

	fclose(f);

	return true;
}

static bool save_global_palette(const nsgif_t *gif)
{
	uint32_t table[NSGIF_MAX_COLOURS];
	size_t entries;

	nsgif_global_palette(gif, table, &entries);

	return save_palette(nsgif_options.file, "global-palette.ppm",
			table, entries);
}

static bool save_local_palette(const nsgif_t *gif, uint32_t frame)
{
	static uint32_t table[NSGIF_MAX_COLOURS];
	char filename[64];
	size_t entries;

	snprintf(filename, sizeof(filename), "local-palette-%"PRIu32".ppm",
			frame);

	if (!nsgif_local_palette(gif, frame, table, &entries)) {
		return false;
	}

	return save_palette(nsgif_options.file, filename, table, entries);
}

static void decode(FILE* ppm, const char *name, nsgif_t *gif, bool first)
{
	nsgif_error err;
	uint32_t frame_prev = 0;
	const nsgif_info_t *info;

	info = nsgif_get_info(gif);

	if (first && ppm != NULL) {
		fprintf(ppm, "P3\n");
		fprintf(ppm, "# %s\n", name);
		fprintf(ppm, "# width                %u \n", info->width);
		fprintf(ppm, "# height               %u \n", info->height);
		fprintf(ppm, "# frame_count          %u \n", info->frame_count);
		fprintf(ppm, "# loop_max             %u \n", info->loop_max);
		fprintf(ppm, "%u %u 256\n", info->width,
				info->height * info->frame_count);
	}

	if (first && nsgif_options.info) {
		print_gif_info(info);
	}
	if (first && nsgif_options.palette && info->global_palette) {
		save_global_palette(gif);
	}

	/* decode the frames */
	while (true) {
		nsgif_bitmap_t *bitmap;
		const uint8_t *image;
		uint32_t frame_new;
		uint32_t delay_cs;
		nsgif_rect_t area;

		err = nsgif_frame_prepare(gif, &area,
				&delay_cs, &frame_new);
		if (err != NSGIF_OK) {
			warning("nsgif_frame_prepare", err);
			return;
		}

		if (frame_new < frame_prev) {
			/* Must be an animation that loops. We only care about
			 * decoding each frame once in this utility. */
			return;
		}
		frame_prev = frame_new;

		if (first && nsgif_options.info) {
			const nsgif_frame_info_t *f_info;

			f_info = nsgif_get_frame_info(gif, frame_new);
			if (f_info != NULL) {
				print_gif_frame_info(f_info, frame_new);
			}
		}
		if (first && nsgif_options.palette) {
			save_local_palette(gif, frame_new);
		}

		err = nsgif_frame_decode(gif, frame_new, &bitmap);
		if (err != NSGIF_OK) {
			fprintf(stderr, "Frame %"PRIu32": "
					"nsgif_decode_frame failed: %s\n",
					frame_new, nsgif_strerror(err));
			/* Continue decoding the rest of the frames. */

		} else if (first && ppm != NULL) {
			fprintf(ppm, "# frame %u:\n", frame_new);
			image = (const uint8_t *) bitmap;
			for (uint32_t y = 0; y != info->height; y++) {
				for (uint32_t x = 0; x != info->width; x++) {
					size_t z = (y * info->width + x) * 4;
					fprintf(ppm, "%u %u %u ",
							image[z],
							image[z + 1],
							image[z + 2]);
				}
				fprintf(ppm, "\n");
			}
		}

		if (delay_cs == NSGIF_INFINITE) {
			/** This frame is the last. */
			return;
		}
	}
}

int main(int argc, char *argv[])
{
	const nsgif_bitmap_cb_vt bitmap_callbacks = {
		.create     = bitmap_create,
		.destroy    = bitmap_destroy,
		.get_buffer = bitmap_get_buffer,
	};
	size_t size;
	nsgif_t *gif;
	uint8_t *data;
	nsgif_error err;
	FILE *ppm = NULL;

	/* Override default options with any command line args */
	if (!cli_parse(&cli, argc, (void *)argv)) {
		cli_help(&cli, argv[0]);
		return EXIT_FAILURE;
	}

	if (nsgif_options.help) {
		cli_help(&cli, argv[0]);
		return EXIT_SUCCESS;
	}

	if (nsgif_options.version) {
		printf("%s %s\n", STR_VAL(NSGIF_NAME), STR_VAL(NSGIF_VERSION));
		return EXIT_SUCCESS;
	}

	if (nsgif_options.ppm != NULL) {
		ppm = fopen(nsgif_options.ppm, "w+");
		if (ppm == NULL) {
			fprintf(stderr, "Unable to open %s for writing\n",
					nsgif_options.ppm);
			return EXIT_FAILURE;
		}
	}

	/* create our gif animation */
	err = nsgif_create(&bitmap_callbacks, NSGIF_BITMAP_FMT_R8G8B8A8, &gif);
	if (err != NSGIF_OK) {
		warning("nsgif_create", err);
		return EXIT_FAILURE;
	}

	/* load file into memory */
	data = load_file(nsgif_options.file, &size);

	/* Scan the raw data */
	err = nsgif_data_scan(gif, size, data);
	if (err != NSGIF_OK) {
		/* Not fatal; some GIFs are nasty. Can still try to decode
		 * any frames that were decoded successfully. */
		warning("nsgif_data_scan", err);
	}

	nsgif_data_complete(gif);

	if (nsgif_options.loops == 0) {
		nsgif_options.loops = 1;
	}

	for (uint64_t i = 0; i < nsgif_options.loops; i++) {
		decode(ppm, nsgif_options.file, gif, i == 0);

		/* We want to ignore any loop limit in the GIF. */
		nsgif_reset(gif);
	}

	if (ppm != NULL) {
		fclose(ppm);
	}

	/* clean up */
	nsgif_destroy(gif);
	free(data);

	return 0;
}
