/*
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * "The contents of this file are subject to the Mozilla Public License
 * Version 1.1 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations under
 * the License.
 *
 * The Original Code is ICEpdf 3.0 open source software code, released
 * May 1st, 2009. The Initial Developer of the Original Code is ICEsoft
 * Technologies Canada, Corp. Portions created by ICEsoft are Copyright (C)
 * 2004-2009 ICEsoft Technologies Canada, Corp. All Rights Reserved.
 *
 * Contributor(s): _____________________.
 *
 * Alternatively, the contents of this file may be used under the terms of
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"
 * License), in which case the provisions of the LGPL License are
 * applicable instead of those above. If you wish to allow use of your
 * version of this file only under the terms of the LGPL License and not to
 * allow others to use your version of this file under the MPL, indicate
 * your decision by deleting the provisions above and replace them with
 * the notice and other provisions required by the LGPL License. If you do
 * not delete the provisions above, a recipient may use your version of
 * this file under either the MPL or the LGPL License."
 *
 */
package com.softwareimaging.jpdf.core.pobjects.graphics;

import android.graphics.Bitmap;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.Region.Op;

import com.softwareimaging.android.graphics.AffineTransform;
import com.softwareimaging.android.graphics.BufferedImage;
import com.softwareimaging.android.graphics.BasicStroke;
import com.softwareimaging.android.graphics.Canvas;
import com.softwareimaging.android.graphics.Color;
import com.softwareimaging.android.graphics.Image;
import com.softwareimaging.android.graphics.Stroke;
import com.softwareimaging.android.graphics.Paint;
import com.softwareimaging.geometry.PathData;
import com.softwareimaging.geometry.RectangleF;
import com.softwareimaging.jpdf.core.pobjects.PRectangle;
import com.softwareimaging.jpdf.core.util.Defs;
import com.softwareimaging.jpdf.core.util.PDFPrintException;
import com.softwareimaging.logger.Logger;

import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Stack;
import java.util.Vector;

/**
 * <p>
 * The Shapes class hold all object that are parsed from a Page's content
 * streams. These contained object make up a pages graphics stack which can be
 * interated through to paint a page's content.
 * <p>
 * <p/>
 * <p>
 * This class is genearlly only used by the Content parser during content
 * parsing. The class also stores points to the images found in the content as
 * well as the text that is encoded on a page.
 * </p>
 * 
 * @since 1.0
 */
public class Shapes {

	// allow scaling of large images to improve clarity on screen
	private static boolean scaleImages;

	static {
		// decide if large images will be scaled
		scaleImages = Defs.sysPropertyBoolean("com.softwareimaging.jpdf.core.scaleImages", true);
	}

	// Graphics stack for a page's content.
	protected Vector<Object> shapes = new Vector<Object>(100, 50);
	// Vector of images found a page.
	private Vector<Bitmap> images = new Vector<Bitmap>();

	// Last AffineTransform to avoid overfilling the stack
	private AffineTransform xform = null;

	private static int paintDelay = 250;

	// Delay between painting calls.
	static {
		try {
			paintDelay = Defs.intProperty("com.softwareimaging.jpdf.core.views.refreshfrequency",
					250);
		} catch (NumberFormatException e) {
			if (Logger.areErrorsSupported()) {
				Logger.outputError("Error reading buffered scale factor");
			}
		}
	}

	/**
	 * Classes used to make the paint process a little easier, when poping
	 * object off the stack.
	 */
	class Draw {
	}

	class Fill {
	}

	public static class Clip extends com.softwareimaging.geometry.Shapes {
		/**
		 * clip operation
		 */
		public Region.Op op = Region.Op.INTERSECT;

		/**
		 * Constructor
		 * 
		 * @param shapes
		 *            the shapes object to use
		 */
		public Clip(com.softwareimaging.geometry.Shapes shapes) {
			super.set(shapes);
		}

		/**
		 * Constructor
		 * 
		 * @param clip
		 *            the clip object to use
		 */
		public Clip(Clip clip) {
			super.set(clip);
		}

		/**
		 * Constructor
		 * 
		 * @param path
		 *            the path data to use
		 */
		public Clip(PathData path) {
			super.add(path);
		}

		/**
		 * Constructor
		 * 
		 * @param rect
		 *            the rectangle to use
		 */
		public Clip(RectangleF rect) {
			super.add(rect);
		}
	}

	class NoClip {
	}

	class BeginProc {
		public int id;
		public RectF boundingBox;

		public BeginProc(int id, RectF boundingBox) {
			this.id = id;
			this.boundingBox = boundingBox;
		}
	}

	class EndProc {
	}

	class SetPaintMatrix {
		public AffineTransform matrix;

		public SetPaintMatrix(AffineTransform matrix) {
			this.matrix = matrix;
		}
	}

	class UnsetPaintMatrix {
	}

	class SaveTransform {
	}

	class RestoreTransform {
	}

	// type3 font text states for d1 token.
	private PRectangle type3BBox;
	// type 3 font text state for d0 token.
	private PointF type3HorizontalDisplacement;

	public PRectangle getType3BBox() {
		return type3BBox;
	}

	public void setType3BBox(PRectangle type3BBox) {
		this.type3BBox = type3BBox;
	}

	public PointF getType3HorizontalDisplacement() {
		return type3HorizontalDisplacement;
	}

	public void setType3HorizontalDisplacement(PointF type3HorizontalDisplacement) {
		this.type3HorizontalDisplacement = type3HorizontalDisplacement;
	}

	public Shapes() {

	}

	/**
	 * Gets the number of shapes on the shapes stack.
	 * 
	 * @return number of shapes on the stack
	 */
	public int getShapesCount() {
		if (shapes != null) {
			return shapes.size();
		} else {
			return 0;
		}
	}

	/**
	 * Clean up.
	 */
	public void dispose() {
		empty();
		// shapes = null;
		xform = null;
	}

	public void empty() {
		Enumeration<Bitmap> imageContent = images.elements();
		Object tmp;
		Bitmap image;
		while (imageContent.hasMoreElements()) {
			tmp = imageContent.nextElement();
			if (tmp instanceof Bitmap) {
				image = (Bitmap) tmp;
				image.recycle();
			}
		}
		// one more try to free up some memory
		images.clear();

		Enumeration<?> shapeContent = shapes.elements();
		while (shapeContent.hasMoreElements()) {
			tmp = shapeContent.nextElement();
			if (tmp instanceof BufferedImage) {
				((BufferedImage) tmp).dispose();
			} else if (tmp instanceof TextSprite) {
				((TextSprite) tmp).dispose();
			} else {
			}
		}
		shapes.clear();
	}

	/**
	 * Add a new object to the Shapes stack. All new objects must be added via
	 * this function to flash cached objects.
	 * 
	 * @param o
	 *            object to add to the graphics stack.
	 */

	public void add(Object o) {
		// The parser creates lots of AffineTransforms - but we only need to
		// keep the last one prior to any other object.
		if (o instanceof AffineTransform) {
			xform = (AffineTransform) o;
			return;
		} else if (xform != null) {
			shapes.add(xform);
			xform = null;
		}

		// if we have an new image we'll check to see if scaling is enabled
		if (o instanceof Bitmap) {
			Bitmap image = (Bitmap) o;
			int width = image.getWidth();
			Bitmap scalledImage;
			// do image scaling on larger images. This improves the softness
			// of some images that contains black and white text.
			if (scaleImages) {
				double scaleFactor = 1.0;
				if (width > 1000 && width < 1500) {
					scaleFactor = 0.75;
				} else if (width > 1500) {
					scaleFactor = 0.5;
				}
				if (scaleFactor < 1.0) {
					scalledImage = Bitmap.createScaledBitmap(image, (int) (width * scaleFactor),
							(int) (image.getHeight() * scaleFactor), true);
					image.recycle();
				} else {
					scalledImage = image;
				}
			} else {
				scalledImage = image;
			}
			images.addElement(scalledImage);
			shapes.add(scalledImage);
			return;
		}

		// this allows us to capture images from an xObject. We unwrap
		// the images from the xobject shapes vector, otherwise we have no
		// way to extract them.
		if (o instanceof Vector) {
			Vector<?> tmp = (Vector<?>) o;
			Iterator<?> iterator = tmp.iterator();
			Object tmpImage;
			while (iterator.hasNext()) {
				tmpImage = iterator.next();
				if (tmpImage instanceof Bitmap) {
					images.addElement((Bitmap) tmpImage);
				}
			}
		}

		shapes.add(o);
	}

	/**
	 * Adds a new draw command to the graphics stack. When the paint method
	 * encounters this object the current geometric shape is drawn.
	 */
	public void addDrawCommand() {
		add(new Draw());
	}

	/**
	 * Adds a new fill command to the graphics stack. When the paint method
	 * encouters this object the current geometric shape is filled with the
	 * current fill colour.
	 */
	public void addFillCommand() {
		add(new Fill());
	}

	/**
	 * Adds a new clip command to the graphics stack. When the paint method
	 * encounters this object the current geometic shape is used as the new clip
	 * shape.
	 * 
	 * @param shapes
	 *            the shapes object to apply to the clip
	 */
	public void addClipCommand(com.softwareimaging.geometry.Shapes shapes) {
		add(new Clip(shapes));
	}

	/**
	 * Adds a new no clip command to the graphics stack.
	 */
	public void addNoClipCommand() {
		add(new NoClip());
	}

	private static final int known = 1;
	private HashMap<Object, Object> knownProcs = new HashMap<Object, Object>();

	/**
	 * Adds a new start procedure command to the graphics stack
	 */
	public void addBeginProc(int procId, RectF boundingBox, Canvas canvas) {
		add(new BeginProc(procId, boundingBox));
		if (canvas != null) {
			// This procedure is being downloaded on the device so it can be
			// re-used in the print job.
			canvas.updateProc(procId);
		} else {
			// This procedure is being downloaded inside of a procedure, this
			// Shapes, so it can only be re-used in this Shapes
			knownProcs.put(Integer.valueOf(procId), Integer.valueOf(known));
		}
	}

	/**
	 * Adds a new end procedure command to the graphics stack
	 */
	public void addEndProc() {
		add(new EndProc());
	}

	/**
	 * Adds a new set-paint-matrix command to the graphics stack
	 */
	public void addSetPaintMatrix(AffineTransform matrix) {
		add(new SetPaintMatrix(matrix));
	}

	/**
	 * Adds a new unset-paint-matrix command to the graphics stack
	 */
	public void addUnsetPaintMatrix() {
		add(new UnsetPaintMatrix());
	}

	/**
	 * Returns true if the procedure exists on the device
	 */
	public boolean knownProc(int procId, Canvas canvas) {
		if (canvas != null) {
			// This procedure is being downloaded on the device so it can be
			// re-used in the print job.
			return canvas.knownProc(procId);
		} else {
			// This procedure is being downloaded inside of a procedure, this
			// Shapes, so it can only be re-used in this Shapes
			return knownProcs.get(Integer.valueOf(procId)) != null;
		}
	}

	// Variables used in the paint() function
	private Paint paint = null;
	private AffineTransform base = null;
	private PathData shape = null;
	private Clip clip = null;
	private boolean isInited = false;
	private Stack<AffineTransform> afStack = new Stack<AffineTransform>();

	private void init(Canvas g) {
		if (!isInited) {
			paint = new Paint();
			base = new AffineTransform();
			g.getMatrix(base);
			isInited = true;
		}
	}

	/**
	 * Copy some of the shape's internal settings to a new child shapes
	 * structure
	 * 
	 * @param g
	 *            graphics context to paint to
	 * @param childShapes
	 *            drawing instructions
	 */
	private void copyToChild(Canvas g, Shapes childShapes) {
		childShapes.paint = paint;

		// The base matrix is inherited from the parent
		childShapes.base = new AffineTransform();
		g.getMatrix(childShapes.base);

		childShapes.isInited = true;
	}

	/**
	 * Paint the graphics stack to the graphics context
	 * 
	 * @param g
	 *            graphics context to paint to.
	 * @throws PDFPrintException
	 */
	public synchronized void paint(Canvas g) throws PDFPrintException {
		init(g);

		Object nextShape;

		long currentTime;
		long lastPaintTime = System.currentTimeMillis();

		// int paintCount = 0;
		Enumeration<?> shapesEnumeration = shapes.elements();
		try {
			while (shapesEnumeration.hasMoreElements()) {

				nextShape = shapesEnumeration.nextElement();

				if (nextShape instanceof TextSprite) {
					((TextSprite) nextShape).paint(g, paint);
					// Send a PaintPage Event to listeners
					currentTime = System.currentTimeMillis();
					if (currentTime - lastPaintTime > paintDelay) {
						// paintCount++;
						lastPaintTime = currentTime;
					}
				} else if (nextShape instanceof PathData) {
					shape = (PathData) nextShape;
				} else if (nextShape instanceof Fill) {
					paint.setStyle(Paint.Style.FILL);
					g.drawPath(shape, paint);
					// Send a PaintPage Event to listeners
					currentTime = System.currentTimeMillis();
					if (currentTime - lastPaintTime > paintDelay) {
						// paintCount++;
						lastPaintTime = currentTime;
					}
				} else if (nextShape instanceof AffineTransform) {
					AffineTransform af = new AffineTransform(base);
					af.concatenate((AffineTransform) nextShape);
					g.setTransform(af);
				} else if (nextShape instanceof Clip) {
					clip = (Clip) nextShape;

					// Capture the current af for the page
					AffineTransform af = new AffineTransform(g.getTransform());
					g.getMatrix(af);
					// Set the transform to the base, which is fact where the
					// page lies in the viewport, very dynamic.
					g.setTransform(base);
					// apply the af, which places the clip in the correct
					// location
					g.setTransform(af);
					if (!clip.isEmpty()) {
						clip.preConcat(Canvas.toMatrix(base));
						g.clipPath(clip, clip.op);
					}
				} else if (nextShape instanceof Draw) {
					paint.setStyle(Paint.Style.STROKE);
					g.drawPath(shape, paint);
					// Send a PaintPage Event to listeners
					currentTime = System.currentTimeMillis();
					if (currentTime - lastPaintTime > paintDelay) {
						// paintCount++;
						lastPaintTime = currentTime;
					}
				} else if (nextShape instanceof NoClip) {
					AffineTransform af = new AffineTransform(g.getTransform());
					g.getMatrix(af);
					g.setTransform(base);
					g.clipRect((RectF) null, Op.REPLACE);

					g.setTransform(af);
				} else if (nextShape instanceof Stroke) {
					BasicStroke stroke = (BasicStroke) nextShape;
					paint.setStrokeJoin(stroke.getJoin());
					paint.setStrokeCap(stroke.getCap());
					paint.setStrokeMiter(stroke.getMiter());
					paint.setStrokeWidth(stroke.getWidth());
				} else if (nextShape instanceof Image) {
					Image tmpImage = (Image) nextShape;
					try {
						g.drawBitmap(tmpImage, 0, 0, paint);
					} catch (OutOfMemoryError memErr) {
						Logger.outputStackTrace(memErr);
					}
				} else if (nextShape instanceof Shapes) {
					// We don't want to go through the init function here.
					Shapes childShapes = (Shapes) nextShape;
					copyToChild(g, childShapes);
					childShapes.paint(g);
				} else if (nextShape instanceof Color) {
					paint.setColor(((Color) nextShape).getRGB());
				} else if (nextShape instanceof BeginProc) {
					BeginProc beginProc = (BeginProc) nextShape;
					afStack.push(base); // Save the current base matrix
					base = new AffineTransform();
					g.beginProc(beginProc.id, beginProc.boundingBox);
				} else if (nextShape instanceof EndProc) {
					g.endProc();
					base = afStack.pop(); // Restore the current base matrix
				} else if (nextShape instanceof SetPaintMatrix) {
					g.setPaintMatrix(((SetPaintMatrix) nextShape).matrix);
				} else if (nextShape instanceof UnsetPaintMatrix) {
					g.resetPaintMatrix();
				} else if (nextShape instanceof TilingPattern) {
					TilingPattern tilingpattern = (TilingPattern) nextShape;
					paint.setPattern((int) tilingpattern.getXStep(),
							(int) tilingpattern.getYStep(), tilingpattern.getReference());
				}
			}
			// not pretty, but avoid any problems which disposing a page in
			// the middle of a paint.
		} catch (NoSuchElementException e) {
			// eat any errors.
		} catch (PDFPrintException e) {
			throw e;
		} catch (Exception e) {
			if (Logger.areErrorsSupported()) {
				Logger.outputError("Error painting shapes.", e);
			}
		}
	}

	/**
	 * Gets all the images that where found when parsing the pages' content.
	 * Each element in the Vector represents a seperate image.
	 * 
	 * @return all images in a page's content, if any.
	 */
	public Vector<Bitmap> getImages() {
		return images;
	}
}
