// Copyright 2000-2006 FreeHEP
package org.freehep.graphicsio.cgm;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.GraphicsConfiguration;
import java.awt.Paint;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.TexturePaint;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.ColorModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;

import org.freehep.graphics2d.PrintColor;
import org.freehep.graphics2d.VectorGraphics;
import org.freehep.graphicsio.AbstractVectorGraphicsIO;
import org.freehep.graphicsio.PageConstants;
import org.freehep.util.UserProperties;
import org.freehep.util.io.TaggedOutput;

/**
 * Implementation of <tt>VectorGraphics</tt> that writes the output to a CGM
 * file. Users of this class have to generate a <tt>TagWriter</tt> and create
 * an instance by invoking the factory method or the constructor. Document
 * specific settings like page size can then be made by the appropriate setter
 * methods. Before starting to draw, <tt>startExport()</tt> must be called.
 * When drawing is finished, call <tt>endExport()</tt>.
 *
 * @author Mark Donszelmann
 * @author Ian Graham - fixed constructor bug, added writeImage for raster
 *         support, and refactored for reuse
 * @version $Id: freehep-graphicsio-cgm/src/main/java/org/freehep/graphicsio/cgm/CGMGraphics2D.java d9a2ef8950b1 2006/03/03 19:08:18 duns $
 */
public class CGMGraphics2D extends AbstractVectorGraphicsIO {

    /*
     * ================================================================================
     * Table of Contents: ------------------ 1. Constructors & Factory Methods
     * 2. Document Settings 3. Header, Trailer, Multipage & Comments 3.1 Header &
     * Trailer 3.3 comments 4. Create 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.
     * Private/Utility Methos 9.1. drawing, shape creation 9.2. font, strings
     * 9.3. images 9.4. transformations 11. Todo List
     * ================================================================================
     */

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

    private static final Color DEFAULT_TRANSPARENT_CELL_COLOR = new Color(1, 2,
            3); // unlikely in real use

    public static final String BACKGROUND = rootKey + "."
            + PageConstants.BACKGROUND;

    public static final String BACKGROUND_COLOR = rootKey + "."
            + PageConstants.BACKGROUND_COLOR;

    public static final String BINARY = rootKey + ".Binary";

    public static final String AUTHOR = rootKey + ".Author";

    public static final String TITLE = rootKey + ".Title";

    public static final String SUBJECT = rootKey + ".Subject";

    public static final String KEYWORDS = rootKey + ".Keywords";

    private static final UserProperties defaultProperties = new UserProperties();
    static {
        defaultProperties.setProperty(BACKGROUND, false);
        defaultProperties.setProperty(BACKGROUND_COLOR, Color.GRAY);

        defaultProperties.setProperty(BINARY, true);
        defaultProperties.setProperty(AUTHOR, "");
        defaultProperties.setProperty(TITLE, "");
        defaultProperties.setProperty(SUBJECT, "");
        defaultProperties.setProperty(KEYWORDS, "");
    }

    public static Properties getDefaultProperties() {
        return defaultProperties;
    }

    public static void setDefaultProperties(Properties newProperties) {
        defaultProperties.setProperties(newProperties);
    }

    public final static String version = "$Revision$";

    private static final int CGM_VERSION = 3;

    private static final double FONTSIZE_CORRECTION = 0.8;

    private static final String[] STANDARD_FONT = { "Courier", "Courier-Bold",
            "Courier-Italic", "Courier-BoldItalic", "Helvetica",
            "Helvetica-Bold", "Helvetica-Italic", "Helvetica-BoldItalic",
            "TimesRoman", "TimesRoman-Bold", "TimesRoman-Italic",
            "TimesRoman-BoldItalic", "Symbol", "ZapfDingbats" };

    // output
    private OutputStream ros;

    private TaggedOutput os;

    private Color transparentCellColor = null;

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

    public CGMGraphics2D(File file, Dimension size)
            throws FileNotFoundException {
        this(new FileOutputStream(file), size);
    }

    public CGMGraphics2D(File file, Component component)
            throws FileNotFoundException {
        this(new FileOutputStream(file), component);
    }

    public CGMGraphics2D(OutputStream os, Dimension size) {
        super(size, false);
        init(os);
    }

    public CGMGraphics2D(OutputStream os, Component component) {
        super(component, false);
        init(os);
    }

    private void init(OutputStream os) {
        this.ros = os;
        initProperties(defaultProperties);
    }

    /** Cloneconstructor */
    protected CGMGraphics2D(CGMGraphics2D graphics) {
        super(graphics, false);

        os = graphics.os;
        transparentCellColor = graphics.transparentCellColor;
    }

    /*
     * ================================================================================ |
     * 2. Document Settings
     * ================================================================================
     */

    /*
     * ================================================================================ |
     * 3. Header, Trailer, Multipage & Comments
     * ================================================================================
     */

    /*--------------------------------------------------------------------------------
     | 3.1 Header & Trailer
     *--------------------------------------------------------------------------------*/

    /**
     * Writes the catalog, docinfo, preferences, and (as we use only single page
     * output the page tree. Written as a template method to enable reuse.
     */
    public void writeHeader() throws IOException {
        if (isProperty(BINARY)) {
            os = new CGMOutputStream(new BufferedOutputStream(ros), CGM_VERSION);
            ((CGMOutputStream) os).setRealPrecision(false, true);
        } else {
            os = new CGMWriter(new BufferedWriter(new OutputStreamWriter(ros)),
                    CGM_VERSION);
            ((CGMWriter) os).setRealPrecision(false, true);
        }

        os.writeTag(new BeginMetafile(
                "Generated by CGMGraphics2D, FreeHEP Graphics2D Driver"));
        writeMetafileDescriptor();

        os.writeTag(new BeginPicture("Picture"));
        writePictureDescriptor();

        writeBeginningOfPictureBody();

        // FIXME
        if (getClip() != null) {
            setClip(getClip());
        }
    }

    protected void writeMetafileDescriptor() throws IOException {
        String producer = getClass().getName();
        if (!isDeviceIndependent()) {
            producer += " " + version.substring(1, version.length() - 1);
        }
        os.writeTag(new MetafileVersion(CGM_VERSION));
        os.writeTag(new MetafileDescription("Producer: " + producer));
        os.writeTag(new MetafileDescription("Creator: " + getCreator()));
        // info
        os.writeTag(new MetafileDescription("Title: " + getProperty(TITLE)));
        os.writeTag(new MetafileDescription("Author: " + getProperty(AUTHOR)));
        os
                .writeTag(new MetafileDescription("Subject: "
                        + getProperty(SUBJECT)));
        os.writeTag(new MetafileDescription("Info: " + getProperty(KEYWORDS)));

        if (!isDeviceIndependent()) {
            os.writeTag(new MetafileDescription("CreationDate: "
                    + (new SimpleDateFormat().format(new Date()))));
        }
        os.writeTag(new MetafileElementList(MetafileElementList.VERSION_3_SET));
        os.writeTag(new VDCType(VDCType.REAL));
        // os.writeTag(new VDCRealPrecision()); // PowerPoint 10 does not accept
        os.writeTag(new FontList(STANDARD_FONT));
        os.writeTag(new ClipIndicator(true));
    }

    protected void writePictureDescriptor() throws IOException {
        os.writeTag(new ColorSelectionMode(ColorSelectionMode.DIRECT));
        os.writeTag(new EdgeWidthSpecificationMode(
                EdgeWidthSpecificationMode.ABSOLUTE));
        os.writeTag(new LineWidthSpecificationMode(
                LineWidthSpecificationMode.ABSOLUTE));
        Dimension size = getSize();
        os.writeTag(new VDCExtent(new Point2D.Double(0, size.height),
                new Point2D.Double(size.width, 0)));
    }

    protected void writeBeginningOfPictureBody() throws IOException {
        setTransparentCellColor(DEFAULT_TRANSPARENT_CELL_COLOR);
        os.writeTag(new BeginPictureBody());
        os.writeTag(new EdgeVisibility(false));
        os.writeTag(new EdgeCap(EdgeCap.BUTT));
        os.writeTag(new LineCap(LineCap.BUTT));
        os.writeTag(new EdgeColor(Color.black));
        os.writeTag(new FillColor(Color.black));
        os.writeTag(new LineColor(Color.black));
        os.writeTag(new TextColor(Color.black));
        // os.writeTag(new TransparentCellColour(true, new Color(0, 0, 0)));
        os.writeTag(new EdgeJoin(EdgeJoin.MITRE));
        os.writeTag(new LineJoin(LineJoin.MITRE));
        os.writeTag(new EdgeType(EdgeType.SOLID));
        os.writeTag(new LineType(LineType.SOLID));
        os.writeTag(new EdgeWidth(1));
        os.writeTag(new LineWidth(1));
        os.writeTag(new InteriorStyle(InteriorStyle.SOLID));
        os.writeTag(new CharacterHeight(getFont().getSize()));
        os.writeTag(new TextFontIndex(fontIndex(getFont())));
    }

    public void writeBackground() throws IOException {
        // since CGM is non-transparent, write current background
        if (isProperty(BACKGROUND)) {
            setBackground(getPropertyColor(BACKGROUND_COLOR));
        } else {
            setBackground(getComponent() != null ? getComponent()
                    .getBackground() : Color.WHITE);
        }
        clearRect(0.0, 0.0, getSize().width, getSize().height);
    }

    public void writeTrailer() throws IOException {
        os.writeTag(new EndPicture());
        os.writeTag(new EndMetafile());
    }

    public void closeStream() throws IOException {
        os.close();
    }

    /*
     * ================================================================================ |
     * 4. Create
     * ================================================================================
     */

    public Graphics create() {
        try {
            writeGraphicsSave();
        } catch (IOException e) {
            handleException(e);
        }
        return new CGMGraphics2D(this);
    }

    public Graphics create(double x, double y, double width, double height) {
        try {
            writeGraphicsSave();
        } catch (IOException e) {
            handleException(e);
        }
        VectorGraphics graphics = new CGMGraphics2D(this);
        graphics.translate(x, y);
        graphics.clipRect(0, 0, width, height);
        return graphics;
    }

    protected void writeGraphicsSave() throws IOException {
        // not applicable
    }

    protected void writeGraphicsRestore() throws IOException {
        // not applicable
    }

    /*
     * ================================================================================ |
     * 5. Drawing Methods
     * ================================================================================
     */
    /* 5.1.4. shapes */
    public void draw(Shape s) {
        try {
            if (getStroke() instanceof BasicStroke) {
                // in this case we've already handled the stroke
                drawPath(s, false);
            } else {
                // otherwise handle it now
                drawPath(getStroke().createStrokedShape(s), true);
            }
        } catch (IOException e) {
            handleException(e);
        }
    }

    public void fill(Shape s) {
        try {
            drawPath(s, true);
        } catch (IOException e) {
            handleException(e);
        }
    }

    // TODO: Does not use current stroke yet
    public void fillAndDraw(Shape s, Color fillColor) {
        try {
            setNonStrokeColor(fillColor);
            drawPath(s, true);
            setNonStrokeColor(getColor());
            drawPath(s, false);
        } catch (IOException e) {
            handleException(e);
        }
    }

    private void drawPath(Shape s, boolean fill) throws IOException {
        // use a resolution compatible with the current transform
        AffineTransform t = getTransform();
        double resolution = 0.5 / (20 * Math.min(t.getScaleX(), t.getScaleY()));

        CGMPathConstructor path = new CGMPathConstructor(os, fill,
                getTransform(), resolution);
        boolean eo = path.addPath(s);
        path.writePolyline();
        if (!eo) {
            writeWarning(getClass()
                    + ": cannot fill using non-zero winding rule.");
        }
    }

    /* 5.2. Images */
    public void copyArea(int x, int y, int width, int height, int dx, int dy) {
        writeWarning(getClass()
                + ": copyArea(int, int, int, int, int, int) not implemented.");
    }

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

        int originX = image.getMinX();
        int originY = image.getMinY();
        int width = image.getWidth();
        int height = image.getHeight();
        Point untransformedP = new Point(originX, originY);
        Point untransformedQ = new Point(originX + width, originY + height);
        Point untransformedR = new Point(originX + width, 0);
        AffineTransform compoundTransform = getTransform();
        compoundTransform.concatenate(xform);
        Point2D p = compoundTransform.transform(untransformedP, null);
        Point2D q = compoundTransform.transform(untransformedQ, null);
        Point2D r = compoundTransform.transform(untransformedR, null);

        ColorModel colorModel = image.getColorModel();
        Raster raster = image.getData();
        Object currentDataElements = null;
        Color[][] colorArray = new Color[height][width];
        for (int xIndex = 0; xIndex < width; xIndex++) {
            for (int yIndex = 0; yIndex < height; yIndex++) {
                Color color;
                currentDataElements = raster.getDataElements(xIndex, yIndex,
                        currentDataElements);
                int alphaComponent = colorModel.getAlpha(currentDataElements);
                if (alphaComponent > 0 || transparentCellColor == null) {
                    int redComponent = colorModel.getRed(currentDataElements);
                    int greenComponent = colorModel
                            .getGreen(currentDataElements);
                    int blueComponent = colorModel.getBlue(currentDataElements);
                    color = new Color(redComponent, greenComponent,
                            blueComponent, alphaComponent);
                } else {
                    color = transparentCellColor;
                }
                colorArray[yIndex][xIndex] = color;
            }
        }

        CellArray cgmCellArray = new CellArray(p, q, r, colorArray);
        os.writeTag(cgmCellArray);
    }

    /* 5.3. Strings */
    protected void writeString(String str, double x, double y)
            throws IOException {
        Point2D point = new Point2D.Double(x, y);
        AffineTransform currentTransform = new AffineTransform(getTransform());
        currentTransform.concatenate(getFont().getTransform());
        os.writeTag(new CharacterOrientation(-currentTransform.getShearX(),
                -currentTransform.getScaleY(), currentTransform.getScaleX(),
                currentTransform.getShearY()));
        os.writeTag(new Text(currentTransform.transform(point, point), str));
    }

    /*
     * ================================================================================ |
     * 6. Transformations
     * ================================================================================
     */
    protected void writeTransform(AffineTransform t) throws IOException {
        // ignored, coordinates are pre-calculated before written to CGM
    }

    protected void writeSetTransform(AffineTransform t) throws IOException {
        // ignored, coordinates are pre-calculated before written to CGM
    }

    /*
     * ================================================================================ |
     * 7. Clipping
     * ================================================================================
     */
    protected void writeClip(Shape s) throws IOException {
        if (s instanceof Rectangle2D) {
            AffineTransform currentTransform = getTransform();
            Point2D xy = currentTransform.transform(
                new Point2D.Double(
                    ((Rectangle2D)s).getX(),
                    ((Rectangle2D)s).getY()), null);
            Point2D wh = currentTransform.deltaTransform(
                new Point2D.Double(
                    ((Rectangle2D)s).getWidth(),
                    ((Rectangle2D)s).getHeight()), null);

            os.writeTag(
                new ClipRectangle(
                    new Rectangle2D.Double(
                        xy.getX(),
                        xy.getY(),
                        wh.getX(),
                        wh.getY())));
        } else {
            writeWarning(getClass()+": writeClip(Shape) not implemented.");
        }
    }



    protected void writeSetClip(Shape s) throws IOException {
        writeWarning(getClass()+": writeSetClip(Shape) not implemented.");
    }

    /*
     * ================================================================================ |
     * 8. Graphics State
     * ================================================================================
     */
    /* 8.1. stroke/linewidth */
    protected void writeWidth(float width) throws IOException {
        os.writeTag(new EdgeWidth(width));
        os.writeTag(new LineWidth(width));
    }

    protected void writeCap(int cap) throws IOException {
        switch (cap) {
        case BasicStroke.CAP_BUTT:
            os.writeTag(new EdgeCap(EdgeCap.BUTT));
            os.writeTag(new LineCap(LineCap.BUTT));
            break;
        case BasicStroke.CAP_ROUND:
            os.writeTag(new EdgeCap(EdgeCap.ROUND));
            os.writeTag(new LineCap(LineCap.ROUND));
            break;
        case BasicStroke.CAP_SQUARE:
            os.writeTag(new EdgeCap(EdgeCap.SQUARE));
            os.writeTag(new LineCap(LineCap.SQUARE));
            break;
        }
    }

    protected void writeJoin(int join) throws IOException {
        switch (join) {
        default:
        case BasicStroke.JOIN_MITER:
            os.writeTag(new EdgeJoin(EdgeJoin.MITRE));
            os.writeTag(new LineJoin(LineJoin.MITRE));
            break;
        case BasicStroke.JOIN_ROUND:
            os.writeTag(new EdgeJoin(EdgeJoin.ROUND));
            os.writeTag(new LineJoin(LineJoin.ROUND));
            break;
        case BasicStroke.JOIN_BEVEL:
            os.writeTag(new EdgeJoin(EdgeJoin.BEVEL));
            os.writeTag(new LineJoin(LineJoin.BEVEL));
            break;
        }
    }

    protected void writeMiterLimit(float limit) throws IOException {
        os.writeTag(new MitreLimit(limit));
    }

    protected void writeDash(float[] dash, float phase) throws IOException {
        // NOTE: patterns are ignored, a guess is made
        switch (dash.length) {
        case 0:
            os.writeTag(new EdgeType(EdgeType.SOLID));
            os.writeTag(new LineType(LineType.SOLID));
            break;
        default:
        case 1:
            os.writeTag(new EdgeType(EdgeType.DASH));
            os.writeTag(new LineType(LineType.DASH));
            break;
        case 2:
            os.writeTag(new EdgeType(EdgeType.DOT));
            os.writeTag(new LineType(LineType.DOT));
            break;
        case 3:
            os.writeTag(new EdgeType(EdgeType.DASH_DOT));
            os.writeTag(new LineType(LineType.DASH_DOT));
            break;
        case 4:
            os.writeTag(new EdgeType(EdgeType.DASH_DOT_DOT));
            os.writeTag(new LineType(LineType.DASH_DOT_DOT));
            break;
        }
    }

    /* 8.2. paint/color */
    public void setPaintMode() {
        writeWarning(getClass() + ": setPaintMode() not implemented.");
    }

    public void setXORMode(Color c1) {
        writeWarning(getClass() + ": setXORMode(Color) not implemented.");
    }

    public void setTransparentCellColor(Color color) throws IOException {
        if (color != null) {
            os.writeTag(new TransparentCellColour(true, color));
        } else {
            os.writeTag(new TransparentCellColour(false, null));
        }
        transparentCellColor = color;
    }

    protected void writePaint(Color c) throws IOException {
        os.writeTag(new LineColor(c));
        os.writeTag(new EdgeColor(c));
        os.writeTag(new FillColor(c));
        os.writeTag(new TextColor(c));
    }

    protected void writePaint(GradientPaint paint) throws IOException {
        // NOTE: CGM does not support gradients, so we average the colors
        setColor(PrintColor.mixColor(paint.getColor1(), paint.getColor2()));
    }

    protected void writePaint(TexturePaint p) throws IOException {
        writeWarning(getClass() + ": writePaint(TexturePaint) not implemented.");
    }

    protected void writePaint(Paint p) throws IOException {
        writeWarning(getClass() + ": writePaint(Paint) not implemented for "
                + p.getClass());
    }

    /* 8.3. font */
    protected void writeFont(Font font) throws IOException {
	os.writeTag(new TextFontIndex(fontIndex(font)));
	os.writeTag(new CharacterHeight(font.getSize() * FONTSIZE_CORRECTION));
    }

    /*
     * ================================================================================ |
     * 9. Auxiliary
     * ================================================================================
     */
    public GraphicsConfiguration getDeviceConfiguration() {
        writeWarning(getClass() + ": getDeviceConfiguration() not implemented.");
        return null;
    }

    public boolean hit(Rectangle rect, Shape s, boolean onStroke) {
        writeWarning(getClass()
                + ": hit(Rectangle, Shape, boolean) not implemented.");
        return false;
    }

    public void writeComment(String comment) throws IOException {
        // comments are ignored and disabled, because they confuse compressed
        // streams
    }

    public String toString() {
        return "CGMGraphics2D";
    }

    /*
     * ================================================================================ |
     * 10. Private/Utility Methos
     * ================================================================================
     */
    private static int fontIndex(Font font) {
        StringBuffer fontName = new StringBuffer(font.getName());
        if (font.isBold() || font.isItalic())
            fontName.append("-");
        if (font.isBold())
            fontName.append("Bold");
        if (font.isItalic())
            fontName.append("Italic");
        String name = fontName.toString();
        for (int i = 0; i < STANDARD_FONT.length; i++) {
            if (name.equals(STANDARD_FONT[i]))
                return i + 1;
        }
        return 5; // Helvetica
    }

    private void setNonStrokeColor(Color c) throws IOException {
        os.writeTag(new FillColor(getPrintColor(c)));
    }

    // private void setStrokeColor(Color c) throws IOException {
    // os.writeTag(new LineColor(getPrintColor(c)));
    // }

    /**
     * Return the tagged output stream to which to write tags.
     * 
     * @return TaggedOutput
     */
    protected TaggedOutput getTaggedOutput() {
        return os;
    }

}
