#include "common.h"

#import "MuPageViewNormal.h"
#import "MuPageViewReflow.h"
#import "MuDocumentController.h"
#import "MuTextFieldController.h"
#import "MuChoiceFieldController.h"
#import "MuPrintPageRenderer.h"

#define GAP 20
#define INDICATOR_Y -44-24
#define SLIDER_W (width - GAP - 24)
#define SEARCH_W (width - GAP - 170)
#define MIN_SCALE (1.0)
#define MAX_SCALE (5.0)

static NSString *const AlertTitle = @"Save Document?";
// Correct functioning of the app relies on CloseAlertMessage and ShareAlertMessage differing
static NSString *const CloseAlertMessage = @"Changes have been made to the document that will be lost if not saved";
static NSString *const ShareAlertMessage = @"Your changes will not be shared unless the document is first saved";

static void flattenOutline(NSMutableArray *titles, NSMutableArray *pages, fz_outline *outline, int level)
{
	char indent[8*4+1];
	if (level > 8)
		level = 8;
	memset(indent, ' ', level * 4);
	indent[level * 4] = 0;
	while (outline)
	{
		int page = outline->page;
		if (page >= 0 && outline->title)
		{
			NSString *title = @(outline->title);
			[titles addObject: [NSString stringWithFormat: @"%s%@", indent, title]];
			[pages addObject: @(page)];
		}
		flattenOutline(titles, pages, outline->down, level + 1);
		outline = outline->next;
	}
}

static char *tmp_path(const char *path)
{
	int f;
	char *buf = malloc(strlen(path) + 6 + 1);
	if (!buf)
		return NULL;

	strcpy(buf, path);
	strcat(buf, "XXXXXX");

	f = mkstemp(buf);

	if (f >= 0)
	{
		close(f);
		return buf;
	}
	else
	{
		free(buf);
		return NULL;
	}
}

static void saveDoc(const char *current_path, fz_document *doc)
{
	char *tmp;
	pdf_document *idoc = pdf_specifics(ctx, doc);
	pdf_write_options opts = { 0 };

	opts.do_incremental = 1;

	if (!idoc)
		return;

	tmp = tmp_path(current_path);
	if (tmp)
	{
		int written = 0;

		fz_var(written);
		fz_try(ctx)
		{
			FILE *fin = fopen(current_path, "rb");
			FILE *fout = fopen(tmp, "wb");
			char buf[256];
			size_t n;
			int err = 1;

			if (fin && fout)
			{
				while ((n = fread(buf, 1, sizeof(buf), fin)) > 0)
					fwrite(buf, 1, n, fout);
				err = (ferror(fin) || ferror(fout));
			}

			if (fin)
				fclose(fin);
			if (fout)
				fclose(fout);

			if (!err)
			{
				pdf_save_document(ctx, idoc, tmp, &opts);
				written = 1;
			}
		}
		fz_catch(ctx)
		{
			written = 0;
		}

		if (written)
		{
			rename(tmp, current_path);
		}

		free(tmp);
	}
}

@implementation MuDocumentController
{
	fz_document *doc;
	MuDocRef *docRef;
	NSString *key;
	NSString *_filePath;
	BOOL reflowMode;
	MuOutlineController *outline;
	UIScrollView *canvas;
	UILabel *indicator;
	UISlider *slider;
	UISearchBar *searchBar;
	UIBarButtonItem *nextButton, *prevButton, *cancelButton, *searchButton, *outlineButton, *linkButton;
	UIBarButtonItem *moreButton;
	UIBarButtonItem *shareButton, *printButton, *annotButton;
	UIBarButtonItem *highlightButton, *underlineButton, *strikeoutButton;
	UIBarButtonItem *inkButton;
	UIBarButtonItem *tickButton;
	UIBarButtonItem *deleteButton;
	UIBarButtonItem *reflowButton;
	UIBarButtonItem *backButton;
	UIBarButtonItem *sliderWrapper;
	int barmode;
	int searchPage;
	int cancelSearch;
	int showLinks;
	int width; // current screen size
	int height;
	int current; // currently visible page
	int scroll_animating; // stop view updates during scrolling animations
	float scale; // scale applied to views (only used in reflow mode)
	BOOL _isRotating;
}

- (instancetype) initWithFilename: (NSString*)filename path:(NSString *)cstr document: (MuDocRef *)aDoc
{
	self = [super initWithNibName:nil bundle:nil];
	if (!self)
		return nil;

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
	if ([self respondsToSelector:@selector(automaticallyAdjustsScrollViewInsets)])
		self.automaticallyAdjustsScrollViewInsets = NO;
#endif
	key = [filename copy];
	docRef = [aDoc retain];
	doc = docRef->doc;
	_filePath = [cstr copy];

	//  this will be created right before the outline is shown
	outline = nil;

	dispatch_sync(queue, ^{});

	return self;
}

- (UIBarButtonItem *) newResourceBasedButton:(NSString *)resource withAction:(SEL)selector
{
	if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
	{
		return [[UIBarButtonItem alloc] initWithImage:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:resource ofType:@"png"]] style:UIBarButtonItemStylePlain target:self action:selector];
	}
	else
	{
		UIView *buttonView;
		BOOL iOS7Style = (([UIDevice currentDevice].systemVersion).floatValue >= 7.0f);
		UIButton *button = [UIButton buttonWithType:iOS7Style ? UIButtonTypeSystem : UIButtonTypeCustom];
		[button setImage:[UIImage imageNamed:resource] forState:UIControlStateNormal];
		[button addTarget:self action:selector forControlEvents:UIControlEventTouchUpInside];
		[button sizeToFit];
		buttonView = button;

		return [[UIBarButtonItem alloc] initWithCustomView:buttonView];
	}
}

- (void) addMainMenuButtons
{
	NSMutableArray *array = [NSMutableArray arrayWithCapacity:3];
	[array addObject:moreButton];
	[array addObject:searchButton];
	if (outlineButton)
		[array addObject:outlineButton];
	[array addObject:reflowButton];
	[array addObject:linkButton];
	self.navigationItem.rightBarButtonItems = array ;
	self.navigationItem.leftBarButtonItem = backButton;
}

- (void) loadView
{
	[[NSUserDefaults standardUserDefaults] setObject: key forKey: @"OpenDocumentKey"];

	current = (int)[[NSUserDefaults standardUserDefaults] integerForKey: key];
	if (current < 0 || current >= fz_count_pages(ctx, doc))
		current = 0;

	UIView *view = [[UIView alloc] initWithFrame: CGRectZero];
	view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
	[view setAutoresizesSubviews: YES];
	view.backgroundColor = [UIColor grayColor];

	canvas = [[UIScrollView alloc] initWithFrame: CGRectMake(0,0,GAP,0)];
	canvas.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
	[canvas setPagingEnabled: YES];
	[canvas setShowsHorizontalScrollIndicator: NO];
	[canvas setShowsVerticalScrollIndicator: NO];
	canvas.delegate = self;

	UITapGestureRecognizer *tapRecog = [[UITapGestureRecognizer alloc] initWithTarget: self action: @selector(onTap:)];
	tapRecog.delegate = self;
	[canvas addGestureRecognizer: tapRecog];
	[tapRecog release];
	// In reflow mode, we need to track pinch gestures on the canvas and pass
	// the scale changes to the subviews.
	UIPinchGestureRecognizer *pinchRecog = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(onPinch:)];
	pinchRecog.delegate = self;
	[canvas addGestureRecognizer:pinchRecog];
	[pinchRecog release];

	scale = 1.0;

	scroll_animating = NO;

	indicator = [[UILabel alloc] initWithFrame: CGRectZero];
	indicator.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin;
	indicator.text = @"0000 of 9999";
	[indicator sizeToFit];
	indicator.center = CGPointMake(0, INDICATOR_Y);
	indicator.textAlignment = NSTextAlignmentCenter;
	indicator.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent: 0.5];
	indicator.textColor = [UIColor whiteColor];

	[view addSubview: canvas];
	[view addSubview: indicator];

	slider = [[UISlider alloc] initWithFrame: CGRectZero];
	slider.minimumValue = 0;
	slider.maximumValue = fz_count_pages(ctx, doc) - 1;
	[slider addTarget: self action: @selector(onSlide:) forControlEvents: UIControlEventValueChanged];

	if ([UIDevice currentDevice].systemVersion.floatValue < 7.0)
	{
		sliderWrapper = [[UIBarButtonItem alloc] initWithCustomView: slider];

		self.toolbarItems = @[sliderWrapper];
	}

	// Set up the buttons on the navigation and search bar

	fz_outline *outlineRoot;
	fz_try(ctx)
		outlineRoot = fz_load_outline(ctx, doc);
	fz_catch(ctx)
		outlineRoot = NULL;
	if (outlineRoot)
	{
		//  only show the outline button if there is an outline
		outlineButton = [self newResourceBasedButton:@"ic_list" withAction:@selector(onShowOutline:)];
		fz_drop_outline(ctx, outlineRoot);
	}

	linkButton = [self newResourceBasedButton:@"ic_link" withAction:@selector(onToggleLinks:)];
	cancelButton = [self newResourceBasedButton:@"ic_cancel" withAction:@selector(onCancel:)];
	searchButton = [self newResourceBasedButton:@"ic_magnifying_glass" withAction:@selector(onShowSearch:)];
	prevButton = [self newResourceBasedButton:@"ic_arrow_left" withAction:@selector(onSearchPrev:)];
	nextButton = [self newResourceBasedButton:@"ic_arrow_right" withAction:@selector(onSearchNext:)];
	reflowButton = [self newResourceBasedButton:@"ic_reflow" withAction:@selector(onToggleReflow:)];
	moreButton = [self newResourceBasedButton:@"ic_more" withAction:@selector(onMore:)];
	annotButton = [self newResourceBasedButton:@"ic_annotation" withAction:@selector(onAnnot:)];
	shareButton = [self newResourceBasedButton:@"ic_share" withAction:@selector(onShare:)];
	printButton = [self newResourceBasedButton:@"ic_print" withAction:@selector(onPrint:)];
	highlightButton = [self newResourceBasedButton:@"ic_highlight" withAction:@selector(onHighlight:)];
	underlineButton = [self newResourceBasedButton:@"ic_underline" withAction:@selector(onUnderline:)];
	strikeoutButton = [self newResourceBasedButton:@"ic_strike" withAction:@selector(onStrikeout:)];
	inkButton = [self newResourceBasedButton:@"ic_pen" withAction:@selector(onInk:)];
	tickButton = [self newResourceBasedButton:@"ic_check" withAction:@selector(onTick:)];
	deleteButton = [self newResourceBasedButton:@"ic_trash" withAction:@selector(onDelete:)];
	searchBar = [[UISearchBar alloc] initWithFrame: CGRectMake(0,0,50,32)];
	backButton = [self newResourceBasedButton:@"ic_arrow_left" withAction:@selector(onBack:)];
	searchBar.placeholder = @"Search";
	searchBar.delegate = self;

	[prevButton setEnabled: NO];
	[nextButton setEnabled: NO];

	[self addMainMenuButtons];

	// TODO: add activityindicator to search bar

	self.view = view;
	[view release];
}

- (void) dealloc
{
	[docRef release]; docRef = nil; doc = NULL;
	[indicator release]; indicator = nil;
	[slider release]; slider = nil;
	[sliderWrapper release]; sliderWrapper = nil;
	[reflowButton release]; reflowButton = nil;
	[backButton release]; backButton = nil;
	[moreButton release]; moreButton = nil;
	[searchBar release]; searchBar = nil;
	[outlineButton release]; outlineButton = nil;
	[linkButton release]; linkButton = nil;
	[searchButton release]; searchButton = nil;
	[cancelButton release]; cancelButton = nil;
	[prevButton release]; prevButton = nil;
	[nextButton release]; nextButton = nil;
	[shareButton release]; shareButton = nil;
	[printButton release]; printButton = nil;
	[annotButton release]; annotButton = nil;
	[highlightButton release]; highlightButton = nil;
	[underlineButton release]; underlineButton = nil;
	[strikeoutButton release]; strikeoutButton = nil;
	[inkButton release]; inkButton = nil;
	[tickButton release]; tickButton = nil;
	[deleteButton release]; deleteButton = nil;
	[canvas release]; canvas = nil;
	[_filePath release]; _filePath = NULL;

	[outline release];
	[key release];
	[super dealloc];
}

- (void) viewWillAppear: (BOOL)animated
{
	[super viewWillAppear:animated];
	self.title = key.lastPathComponent;

	slider.value = current;

	if ([UIDevice currentDevice].systemVersion.floatValue >= 7.0)
		[self.navigationController.toolbar addSubview:slider];

	indicator.text = [NSString stringWithFormat: @" %d of %d ", current+1, fz_count_pages(ctx, doc)];

	[self.navigationController setToolbarHidden: NO animated: animated];
}

- (void) viewWillLayoutSubviews
{
	CGSize size = canvas.frame.size;
	int max_width = fz_max(width, size.width);

	width = size.width;
	height = size.height;

	canvas.contentInset = UIEdgeInsetsZero;
	canvas.contentSize = CGSizeMake(fz_count_pages(ctx, doc) * width, height);
	canvas.contentOffset = CGPointMake(current * width, 0);

	[sliderWrapper setWidth: SLIDER_W];
	searchBar.frame = CGRectMake(0,0,SEARCH_W,32);
	if ([UIDevice currentDevice].systemVersion.floatValue >= 7.0)
	{
		CGRect r = self.navigationController.toolbar.frame;
		r.origin.x = 0;
		r.origin.y = 0;
		slider.frame = r;
	}

	[self.navigationController.toolbar setNeedsLayout]; // force layout!

	// use max_width so we don't clamp the content offset too early during animation
	canvas.contentSize = CGSizeMake(fz_count_pages(ctx, doc) * max_width, height);
	canvas.contentOffset = CGPointMake(current * width, 0);

	for (UIView<MuPageView> *view in canvas.subviews) {
		if (view.number == current) {
			view.frame = CGRectMake(view.number * width, 0, width-GAP, height);
			[view willRotate];
		}
	}
	for (UIView<MuPageView> *view in canvas.subviews) {
		if (view.number != current) {
			view.frame = CGRectMake(view.number * width, 0, width-GAP, height);
			[view willRotate];
		}
	}
}

- (void) viewDidAppear: (BOOL)animated
{
	[super viewDidAppear:animated];
	[self scrollViewDidScroll: canvas];
}

- (void) viewWillDisappear: (BOOL)animated
{
	[super viewWillDisappear:animated];
	if ([UIDevice currentDevice].systemVersion.floatValue >= 7.0)
		[slider removeFromSuperview];

	self.title = @"Resume";
	[[NSUserDefaults standardUserDefaults] removeObjectForKey: @"OpenDocumentKey"];
	[self.navigationController setToolbarHidden: YES animated: animated];
}

- (void) showNavigationBar
{
	if (self.navigationController.navigationBarHidden) {
		[sliderWrapper setWidth: SLIDER_W];
		if ([UIDevice currentDevice].systemVersion.floatValue >= 7.0)
		{
			CGRect r = self.navigationController.toolbar.frame;
			r.origin.x = 0;
			r.origin.y = 0;
			slider.frame = r;
		}
		[self.navigationController setNavigationBarHidden: NO];
		[self.navigationController setToolbarHidden: NO];
		[indicator setHidden: NO];

		[UIView beginAnimations: @"MuNavBar" context: NULL];

		self.navigationController.navigationBar.alpha = 1;
		self.navigationController.toolbar.alpha = 1;
		indicator.alpha = 1;

		[UIView commitAnimations];
	}
}

- (void) hideNavigationBar
{
	if (!self.navigationController.navigationBarHidden) {
		[searchBar resignFirstResponder];

		[UIView beginAnimations: @"MuNavBar" context: NULL];
		[UIView setAnimationDelegate: self];
		[UIView setAnimationDidStopSelector: @selector(onHideNavigationBarFinished)];

		self.navigationController.navigationBar.alpha = 0;
		self.navigationController.toolbar.alpha = 0;
		indicator.alpha = 0;

		[UIView commitAnimations];
	}
}

- (void) onHideNavigationBarFinished
{
	[self.navigationController setNavigationBarHidden: YES];
	[self.navigationController setToolbarHidden: YES];
	[indicator setHidden: YES];
}

- (void) onShowOutline: (id)sender
{
	//  rebuild the outline in case the layout has changed
	fz_outline *root;
	fz_try(ctx)
		root = fz_load_outline(ctx, doc);
	fz_catch(ctx)
		root = NULL;
	if (root)
	{
		NSMutableArray *titles = [[NSMutableArray alloc] init];
		NSMutableArray *pages = [[NSMutableArray alloc] init];
		flattenOutline(titles, pages, root, 0);
		[outline release];
		if (titles.count)
			outline = [[MuOutlineController alloc] initWithTarget: self titles: titles pages: pages];
		[titles release];
		[pages release];
		fz_drop_outline(ctx, root);
	}

	//  now show it

	[self.navigationController pushViewController: outline animated: YES];
}

- (void) onToggleLinks: (id)sender
{
	showLinks = !showLinks;
	for (UIView<MuPageView> *view in canvas.subviews)
	{
		if (showLinks)
			[view showLinks];
		else
			[view hideLinks];
	}
}

- (void) onToggleReflow: (id)sender
{
	reflowMode = !reflowMode;

	annotButton.enabled = !reflowMode;
	searchButton.enabled = !reflowMode;
	linkButton.enabled = !reflowMode;
	moreButton.enabled = !reflowMode;

	[canvas.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
	[self scrollViewDidScroll:canvas];
}

- (void) showMoreMenu
{
	NSMutableArray *rightbuttons = [NSMutableArray arrayWithObjects:printButton, shareButton, nil];
	if (docRef->interactive)
		[rightbuttons insertObject:annotButton atIndex:0];
	self.navigationItem.rightBarButtonItems = rightbuttons;
	self.navigationItem.leftBarButtonItem = cancelButton;

	barmode = BARMODE_MORE;
}

- (void) showAnnotationMenu
{
	self.navigationItem.rightBarButtonItems = @[inkButton, strikeoutButton, underlineButton, highlightButton];
	self.navigationItem.leftBarButtonItem = cancelButton;

	for (UIView<MuPageView> *view in canvas.subviews)
	{
		if (view.number == current)
			[view deselectAnnotation];
	}

	barmode = BARMODE_ANNOTATION;
}

- (void) update
{
	for (UIView<MuPageView> *view in canvas.subviews)
		[view update];
}

- (void) onMore: (id)sender
{
	[self showMoreMenu];
}

- (void) onAnnot: (id)sender
{
	[self showAnnotationMenu];
}

- (void) onPrint: (id)sender
{
	UIPrintInteractionController *pic = [UIPrintInteractionController sharedPrintController];
	if (pic) {
		UIPrintInfo *printInfo = [UIPrintInfo printInfo];
		printInfo.outputType = UIPrintInfoOutputGeneral;
		printInfo.jobName = key;
		printInfo.duplex = UIPrintInfoDuplexLongEdge;
		pic.printInfo = printInfo;
		pic.showsPageRange = YES;
		pic.printPageRenderer = [[[MuPrintPageRenderer alloc] initWithDocRef:docRef] autorelease];

		void (^completionHandler)(UIPrintInteractionController *, BOOL, NSError *) =
			^(UIPrintInteractionController *pic, BOOL completed, NSError *error) {
				if (!completed && error)
					NSLog(@"FAILED! due to error in domain %@ with error code %u",
							error.domain, (unsigned int)error.code);
			};
		if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
			[pic presentFromBarButtonItem:printButton animated:YES
				completionHandler:completionHandler];
		} else {
			[pic presentAnimated:YES completionHandler:completionHandler];
		}
	}
}

- (void) shareDocument
{
	NSURL *url = [NSURL fileURLWithPath:_filePath];
	UIActivityViewController *cont = [[UIActivityViewController alloc] initWithActivityItems:@[url] applicationActivities:nil];
	cont.popoverPresentationController.barButtonItem = shareButton;
	[self presentViewController:cont animated:YES completion:nil];
	[cont release];
}

- (void) onShare: (id)sender
{
	pdf_document *idoc = pdf_specifics(ctx, doc);
	if (idoc && pdf_has_unsaved_changes(ctx, idoc))
	{
		UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:AlertTitle message:ShareAlertMessage delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Save and Share", nil];
		[alertView show];
		[alertView release];
	}
	else
	{
		[self shareDocument];
	}
}

- (void) textSelectModeOn
{
	self.navigationItem.rightBarButtonItems = @[tickButton];
	for (UIView<MuPageView> *view in canvas.subviews)
	{
		if (view.number == current)
			[view textSelectModeOn];
	}
}

- (void) textSelectModeOff
{
	for (UIView<MuPageView> *view in canvas.subviews)
	{
		[view textSelectModeOff];
	}
}

- (void) inkModeOn
{
	self.navigationItem.rightBarButtonItems = @[tickButton];
	for (UIView<MuPageView> *view in canvas.subviews)
	{
		if (view.number == current)
			[view inkModeOn];
	}
}

- (void) deleteModeOn
{
	self.navigationItem.rightBarButtonItems = @[deleteButton];
	barmode = BARMODE_DELETE;
}

- (void) inkModeOff
{
	for (UIView<MuPageView> *view in canvas.subviews)
	{
		[view inkModeOff];
	}
}

- (void) onHighlight: (id)sender
{
	barmode = BARMODE_HIGHLIGHT;
	[self textSelectModeOn];
}

- (void) onUnderline: (id)sender
{
	barmode = BARMODE_UNDERLINE;
	[self textSelectModeOn];
}

- (void) onStrikeout: (id)sender
{
	barmode = BARMODE_STRIKE;
	[self textSelectModeOn];
}

- (void) onInk: (id)sender
{
	barmode = BARMODE_INK;
	[self inkModeOn];
}

- (void) onShowSearch: (id)sender
{
	self.navigationItem.rightBarButtonItems =	@[nextButton, prevButton];
	self.navigationItem.leftBarButtonItem = cancelButton;
	self.navigationItem.titleView = searchBar;
	[searchBar becomeFirstResponder];
	barmode = BARMODE_SEARCH;
}

- (void) onTick: (id)sender
{

	for (UIView<MuPageView> *view in canvas.subviews)
	{
		if (view.number == current)
		{
			switch (barmode)
			{
				case BARMODE_HIGHLIGHT:
					[view saveSelectionAsMarkup:PDF_ANNOT_HIGHLIGHT];
					break;

				case BARMODE_UNDERLINE:
					[view saveSelectionAsMarkup:PDF_ANNOT_UNDERLINE];
					break;

				case BARMODE_STRIKE:
					[view saveSelectionAsMarkup:PDF_ANNOT_STRIKE_OUT];
					break;

				case BARMODE_INK:
					[view saveInk];
			}
		}
	}

	[self showAnnotationMenu];
}

- (void) onDelete: (id)sender
{
	for (UIView<MuPageView> *view in canvas.subviews)
	{
		if (view.number == current)
			[view deleteSelectedAnnotation];
	}
	[self showAnnotationMenu];
}

- (void) onCancel: (id)sender
{
	switch (barmode)
	{
		case BARMODE_SEARCH:
			cancelSearch = YES;
			[searchBar resignFirstResponder];
			[self resetSearch];
			/* fallthrough */
		case BARMODE_ANNOTATION:
		case BARMODE_MORE:
			[self.navigationItem setTitleView: nil];
			[self addMainMenuButtons];
			barmode = BARMODE_MAIN;
			break;

		case BARMODE_HIGHLIGHT:
		case BARMODE_UNDERLINE:
		case BARMODE_STRIKE:
		case BARMODE_DELETE:
			[self showAnnotationMenu];
			[self textSelectModeOff];
			break;

		case BARMODE_INK:
			[self showAnnotationMenu];
			[self inkModeOff];
			break;
	}
}

- (void) onBack: (id)sender
{
	pdf_document *idoc = pdf_specifics(ctx, doc);
	if (idoc && pdf_has_unsaved_changes(ctx, idoc))
	{
		UIAlertView *saveAlert = [[UIAlertView alloc]
			initWithTitle:AlertTitle message:CloseAlertMessage delegate:self
			cancelButtonTitle:@"Discard" otherButtonTitles:@"Save", nil];
		[saveAlert show];
		[saveAlert release];
	}
	else
	{
		[self.navigationController popViewControllerAnimated:YES];
	}
}

- (void) alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
	if ([CloseAlertMessage isEqualToString:alertView.message])
	{
		if (buttonIndex == 1)
			saveDoc(_filePath.UTF8String, doc);

		[alertView dismissWithClickedButtonIndex:buttonIndex animated:YES];
		[self.navigationController popViewControllerAnimated:YES];
	}

	if ([ShareAlertMessage isEqualToString:alertView.message])
	{
		[alertView dismissWithClickedButtonIndex:buttonIndex animated:NO];
		if (buttonIndex == 1)
		{
			saveDoc(_filePath.UTF8String, doc);
			[self shareDocument];
		}
	}
}

- (void) resetSearch
{
	searchPage = -1;
	for (UIView<MuPageView> *view in canvas.subviews)
		[view clearSearchResults];
}

- (void) showSearchResults: (int)count forPage: (int)number
{
	printf("search found match on page %d\n", number);
	searchPage = number;
	[self gotoPage: number animated: NO];
	for (UIView<MuPageView> *view in canvas.subviews)
		if (view.number == number)
			[view showSearchResults: count];
		else
			[view clearSearchResults];
}

- (void) searchInDirection: (int)dir
{
	UITextField *searchField;
	char *needle;
	int start;

	[searchBar resignFirstResponder];

	if (searchPage == current)
		start = current + dir;
	else
		start = current;

	needle = strdup(searchBar.text.UTF8String);

	searchField = nil;
	for (id view in searchBar.subviews)
		if ([view isKindOfClass: [UITextField class]])
			searchField = view;

	[prevButton setEnabled: NO];
	[nextButton setEnabled: NO];
	[searchField setEnabled: NO];

	cancelSearch = NO;

	dispatch_async(queue, ^{
		for (int i = start; i >= 0 && i < fz_count_pages(ctx, doc); i += dir) {
			int n = search_page(doc, i, needle, NULL);
			if (n) {
				dispatch_async(dispatch_get_main_queue(), ^{
					[prevButton setEnabled: YES];
					[nextButton setEnabled: YES];
					[searchField setEnabled: YES];
					[self showSearchResults: n forPage: i];
					free(needle);
				});
				return;
			}
			if (cancelSearch) {
				dispatch_async(dispatch_get_main_queue(), ^{
					[prevButton setEnabled: YES];
					[nextButton setEnabled: YES];
					[searchField setEnabled: YES];
					free(needle);
				});
				return;
			}
		}
		dispatch_async(dispatch_get_main_queue(), ^{
			printf("no search results found\n");
			[prevButton setEnabled: YES];
			[nextButton setEnabled: YES];
			[searchField setEnabled: YES];
			UIAlertView *alert = [[UIAlertView alloc]
				initWithTitle: @"No matches found for:"
				message: @(needle)
				delegate: nil
				cancelButtonTitle: @"Close"
				otherButtonTitles: nil];
			[alert show];
			[alert release];
			free(needle);
		});
	});
}

- (void) onSearchPrev: (id)sender
{
	[self searchInDirection: -1];
}

- (void) onSearchNext: (id)sender
{
	[self searchInDirection: 1];
}

- (void) searchBarSearchButtonClicked: (UISearchBar*)sender
{
	[self onSearchNext: sender];
}

- (void) searchBar: (UISearchBar*)sender textDidChange: (NSString*)searchText
{
	[self resetSearch];
	if (searchBar.text.length > 0) {
		[prevButton setEnabled: YES];
		[nextButton setEnabled: YES];
	} else {
		[prevButton setEnabled: NO];
		[nextButton setEnabled: NO];
	}
}

- (void) onSlide: (id)sender
{
	int number = slider.value;
	if (slider.tracking)
		indicator.text = [NSString stringWithFormat: @" %d of %d ", number+1, fz_count_pages(ctx, doc)];
	else
		[self gotoPage: number animated: NO];
}

- (BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
	// For reflow mode, we load UIWebViews into the canvas. Returning YES
	// here prevents them stealing our tap and pinch events.
	return YES;
}

- (void) onTap: (UITapGestureRecognizer*)sender
{
	CGPoint p = [sender locationInView: canvas];
	CGPoint ofs = canvas.contentOffset;
	float x0 = (width - GAP) / 5;
	float x1 = (width - GAP) - x0;
	p.x -= ofs.x;
	p.y -= ofs.y;
	__block BOOL tapHandled = NO;
	for (UIView<MuPageView> *view in canvas.subviews)
	{
		CGPoint pp = [sender locationInView:view];
		if (CGRectContainsPoint(view.bounds, pp))
		{
			MuTapResult *result = [view handleTap:pp];
			__block BOOL hitAnnot = NO;
			[result switchCaseInternal:^(MuTapResultInternalLink *link) {
				[self gotoPage:link.pageNumber animated:NO];
				tapHandled = YES;
			} caseExternal:^(MuTapResultExternalLink *link) {
				// Not currently supported
			} caseRemote:^(MuTapResultRemoteLink *link) {
				// Not currently supported
			} caseWidget:^(MuTapResultWidget *widget) {
				tapHandled = YES;
			} caseAnnotation:^(MuTapResultAnnotation *annot) {
				hitAnnot = YES;
			}];

			switch (barmode)
			{
				case BARMODE_ANNOTATION:
					if (hitAnnot)
						[self deleteModeOn];
					tapHandled = YES;
					break;

				case BARMODE_DELETE:
					if (!hitAnnot)
						[self showAnnotationMenu];
					tapHandled = YES;
					break;

				default:
					if (hitAnnot)
					{
						// Annotation will have been selected, which is wanted
						// only in annotation-editing mode
						[view deselectAnnotation];
					}
					break;
			}

			if (tapHandled)
				break;
		}
	}
	if (tapHandled) {
		// Do nothing further
	} else if (p.x < x0) {
		[self gotoPage: current-1 animated: YES];
	} else if (p.x > x1) {
		[self gotoPage: current+1 animated: YES];
	} else {
		if (self.navigationController.navigationBarHidden)
			[self showNavigationBar];
		else if (barmode == BARMODE_MAIN)
			[self hideNavigationBar];
	}
}

- (void) onPinch:(UIPinchGestureRecognizer*)sender
{
	if (sender.state == UIGestureRecognizerStateBegan)
		sender.scale = scale;

	if (sender.scale < MIN_SCALE)
		sender.scale = MIN_SCALE;

	if (sender.scale > MAX_SCALE)
		sender.scale = MAX_SCALE;

	if (sender.state == UIGestureRecognizerStateEnded)
		scale = sender.scale;

	for (UIView<MuPageView> *view in canvas.subviews)
	{
		// Zoom only the visible page until end of gesture
		if (view.number == current || sender.state == UIGestureRecognizerStateEnded)
			[view setScale:sender.scale];
	}
}

- (void) scrollViewWillBeginDragging: (UIScrollView *)scrollView
{
	if (barmode == BARMODE_MAIN)
		[self hideNavigationBar];
}

- (void) scrollViewDidScroll: (UIScrollView*)scrollview
{
	// scrollViewDidScroll seems to get called part way through a screen rotation.
	// (This is possibly a UIScrollView bug - see
	// http://stackoverflow.com/questions/4123991/uiscrollview-disable-scrolling-while-rotating-on-iphone-ipad/8141423#8141423 ).
	// This ends up corrupting the current page number, because the calculation
	// 'current = x / width' is using the new value of 'width' before the
	// pages have been resized/repositioned. To avoid this problem, we filter out
	// calls to scrollViewDidScroll during rotation.
	if (_isRotating)
		return;

	if (width == 0)
		return; // not visible yet

	if (scroll_animating)
		return; // don't mess with layout during animations

	float x = canvas.contentOffset.x + width * 0.5f;
	current = x / width;

	[[NSUserDefaults standardUserDefaults] setInteger: current forKey: key];

	indicator.text = [NSString stringWithFormat: @" %d of %d ", current+1, fz_count_pages(ctx, doc)];
	slider.value = current;

	// swap the distant page views out

	NSMutableSet *invisiblePages = [[NSMutableSet alloc] init];
	for (UIView<MuPageView> *view in canvas.subviews) {
		if (view.number != current)
			[view resetZoomAnimated: YES];
		if (view.number < current - 2 || view.number > current + 2)
			[invisiblePages addObject: view];
	}
	for (UIView<MuPageView> *view in invisiblePages)
		[view removeFromSuperview];
	[invisiblePages release]; // don't bother recycling them...

	[self createPageView: current];
	[self createPageView: current - 1];
	[self createPageView: current + 1];

	// reset search results when page has flipped
	if (current != searchPage)
		[self resetSearch];
}

- (void) createPageView: (int)number
{
	if (number < 0 || number >= fz_count_pages(ctx, doc))
		return;
	int found = 0;
	for (UIView<MuPageView> *view in canvas.subviews)
		if (view.number == number)
			found = 1;
	if (!found) {
		UIView<MuPageView> *view
			= reflowMode
				? [[MuPageViewReflow alloc] initWithFrame:CGRectMake(number * width, 0, width-GAP, height) document:docRef page:number]
		: [[MuPageViewNormal alloc] initWithFrame:CGRectMake(number * width, 0, width-GAP, height) dialogCreator:self updater:self document:docRef page:number];
		[view setScale:scale];
		[canvas addSubview: view];
		if (showLinks)
			[view showLinks];
		[view release];
	}
}

- (void) gotoPage: (int)number animated: (BOOL)animated
{
	if (number < 0)
		number = 0;
	if (number >= fz_count_pages(ctx, doc))
		number = fz_count_pages(ctx, doc) - 1;
	if (current == number)
		return;
	if (animated) {
		// setContentOffset:animated: does not use the normal animation
		// framework. It also doesn't play nice with the tap gesture
		// recognizer. So we do our own page flipping animation here.
		// We must set the scroll_animating flag so that we don't create
		// or remove subviews until after the animation, or they'll
		// swoop in from origo during the animation.

		scroll_animating = YES;
		[UIView beginAnimations: @"MuScroll" context: NULL];
		[UIView setAnimationDuration: 0.4];
		[UIView setAnimationBeginsFromCurrentState: YES];
		[UIView setAnimationDelegate: self];
		[UIView setAnimationDidStopSelector: @selector(onGotoPageFinished)];

		for (UIView<MuPageView> *view in canvas.subviews)
			[view resetZoomAnimated: NO];

		canvas.contentOffset = CGPointMake(number * width, 0);
		slider.value = number;
		indicator.text = [NSString stringWithFormat: @" %d of %d ", number+1, fz_count_pages(ctx, doc)];

		[UIView commitAnimations];
	} else {
		for (UIView<MuPageView> *view in canvas.subviews)
			[view resetZoomAnimated: NO];
		canvas.contentOffset = CGPointMake(number * width, 0);
	}
	current = number;
}

- (void) invokeTextDialog:(NSString *)aString okayAction:(void (^)(NSString *))block
{
	MuTextFieldController *tf = [[MuTextFieldController alloc] initWithText:aString okayAction:block];
	tf.modalPresentationStyle = UIModalPresentationFormSheet;
	[self presentViewController:tf animated:YES completion:nil];
	[tf release];
}

- (void) invokeChoiceDialog:(NSArray *)anArray okayAction:(void (^)(NSArray *))block
{
	MuChoiceFieldController *cf = [[MuChoiceFieldController alloc] initWithChoices:anArray okayAction:block];
	cf.modalPresentationStyle = UIModalPresentationFormSheet;
	[self presentViewController:cf animated:YES completion:nil];
	[cf release];
}

- (void) onGotoPageFinished
{
	scroll_animating = NO;
	[self scrollViewDidScroll: canvas];
}

- (BOOL) shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
{
	return YES;
}

- (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
	_isRotating = YES;
}

- (void) didRotateFromInterfaceOrientation: (UIInterfaceOrientation)o
{
	_isRotating = NO;

	// We need to set these here, because during the animation we may use a wider
	// size (the maximum of the landscape/portrait widths), to avoid clipping during
	// the rotation.
	canvas.contentSize = CGSizeMake(fz_count_pages(ctx, doc) * width, height);
	canvas.contentOffset = CGPointMake(current * width, 0);
}

@end