// Copyright 2000-2009, FreeHEP
package org.freehep.graphicsio;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Composite;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.GraphicsConfiguration;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Paint;
import java.awt.Panel;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.TexturePaint;
import java.awt.Toolkit;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.GeneralPath;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.CropImageFilter;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageFilter;
import java.awt.image.ImageObserver;
import java.awt.image.RenderedImage;
import java.awt.image.renderable.RenderContext;
import java.awt.image.renderable.RenderableImage;
import java.io.IOException;
import java.text.AttributedCharacterIterator;
import java.text.AttributedCharacterIterator.Attribute;
import java.util.Arrays;
import java.util.Map;

import org.freehep.graphics2d.font.FontEncoder;
import org.freehep.graphics2d.font.FontUtilities;
import org.freehep.graphicsbase.util.images.ImageUtilities;

/**
 * This class provides an abstract VectorGraphicsIO class for specific output
 * drivers.
 *
 * @author Charles Loomis
 * @author Mark Donszelmann
 * @author Steffen Greiffenberg
 */
public abstract class AbstractVectorGraphicsIO extends VectorGraphicsIO {

    private static final String rootKey = AbstractVectorGraphicsIO.class
            .getName();

    public static final String EMIT_WARNINGS = rootKey + ".EMIT_WARNINGS";

    public static final String TEXT_AS_SHAPES = rootKey + "." + FontConstants.TEXT_AS_SHAPES;

    public static final String EMIT_ERRORS = rootKey + ".EMIT_ERRORS";

    public static final String CLIP = rootKey+".CLIP";

    /*
     * ================================================================================
     * Table of Contents: ------------------ 1. Constructors & Factory Methods
     * 2. Document Settings 3. Header, Trailer, Multipage & Comments 3.1 Header &
     * Trailer 3.2 MultipageDocument methods 4. Create & Dispose 5. Drawing
     * Methods 5.1. shapes (draw/fill) 5.1.1. lines, rectangles, round
     * rectangles 5.1.2. polylines, polygons 5.1.3. ovals, arcs 5.1.4. shapes
     * 5.2. Images 5.3. Strings 6. Transformations 7. Clipping 8. Graphics State /
     * Settings 8.1. stroke/linewidth 8.2. paint/color 8.3. font 8.4. rendering
     * hints 9. Auxiliary 10. Private/Utility Methods
     * ================================================================================
     */

    private Dimension size;

    private Component component;

    private boolean doRestoreOnDispose;

    private Rectangle deviceClip;

    /**
     * Untransformed clipping Area defined by the user
     */
    private Area userClip;

    private AffineTransform currentTransform;
    
    // only for use in writeSetTransform to calculate the difference.
	private AffineTransform oldTransform = new AffineTransform();

    private Composite currentComposite;

    private Stroke currentStroke;

    private RenderingHints hints;

    /*
     * ================================================================================
     * 1. Constructors & Factory Methods
     * ================================================================================
     */

    /**
     * Constructs a Graphics context with the following graphics state:
     * <UL>
     * <LI>Paint: black
     * <LI>Font: Dailog, Plain, 12pt
     * <LI>Stroke: Linewidth 1.0; No Dashing; Miter Join Style; Miter Limit 10;
     * Square Endcaps;
     * <LI>Transform: Identity
     * <LI>Composite: AlphaComposite.SRC_OVER
     * <LI>Clip: Rectangle(0, 0, size.width, size.height)
     * </UL>
     *
     * @param size rectangle specifying the bounds of the image
     * @param doRestoreOnDispose true if writeGraphicsRestore() should be called
     *        when this graphics context is disposed of.
     */
    protected AbstractVectorGraphicsIO(Dimension size,
            boolean doRestoreOnDispose) {
        super();

        this.size = size;
        this.component = null;
        this.doRestoreOnDispose = doRestoreOnDispose;

        deviceClip = (size != null ? new Rectangle(0, 0, size.width,
                size.height) : null);
        userClip = null;
        currentTransform = new AffineTransform();
        currentComposite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER);
        currentStroke = new BasicStroke(1.0f, BasicStroke.CAP_SQUARE,
                BasicStroke.JOIN_MITER, 10.0f, null, 0.0f);

        super.setColor(Color.BLACK);
        super.setBackground(Color.BLACK);
        super.setFont(new Font("Dialog", Font.PLAIN, 12));

        // Initialize the rendering hints.
        hints = new RenderingHints(null);
    }

    /**
     * Constructs a Graphics context with the following graphics state:
     * <UL>
     * <LI>Paint: The color of the component.
     * <LI>Font: The font of the component.
     * <LI>Stroke: Linewidth 1.0; No Dashing; Miter Join Style; Miter Limit 10;
     * Square Endcaps;
     * <LI>Transform: The getDefaultTransform for the GraphicsConfiguration of
     * the component.
     * <LI>Composite: AlphaComposite.SRC_OVER
     * <LI>Clip: The size of the component, Rectangle(0, 0, size.width,
     * size.height)
     * </UL>
     *
     * @param component to be used to initialize the values of the graphics
     *        state
     * @param doRestoreOnDispose true if writeGraphicsRestore() should be called
     *        when this graphics context is disposed of.
     */
    protected AbstractVectorGraphicsIO(Component component,
            boolean doRestoreOnDispose) {
        super();

        this.size = component.getSize();
        this.component = component;
        this.doRestoreOnDispose = doRestoreOnDispose;

        deviceClip = (size != null ? new Rectangle(0, 0, size.width,
                size.height) : null);
        userClip = null;
        GraphicsConfiguration gc = component.getGraphicsConfiguration();
        currentTransform = (gc != null) ? gc.getDefaultTransform()
                : new AffineTransform();
        currentComposite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER);
        currentStroke = new BasicStroke(1.0f, BasicStroke.CAP_SQUARE,
                BasicStroke.JOIN_MITER, 10.0f, null, 0.0f);

        super.setFont(component.getFont());
        super.setBackground(component.getBackground());
        super.setColor(component.getForeground());

        // Initialize the rendering hints.
        hints = new RenderingHints(null);
    }

    /**
     * Constructs a subgraphics context.
     *
     * @param graphics context to clone from
     * @param doRestoreOnDispose true if writeGraphicsRestore() should be called
     *        when this graphics context is disposed of.
     */
    protected AbstractVectorGraphicsIO(AbstractVectorGraphicsIO graphics,
            boolean doRestoreOnDispose) {
        super(graphics);
        this.doRestoreOnDispose = doRestoreOnDispose;

        size = new Dimension(graphics.size);
        component = graphics.component;

        deviceClip = new Rectangle(graphics.deviceClip);
        userClip = (graphics.userClip != null) ? new Area(graphics.userClip)
                : null;
        currentTransform = new AffineTransform(graphics.currentTransform);
        currentComposite = graphics.currentComposite;
        currentStroke = graphics.currentStroke;
        hints = graphics.hints;
    }

    /*
     * ================================================================================ |
     * 2. Document Settings
     * ================================================================================
     */
    public Dimension getSize() {
        return size;
    }

    public Component getComponent() {
        return component;
    }

    /*
     * ================================================================================ |
     * 3. Header, Trailer, Multipage & Comments
     * ================================================================================
     */
    /* 3.1 Header & Trailer */
    public void startExport() {
        try {
            writeHeader();

            // delegate this to openPage if it is a MultiPage document
            if (!(this instanceof MultiPageDocument)) {
                writeGraphicsState();
                writeBackground();
            }
        } catch (IOException e) {
            handleException(e);
        }
    }

    public void endExport() {
        try {
            dispose();
            writeTrailer();
            closeStream();
        } catch (IOException e) {
            handleException(e);
        }
    }

    /**
     * Called to write the header part of the output.
     */
    public abstract void writeHeader() throws IOException;

    /**
     * Called to write the initial graphics state.
     */
    public void writeGraphicsState() throws IOException {
		writePaint(getPrintColor(getColor()));

		writeSetTransform(getTransform());

//		writeStroke(getStroke());
		
        setClip(getClip());
         
        
		// Silly assignment, Font is written when String is drawed and "extra" writeFont does not exist
		// setFont(getFont());

		// Silly assignment and "extra" writeComposite does not exist
		// setComposite(getComposite);
    }

    public abstract void writeBackground() throws IOException;

    /**
     * Called to write the trailing part of the output.
     */
    public abstract void writeTrailer() throws IOException;

    /**
     * Called to close the stream you are writing to.
     */
    public abstract void closeStream() throws IOException;

    public void printComment(String comment) {
        try {
            writeComment(comment);
        } catch (IOException e) {
            handleException(e);
        }
    }

    /**
     * Called to Write out a comment.
     *
     * @param comment to be written
     */
    public abstract void writeComment(String comment) throws IOException;

    /* 3.2 MultipageDocument methods */
    protected void resetClip(Rectangle clip) {
        deviceClip = clip;
        userClip = null;
    }

    /*
     * ================================================================================
     * 4. Create & Dispose
     * ================================================================================
     */
    /**
     * Disposes of the graphics context. If on creation restoreOnDispose was
     * true, writeGraphicsRestore() will be called.
     */
    public void dispose() {
        try {
            // Swing sometimes calls dispose several times for a given
            // graphics object. Ensure that the grestore is only written
            // once if this happens.
            if (doRestoreOnDispose) {
                writeGraphicsRestore();
                doRestoreOnDispose = false;
            }
        } catch (IOException e) {
            handleException(e);
        }
    }

    /**
     * Writes out the save of a graphics context for a later restore. Some
     * implementations keep track of this by hand if the output format does not
     * support it.
     */
    protected abstract void writeGraphicsSave() throws IOException;

    /**
     * Writes out the restore of a graphics context. Some implementations keep
     * track of this by hand if the output format does not support it.
     */
    protected abstract void writeGraphicsRestore() throws IOException;

    /*
     * ================================================================================ |
     * 5. Drawing Methods
     * ================================================================================
     */

    /* 5.3. Images */
    public boolean drawImage(Image image, int x, int y, ImageObserver observer) {
        int imageWidth = image.getWidth(observer);
        int imageHeight = image.getHeight(observer);
        return drawImage(image, x, y, x + imageWidth, y + imageHeight, 0, 0,
                imageWidth, imageHeight, null, observer);
    }

    public boolean drawImage(Image image, int x, int y, int width, int height,
            ImageObserver observer) {
        int imageWidth = image.getWidth(observer);
        int imageHeight = image.getHeight(observer);
        return drawImage(image, x, y, x + width, y + height, 0, 0, imageWidth,
                imageHeight, null, observer);
    }

    public boolean drawImage(Image image, int x, int y, int width, int height,
            Color bgColor, ImageObserver observer) {
        int imageWidth = image.getWidth(observer);
        int imageHeight = image.getHeight(observer);
        return drawImage(image, x, y, x + width, y + height, 0, 0, imageWidth,
                imageHeight, bgColor, observer);
    }

    public boolean drawImage(Image image, int x, int y, Color bgColor,
            ImageObserver observer) {
        int imageWidth = image.getWidth(observer);
        int imageHeight = image.getHeight(observer);
        return drawImage(image, x, y, x + imageWidth, y + imageHeight, 0, 0,
                imageWidth, imageHeight, bgColor, observer);
    }

    public boolean drawImage(Image image, int dx1, int dy1, int dx2, int dy2,
            int sx1, int sy1, int sx2, int sy2, ImageObserver observer) {
        return drawImage(image, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, null,
                observer);
    }

    public boolean drawImage(Image image, AffineTransform xform,
            ImageObserver observer) {
        drawRenderedImage(ImageUtilities.createRenderedImage(image, observer,
                null), xform);
        return true;
    }

    public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) {
        drawImage(op.filter(img, null), x, y, null);
    }

    // NOTE: not tested yet!!!
    public void drawRenderableImage(RenderableImage image, AffineTransform xform) {
        drawRenderedImage(image.createRendering(new RenderContext(
                new AffineTransform(), getRenderingHints())), xform);
    }

    /**
     * Draw and resizes (transparent) image. Calls writeImage(...).
     *
     * @param image image to be drawn
     * @param dx1 destination image bounds
     * @param dy1 destination image bounds
     * @param dx2 destination image bounds
     * @param dy2 destination image bounds
     * @param sx1 source image bounds
     * @param sy1 source image bounds
     * @param sx2 source image bounds
     * @param sy2 source image bounds
     * @param bgColor background color
     * @param observer for updates if image still incomplete
     * @return true if successful
     */
    public boolean drawImage(Image image, int dx1, int dy1, int dx2, int dy2,
            int sx1, int sy1, int sx2, int sy2, Color bgColor,
            ImageObserver observer) {
        try {
            int srcX = Math.min(sx1, sx2);
            int srcY = Math.min(sy1, sy2);
            int srcWidth = Math.abs(sx2 - sx1);
            int srcHeight = Math.abs(sy2 - sy1);
            int width = Math.abs(dx2 - dx1);
            int height = Math.abs(dy2 - dy1);

            if ((srcX != 0) || (srcY != 0)
                    || (srcWidth != image.getWidth(observer))
                    || (srcHeight != image.getHeight(observer))) {
                // crop the source image
                ImageFilter crop = new CropImageFilter(srcX, srcY, srcWidth,
                        srcHeight);
                image = Toolkit.getDefaultToolkit().createImage(
                        new FilteredImageSource(image.getSource(), crop));
                MediaTracker mediaTracker = new MediaTracker(new Panel());
                mediaTracker.addImage(image, 0);
                try {
                    mediaTracker.waitForAll();
                } catch (InterruptedException e) {
                    handleException(e);
                }
            }

            boolean flipHorizontal = (dx2 < dx1) ^ (sx2 < sx1); // src flipped
                                                                // and not dest
                                                                // flipped or
                                                                // vice versa
            boolean flipVertical = (dy2 < dy1) ^ (sy2 < sy1); // <=> source
                                                                // flipped XOR
                                                                // dest flipped

            double tx = (flipHorizontal) ? (double) dx2 : (double) dx1;
            double ty = (flipVertical) ? (double) dy2 : (double) dy1;

            double sx = (double) width / srcWidth;
            sx = flipHorizontal ? -1 * sx : sx;
            double sy = (double) height / srcHeight;
            sy = flipVertical ? -1 * sy : sy;

            writeImage(ImageUtilities.createRenderedImage(image, observer,
                    bgColor), new AffineTransform(sx, 0, 0, sy, tx, ty),
                    bgColor);
            return true;
        } catch (IOException e) {
            handleException(e);
            return false;
        }
    }

    /*
     * // first use the original orientation int clippingWidth = Math.abs(sx2 -
     * sx1); int clippingHeight = Math.abs(sy2 - sy1); int sulX = Math.min(sx1,
     * sx2); int sulY = Math.min(sy1, sy2); Image background = null; if (bgColor !=
     * null) { // Draw the image on the background color // maybe we could crop
     * it and fill the transparent pixels in one go // by means of a filter.
     * background = new BufferedImage(clippingWidth, clippingHeight,
     * BufferedImage.TYPE_INT_ARGB); Graphics bgGfx = background.getGraphics();
     * bgGfx.drawImage(image, 0, 0, clippingWidth, clippingWidth, sulX, sulY,
     * sulX+clippingWidth, sulY+clippingHeight, getPrintColor(bgColor),
     * observer); } else { // crop the source image ImageFilter crop = new
     * CropImageFilter(sulX, sulY, clippingWidth, clippingHeight); background =
     * Toolkit.getDefaultToolkit().createImage(new
     * FilteredImageSource(image.getSource(), crop)); MediaTracker mediaTracker =
     * new MediaTracker(new Panel()); mediaTracker.addImage(background, 0); try {
     * mediaTracker.waitForAll(); } catch (InterruptedException e) {
     * handleException(e); } } // now flip the image if necessary boolean
     * flipHorizontal = (dx2<dx1) ^ (sx2<sx1); // src flipped and not dest
     * flipped or vice versa boolean flipVertical = (dy2<dy1) ^ (sy2<sy1); //
     * <=> source flipped XOR dest flipped int destWidth = Math.abs(dx2-dx1);
     * int destHeight = Math.abs(dy2-dy1); try { return writeImage(background,
     * flipHorizontal ? dx2 : dx1, flipVertical ? dy2 : dy1, flipHorizontal ?
     * -destWidth : destWidth, flipVertical ? -destHeight : destHeight, (bgColor ==
     * null), observer); } catch (IOException e) { return false; } }
     */
    /**
     * Draws a rendered image using a transform.
     *
     * @param image to be drawn
     * @param xform transform to be used on the image
     */
    public void drawRenderedImage(RenderedImage image, AffineTransform xform) {
        try {
            writeImage(image, xform, null);
        } catch (Exception e) {
            handleException(e);
        }
    }

    protected abstract void writeImage(RenderedImage image,
            AffineTransform xform, Color bkg) throws IOException;

    /**
     * Clears rectangle by painting it with the backgroundColor.
     *
     * @param x rectangle to be cleared.
     * @param y rectangle to be cleared.
     * @param width rectangle to be cleared.
     * @param height rectangle to be cleared.
     */
    public void clearRect(double x, double y, double width, double height) {
        Paint temp = getPaint();
        setPaint(getBackground());
        fillRect(x, y, width, height);
        setPaint(temp);
    }

    /**
     * Draws the string at (x, y). If TEXT_AS_SHAPES is set
     * {@link #drawGlyphVector(java.awt.font.GlyphVector, float, float)} is used, otherwise
     * {@link #writeString(String, double, double)} for a more direct output of the string.
     *
     * @param string
     * @param x
     * @param y
     */
    public void drawString(String string, double x, double y) {
        // something to draw?
        if (string == null || string.equals("")) {
            return;
        }

        // draw strings directly?
        if (isProperty(TEXT_AS_SHAPES)) {
        	
        	Font font = getFont();
        	
            // NOTE, see FVG-199, createGlyphVector does not seem to create the proper glyphcodes
            // for either ZapfDingbats or Symbol. We use our own encoding which seems to work...
        	String fontName = font.getName();
        	if (fontName.equals("Symbol") || fontName.equals("ZapfDingbats")) {
        		string = FontEncoder.getEncodedString(string, fontName);
        		// use a standard font, not Symbol.
        		font = new Font("Serif", font.getStyle(), font.getSize());
        	}
        	
        	// create glyph
            GlyphVector gv = font.createGlyphVector(getFontRenderContext(), string);

            // draw it
            drawGlyphVector(gv, (float) x, (float) y);
        } else {
            // write string directly
            try {
                writeString(string, x, y);
            } catch (IOException e) {
                handleException(e);
            }
        }
    }

    protected abstract void writeString(String string, double x, double y)
            throws IOException;

    /**
     * Use the transformation of the glyphvector and draw it
     *
     * @param g
     * @param x
     * @param y
     */
    public void drawGlyphVector(GlyphVector g, float x, float y) {
        fill(g.getOutline(x, y));
    }

    public void drawString(AttributedCharacterIterator iterator, float x,
            float y) {

        // TextLayout draws the iterator as glyph vector
        // thats why we use it only in the case of TEXT_AS_SHAPES,
        // otherwise tagged strings are always written as glyphs
        if (isProperty(TEXT_AS_SHAPES)) {
            // draws all attributes
            TextLayout tl = new TextLayout(iterator, getFontRenderContext());
            tl.draw(this, x, y);
        } else {
            // reset to that font at the end
            Font font = getFont();

            // initial attributes, we us TextAttribute.equals() rather
            // than Font.equals() because using Font.equals() we do
            // not get a 'false' if underline etc. is changed
            Map<Attribute, Object> attributes = FontUtilities.getAttributes(font);

            // stores all characters which are written with the same font
            // if font is changed the buffer will be written and cleared
            // after it
            StringBuffer sb = new StringBuffer();

            for (char c = iterator.first();
                 c != AttributedCharacterIterator.DONE;
                 c = iterator.next()) {

                // append c if font is not changed
                if (attributes.equals(iterator.getAttributes())) {
                    sb.append(c);

                } else {
                    // TextLayout does not like 0 length strings
                    if (sb.length() > 0) {
                        // draw sb if font is changed
                        drawString(sb.toString(), x, y);
    
                        // change the x offset for the next drawing
                        // FIXME: change y offset for vertical text
                        TextLayout tl = new TextLayout(
                            sb.toString(),
                            attributes,
                            getFontRenderContext());
    
                        // calculate real width
                        x = x + Math.max(
                            tl.getAdvance(),
                            (float)tl.getBounds().getWidth());
                    }
                    
                    // empty sb
                    sb = new StringBuffer();
                    sb.append(c);

                    // change the font
                    attributes = iterator.getAttributes();
                    setFont(new Font(attributes));
                }
            }

            // draw the rest
            if (sb.length() > 0) {
                drawString(sb.toString(), x, y);
            }

            // use the old font for the next string drawing
            setFont(font);
        }
    }

    /*
     * ================================================================================ |
     * 6. Transformations
     * ================================================================================
     */
    /**
     * Get the current transform.
     *
     * @return current transform
     */
    public AffineTransform getTransform() {
        return new AffineTransform(currentTransform);
    }

    /**
     * Set the current transform. Calls writeSetTransform(Transform).
     *
     * @param transform to be set
     */
    public void setTransform(AffineTransform transform) {
        // Fix for FREEHEP-569
    	oldTransform.setTransform(currentTransform);
        currentTransform.setTransform(transform);
        try {
            writeSetTransform(transform);
        } catch (IOException e) {
            handleException(e);
        }
    }

    /**
     * Transforms the current transform. Calls writeTransform(Transform)
     *
     * @param transform to be applied
     */
    public void transform(AffineTransform transform) {
        currentTransform.concatenate(transform);
        try {
            writeTransform(transform);
        } catch (IOException e) {
            handleException(e);
        }
    }

    /**
     * Translates the current transform. Calls writeTransform(Transform)
     *
     * @param x amount by which to translate
     * @param y amount by which to translate
     */
    public void translate(double x, double y) {
        currentTransform.translate(x, y);
        try {
            writeTransform(new AffineTransform(1, 0, 0, 1, x, y));
        } catch (IOException e) {
            handleException(e);
        }
    }

    /**
     * Rotate the current transform over the Z-axis. Calls
     * writeTransform(Transform). Rotating with a positive angle theta rotates
     * points on the positive x axis toward the positive y axis.
     *
     * @param theta radians over which to rotate
     */
    public void rotate(double theta) {
        currentTransform.rotate(theta);
        try {
            writeTransform(new AffineTransform(Math.cos(theta),
                    Math.sin(theta), -Math.sin(theta), Math.cos(theta), 0, 0));
        } catch (IOException e) {
            handleException(e);
        }
    }

    /**
     * Scales the current transform. Calls writeTransform(Transform).
     *
     * @param sx amount used for scaling
     * @param sy amount used for scaling
     */
    public void scale(double sx, double sy) {
        currentTransform.scale(sx, sy);
        try {
            writeTransform(new AffineTransform(sx, 0, 0, sy, 0, 0));
        } catch (IOException e) {
            handleException(e);
        }
    }

    /**
     * Shears the current transform. Calls writeTransform(Transform).
     *
     * @param shx amount for shearing
     * @param shy amount for shearing
     */
    public void shear(double shx, double shy) {
        currentTransform.shear(shx, shy);
        try {
            writeTransform(new AffineTransform(1, shy, shx, 1, 0, 0));
        } catch (IOException e) {
            handleException(e);
        }
    }

    /**
     * Writes out the transform as it needs to be concatenated to the internal
     * transform of the output format. If there is no implementation of an
     * internal transform, then this method needs to do nothing, BUT all
     * coordinates need to be transformed by the currentTransform before being
     * written out.
     *
     * @param transform to be written
     */
    protected abstract void writeTransform(AffineTransform transform)
            throws IOException;

    /**
     * Clears any existing transformation and sets the a new one.
     * The default implementation calls writeTransform using the
     * inverted affine transform to calculate it.
s     *
     * @param transform to be written
     */
    protected void writeSetTransform(AffineTransform transform) throws IOException {
    	try {
	    	AffineTransform deltaTransform = new AffineTransform(transform);
	        deltaTransform.concatenate(oldTransform.createInverse());
	    	writeTransform(deltaTransform);
    	} catch (NoninvertibleTransformException e) {
    		// ignored...
    		System.err.println("Warning: (ignored) Could not invert matrix: "+oldTransform);
    	}
    }

    /*
     * ================================================================================ |
     * 7. Clipping
     * ================================================================================
     */

    /**
     * Gets the current clip in form of a Shape (Rectangle).
     *
     * @return current clip
     */
    public Shape getClip() {
        return (userClip != null) ? new Area(untransformShape(userClip)) : null;
    }

    /**
     * Gets the current clip in form of a Rectangle.
     *
     * @return current clip
     */
    public Rectangle getClipBounds() {
        Shape clip = getClip();
        return (clip != null) ? getClip().getBounds() : null;
    }

    /**
     * Gets the current clip in form of a Rectangle.
     *
     * @return current clip
     */
    public Rectangle getClipBounds(Rectangle r) {
        Rectangle bounds = getClipBounds();
        if (bounds != null)
            r.setBounds(bounds);
        return r;
    }

    /**
     * Clips rectangle. Calls clip(Rectangle).
     *
     * @param x rectangle for clipping
     * @param y rectangle for clipping
     * @param width rectangle for clipping
     * @param height rectangle for clipping
     */
    public void clipRect(int x, int y, int width, int height) {
        clip(new Rectangle(x, y, width, height));
    }

    /**
     * Clips rectangle. Calls clip(Rectangle2D).
     *
     * @param x rectangle for clipping
     * @param y rectangle for clipping
     * @param width rectangle for clipping
     * @param height rectangle for clipping
     */
    public void clipRect(double x, double y, double width, double height) {
        clip(new Rectangle2D.Double(x, y, width, height));
    }

    /**
     * Clips rectangle. Calls clip(Rectangle).
     *
     * @param x rectangle for clipping
     * @param y rectangle for clipping
     * @param width rectangle for clipping
     * @param height rectangle for clipping
     */
    public void setClip(int x, int y, int width, int height) {
        setClip(new Rectangle(x, y, width, height));
    }

    /**
     * Clips rectangle. Calls clip(Rectangle2D).
     *
     * @param x rectangle for clipping
     * @param y rectangle for clipping
     * @param width rectangle for clipping
     * @param height rectangle for clipping
     */
    public void setClip(double x, double y, double width, double height) {
        setClip(new Rectangle2D.Double(x, y, width, height));
    }

    /**
     * Clips shape. Clears userClip and calls clip(Shape).
     *
     * @param s used for clipping
     */
    public void setClip(Shape s) {

        Shape ts = transformShape(s);
        userClip = (ts != null) ? new Area(ts) : null;

        try {
            writeSetClip(s);
        } catch (IOException e) {
            handleException(e);
        }
    }

    /**
     * Clips using given shape. Dispatches to writeClip(Rectangle),
     * writeClip(Rectangle2D) or writeClip(Shape).
     *
     * @param s used for clipping
     */
    public void clip(Shape s) {
        Shape ts = transformShape(s);
        if (userClip != null) {
            if (ts != null) {
                userClip.intersect(new Area(ts));
            } else {
                userClip = null;
            }
        } else {
            userClip = (ts != null) ? new Area(ts) : null;
        }

        try {
	    writeClip(s);
        } catch (IOException e) {
            handleException(e);
        }
    }

    /**
     * Write out Shape clip.
     *
     * @param shape to be used for clipping
     */
    protected abstract void writeClip(Shape shape) throws IOException;

    /**
     * Write out Shape clip.
     *
     * @param shape to be used for clipping
     */
    protected abstract void writeSetClip(Shape shape) throws IOException;

    /*
     * ================================================================================ |
     * 8. Graphics State
     * ================================================================================
     */
    /* 8.1. stroke/linewidth */
    /**
     * Get the current stroke.
     *
     * @return current stroke
     */
    public Stroke getStroke() {
        return currentStroke;
    }

    /**
     * Sets the current stroke. Calls writeStroke if stroke is unequal to the
     * current stroke.
     *
     * @param stroke to be set
     */
    public void setStroke(Stroke stroke) {
        if (stroke.equals(currentStroke)) {
            return;
        }
        try {
            writeStroke(stroke);
        } catch (IOException e) {
            handleException(e);
        }
        currentStroke = stroke;
    }

    /**
     * Writes the current stroke. If stroke is an instance of BasicStroke it
     * will call writeWidth, writeCap, writeJoin, writeMiterLimit and writeDash,
     * if any were different than the current stroke.
     */
    protected void writeStroke(Stroke stroke) throws IOException {
        if (stroke instanceof BasicStroke) {
            BasicStroke ns = (BasicStroke) stroke;

            // get the current values for comparison if available,
            // otherwise set them to -1="undefined"
            int currentCap = -1, currentJoin = -1;
            float currentWidth = -1, currentLimit = -1, currentDashPhase = -1;
            float[] currentDashArray = null;
            if ((currentStroke != null)
                    && (currentStroke instanceof BasicStroke)) {
                BasicStroke cs = (BasicStroke) currentStroke;
                currentCap = cs.getEndCap();
                currentJoin = cs.getLineJoin();
                currentWidth = cs.getLineWidth();
                currentLimit = cs.getMiterLimit();
                currentDashArray = cs.getDashArray();
                currentDashPhase = cs.getDashPhase();
            }

            // Check the linewidth.
            float width = ns.getLineWidth();
            if (currentWidth != width) {
                writeWidth(width);
            }

            // Check the line caps.
            int cap = ns.getEndCap();
            if (currentCap != cap) {
                writeCap(cap);
            }

            // Check the line joins.
            int join = ns.getLineJoin();
            if (currentJoin != join) {
                writeJoin(join);
            }

            // Check the miter limit and validity of value
            float limit = ns.getMiterLimit();
            if ((currentLimit != limit) && (limit >= 1.0f)) {
                writeMiterLimit(limit);
            }

            // Check to see if there are differences in the phase or dash
            if(!Arrays.equals(currentDashArray, ns.getDashArray()) ||
               (currentDashPhase != ns.getDashPhase())) {

                // write the dashing parameters
                if (ns.getDashArray() != null) {
                    writeDash(ns.getDashArray(), ns.getDashPhase());
                } else {
                    writeDash(new float[0], ns.getDashPhase());
                }
            }
        }
    }

    /**
     * Writes out the width of the stroke.
     *
     * @param width of the stroke
     */
    protected void writeWidth(float width) throws IOException {
        writeWarning(getClass() + ": writeWidth() not implemented.");
    }

    /**
     * Writes out the cap of the stroke.
     *
     * @param cap of the stroke
     */
    protected void writeCap(int cap) throws IOException {
        writeWarning(getClass() + ": writeCap() not implemented.");
    }

    /**
     * Writes out the join of the stroke.
     *
     * @param join of the stroke
     */
    protected void writeJoin(int join) throws IOException {
        writeWarning(getClass() + ": writeJoin() not implemented.");
    }

    /**
     * Writes out the miter limit of the stroke.
     *
     * @param limit miter limit of the stroke
     */
    protected void writeMiterLimit(float limit) throws IOException {
        writeWarning(getClass() + ": writeMiterLimit() not implemented.");
    }

    /**
     * Writes out the dash of the stroke.
     *
     * @param dash dash pattern, empty array is solid line
     * @param phase of the dash pattern
     */
    protected void writeDash(float[] dash, float phase) throws IOException {
        // for backward compatibility
        double[] dd = new double[dash.length];
        for (int i = 0; i < dash.length; i++) {
            dd[i] = dash[i];
        }
        writeDash(dd, (double)phase);
    }

    /**
     * Writes out the dash of the stroke.

     * @deprecated use writeDash(float[], float)
     * @param dash dash pattern, empty array is solid line
     * @param phase of the dash pattern
     */
    protected void writeDash(double[] dash, double phase) throws IOException {
        writeWarning(getClass() + ": writeDash() not implemented.");
    }

    /* 8.2 Paint */
    public void setColor(Color color) {
    	if (color == null) return;
    	
        if (color.equals(getColor()))
            return;

        try {
            super.setColor(color);
            writePaint(getPrintColor(color));
        } catch (IOException e) {
            handleException(e);
        }
    }

    /**
     * Sets the current paint. Dispatches to writePaint(Color),
     * writePaint(GradientPaint), writePaint(TexturePaint paint) or
     * writePaint(Paint). In the case paint is a Color the current color is also
     * changed.
     *
     * @param paint to be set
     */
    public void setPaint(Paint paint) {
    	if (paint == null) return;
    	
        if (paint.equals(getPaint()))
            return;

        try {
            if (paint instanceof Color) {
                setColor((Color) paint);
            } else if (paint instanceof GradientPaint) {
                super.setPaint(paint);
                writePaint((GradientPaint) paint);
            } else if (paint instanceof TexturePaint) {
                super.setPaint(paint);
                writePaint((TexturePaint) paint);
            } else {
                super.setPaint(paint);
                writePaint(paint);
            }
        } catch (IOException e) {
            handleException(e);
        }
    }

    /**
     * Writes out paint as the given color.
     *
     * @param color to be written
     */
    protected abstract void writePaint(Color color) throws IOException;

    /**
     * Writes out paint as the given gradient.
     *
     * @param paint to be written
     */
    protected abstract void writePaint(GradientPaint paint) throws IOException;

    /**
     * Writes out paint as the given texture.
     *
     * @param paint to be written
     */
    protected abstract void writePaint(TexturePaint paint) throws IOException;

    /**
     * Writes out paint.
     *
     * @param paint to be written
     */
    protected abstract void writePaint(Paint paint) throws IOException;

    /* 8.3. font */
    /**
     * Gets the current font render context. This returns an standard
     * FontRenderContext with anti-aliasing and uses
     * fractional metrics.
     *
     * @return current font render context
     */
    public FontRenderContext getFontRenderContext() {
        // NOTE: not sure?
        // Fixed for VG-285
        return new FontRenderContext(new AffineTransform(1, 0, 0, 1, 0, 0),
                true, true);
    }

    /**
     * Gets the fontmetrics.
     *
     * @deprecated
     * @param font to be used for retrieving fontmetrics
     * @return fontmetrics for given font
     */
    public FontMetrics getFontMetrics(Font font) {
        return Toolkit.getDefaultToolkit().getFontMetrics(font);
    }

    /* 8.4. rendering hints */
    /**
     * Gets a copy of the rendering hints.
     *
     * @return clone of table of rendering hints.
     */
    public RenderingHints getRenderingHints() {
        return (RenderingHints) hints.clone();
    }

    /**
     * Adds to table of rendering hints.
     *
     * @param hints table to be added
     */
    public void addRenderingHints(Map<?, ?> hints) {
        this.hints.putAll(hints);
    }

    /**
     * Sets table of rendering hints.
     *
     * @param hints table to be set
     */
    public void setRenderingHints(Map<?, ?> hints) {
        this.hints.clear();
        if (hints instanceof RenderingHints) {
        	RenderingHints renderingHints = (RenderingHints)hints;
        	this.hints.putAll((Map<?, ?>)renderingHints.clone());
        } else {
        	this.hints.putAll(hints);
        }
    }

    /**
     * Gets a given rendering hint.
     *
     * @param key hint key
     * @return hint associated to key
     */
    public Object getRenderingHint(RenderingHints.Key key) {
        return hints.get(key);
    }

    /**
     * Sets a given rendering hint.
     *
     * @param key hint key
     * @param hint to be associated with key
     */
    public void setRenderingHint(RenderingHints.Key key, Object hint) {
        // extra protection, failed on under MacOS X 10.2.6, jdk 1.4.1_01-39/14
        if ((key == null) || (hint == null))
            return;
        hints.put(key, hint);
    }

    /**
     * Sets the current font.
     *
     * @param font to be set
     */
    public void setFont(Font font) {
    	if (font == null) return;
    	
        // FIXME: maybe add delayed setting
        super.setFont(font);

        // write the font
        try {
            writeFont(font);
        } catch (IOException e) {
            handleException(e);
        }
    }

    /**
     * Writes the font
     *
     * @param font to be written
     */
    protected abstract void writeFont(Font font) throws IOException;

    /*
	 * ================================================================================ |
	 * 9. Auxiliary
	 * ================================================================================
	 */
    /**
     * Gets current composite.
     *
     * @return current composite
     */
    public Composite getComposite() {
        return currentComposite;
    }

    /**
     * Sets current composite.
     *
     * @param composite to be set
     */
    public void setComposite(Composite composite) {
        currentComposite = composite;
    }

    /**
     * Handles an exception which has been caught. Dispatches exception to
     * writeWarning for UnsupportedOperationExceptions and writeError for others
     *
     * @param exception to be handled
     */
    protected void handleException(Exception exception) {
        if (exception instanceof UnsupportedOperationException) {
            writeWarning(exception);
        } else {
            writeError(exception);
        }
    }

    /**
     * Writes out a warning, by default to System.err.
     *
     * @param exception warning to be written
     */
    protected void writeWarning(Exception exception) {
        writeWarning(exception.getMessage());
    }

    /**
     * Writes out a warning, by default to System.err.
     *
     * @param warning to be written
     */
    protected void writeWarning(String warning) {
        if (isProperty(EMIT_WARNINGS)) {
            System.err.println(warning);
        }
    }

    /**
     * Writes out an error, by default the stack trace is printed.
     *
     * @param exception error to be reported
     */
    protected void writeError(Exception exception) {
        throw new RuntimeException(exception);
        // FIXME decide what we should do
        /*
         * if (isProperty(EMIT_ERRORS)) { System.err.println(exception);
         * exception.printStackTrace(System.err); }
         */
    }

    protected Shape createShape(double[] xPoints, double[] yPoints,
            int nPoints, boolean close) {
        GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
        if (nPoints > 0) {
            path.moveTo((float) xPoints[0], (float) yPoints[0]);
            for (int i = 1; i < nPoints; i++) {
                path.lineTo((float) xPoints[i], (float) yPoints[i]);
            }
            if (close)
                path.closePath();
        }
        return path;
    }

    private Shape transformShape(AffineTransform at, Shape s) {
        if (s == null)
            return null;
        return at.createTransformedShape(s);
    }

    private Shape transformShape(Shape s) {
        return transformShape(getTransform(), s);
    }

    private Shape untransformShape(Shape s) {
        if (s == null)
            return null;
        try {
            return transformShape(getTransform().createInverse(), s);
        } catch (NoninvertibleTransformException e) {
            return null;
        }
    }

    /**
     * Draws an overline for the text at (x, y). The method is usesefull for
     * drivers that do not support overlines by itself.
     *
     * @param text text for width calulation
     * @param font font for width calulation
     * @param x position of text
     * @param y position of text
     */
    protected void overLine(String text, Font font, float x, float y) {
        TextLayout layout = new TextLayout(text, font, getFontRenderContext());
        float width = Math.max(
            layout.getAdvance(),
            (float) layout.getBounds().getWidth());

        GeneralPath path = new GeneralPath();
        path.moveTo(x, y + (float) layout.getBounds().getY() - layout.getAscent());
        path.lineTo(x + width, y + (float) layout.getBounds().getY() - layout.getAscent() - layout.getAscent());
        draw(path);
    }
}
