/*

 ============================================================================
                   The Apache Software License, Version 1.1
 ============================================================================

 Copyright (C) 1999-2003 The Apache Software Foundation. All rights reserved.

 Redistribution and use in source and binary forms, with or without modifica-
 tion, are permitted provided that the following conditions are met:

 1. Redistributions of  source code must  retain the above copyright  notice,
    this list of conditions and the following disclaimer.

 2. Redistributions in binary form must reproduce the above copyright notice,
    this list of conditions and the following disclaimer in the documentation
    and/or other materials provided with the distribution.

 3. The end-user documentation included with the redistribution, if any, must
    include  the following  acknowledgment:  "This product includes  software
    developed  by the  Apache Software Foundation  (http://www.apache.org/)."
    Alternately, this  acknowledgment may  appear in the software itself,  if
    and wherever such third-party acknowledgments normally appear.

 4. The names "Batik" and  "Apache Software Foundation" must  not  be
    used to  endorse or promote  products derived from  this software without
    prior written permission. For written permission, please contact
    apache@apache.org.

 5. Products  derived from this software may not  be called "Apache", nor may
    "Apache" appear  in their name,  without prior written permission  of the
    Apache Software Foundation.

 THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 FITNESS  FOR A PARTICULAR  PURPOSE ARE  DISCLAIMED.  IN NO  EVENT SHALL  THE
 APACHE SOFTWARE  FOUNDATION  OR ITS CONTRIBUTORS  BE LIABLE FOR  ANY DIRECT,
 INDIRECT, INCIDENTAL, SPECIAL,  EXEMPLARY, OR CONSEQUENTIAL  DAMAGES (INCLU-
 DING, BUT NOT LIMITED TO, PROCUREMENT  OF SUBSTITUTE GOODS OR SERVICES; LOSS
 OF USE, DATA, OR  PROFITS; OR BUSINESS  INTERRUPTION)  HOWEVER CAUSED AND ON
 ANY  THEORY OF LIABILITY,  WHETHER  IN CONTRACT,  STRICT LIABILITY,  OR TORT
 (INCLUDING  NEGLIGENCE OR  OTHERWISE) ARISING IN  ANY WAY OUT OF THE  USE OF
 THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

 This software  consists of voluntary contributions made  by many individuals
 on  behalf of the Apache Software  Foundation. For more  information on the
 Apache Software Foundation, please see <http://www.apache.org/>.

*/

package org.apache.batik.svggen;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.zip.Adler32;
import java.util.zip.Checksum;

import org.w3c.dom.Document;
import org.w3c.dom.Element;


/**
 * This class implements caching functionality for raster images.
 *
 * @author <a href="mailto:paul_evenblij@compuware.com">Paul Evenblij</a>
 * @version $Id: ImageCacher.java,v 1.5 2003/08/09 16:58:39 deweese Exp $
 */
public abstract class ImageCacher implements SVGSyntax, ErrorConstants {
  
    DOMTreeManager  domTreeManager = null;
    Hashtable       imageCache;
    Checksum        checkSum;
    
    /**
     * Creates an ImageCacher.
     */
    public ImageCacher() {
        imageCache = new Hashtable();
        checkSum = new Adler32();
    }

    /**
     * Creates an ImageCacher.
     *
     * @param domTreeManager the DOMTreeManager for the tree this cacher works on
     */
    public ImageCacher(DOMTreeManager domTreeManager) {
        this();
        setDOMTreeManager(domTreeManager);
    }

    /**
     * Sets the DOMTreeManager this cacher should work on.
     *
     * @param domTreeManager the DOMTreeManager for the tree this cacher works on
     */
    public void setDOMTreeManager(DOMTreeManager domTreeManager) {
        if (domTreeManager == null){
            throw new IllegalArgumentException();
        }
        this.domTreeManager = domTreeManager;
    }

    public DOMTreeManager getDOMTreeManager(){
        return domTreeManager;
    }

    /**
     * Checks if the image is already in the cache, and
     * adds it if not. Returns a unique id for the entry.
     *
     * @param os                the image as a byte stream
     * @param width             the width of the image
     * @param height            the height of the image
     * @param ctx               the SVGGeneratorContext
     *
     * @return a URI for the image
     * @throws SVGGraphics2DIOException if an error occurs during image file i/o
     */     
    public String lookup(ByteArrayOutputStream os,
                         int width, int height,
                         SVGGeneratorContext ctx)
                             throws SVGGraphics2DIOException {
        // We determine a checksum value for the byte data, and use it
        // as hash key for the image. This may not be unique, so we
        // need to check on actual byte-for-byte equality as well.
        // The checksum will be sufficient in most cases.
        int     checksum = getChecksum(os.toByteArray());
        Integer key      = new Integer(checksum);
        String  href     = null;
        
        Object data = getCacheableData(os);
        
        LinkedList list = (LinkedList) imageCache.get(key);
        if(list == null) {
            // Key not found: make a new key/value pair
            list = new LinkedList();
            imageCache.put(key, list);
        } else {
            // Key found: check if the image is already in the list
            for(ListIterator i = list.listIterator(0); i.hasNext(); ) {
                ImageCacheEntry entry = (ImageCacheEntry) i.next();
                if(entry.checksum == checksum && imagesMatch(entry.src, data)) {
                    href = entry.href;
                    break;
                }
            }
        }
        
        if(href == null) {
            // Still no hit: add our own
            ImageCacheEntry newEntry = createEntry(checksum, data,
                                                   width, height,
                                                   ctx);
            list.add(newEntry);
            href = newEntry.href;
        }
 
        return href;
    }
    
    /**
     * Returns an object which can be cached.
     * Implementation must determine which information
     * should actually be stored.
     *
     * @param os the byte stream which is to be coerced
     */
    abstract Object getCacheableData(ByteArrayOutputStream os);
    
    /**
     * Determines if two images are equal.
     * Interpretation of the objects referred to by
     * <code>o1</code> and <code>o2</code> is entirely
     * implementation-dependent.
     *
     * @param o1 object referring to one image
     * @param o2 object referring to the other image
     */
    abstract boolean imagesMatch(Object o1, Object o2)
                                             throws SVGGraphics2DIOException;

    /**
     * Creates a new entry for keeping in the cache.
     *
     * @param checksum the checksum from which the hash key is derived
     * @param data     the data to be cached
     * @param width    image width
     * @param height   image height
     * @param ctx      the SVGGeneratorContext
     */
    abstract ImageCacheEntry createEntry(int checksum,
                                         Object data,
                                         int width, int height,
                                         SVGGeneratorContext ctx)
                                             throws SVGGraphics2DIOException;

    /**
     *  Calculates a checksum value for the given data.
     */
    int getChecksum(byte[] data) {
        checkSum.reset();
        checkSum.update(data, 0, data.length);
        return (int) checkSum.getValue();
    }

    /**
     * Instances of this class are created to keep track of the
     * set of images processed by the ImageHandler. Each entry
     * corresponds to one unique member of this set.
     */
    private class ImageCacheEntry {

        /** A checksum calculated for the data cached */
        public int checksum;
        
        /** An implementation-dependent object referring to the data */
        public Object src;
        
        /** A uri identifying the data */
        public String href;

        /**
         * Creates a new entry
         */
        public ImageCacheEntry(int    checksum,
                               Object src,
                               String href) {
            this.checksum = checksum;
            this.src      = src;
            this.href     = href;
        }
    }

    /**
     * Cache implementation for images embedded in the SVG file.
     */
    public static class Embedded extends ImageCacher {
    
        /**
         * Sets the DOMTreeManager this cacher should work on.
         *
         * @param domTreeManager the DOMTreeManager for the tree this cacher works on
         */
        public void setDOMTreeManager(DOMTreeManager domTreeManager) {
            // A new DOMTreeManager implies a new cache, because we cache
            // images in the SVG tree itself
            if(this.domTreeManager != domTreeManager) {
                this.domTreeManager = domTreeManager;
                this.imageCache     = new Hashtable();
            }
        }

        Object getCacheableData(ByteArrayOutputStream os) {
            // In order to have only one instance of the image data
            // in memory, we cache the entire xlink:href attribute value,
            // so we can just pass a reference to the tree manager.
            return DATA_PROTOCOL_PNG_PREFIX + os.toString();
        }
        
        boolean imagesMatch(Object o1, Object o2) {
            return o1.equals(o2);
        }

        ImageCacheEntry createEntry(int checksum, Object data,
                                    int width, int height,
                                    SVGGeneratorContext ctx) {

            // Get a new unique id
            String id = ctx.idGenerator.generateID(ID_PREFIX_IMAGE);
            
            // Add the image data reference to the <defs> section
            addToTree(id, (String) data, width, height, ctx);

            // Create new cache entry
            return new ImageCacheEntry(checksum, data, SIGN_POUND + id);
        }

        /**
         *  Adds a new image element to the defs section for cached images.
         */
        private void addToTree(String id,
                               String href,
                               int width, int height,
                               SVGGeneratorContext ctx) {

            Document domFactory = domTreeManager.getDOMFactory();
            // Element imageDefs = getImageDefs(domFactory, ctx);
            
            // Create and initialize the new image element
            Element imageElement = domFactory.createElementNS(SVG_NAMESPACE_URI,
                                                              SVG_IMAGE_TAG);
            imageElement.setAttributeNS(null, SVG_ID_ATTRIBUTE,
                                              id);
            imageElement.setAttributeNS(null, SVG_WIDTH_ATTRIBUTE,
                                              Integer.toString(width));
            imageElement.setAttributeNS(null, SVG_HEIGHT_ATTRIBUTE,
                                              Integer.toString(height));
            imageElement.setAttributeNS(DefaultImageHandler.XLINK_NAMESPACE_URI,
                                              ATTR_XLINK_HREF,
                                              href);
            // imageDefs.appendChild(imageElement);
            domTreeManager.addOtherDef(imageElement);
        }
        

       /**
         *  Returns the top level defs section dedicated to cached
         *  embedded images, creating one if necessary.
         *  This one very simply creates a new defs section for each image,
         *  causing them to be spread throughout the entire SVG tree.
         *  A nicer implementation would group all imageDefs sections into
         *  one.
         */
        /*private Element getImageDefs(Document domFactory,
                                     SVGGeneratorContext ctx) {
        
            Element imageDefs = domFactory.createElementNS(SVG_NAMESPACE_URI,
                                                           SVG_DEFS_TAG);

            String id = ctx.idGenerator.generateID(ID_PREFIX_IMAGE_DEFS);        
            imageDefs.setAttributeNS(null, SVG_ID_ATTRIBUTE, id);
            
            domTreeManager.appendGroup(imageDefs, null);

            return imageDefs;
            }*/
    }
        
    /**
     * Cache implementation for file-based images.
     */
    public static class External extends ImageCacher {
    
        private String imageDir;
        private String prefix;
        private String suffix;
    
        public External(String imageDir, String prefix, String suffix) {
            super();
            this.imageDir = imageDir;
            this.prefix   = prefix;
            this.suffix   = suffix;
        }

        Object getCacheableData(ByteArrayOutputStream os) {
            return os;
        }
        
        boolean imagesMatch(Object o1, Object o2)
                throws SVGGraphics2DIOException {
            boolean match = false;
            try {     
                FileInputStream imageStream =
                                    new FileInputStream((File) o1);
                int imageLen = imageStream.available();
                byte[] imageBytes = new byte[imageLen];
                byte[] candidateBytes =
                        ((ByteArrayOutputStream) o2).toByteArray();
                
                int bytesRead = 0;
                while (bytesRead != imageLen) {
                    bytesRead += imageStream.read
                      (imageBytes, bytesRead, imageLen-bytesRead);
                }
                            
                match = Arrays.equals(imageBytes, candidateBytes);
            } catch(IOException e) {
                throw new SVGGraphics2DIOException(
                                    ERR_READ+((File) o1).getName());
            }
            return match;
        }

        ImageCacheEntry createEntry(int checksum, Object data,
                                    int width, int height,
                                    SVGGeneratorContext ctx)
            throws SVGGraphics2DIOException {
        
            // Create a new file in image directory
            File imageFile = null;

            try {    
                // While the files we are generating exist, try to create
                // another unique id.
                while (imageFile == null) {
                    String fileId = ctx.idGenerator.generateID(prefix);
                    imageFile = new File(imageDir, fileId + suffix);
                    if (imageFile.exists())
                        imageFile = null;
                }
                
                // Write data to file
                OutputStream outputStream = new FileOutputStream(imageFile);
                ((ByteArrayOutputStream) data).writeTo(outputStream);
                ((ByteArrayOutputStream) data).close();
            } catch(IOException e) {
                throw new SVGGraphics2DIOException(ERR_WRITE+imageFile.getName());
            }
            
            // Create new cache entry
            return new ImageCacheEntry(checksum, imageFile, imageFile.getName());
        }

    }

}


