#include "mupdf/fitz.h"

struct info
{
	fz_colorspace *cs;
	int width, height;
	int maxval, depth;
	int alpha;
	char *tupletype;
};

/* TODO: parse PAM */

static inline int iswhiteeol(int a)
{
	switch (a) {
	case ' ': case '\t': case '\r': case '\n':
		return 1;
	}
	return 0;
}

static inline int iswhite(int a)
{
	switch (a) {
	case ' ': case '\t':
		return 1;
	}
	return 0;
}

static inline int iseol(int a)
{
	switch (a) {
	case '\r': case '\n':
		return 1;
	}
	return 0;
}

static unsigned char *
pnm_read_white(fz_context *ctx, unsigned char *p, unsigned char *e, int single_line)
{
	if (e - p < 1)
		fz_throw(ctx, FZ_ERROR_GENERIC, "cannot parse whitespace in pnm image");

	if (single_line)
	{
		if (!iswhiteeol(*p) && *p != '#')
			fz_throw(ctx, FZ_ERROR_GENERIC, "expected whitespace/comment in pnm image");
		while (p < e && iswhite(*p))
			p++;

		if (p < e && *p == '#')
			while (p < e && !iseol(*p))
				p++;
		if (p < e && iseol(*p))
			p++;
	}
	else
	{
		if (!iswhiteeol(*p) && *p != '#')
			fz_throw(ctx, FZ_ERROR_GENERIC, "expected whitespace in pnm image");
		while (p < e && iswhiteeol(*p))
			p++;

		while (p < e && *p == '#')
		{
			while (p < e && !iseol(*p))
				p++;

			if (p < e && iseol(*p))
				p++;

			while (p < e && iswhiteeol(*p))
				p++;

			if (p < e && iseol(*p))
				p++;
		}

	}

	return p;
}

static unsigned char *
pnm_read_signature(fz_context *ctx, unsigned char *p, unsigned char *e, char *signature)
{
	if (e - p < 2)
		fz_throw(ctx, FZ_ERROR_GENERIC, "cannot parse magic number in pnm image");
	if (p[0] != 'P' || p[1] < '1' || p[1] > '7')
		fz_throw(ctx, FZ_ERROR_GENERIC, "expected signature in pnm image");

	signature[0] = *p++;
	signature[1] = *p++;
	return p;
}

static unsigned char *
pnm_read_number(fz_context *ctx, unsigned char *p, unsigned char *e, int *number)
{
	if (e - p < 1)
		fz_throw(ctx, FZ_ERROR_GENERIC, "cannot parse number in pnm image");
	if (*p < '0' && *p > '9')
		fz_throw(ctx, FZ_ERROR_GENERIC, "expected numeric field in pnm image");

	while (p < e && *p >= '0' && *p <= '9')
	{
		*number = *number * 10 + *p - '0';
		p++;
	}

	return p;
}

static unsigned char *
pnm_read_string(fz_context *ctx, unsigned char *p, unsigned char *e, char **out)
{
	unsigned char *s;

	if (e - p < 1)
		fz_throw(ctx, FZ_ERROR_GENERIC, "cannot parse string in pnm image");

	s = p;
	while (!iswhiteeol(*p))
		p++;

	*out = fz_malloc(ctx, p - s + 1);
	memcpy(*out, s, p - s);
	(*out)[p - s] = '\0';

	return p;
}

static int
map_color(fz_context *ctx, int color, int inmax, int outmax)
{
	float f = (float) color / inmax;
	return f * outmax;
}

static fz_pixmap *
pnm_ascii_read_image(fz_context *ctx, struct info *pnm, unsigned char *p, unsigned char *e, int onlymeta, int bitmap)
{
	fz_pixmap *img = NULL;

	p = pnm_read_number(ctx, p, e, &pnm->width);
	p = pnm_read_white(ctx, p, e, 0);

	if (bitmap)
	{
		p = pnm_read_number(ctx, p, e, &pnm->height);
		p = pnm_read_white(ctx, p, e, 1);
		pnm->maxval = 1;
	}
	else
	{
		p = pnm_read_number(ctx, p, e, &pnm->height);
		p = pnm_read_white(ctx, p, e, 0);
		p = pnm_read_number(ctx, p, e, &pnm->maxval);
		p = pnm_read_white(ctx, p, e, 0);
		if (pnm->maxval < 0 || pnm->maxval >= 65536)
			fz_throw(ctx, FZ_ERROR_GENERIC, "maximum sample value of out range in pnm image: %d", pnm->maxval);
	}

	if (!onlymeta)
	{
		unsigned char *dp;
		int x, y, k;

		img = fz_new_pixmap(ctx, pnm->cs, pnm->width, pnm->height, 0);
		dp = img->samples;

		if (bitmap)
		{
			for (y = 0; y < pnm->height; y++)
			{
				for (x = 0; x < pnm->width; x++)
				{
					int v = 0;
					p = pnm_read_number(ctx, p, e, &v);
					p = pnm_read_white(ctx, p, e, 0);
					if (v == 0)
						*dp = 0xff;
					else
						*dp = 0x00;

					dp++;
				}
			}
		}
		else
		{
			for (y = 0; y < pnm->height; y++)
				for (x = 0; x < pnm->width; x++)
					for (k = 0; k < img->colorspace->n; k++)
					{
						int v = 0;
						p = pnm_read_number(ctx, p, e, &v);
						p = pnm_read_white(ctx, p, e, 0);
						v = fz_clampi(v, 0, pnm->maxval);
						*dp++ = map_color(ctx, v, pnm->maxval, 255);
					}
		}

	}

	return img;
}

static fz_pixmap *
pnm_binary_read_image(fz_context *ctx, struct info *pnm, unsigned char *p, unsigned char *e, int onlymeta, int bitmap)
{
	fz_pixmap *img = NULL;

	p = pnm_read_number(ctx, p, e, &pnm->width);
	p = pnm_read_white(ctx, p, e, 0);

	if (bitmap)
	{
		p = pnm_read_number(ctx, p, e, &pnm->height);
		p = pnm_read_white(ctx, p, e, 1);
		pnm->maxval = 1;
	}
	else
	{
		p = pnm_read_number(ctx, p, e, &pnm->height);
		p = pnm_read_white(ctx, p, e, 0);
		p = pnm_read_number(ctx, p, e, &pnm->maxval);
		p = pnm_read_white(ctx, p, e, 1);
		if (pnm->maxval < 0 || pnm->maxval >= 65536)
			fz_throw(ctx, FZ_ERROR_GENERIC, "maximum sample value of out range in pnm image: %d", pnm->maxval);
	}

	if (!onlymeta)
	{
		unsigned char *dp;
		int x, y, k;

		img = fz_new_pixmap(ctx, pnm->cs, pnm->width, pnm->height, 0);
		dp = img->samples;

		if (bitmap)
		{
			for (y = 0; y < pnm->height; y++)
			{
				for (x = 0; x < pnm->width; x++)
				{
					if (*p & (1 << (7 - (x & 0x7))))
						*dp = 0xff;
					else
						*dp = 0x00;

					dp++;
					if ((x & 0x7) == 7)
						p++;
				}
				if (pnm->width & 0x7)
					p++;
			}
		}
		else
		{
			if (pnm->maxval == 255)
			{
				for (y = 0; y < pnm->height; y++)
					for (x = 0; x < pnm->width; x++)
						for (k = 0; k < img->colorspace->n; k++)
							*dp++ = *p++;
			}
			else if (pnm->maxval < 256)
			{
				for (y = 0; y < pnm->height; y++)
					for (x = 0; x < pnm->width; x++)
						for (k = 0; k < img->colorspace->n; k++)
							*dp++ = map_color(ctx, *p++, pnm->maxval, 255);
			}
			else
			{
				for (y = 0; y < pnm->height; y++)
					for (x = 0; x < pnm->width; x++)
						for (k = 0; k < img->colorspace->n; k++)
						{
							*dp++ = map_color(ctx, (p[0] << 8) | p[1], pnm->maxval, 255);
							p += 2;
						}
			}
		}

	}

	return img;
}

static unsigned char *
pam_binary_read_header(fz_context *ctx, struct info *pnm, unsigned char *p, unsigned char *e)
{
	char *token = fz_strdup(ctx, "");

	fz_try(ctx)
	{
		while (p < e && strcmp(token, "ENDHDR"))
		{
			fz_free(ctx, token);
			p = pnm_read_string(ctx, p, e, &token);
			p = pnm_read_white(ctx, p, e, 0);
			if (!strcmp(token, "WIDTH"))
				p = pnm_read_number(ctx, p, e, &pnm->width);
			else if (!strcmp(token, "HEIGHT"))
				p = pnm_read_number(ctx, p, e, &pnm->height);
			else if (!strcmp(token, "DEPTH"))
				p = pnm_read_number(ctx, p, e, &pnm->depth);
			else if (!strcmp(token, "MAXVAL"))
				p = pnm_read_number(ctx, p, e, &pnm->maxval);
			else if (!strcmp(token, "TUPLTYPE"))
				p = pnm_read_string(ctx, p, e, &pnm->tupletype);
			else if (strcmp(token, "ENDHDR"))
				fz_throw(ctx, FZ_ERROR_GENERIC, "unknown header token in pnm image");

			if (strcmp(token, "ENDHDR"))
				p = pnm_read_white(ctx, p, e, 0);
		}
	}
	fz_always(ctx)
	{
		fz_free(ctx, token);
	}
	fz_catch(ctx)
	{
		fz_rethrow(ctx);
	}

	return p;
}

static fz_pixmap *
pam_binary_read_image(fz_context *ctx, struct info *pnm, unsigned char *p, unsigned char *e, int onlymeta)
{
	fz_pixmap *img = NULL;
	int bitmap = 0;

	p = pam_binary_read_header(ctx, pnm, p, e);

	if (pnm->tupletype == NULL)
		switch (pnm->depth)
		{
		case 1: pnm->tupletype = fz_strdup(ctx, "BLACKANDWHITE"); break;
		case 2: pnm->tupletype = fz_strdup(ctx, "GRAYSCALE_ALPHA"); break;
		case 3: pnm->tupletype = fz_strdup(ctx, "RGB"); break;
		case 4: pnm->tupletype = fz_strdup(ctx, "CMYK"); break;
		case 5: pnm->tupletype = fz_strdup(ctx, "CMYK_ALPHA"); break;
		default:
			fz_throw(ctx, FZ_ERROR_GENERIC, "cannot guess tupletype based on depth in pnm image");
		}

	if (!strcmp(pnm->tupletype, "BLACKANDWHITE"))
	{
		if (pnm->depth != 1)
			fz_throw(ctx, FZ_ERROR_GENERIC, "depth out of range for b/w pnm image");
		if (pnm->maxval == 1)
			bitmap = 1;
		else if (pnm->maxval < 2 || pnm->maxval > 65535)
			fz_throw(ctx, FZ_ERROR_GENERIC, "maxval out of range for grayscale pnm image");

		pnm->cs = fz_device_gray(ctx);
	}
	else if (!strcmp(pnm->tupletype, "GRAYSCALE"))
	{
		if (pnm->maxval < 2 || pnm->maxval > 65535)
			fz_throw(ctx, FZ_ERROR_GENERIC, "maxval out of range for grayscale pnm image");
		if (pnm->depth != 1)
			fz_throw(ctx, FZ_ERROR_GENERIC, "depth out of range for grayscale pnm image");
		pnm->cs = fz_device_gray(ctx);
	}
	else if (!strcmp(pnm->tupletype, "GRAYSCALE_ALPHA"))
	{
		if (pnm->maxval < 2 || pnm->maxval > 65535)
			fz_throw(ctx, FZ_ERROR_GENERIC, "maxval out of range for grayscale pnm image with alpha");
		if (pnm->depth != 2)
			fz_throw(ctx, FZ_ERROR_GENERIC, "depth out of range for grayscale pnm image with alpha");
		pnm->cs = fz_device_gray(ctx);
		pnm->alpha = 1;
	}
	else if (!strcmp(pnm->tupletype, "RGB"))
	{
		if (pnm->maxval < 1 || pnm->maxval > 65535)
			fz_throw(ctx, FZ_ERROR_GENERIC, "maxval out of range for rgb pnm image");
		if (pnm->depth != 3)
			fz_throw(ctx, FZ_ERROR_GENERIC, "depth out of range for rgb pnm image");
		pnm->cs = fz_device_rgb(ctx);
	}
	else if (!strcmp(pnm->tupletype, "RGB_ALPHA"))
	{
		if (pnm->maxval < 1 || pnm->maxval > 65535)
			fz_throw(ctx, FZ_ERROR_GENERIC, "maxval out of range for rgb pnm image with alpha");
		if (pnm->depth != 4)
			fz_throw(ctx, FZ_ERROR_GENERIC, "depth out of range for rgb pnm image with alpha");
		pnm->cs = fz_device_rgb(ctx);
		pnm->alpha = 1;
	}
	else if (!strcmp(pnm->tupletype, "CMYK"))
	{
		if (pnm->maxval < 1 || pnm->maxval > 65535)
			fz_throw(ctx, FZ_ERROR_GENERIC, "maxval out of range for cmyk pnm image");
		if (pnm->depth != 4)
			fz_throw(ctx, FZ_ERROR_GENERIC, "depth out of range for cmyk pnm image");
		pnm->cs = fz_device_cmyk(ctx);
	}
	else if (!strcmp(pnm->tupletype, "CMYK_ALPHA"))
	{
		if (pnm->maxval < 1 || pnm->maxval > 65535)
			fz_throw(ctx, FZ_ERROR_GENERIC, "maxval out of range for cmyk pnm image with alpha");
		if (pnm->depth != 5)
			fz_throw(ctx, FZ_ERROR_GENERIC, "depth out of range for cmyk pnm image with alpha");
		pnm->cs = fz_device_cmyk(ctx);
		pnm->alpha = 1;
	}
	else
	{
		fz_free(ctx, pnm->tupletype);
		fz_throw(ctx, FZ_ERROR_GENERIC, "unsupported tupletype");
	}

	fz_free(ctx, pnm->tupletype);

	if (!onlymeta)
	{
		unsigned char *dp;
		int x, y, k;

		img = fz_new_pixmap(ctx, pnm->cs, pnm->width, pnm->height, pnm->alpha);
		dp = img->samples;

		if (bitmap)
		{
			if (e - p < pnm->height * pnm->width * img->n)
				fz_throw(ctx, FZ_ERROR_GENERIC, "truncated image");

			for (y = 0; y < pnm->height; y++)
				for (x = 0; x < pnm->width; x++)
					for (k = 0; k < img->colorspace->n; k++)
					{
						if (*p)
							*dp = 0x00;
						else
							*dp = 0xff;
						dp++;
						p++;
					}
		}
		else
		{
			if (pnm->maxval == 255)
			{
				if (e - p < pnm->height * pnm->width * img->n)
					fz_throw(ctx, FZ_ERROR_GENERIC, "truncated image");

				for (y = 0; y < pnm->height; y++)
					for (x = 0; x < pnm->width; x++)
						for (k = 0; k < img->n; k++)
							*dp++ = *p++;
			}
			else if (pnm->maxval < 256)
			{
				if (e - p < pnm->height * pnm->width * img->n)
					fz_throw(ctx, FZ_ERROR_GENERIC, "truncated image");

				for (y = 0; y < pnm->height; y++)
					for (x = 0; x < pnm->width; x++)
						for (k = 0; k < img->n; k++)
							*dp++ = map_color(ctx, *p++, pnm->maxval, 255);
			}
			else
			{
				if (e - p < pnm->height * pnm->width * img->n * 2)
					fz_throw(ctx, FZ_ERROR_GENERIC, "truncated image");

				for (y = 0; y < pnm->height; y++)
					for (x = 0; x < pnm->width; x++)
						for (k = 0; k < img->n; k++)
						{
							*dp++ = map_color(ctx, (p[0] << 8) | p[1], pnm->maxval, 255);
							p += 2;
						}
			}
		}
	}

	return img;
}

static fz_pixmap *
pnm_read_image(fz_context *ctx, struct info *pnm, unsigned char *p, size_t total, int onlymeta)
{
	unsigned char *e = p + total;
	char signature[3] = { 0 };

	p = pnm_read_signature(ctx, p, e, signature);
	p = pnm_read_white(ctx, p, e, 0);

	if (!strcmp(signature, "P1"))
	{
		if (!onlymeta)
			pnm->cs = fz_device_gray(ctx);
		return pnm_ascii_read_image(ctx, pnm, p, e, onlymeta, 1);
	}
	else if (!strcmp(signature, "P2"))
	{
		if (!onlymeta)
			pnm->cs = fz_device_gray(ctx);
		return pnm_ascii_read_image(ctx, pnm, p, e, onlymeta, 0);
	}
	else if (!strcmp(signature, "P3"))
	{
		if (!onlymeta)
			pnm->cs = fz_device_rgb(ctx);
		return pnm_ascii_read_image(ctx, pnm, p, e, onlymeta, 0);
	}
	else if (!strcmp(signature, "P4"))
	{
		if (!onlymeta)
			pnm->cs = fz_device_gray(ctx);
		return pnm_binary_read_image(ctx, pnm, p, e, onlymeta, 1);
	}
	else if (!strcmp(signature, "P5"))
	{
		if (!onlymeta)
			pnm->cs = fz_device_gray(ctx);
		return pnm_binary_read_image(ctx, pnm, p, e, onlymeta, 0);
	}
	else if (!strcmp(signature, "P6"))
	{
		if (!onlymeta)
			pnm->cs = fz_device_rgb(ctx);
		return pnm_binary_read_image(ctx, pnm, p, e, onlymeta, 0);
	}
	else if (!strcmp(signature, "P7"))
		return pam_binary_read_image(ctx, pnm, p, e, onlymeta);
	else
		fz_throw(ctx, FZ_ERROR_GENERIC, "unsupported portable anymap signature (0x%02x, 0x%02x)", signature[0], signature[1]);
}

fz_pixmap *
fz_load_pnm(fz_context *ctx, unsigned char *p, size_t total)
{
	fz_pixmap *img;
	struct info pnm = { 0 };

	img = pnm_read_image(ctx, &pnm, p, total, 0);

	return img;
}

void
fz_load_pnm_info(fz_context *ctx, unsigned char *p, size_t total, int *wp, int *hp, int *xresp, int *yresp, fz_colorspace **cspacep)
{
	struct info pnm = { 0 };

	pnm_read_image(ctx, &pnm, p, total, 1);

	*cspacep = pnm.cs;
	*wp = pnm.width;
	*hp = pnm.height;
	*xresp = 72;
	*yresp = 72;
}