// Copyright (C) 2004-2021 Artifex Software, Inc.
//
// This file is part of MuPDF.
//
// MuPDF is free software: you can redistribute it and/or modify it under the
// terms of the GNU Affero General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// MuPDF is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
// details.
//
// You should have received a copy of the GNU Affero General Public License
// along with MuPDF. If not, see <https://www.gnu.org/licenses/agpl-3.0.en.html>
//
// Alternative licensing terms are available from the licensor.
// For commercial licensing, see <https://www.artifex.com/> or contact
// Artifex Software, Inc., 1305 Grant Avenue - Suite 200, Novato,
// CA 94945, U.S.A., +1(415)492-9861, for further information.

/**
 * Mu Office Library
 *
 * Provided access to the core document, loading, displaying and
 * editing routines
 *
 * Intended for use with native UI
 */

#include "mupdf/fitz.h"
#include "mupdf/pdf.h"
#include "mupdf/helpers/mu-office-lib.h"
#include "mupdf/helpers/mu-threads.h"
#include "mupdf/memento.h"

#include <assert.h>

enum
{
	MuError_OK = 0,
	MuError_OOM = -1,
	MuError_BadNull = -2,
	MuError_Generic = -3,
	MuError_NotImplemented = -4,
	MuError_PasswordPending = -5,
};

enum {
	LAYOUT_W = 450,
	LAYOUT_H = 600,
	LAYOUT_EM = 12
};

#ifdef DISABLE_MUTHREADS
#error "mu-office-lib requires threading to be enabled"
#endif

/*
	If we are building as part of a smartoffice build, then we
	should appeal to Pal_Mem_etc to get memory. If not, then
	we should use malloc instead.

	FIXME: Allow for something other than malloc/calloc/realloc/
	free here.
*/
#ifndef SMARTOFFICE_BUILD
void *Pal_Mem_calloc(unsigned int num, size_t size)
{
	return calloc(num, size);
}

void *Pal_Mem_malloc(size_t size)
{
	return malloc(size);
}

void *Pal_Mem_realloc(void *ptr, size_t size)
{
	return realloc(ptr, size);
}

void Pal_Mem_free(void *address)
{
	free(address);
}
#endif

/*
	All MuPDF's allocations are redirected through the
	following functions.
*/
static void *muoffice_malloc(void *arg, size_t size)
{
	return Pal_Mem_malloc(size);
}

static void *muoffice_realloc(void *arg, void *old, size_t size)
{
	return Pal_Mem_realloc(old, size);
}

static void muoffice_free(void *arg, void *ptr)
{
	Pal_Mem_free(ptr);
}

static fz_alloc_context muoffice_alloc =
{
	/* user */
	NULL,

	/* void *(*malloc)(void *, size_t); */
	muoffice_malloc,

	/* void *(*realloc)(void *, void *, size_t); */
	muoffice_realloc,

	/* void (*free)(void *, void *); */
	muoffice_free
};

/*
	All MuPDF's locking is done using the following functions
*/
static void muoffice_lock(void *user, int lock);

static void muoffice_unlock(void *user, int lock);

struct MuOfficeLib
{
	fz_context *ctx;
	mu_mutex mutexes[FZ_LOCK_MAX+1];
	fz_locks_context locks;
};

/*
	We add 1 extra lock which we use in this helper to protect
	against accessing the fz_document from multiple threads
	inadvertently when the caller is calling 'run' or
	'runBackground'.
*/
enum
{
	DOCLOCK = FZ_LOCK_MAX
};

static void muoffice_lock(void *user, int lock)
{
	MuOfficeLib *mu = (MuOfficeLib *)user;

	mu_lock_mutex(&mu->mutexes[lock]);
}

static void muoffice_unlock(void *user, int lock)
{
	MuOfficeLib *mu = (MuOfficeLib *)user;

	mu_unlock_mutex(&mu->mutexes[lock]);
}

static void muoffice_doc_lock(MuOfficeLib *mu)
{
	mu_lock_mutex(&mu->mutexes[DOCLOCK]);
}

static void muoffice_doc_unlock(MuOfficeLib *mu)
{
	mu_unlock_mutex(&mu->mutexes[DOCLOCK]);
}

static void fin_muoffice_locks(MuOfficeLib *mu)
{
	int i;

	for (i = 0; i < FZ_LOCK_MAX+1; i++)
		mu_destroy_mutex(&mu->mutexes[i]);
}

static fz_locks_context *init_muoffice_locks(MuOfficeLib *mu)
{
	int i;
	int failed = 0;

	for (i = 0; i < FZ_LOCK_MAX+1; i++)
		failed |= mu_create_mutex(&mu->mutexes[i]);

	if (failed)
	{
		fin_muoffice_locks(mu);
		return NULL;
	}

	mu->locks.user = mu;
	mu->locks.lock = muoffice_lock;
	mu->locks.unlock = muoffice_unlock;

	return &mu->locks;
}

MuError MuOfficeLib_create(MuOfficeLib **pMu)
{
	MuOfficeLib *mu;
	fz_locks_context *locks;

	if (pMu == NULL)
		return MuOfficeDocErrorType_IllegalArgument;

	mu = Pal_Mem_calloc(1, sizeof(MuOfficeLib));
	if (mu == NULL)
		return MuOfficeDocErrorType_OutOfMemory;

	locks = init_muoffice_locks(mu);
	if (locks == NULL)
		goto Fail;

	mu->ctx = fz_new_context(&muoffice_alloc, locks, FZ_STORE_DEFAULT);
	if (mu->ctx == NULL)
		goto Fail;

	fz_try(mu->ctx)
		fz_register_document_handlers(mu->ctx);
	fz_catch(mu->ctx)
		goto Fail;

	*pMu = mu;

	return MuOfficeDocErrorType_NoError;

Fail:
	if (mu)
	{
		fin_muoffice_locks(mu);
		Pal_Mem_free(mu);
	}
	return MuOfficeDocErrorType_OutOfMemory;
}

/**
 * Destroy a MuOfficeLib instance
 *
 * @param mu  the instance to destroy
 */
void MuOfficeLib_destroy(MuOfficeLib *mu)
{
	if (mu == NULL)
		return;

	fz_drop_context(mu->ctx);
	fin_muoffice_locks(mu);

	Pal_Mem_free(mu);
}

/**
 * Perform MuPDF native operations on a given MuOfficeLib
 * instance.
 *
 * The function is called with a fz_context value that can
 * be safely used (i.e. the context is cloned/dropped
 * appropriately around the call). The function should signal
 * errors by fz_throw-ing.
 *
 * @param mu           the MuOfficeLib instance.
 * @param fn           the function to call to run the operations.
 * @param arg          Opaque data pointer.
 *
 * @return             error indication - 0 for success
 */
MuError MuOfficeLib_run(MuOfficeLib *mu, void (*fn)(fz_context *ctx, void *arg), void *arg)
{
	fz_context *ctx;
	MuError err = MuError_OK;

	if (mu == NULL)
		return MuError_BadNull;
	if (fn == NULL)
		return err;

	ctx = fz_clone_context(mu->ctx);
	if (ctx == NULL)
		return MuError_OOM;

	fz_try(ctx)
		fn(ctx, arg);
	fz_catch(ctx)
		err = MuError_Generic;

	fz_drop_context(ctx);

	return err;
}

/**
 * Find the type of a file given its filename extension.
 *
 * @param path      path to the file (in utf8)
 *
 * @return          a valid MuOfficeDocType value, or MuOfficeDocType_Other
 */
MuOfficeDocType MuOfficeLib_getDocTypeFromFileExtension(const char *path)
{
	return /* FIXME */MuOfficeDocType_PDF;
}

/**
 * Return a list of file extensions supported by Mu Office library.
 *
 * @return    comma-delimited list of extensions, without the leading ".".
 *            The caller should free the returned pointer..
 */
char * MuOfficeLib_getSupportedFileExtensions(void)
{
	/* FIXME */
	return NULL;
}

struct MuOfficeDoc
{
	MuOfficeLib *mu;
	fz_context *ctx;
	MuOfficeLoadingProgressFn *progress;
	MuOfficeLoadingErrorFn *error;
	void *cookie;
	char *path;
	char *password;
	mu_semaphore password_sem;
	mu_thread thread;
	int needs_password;
	int aborted;
	fz_document *doc;

	MuOfficePage *pages;
};

struct MuOfficePage
{
	MuOfficePage *next;
	MuOfficeDoc *doc;
	int pageNum;
	void *cookie;
	MuOfficePageUpdateFn *updateFn;
	fz_page *page;
	fz_display_list *list;
};

struct MuOfficeRender
{
	MuOfficePage *page;
	float zoom;
	const MuOfficeBitmap *bitmap;
	int area_valid;
	MuOfficeRenderArea area;
	MuOfficeRenderProgressFn *progress;
	MuError error;
	mu_thread thread;
	void *cookie;
	fz_cookie mu_cookie;
};

static void load_worker(void *arg)
{
	MuOfficeDoc *doc = (MuOfficeDoc *)arg;
	int numPages = 0;
	fz_context *ctx = fz_clone_context(doc->ctx);
	int err = 0;

	if (ctx == NULL)
	{
		return;
	}

	muoffice_doc_lock(doc->mu);

	fz_try(ctx)
	{
		doc->doc = fz_open_document(ctx, doc->path);
		doc->needs_password = fz_needs_password(ctx, doc->doc);
	}
	fz_catch(ctx)
	{
		err = MuOfficeDocErrorType_UnsupportedDocumentType;
		goto fail;
	}

	fz_try(ctx)
	{
		if (doc->needs_password && doc->error)
		{
			do
			{
				doc->error(doc->cookie, MuOfficeDocErrorType_PasswordRequest);
				mu_wait_semaphore(&doc->password_sem);
				if (doc->aborted)
					break;
				doc->needs_password = (fz_authenticate_password(ctx, doc->doc, doc->password) != 0);
				Pal_Mem_free(doc->password);
				doc->password = NULL;
			}
			while (doc->needs_password);
		}

		fz_layout_document(ctx, doc->doc, LAYOUT_W, LAYOUT_H, LAYOUT_EM);

		numPages = fz_count_pages(ctx, doc->doc);
	}
	fz_catch(ctx)
		err = MuOfficeDocErrorType_UnableToLoadDocument;

fail:
	muoffice_doc_unlock(doc->mu);

	if (err)
		doc->error(doc->cookie, err);

	if (doc->progress)
		doc->progress(doc->cookie, numPages, 1);

	fz_drop_context(ctx);
}

/**
 * Load a document
 *
 * Call will return immediately, leaving the document loading
 * in the background
 *
 * @param so         a MuOfficeLib instance
 * @param path       path to the file to load (in utf8)
 * @param progressFn callback for monitoring progress
 * @param errorFn    callback for monitoring errors
 * @param cookie     a pointer to pass back to the callbacks
 * @param pDoc       address for return of a MuOfficeDoc object
 *
 * @return          error indication - 0 for success
 *
 * The progress callback may be called several times, with increasing
 * values of pagesLoaded. Unless MuOfficeDoc_destroy is called,
 * before loading completes, a call with "completed" set to true
 * is guaranteed.
 *
 * Once MuOfficeDoc_destroy is called there will be no
 * further callbacks.
 *
 * Alternatively, in a synchronous context, MuOfficeDoc_getNumPages
 * can be called to wait for loading to complete and return the total
 * number of pages. In this mode of use, progressFn can be NULL. 
 */
MuError MuOfficeLib_loadDocument(	MuOfficeLib               *mu,
					const char                *path,
					MuOfficeLoadingProgressFn *progressFn,
					MuOfficeLoadingErrorFn    *errorFn,
					void                      *cookie,
					MuOfficeDoc              **pDoc)
{
	MuOfficeDoc *doc;
	fz_context *ctx;

	if (mu == NULL || pDoc == NULL)
		return MuOfficeDocErrorType_IllegalArgument;

	*pDoc = NULL;

	doc = Pal_Mem_calloc(1, sizeof(*doc));
	if (doc == NULL)
		return MuOfficeDocErrorType_NoError;

	ctx = mu->ctx;
	doc->mu       = mu;
	doc->ctx      = fz_clone_context(ctx);
	doc->progress = progressFn;
	doc->error    = errorFn;
	doc->cookie   = cookie;
	doc->path     = fz_strdup(ctx, path);
	if (mu_create_semaphore(&doc->password_sem))
		goto fail;

	if (mu_create_thread(&doc->thread, load_worker, doc))
		goto fail;

	*pDoc = doc;

	return MuError_OK;
fail:
	mu_destroy_semaphore(&doc->password_sem);
	Pal_Mem_free(doc);

	return MuError_OOM;
}

/**
 * Provide the password for a document
 *
 * This function should be called to provide a password with a document
 * error of MuOfficeError_PasswordRequired is received.
 *
 * If a password is requested again, this means the password was incorrect.
 *
 * @param doc         the document object
 * @param password    the password (UTF8 encoded)
 * @return            error indication - 0 for success
 */
int MuOfficeDoc_providePassword(MuOfficeDoc *doc, const char *password)
{
	size_t len;

	if (doc->password)
		return MuError_PasswordPending;
	if (!password)
		password = "";

	len = strlen(password);
	doc->password = Pal_Mem_malloc(len+1);
	strcpy(doc->password, password);
	mu_trigger_semaphore(&doc->password_sem);

	return MuError_OK;
}

/**
 * Return the type of an open document
 *
 * @param doc         the document object
 *
 * @return            the document type
 */
MuOfficeDocType MuOfficeDoc_docType(MuOfficeDoc *doc)
{
	return /* FIXME */MuOfficeDocType_PDF;
}

static void
ensure_doc_loaded(MuOfficeDoc *doc)
{
	if (doc == NULL)
		return;

	mu_destroy_thread(&doc->thread);
}

/**
 * Return the number of pages of a document
 *
 * This function waits for document loading to complete before returning
 * the result. It may block the calling thread for a significant period of
 * time. To avoid blocking, this call should be avoided in favour of using
 * the MuOfficeLib_loadDocument callbacks to monitor loading.
 *
 * If background loading fails, the associated error will be returned
 * from this call.
 *
 * @param doc         the document
 * @param pNumPages   address for return of the number of pages
 *
 * @return            error indication - 0 for success
 */
MuError MuOfficeDoc_getNumPages(MuOfficeDoc *doc, int *pNumPages)
{
	fz_context *ctx;
	MuError err = MuError_OK;

	if (doc == NULL)
	{
		*pNumPages = 0;
		return MuError_BadNull;
	}

	ensure_doc_loaded(doc);

	ctx = doc->ctx;

	fz_try(ctx)
	{
		*pNumPages = fz_count_pages(ctx, doc->doc);
	}
	fz_catch(ctx)
	{
		err = MuError_Generic;
	}

	return err;
}

/**
 * Determine if the document has been modified
 *
 * @param doc         the document
 *
 * @return            modified flag
 */
int MuOfficeDoc_hasBeenModified(MuOfficeDoc *doc)
{
	fz_context *ctx;
	pdf_document *pdoc;
	int modified = 0;

	if (doc == NULL)
		return 0;

	ensure_doc_loaded(doc);

	ctx = doc->ctx;
	pdoc = pdf_specifics(ctx, doc->doc);

	if (pdoc == NULL)
		return 0;

	fz_try(ctx)
		modified = pdf_has_unsaved_changes(ctx, pdoc);
	fz_catch(ctx)
		modified = 0;

	return modified;
}

/**
 * Start a save operation
 *
 * @param doc         the document
 * @param path        path of the file to which to save
 * @param resultFn    callback used to report completion
 * @param cookie      a pointer to pass to the callback
 *
 * @return            error indication - 0 for success
 */
MuError MuOfficeDoc_save(	MuOfficeDoc          *doc,
				const char              *path,
				MuOfficeSaveResultFn *resultFn,
				void                    *cookie)
{
	return MuError_NotImplemented; /* FIXME */
}

/**
 * Stop a document loading. The document is not destroyed, but
 * no further content will be read from the file.
 *
 * @param doc       the MuOfficeDoc object
 */
void MuOfficeDoc_abortLoad(MuOfficeDoc *doc)
{
	fz_context *ctx;

	if (doc == NULL)
		return;

	ctx = doc->ctx;
	doc->aborted = 1;
	mu_trigger_semaphore(&doc->password_sem);
}

/**
 * Destroy a MuOfficeDoc object. Loading of the document is shutdown
 * and no further callbacks will be issued for the specified object.
 *
 * @param doc       the MuOfficeDoc object
 */
void MuOfficeDoc_destroy(MuOfficeDoc *doc)
{
	MuOfficeDoc_abortLoad(doc);
	mu_destroy_thread(&doc->thread);
	mu_destroy_semaphore(&doc->password_sem);

	fz_drop_document(doc->ctx, doc->doc);
	fz_drop_context(doc->ctx);
	Pal_Mem_free(doc->path);
	Pal_Mem_free(doc);
}

/**
 * Get a page of a document
 *
 * @param doc          the document object
 * @param pageNumber   the number of the page to load (lying in the
 *                     range 0 to one less than the number of pages)
 * @param updateFn     Function to be called back when the page updates
 * @param cookie       Opaque value to pass for any updates
 * @param pPage        Address for return of the page object
 *
 * @return             error indication - 0 for success
 */
MuError MuOfficeDoc_getPage(	MuOfficeDoc          *doc,
				int                   pageNumber,
				MuOfficePageUpdateFn *updateFn,
				void                 *cookie,
				MuOfficePage        **pPage)
{
	MuOfficePage *page;
	MuError err = MuError_OK;
	fz_context *ctx;

	if (!doc)
		return MuError_BadNull;
	if (!pPage)
		return MuError_OK;

	*pPage = NULL;

	ensure_doc_loaded(doc);
	ctx = doc->ctx;

	page = Pal_Mem_calloc(1, sizeof(*page));
	if (page == NULL)
		return MuError_OOM;

	muoffice_doc_lock(doc->mu);

	fz_try(ctx)
	{
		page->doc = doc;
		page->pageNum = pageNumber;
		page->cookie = cookie;
		page->updateFn = updateFn;
		page->page = fz_load_page(doc->ctx, doc->doc, pageNumber);
		page->next = doc->pages;
		doc->pages = page;
		*pPage = page;
	}
	fz_catch(ctx)
	{
		Pal_Mem_free(page);
		err = MuError_Generic;
	}

	muoffice_doc_unlock(doc->mu);

	return err;
}

/**
 * Perform MuPDF native operations on a given document.
 *
 * The function is called with fz_context and fz_document
 * values that can be safely used (i.e. the context is
 * cloned/dropped appropriately around the function, and
 * locking is used to ensure that no other threads are
 * simultaneously using the document). Functions can
 * signal errors by fz_throw-ing.
 *
 * Due to the locking, it is best to ensure that as little
 * time is taken here as possible (i.e. if you fetch some
 * data and then spend a long time processing it, it is
 * probably best to fetch the data using MuOfficeDoc_run
 * and then process it outside). This avoids potentially
 * blocking the UI.
 *
 * @param doc          the document object.
 * @param fn           the function to call with fz_context/fz_document
 *                     values.
 * @param arg          Opaque data pointer.
 *
 * @return             error indication - 0 for success
 */
MuError MuOfficeDoc_run(MuOfficeDoc *doc, void (*fn)(fz_context *ctx, fz_document *doc, void *arg), void *arg)
{
	fz_context *ctx;
	MuError err = MuError_OK;

	if (doc == NULL)
		return MuError_BadNull;
	if (fn == NULL)
		return err;

	ensure_doc_loaded(doc);

	ctx = fz_clone_context(doc->mu->ctx);
	if (ctx == NULL)
		return MuError_OOM;

	muoffice_doc_lock(doc->mu);

	fz_try(ctx)
		fn(ctx, doc->doc, arg);
	fz_catch(ctx)
		err = MuError_Generic;

	muoffice_doc_unlock(doc->mu);

	fz_drop_context(ctx);

	return err;
}

/**
 * Destroy a page object
 *
 * Note this does not delete or remove the page from the document.
 * It simply destroys the page object which is merely a reference
 * to the page.
 *
 * @param page         the page object
 */
void MuOfficePage_destroy(MuOfficePage *page)
{
	MuOfficeDoc *doc;
	MuOfficePage **ptr;

	if (!page)
		return;

	/* Unlink page from doc */
	doc = page->doc;
	ptr = &doc->pages;
	while (*ptr && *ptr != page)
		ptr = &(*ptr)->next;
	assert(*ptr);
	*ptr = page->next;

	fz_drop_page(doc->ctx, page->page);
	fz_drop_display_list(doc->ctx, page->list);
	fz_free(doc->ctx, page);
}

/**
 * Get the size of a page in pixels
 *
 * This returns the size of the page in pixels. Pages can be rendered
 * with a zoom factor. The returned value is the size of bitmap
 * appropriate for rendering with a zoom of 1.0 and corresponds to
 * 90 dpi. The returned values are not necessarily whole numbers.
 *
 * @param page         the page object
 * @param pWidth       address for return of the width
 * @param pHeight      address for return of the height
 *
 * @return             error indication - 0 for success
 */
MuError MuOfficePage_getSize(	MuOfficePage *page,
				float        *pWidth,
				float        *pHeight)
{
	MuOfficeDoc *doc;
	fz_rect rect;

	if (!page)
		return MuError_BadNull;
	doc = page->doc;
	if (!doc)
		return MuError_BadNull;

	rect = fz_bound_page(doc->ctx, page->page);

	/* MuPDF measures in points (72ths of an inch). This API wants
	 * 90ths of an inch, so adjust. */

	if (pWidth)
		*pWidth = 90 * (rect.x1 - rect.x0) / 72;
	if (pHeight)
		*pHeight = 90 * (rect.y1 - rect.y0) / 72;

	return MuError_OK;
}

/**
 * Return the zoom factors necessary to render at to a given
 * size in pixels. (deprecated)
 *
 * @param page         the page object
 * @param width        the desired width
 * @param height       the desired height
 * @param pXZoom       Address for return of zoom necessary to fit width
 * @param pYZoom       Address for return of zoom necessary to fit height
 *
 * @return             error indication - 0 for success
 */
MuError MuOfficePage_calculateZoom(	MuOfficePage *page,
					int           width,
					int           height,
					float	     *pXZoom,
					float        *pYZoom)
{
	MuOfficeDoc *doc;
	fz_rect rect;
	float w, h;

	if (!page)
		return MuError_BadNull;
	doc = page->doc;
	if (!doc)
		return MuError_BadNull;

	rect = fz_bound_page(doc->ctx, page->page);

	/* MuPDF measures in points (72ths of an inch). This API wants
	 * 90ths of an inch, so adjust. */
	w = 90 * (rect.x1 - rect.x0) / 72;
	h = 90 * (rect.y1 - rect.y0) / 72;

	if (pXZoom)
		*pXZoom = width/w;
	if (pYZoom)
		*pYZoom = height/h;

	return MuError_OK;
}

/**
 * Get the size of a page in pixels for a specified zoom factor
 * (deprecated)
 *
 * This returns the size of bitmap that should be used to display
 * the entire page at the given zoom factor. A zoom of 1.0
 * corresponds to 90 dpi.
 *
 * @param page         the page object
 * @param zoom         the zoom factor
 * @param pWidth       address for return of the width
 * @param pHeight      address for return of the height
 *
 * @return             error indication - 0 for success
 */
MuError MuOfficePage_getSizeForZoom(	MuOfficePage *page,
					float         zoom,
					int          *pWidth,
					int          *pHeight)
{
	MuOfficeDoc *doc;
	fz_rect rect;
	float w, h;

	if (!page)
		return MuError_BadNull;
	doc = page->doc;
	if (!doc)
		return MuError_BadNull;

	rect = fz_bound_page(doc->ctx, page->page);

	/* MuPDF measures in points (72ths of an inch). This API wants
	 * 90ths of an inch, so adjust. */
	w = 90 * (rect.x1 - rect.x0) / 72;
	h = 90 * (rect.y1 - rect.y0) / 72;

	if (pWidth)
		*pWidth = (int)(w * zoom + 0.5f);
	if (pHeight)
		*pHeight = (int)(h * zoom + 0.5f);

	return MuError_OK;
}

/**
 * Perform MuPDF native operations on a given page.
 *
 * The function is called with fz_context and fz_page
 * values that can be safely used (i.e. the context is
 * cloned/dropped appropriately around the function, and
 * locking is used to ensure that no other threads are
 * simultaneously using the document). Functions can
 * signal errors by fz_throw-ing.
 *
 * Due to the locking, it is best to ensure that as little
 * time is taken here as possible (i.e. if you fetch some
 * data and then spend a long time processing it, it is
 * probably best to fetch the data using MuOfficePage_run
 * and then process it outside). This avoids potentially
 * blocking the UI.
 *
 * @param page         the page object.
 * @param fn           the function to call with fz_context/fz_document
 *                     values.
 * @param arg          Opaque data pointer.
 *
 * @return             error indication - 0 for success
 */
MuError MuOfficePage_run(MuOfficePage *page, void (*fn)(fz_context *ctx, fz_page *page, void *arg), void *arg)
{
	fz_context *ctx;
	MuError err = MuError_OK;

	if (page == NULL)
		return MuError_BadNull;
	if (fn == NULL)
		return err;

	ctx = fz_clone_context(page->doc->mu->ctx);
	if (ctx == NULL)
		return MuError_OOM;

	muoffice_doc_lock(page->doc->mu);

	fz_try(ctx)
		fn(ctx, page->page, arg);
	fz_catch(ctx)
		err = MuError_Generic;

	muoffice_doc_unlock(page->doc->mu);

	fz_drop_context(ctx);

	return err;
}

static void render_worker(void *arg)
{
	MuOfficeRender *render = (MuOfficeRender *)arg;
	MuOfficePage *page = render->page;
	fz_context *ctx = fz_clone_context(page->doc->ctx);
	int err = 0;
	fz_pixmap *pixmap = NULL;
	fz_device *dev = NULL;
	float scalex;
	float scaley;
	fz_rect page_bounds;
	int locked = 0;

	if (ctx == NULL)
		return;

	fz_var(pixmap);
	fz_var(dev);
	fz_var(locked);

	fz_try(ctx)
	{
		if (page->list == NULL)
		{
			muoffice_doc_lock(page->doc->mu);
			locked = 1;
			page->list = fz_new_display_list_from_page(ctx, page->page);
			locked = 0;
			muoffice_doc_unlock(page->doc->mu);
		}
		/* Make a pixmap from the bitmap */
		if (!render->area_valid)
		{
			render->area.renderArea.x = 0;
			render->area.renderArea.y = 0;
			render->area.renderArea.width = render->bitmap->width;
			render->area.renderArea.height = render->bitmap->height;
		}
		pixmap = fz_new_pixmap_with_data(ctx,
						fz_device_rgb(ctx),
						render->area.renderArea.width,
						render->area.renderArea.height,
						NULL,
						1,
						render->bitmap->lineSkip,
						((unsigned char *)render->bitmap->memptr) +
							render->bitmap->lineSkip * ((int)render->area.renderArea.x + (int)render->area.origin.x) +
							4 * ((int)render->area.renderArea.y + (int)render->area.origin.y));
		/* Be a bit clever with the scaling to make sure we get
		 * integer width/heights. First calculate the target
		 * width/height. */
		page_bounds = fz_bound_page(ctx, render->page->page);
		scalex = (int)(90 * render->zoom * (page_bounds.x1 - page_bounds.x0) / 72 + 0.5f);
		scaley = (int)(90 * render->zoom * (page_bounds.y1 - page_bounds.y0) / 72 + 0.5f);
		/* Now calculate the actual scale factors required */
		scalex /= (page_bounds.x1 - page_bounds.x0);
		scaley /= (page_bounds.y1 - page_bounds.y0);
		/* Render the list */
		fz_clear_pixmap_with_value(ctx, pixmap, 0xFF);
		dev = fz_new_draw_device(ctx, fz_post_scale(fz_translate(-page_bounds.x0, -page_bounds.y0), scalex, scaley), pixmap);
		fz_run_display_list(ctx, page->list, dev, fz_identity, fz_infinite_rect, NULL);
		fz_close_device(ctx, dev);
	}
	fz_always(ctx)
	{
		fz_drop_pixmap(ctx, pixmap);
		fz_drop_device(ctx, dev);
	}
	fz_catch(ctx)
	{
		if (locked)
			muoffice_doc_unlock(page->doc->mu);
		err = MuError_Generic;
		goto fail;
	}

fail:
	if (render->progress)
		render->progress(render->cookie, err);
	render->error = err;

	fz_drop_context(ctx);
}

/**
 * Schedule the rendering of an area of document page to
 * an area of a bitmap.
 *
 * The alignment between page and bitmap is defined by specifying
 * document's origin within the bitmap, possibly either positive or
 * negative. A render object is returned via which the process can
 * be monitored or terminated.
 *
 * The progress function is called exactly once per render in either
 * the success or failure case.
 *
 * Note that, since a render object represents a running thread that
 * needs access to the page, document, and library objects, it is important
 * to call MuOfficeRender_destroy, not only before using or deallocating
 * the bitmap, but also before calling MuOfficePage_destroy, etc..
 *
 * @param page              the page to render
 * @param zoom              the zoom factor
 * @param bitmap            the bitmap
 * @param area              area to render
 * @param progressFn        the progress callback function
 * @param cookie            a pointer to pass to the callback function
 * @param pRender           Address for return of the render object
 *
 * @return                  error indication - 0 for success
 */
MuError MuOfficePage_render(	MuOfficePage             *page,
				float                     zoom,
			const	MuOfficeBitmap           *bitmap,
			const	MuOfficeRenderArea       *area,
				MuOfficeRenderProgressFn *progressFn,
				void                     *cookie,
				MuOfficeRender          **pRender)
{
	MuOfficeRender *render;
	MuOfficeDoc *doc;
	fz_context *ctx;

	if (!pRender)
		return MuError_BadNull;
	*pRender = NULL;
	if (!page)
		return MuError_BadNull;
	doc = page->doc;
	ctx = doc->ctx;

	render = Pal_Mem_calloc(1, sizeof(*render));
	if (render == NULL)
		return MuError_OOM;

	render->page = page;
	render->zoom = zoom;
	render->bitmap = bitmap;
	if (area)
	{
		render->area = *area;
		render->area_valid = 1;
	}
	else
	{
		render->area_valid = 0;
	}
	render->progress = progressFn;
	render->cookie = cookie;

	if (mu_create_thread(&render->thread, render_worker, render))
	{
		Pal_Mem_free(render);
		return MuError_OOM;
	}

	*pRender = render;

	return MuError_OK;
}

/**
 * Destroy a render
 *
 * This call destroys a MuOfficeRender object, aborting any current
 * render.
 *
 * This call is intended to support an app dealing with a user quickly
 * flicking through document pages. A render may be scheduled but, before
 * completion, be found not to be needed. In that case the bitmap will
 * need to be reused, which requires any existing render to be aborted.
 * The call to MuOfficeRender_destroy will cut short the render and
 * allow the bitmap to be reused immediately.
 *
 * @note If an active render thread is destroyed, it will be aborted.
 * While fast, this is not an instant operation. For maximum
 * responsiveness, it is best to 'abort' as soon as you realise you
 * don't need the render, and to destroy when you get the callback.
 *
 * @param render           The render object
 */
void MuOfficeRender_destroy(MuOfficeRender *render)
{
	if (!render)
		return;

	MuOfficeRender_abort(render);
	mu_destroy_thread(&render->thread);
	Pal_Mem_free(render);
}

/**
 * Abort a render
 *
 * This call aborts any rendering currently underway. The 'render
 * complete' callback (if any) given when the render was created will
 * still be called. If a render has completed, this call will have no
 * effect.
 *
 * This call will not block to wait for the render thread to stop, but
 * will cause it to stop as soon as it can in the background.
 *
 * @note It is important not to start any new render to the same bitmap
 * until the callback comes in (or until waitUntilComplete returns), as
 * otherwise you can have multiple renders drawing to the same bitmap
 * with unpredictable consequences.
 *
 * @param render           The render object to abort
 */
void MuOfficeRender_abort(MuOfficeRender *render)
{
	if (render)
		render->mu_cookie.abort = 1;
}

/**
 * Wait for a render to complete.
 *
 * This call will not return until rendering is complete, so on return
 * the bitmap will contain the page image (assuming the render didn't
 * run into an error condition) and will not be used further by any
 * background processing. Any error during rendering will be returned
 * from this function.
 *
 * This call may block the calling thread for a significant period of
 * time. To avoid blocking, supply a progress-monitoring callback
 * function to MuOfficePage_render.
 *
 * @param render           The render object to destroy
 * @return render error condition - 0 for no error.
 */
MuError MuOfficeRender_waitUntilComplete(MuOfficeRender *render)
{
	if (!render)
		return MuError_OK;

	mu_destroy_thread(&render->thread);

	return render->error;
}
