/*
 * 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;
import org.eclipse.swt.graphics.Transform;

/**
 * A decorator print that scales it's target larger or smaller.
 * <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 ScalePrint triggers the bug, causing the document to scale very large
 * on paper. This bug manifests itself only on paper, not with on-screen
 * viewing.
 * 
 * @author Matthew Hall
 */
public class ScalePrint implements Print {
	final Print target;
	final Double scale;

	/**
	 * Constructs a ScalePrint which scales down it's target to print at it's
	 * preferred size. This constructor is equivalent to calling new
	 * ScalePrint(target, null).
	 * 
	 * @param target
	 *            the print to scale down.
	 */
	public ScalePrint(Print target) {
		this(target, null);
	}

	/**
	 * Constructs a ScalePrint which scales it's target by the given factor.
	 * 
	 * @param target
	 * @param scale
	 *            the scale factor (must be >0). A value of 2.0 draws at double
	 *            the size, and a value of 0.5 draws at half the size. A null
	 *            value automatically scales down so the target is rendered at
	 *            it's preferred size.
	 */
	public ScalePrint(Print target, Double scale) {
		Util.notNull(target);
		if (scale != null && !(scale.doubleValue() > 0))
			PaperClips.error(SWT.ERROR_INVALID_ARGUMENT,
					"Scale " + scale + " must be > 0"); //$NON-NLS-1$ //$NON-NLS-2$

		this.target = target;
		this.scale = scale;
	}

	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((scale == null) ? 0 : scale.hashCode());
		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;
		ScalePrint other = (ScalePrint) obj;
		if (scale == null) {
			if (other.scale != null)
				return false;
		} else if (!scale.equals(other.scale))
			return false;
		if (target == null) {
			if (other.target != null)
				return false;
		} else if (!target.equals(other.target))
			return false;
		return true;
	}

	/**
	 * Returns the print being scaled.
	 * 
	 * @return the print being scaled.
	 */
	public Print getTarget() {
		return target;
	}

	/**
	 * Returns the scale by which the target will be scaled, or null (indicating
	 * automatic scale down to fit).
	 * 
	 * @return the scale by which the target will be scaled, or null (indicating
	 *         automatic scale down to fit).
	 */
	public Double getScale() {
		return scale;
	}

	public PrintIterator iterator(Device device, GC gc) {
		return new ScaleIterator(this, device, gc);
	}
}

class ScaleIterator implements PrintIterator {
	private final Device device;
	private final PrintIterator target;
	private final Double scale;

	private final Point minimumSize;
	private final Point preferredSize;

	ScaleIterator(ScalePrint print, Device device, GC gc) {
		Util.notNull(print, device, gc);

		this.device = device;
		this.target = print.target.iterator(device, gc);
		this.scale = print.scale;

		Point min = target.minimumSize();
		Point pref = target.preferredSize();
		if (scale == null) { // auto-scale
			minimumSize = new Point(1, 1);
			preferredSize = pref;
		} else { // specific scale
			double s = scale.doubleValue();
			minimumSize = new Point((int) Math.ceil(min.x * s), (int) Math
					.ceil(min.y * s));
			preferredSize = new Point((int) Math.ceil(pref.x * s), (int) Math
					.ceil(pref.y * s));
		}
	}

	private ScaleIterator(ScaleIterator that) {
		this.device = that.device;
		this.target = that.target.copy();
		this.scale = that.scale;

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

	public Point minimumSize() {
		return minimumSize;
	}

	public Point preferredSize() {
		return preferredSize;
	}

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

	public PrintPiece next(int width, int height) {
		// Find out what scale we're going to iterate at.
		double scale;
		Point pref = target.preferredSize();
		if (this.scale == null)
			scale = Math.min(Math.min((double) width / (double) pref.x,
					(double) height / (double) pref.y), 1.0);
		else
			scale = this.scale.doubleValue();

		// Calculate the width and height to be passed to the target.
		final int scaledWidth = (int) Math.ceil(width / scale);
		final int scaledHeight = (int) Math.ceil(height / scale);

		PrintPiece target = PaperClips.next(this.target, scaledWidth,
				scaledHeight);

		if (target == null)
			return null;

		return new ScalePiece(device, target, scale, width, height);
	}

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

final class ScalePiece implements PrintPiece {
	private final Device device;
	private final PrintPiece target;
	private final double scale;
	private final Point size;

	private Transform oldTransform;
	private Transform transform;

	ScalePiece(Device device, PrintPiece target, double scale, int maxWidth,
			int maxHeight) {
		Util.notNull(device, target);
		this.device = device;
		this.target = target;
		this.scale = scale;
		Point targetSize = target.getSize();
		this.size = new Point(Math.min((int) Math.ceil(targetSize.x * scale),
				maxWidth), Math.min((int) Math.ceil(targetSize.y * scale),
				maxHeight));
	}

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

	private Transform getOldTransform() {
		if (oldTransform == null)
			oldTransform = new Transform(device);
		return oldTransform;
	}

	private Transform getTransform() {
		if (transform == null)
			transform = new Transform(device);
		return transform;
	}

	public void paint(GC gc, int x, int y) {
		Transform oldTransform = getOldTransform();
		gc.getTransform(oldTransform);

		Transform transform = getTransform();
		gc.getTransform(transform);
		transform.translate(x, y);
		transform.scale((float) scale, (float) scale);
		gc.setTransform(transform);

		target.paint(gc, 0, 0);

		gc.setTransform(oldTransform);
	}

	public void dispose() {
		if (oldTransform != null) {
			oldTransform.dispose();
			oldTransform = null;
		}
		if (transform != null) {
			transform.dispose();
			transform = null;
		}
		target.dispose();
	}
}