/*
 * Copyright (c) 2006 Matthew Hall and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     Matthew Hall - initial API and implementation
 */
package net.sf.paperclips;

import java.util.ArrayList;
import java.util.List;

import net.sf.paperclips.internal.PaperClipsUtil;
import net.sf.paperclips.internal.Util;

import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.printing.Printer;
import org.eclipse.swt.printing.PrinterData;

/**
 * This class contains static constants and methods for preparing and printing
 * documents. Methods in this class supersede those in PrintUtil.
 * 
 * @author Matthew Hall
 */
public class PaperClips {
	private PaperClips() {
	} // no instances

	static boolean debug = false;

	/**
	 * Indicates that the printer's default page orientation should be used.
	 */
	public static final int ORIENTATION_DEFAULT = SWT.DEFAULT;

	/**
	 * Indicates portrait page orientation.
	 */
	public static final int ORIENTATION_PORTRAIT = SWT.VERTICAL;

	/**
	 * Indicates landscape page orientation.
	 */
	public static final int ORIENTATION_LANDSCAPE = SWT.HORIZONTAL;

	/**
	 * Triggers an appropriate exception based on the passed in error code.
	 * 
	 * @param code
	 *            the SWT error code.
	 */
	public static void error(int code) {
		error(code, null);
	}

	/**
	 * Triggers an unspecified exception with the passed in detail.
	 * 
	 * @param detail
	 *            more information about error.
	 */
	public static void error(String detail) {
		error(SWT.ERROR_UNSPECIFIED, detail);
	}

	/**
	 * Triggers an appropriate exception based on the passed in error code.
	 * 
	 * @param code
	 *            the SWT error code.
	 * @param detail
	 *            more information about error.
	 */
	public static void error(int code, String detail) {
		SWT.error(code, null, detail);
	}

	/**
	 * <b>EXPERIMENTAL</b>: Sets whether debug mode is enabled. This mode may be
	 * used for troubleshooting documents that cannot be laid out for some
	 * reason (e.g. "Cannot layout page x" error occurs).
	 * 
	 * <p>
	 * <b>THIS API IS EXPERIMENTAL AND MAY BE REMOVED OR CHANGED IN THE
	 * FUTURE.</b>
	 * 
	 * @param debug
	 *            true to enable debug mode, false to disable it.
	 */
	public static void setDebug(boolean debug) {
		PaperClips.debug = debug;
	}

	/**
	 * <b>EXPERIMENTAL</b>: Returns whether debug mode is enabled.
	 * 
	 * <p>
	 * <b>THIS API IS EXPERIMENTAL AND MAY BE REMOVED OR CHANGED IN THE
	 * FUTURE.</b>
	 * 
	 * @return whether debug mode is enabled.
	 */
	public static boolean getDebug() {
		return debug;
	}

	/**
	 * Returns a PrinterData for the system-default printer, or the first
	 * printer if no default printer is configured.
	 * 
	 * @return a PrinterData for the system-default printer, or the first
	 *         printer if no default printer is configured.
	 */
	public static PrinterData getDefaultPrinterData() {
		PrinterData printerData = Printer.getDefaultPrinterData();
		if (printerData == null) {
			// Linux may have one or more printers without a default printer
			PrinterData[] list = Printer.getPrinterList();
			if (list.length > 0)
				printerData = list[0];
		}
		return printerData;
	}

	/**
	 * Calls iterator.next(width, height) and returns the result. This method
	 * checks multiple conditions to ensure proper usage and behavior of
	 * PrintIterators.
	 * <p>
	 * This method is intended to be used by PrintIterator classes, as a
	 * results-checking alternative to calling next(int, int) directly on the
	 * target iterator. All PrintIterator classes in the PaperClips library use
	 * this method instead of directly calling the
	 * {@link PrintIterator#next(int, int)} method.
	 * 
	 * @param iterator
	 *            the PrintIterator
	 * @param width
	 *            the available width.
	 * @param height
	 *            the available height.
	 * @return the next portion of the Print, or null if the width and height
	 *         are not enough to display any of the iterator's contents.
	 */
	public static PrintPiece next(PrintIterator iterator, int width, int height) {
		Util.notNull(iterator);
		if (width < 0 || height < 0)
			error(SWT.ERROR_INVALID_ARGUMENT,
					"PrintPiece size " + width + "x" + height + " not possible"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		if (!iterator.hasNext())
			error("Iterator " + iterator + " has no more content."); //$NON-NLS-1$ //$NON-NLS-2$

		PrintPiece result = iterator.next(width, height);

		if (result != null) {
			Point size = result.getSize();
			if (size.x > width || size.y > height)
				error("Iterator " + iterator + " produced a " + size.x + "x" + size.y + " piece for a " + width //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
						+ "x" + height + " area."); //$NON-NLS-1$//$NON-NLS-2$
		} else if (debug) {
			return new NullPrintPiece();
		}
		return result;
	}

	/**
	 * Prints the print job to the given printer. This method constructs a
	 * Printer, forwards to {@link #print(PrintJob, Printer)}, and disposes the
	 * printer before returning.
	 * 
	 * @param printJob
	 *            the print job.
	 * @param printerData
	 *            the PrinterData of the selected printer.
	 */
	public static void print(PrintJob printJob, PrinterData printerData) {
		Printer printer = new Printer(printerData);
		try {
			print(printJob, printer);
		} finally {
			printer.dispose();
		}
	}

	/**
	 * Prints the print job to the given printer.
	 * 
	 * @param printJob
	 *            the print job.
	 * @param printer
	 *            the printer device.
	 */
	public static void print(PrintJob printJob, Printer printer) {
		// Bug in SWT on OSX: If Printer.startJob() is not called first, the GC
		// will be disposed by
		// default.
		startJob(printer, printJob.getName());

		boolean completed = false;
		try {
			GC gc = createAndConfigureGC(printer);
			try {
				print(printJob, printer, gc);
			} finally {
				gc.dispose();
			}
			printer.endJob();
			completed = true;
		} finally {
			if (!completed)
				cancelJob(printer);
		}
	}

	private static void startJob(Printer printer, String jobName) {
		if (!printer.startJob(jobName))
			error("Unable to start print job"); //$NON-NLS-1$
	}

	private static void cancelJob(Printer printer) {
		if (isGTK())
			printer.endJob(); // Printer.cancelJob() not implemented on GTK
		else
			printer.cancelJob();
	}

	private static GC createAndConfigureGC(Printer printer) {
		GC gc = new GC(printer);
		gc.setAdvanced(true);
		return gc;
	}

	/**
	 * Prints the print job to the specified printer using the GC. This method
	 * does not manage the print job lifecycle (it does not call startJob or
	 * endJob).
	 * 
	 * @param printJob
	 *            the print job
	 * @param printer
	 *            the printer
	 * @param gc
	 *            the GC
	 */
	private static void print(PrintJob printJob, Printer printer, final GC gc) {
		final PrinterData printerData = printer.getPrinterData();

		PrintPiece[] pages = getPages(printJob, printer, gc);

		int startPage = 0;
		int endPage = pages.length - 1;
		if (printerData.scope == PrinterData.PAGE_RANGE) {
			// Convert from PrinterData's one-based indices to our zero-based
			// indices
			startPage = Math.max(startPage, printerData.startPage - 1);
			endPage = Math.min(endPage, printerData.endPage - 1);
		}

		final int collatedCopies;
		final int noncollatedCopies;
		if (printerData.collate) { // always false if printer driver performs
			// collation
			collatedCopies = printerData.copyCount; // always 1 if printer
			// driver handles copy count
			noncollatedCopies = 1;
		} else {
			noncollatedCopies = printerData.copyCount; // always 1 if printer
			// driver handles copy
			// count
			collatedCopies = 1;
		}

		printPages(printer, gc, pages, startPage, endPage, collatedCopies,
				noncollatedCopies);
	}

	private static void printPages(final Printer printer, final GC gc,
			final PrintPiece[] pages, final int startPage, final int endPage,
			final int collatedCopies, final int noncollatedCopies) {
		disposeUnusedPages(pages, startPage, endPage);

		Rectangle paperBounds = getPaperBounds(printer);
		final int x = paperBounds.x;
		final int y = paperBounds.y;

		try {
			for (int collated = 0; collated < collatedCopies; collated++) {
				for (int pageIndex = startPage; pageIndex <= endPage; pageIndex++) {
					for (int noncollated = 0; noncollated < noncollatedCopies; noncollated++) {
						if (printer.startPage()) {
							pages[pageIndex].paint(gc, x, y);
							pages[pageIndex].dispose();
							printer.endPage();
						} else {
							error("Unable to start page " + pageIndex); //$NON-NLS-1$
						}
					}
				}
			}
		} finally {
			PaperClipsUtil.dispose(pages);
		}
	}

	private static void disposeUnusedPages(PrintPiece[] pages, int startPage,
			int endPage) {
		PaperClipsUtil.dispose(pages, 0, startPage);
		PaperClipsUtil.dispose(pages, endPage + 1, pages.length);
	}

	/**
	 * Processes the print job and returns an array of pages for the given
	 * printer device. Each element in the returned array has already had the
	 * page orientation and page margins applied. Therefore, when calling the
	 * paint(GC, int, int) method on each page, the printer's trim should be
	 * provided as the x and y arguments. In other words, the trim is taken as a
	 * minimum margin while applying calculating margins, but the position where
	 * the page's content is drawn is determined solely by the margin, and is
	 * not offset by the trim. This behavior is helpful for screen display, and
	 * is already compensated for in the {@link #print(PrintJob, Printer)}
	 * method.
	 * 
	 * @param printer
	 *            the printing device.
	 * @param printJob
	 *            the print job.
	 * @return an array of all pages of the print job. Each element of the
	 *         returned array represents one page in the printed document.
	 */
	public static PrintPiece[] getPages(PrintJob printJob, Printer printer) {
		startDummyJob(printer, printJob.getName());

		try {
			GC gc = createAndConfigureGC(printer);
			try {
				return getPages(printJob, printer, gc);
			} finally {
				gc.dispose();
			}
		} finally {
			endDummyJob(printer);
		}
	}

	/**
	 * Starts a dummy job on the given Printer if the platform requires it.
	 * Dummy jobs allow the various Print components of PaperClips to perform
	 * measurements required for document layout, without actually sending a job
	 * to the printer. Only Mac OS X Carbon and Linux GTK+ are known to require
	 * dummy jobs.
	 * 
	 * @param printer
	 *            the Printer hosting the dummy print job.
	 * @param name
	 *            the name of the dummy print job.
	 */
	public static void startDummyJob(Printer printer, String name) {
		// On Mac OS X Carbon and Linux GTK+, created GC is disposed unless
		// Printer.startJob() is called
		// first.
		if (isCarbon() || isGTK())
			startJob(printer, name);
	}

	/**
	 * Ends a dummy job on the given Printer if the platform requires a dummy
	 * job.
	 * 
	 * @param printer
	 *            the Printer hosting the dummy print job.
	 */
	public static void endDummyJob(Printer printer) {
		if (isGTK()) { // Linux GTK
			// Printer.cancelJob() is not implemented in SWT since GTK has no
			// API for cancelling a print
			// job.
			// For now we must use endJob(), even though it spits out an empty
			// page.
			// http://sourceforge.net/tracker/index.php?func=detail&aid=1773091&group_id=148509&atid=771872

			// printer.cancelJob(); // Not implemented in SWT on GTK
			printer.endJob();

			// See also:
			// http://bugzilla.gnome.org/show_bug.cgi?id=339323
			// https://bugs.eclipse.org/bugs/show_bug.cgi?id=212594
		} else if (isCarbon()) // Mac OSX
			// 2007-04-30: A bug in SWT on Mac OSX prior to 3.3 renders Printer
			// instances useless after a call to cancelJob().
			// Therefore on Mac OSX we call endJob() instead of cancelJob().
			if (SWT.getVersion() < 3346) { // Version 3.3
				printer.endJob();
			} else {
				printer.cancelJob();
			}
	}

	private static boolean isCarbon() {
		return SWT.getPlatform().equals("carbon"); //$NON-NLS-1$
	}

	private static boolean isGTK() {
		return SWT.getPlatform().equals("gtk"); //$NON-NLS-1$
	}

	private static PrintPiece[] getPages(PrintJob printJob, Printer printer,
			GC gc) {
		PageEnumeration enumeration = new PageEnumeration(printJob, printer, gc);
		List pages = new ArrayList();
		while (enumeration.hasNext()) {
			PrintPiece page = enumeration.nextPage();
			if (page == null) {
				int pageNumber = pages.size() + 1;
				PaperClipsUtil.dispose(pages);
				error("Unable to layout page " + pageNumber); //$NON-NLS-1$
			}
			pages.add(page);
		}

		return (PrintPiece[]) pages.toArray(new PrintPiece[pages.size()]);
	}

	/**
	 * Returns a {@link PageEnumeration} for the passed in PrintJob on the given
	 * Printer, using the given GC. The Printer and GC must not be disposed
	 * while the enumeration is in use.
	 * 
	 * @param printJob
	 *            the print job
	 * @param printer
	 *            the Printer device, which must not be disposed while the
	 *            PageEnumeration is in use.
	 * @param gc
	 *            the GC, which must not be disposed while the PageEnumeration
	 *            is in use.
	 * @return a {@link PageEnumeration} for the passed in PrintJob.
	 */
	public static PageEnumeration getPageEnumeration(PrintJob printJob,
			Printer printer, GC gc) {
		return new PageEnumeration(printJob, printer, gc);
	}

	/**
	 * Returns the bounding rectangle of the paper, including non-printable
	 * margins.
	 * 
	 * @param printer
	 *            the printer device.
	 * @return a rectangle whose edges correspond to the edges of the paper.
	 */
	public static Rectangle getPaperBounds(Printer printer) {
		Rectangle rect = getPrintableBounds(printer);
		return printer.computeTrim(rect.x, rect.y, rect.width, rect.height);
	}

	/**
	 * Returns the bounding rectangle of the printable area on the paper.
	 * 
	 * @param printer
	 *            the printer device.
	 * @return the bounding rectangle of the printable area on the paper.
	 */
	public static Rectangle getPrintableBounds(Printer printer) {
		return printer.getClientArea();
	}

	/**
	 * Returns the bounding rectangle of the printable area which is inside the
	 * given margins on the paper. The printer's minimum margins are reflected
	 * in the returned rectangle.
	 * 
	 * @param printer
	 *            the printer device.
	 * @param margins
	 *            the desired page margins.
	 * @return the bounding rectangle on the printable area which is within the
	 *         margins.
	 */
	public static Rectangle getMarginBounds(Margins margins, Printer printer) {
		Rectangle paperBounds = getPaperBounds(printer);

		// Calculate the pixel coordinates for the margins
		Point dpi = printer.getDPI();
		int top = paperBounds.y + (margins.top * dpi.y / 72);
		int left = paperBounds.x + (margins.left * dpi.x / 72);
		int right = paperBounds.x + paperBounds.width
				- (margins.right * dpi.x / 72);
		int bottom = paperBounds.y + paperBounds.height
				- (margins.bottom * dpi.y / 72);

		// Enforce the printer's minimum margins.
		Rectangle printableBounds = getPrintableBounds(printer);
		if (top < printableBounds.y)
			top = printableBounds.y;
		if (left < printableBounds.x)
			left = printableBounds.x;
		if (right > printableBounds.x + printableBounds.width)
			right = printableBounds.x + printableBounds.width;
		if (bottom > printableBounds.y + printableBounds.height)
			bottom = printableBounds.y + printableBounds.height;

		return new Rectangle(left, top, right - left, bottom - top);
	}
}