/*  This file is part of the source code for 3D-XplorMath-J, Version 1.0 (January 2008).
 *  Copyright (c) 2008 The 3D-XplorMath Consortium (http://3d-xplormath.org).
 *  This source code is released under a BSD License, which allows redistribution   
 *  in source and binary form, with or without modification, provided copyright
 *  and license information are included, and with no warranty or guarantee of
 *  any kind.  For details, see http://3d-xplormath.org/j/source/BSDLicense.txt
 */
 
 package vmm.core.render;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;

import vmm.core.Transform;
import vmm.core.View;

public class ImageRenderer2D implements Renderer2D {
	
	protected BufferedImage image;
	
	protected Transform transform;
	protected Graphics2D currentGraphics;
	
	protected Color foregroundColor;   // from the View
	protected Color backgroundColor;
	
	protected boolean antialiased;
	
	protected Color currentColor;
	
	private Point2D tempPoint = new Point2D.Double();
	
	public void startRender(View view, Transform transform, int width, int height) {
		if (image == null || image.getWidth() != width || image.getHeight() != height || image.getType() != BufferedImage.TYPE_INT_RGB) {
			image = null;
			image = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
		}
		this.transform = transform;
		foregroundColor = view.getForeground();
		backgroundColor = view.getBackground();
		currentGraphics = image.createGraphics();
		currentGraphics.setBackground(backgroundColor);
		currentGraphics.clearRect(0, 0, width, height);
		currentColor = foregroundColor;
		currentGraphics.setColor(currentColor);
		antialiased = view.getAntialiased();
		if (antialiased)
			currentGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
		transform.setUpDrawInfo(currentGraphics, 0, 0, width, height, view.getPreserveAspect(), view.getApplyGraphics2DTransform());
	}
	
	public boolean restartRender(View view, Transform transform, int width, int height) {
		if (image == null || image.getWidth() != width || image.getHeight() != height)
			return false;
		this.transform = transform;
		foregroundColor = view.getForeground();
		backgroundColor = view.getBackground();
		currentGraphics = image.createGraphics();
		currentColor = foregroundColor;
		currentGraphics.setColor(currentColor);
		antialiased = view.getAntialiased();
		if (antialiased)
		if (view.getAntialiased())
			currentGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
		transform.setUpDrawInfo(currentGraphics, 0, 0, width, height, view.getPreserveAspect(), view.getApplyGraphics2DTransform());
		return true;
	}
		
	public void endRender() {
		currentGraphics.dispose();
		currentGraphics = null;
		transform.finishDrawing();
		transform = null;
	}
	
	public void dispose() {
		image = null;
	}
	
	public void draw(Graphics2D g) {
		g.drawImage(image,0,0,null);
	}
	
	public BufferedImage getImage(boolean alwaysCopy) {
		if ( image == null || !alwaysCopy )
			return image;
		BufferedImage copy = new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
		Graphics g = copy.getGraphics();
		g.drawImage(image,0,0,null);
		g.dispose();
		return copy;
	}
	
	/**
	 * This method will set the color in the current graphics context.  This method should only
	 * be used while drawing an exhibit or after {@link #beginDrawToOffscreenImage()}.  If called
	 * at other times, there is no effect.
	 * @param c the color to be used for subsequent drawing; if c is null, then the default foreground
	 * color of the View is restored.
	 */
	public void setColor(Color c) {
		if (currentGraphics != null)
			currentGraphics.setColor( c == null? foregroundColor : c );
		currentColor = currentGraphics.getColor();
	}
	
	/**
	 * Returns the drawing color of the current graphics context.  This method should only be
	 * used while drawing an exhibit or after {@link #beginDrawToOffscreenImage()}.  If called
	 * at other times, the return value is null.
	 */
	public Color getColor() {
		return (currentGraphics == null)? null : currentGraphics.getColor();
	}
	
	/**
	 * Returns the Stroke used by current graphics context.  This method should only be
	 * used while drawing an exhibit or after {@link #beginDrawToOffscreenImage()}.  If called
	 * at other times, the return value is null.
	 */
	public Stroke getStroke() {
		if (currentGraphics != null)
			return currentGraphics.getStroke();
		else
			return null;
	}
	
	/**
	 * This method will set the Stroke for the current graphics context.  This method should only
	 * be used while drawing an exhibit or after {@link #beginDrawToOffscreenImage()}.  If called
	 * at other times, there is no effect.
	 * @param stroke the stroke to be used for subsequent drawing; if stroke is null, then a default stroke
	 * is used with a width of 1 pixel.
	 */
	public void setStroke(Stroke stroke) {
		if (currentGraphics != null)
			currentGraphics.setStroke(stroke == null? new BasicStroke(transform.getDefaultStrokeSize()) : stroke);
	}
	
	/**
	 * Draws a point by turning on a single pixel.  This is done by transforming (x,y) to pixel coordinates,
	 * then using <code>drawPixelDirect</code> to set the color of the single pixel.
	 * @see #drawPixelDirect(Color, int, int)
	 * @param x The x-coordinate of the point, in window (real xy) coodinates.
	 * @param y The y-coordinate of the point, in window (real xy) coodinates.
	 */
	public void drawPixel(double x, double y) {
		tempPoint.setLocation(x,y);
		transform.windowToViewport(tempPoint);
		int xInt = (int)(tempPoint.getX() + 0.4999);
		int yInt = (int)(tempPoint.getY() + 0.4999);
		int rgb = currentColor.getRGB();
		try {
			image.setRGB(xInt,yInt,rgb);
		}
		catch (Exception e) {
		}
	}
	
	/**
	 * Draws a dot of specified diameter centered at a specified point.
	 * This is done by calling the Graphics2D fill() command for an
	 * appropriate Ellipse2D.  The diameter is specified in <b>pixels</b>.
	 * Note that if the preserveAspectRatio is off for this View, then
	 * the dot will can be an oval rather than a circle.
	 */
	public void drawDot(Point2D pt, double diameter) {
		tempPoint.setLocation(pt);
		transform.windowToDrawingCoords(tempPoint);
		double h = diameter*transform.getPixelWidth();
		double w = diameter*transform.getPixelHeight();
		currentGraphics.fill(new Ellipse2D.Double(pt.getX()-h/2,pt.getY()-w/2,h,w));
	}
	
	/**
	 * Draws a list of pixels in the current drawing context, where the pixels are specified in object coordinates.
	 * This should only be called while drawing is in progress.*/
	public void drawPixels(Point2D[] points, int pointIndexStart, int pointIndexEnd) {  // points in window coordinates
		if (points == null)
			return;
		if (pointIndexStart >= points.length)
			pointIndexStart = points.length - 1;
		if (pointIndexStart < 0)
			pointIndexStart = 0;
		if (pointIndexEnd >= points.length)
			pointIndexEnd = points.length - 1;
		if (pointIndexEnd < 0)
			pointIndexEnd = 0;
		if (pointIndexEnd <= pointIndexStart)
			return;
		Color color = currentGraphics.getColor();
		int rgb = color.getRGB();
		for (int i = pointIndexStart; i <= pointIndexEnd; i++) { 
			if (points[i] != null) {
				tempPoint.setLocation(points[i]);
				transform.windowToViewport(tempPoint);
				try {
					image.setRGB((int)(tempPoint.getX()+0.499), (int)(tempPoint.getY()+0.499),rgb);
				}
				catch (Exception e) {
				}
			}
		}
	}
	
	/**
	 * This can be called during drawing to draw a string at a specified point, given in window (real x,y) coordinates.
	 * The font is NOT transformed, as it would be if you simply used the <code>drawString</code>
	 * method of a drawing context to which a transform has been applied.
	 * The point (x,y) is properly transformed from xy-coordinates to pixel coordinates, whether a
	 * transform has been applied ot the drawing context or not.  After conversion, if necessary, the
	 * graphics context returned by {@link Transform#getUntransformedGraphics()} is used to draw the string.
	 * @see #drawString(String, Point2D)
	 */
	public void drawString(String s, double x, double y) {
		Point2D pt = new Point2D.Double(x,y);
		transform.windowToViewport(pt);
		transform.getUntransformedGraphics().drawString(s,(float)pt.getX(),(float)pt.getY());
	}
	

	/**
	 * Draws a line in the current drawing context.  This should only be called while drawing is
	 * in progress.  The line has endpoints (x1,y1) and (x2,y2), where the coordinates are
	 * in window (real xy) coordinates.  This should only be called when a drawing operation is in
	 * progress.
	 */
	public void drawLine(double x1, double y1, double x2, double y2) {
		double tx1, ty1;
		tempPoint.setLocation(x1,y1);
	    transform.windowToDrawingCoords(tempPoint);
	    tx1 = tempPoint.getX();
	    ty1 = tempPoint.getY();
	    tempPoint.setLocation(x2,y2);
		transform.windowToDrawingCoords(tempPoint);
		currentGraphics.draw(new Line2D.Double(tx1, ty1, tempPoint.getX(), tempPoint.getY()));
	}
	
	/**
	 * Draws a curve in the current drawing context.  The points on the curve are given in window (real xy) coordinates.
	 * This should only be called while drawing is in progress. 
	 * @param points The curve is drawn through some or all of the points in this array.  If the array is null, nothing is done.
	 * The curve is acutually just made up of lines from one point to the next.  An element in the array can be null.
	 * In that case, one or two segments are missing from the curve -- the segments on either side of the missing point.
	 * Consecutive points are also not joined by a segment if the jump from one point to the next is too large.
	 * @param pointIndexStart The number of points in the array that should be used for the curve.  A curve is drawn
	 * though points[pointIndexStart], point[pointIndexStart+1], ..., points[pointIndexEnd].  The value of pointIndexStart
	 * is clamped to lie in the range 0 to points.length-1.
	 * @param pointIndexEnd The number of points in the array that should be used for the curve.  A curve is drawn
	 * though points[pointIndexStart], point[pointIndexStart+1], ..., points[pointIndexEnd].  The value of pointIndexEnd
	 * is clamped to lie in the range 0 to points.length-1.  If pointIndexEnd is less than or equal to pointIndexStart,
	 * nothing is drawn.
	 */
	public void drawCurve(Point2D[] points, int pointIndexStart, int pointIndexEnd) {  // points in window coordinates
		if (points == null)
			return;
		if (pointIndexStart >= points.length)
			pointIndexStart = points.length - 1;
		if (pointIndexStart < 0)
			pointIndexStart = 0;
		if (pointIndexEnd >= points.length)
			pointIndexEnd = points.length - 1;
		if (pointIndexEnd < 0)
			pointIndexEnd = 0;
		if (pointIndexEnd <= pointIndexStart)
			return;
		GeneralPath curve = new GeneralPath();
		double maxJumpX = Math.abs(transform.getXmax() - transform.getXmin())/4;
		double maxJumpY = Math.abs(transform.getYmax() - transform.getYmin())/4;
		if (transform.appliedTransform2D())
			maxJumpX = maxJumpY = Math.max(maxJumpX,maxJumpY);
		boolean moved = false;
		for (int i = pointIndexStart; i < pointIndexEnd; i++) { 
			if (points[i] != null) {
				tempPoint.setLocation(points[i]);
				transform.windowToDrawingCoords(tempPoint);
				if (i == pointIndexStart) 
				     curve.moveTo((float)tempPoint.getX(), (float)tempPoint.getY());
				else if (i > pointIndexStart && points[i-1] != null && Math.abs(points[i].getX() - points[i-1].getX()) <= maxJumpX 
						&&  Math.abs(points[i].getY() - points[i-1].getY()) <= maxJumpY)
				     curve.lineTo((float)tempPoint.getX(), (float)tempPoint.getY());
				else {
					curve.moveTo((float)tempPoint.getX(), (float)tempPoint.getY());
					moved = true;
				}
			}
		}
		if (points[pointIndexEnd] != null) {
			if ( (pointIndexStart == 0) && (pointIndexEnd == points.length-1) 
					&& (points[0] != null) && (Math.abs(points[0].getX() - points[pointIndexEnd].getX()) <= transform.getPixelWidth()/100 )  
					&&  (Math.abs(points[0].getY() - points[pointIndexEnd].getY()) <= transform.getPixelHeight()/100 ) 
					&& (!moved) ) { 
				curve.closePath(); // System.out.println("Path was closed");  // Do NOT close if moved = true - leads to errors.
			}  
			else {
				// replaced from here
				tempPoint.setLocation(points[pointIndexEnd]);
				transform.windowToDrawingCoords(tempPoint);
				if (points[pointIndexEnd-1] != null && Math.abs(points[pointIndexEnd].getX() - points[pointIndexEnd-1].getX()) <= maxJumpX 
						&&  Math.abs(points[pointIndexEnd].getY() - points[pointIndexEnd-1].getY()) <= maxJumpY)
					curve.lineTo((float)tempPoint.getX(), (float)tempPoint.getY());
				else
					curve.moveTo((float)tempPoint.getX(), (float)tempPoint.getY());
			}
		} 
		currentGraphics.draw(curve);
	}
	
	public void drawOval(double x, double y, double width, double height) {
		Point2D pt1 = new Point2D.Double(x,y);
		Point2D pt2 = new Point2D.Double(x+width,y+height);
		transform.windowToDrawingCoords(pt1);
		transform.windowToDrawingCoords(pt2);
		currentGraphics.draw(new Ellipse2D.Double(pt1.getX(), pt1.getY(), pt2.getX()-pt1.getX(), pt2.getY()-pt1.getY()));
	}
	
	public void fillOval(double x, double y, double width, double height) {
		Point2D pt1 = new Point2D.Double(x,y);
		Point2D pt2 = new Point2D.Double(x+width,y+height);
		transform.windowToDrawingCoords(pt1);
		transform.windowToDrawingCoords(pt2);
		currentGraphics.fill(new Ellipse2D.Double(pt1.getX(), pt1.getY(), pt2.getX()-pt1.getX(), pt2.getY()-pt1.getY()));
	}
	
	public void drawRect(double x, double y, double width, double height) {
		Point2D pt1 = new Point2D.Double(x,y);
		Point2D pt2 = new Point2D.Double(x+width,y+height);
		transform.windowToDrawingCoords(pt1);
		transform.windowToDrawingCoords(pt2);
		currentGraphics.draw(new Rectangle2D.Double(pt1.getX(), pt1.getY(), pt2.getX()-pt1.getX(), pt2.getY()-pt1.getY()));
	}
	
	public void fillRect(double x, double y, double width, double height) {
		Point2D pt1 = new Point2D.Double(x,y);
		Point2D pt2 = new Point2D.Double(x+width,y+height);
		transform.windowToDrawingCoords(pt1);
		transform.windowToDrawingCoords(pt2);
		currentGraphics.fill(new Rectangle2D.Double(pt1.getX(), pt1.getY(), pt2.getX()-pt1.getX(), pt2.getY()-pt1.getY()));
	}
	
	public void drawCrosshair(double x, double y, int armlength, int strokeWidth, Color color, Color background) {
		Point2D pt = new Point2D.Double(x,y);
		transform.windowToViewport(pt);
		int cx = (int)(pt.getX()+0.499);
		int cy = (int)(pt.getY()+0.499);
		Graphics2D g1 = transform.getUntransformedGraphics();
		Color saveColor = g1.getColor();
		int size = 2*armlength + strokeWidth;
		int offset = (size+1)/2;
		int offset2 = (strokeWidth+1)/2;
		if (backgroundColor != null) {
			g1.setColor(backgroundColor);
			g1.fillRect(cx-offset-1, cy-offset2-1, size+2, strokeWidth+2);
			g1.fillRect(cx-offset2-1, cy-offset-1, strokeWidth+2, size+2);
		}
		if (color != null)
			g1.setColor(color);
		g1.fillRect(cx-offset, cy-offset2, size, strokeWidth);
		g1.fillRect(cx-offset2, cy-offset, strokeWidth, size);
		g1.setColor(saveColor);
	}
	
	public void setDrawAntialiased(boolean antialiased) {
		currentGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
				antialiased ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
		this.antialiased = antialiased;
	}
		
	public boolean getDrawAntialiased() {
		return antialiased;
	}

//	//--------------------------- Some pixel-oriented drawing methods -----------------------
//	
//	/**
//	 * Sets the pixel with pixel coordinates (x,y) to be a specified color.
//	 * The pixel color is changed in the off-screen image, not on the screen immmediately.
//	 * The current transformation is <b>not</b> applied to the coordinates.
//	 * @param color the color for the pixel; if null, the current drawing color is used.
//	 * @param x the horizontal pixel coordinate.
//	 * @param y the vertical pixel coordinate.
//	 */
//	public void drawPixelDirect(Color color, int x, int y) {
//		if (x < 0 || y < 0 || x >= currentImage.getWidth() || y >= currentImage.getHeight())
//			return;
//		if (color == null)
//			color = currentGraphics.getColor();
//		int rgb;
//		rgb = (color.getRed() << 16) | (color.getGreen() << 8) | (color.getBlue());
//		currentImage.setRGB(x,y,rgb);
//	}
//		
//	/**
//	 * Draws a line in the current color between points that are specified using
//	 * pixel coordinates.
//	 */
//	public void drawLineDirect(int x1, int y1, int x2, int y2) {
//		Graphics2D g = currentImage.createGraphics();
//		g.setColor(currentColor);
//		if (antialiased)
//			g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
//		g.drawLine(x1,y1,x2,y2);
//		g.dispose();
//	}
//	
//	/**
//	 * Draws a filled-in rectangle in the current color, where the rectangle is specified in pixel coordinates.
//	 */
//	public void fillRectDirect(int x, int y, int width, int height) {
//		Graphics g = currentImage.getGraphics();
//		g.setColor(currentColor);
//		g.fillRect(x,y,width,height);
//		g.dispose();
//	}
//
//	public void setImageRGBDirect(int x, int y, int width, int height, int[] rgb) {
//		try {
//			currentImage.setRGB(x,y,width,height,rgb,0,width);
//		}
//		catch (Exception e) {
//		}
//	}
//
//

}
