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

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

/**
 * A class for printing styled text. Text of varying size and style are aligned
 * along the baseline.
 * 
 * @author Matthew Hall
 */
public class StyledTextPrint implements Print {
	private final List elements = new ArrayList();
	private TextStyle style = new TextStyle();

	/**
	 * Constructs a new StyledTextPrint.
	 */
	public StyledTextPrint() {
	}

	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((elements == null) ? 0 : elements.hashCode());
		result = prime * result + ((style == null) ? 0 : style.hashCode());
		return result;
	}

	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		StyledTextPrint other = (StyledTextPrint) obj;
		if (elements == null) {
			if (other.elements != null)
				return false;
		} else if (!elements.equals(other.elements))
			return false;
		if (style == null) {
			if (other.style != null)
				return false;
		} else if (!style.equals(other.style))
			return false;
		return true;
	}

	/**
	 * Sets the text style that will be applied to text added through the
	 * {@link #append(String)}
	 * 
	 * @param style
	 *            the new text style.
	 * @return this StyledTextPrint, for chaining method calls.
	 */
	public StyledTextPrint setStyle(TextStyle style) {
		Util.notNull(style);
		this.style = style;
		return this;
	}

	/**
	 * Appends the given text to the end of the document, using the default
	 * style. This method is equivalent to calling append(text, getStyle()).
	 * 
	 * @param text
	 *            the text to append.
	 * @return this StyledTextPrint, for chaining method calls.
	 */
	public StyledTextPrint append(String text) {
		return append(text, style);
	}

	/**
	 * Appends the given text to the end of the document, using the given style.
	 * 
	 * @param text
	 *            the text to append.
	 * @param style
	 *            the text style.
	 * @return this StyledTextPrint, for chaining method calls.
	 */
	public StyledTextPrint append(String text, TextStyle style) {
		TextPrint textPrint = new TextPrint(text, style);
		textPrint.setWordSplitting(false);
		return append(textPrint);
	}

	/**
	 * Appends a line break to the document. If a line break produces a blank
	 * line, that line will take the height of the font in the default text
	 * style.
	 * 
	 * @return this StyledTextPrint, for chaining method calls.
	 */
	public StyledTextPrint newline() {
		return append(new LineBreakPrint(style.getFontData()));
	}

	/**
	 * Appends the given element to the document.
	 * 
	 * @param element
	 *            the element to append.
	 * @return this StyledTextPrint, for chaining method calls.
	 */
	public StyledTextPrint append(Print element) {
		elements.add(element);
		return this;
	}

	public PrintIterator iterator(Device device, GC gc) {
		return new StyledTextIterator((Print[]) elements
				.toArray(new Print[elements.size()]), device, gc);
	}
}

class StyledTextIterator implements PrintIterator {
	private final PrintIterator[] elements;
	private final Point minimumSize;
	private final Point preferredSize;

	private int cursor = 0;

	StyledTextIterator(Print[] elements, Device device, GC gc) {
		this.elements = new PrintIterator[elements.length];
		for (int i = 0; i < elements.length; i++)
			this.elements[i] = elements[i].iterator(device, gc);
		minimumSize = computeSize(PrintSizeStrategy.MINIMUM);
		preferredSize = computeSize(PrintSizeStrategy.PREFERRED);
	}

	private StyledTextIterator(StyledTextIterator that) {
		elements = new PrintIterator[that.elements.length - that.cursor];
		minimumSize = that.minimumSize;
		preferredSize = that.preferredSize;
		for (int i = 0; i < elements.length; i++)
			elements[i] = that.elements[that.cursor + i].copy();
	}

	private Point computeSize(PrintSizeStrategy strategy) {
		Point result = new Point(0, 0);
		for (int i = 0; i < elements.length; i++) {
			Point current = strategy.computeSize(elements[i]);
			result.x = Math.max(result.x, current.x);
			result.y = Math.max(result.y, current.y);
		}
		return result;
	}

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

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

	public boolean hasNext() {
		advanceCursor();
		return cursor < elements.length;
	}

	public PrintPiece next(int width, int height) {
		if (width < 0 || height < 0)
			return null;

		int y = 0;

		List rows = new ArrayList();
		while (y < height) {
			PrintPiece row = nextRow(width, height - y);
			if (row == null)
				break;
			rows.add(new CompositeEntry(row, new Point(0, y)));
			y += row.getSize().y;
		}

		if (rows.size() == 0)
			return null;

		return new CompositePiece(rows);
	}

	private PrintPiece nextRow(int width, int height) {
		int x = 0;
		int maxAscent = 0;
		int maxDescent = 0;

		final int backupCursor = cursor;
		final List backup = new ArrayList();

		List rowElements = new ArrayList();
		while (hasNext()) { // hasNext advances cursor internally
			PrintIterator element = elements[cursor];
			Point preferredSize = element.preferredSize();
			if (preferredSize.y > height)
				break;

			PrintIterator elementBackup = element.copy();
			PrintPiece piece = PaperClips.next(element, width - x,
					preferredSize.y);
			if (piece == null)
				break;

			rowElements.add(piece);
			backup.add(elementBackup);

			maxAscent = Math.max(maxAscent, getAscent(piece));
			maxDescent = Math.max(maxDescent, getDescent(piece));
			if (maxAscent + maxDescent > height) {
				restoreBackup(backupCursor, backup);
				return null;
			}

			if (element.hasNext())
				break;

			x += piece.getSize().x;
		}

		return createRowResult(maxAscent, rowElements);
	}

	private PrintPiece createRowResult(int rowAscent, List rowElements) {
		int x;
		if (rowElements.size() == 0)
			return null;

		x = 0;
		for (int i = 0; i < rowElements.size(); i++) {
			PrintPiece piece = (PrintPiece) rowElements.get(i);
			int ascent = getAscent(piece);
			rowElements.set(i, new CompositeEntry(piece, new Point(x, rowAscent
					- ascent)));
			x += piece.getSize().x;
		}

		return new CompositePiece(rowElements);
	}

	private void restoreBackup(final int backupCursor, final List backup) {
		for (int i = 0; i < backup.size(); i++)
			elements[backupCursor + i] = (PrintIterator) backup.get(i);
		cursor = backupCursor;
	}

	private int getAscent(PrintPiece piece) {
		if (piece instanceof TextPrintPiece)
			return ((TextPrintPiece) piece).getAscent();
		return piece.getSize().y;
	}

	private int getDescent(PrintPiece piece) {
		if (piece instanceof TextPrintPiece)
			return piece.getSize().y - ((TextPrintPiece) piece).getAscent();
		return 0;
	}

	private void advanceCursor() {
		while (cursor < elements.length && !elements[cursor].hasNext())
			cursor++;
	}

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