/******************************************************************************
 * Copyright (c) 2005, 2010 IBM Corporation 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:
 *    IBM Corporation - initial API and implementation 
 ****************************************************************************/

package org.eclipse.gmf.runtime.draw2d.ui.render.awt.internal.graphics;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Image;
import java.awt.Paint;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.PathIterator;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.awt.image.RenderedImage;
import java.awt.image.renderable.RenderableImage;
import java.text.AttributedCharacterIterator;

import org.apache.batik.ext.awt.g2d.DefaultGraphics2D;
import org.apache.batik.ext.awt.g2d.GraphicContext;
import org.eclipse.draw2d.Graphics;
import org.eclipse.draw2d.SWTGraphics;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.gmf.runtime.draw2d.ui.render.awt.internal.image.ImageConverter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Path;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Transform;

/**
 * Objects of this class can be used with awt to render to a SWT Graphics
 * object.
 * 
 * @author sshaw
 * 
 */
public class Graphics2DToGraphicsAdaptor
	extends DefaultGraphics2D {

	private Graphics swtGraphics;
	private org.eclipse.swt.graphics.Font currentFont = null;
	private org.eclipse.swt.graphics.Color currentColor = null;
	private Transform currentTransform = null;
	private GC swtGC;
	private RGB transparency;
	private RGB transparency_replace;
	private Path clipPath;
	
	/**
	 * Constructor
	 * 
	 * @param swtGC the <code>GC</code> to render to
	 * @param transparency the <code>RGB</code> that will be the transparent color
	 * @param transparency_replace the <code>RGB</code> that will replace the transparent color
	 * in the rendering.
	 */
	public Graphics2DToGraphicsAdaptor(GC swtGC, RGB transparency, RGB transparency_replace) {
		super(true);
		gc = new GraphicContext();
		this.swtGC = swtGC;
		swtGC.setAdvanced(true);
		if (swtGC.getAdvanced() != true)
			throw new UnsupportedOperationException("Advanced graphics support is required"); //$NON-NLS-1$
		
		this.swtGraphics = new SWTGraphics(swtGC);
		this.transparency = transparency;
		this.transparency_replace = transparency_replace;
	}
	
	/* 
	 * (non-Javadoc)
	 * @see org.apache.batik.ext.awt.g2d.DefaultGraphics2D#dispose()
	 */
	public void dispose() {
		if (currentColor != null)
			currentColor.dispose();
		currentColor = null;
		if (currentFont != null)
			currentFont.dispose();
		currentFont = null;
		if (currentTransform != null) 
			currentTransform.dispose();
		currentTransform = null;
		
		if (clipPath != null && !clipPath.isDisposed()) {
			clipPath.dispose();
		}
		
		if (swtGraphics != null)
			swtGraphics.dispose();
		swtGraphics = null;
	}

	/**
	 * Setup the swt graphics object with the appropriate configuration details.
	 */
	protected final boolean configureGraphics() {
		swtGraphics.pushState();

		boolean supported = true;
		
		supported &= configureStroke(getStroke());
		supported &= configureTransformation(getTransform());
		supported &= configureClipping(getClip());
		supported &= configureComposite(getComposite());
		supported &= configurePaintMode(getPaint());
		supported &= configureRenderingHints();

		return supported;
	}

	private boolean configureRenderingHints() {
		Object antiAlias = getRenderingHint(RenderingHints.KEY_ANTIALIASING);
		if (antiAlias != null) {
			if (antiAlias.equals(RenderingHints.VALUE_ANTIALIAS_ON))
				swtGraphics.setAntialias(SWT.ON);
			else if (antiAlias.equals(RenderingHints.VALUE_ANTIALIAS_OFF))
				swtGraphics.setAntialias(SWT.OFF);
			else if (antiAlias.equals(RenderingHints.VALUE_ANTIALIAS_DEFAULT))
				swtGraphics.setAntialias(SWT.DEFAULT);
		}

		Object textAntiAlias = getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING);
		if (textAntiAlias != null) {
			if (textAntiAlias.equals(RenderingHints.VALUE_TEXT_ANTIALIAS_ON))
				swtGraphics.setTextAntialias(SWT.ON);
			else if (textAntiAlias
				.equals(RenderingHints.VALUE_TEXT_ANTIALIAS_OFF))
				swtGraphics.setTextAntialias(SWT.OFF);
			else if (textAntiAlias
				.equals(RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT))
				swtGraphics.setTextAntialias(SWT.DEFAULT);
		}

		Object interpolation = getRenderingHint(RenderingHints.KEY_INTERPOLATION);
		if (interpolation != null) {
			if (interpolation
				.equals(RenderingHints.VALUE_INTERPOLATION_BICUBIC))
				swtGraphics.setInterpolation(SWT.HIGH);
			else if (interpolation
				.equals(RenderingHints.VALUE_INTERPOLATION_BILINEAR))
				swtGraphics.setInterpolation(SWT.LOW);
			else if (interpolation
				.equals(RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR))
				swtGraphics.setInterpolation(SWT.NONE);
		}
		return true;
	}

	private boolean configureStroke(Stroke s) {
		if (s instanceof BasicStroke) {
			BasicStroke basicStroke = (BasicStroke) s;
			switch (basicStroke.getEndCap()) {
				case BasicStroke.CAP_BUTT:
					swtGC.setLineCap(SWT.CAP_FLAT);
					break;
				case BasicStroke.CAP_ROUND:
					swtGC.setLineCap(SWT.CAP_ROUND);
					break;
				case BasicStroke.CAP_SQUARE:
					swtGC.setLineCap(SWT.CAP_SQUARE);
					break;
				default:
					return false;
			}

			if (basicStroke.getDashArray() != null) {
				swtGC.setLineStyle(SWT.LINE_CUSTOM);
				float[] currentDash = basicStroke.getDashArray();
				int[] dash = new int[currentDash.length];
				for (int i = 0; i < currentDash.length; i++)
					dash[i] = Math.round(currentDash[i]);
				swtGC.setLineDash(dash);
			} else {
				swtGC.setLineStyle(SWT.LINE_SOLID);
				swtGC.setLineDash(null);
			}

			switch (basicStroke.getLineJoin()) {
				case BasicStroke.JOIN_BEVEL:
					swtGC.setLineJoin(SWT.JOIN_BEVEL);
					break;
				case BasicStroke.JOIN_MITER:
					swtGC.setLineJoin(SWT.JOIN_MITER);
					break;
				case BasicStroke.JOIN_ROUND:
					swtGC.setLineJoin(SWT.JOIN_ROUND);
					break;
				default:
					return false;
			}

			// since we don't have precision of less then 1 pixel in
			// swt, if the image is defined as very small in the original
			// file, then the linewidth gets scaled out of proportion.
			if (basicStroke.getLineWidth() < 1 &&
				basicStroke.getLineWidth() > 0)
				throw new UnsupportedOperationException();
			
			swtGC.setLineWidth(Math.round(basicStroke.getLineWidth()));
			return true;
		}

		return false;
	}

	private boolean configurePaintMode(Paint p) {
		if (p instanceof Color) {
			Color c = (Color) p;
			
			if (c.getAlpha() != 255) {
				// swt graphics doesn't support blitting onto transparency mask explicitly.
				throw new UnsupportedOperationException();
			}
		
			RGB rgb = new RGB(c.getRed(), c.getGreen(), c.getBlue());
			if (rgb.equals(transparency))
				rgb = transparency_replace;
			
			if (currentColor != null)
				currentColor.dispose();
			currentColor = new org.eclipse.swt.graphics.Color(
				null, rgb.red, rgb.green, rgb.blue);
			swtGraphics.setBackgroundColor(currentColor);
			swtGraphics.setForegroundColor(currentColor);
			return true;
		} 

		return false;
	}

	private boolean configureComposite(Composite c) {
		if (c instanceof AlphaComposite) {
			AlphaComposite ac = (AlphaComposite)c;
			
			// swt graphics doesn't support blitting onto transparency mask explicitly.
			if (ac.getAlpha() != 1.0)
				throw new UnsupportedOperationException();
			
			swtGC.setAlpha(Math.round(ac.getAlpha() * 255));
			if (ac.getRule() == AlphaComposite.SRC_OVER)
				return true;
		} 

		return false;
	}

	private boolean configureTransformation(AffineTransform cxPrime) {
		double[] values = new double[6];
		cxPrime.getMatrix(values);
		
		float[] fltVals = new float[6];
		for (int i=0; i<values.length; i++)
			fltVals[i] = (float)values[i];
		
		if (currentTransform != null)
			currentTransform.dispose();
		currentTransform = new Transform(null, fltVals);
		swtGC.setTransform(currentTransform);
		
		return true;
	}

	private boolean configureClipping(Shape clipShape) {
		if (clipPath != null && !clipPath.isDisposed()) {
			clipPath.dispose();
		}
		clipPath = getPath(clipShape);
		java.awt.Rectangle rectClip = clipShape.getBounds();
		if (clipPath != null) {
			swtGraphics.setClip(new Rectangle(rectClip.x, rectClip.y,
				rectClip.width, rectClip.height));
			swtGC.setClipping(clipPath);
		}
		return true;
	}

	private final AffineTransform identityTransform = new AffineTransform();
	private final Stroke defaultStroke = new BasicStroke();
	
	/**
	 * Reset the graphics objects back to defaults
	 */
	protected final void resetGraphics() {
		configureTransformation(identityTransform);
		configurePaintMode(Color.WHITE);
		configureStroke(defaultStroke);
		configureComposite(AlphaComposite.SrcOver);
		
		if (currentColor != null)
			currentColor.dispose();
		currentColor = null;
		if (currentFont != null)
			currentFont.dispose();
		currentFont = null;
		if (currentTransform != null) 
			currentTransform.dispose();
		currentTransform = null;
		
		swtGC.setClipping((Path)null);
		
		if (clipPath != null && !clipPath.isDisposed()) {
			clipPath.dispose();
		}
		
		swtGraphics.popState();
	}

	private void drawImageBase(BufferedImage bufImg, int x, int y) {
		drawImageBase(bufImg, 0, 0, bufImg.getWidth(), bufImg.getHeight(), x, y, 
			bufImg.getWidth(), bufImg.getHeight());
	}
	
	private void drawImageBase(BufferedImage bufImg, int x, int y, int width, int height, 
			int tx, int ty, int twidth, int theight) {
		org.eclipse.swt.graphics.Image swtImage = ImageConverter.convert(bufImg);
		swtGraphics.drawImage(swtImage, x, y, width, height, tx, ty, twidth, theight);
		swtImage.dispose();
	}
	
	private BufferedImage createImage(Shape s) {
		java.awt.Rectangle rect = s.getBounds();
		if (rect.width == 0)
			rect.width = 1;
		if (rect.height == 0)
			rect.height = 1;
		return new BufferedImage(rect.width, rect.height, BufferedImage.TYPE_INT_ARGB);
	}
	
	private Graphics2D configureGraphics2D(BufferedImage bufImg, int x, int y) {
		Graphics2D g = bufImg.createGraphics();
		g.setPaint(getPaint());
		g.setComposite(getComposite());
		g.setStroke(getStroke());
		g.setRenderingHints(getRenderingHints());
		g.setFont(getFont());
		g.translate(-x, -y);
		return g;
	}
	

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.batik.ext.awt.g2d.DefaultGraphics2D#draw(java.awt.Shape)
	 */
	public void draw(Shape s) {
		boolean supportedByDraw2d = configureGraphics();
		if (supportedByDraw2d) {
			Path path = getPath(s);
			if (path != null) {
				swtGraphics.drawPath(path);
				path.dispose();
			}
		} else {
			java.awt.Rectangle rect = s.getBounds();
			BufferedImage bufImg = createImage(s);
			Graphics2D g = configureGraphics2D(bufImg, rect.x, rect.y);
			g.draw(s);
			
			drawImageBase(bufImg, rect.x, rect.y);
		}

		resetGraphics();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.batik.ext.awt.g2d.DefaultGraphics2D#fill(java.awt.Shape)
	 */
	public void fill(Shape s) {
		boolean supportedByDraw2d = configureGraphics();
		if (supportedByDraw2d) {
			Path path = getPath(s);
			if (path != null) {
				swtGraphics.fillPath(path);
				path.dispose();
			}
		} else {
			java.awt.Rectangle rect = s.getBounds();
			BufferedImage bufImg = createImage(s);
			Graphics2D g = configureGraphics2D(bufImg, rect.x, rect.y);
			g.fill(s);
			
			drawImageBase(bufImg, rect.x, rect.y);
		}

		resetGraphics();
	}


	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.batik.ext.awt.g2d.DefaultGraphics2D#drawImage(java.awt.Image,
	 *      int, int, java.awt.image.ImageObserver)
	 */
	public boolean drawImage(Image img, int x, int y, ImageObserver observer) {
		boolean supportedByDraw2d = configureGraphics();

		if (img instanceof BufferedImage && supportedByDraw2d) {
			drawImageBase((BufferedImage)img, x, y);
		} else {
			throw new UnsupportedOperationException();
		}

		resetGraphics();
		return true;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.batik.ext.awt.g2d.DefaultGraphics2D#drawImage(java.awt.Image,
	 *      int, int, int, int, java.awt.image.ImageObserver)
	 */
	public boolean drawImage(Image img, int x, int y, int width, int height,
			ImageObserver observer) {
		boolean supportedByDraw2d = configureGraphics();

		if (img instanceof BufferedImage && supportedByDraw2d) {
			BufferedImage bufImg = (BufferedImage)img;
			drawImageBase((BufferedImage)img, 0, 0, bufImg.getWidth(), bufImg.getHeight(),
					x, y, width, height);
		} else {
			throw new UnsupportedOperationException();
		}

		resetGraphics();
		return true;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.batik.ext.awt.g2d.DefaultGraphics2D#drawRenderableImage(java.awt.image.renderable.RenderableImage,
	 *      java.awt.geom.AffineTransform)
	 */
	public void drawRenderableImage(RenderableImage img, AffineTransform xform) {
		throw new UnsupportedOperationException();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.batik.ext.awt.g2d.DefaultGraphics2D#drawRenderedImage(java.awt.image.RenderedImage,
	 *      java.awt.geom.AffineTransform)
	 */
	public void drawRenderedImage(RenderedImage img, AffineTransform xform) {
		throw new UnsupportedOperationException();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.batik.ext.awt.g2d.DefaultGraphics2D#drawString(java.text.AttributedCharacterIterator,
	 *      float, float)
	 */
	public void drawString(AttributedCharacterIterator iterator, float x,
			float y) {
		throw new UnsupportedOperationException();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.batik.ext.awt.g2d.DefaultGraphics2D#drawString(java.lang.String,
	 *      float, float)
	 */
	public void drawString(String s, float x, float y) {
		throw new UnsupportedOperationException();
	}

	/**
	 * Used to create proper font metrics
	 */
	private GraphicsConfiguration gconfig;

	{
		BufferedImage bi = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
		gconfig = bi.createGraphics().getDeviceConfiguration();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.batik.ext.awt.g2d.DefaultGraphics2D#getDeviceConfiguration()
	 */
	public GraphicsConfiguration getDeviceConfiguration() {
		return gconfig;
	}

	private Path getPath(Shape s) {
		PathIterator pi = s.getPathIterator(null);
		Path path = new Path(null);
		float seg[] = new float[6];
		int segType = 0;
		while (!pi.isDone()) {
			segType = pi.currentSegment(seg);
			switch (segType) {
				case PathIterator.SEG_LINETO:
					path.lineTo(seg[0], seg[1]);
					break;
				case PathIterator.SEG_CLOSE:
					path.close();
					break;
				case PathIterator.SEG_MOVETO:
					path.moveTo(seg[0], seg[1]);
					break;
				case PathIterator.SEG_QUADTO:
					path.quadTo(seg[0], seg[1], seg[2], seg[3]);
					break;
				case PathIterator.SEG_CUBICTO:
					path
						.cubicTo(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]);
					break;
				default:
					throw new Error();
			}
			pi.next();
		}

		return path;
	}

}
