/*****************************************************************************
 * 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.ext.awt.image.renderable;

import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.RenderedImage;
import java.awt.image.renderable.RenderContext;
import java.awt.image.renderable.RenderableImage;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import org.apache.batik.ext.awt.image.PadMode;
import org.apache.batik.ext.awt.image.rendered.CachableRed;
import org.apache.batik.ext.awt.image.rendered.PadRed;
import org.apache.batik.ext.awt.image.rendered.RenderedImageCachableRed;

/**
 * This is an abstract base class that takes care of most of the
 * normal issues surrounding the implementation of the RenderableImage
 * interface.  It tries to make no assumptions about the subclass
 * implementation.
 *
 * @author <a href="mailto:Thomas.DeWeeese@Kodak.com">Thomas DeWeese</a>
 * @version $Id: AbstractRable.java,v 1.5 2003/04/11 13:57:44 vhardy Exp $
 */
public abstract class AbstractRable implements Filter {

    protected Vector srcs;
    protected Map    props = new HashMap();
    protected long   stamp = 0;

    /**
     * void constructor. The subclass must call one of the
     * flavors of init before the object becomes usable.
     * This is useful when the proper parameters to the init
     * method need to be computed in the subclasses constructor.
     */
    protected AbstractRable() {
        srcs = new Vector();
    }

    /**
     * Construct an Abstract Rable from src.
     * @param src will be the first (and only) member of the srcs
     * Vector. The bounds of src are also used to set the bounds of
     * this renderable.
     */
    protected AbstractRable(Filter src) {
        init(src, null);
    }

    /**
     * Construct an Abstract Rable from src and props.
     * @param src will also be set as the first (and only) member of
     * the srcs Vector.
     * @param props use to initialize the properties on this renderable image.
     */
    protected AbstractRable(Filter src, Map props) {
        init(src, props);
    }

    /**
     * Construct an Abstract Rable from a list of sources.
     * @param srcs This is used to initialize the srcs Vector.
     * The bounds of this renderable will be the union of the bounds
     * of all the sources in srcs.  All the members of srcs must be
     * CacheableRable otherwise an error will be thrown.
     */
    protected AbstractRable(List srcs) {
        this(srcs, null);
    }

    /**
     * Construct an Abstract Rable from a list of sources, and bounds.
     * @param srcs This is used to initialize the srcs Vector.  All
     * the members of srcs must be CacheableRable otherwise an error
     * will be thrown.
     * @param props use to initialize the properties on this renderable image.
     */
    protected AbstractRable(List srcs, Map props) {
        init(srcs, props);
    }

    /**
     * Increments the time stamp.  This should be called when ever
     * the image changes in such a way that cached output should be
     * discarded.
     */
    public final void touch() { stamp++; }

      /**
       * Returns the current modification timestamp on this Renderable
       * node.  This value will change whenever cached output data becomes
       * invalid.
       * @return Current modification timestamp value.
       */
    public long getTimeStamp() { return stamp; }

    /**
     * Initialize an Abstract Rable from src, bounds and props.  This
     * can be called long after the object is constructed to reset the
     * state of the Renderable.
     * @param src will become the first (and only) member of the srcs Vector.
     * @param bounds defines the extent of this Rable in it's user
     * coordinate system.
     */
    protected void init(Filter src) {
        touch();

        this.srcs   = new Vector(1);
        if (src != null) {
            this.srcs.add(src);
        }
    }

    /**
     * Initialize an Abstract Rable from src, bounds and props.  This
     * can be called long after the object is constructed to reset the
     * state of the Renderable.
     * @param src will also be set as the first (and only) member of
     * the srcs Vector.
     * @param bounds defines the extent of this Rable in it's user
     * coordinate system.
     * @param props use to set the properties on this renderable image.
     * Always clears the current properties (even if null).
     */
    protected void init(Filter src, Map props) {
        init (src);
        if(props != null){
            this.props.putAll(props);
        }
    }

    /**
     * Initialize an Abstract Rable from a list of sources, and
     * possibly a bounds.  This can be called long after the object is
     * constructed to reset the state of the Renderable.
     * @param srcs Used the create a new srcs Vector (old sources are dropped).
     * @param bounds defines the new bounds of the object. If it is null
     * then the bounds become the union of the bounds of all sources.
     */
    protected void init(List srcs) {
        touch();
        this.srcs   = new Vector(srcs);
    }

    /**
     * Initialize an Abstract Rable from a list of sources, and
     * possibly a bounds.  This can be called long after the object is
     * constructed to reset the state of the Renderable.
     * @param srcs Used the create a new srcs Vector (old sources are dropped).
     * @param bounds defines the new bounds of the object. If it is null
     * then the bounds become the union of the bounds of all sources.
     * @param props use to set the properties on this renderable image.
     * Always clears the current properties (even if null).
     */
    protected void init(List srcs, Map props) {
        init (srcs);
        if(props != null)
            this.props.putAll(props);
    }

    public Rectangle2D getBounds2D() {
        Rectangle2D bounds = null;
        if (this.srcs.size() != 0) {
            Iterator i = srcs.iterator();
            Filter src = (Filter)i.next();
            bounds = (Rectangle2D)src.getBounds2D().clone();
            Rectangle2D r;
            while (i.hasNext()) {
                src = (Filter)i.next();
                r = src.getBounds2D();
                Rectangle2D.union(bounds, r, bounds);
            }
        }
        return bounds;
    }

    public Vector getSources() {
        return srcs;
    }

    public RenderedImage createDefaultRendering() {
        return createScaledRendering(100, 100, null);
    }

    public RenderedImage createScaledRendering(int w, int h, 
                                           RenderingHints hints) {
        float sX = w/getWidth();
        float sY = h/getHeight();
        float scale = Math.min(sX, sY);

        AffineTransform at = AffineTransform.getScaleInstance(scale, scale);
        RenderContext rc = new RenderContext(at, hints);

        float dX = (getWidth()*scale)-w;
        float dY = (getHeight()*scale)-h;

        RenderedImage ri = createRendering(rc);
        CachableRed cr = RenderedImageCachableRed.wrap(ri);
        return new PadRed(cr, new Rectangle((int)(dX/2), (int)(dY/2), w, h),
                          PadMode.ZERO_PAD, null);
    }

    public float getMinX() {
        return (float)getBounds2D().getX();
    }
    public float getMinY() {
        return (float)getBounds2D().getY();
    }
    public float getWidth() {
        return (float)getBounds2D().getWidth();
    }
    public float getHeight() {
        return (float)getBounds2D().getHeight();
    }

    public Object getProperty(String name) {
        Object ret = props.get(name);
        if (ret != null) return ret;
        Iterator i = srcs.iterator();
        while (i.hasNext()) {
            RenderableImage ri = (RenderableImage)i.next();
            ret = ri.getProperty(name);
            if (ret != null) return ret;
        }
        return null;
    }

    public String [] getPropertyNames() {
        Set keys = props.keySet();
        Iterator iter = keys.iterator();
        String [] ret  = new String[keys.size()];
        int i=0;
        while (iter.hasNext()) {
            ret[i++] = (String)iter.next();
        }

        iter = srcs.iterator();
        while (iter.hasNext()) {
            RenderableImage ri = (RenderableImage)iter.next();
            String [] srcProps = ri.getPropertyNames();
            if (srcProps.length != 0) {
                String [] tmp = new String[ret.length+srcProps.length];
                System.arraycopy(tmp,0,tmp,0,ret.length);
                System.arraycopy(tmp,ret.length,srcProps,0,srcProps.length);
                ret = tmp;
            }
        }

        return ret;
    }

    public boolean isDynamic() { return false; }

    public Shape getDependencyRegion(int srcIndex, 
                                     Rectangle2D outputRgn) {
        if ((srcIndex < 0) || (srcIndex > srcs.size()))
            throw new IndexOutOfBoundsException
                ("Nonexistant source requested.");

        // We only depend on our source for stuff that is inside
        // our bounds...
        Rectangle2D srect = (Rectangle2D)outputRgn.clone();
        Rectangle2D bounds = getBounds2D();

        // Return empty rect if they don't intersect.
        if (bounds.intersects(srect) == false)
            return new Rectangle2D.Float();
            
        Rectangle2D.intersect(srect, bounds, srect);
        return srect;
    }

    public Shape getDirtyRegion(int srcIndex, 
                                Rectangle2D inputRgn) {
        if ((srcIndex < 0) || (srcIndex > srcs.size()))
            throw new IndexOutOfBoundsException
                ("Nonexistant source requested.");

          // Changes in the input region don't propogate outside our
          // bounds.
        Rectangle2D drect = (Rectangle2D)inputRgn.clone();
        Rectangle2D bounds = getBounds2D();

        // Return empty rect if they don't intersect.
        if (bounds.intersects(drect) == false)
            return new Rectangle2D.Float();

        Rectangle2D.intersect(drect, bounds, drect);
        return drect;
    }
    
    
    /* left for subclass:
       public RenderedImage createRendering(RenderContext rc);
    */
}
