/*
 * 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 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 that rotates it's target by increments of 90 degrees.
 * <p>
 * <em>Note</em>: On Windows, this class depends on a bugfix available as of
 * Eclipse build 3.2, release candidate 3 (2006-04-28). Prior to this release,
 * using SidewaysPrint triggers the bug, causing the document to scale very
 * large on paper. This bug only manifests itself on paper, not with on-screen
 * viewing.
 * <p>
 * SidewaysPrint, unlike RotatePrint, is neither horizontally nor vertically
 * greedy. Greedy prints take up all the available space on the page.
 * 
 * @author Matthew Hall
 */
public final class SidewaysPrint implements Print {
	private final Print target;
	private final int angle;

	/**
	 * Constructs a SidewaysPrint that rotates it's target 90 degrees
	 * counter-clockwise.
	 * 
	 * @param target
	 *            the print to rotate.
	 */
	public SidewaysPrint(Print target) {
		this(target, 90);
	}

	/**
	 * Constructs a SidewaysPrint.
	 * 
	 * @param target
	 *            the print to rotate.
	 * @param angle
	 *            the angle by which the target will be rotated, expressed in
	 *            degrees counter-clockwise. Positive values rotate
	 *            counter-clockwise, and negative values rotate clockwise. Must
	 *            be a multiple of 90.
	 */
	public SidewaysPrint(Print target, int angle) {
		Util.notNull(target);
		this.target = target;
		this.angle = checkAngle(angle);
	}

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

	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		SidewaysPrint other = (SidewaysPrint) obj;
		if (angle != other.angle)
			return false;
		if (target == null) {
			if (other.target != null)
				return false;
		} else if (!target.equals(other.target))
			return false;
		return true;
	}

	private static int checkAngle(int angle) {
		// Make sure angle is a multiple of 90.
		if (Math.abs(angle) % 90 != 0)
			PaperClips.error(SWT.ERROR_INVALID_ARGUMENT,
					"Angle must be a multiple of 90 degrees"); //$NON-NLS-1$

		// Bring angle within the range [0, 360)
		if (angle < 0)
			angle = 360 - (-angle % 360);
		if (angle >= 360)
			angle = angle % 360;

		return angle;
	}

	/**
	 * Returns the print to be rotated.
	 * 
	 * @return the print to be rotated.
	 */
	public Print getTarget() {
		return target;
	}

	/**
	 * Returns the angle by which the target will be rotated (one of 0, 90, 180,
	 * or 270).
	 * 
	 * @return the angle by which the target will be rotated.
	 */
	public int getAngle() {
		return angle;
	}

	public PrintIterator iterator(Device device, GC gc) {
		if (angle == 0)
			return target.iterator(device, gc);
		return new SidewaysIterator(target, angle, device, gc);
	}
}

final class SidewaysIterator implements PrintIterator {
	private final Device device;
	private final PrintIterator target;
	private final int angle;

	private final Point minimumSize;
	private final Point preferredSize;

	SidewaysIterator(Print target, int angle, Device device, GC gc) {
		Util.notNull(target, device, gc);

		this.device = device;
		this.target = target.iterator(device, gc);
		this.angle = checkAngle(angle); // returns 90, 180, or 270 only

		Point min = this.target.minimumSize();
		Point pref = this.target.preferredSize();

		if (this.angle == 180) {
			this.minimumSize = new Point(min.x, min.y);
			this.preferredSize = new Point(pref.x, pref.y);
		} else { // flip x and y sizes if rotating by 90 or 270 degrees
			this.minimumSize = new Point(min.y, min.x);
			this.preferredSize = new Point(pref.y, pref.x);
		}
	}

	private SidewaysIterator(SidewaysIterator that) {
		this.device = that.device;
		this.target = that.target.copy();
		this.angle = that.angle;
		this.minimumSize = that.minimumSize;
		this.preferredSize = that.preferredSize;
	}

	private static int checkAngle(int angle) {
		switch (angle) {
		case 90:
		case 180:
		case 270:
			break;
		default:
			PaperClips.error(SWT.ERROR_INVALID_ARGUMENT,
					"Angle must be 90, 180, or 270"); //$NON-NLS-1$
		}
		return angle;
	}

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

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

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

	public PrintPiece next(int width, int height) {
		PrintPiece target;
		if (angle == 180)
			target = PaperClips.next(this.target, width, height);
		else
			// flip width and height if rotating by 90 or 270
			target = PaperClips.next(this.target, height, width);

		if (target == null)
			return null;

		Point size = target.getSize();
		if (angle == 90 || angle == 270)
			size = new Point(size.y, size.x);

		return new RotatePiece(device, target, angle, size);
	}

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