/*
 * pdfdraw:
 *   Draw pages to PPM bitmaps.
 *   Dump parsed display list as XML.
 *   Dump text content as UTF-8.
 *   Benchmark rendering speed.
 */

#include "pdftool.h"

#ifdef _MSC_VER
#include <winsock2.h>
#else
#include <sys/time.h>
#endif

enum { DRAWPNM, DRAWTXT, DRAWXML };

struct benchmark
{
	int pages;
	long min;
	int minpage;
	long avg;
	long max;
	int maxpage;
};

static fz_renderer *drawgc = nil;

static int drawmode = DRAWPNM;
static char *drawpattern = nil;
static pdf_page *drawpage = nil;
static float drawzoom = 1.0;
static int drawrotate = 0;
static int drawbands = 1;
static int drawcount = 0;
static int benchmark = 0;

static void local_cleanup(void)
{
	if (xref && xref->store)
	{
		pdf_dropstore(xref->store);
		xref->store = nil;
	}

	if (drawgc)
	{
		fz_droprenderer(drawgc);
		drawgc = nil;
	}
}

static void drawusage(void)
{
	fprintf(stderr,
		"usage: pdfdraw [options] [file.pdf pages ... ]\n"
		"  -b -\tdraw page in N bands\n"
		"  -d -\tpassword for decryption\n"
		"  -o -\tpattern (%%d for page number) for output file\n"
		"  -r -\tresolution in dpi\n"
		"  -t  \tutf-8 text output instead of graphics\n"
		"  -x  \txml dump of display tree\n"
		"  -m  \tprint benchmark results\n"
		"  example:\n"
		"    pdfdraw -o output%%03d.pnm input.pdf 1-3,5,9-\n");
	exit(1);
}

static void gettime(long *time_)
{
	struct timeval tv;

	if (gettimeofday(&tv, NULL) < 0)
		abort();

	*time_ = tv.tv_sec * 1000000 + tv.tv_usec;
}

static void drawloadpage(int pagenum, struct benchmark *loadtimes)
{
	fz_error error;
	fz_obj *pageobj;
	long start;
	long end;
	long elapsed;

	fprintf(stderr, "draw %s:%03d ", basename, pagenum);
	if (benchmark && loadtimes)
	{
		fflush(stderr);
		gettime(&start);
	}

	pageobj = pdf_getpageobject(xref, pagenum);
	error = pdf_loadpage(&drawpage, xref, pageobj);
	if (error)
		die(error);

	if (benchmark && loadtimes)
	{
		gettime(&end);
		elapsed = end - start;

		if (elapsed < loadtimes->min)
		{
			loadtimes->min = elapsed;
			loadtimes->minpage = pagenum;
		}
		if (elapsed > loadtimes->max)
		{
			loadtimes->max = elapsed;
			loadtimes->maxpage = pagenum;
		}
		loadtimes->avg += elapsed;
		loadtimes->pages++;
	}

	if (benchmark)
		fflush(stderr);
}

static void drawfreepage(void)
{
	pdf_droppage(drawpage);
	drawpage = nil;

	flushxref();

	/* Flush resources between pages.
	 * TODO: should check memory usage before deciding to do this.
	 */
	if (xref && xref->store)
	{
		/* pdf_debugstore(xref->store); */
		pdf_agestoreditems(xref->store);
		pdf_evictageditems(xref->store);
		fflush(stderr);
	}
}

static void drawpnm(int pagenum, struct benchmark *loadtimes, struct benchmark *drawtimes)
{
	fz_error error;
	fz_matrix ctm;
	fz_irect bbox;
	fz_pixmap *pix;
	char name[256];
	char pnmhdr[256];
	int i, x, y, w, h, b, bh;
	int fd = -1;
	long start;
	long end;
	long elapsed;
	fz_md5 digest;

	if (!drawpattern)
		fz_md5init(&digest);

	drawloadpage(pagenum, loadtimes);

	if (benchmark)
		gettime(&start);

	ctm = fz_identity();
	ctm = fz_concat(ctm, fz_translate(0, -drawpage->mediabox.y1));
	ctm = fz_concat(ctm, fz_scale(drawzoom, -drawzoom));
	ctm = fz_concat(ctm, fz_rotate(drawrotate + drawpage->rotate));

	bbox = fz_roundrect(fz_transformaabb(ctm, drawpage->mediabox));
	w = bbox.x1 - bbox.x0;
	h = bbox.y1 - bbox.y0;
	bh = h / drawbands;

	if (drawpattern)
	{
		sprintf(name, drawpattern, drawcount++);
		fd = open(name, O_BINARY|O_WRONLY|O_CREAT|O_TRUNC, 0666);
		if (fd < 0)
			die(fz_throw("ioerror: could not open file '%s'", name));

		sprintf(pnmhdr, "P6\n%d %d\n255\n", w, h);
		write(fd, pnmhdr, strlen(pnmhdr));
	}

	error = fz_newpixmap(&pix, bbox.x0, bbox.y0, w, bh, 4);
	if (error)
		die(error);

	memset(pix->samples, 0xff, pix->h * pix->w * pix->n);

	for (b = 0; b < drawbands; b++)
	{
		if (drawbands > 1)
			fprintf(stderr, "drawing band %d / %d\n", b + 1, drawbands);

		error = fz_rendertreeover(drawgc, pix, drawpage->tree, ctm);
		if (error)
			die(error);

		if (drawpattern)
		{
			for (y = 0; y < pix->h; y++)
			{
				unsigned char *src = pix->samples + y * pix->w * 4;
				unsigned char *dst = src;

				for (x = 0; x < pix->w; x++)
				{
					dst[x * 3 + 0] = src[x * 4 + 1];
					dst[x * 3 + 1] = src[x * 4 + 2];
					dst[x * 3 + 2] = src[x * 4 + 3];
				}

				write(fd, dst, pix->w * 3);

				memset(src, 0xff, pix->w * 4);
			}
		}

		if (!drawpattern)
			fz_md5update(&digest, pix->samples, pix->h * pix->w * 4);

		pix->y += bh;
		if (pix->y + pix->h > bbox.y1)
			pix->h = bbox.y1 - pix->y;
	}

	fz_droppixmap(pix);

	if (!drawpattern) {
		unsigned char buf[16];
		fz_md5final(&digest, buf);
		for (i = 0; i < 16; i++)
			fprintf(stderr, "%02x", buf[i]);
	}

	if (drawpattern)
		close(fd);

	drawfreepage();

	if (benchmark)
	{
		gettime(&end);
		elapsed = end - start;

		if (elapsed < drawtimes->min)
		{
			drawtimes->min = elapsed;
			drawtimes->minpage = pagenum;
		}
		if (elapsed > drawtimes->max)
		{
			drawtimes->max = elapsed;
			drawtimes->maxpage = pagenum;
		}
		drawtimes->avg += elapsed;
		drawtimes->pages++;

		fprintf(stderr, " time %.3fs",
			elapsed / 1000000.0);
	}

	fprintf(stderr, "\n");
}

static void drawtxt(int pagenum)
{
	fz_error error;
	pdf_textline *line;
	fz_matrix ctm;

	drawloadpage(pagenum, NULL);

	ctm = fz_concat(
		fz_translate(0, -drawpage->mediabox.y1),
		fz_scale(drawzoom, -drawzoom));

	error = pdf_loadtextfromtree(&line, drawpage->tree, ctm);
	if (error)
		die(error);

	pdf_debugtextline(line);
	pdf_droptextline(line);

	drawfreepage();
}

static void drawxml(int pagenum)
{
	drawloadpage(pagenum, NULL);
	fz_debugtree(drawpage->tree);
	drawfreepage();
}

static void drawpages(char *pagelist)
{
	int page, spage, epage;
	char *spec, *dash;
	struct benchmark loadtimes, drawtimes;

	if (!xref)
		drawusage();

	if (benchmark)
	{
		memset(&loadtimes, 0x00, sizeof (loadtimes));
		loadtimes.min = LONG_MAX;
		memset(&drawtimes, 0x00, sizeof (drawtimes));
		drawtimes.min = LONG_MAX;
	}

	spec = strsep(&pagelist, ",");
	while (spec)
	{
		dash = strchr(spec, '-');

		if (dash == spec)
			spage = epage = 1;
		else
			spage = epage = atoi(spec);

		if (dash)
		{
			if (strlen(dash) > 1)
				epage = atoi(dash + 1);
			else
				epage = pagecount;
		}

		if (spage > epage)
			page = spage, spage = epage, epage = page;

		if (spage < 1)
			spage = 1;
		if (epage > pagecount)
			epage = pagecount;

		printf("Drawing pages %d-%d...\n", spage, epage);
		for (page = spage; page <= epage; page++)
		{
			switch (drawmode)
			{
			case DRAWPNM: drawpnm(page, &loadtimes, &drawtimes); break;
			case DRAWTXT: drawtxt(page); break;
			case DRAWXML: drawxml(page); break;
			}
		}

		spec = strsep(&pagelist, ",");
	}

	if (benchmark)
	{
		if (loadtimes.pages > 0)
		{
			loadtimes.avg /= loadtimes.pages;
			drawtimes.avg /= drawtimes.pages;

			printf("benchmark[load]: min: %6.3fs (page % 4d), avg: %6.3fs, max: %6.3fs (page % 4d)\n",
				loadtimes.min / 1000000.0, loadtimes.minpage,
				loadtimes.avg / 1000000.0,
				loadtimes.max / 1000000.0, loadtimes.maxpage);
			printf("benchmark[draw]: min: %6.3fs (page % 4d), avg: %6.3fs, max: %6.3fs (page % 4d)\n",
				drawtimes.min / 1000000.0, drawtimes.minpage,
				drawtimes.avg / 1000000.0,
				drawtimes.max / 1000000.0, drawtimes.maxpage);
		}
	}
}

int main(int argc, char **argv)
{
	fz_error error;
	char *password = "";
	int c;
	enum { NO_FILE_OPENED, NO_PAGES_DRAWN, DREW_PAGES } state;

	while ((c = fz_getopt(argc, argv, "b:d:o:r:txm")) != -1)
	{
		switch (c)
		{
		case 'b': drawbands = atoi(fz_optarg); break;
		case 'd': password = fz_optarg; break;
		case 'o': drawpattern = fz_optarg; break;
		case 'r': drawzoom = atof(fz_optarg) / 72.0; break;
		case 't': drawmode = DRAWTXT; break;
		case 'x': drawmode = DRAWXML; break;
		case 'm': benchmark = 1; break;
		default:
			drawusage();
			break;
		}
	}

	if (fz_optind == argc)
		drawusage();

	setcleanup(local_cleanup);

	state = NO_FILE_OPENED;
	while (fz_optind < argc)
	{
		if (strstr(argv[fz_optind], ".pdf") || strstr(argv[fz_optind], ".PDF"))
		{
			if (state == NO_PAGES_DRAWN)
				drawpages("1-");

			closexref();

			error = fz_newrenderer(&drawgc, pdf_devicergb, 0, 1024 * 512);
			if (error)
				die(error);

			openxref(argv[fz_optind], password, 0);
			state = NO_PAGES_DRAWN;
		}
		else
		{
			drawpages(argv[fz_optind]);
			state = DREW_PAGES;
		}
		fz_optind++;
	}

	if (state == NO_PAGES_DRAWN)
		drawpages("1-");

	closexref();
}

