/*****************************************************************************
 * Copyright (C) The Apache Software Foundation. All rights reserved.        *
 * ------------------------------------------------------------------------- *
 * This software is published under the terms of the Apache Software License *
 * version 1.1, a copy of which has been included with this distribution in  *
 * the LICENSE file.                                                         *
 *****************************************************************************/

package org.apache.batik.svggen;

import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.awt.image.renderable.RenderableImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Method;

import org.w3c.dom.Element;

/**
 * This class is a default implementation of the GenericImageHandler
 * for handlers implementing a caching strategy.
 *
 * @author <a href="mailto:vincent.hardy@eng.sun.com">Vincent Hardy</a>
 * @version $Id: DefaultCachedImageHandler.java,v 1.5 2003/07/04 15:25:37 vhardy Exp $
 * @see             org.apache.batik.svggen.SVGGraphics2D
 */
public abstract class DefaultCachedImageHandler 
    implements CachedImageHandler, 
               SVGSyntax,
               ErrorConstants {

    // duplicate the string here to remove dependencies on
    // org.apache.batik.dom.util.XLinkSupport
    static final String XLINK_NAMESPACE_URI =
        "http://www.w3.org/1999/xlink";

    static final AffineTransform IDENTITY = new AffineTransform();

    // for createGraphics method.
    private static Method createGraphics = null;
    private static boolean initDone = false;
    private final static Class[] paramc = new Class[] {BufferedImage.class};
    private static Object[] paramo = null;

    protected ImageCacher imageCacher;

    /**
     * The image cache can be used by subclasses for efficient image storage
     */
    public ImageCacher getImageCacher() {
        return imageCacher;
    }

    void setImageCacher(ImageCacher imageCacher) {
        if (imageCacher == null){
            throw new IllegalArgumentException();
        }

        // Save current DOMTreeManager if any
        DOMTreeManager dtm = null;
        if (this.imageCacher != null){
            dtm = this.imageCacher.getDOMTreeManager();
        }

        this.imageCacher = imageCacher;
        if (dtm != null){
            this.imageCacher.setDOMTreeManager(dtm);
        }
    }

    /**
     * This <tt>GenericImageHandler</tt> implementation does not
     * need to interact with the DOMTreeManager.
     */
    public void setDOMTreeManager(DOMTreeManager domTreeManager){
        imageCacher.setDOMTreeManager(domTreeManager);
    }

    /**
     * This method creates a <code>Graphics2D</code> from a
     * <code>BufferedImage</code>. If Batik extensions to AWT are
     * in the CLASSPATH it uses them, otherwise, it uses the regular
     * AWT method.
     */
    private static Graphics2D createGraphics(BufferedImage buf) {
        if (!initDone) {
            try {
                Class clazz = Class.forName("org.apache.batik.ext.awt.image.GraphicsUtil");
                createGraphics = clazz.getMethod("createGraphics", paramc);
                paramo = new Object[1];
            } catch (Throwable t) {
                // happen only if Batik extensions are not their
            } finally {
                initDone = true;
            }
        }
        if (createGraphics == null)
            return buf.createGraphics();
        else {
            paramo[0] = buf;
            Graphics2D g2d = null;
            try {
                g2d = (Graphics2D)createGraphics.invoke(null, paramo);
            } catch (Exception e) {
                // should not happened
            }
            return g2d;
        }
    }

    /**
     * Creates an Element which can refer to an image.
     * Note that no assumptions should be made by the caller about the
     * corresponding SVG tag. By default, an &lt;image&gt; tag is 
     * used, but the {@link CachedImageHandlerBase64Encoder}, for
     * example, overrides this method to use a different tag.
     */
    public Element createElement(SVGGeneratorContext generatorContext) {
        // Create a DOM Element in SVG namespace to refer to an image
        Element imageElement =
            generatorContext.getDOMFactory().createElementNS
            (SVG_NAMESPACE_URI, SVG_IMAGE_TAG);

        return imageElement;
    }

    /**
     * The handler sets the xlink:href tag and returns a transform
     */
    public AffineTransform handleImage(Image image,
                                       Element imageElement,
                                       int x, int y,
                                       int width, int height,
                                       SVGGeneratorContext generatorContext) {

        int imageWidth      = image.getWidth(null);
        int imageHeight     = image.getHeight(null);
        AffineTransform af  = null;

        if(imageWidth == 0 || imageHeight == 0 ||
           width == 0 || height == 0) {

            // Forget about it
            handleEmptyImage(imageElement);

        } else {
            // First set the href
            try {
                handleHREF(image, imageElement, generatorContext);
            } catch (SVGGraphics2DIOException e) {
                try {
                    generatorContext.errorHandler.handleError(e);
                } catch (SVGGraphics2DIOException io) {
                    // we need a runtime exception because
                    // java.awt.Graphics2D method doesn't throw exceptions..
                    throw new SVGGraphics2DRuntimeException(io);
                }
            }

            // Then create the transformation:
            // Because we cache image data, the stored image may
            // need to be scaled.
            af = handleTransform(imageElement, (double) x, (double) y,
                                 (double) imageWidth, (double) imageHeight,
                                 (double) width, (double) height,
                                 generatorContext);
        }
        return af;
    }

    /**
     * The handler sets the xlink:href tag and returns a transform
     */
    public AffineTransform handleImage(RenderedImage image,
                                       Element imageElement,
                                       int x, int y,
                                       int width, int height,
                                       SVGGeneratorContext generatorContext) {

        int imageWidth      = image.getWidth();
        int imageHeight     = image.getHeight();
        AffineTransform af  = null;

        if(imageWidth == 0 || imageHeight == 0 ||
           width == 0 || height == 0) {

            // Forget about it
            handleEmptyImage(imageElement);

        } else {
            // First set the href
            try {
                handleHREF(image, imageElement, generatorContext);
            } catch (SVGGraphics2DIOException e) {
                try {
                    generatorContext.errorHandler.handleError(e);
                } catch (SVGGraphics2DIOException io) {
                    // we need a runtime exception because
                    // java.awt.Graphics2D method doesn't throw exceptions..
                    throw new SVGGraphics2DRuntimeException(io);
                }
            }

            // Then create the transformation:
            // Because we cache image data, the stored image may
            // need to be scaled.
            af = handleTransform(imageElement, (double) x, (double) y,
                                 (double) imageWidth, (double) imageHeight,
                                 (double) width, (double) height, generatorContext);
        }
        return af;
    }

    /**
     * The handler sets the xlink:href tag and returns a transform
     */
    public AffineTransform handleImage(RenderableImage image,
                                       Element imageElement,
                                       double x, double y,
                                       double width, double height,
                                       SVGGeneratorContext generatorContext) {

        double imageWidth   = image.getWidth();
        double imageHeight  = image.getHeight();
        AffineTransform af  = null;

        if(imageWidth == 0 || imageHeight == 0 ||
           width == 0 || height == 0) {

            // Forget about it
            handleEmptyImage(imageElement);

        } else {
            // First set the href
            try {
                handleHREF(image, imageElement, generatorContext);
            } catch (SVGGraphics2DIOException e) {
                try {
                    generatorContext.errorHandler.handleError(e);
                } catch (SVGGraphics2DIOException io) {
                    // we need a runtime exception because
                    // java.awt.Graphics2D method doesn't throw exceptions..
                    throw new SVGGraphics2DRuntimeException(io);
                }
            }

            // Then create the transformation:
            // Because we cache image data, the stored image may
            // need to be scaled.
            af = handleTransform(imageElement, x,y,
                                 imageWidth, imageHeight,
                                 width, height, generatorContext);
        }
        return af;
    }

    /**
     * Determines the transformation needed to get the cached image to
     * scale & position properly. Sets x and y attributes on the element
     * accordingly.
     */
    protected AffineTransform handleTransform(Element imageElement,
                                              double x, double y,
                                              double srcWidth,
                                              double srcHeight,
                                              double dstWidth,
                                              double dstHeight,
                                              SVGGeneratorContext generatorContext) {
        // In this the default case, <image> element, we just
        // set x, y, width and height attributes.
        // No additional transform is necessary.
        imageElement.setAttributeNS(null,
                                    SVG_X_ATTRIBUTE,
                                    generatorContext.doubleString(x));
        imageElement.setAttributeNS(null,
                                    SVG_Y_ATTRIBUTE,
                                    generatorContext.doubleString(y));
        imageElement.setAttributeNS(null,
                                    SVG_WIDTH_ATTRIBUTE,
                                    generatorContext.doubleString(dstWidth));
        imageElement.setAttributeNS(null,
                                    SVG_HEIGHT_ATTRIBUTE,
                                    generatorContext.doubleString(dstHeight));
        return null;
    }
              
    protected void handleEmptyImage(Element imageElement) {
        imageElement.setAttributeNS(XLINK_NAMESPACE_URI,
                                    ATTR_XLINK_HREF, "");
        imageElement.setAttributeNS(null, SVG_WIDTH_ATTRIBUTE, "0");
        imageElement.setAttributeNS(null, SVG_HEIGHT_ATTRIBUTE, "0");
    }

    /**
     * The handler should set the xlink:href tag and the width and
     * height attributes.
     */
    public void handleHREF(Image image, Element imageElement,
                           SVGGeneratorContext generatorContext)
        throws SVGGraphics2DIOException {
        if (image == null)
            throw new SVGGraphics2DRuntimeException(ERR_IMAGE_NULL);

        int width = image.getWidth(null);
        int height = image.getHeight(null);

        if (width==0 || height==0) {
            handleEmptyImage(imageElement);
        } else {
            if (image instanceof RenderedImage) {
                handleHREF((RenderedImage)image, imageElement,
                           generatorContext);
            } else {
                BufferedImage buf = buildBufferedImage(new Dimension(width, height));
                Graphics2D g = createGraphics(buf);
                g.drawImage(image, 0, 0, null);
                g.dispose();
                handleHREF((RenderedImage)buf, imageElement,
                           generatorContext);
            }
        }
    }

    /**
     * This method creates a BufferedImage of the right size and type
     * for the derived class.
     */
    public BufferedImage buildBufferedImage(Dimension size){
        return new BufferedImage(size.width, size.height, getBufferedImageType());
    }

    /**
     * This template method should set the xlink:href attribute on the input
     * Element parameter
     */
    protected void handleHREF(RenderedImage image, Element imageElement,
                              SVGGeneratorContext generatorContext)
        throws SVGGraphics2DIOException {
        //
        // Create an buffered image if necessary
        //
        BufferedImage buf = null;
        if (image instanceof BufferedImage 
            &&
            ((BufferedImage)image).getType() == getBufferedImageType()){
            buf = (BufferedImage)image;
        } else {
            Dimension size = new Dimension(image.getWidth(), image.getHeight());
            buf = buildBufferedImage(size);
            
            Graphics2D g = createGraphics(buf);
            
            g.drawRenderedImage(image, IDENTITY);
            g.dispose();
        }

        //
        // Cache image and set xlink:href
        //
        cacheBufferedImage(imageElement, buf, generatorContext);
    }

    /**
     * This method will delegate to the <tt>handleHREF</tt> which
     * uses a <tt>RenderedImage</tt>
     */
    protected void handleHREF(RenderableImage image, Element imageElement,
                              SVGGeneratorContext generatorContext)
        throws SVGGraphics2DIOException {
        // Create an buffered image where the image will be drawn
        Dimension size = new Dimension((int)Math.ceil(image.getWidth()),
                                       (int)Math.ceil(image.getHeight()));
        BufferedImage buf = buildBufferedImage(size);

        Graphics2D g = createGraphics(buf);

        g.drawRenderableImage(image, IDENTITY);
        g.dispose();

        handleHREF((RenderedImage)buf, imageElement, generatorContext);
    }

    protected void cacheBufferedImage(Element imageElement,
                                      BufferedImage buf,
                                      SVGGeneratorContext generatorContext)
        throws SVGGraphics2DIOException {
        
        ByteArrayOutputStream os;
        
        if (generatorContext == null)
            throw new SVGGraphics2DRuntimeException(ERR_CONTEXT_NULL);
        
        try {
            os = new ByteArrayOutputStream();
            // encode the image in memory
            encodeImage(buf, os);
            os.flush();
            os.close();
        } catch (IOException e) {
            // should not happen since we do in-memory processing
            throw new SVGGraphics2DIOException(ERR_UNEXPECTED, e);
        }
        
        // ask the cacher for a reference
        String ref = imageCacher.lookup(os,
                                                  buf.getWidth(),
                                                  buf.getHeight(),
                                                  generatorContext);
        
        // set the URL
        imageElement.setAttributeNS(XLINK_NAMESPACE_URI,
                                    ATTR_XLINK_HREF,
                                    getRefPrefix() + ref);
    }                
 
    /**
     * Should return the prefix with wich the image reference
     * should be pre-concatenated.
     */
    public abstract String getRefPrefix();

    /**
     * Derived classes should implement this method and encode the input
     * BufferedImage as needed
     */
    public abstract void encodeImage(BufferedImage buf, OutputStream os)
        throws IOException;
 
    /**
     * This template method should be overridden by derived classes to 
     * declare the image type they need for saving to file.
     */
    public abstract int getBufferedImageType();
  
}
