// Copyright (C) 2004-2022 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.

package example;

import com.artifex.mupdf.fitz.*;

import java.io.File;
import java.util.Vector;

public class ViewerCore {

	protected Worker worker;
	protected Callback callback;

	protected String documentPath;
	protected String acceleratorPath;

	protected Document doc;
	protected Page page;

	protected Location currentPage;
	protected Location searchHitPage;
	protected String searchNeedle;

	protected OutlineItem[] outline;

	protected boolean cancelSearch;

	public ViewerCore(Worker worker, Callback callback, String documentPath) {
		this.worker = worker;
		this.callback = callback;
		this.documentPath = documentPath;
	}

	public void openDocument(final OnException onException) {
		worker.add(new Worker.Task() {
			Document doc = null;
			String acceleratorPath = null;
			boolean needsPassword = false;
			protected String getAcceleratorPath(String documentPath) {
				try {
					String accelerator = new File(documentPath).
						getCanonicalFile().
						getPath().
						replace(File.separatorChar, '%').
						replace('\\', '%').
						replace('/', '%').
						replace(':', '%') + ".accel";
					String tmpdir = System.getProperty("java.io.tmpdir");
					return new File(tmpdir, accelerator).getCanonicalFile().getPath();
				} catch (Exception e) {
					return null;
				}
			}
			protected boolean acceleratorValid(String documentPath, String acceleratorPath) {
				try {
					long documentModified = new File(documentPath).lastModified();
					long acceleratorModified = new File(acceleratorPath).lastModified();
					return acceleratorModified != 0 && acceleratorModified > documentModified;
				} catch (Exception e) {
					return false;
				}
			}
			public void work() {
				String acceleratorPath = getAcceleratorPath(documentPath);
				if (!acceleratorValid(documentPath, acceleratorPath))
					acceleratorPath = null;
				if (acceleratorPath != null)
					doc = Document.openDocument(documentPath, acceleratorPath);
				else
					doc = Document.openDocument(documentPath);
				needsPassword = doc.needsPassword();
			}
			public void run() {
				ViewerCore.this.doc = doc;
				ViewerCore.this.acceleratorPath = acceleratorPath;
				if (needsPassword) {
					String password = callback.askPassword();
					if (password != null)
						checkPassword(password, onException);
				}
				else
					loadDocument(onException);
			}
			public void exception(Throwable t) {
				ViewerCore.this.acceleratorPath = null;
				ViewerCore.this.doc = null;
				if (onException != null)
					onException.run(t);
			}
		});
	}

	public void reloadDocument(final OnException onException) {
		acceleratorPath = null;
		doc = null;
		page = null;
		searchHitPage = null;
		searchNeedle = null;
		outline = null;
		cancelSearch = false;
		openDocument(onException);
	}

	protected void checkPassword(final String password, final OnException onException) {
		worker.add(new Worker.Task() {
			boolean passwordOK = false;
			public void work() {
				passwordOK = doc.authenticatePassword(password);
			}
			public void run() {
				if (!passwordOK) {
					String password = callback.askPassword();
					if (password != null)
						checkPassword(password, onException);
				}
				else
					loadDocument(onException);
			}
			public void exception(Throwable t) {
				ViewerCore.this.documentPath = null;
				ViewerCore.this.acceleratorPath = null;
				ViewerCore.this.doc = null;
				if (onException != null)
					onException.run(t);
			}
		});
	}

	public void refreshMetadata(final OnException onException) {
		worker.add(new Worker.Task() {
			String title = "";
			String author = "";
			String format = "";
			String encryption = "";
			boolean print = false;
			boolean edit = false;
			boolean copy = false;
			boolean annotate = false;
			boolean isPDF = false;
			boolean reflowable = false;
			boolean linear = false;
			int updates = 0;
			int history = 0;
			public void work() {
				title = doc.getMetaData(Document.META_INFO_TITLE);
				author = doc.getMetaData(Document.META_INFO_AUTHOR);
				format = doc.getMetaData(Document.META_FORMAT);
				encryption = doc.getMetaData(Document.META_ENCRYPTION);
				print = doc.hasPermission(Document.PERMISSION_PRINT);
				copy = doc.hasPermission(Document.PERMISSION_COPY);
				edit = doc.hasPermission(Document.PERMISSION_EDIT);
				annotate = doc.hasPermission(Document.PERMISSION_ANNOTATE);
				reflowable = doc.isReflowable();
				isPDF = doc.isPDF();
				if (isPDF) {
					PDFDocument pdf = (PDFDocument) doc;
					linear = pdf.wasLinearized();
					updates = pdf.countVersions();
					history = pdf.validateChangeHistory();
				}
			}
			public void run() {
				callback.onMetadataChange(title, author, format, encryption);
				callback.onPermissionsChange(print, copy, edit, annotate);
				callback.onReflowableChange(reflowable);
				if (isPDF) {
					callback.onLinearizedChange(linear);
					callback.onUpdatesChange(updates, history);
				}
			}
			public void exception(Throwable t) {
				if (onException != null)
					onException.run(t);
			}
		});
	}

	protected void loadDocument(final OnException onException) {
		worker.add(new Worker.Task() {
			int chapterCount = 0;
			int pageCount = 0;
			public void work() {
				chapterCount = doc.countChapters();
				pageCount = doc.countPages();
				if (acceleratorPath != null)
					doc.saveAccelerator(acceleratorPath);
			}
			public void run() {
				callback.onChapterCountChange(chapterCount);
				callback.onPageCountChange(pageCount);
				loadOutline(onException);
				refreshMetadata(onException);
				if (currentPage != null)
					gotoLocation(currentPage, onException);
				else
					gotoFirstPage(onException);
			}
			public void exception(Throwable t) {
				if (onException != null)
					onException.run(t);
			}
		});
	}

	public void relayoutDocument(final int width, final int height, final int em, final OnException onException) {
		worker.add(new Worker.Task() {
			int chapterCount = 0;
			int pageCount = 0;
			Location newPage;
			public void work() {
				long mark = doc.makeBookmark(ViewerCore.this.currentPage);
				doc.layout(width, height, em);
				chapterCount = doc.countChapters();
				pageCount = doc.countPages();
				newPage = doc.findBookmark(mark);
			}
			public void run() {
				callback.onLayoutChange(width, height, em);
				callback.onChapterCountChange(chapterCount);
				callback.onPageCountChange(pageCount);
				gotoLocation(newPage, onException);
				loadOutline(onException);
			}
			public void exception(Throwable t) {
				callback.onLayoutChange(450, 600, 12);
				callback.onChapterCountChange(0);
				callback.onPageCountChange(0);
				gotoLocation(null, onException);
				if (onException != null)
					onException.run(t);
			}
		});
	}

	protected void loadOutline(final OnException onException) {
		worker.add(new Worker.Task() {
			OutlineItem[] outline = null;
			protected void flattenOutline(Outline[] rawOutline, String indent, Vector<OutlineItem> v) {
				for (Outline node : rawOutline) {
					if (node.title != null) {
						Location loc = doc.resolveLink(node);
						String title = indent + node.title;
						v.add(new OutlineItem(title, node.uri, loc));
					}
					if (node.down != null)
						flattenOutline(node.down, indent + "    ", v);
				}
			}
			public void work() {
				Outline[] rawOutline = doc.loadOutline();
				if (rawOutline != null) {
					Vector<OutlineItem> v = new Vector<OutlineItem>();
					flattenOutline(rawOutline, "", v);

					outline = new OutlineItem[v.size()];
					v.toArray(outline);
				}
			}
			public void run() {
				ViewerCore.this.outline = outline;
				callback.onOutlineChange(outline);
			}
			public void exception(Throwable t) {
				callback.onOutlineChange(null);
				if (onException != null)
					onException.run(t);
			}
		});
	}

	public void gotoLocation(final Location location, final OnException onException) {
		worker.add(new Worker.Task() {
			int itemIndex = -1;
			Page page = null;
			Rect bbox = null;
			int chapterNumber = 0;
			int pageNumber = 0;
			public void work() {
				if (location == null)
					return;

				chapterNumber = location.chapter;
				pageNumber = doc.pageNumberFromLocation(location);

				if (outline != null) {
					for (int i = 0; i < outline.length; ++i) {
						Location loc = outline[i].location;
						int outlinePageNumber = doc.pageNumberFromLocation(loc);
						if (outlinePageNumber <= pageNumber)
							itemIndex = i;
					}
				}

				page = doc.loadPage(location);
				bbox = page.getBounds();
			}
			public void run() {
				ViewerCore.this.page = page;
				ViewerCore.this.currentPage = location;
				if (currentPage == null || !currentPage.equals(searchHitPage))
					ViewerCore.this.searchHitPage = null;
				callback.onOutlineItemChange(itemIndex);
				callback.onPageChange(location, chapterNumber, pageNumber, bbox);
			}
			public void exception(Throwable t) {
				ViewerCore.this.page = null;
				ViewerCore.this.currentPage = null;
				ViewerCore.this.searchHitPage = null;
				callback.onOutlineItemChange(-1);
				if (onException != null)
					onException.run(t);
			}
		});
	}

	public void flipPages(final int flips, final OnException onException) {
		worker.add(new Worker.Task() {
			Location location = currentPage;
			public void work() {
				int page = doc.pageNumberFromLocation(location);
				int newPage = page + flips;
				location = doc.locationFromPageNumber(newPage);
			}
			public void run() {
				gotoLocation(location, onException);
			}
			public void exception(Throwable t) {
				if (onException != null)
					onException.run(t);
			}
		});
	}

	public void gotoFirstPage(final OnException onException) {
		worker.add(new Worker.Task() {
			Location location;
			public void work() {
				location = doc.locationFromPageNumber(0);
			}
			public void run() {
				gotoLocation(location, onException);
			}
			public void exception(Throwable t) {
				if (onException != null)
					onException.run(t);
			}
		});
	}

	public void gotoLastPage(final OnException onException) {
		worker.add(new Worker.Task() {
			Location location;
			public void work() {
				location = doc.lastPage();
			}
			public void run() {
				gotoLocation(location, onException);
			}
			public void exception(Throwable t) {
				if (onException != null)
					onException.run(t);
			}
		});
	}

	public void gotoPage(final int pageNumber, final OnException onException) {
		worker.add(new Worker.Task() {
			Location location;
			public void work() {
				location = doc.locationFromPageNumber(pageNumber);
			}
			public void run() {
				gotoLocation(location, onException);
			}
			public void exception(Throwable t) {
				if (onException != null)
					onException.run(t);
			}
		});
	}

	public void renderPage(final Matrix ctm, final Rect bbox, final boolean icc, final int antialias, final boolean invert, final boolean tint, final int tintBlack, final int tintWhite, final OnException onException) {
		worker.add(new Worker.Task() {
			Pixmap pixmap = null;
			Link[] links = null;
			Quad[][] hits = null;
			public void work() {
				links = page.getLinks();
				if (links == null)
					links = new Link[0];
				for (Link link: links)
					link.bounds.transform(ctm);

				if (currentPage.equals(searchHitPage))
					hits = page.search(searchNeedle);
				if (hits == null)
					hits = new Quad[0][];
				for (Quad[] hit : hits)
					for (Quad q : hit)
						q.transform(ctm);

				pixmap = new Pixmap(ColorSpace.DeviceBGR, bbox, true);
				pixmap.clear(255);

				if (icc)
					Context.enableICC();
				else
					Context.disableICC();
				Context.setAntiAliasLevel(antialias);

				DrawDevice dev = new DrawDevice(pixmap);
				page.run(dev, ctm, null);
				dev.close();
				dev.destroy();

				if (invert) {
					pixmap.invertLuminance();
					pixmap.gamma(1 / 1.4f);
				}

				if (tint)
					pixmap.tint(tintBlack, tintWhite);
			}
			public void run() {
				callback.onPageContentsChange(pixmap, links, hits);
			}
			public void exception(Throwable t) {
				callback.onPageContentsChange(null, new Link[0], new Quad[0][]);
				if (onException != null)
					onException.run(t);
			}
		});
	}

	public void search(final String needle, final int direction, final OnException onException) {
		cancelSearch = false;
		if (doc == null)
			return;
		worker.add(new Worker.Task() {
			Location startPage, finalPage;
			public void work() {
				if (!currentPage.equals(searchHitPage))
					startPage = currentPage;
				else if (direction >= 0)
					startPage = doc.nextPage(currentPage);
				else
					startPage = doc.previousPage(currentPage);

				if (direction >= 0)
					finalPage = doc.lastPage();
				else
					finalPage = doc.locationFromPageNumber(0);
			}
			public void run() {
				callback.onSearchStart(startPage, finalPage, direction, needle);
				ViewerCore.this.searchNeedle = needle;
				runSearch(startPage, finalPage, direction, needle, onException);
			}
			public void exception(Throwable t) {
				if (onException != null)
					onException.run(t);
			}
		});
	}

	public void cancelSearch(final OnException onException) {
		cancelSearch = true;
	}

	public void runSearch(final Location startPage, final Location finalPage, final int direction, final String needle, final OnException onException) {
		worker.add(new Worker.Task() {
			Location searchPage = startPage;
			Location hitPage = null;
			Quad[][] hits = new Quad[0][];
			boolean done = false;
			boolean cancelled = false;
			public void work() {
				long executionUntil = System.currentTimeMillis() + 100;
				do {
					if (cancelSearch) {
						cancelled = true;
						continue;
					}

					Page page = doc.loadPage(searchPage);
					hits = page.search(needle);
					if (hits != null && hits.length > 0) {
						hitPage = searchPage;
						done = true;
						continue;
					}
					page.destroy();

					if (searchPage.equals(finalPage)) {
						done = true;
					}
					else if (direction >= 0)
						searchPage = doc.nextPage(searchPage);
					else
						searchPage = doc.previousPage(searchPage);
				} while (!cancelled && !done && System.currentTimeMillis() < executionUntil);
			}
			public void run() {
				if (cancelled) {
					ViewerCore.this.searchNeedle = null;
					ViewerCore.this.searchHitPage = null;
					callback.onSearchCancelled();
				} else if (done) {
					ViewerCore.this.searchHitPage = hitPage;
					callback.onSearchStop(needle, hitPage);
				} else {
					callback.onSearchPage(searchPage, needle);
					worker.add(this);
				}
			}
			public void exception(Throwable t) {
				callback.onSearchStop(needle, null);
				if (onException != null)
					onException.run(t);
			}
		});
	}

	public void save(final String path, final String options, final DocumentWriter.OCRListener ocrListener, final OnException onException) {
		worker.add(new Worker.Task() {
			public void work() {
				PDFDocument pdf = (PDFDocument) doc;
				pdf.save(path, options);
			}
			public void run() {
				callback.onSaveComplete();
			}
			public void exception(Throwable t) {
				if (onException != null)
					onException.run(t);
			}
		});
	}

	public void save(final SeekableOutputStream stream, final String options, final DocumentWriter.OCRListener ocrListener, final OnException onException) {
		worker.add(new Worker.Task() {
			public void work() {
				PDFDocument pdf = (PDFDocument) doc;
				DocumentWriter wri = null;
				int pageCount = doc.countPages();
				try {
					wri = new DocumentWriter(stream, "ocr", "");
					wri.addOCRListener(ocrListener);

					for (int i = 0; i < pageCount; i++)
					{
						Page page = pdf.loadPage(i);
						Rect bounds = page.getBounds();
						Device dev = wri.beginPage(bounds);
						page.run(dev, new Matrix());
						wri.endPage();
					}

					wri.close();
				} finally {
					if (wri != null) wri.destroy();
				}
			}
			public void run() {
				callback.onSaveComplete();
			}
			public void exception(Throwable t) {
				if (onException != null)
					onException.run(t);
			}
		});
	}

	public interface Callback {
		public String askPassword();

		public void onChapterCountChange(int chapters);
		public void onLayoutChange(int width, int height, int em);
		public void onLinearizedChange(boolean linearized);
		public void onMetadataChange(String title, String author, String format, String encryption);
		public void onOutlineChange(OutlineItem[] outline);
		public void onOutlineItemChange(int index);
		public void onPageChange(Location page, int chapterNumber, int pageNumber, Rect bbox);
		public void onPageContentsChange(Pixmap pixmap, Link[] links, Quad[][] searchHits);
		public void onPageCountChange(int pages);
		public void onPermissionsChange(boolean print, boolean copy, boolean edit, boolean annotate);
		public void onReflowableChange(boolean reflowable);
		public void onSaveComplete();
		public void onSearchCancelled();
		public void onSearchPage(Location page, String needle);
		public void onSearchStart(Location startPage, Location finalPage, int direction, String needle);
		public void onSearchStop(String needle, Location page);
		public void onUpdatesChange(int update, int history);
	}

	public interface OnException {
		public void run(Throwable t);
	}

	protected static class OutlineItem {
		protected String title;
		protected String uri;
		protected Location location;
		public OutlineItem(String title, String uri, Location location) {
			this.title = title;
			this.uri = uri;
			this.location = location;
		}
		public String toString() {
			return title;
		}
	}
}
