/*
 * Copyright (c) 2005 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.PrintSizeStrategy;
import net.sf.paperclips.internal.Util;

import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Device;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;

/**
 * A decorator Print which displays page headers and footers around a document
 * body, with page numbering capabilities.
 * <p>
 * PagePrint is horizontally and vertically greedy. Greedy prints take up all
 * the available space on the page.
 * <p>
 * <b>Note:</b> Avoid wrapping PagePrint in prints with space-optimizing
 * semantics (e.g. ColumnPrint equalizes columns on the last page), as this may
 * cause the total page count to be incorrect on some pages. At this time there
 * is no known fix. If wrapping a PagePrint is unavoidable, consider using a
 * custom PageNumberFormat which does not display the total page count.
 * 
 * @author Matthew Hall
 */
public class PagePrint implements Print {
	private static final int DEFAULT_GAP = 1;

	PageDecoration header;
	int headerGap = DEFAULT_GAP; // in points
	Print body;
	int footerGap = DEFAULT_GAP; // in points
	PageDecoration footer;

	/**
	 * Constructs a PagePrint with the given header and body.
	 * 
	 * @param header
	 *            a PageDecoration for creating the header. May be null.
	 * @param headerGap
	 *            the gap between the header and body, in points.
	 * @param body
	 *            the Print being decorated.
	 */
	public PagePrint(PageDecoration header, int headerGap, Print body) {
		this(header, headerGap, body, DEFAULT_GAP, null);
	}

	/**
	 * Constructs a PagePrint with the given header and body.
	 * 
	 * @param body
	 *            the Print being decorated.
	 * @param header
	 *            a PageDecoration for creating the header. May be null.
	 */
	public PagePrint(PageDecoration header, Print body) {
		this(header, DEFAULT_GAP, body);
	}

	/**
	 * Constructs a PagePrint with the given body.
	 * 
	 * @param body
	 *            the Print being decorated.
	 */
	public PagePrint(Print body) {
		this(null, body, null);
	}

	/**
	 * Constructs a PagePrint with the given body and footer.
	 * 
	 * @param body
	 *            the Print being decorated.
	 * @param footer
	 *            a PageDecoration for creating the footer. may be null.
	 */
	public PagePrint(Print body, PageDecoration footer) {
		this(body, DEFAULT_GAP, footer);
	}

	/**
	 * Constructs a PagePrint with the given body, header and footer.
	 * 
	 * @param body
	 *            the Print being decorated.
	 * @param footerGap
	 *            the gap between the body and footer, in points.
	 * @param footer
	 *            a PageDecoration for creating the footer. May be null.
	 */
	public PagePrint(Print body, int footerGap, PageDecoration footer) {
		this(null, DEFAULT_GAP, body, footerGap, footer);
	}

	/**
	 * Constructs a PagePrint with the given body, header and footer.
	 * 
	 * @param body
	 *            the Print being decorated.
	 * @param header
	 *            a PageDecoration for creating the header. May be null.
	 * @param footer
	 *            a PageDecoration for creating the footer. may be null.
	 * @deprecated PagePrint(PageDecoration, Print, PageDecoration) instead.
	 */
	public PagePrint(Print body, PageDecoration header, PageDecoration footer) {
		this(header, body, footer);
	}

	/**
	 * Constructs a PagePrint with the given body, header and footer.
	 * 
	 * @param header
	 *            a PageDecoration for creating the header. May be null.
	 * @param body
	 *            the Print being decorated.
	 * @param footer
	 *            a PageDecoration for creating the footer. may be null.
	 */
	public PagePrint(PageDecoration header, Print body, PageDecoration footer) {
		this(header, DEFAULT_GAP, body, DEFAULT_GAP, footer);
	}

	/**
	 * Constructs a PagePrint with the given body, header and footer.
	 * 
	 * @param header
	 *            a PageDecoration for creating the header. May be null.
	 * @param headerGap
	 *            the gap between the header and body, in points.
	 * @param body
	 *            the Print being decorated.
	 * @param footerGap
	 *            the gap between the body and footer, in points.
	 * @param footer
	 *            a PageDecoration for creating the footer. May be null.
	 */
	public PagePrint(PageDecoration header, int headerGap, Print body,
			int footerGap, PageDecoration footer) {
		setHeader(header);
		setHeaderGap(headerGap);
		setBody(body);
		setFooterGap(footerGap);
		setFooter(footer);
	}

	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((body == null) ? 0 : body.hashCode());
		result = prime * result + ((footer == null) ? 0 : footer.hashCode());
		result = prime * result + footerGap;
		result = prime * result + ((header == null) ? 0 : header.hashCode());
		result = prime * result + headerGap;
		return result;
	}

	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		PagePrint other = (PagePrint) obj;
		if (body == null) {
			if (other.body != null)
				return false;
		} else if (!body.equals(other.body))
			return false;
		if (footer == null) {
			if (other.footer != null)
				return false;
		} else if (!footer.equals(other.footer))
			return false;
		if (footerGap != other.footerGap)
			return false;
		if (header == null) {
			if (other.header != null)
				return false;
		} else if (!header.equals(other.header))
			return false;
		if (headerGap != other.headerGap)
			return false;
		return true;
	}

	/**
	 * Returns the page header.
	 * 
	 * @return the page header.
	 */
	public PageDecoration getHeader() {
		return header;
	}

	/**
	 * Sets the page header to the argument.
	 * 
	 * @param header
	 *            a PageDecoration which creates the header. May be null.
	 */
	public void setHeader(PageDecoration header) {
		this.header = header;
	}

	/**
	 * Returns the gap between the header and body, expressed in points.
	 * 
	 * @return the gap between the header and body, expressed in points.
	 */
	public int getHeaderGap() {
		return headerGap;
	}

	/**
	 * Sets the gap between the header and body to the argument, expressed in
	 * points.
	 * 
	 * @param points
	 *            the new gap between the header and body, expressed in points.
	 *            72 points = 1".
	 */
	public void setHeaderGap(int points) {
		this.headerGap = checkGap(points);
	}

	/**
	 * Returns the page body.
	 * 
	 * @return the page body.
	 */
	public Print getBody() {
		return body;
	}

	/**
	 * Sets the page body to the argument.
	 * 
	 * @param body
	 *            the new page body.
	 */
	public void setBody(Print body) {
		Util.notNull(body);
		this.body = body;
	}

	/**
	 * Returns the page footer.
	 * 
	 * @return the page footer.
	 */
	public PageDecoration getFooter() {
		return footer;
	}

	/**
	 * Sets the page footer to the argument.
	 * 
	 * @param footer
	 *            a PageDecoration which creates the footer. May be null.
	 */
	public void setFooter(PageDecoration footer) {
		this.footer = footer;
	}

	/**
	 * Returns the gap between the body and footer, expressed in points.
	 * 
	 * @return the gap between the body and footer, expressed in points.
	 */
	public int getFooterGap() {
		return footerGap;
	}

	/**
	 * Sets the gap between the body and footer to the argument, expressed in
	 * points.
	 * 
	 * @param points
	 *            the new gap between the body and footer (if there is a
	 *            footer).
	 */
	public void setFooterGap(int points) {
		this.footerGap = checkGap(points);
	}

	private static int checkGap(int gap) {
		if (gap < 0)
			PaperClips.error(SWT.ERROR_INVALID_ARGUMENT,
					"Gap must be >= 0 (value is " + gap + ")"); //$NON-NLS-1$ //$NON-NLS-2$
		return gap;
	}

	public PrintIterator iterator(Device device, GC gc) {
		if (header == null && footer == null)
			return body.iterator(device, gc);

		return new PageIterator(this, device, gc);
	}
}

class PageIterator implements PrintIterator {
	class PageNumberer {
		int pageCount = 0;

		synchronized PageNumber next() {
			return new InnerPageNumber();
		}

		class InnerPageNumber implements PageNumber {
			final int pageNumber = pageCount++; // POST-increment

			public int getPageCount() {
				return pageCount;
			}

			public int getPageNumber() {
				return pageNumber;
			}
		}

		PageNumberer copy() {
			PageNumberer result = new PageNumberer();
			result.pageCount = this.pageCount;
			return result;
		}
	}

	final Device device;
	final GC gc;

	final PageDecoration header;
	final int headerGap; // pixels
	final PrintIterator body;
	final int footerGap; // pixels
	final PageDecoration footer;

	final PageNumberer numberer;

	final Point minimumSize;
	final Point preferredSize;

	PageIterator(PagePrint print, Device device, GC gc) {
		this.device = device;
		this.gc = gc;

		Point dpi = device.getDPI();

		body = print.body.iterator(device, gc);
		header = print.header;
		headerGap = header == null ? 0 : print.headerGap * dpi.y / 72;
		footer = print.footer;
		footerGap = footer == null ? 0 : print.footerGap * dpi.y / 72;

		this.numberer = new PageNumberer();

		this.minimumSize = computeSize(PrintSizeStrategy.MINIMUM);
		this.preferredSize = computeSize(PrintSizeStrategy.PREFERRED);
	}

	PageIterator(PageIterator that) {
		this.device = that.device;
		this.gc = that.gc;

		this.body = that.body.copy();
		this.header = that.header;
		this.headerGap = that.headerGap;
		this.footer = that.footer;
		this.footerGap = that.footerGap;

		// FIXME: Wrapping PagePrint in a class with space-optimizing semantics
		// (ColumnPrint) can fork the total
		// page count. i.e. if the copied PageIterator is chosen as the optimal
		// layout, then previous pages will
		// have a page number spawned from a different page numberer. Thus the
		// total page count for those
		// previous pages will no longer be incremented with each new page.
		this.numberer = that.numberer.copy();
		this.pageNumber = that.pageNumber;

		this.minimumSize = that.minimumSize;
		this.preferredSize = that.preferredSize;
	}

	private Point computeSize(PrintSizeStrategy strategy) {
		Point size = strategy.computeSize(body);

		PageNumber samplePageNumber = new PageNumber() {
			public int getPageCount() {
				return 1;
			}

			public int getPageNumber() {
				return 0;
			}
		};

		if (header != null) {
			Print headerPrint = header.createPrint(samplePageNumber);
			if (headerPrint != null) {
				PrintIterator iter = headerPrint.iterator(device, gc);

				size.y += headerGap;

				Point headerSize = strategy.computeSize(iter);
				size.x = Math.max(size.x, headerSize.x);
				size.y += headerSize.y;
			}
		}

		if (footer != null) {
			Print footerPrint = footer.createPrint(samplePageNumber);
			if (footerPrint != null) {
				PrintIterator iter = footerPrint.iterator(device, gc);

				size.y += footerGap;

				Point footerSize = strategy.computeSize(iter);
				size.x = Math.max(size.x, footerSize.x);
				size.y += footerSize.y;
			}
		}

		return size;
	}

	public boolean hasNext() {
		return body.hasNext();
	}

	public Point minimumSize() {
		return new Point(minimumSize.x, minimumSize.y);
	}

	public Point preferredSize() {
		return new Point(preferredSize.x, preferredSize.y);
	}

	public PrintPiece next(int width, final int height) {
		PageNumber pageNumber = getCurrentPageNumber();

		// HEADER
		PrintPiece headerPiece = null;
		int availableHeight = height;
		if (header != null) {
			Print headerPrint = header.createPrint(pageNumber);
			if (headerPrint != null) {
				headerPiece = getDecorationPrintPiece(headerPrint, width,
						availableHeight);
				if (headerPiece == null)
					return null;
				availableHeight -= (heightOf(headerPiece) + headerGap);
			}
		}

		// FOOTER
		PrintPiece footerPiece = null;
		if (footer != null) {
			Print footerPrint = footer.createPrint(pageNumber);
			if (footerPrint != null) {
				footerPiece = getDecorationPrintPiece(footerPrint, width,
						availableHeight);
				if (footerPiece == null) {
					PaperClipsUtil.dispose(headerPiece);
					return null;
				}
				availableHeight -= (heightOf(footerPiece) + footerGap);
			}
		}

		// BODY
		PrintPiece bodyPiece = PaperClips.next(body, width, availableHeight);
		if (bodyPiece == null) {
			PaperClipsUtil.dispose(headerPiece, footerPiece);
			return null;
		}

		PrintPiece result = createResult(height, headerPiece, bodyPiece,
				footerPiece);
		advancePageNumber();
		return result;
	}

	private int heightOf(PrintPiece piece) {
		return piece.getSize().y;
	}

	PageNumber pageNumber;

	private void advancePageNumber() {
		// Null the pageNumber field so the next iteration advances to the next
		// page.
		pageNumber = null;
	}

	private PageNumber getCurrentPageNumber() {
		if (pageNumber == null)
			pageNumber = numberer.next();
		return pageNumber;
	}

	private PrintPiece createResult(int height, PrintPiece headerPiece,
			PrintPiece bodyPiece, PrintPiece footerPiece) {
		if (headerPiece == null && footerPiece == null)
			return bodyPiece;

		List entries = new ArrayList();

		if (headerPiece != null)
			entries.add(createEntry(headerPiece, 0));

		int y = headerPiece == null ? 0 : heightOf(headerPiece) + headerGap;
		entries.add(createEntry(bodyPiece, y));

		if (footerPiece != null) {
			y = height - heightOf(footerPiece);
			entries.add(createEntry(footerPiece, y));
		}

		return new CompositePiece(entries);
	}

	private CompositeEntry createEntry(PrintPiece piece, int y) {
		return new CompositeEntry(piece, new Point(0, y));
	}

	private PrintPiece getDecorationPrintPiece(Print decoration, int width,
			int height) {
		PrintIterator iterator = decoration.iterator(device, gc);
		PrintPiece piece = PaperClips.next(iterator, width, height);

		if (piece == null)
			return null;
		if (iterator.hasNext()) {
			piece.dispose();
			return null;
		}
		return piece;
	}

	public PrintIterator copy() {
		return new PageIterator(this);
	}
}