//============================================================================ 
// 
// Filename: SVGLayer.java
// 
// Creation: 
//  First author: Xavier Wielemans, http://www.alterface.com
//  Creation date: 04-2002
//
// Description:   Java SVGLayer class
//
//                Interface between Apache's Batik SVG toolkit (Java)
//                and a native C/C++ software project (through JNI).
//                Freely inspired from existing batik code... ;)
//
//============================================================================ 


package com.alterface.scenario;

import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Dimension2D;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferInt;
import java.awt.image.Raster;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import org.apache.batik.bridge.BridgeContext;
import org.apache.batik.bridge.BridgeException;
import org.apache.batik.bridge.BridgeExtension;
import org.apache.batik.bridge.GVTBuilder;
import org.apache.batik.bridge.UserAgent;
import org.apache.batik.bridge.ViewBox;
import org.apache.batik.dom.svg.DefaultSVGContext;
import org.apache.batik.dom.svg.SAXSVGDocumentFactory;
import org.apache.batik.dom.svg.SVGOMDocument;
import org.apache.batik.dom.util.DocumentFactory;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.gvt.event.EventDispatcher;
import org.apache.batik.gvt.renderer.ConcreteImageRendererFactory;
import org.apache.batik.gvt.renderer.ImageRenderer;
import org.apache.batik.util.SVGConstants;
import org.apache.batik.util.XMLResourceDescriptor;

import org.w3c.dom.Document;
import org.w3c.dom.DOMException;
import org.w3c.dom.Element;
import org.w3c.dom.svg.SVGAElement;
import org.w3c.dom.svg.SVGDocument;

/**
 * Java SVGLayer class
 *
 * <p>Interface between Apache's Batik SVG toolkit (Java)
 * and a native C/C++ software project (through JNI).
 * Freely inspired from existing batik code... ;)
 *
 * @author <a href="mailto:willy@alterface.net">Xavier Wielemans</a>
 */
public final class SVGLayer {

    /**
     * Main method (for testing purposes only)
     */
    public static final void main(String [] args) {
	if (args.length != 1) {
	    System.out.println("Usage: SVGLayer svgFile");
	    System.exit(1);
	}

	System.out.println("Loading...");
	SVGLayer layer = new SVGLayer(args[0]);
	System.out.println("Setting scene size...");
	layer.setSceneSize(new Point(640,480));
	
	double angleMult = 2*Math.PI/2500;
	long lastTimeMillis = 0,
	    cumulatedTimeMillis = 0, 
	    minTimeMillis = Long.MAX_VALUE, 
	    maxTimeMillis = Long.MIN_VALUE;
	int numFrames = 0;

	while (true) {
	    System.out.println("Setting layer position, scale and rotation angle...");    
	    long longTime = System.currentTimeMillis();
	    int time = (int)(longTime - 5000*(longTime/5000)); // = longTime%5000 : in milliseconds, grows from 0 to 4999 every 5 sec.
	    //System.out.println("time: " + time);
	    layer.setScenePosition(new Point((639*time)/4999, (479*(4999-time))/4999));
	    double angle = angleMult*time;
	    layer.setScale(new Point2D.Double((Math.sin(2*angle)+2)/2, (Math.cos(2*angle)+2)/2)); 
	    layer.setRotation(angle);

	    System.out.println("Rendering...");
	    long before = System.currentTimeMillis();
	    int[] result = layer.render();
	    long after = System.currentTimeMillis();

	    lastTimeMillis = after-before;
	    cumulatedTimeMillis += lastTimeMillis;
	    numFrames++;
	    if (lastTimeMillis < minTimeMillis) {
		minTimeMillis = lastTimeMillis;
	    }

	    if (lastTimeMillis > maxTimeMillis) {
		maxTimeMillis = lastTimeMillis;
	    }

	    System.out.println("Rendering framerate (min/avg/max): " + 1000.0/maxTimeMillis + "/" + numFrames*1000.0/cumulatedTimeMillis + "/" + 1000.0/minTimeMillis + " fps.");
	    /*
	      System.out.println("Garbage collecting...");
	      System.gc();
	    */
	    System.out.println("-----------------------");
	}
    }

    /**
     * Constructs a new <tt>SVGLayer</tt>.
     *
     * @param svgFile source file containing the SVG description
     */
    public SVGLayer(String svgFile) {

	// Check file access

	m_source = new File(svgFile);

	if (!m_source.getPath().endsWith(".svg")) {
	    System.out.println("Source file '" + m_source.getPath() + "' should end with '.svg' extension...");
	    System.exit(1);
	}
	
	if (!m_source.canRead()) {
	    System.out.println("Source file '" + m_source.getPath() + "' is unreadable...");
	    System.exit(1);
	}
	
	// Parse source file to SVG DOM tree

	// Create DOM tree builder
	UserAgent userAgent = new SVGLayerUserAgent();
	DocumentFactory f = new SAXSVGDocumentFactory(userAgent.getXMLParserClassName());
	Document domTree = null;
	f.setValidating(userAgent.isXMLParserValidating());
	try {
	    // Build DOM tree
	    domTree = f.createDocument(SVGConstants.SVG_NAMESPACE_URI, // SVG namespace
				       SVGConstants.SVG_SVG_TAG,       // SVG root-element (<svg> tag)
				       m_source.toURL().toString());   // Source URI.
	} catch (DOMException ex) {
	    ex.printStackTrace();
	    System.exit(1);
	} catch (IOException ex) {
	    ex.printStackTrace();
	    System.exit(1);
	}
	
	f = null;

	if (domTree==null || !(domTree instanceof SVGOMDocument)) {
	    System.out.println("Couldn't build SVG DOM tree from source file !");
	    System.exit(1);
	}

	// Convert DOM tree to GVT tree (simpler structure optimized for faster rendering)

        SVGDocument svgTree = (SVGDocument)domTree;
        // initialize the SVG document with the appropriate context
        DefaultSVGContext svgCtx = new DefaultSVGContext();
        svgCtx.setPixelToMM(userAgent.getPixelToMM());
        ((SVGOMDocument)domTree).setSVGContext(svgCtx);

        // build the GVT tree
        GVTBuilder builder = new GVTBuilder();
        BridgeContext ctx = new BridgeContext(userAgent);
	GraphicsNode gvtRoot = null;
        try {
            gvtRoot = builder.build(ctx, svgTree);
        } catch (BridgeException ex) {
	    ex.printStackTrace();
	    System.exit(1);
        }
	
	if (gvtRoot == null) {
	    System.out.println("Couldn't build GVT tree from the DOM SVG tree !");
	    System.exit(1);
	}

	// Compute rendering viewpoint

        // get the 'width' and 'height' attributes of the SVG document
        m_docWidth = ctx.getDocumentSize().getWidth();
        m_docHeight = ctx.getDocumentSize().getHeight();
        ctx = null;
        builder = null;

        // compute the viewpoint transformation defined in the document
        try {
            m_sourceTransform = ViewBox.getPreserveAspectRatioTransform(svgTree.getRootElement(), (float)m_docWidth, (float)m_docHeight);
	    //System.out.println("Source transform: " + m_sourceTransform);
        } catch (BridgeException ex) {
	    ex.printStackTrace();
	    System.exit(1);	    
        }

	// Create SVG renderer

        m_renderer = new ConcreteImageRendererFactory().createStaticImageRenderer();
	//m_renderer.setDoubleBuffered(true);
        m_renderer.setTree(gvtRoot);
        gvtRoot = null; // We're done with it...
    }

    /**
     * Sets the layer's scale
     *
     * @param scale the new x/y scale at which the layer is to be rendered
     */
    public void setScale(Point2D.Double scale) {
	m_scale = (Point2D.Double)scale.clone();
    }

    /**
     * Sets the layer's rotation angle
     *
     * @param angle the new rotation angle at which the layer is to be rendered
     */
    public void setRotation(double angle) {
	m_angle = angle;
    }

    /**
     * Sets the scene size.
     * This information, along with the scene position of the layer, will be used
     * to clip document shape so that only visible parts will actually be rendered.
     *
     * @param size the new x/y size of the scene in which the svg document is to be rendered.
     */
    public void setSceneSize(Point size) {
	m_sceneSize = (Point)size.clone();
    }

    /**
     * Sets the layer position in the scene.
     * This information, along with the scene size, will be used
     * to clip document shape so that only visible parts will actually be rendered.
     *
     * @param position the new x/y position of the document's center point in the scene.
     */
    public void setScenePosition(Point position) {
	m_scenePosition = (Point)position.clone();
    }

    /**
     * Gets the document width
     *
     * @return the source document's width (as an int)
     */
    public int getDocWidth() {
	return (int)m_docWidth;
    }

    /**
     * Gets the document height
     *
     * @return the source document's layer's height (as an int)
     */
    public int getDocHeight() {
	return (int)m_docHeight;
    }

    /**
     * Gets the current rendered image width
     *
     * @return the current width of the rendered image,
     * taking into account current scale/rotation, scene
     * size and scene position.
     */
    public int getImgWidth() {
	return m_imgWidth;
    }

    /**
     * Gets the current rendered image height
     *
     * @return the current height of the rendered image,
     * taking into account current scale/rotation, scene
     * size and scene position.
     */
    public int getImgHeight() {
	return m_imgHeight;
    }

    /**
     * Gets REAL data buffer width.
     * The data buffer is often actually larger than the image !
     * In that case, the data buffer offset indicates the X/Y translation
     * between top-left corners of image and data buffer.
     * so that both origins match.
     *
     * NB: updated at each rendering.
     *
     * @return the actual width of the data buffer returned by 'render()'.
     */
    public int getDataBufferWidth() {
	return m_dataBufferWidth;
    }

    /**
     * Gets REAL data buffer height.
     * The data buffer is often actually larger than the image !
     * In that case, the data buffer offset indicates the X/Y translation
     * between top-left corners of image and data buffer.
     *
     * NB: updated at each rendering.
     *
     * @return the actual height of the data buffer returned by 'render()'.
     */
    public int getDataBufferHeight() {
	return m_dataBufferHeight;
    }

    /**
     * Gets data buffer offset in the X direction.
     * It is equal to the X position of the image top-left corner pixel
     * in the data buffer. 
     *
     * NB: updated at each rendering.
     *
     * @return the image X offset into the data buffer
     */
    public int getDataBufferXOffset() {
	return m_dataBufferXOffset;
    }

    /**
     * Gets the layer's data buffer offset in the Y direction.
     * It is equal to the Y position of the image top-left corner pixel
     * in the data buffer.
     *
     * NB: updated at each rendering.
     *
     * @return the image Y offset into the data buffer
     */
    public int getDataBufferYOffset() {
	return m_dataBufferYOffset;
    }

    /**
     * Gets image offset in the X direction.
     * It is equal to the X position of the document center
     * in the image.
     *
     * NB: updated at each rendering.
     *
     * @return the document center X offset into the image
     */
    public int getImgXOffset() {
	return m_imgXOffset;
    }

    /**
     * Gets image offset in the Y direction.
     * It is equal to the Y position of the document center
     * in the image.
     *
     * NB: updated at each rendering.
     *
     * @return the document center Y offset into the image
     */
    public int getImgYOffset() {
	return m_imgYOffset;
    }

    /**
     * To be called just before rendering
     * Recomputes affine transform applied to rendered document, deduces final 
     * image shape, intersects it with the user-supplied scene boundaries
     * and finally sets rendering buffer size, image size and offset.
     *
     * NB: I am not really sure I take the source transform 
     * (=affine transform defined in the SVG document itself) correctly into account here!
     */
    private void updateTransform() {
	// Preliminary note: the final transformation computed by this method is the following:
	// svg document->svg viewport->global scene->rendering buffer.
	// - The document->viewport transform is the *source* transform, defined in the source SVG file
	// - The viewport->scene transform is defined by the scale/rotate/position params set by 
	//   the user of this class (through the setScale,setRotation,setScenePosition methods).
	// - The scene->buffer transform goes from scene space to buffer space.  It is simply 
	//   a translation that maps the rendering buffer top-left corner onto the (0,0) origin point.

	if (m_sceneSize.getX() < 0 || m_sceneSize.getY() < 0) {
	    System.out.println("Negative scene size - maybe you forgot to set it before rendering ?");
	    System.exit(1);
	}
	
	// Create rotate/scale/scene-positionning transform (viewport->scene)
	AffineTransform rotoScalePos = AffineTransform.getTranslateInstance(-m_docWidth/2, -m_docHeight/2);    // move document center to (0,0)
	rotoScalePos.preConcatenate(AffineTransform.getScaleInstance(m_scale.getX(), m_scale.getY()));
	rotoScalePos.preConcatenate(AffineTransform.getRotateInstance(m_angle));
	rotoScalePos.preConcatenate(AffineTransform.getTranslateInstance(m_scenePosition.getX(), m_scenePosition.getY()));      // move document center to its position in the scene
	
	/*
	System.out.println("Scale: " + m_scale);
	System.out.println("Angle: " + m_angle);
	System.out.println("Scene Position: " + m_scenePosition);
	*/

	// Transform SVG viewport boundaries to scene space
	Rectangle2D.Double documentShape = new Rectangle2D.Double(0, 0, m_docWidth, m_docHeight);
	Shape sceneShape = rotoScalePos.createTransformedShape(documentShape);
	
	// Intersect transformed shape with scene boundaries (to determine the exact area it will cover inside in the scene)
	// and extract bounding box of the resulting area.
	Area coveredSceneArea = new Area(sceneShape);
	coveredSceneArea.intersect(new Area(new Rectangle((int)m_sceneSize.getX(), (int)m_sceneSize.getY())));
	Rectangle docBBox = coveredSceneArea.getBounds();
	
	// Set rendering image size (and provide it to renderer) and offset.
	m_imgWidth = (int)docBBox.getWidth();
	m_imgHeight = (int)docBBox.getHeight();
	m_renderer.updateOffScreen(m_imgWidth, m_imgHeight);
	m_imgXOffset = (int)m_scenePosition.getX()-(int)docBBox.getX();
	m_imgYOffset = (int)m_scenePosition.getY()-(int)docBBox.getY();
	
	// Concatenate source transform and roto/scale/pos transform 
	// in order to obtain the total document->scene transform
	rotoScalePos.concatenate(m_sourceTransform);

	// Apply inverse transform to covered scene area to obtain area of document that will need to be rendered.
	try {
	    coveredSceneArea.transform(rotoScalePos.createInverse());
	    m_docRenderShape = (Shape)coveredSceneArea.clone();
	} catch (NoninvertibleTransformException ex) {
	    ex.printStackTrace();
	    System.exit(1);
	}

	// Finally, add the scene->buffer transform.
	rotoScalePos.preConcatenate(AffineTransform.getTranslateInstance(-docBBox.getX(), -docBBox.getY()));

	// Provide the result to the renderer
	m_renderer.setTransform(rotoScalePos);	
    }

    /**
     * Renders the SVG layer.
     */
    public int [] render() {
	//System.out.println("Entering render()...");

	updateTransform();
        try {
            m_renderer.repaint(m_docRenderShape); // Re-render
        } catch (Exception ex) {
	    ex.printStackTrace();
	    System.exit(1);
        }

	// Find real size of the data buffer on which the image is rendered (not
	// necessarily equal to the image size, often actally larger !!!)
	Raster raster = m_renderer.getOffScreen().getRaster();
	Raster ancestorRaster = raster;
	while (ancestorRaster.getParent() != null) {
	    ancestorRaster = ancestorRaster.getParent();
	}
	m_dataBufferWidth = ancestorRaster.getWidth();
	m_dataBufferHeight = ancestorRaster.getHeight();

	// Find offset of the image in the data buffer
	m_dataBufferXOffset = -raster.getSampleModelTranslateX();
	m_dataBufferYOffset = -raster.getSampleModelTranslateY();
	
	// Validity check
	if (m_dataBufferXOffset < 0 ||
	    m_dataBufferYOffset < 0 ||
	    m_dataBufferXOffset+m_imgWidth > m_dataBufferWidth ||
	    m_dataBufferYOffset+m_imgHeight > m_dataBufferHeight) {
	    System.out.println("Invalid databuffer size (" + m_dataBufferWidth + ", " + m_dataBufferHeight + 
			       ") or offset (" + m_dataBufferXOffset + ", " + m_dataBufferYOffset + 
			       ") compared to image size (" + m_imgWidth + ", " + m_imgHeight + ").");
	    System.exit(1);
	}
	
	// Retrieve data array
	int [] data = null;
	try {
	    data = ((DataBufferInt)raster.getDataBuffer()).getData();
	} catch (ClassCastException ex) {
	    ex.printStackTrace();
	    System.exit(1);
	}
	if (data.length != m_dataBufferWidth*m_dataBufferHeight) {
	    System.out.println("Invalid dimensions for data buffer !");
	    System.exit (1);
	}

	//System.out.println("Leaving render()...");
	return data;
    }

    /**
     * The layer's source file
     */
    private File m_source = null;

    /**
     * The layer's scale.  (1, 1) is the original scale.
     */
    private Point2D.Double m_scale = new Point2D.Double(1,1);

    /**
     * The layer's rotation angle.  0 is the original orientation.
     */
    private double m_angle=0;

    /**
     * The size of the scene in which the document is to be rendered.
     */
    private Point m_sceneSize = new Point(-1,-1);

    /**
     * The position into the scene where the document's center will be placed.
     */
    private Point m_scenePosition = new Point(0,0);

    /**
     * The document's width (as defined in the source file).
     */
    private double m_docWidth=0;

    /**
     * The document's height (as defined in the source file).
     */
    private double m_docHeight=0;

    /**
     * The current image width (after rotation, scaling and clipping).
     */
    private int m_imgWidth=0;

    /**
     * The current image height (after rotation, scaling and clipping).
     */
    private int m_imgHeight=0;

    /**
     * The rendering data buffer's width
     */
    private int m_dataBufferWidth=0;

    /**
     * The rendering data buffer's height
     */
    private int m_dataBufferHeight=0;

    /**
     * The X offset of the rendered image into the data buffer
     */
    private int m_dataBufferXOffset=0;

    /**
     * The Y offset of the rendered image into the data buffer
     */
    private int m_dataBufferYOffset=0;

    /**
     * The X offset of the document center in the rendered image
     */
    private int m_imgXOffset=0;

    /**
     * The Y offset of the document center in the rendered image
     */
    private int m_imgYOffset=0;

    /**
     * The SVG renderer (containing the GVT tree, internal representation of the SVG graphics)
     */
    private ImageRenderer m_renderer = null;

    /**
     * The original affine transform defined in the source document
     * This transform is to be interpreted "from SVG space to screen space" 
     */
    private AffineTransform m_sourceTransform = null;

    /**
     * The bounding box, defined in the SVG document space, of the area
     * that will be visible in the final scene, and thus that need to 
     * be rendered.
     */
    private Shape m_docRenderShape = null;

    // --------------------------------------------------------------------
    // UserAgent implementation
    // --------------------------------------------------------------------

    /**
     * A user agent implementation for <tt>SVGLayer</tt>.
     */
    private static class SVGLayerUserAgent implements UserAgent {

        /**
         * Returns the default size of this user agent (400x400).
         */
        public Dimension2D getViewportSize() {
            return new Dimension(400, 400);
        }

        /**
         * Displays the specified error message.
         */
        public void displayError(String message) {
	    System.out.println("Error while building GVT tree: " + message);
	    System.exit(1);
        }

        /**
         * Displays the specified error.
         */
        public void displayError(Exception e) {
	    System.out.println("Error while building GVT tree:");
	    e.printStackTrace();
	    System.exit(1);
        }

        /**
         * Displays the specified warning message.
         */
        public void displayMessage(String message) {
	    System.out.println("Warning while building GVT tree: " + message);
        }

	/**
	 * Shows an alert dialog box.
	 */
	public void showAlert(String message) {
	    System.out.println("Altert when building GVT tree: " + message);
	}

        /**
         * Returns the pixel to millimeter conversion factor.
         */
        public float getPixelToMM() {
	    return 0.3528f; // 72 dpi
        }

        /**
         * Returns the user language
         */
        public String getLanguages() {
	    return "en";
        }

        /**
         * Returns the user stylesheet
         */
        public String getUserStyleSheetURI() {
            return null;
        }

        /**
         * Returns the XML parser to use
         */
        public String getXMLParserClassName() {
	    return XMLResourceDescriptor.getXMLParserClassName();
        }

	/**
	 * Returns true if the XML parser must be in validation mode, false
	 * otherwise.
	 */
	public boolean isXMLParserValidating() {
	    return true;
	}
	
        /**
         * Returns this user agent's CSS media.
         */
        public String getMedia() {
            return "screen";
        }

        /**
         * Unsupported operation.
         */
        public EventDispatcher getEventDispatcher() {
            return null;
        }

        /**
         * Unsupported operation.
         */
        public void openLink(SVGAElement elt) { }

        /**
         * Unsupported operation.
         */
        public void setSVGCursor(Cursor cursor) { }

        /**
         * Unsupported operation.
         */
        public void runThread(Thread t) { }

        /**
         * Unsupported operation.
         */
        public AffineTransform getTransform() {
            return null;
        }

        /**
         * Unsupported operation.
         */
        public Point getClientAreaLocationOnScreen() {
            return new Point();
        }

	/**
	 * Unsupported operation
	 */
	
 	public String showPrompt(String message){
	    return null;
	}
	    
	/**
	 * Unsupported operation
	 */
	
	public String showPrompt(String message, String defaultValue){
	    return defaultValue;
	}
	    
	/**
	 * Unsupported operation
	 */
	
 	public boolean showConfirm(String message){
	    return true;
	}
	    
        /**
         * Tells whether the given feature is supported by this
         * user agent.
         */
        public boolean hasFeature(String s) {
            return FEATURES.contains(s);
        }

        private Set extensions = new HashSet();

        /**
         * Tells whether the given extension is supported by this
         * user agent.
         */
        public boolean supportExtension(String s) {
            return extensions.contains(s);
        }

        /**
         * Lets the bridge tell the user agent that the following
         * extension is supported by the bridge.
         */
        public void registerExtension(BridgeExtension ext) {
            Iterator i = ext.getImplementedExtensions();
            while (i.hasNext())
                extensions.add(i.next());
        }

        /**
         * Notifies the UserAgent that the input element 
         * has been found in the document. This is sometimes
         * called, for example, to handle &lt;a&gt; or
         * &lt;title&gt; elements in a UserAgent-dependant
         * way.
         */
        public void handleElement(Element elt, Object data){
	    System.out.println("SVGLayer.SVGLayerUserAgent.handleElement() called on element <" + elt.getTagName() + ">.");
        }
    }

    private final static Set FEATURES = new HashSet();
    static {
	FEATURES.add(SVGConstants.SVG_ORG_W3C_SVG_FEATURE);
	FEATURES.add(SVGConstants.SVG_ORG_W3C_SVG_LANG_FEATURE);
	FEATURES.add(SVGConstants.SVG_ORG_W3C_SVG_STATIC_FEATURE);
    }
}
