#include "mupdf/pdf.h"

int
pdf_count_pages(pdf_document *doc)
{
	if (doc->page_count == 0)
	{
		pdf_obj *count = pdf_dict_getp(pdf_trailer(doc), "Root/Pages/Count");
		doc->page_count = pdf_to_int(count);
	}
	return doc->page_count;
}

enum
{
	LOCAL_STACK_SIZE = 16
};

static pdf_obj *
pdf_lookup_page_loc_imp(pdf_document *doc, pdf_obj *node, int *skip, pdf_obj **parentp, int *indexp)
{
	fz_context *ctx = doc->ctx;
	pdf_obj *kids;
	pdf_obj *hit = NULL;
	int i, len;
	pdf_obj *local_stack[LOCAL_STACK_SIZE];
	pdf_obj **stack = &local_stack[0];
	int stack_max = LOCAL_STACK_SIZE;
	int stack_len = 0;

	fz_var(hit);
	fz_var(stack);
	fz_var(stack_len);
	fz_var(stack_max);

	fz_try(ctx)
	{
		do
		{
			kids = pdf_dict_gets(node, "Kids");
			len = pdf_array_len(kids);

			if (len == 0)
				fz_throw(ctx, FZ_ERROR_GENERIC, "Malformed pages tree");

			/* Every node we need to unmark goes into the stack */
			if (stack_len == stack_max)
			{
				if (stack == &local_stack[0])
				{
					stack = fz_malloc_array(ctx, stack_max * 2, sizeof(*stack));
					memcpy(stack, &local_stack[0], stack_max * sizeof(*stack));
				}
				else
					stack = fz_resize_array(ctx, stack, stack_max * 2, sizeof(*stack));
				stack_max *= 2;
			}
			stack[stack_len++] = node;

			if (pdf_mark_obj(node))
				fz_throw(ctx, FZ_ERROR_GENERIC, "cycle in page tree");

			for (i = 0; i < len; i++)
			{
				pdf_obj *kid = pdf_array_get(kids, i);
				char *type = pdf_to_name(pdf_dict_gets(kid, "Type"));
				if (!strcmp(type, "Page") || (!*type && pdf_dict_gets(kid, "MediaBox")))
				{
					if (*skip == 0)
					{
						if (parentp) *parentp = node;
						if (indexp) *indexp = i;
						hit = kid;
						break;
					}
					else
					{
						(*skip)--;
					}
				}
				else if (!strcmp(type, "Pages") || (!*type && pdf_dict_gets(kid, "Kids")))
				{
					int count = pdf_to_int(pdf_dict_gets(kid, "Count"));
					if (*skip < count)
					{
						node = kid;
						break;
					}
					else
					{
						*skip -= count;
					}
				}
				else
				{
					fz_throw(ctx, FZ_ERROR_GENERIC, "non-page object in page tree (%s)", type);
				}
			}
		}
		while (hit == NULL);
	}
	fz_always(ctx)
	{
		for (i = stack_len; i > 0; i--)
			pdf_unmark_obj(stack[i-1]);
		if (stack != &local_stack[0])
			fz_free(ctx, stack);
	}
	fz_catch(ctx)
	{
		fz_rethrow(ctx);
	}

	return hit;
}

pdf_obj *
pdf_lookup_page_loc(pdf_document *doc, int needle, pdf_obj **parentp, int *indexp)
{
	pdf_obj *root = pdf_dict_gets(pdf_trailer(doc), "Root");
	pdf_obj *node = pdf_dict_gets(root, "Pages");
	int skip = needle;
	pdf_obj *hit;

	if (!node)
		fz_throw(doc->ctx, FZ_ERROR_GENERIC, "cannot find page tree");

	hit = pdf_lookup_page_loc_imp(doc, node, &skip, parentp, indexp);
	if (!hit)
		fz_throw(doc->ctx, FZ_ERROR_GENERIC, "cannot find page %d in page tree", needle);
	return hit;
}

pdf_obj *
pdf_lookup_page_obj(pdf_document *doc, int needle)
{
	return pdf_lookup_page_loc(doc, needle, NULL, NULL);
}

static int
pdf_count_pages_before_kid(pdf_document *doc, pdf_obj *parent, int kid_num)
{
	pdf_obj *kids = pdf_dict_gets(parent, "Kids");
	int i, total = 0, len = pdf_array_len(kids);
	for (i = 0; i < len; i++)
	{
		pdf_obj *kid = pdf_array_get(kids, i);
		if (pdf_to_num(kid) == kid_num)
			return total;
		if (!strcmp(pdf_to_name(pdf_dict_gets(kid, "Type")), "Pages"))
		{
			pdf_obj *count = pdf_dict_gets(kid, "Count");
			int n = pdf_to_int(count);
			if (count == NULL || n <= 0)
				fz_throw(doc->ctx, FZ_ERROR_GENERIC, "illegal or missing count in pages tree");
			total += n;
		}
		else
			total++;
	}
	fz_throw(doc->ctx, FZ_ERROR_GENERIC, "kid not found in parent's kids array");
}

int
pdf_lookup_page_number(pdf_document *doc, pdf_obj *node)
{
	fz_context *ctx = doc->ctx;
	int needle = pdf_to_num(node);
	int total = 0;
	pdf_obj *parent, *parent2;

	if (strcmp(pdf_to_name(pdf_dict_gets(node, "Type")), "Page") != 0)
		fz_throw(ctx, FZ_ERROR_GENERIC, "invalid page object");

	parent2 = parent = pdf_dict_gets(node, "Parent");
	fz_var(parent);
	fz_try(ctx)
	{
		while (pdf_is_dict(parent))
		{
			if (pdf_mark_obj(parent))
				fz_throw(ctx, FZ_ERROR_GENERIC, "cycle in page tree (parents)");
			total += pdf_count_pages_before_kid(doc, parent, needle);
			needle = pdf_to_num(parent);
			parent = pdf_dict_gets(parent, "Parent");
		}
	}
	fz_always(ctx)
	{
		/* Run back and unmark */
		while (parent2)
		{
			pdf_unmark_obj(parent2);
			if (parent2 == parent)
				break;
			parent2 = pdf_dict_gets(parent2, "Parent");
		}
	}
	fz_catch(ctx)
	{
		fz_rethrow(ctx);
	}

	return total;
}

static pdf_obj *
pdf_lookup_inherited_page_item(pdf_document *doc, pdf_obj *node, const char *key)
{
	fz_context *ctx = doc->ctx;
	pdf_obj *node2 = node;
	pdf_obj *val;

	/* fz_var(node); Not required as node passed in */

	fz_try(ctx)
	{
		do
		{
			val = pdf_dict_gets(node, key);
			if (val)
				break;
			if (pdf_mark_obj(node))
				fz_throw(ctx, FZ_ERROR_GENERIC, "cycle in page tree (parents)");
			node = pdf_dict_gets(node, "Parent");
		}
		while (node);
	}
	fz_always(ctx)
	{
		do
		{
			pdf_unmark_obj(node2);
			if (node2 == node)
				break;
			node2 = pdf_dict_gets(node2, "Parent");
		}
		while (node2);
	}
	fz_catch(ctx)
	{
		fz_rethrow(ctx);
	}

	return val;
}

/* We need to know whether to install a page-level transparency group */

static int pdf_resources_use_blending(pdf_document *doc, pdf_obj *rdb);

static int
pdf_extgstate_uses_blending(pdf_document *doc, pdf_obj *dict)
{
	pdf_obj *obj = pdf_dict_gets(dict, "BM");
	if (pdf_is_name(obj) && strcmp(pdf_to_name(obj), "Normal"))
		return 1;
	return 0;
}

static int
pdf_pattern_uses_blending(pdf_document *doc, pdf_obj *dict)
{
	pdf_obj *obj;
	obj = pdf_dict_gets(dict, "Resources");
	if (pdf_resources_use_blending(doc, obj))
		return 1;
	obj = pdf_dict_gets(dict, "ExtGState");
	return pdf_extgstate_uses_blending(doc, obj);
}

static int
pdf_xobject_uses_blending(pdf_document *doc, pdf_obj *dict)
{
	pdf_obj *obj = pdf_dict_gets(dict, "Resources");
	return pdf_resources_use_blending(doc, obj);
}

static int
pdf_resources_use_blending(pdf_document *doc, pdf_obj *rdb)
{
	fz_context *ctx = doc->ctx;
	pdf_obj *obj;
	int i, n, useBM = 0;

	if (!rdb)
		return 0;

	/* Have we been here before and remembered an answer? */
	if (pdf_obj_memo(rdb, &useBM))
		return useBM;

	/* stop on cyclic resource dependencies */
	if (pdf_mark_obj(rdb))
		return 0;

	fz_try(ctx)
	{
		obj = pdf_dict_gets(rdb, "ExtGState");
		n = pdf_dict_len(obj);
		for (i = 0; i < n; i++)
			if (pdf_extgstate_uses_blending(doc, pdf_dict_get_val(obj, i)))
				goto found;

		obj = pdf_dict_gets(rdb, "Pattern");
		n = pdf_dict_len(obj);
		for (i = 0; i < n; i++)
			if (pdf_pattern_uses_blending(doc, pdf_dict_get_val(obj, i)))
				goto found;

		obj = pdf_dict_gets(rdb, "XObject");
		n = pdf_dict_len(obj);
		for (i = 0; i < n; i++)
			if (pdf_xobject_uses_blending(doc, pdf_dict_get_val(obj, i)))
				goto found;
		if (0)
		{
found:
			useBM = 1;
		}
	}
	fz_always(ctx)
	{
		pdf_unmark_obj(rdb);
	}
	fz_catch(ctx)
	{
		fz_rethrow(ctx);
	}

	pdf_set_obj_memo(rdb, useBM);
	return useBM;
}

static void
pdf_load_transition(pdf_document *doc, pdf_page *page, pdf_obj *transdict)
{
	char *name;
	pdf_obj *obj;
	int type;

	obj = pdf_dict_gets(transdict, "D");
	page->transition.duration = (obj ? pdf_to_real(obj) : 1);

	page->transition.vertical = (pdf_to_name(pdf_dict_gets(transdict, "Dm"))[0] != 'H');
	page->transition.outwards = (pdf_to_name(pdf_dict_gets(transdict, "M"))[0] != 'I');
	/* FIXME: If 'Di' is None, it should be handled differently, but
	 * this only affects Fly, and we don't implement that currently. */
	page->transition.direction = (pdf_to_int(pdf_dict_gets(transdict, "Di")));
	/* FIXME: Read SS for Fly when we implement it */
	/* FIXME: Read B for Fly when we implement it */

	name = pdf_to_name(pdf_dict_gets(transdict, "S"));
	if (!strcmp(name, "Split"))
		type = FZ_TRANSITION_SPLIT;
	else if (!strcmp(name, "Blinds"))
		type = FZ_TRANSITION_BLINDS;
	else if (!strcmp(name, "Box"))
		type = FZ_TRANSITION_BOX;
	else if (!strcmp(name, "Wipe"))
		type = FZ_TRANSITION_WIPE;
	else if (!strcmp(name, "Dissolve"))
		type = FZ_TRANSITION_DISSOLVE;
	else if (!strcmp(name, "Glitter"))
		type = FZ_TRANSITION_GLITTER;
	else if (!strcmp(name, "Fly"))
		type = FZ_TRANSITION_FLY;
	else if (!strcmp(name, "Push"))
		type = FZ_TRANSITION_PUSH;
	else if (!strcmp(name, "Cover"))
		type = FZ_TRANSITION_COVER;
	else if (!strcmp(name, "Uncover"))
		type = FZ_TRANSITION_UNCOVER;
	else if (!strcmp(name, "Fade"))
		type = FZ_TRANSITION_FADE;
	else
		type = FZ_TRANSITION_NONE;
	page->transition.type = type;
}

pdf_page *
pdf_load_page(pdf_document *doc, int number)
{
	fz_context *ctx = doc->ctx;
	pdf_page *page;
	pdf_annot *annot;
	pdf_obj *pageobj, *pageref, *obj;
	fz_rect mediabox, cropbox, realbox;
	float userunit;
	fz_matrix mat;

	if (doc->file_reading_linearly)
	{
		pageref = pdf_progressive_advance(doc, number);
		if (pageref == NULL)
			fz_throw(doc->ctx, FZ_ERROR_TRYLATER, "page %d not available yet", number);
	}
	else
		pageref = pdf_lookup_page_obj(doc, number);
	pageobj = pdf_resolve_indirect(pageref);

	page = fz_malloc_struct(ctx, pdf_page);
	page->resources = NULL;
	page->contents = NULL;
	page->transparency = 0;
	page->links = NULL;
	page->annots = NULL;
	page->annot_tailp = &page->annots;
	page->deleted_annots = NULL;
	page->tmp_annots = NULL;
	page->me = pdf_keep_obj(pageobj);
	page->incomplete = 0;

	obj = pdf_dict_gets(pageobj, "UserUnit");
	if (pdf_is_real(obj))
		userunit = pdf_to_real(obj);
	else
		userunit = 1;

	pdf_to_rect(ctx, pdf_lookup_inherited_page_item(doc, pageobj, "MediaBox"), &mediabox);
	if (fz_is_empty_rect(&mediabox))
	{
		fz_warn(ctx, "cannot find page size for page %d", number + 1);
		mediabox.x0 = 0;
		mediabox.y0 = 0;
		mediabox.x1 = 612;
		mediabox.y1 = 792;
	}

	pdf_to_rect(ctx, pdf_lookup_inherited_page_item(doc, pageobj, "CropBox"), &cropbox);
	if (!fz_is_empty_rect(&cropbox))
		fz_intersect_rect(&mediabox, &cropbox);

	page->mediabox.x0 = fz_min(mediabox.x0, mediabox.x1) * userunit;
	page->mediabox.y0 = fz_min(mediabox.y0, mediabox.y1) * userunit;
	page->mediabox.x1 = fz_max(mediabox.x0, mediabox.x1) * userunit;
	page->mediabox.y1 = fz_max(mediabox.y0, mediabox.y1) * userunit;

	if (page->mediabox.x1 - page->mediabox.x0 < 1 || page->mediabox.y1 - page->mediabox.y0 < 1)
	{
		fz_warn(ctx, "invalid page size in page %d", number + 1);
		page->mediabox = fz_unit_rect;
	}

	page->rotate = pdf_to_int(pdf_lookup_inherited_page_item(doc, pageobj, "Rotate"));
	/* Snap page->rotate to 0, 90, 180 or 270 */
	if (page->rotate < 0)
		page->rotate = 360 - ((-page->rotate) % 360);
	if (page->rotate >= 360)
		page->rotate = page->rotate % 360;
	page->rotate = 90*((page->rotate + 45)/90);
	if (page->rotate > 360)
		page->rotate = 0;

	fz_pre_rotate(fz_scale(&page->ctm, 1, -1), -page->rotate);
	realbox = page->mediabox;
	fz_transform_rect(&realbox, &page->ctm);
	fz_pre_scale(fz_translate(&mat, -realbox.x0, -realbox.y0), userunit, userunit);
	fz_concat(&page->ctm, &page->ctm, &mat);

	fz_try(ctx)
	{
		obj = pdf_dict_gets(pageobj, "Annots");
		if (obj)
		{
			page->links = pdf_load_link_annots(doc, obj, &page->ctm);
			pdf_load_annots(doc, page, obj);
		}
	}
	fz_catch(ctx)
	{
		if (fz_caught(ctx) != FZ_ERROR_TRYLATER)
			fz_rethrow(ctx);
		page->incomplete |= PDF_PAGE_INCOMPLETE_ANNOTS;
		fz_drop_link(ctx, page->links);
		page->links = NULL;
	}

	page->duration = pdf_to_real(pdf_dict_gets(pageobj, "Dur"));

	obj = pdf_dict_gets(pageobj, "Trans");
	page->transition_present = (obj != NULL);
	if (obj)
	{
		pdf_load_transition(doc, page, obj);
	}

	// TODO: inherit
	page->resources = pdf_lookup_inherited_page_item(doc, pageobj, "Resources");
	if (page->resources)
		pdf_keep_obj(page->resources);

	obj = pdf_dict_gets(pageobj, "Contents");
	fz_try(ctx)
	{
		page->contents = pdf_keep_obj(obj);

		if (pdf_resources_use_blending(doc, page->resources))
			page->transparency = 1;

		for (annot = page->annots; annot && !page->transparency; annot = annot->next)
			if (annot->ap && pdf_resources_use_blending(doc, annot->ap->resources))
				page->transparency = 1;
	}
	fz_catch(ctx)
	{
		if (fz_caught(ctx) != FZ_ERROR_TRYLATER)
		{
			pdf_free_page(doc, page);
			fz_rethrow_message(ctx, "cannot load page %d contents (%d 0 R)", number + 1, pdf_to_num(pageref));
		}
		page->incomplete |= PDF_PAGE_INCOMPLETE_CONTENTS;
	}

	return page;
}

fz_rect *
pdf_bound_page(pdf_document *doc, pdf_page *page, fz_rect *bounds)
{
	fz_matrix mtx;
	fz_rect mediabox = page->mediabox;
	fz_transform_rect(&mediabox, fz_rotate(&mtx, page->rotate));
	bounds->x0 = bounds->y0 = 0;
	bounds->x1 = mediabox.x1 - mediabox.x0;
	bounds->y1 = mediabox.y1 - mediabox.y0;
	return bounds;
}

fz_link *
pdf_load_links(pdf_document *doc, pdf_page *page)
{
	return fz_keep_link(doc->ctx, page->links);
}

void
pdf_free_page(pdf_document *doc, pdf_page *page)
{
	if (page == NULL)
		return;
	pdf_drop_obj(page->resources);
	pdf_drop_obj(page->contents);
	if (page->links)
		fz_drop_link(doc->ctx, page->links);
	if (page->annots)
		pdf_free_annot(doc->ctx, page->annots);
	if (page->deleted_annots)
		pdf_free_annot(doc->ctx, page->deleted_annots);
	if (page->tmp_annots)
		pdf_free_annot(doc->ctx, page->tmp_annots);
	/* doc->focus, when not NULL, refers to one of
	 * the annotations and must be NULLed when the
	 * annotations are destroyed. doc->focus_obj
	 * keeps track of the actual annotation object. */
	doc->focus = NULL;
	pdf_drop_obj(page->me);
	fz_free(doc->ctx, page);
}

void
pdf_delete_page(pdf_document *doc, int at)
{
	pdf_obj *parent, *kids;
	int i;

	pdf_lookup_page_loc(doc, at, &parent, &i);
	kids = pdf_dict_gets(parent, "Kids");
	pdf_array_delete(kids, i);

	while (parent)
	{
		int count = pdf_to_int(pdf_dict_gets(parent, "Count"));
		pdf_dict_puts_drop(parent, "Count", pdf_new_int(doc, count - 1));
		parent = pdf_dict_gets(parent, "Parent");
	}
}

void
pdf_insert_page(pdf_document *doc, pdf_page *page, int at)
{
	fz_context *ctx = doc->ctx;
	int count = pdf_count_pages(doc);
	pdf_obj *parent, *kids;
	pdf_obj *page_ref;
	int i;

	page_ref = pdf_new_ref(doc, page->me);

	fz_try(ctx)
	{
		if (count == 0)
		{
			/* TODO: create new page tree? */
			fz_throw(ctx, FZ_ERROR_GENERIC, "empty page tree, cannot insert page");
		}
		else if (at >= count)
		{
			if (at > count)
				fz_throw(ctx, FZ_ERROR_GENERIC, "cannot insert page beyond end of page tree");

			/* append after last page */
			pdf_lookup_page_loc(doc, count - 1, &parent, &i);
			kids = pdf_dict_gets(parent, "Kids");
			pdf_array_insert(kids, page_ref, i + 1);
		}
		else
		{
			/* insert before found page */
			pdf_lookup_page_loc(doc, at, &parent, &i);
			kids = pdf_dict_gets(parent, "Kids");
			pdf_array_insert(kids, page_ref, i);
		}

		pdf_dict_puts(page->me, "Parent", parent);

		/* Adjust page counts */
		while (parent)
		{
			int count = pdf_to_int(pdf_dict_gets(parent, "Count"));
			pdf_dict_puts_drop(parent, "Count", pdf_new_int(doc, count + 1));
			parent = pdf_dict_gets(parent, "Parent");
		}

	}
	fz_always(ctx)
	{
		pdf_drop_obj(page_ref);
	}
	fz_catch(ctx)
	{
		fz_rethrow(ctx);
	}
}

void
pdf_delete_page_range(pdf_document *doc, int start, int end)
{
	while (start < end)
		pdf_delete_page(doc, start++);
}

pdf_page *
pdf_create_page(pdf_document *doc, fz_rect mediabox, int res, int rotate)
{
	pdf_page *page = NULL;
	pdf_obj *pageobj;
	float userunit = 1;
	fz_context *ctx = doc->ctx;
	fz_matrix ctm, tmp;
	fz_rect realbox;

	page = fz_malloc_struct(ctx, pdf_page);

	fz_try(ctx)
	{
		page->resources = NULL;
		page->contents = NULL;
		page->transparency = 0;
		page->links = NULL;
		page->annots = NULL;
		page->me = pageobj = pdf_new_dict(doc, 4);

		pdf_dict_puts_drop(pageobj, "Type", pdf_new_name(doc, "Page"));

		page->mediabox.x0 = fz_min(mediabox.x0, mediabox.x1) * userunit;
		page->mediabox.y0 = fz_min(mediabox.y0, mediabox.y1) * userunit;
		page->mediabox.x1 = fz_max(mediabox.x0, mediabox.x1) * userunit;
		page->mediabox.y1 = fz_max(mediabox.y0, mediabox.y1) * userunit;
		pdf_dict_puts_drop(pageobj, "MediaBox", pdf_new_rect(doc, &page->mediabox));

		/* Snap page->rotate to 0, 90, 180 or 270 */
		if (page->rotate < 0)
			page->rotate = 360 - ((-page->rotate) % 360);
		if (page->rotate >= 360)
			page->rotate = page->rotate % 360;
		page->rotate = 90*((page->rotate + 45)/90);
		if (page->rotate > 360)
			page->rotate = 0;
		pdf_dict_puts_drop(pageobj, "Rotate", pdf_new_int(doc, page->rotate));

		fz_pre_rotate(fz_scale(&ctm, 1, -1), -page->rotate);
		realbox = page->mediabox;
		fz_transform_rect(&realbox, &ctm);
		fz_pre_scale(fz_translate(&tmp, -realbox.x0, -realbox.y0), userunit, userunit);
		fz_concat(&ctm, &ctm, &tmp);
		page->ctm = ctm;
		/* Do not create a Contents, as an empty Contents dict is not
		 * valid. See Bug 694712 */
	}
	fz_catch(ctx)
	{
		pdf_drop_obj(page->me);
		fz_free(ctx, page);
		fz_rethrow_message(ctx, "Failed to create page");
	}

	return page;
}