/*****************************************************************************
 * 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.NoninvertibleTransformException;
import java.awt.geom.Rectangle2D;
import java.awt.image.RenderedImage;
import java.awt.image.renderable.RenderContext;

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

/**
 * GaussianBlurRable implementation
 *
 * @author <a href="mailto:vincent.hardy@eng.sun.com>Vincent Hardy</a>
 * @version $Id: GaussianBlurRable8Bit.java,v 1.7 2003/04/11 13:57:48 vhardy Exp $
 */
public class GaussianBlurRable8Bit
    extends    AbstractColorInterpolationRable
    implements GaussianBlurRable {

    /**
     * Deviation along the x-axis
     */
    private double stdDeviationX;

    /**
     * Deviation along the y-axis
     */
    private double stdDeviationY;

    public GaussianBlurRable8Bit(Filter src,
                                 double stdevX, double stdevY) {
        super(src, null);
        setStdDeviationX(stdevX);
        setStdDeviationY(stdevY);
    }

    /**
     * The deviation along the x axis, in user space.
     * @param stdDeviationX should be greater than zero.
     */
    public void setStdDeviationX(double stdDeviationX){
        if(stdDeviationX < 0){
            throw new IllegalArgumentException();
        }

        touch();
        this.stdDeviationX = stdDeviationX;
    }

    /**
     * The deviation along the y axis, in user space.
     * @param stdDeviationY should be greater than zero
     */
    public void setStdDeviationY(double stdDeviationY){
        if(stdDeviationY < 0){
            throw new IllegalArgumentException();
        }
        touch();
        this.stdDeviationY = stdDeviationY;
    }

    /**
     * Returns the deviation along the x-axis, in user space.
     */
    public double getStdDeviationX(){
        return stdDeviationX;
    }

    /**
     * Returns the deviation along the y-axis, in user space.
     */
    public double getStdDeviationY(){
        return stdDeviationY;
    }

    /**
     * Sets the source of the blur operation
     */
    public void setSource(Filter src){
        init(src, null);
    }

    /**
     * Constant: 3*sqrt(2*PI)/4
     */
    static final float DSQRT2PI = (float)(Math.sqrt(2*Math.PI)*3.0/4.0);

    /**
     * Grow the source's bounds
     */
    public Rectangle2D getBounds2D(){
        Rectangle2D src = getSource().getBounds2D();
        float dX = (float)(stdDeviationX*DSQRT2PI);
        float dY = (float)(stdDeviationY*DSQRT2PI);
        float radX = 3*dX/2;
        float radY = 3*dY/2;
        return new Rectangle2D.Float
            ((float)(src.getMinX()  -radX),
             (float)(src.getMinY()  -radY),
             (float)(src.getWidth() +2*radX),
             (float)(src.getHeight()+2*radY));
    }

    /**
     * Returns the source of the blur operation
     */
    public Filter getSource(){
        return (Filter)getSources().get(0);
    }

    public final static double eps = 0.0001;
    public static boolean eps_eq(double f1, double f2) {
        return ((f1 >= f2-eps) && (f1 <= f2+eps));
    }
    public static boolean eps_abs_eq(double f1, double f2) {
        if (f1 <0) f1 = -f1;
        if (f2 <0) f2 = -f2;
        return eps_eq(f1, f2);
    }

    public RenderedImage createRendering(RenderContext rc) {
        // Just copy over the rendering hints.
        RenderingHints rh = rc.getRenderingHints();
        if (rh == null) rh = new RenderingHints(null);

        // update the current affine transform
        AffineTransform at = rc.getTransform();


        // This splits out the scale and applies it
        // prior to the Gaussian.  Then after appying the gaussian
        // it applies the shear (rotation) and translation components.
        double sx = at.getScaleX();
        double sy = at.getScaleY();

        double shx = at.getShearX();
        double shy = at.getShearY();

        double tx = at.getTranslateX();
        double ty = at.getTranslateY();

        // The Scale is the "hypotonose" of the matrix vectors.
        double scaleX = Math.sqrt(sx*sx + shy*shy);
        double scaleY = Math.sqrt(sy*sy + shx*shx);

        double sdx = stdDeviationX*scaleX;
        double sdy = stdDeviationY*scaleY;

        // This is the affine transform between our usr space and an
        // intermediate space which is scaled similarly to our device
        // space but is still axially aligned with our device space.
        AffineTransform srcAt;

        // This is the affine transform between our intermediate
        // coordinate space and the real device space, or null (if
        // we don't need an intermediate space).
        AffineTransform resAt;

        int outsetX, outsetY;
        if ((sdx < 10)           &&
            (sdy < 10)           &&
            eps_eq    (sdx, sdy) &&
            eps_abs_eq(sx/scaleX, sy/scaleY)) {
            // Ok we have a square Gaussian kernel which means it is
            // circularly symetric, further our residual matrix (after
            // removing scaling) is a rotation matrix (perhaps with
            // mirroring), thus we can generate our source directly in
            // device space and convolve there rather than going to an
            // intermediate space (axially aligned with usr space) and
            // then completing the requested rotation/shear, with an
            // AffineRed...

            srcAt = at;
            resAt = null;
            outsetX = 0;
            outsetY = 0;
        } else {

            // Limit std dev to 10.  Put any extra into our
            // residual matrix.  This will effectively linearly
            // interpolate, but with such a large StdDev the
            // function is fairly smooth anyway...
            if (sdx > 10) {
                scaleX = scaleX*10/sdx;
                sdx = 10;
            }
            if (sdy > 10) {
                scaleY = scaleY*10/sdy;
                sdy = 10;
            }

            // Scale to device coords.
            srcAt = AffineTransform.getScaleInstance(scaleX, scaleY);

            // The shear/rotation simply divides out the
            // common scale factor in the matrix.
            resAt = new AffineTransform(sx/scaleX, shy/scaleX,
                                        shx/scaleY,  sy/scaleY,
                                        tx, ty);
            // Add a pixel all around for the affine to interpolate with.
            outsetX = 1;
            outsetY = 1;
        }


        Shape aoi = rc.getAreaOfInterest();
        if(aoi == null)
            aoi = getBounds2D();

        Shape devShape = srcAt.createTransformedShape(aoi);
        Rectangle devRect = devShape.getBounds();

        outsetX += GaussianBlurRed8Bit.surroundPixels(sdx, rh);
        outsetY += GaussianBlurRed8Bit.surroundPixels(sdy, rh);

        devRect.x      -= outsetX;
        devRect.y      -= outsetY;
        devRect.width  += 2*outsetX;
        devRect.height += 2*outsetY;

        Rectangle2D r;
        try {
            AffineTransform invSrcAt = srcAt.createInverse();
            r = invSrcAt.createTransformedShape(devRect).getBounds2D();
        } catch (NoninvertibleTransformException nte) {
            // Grow the region in usr space.
            r = aoi.getBounds2D();
            r = new Rectangle2D.Double(r.getX()-outsetX/scaleX,
                                       r.getY()-outsetY/scaleY,
                                       r.getWidth() +2*outsetX/scaleX,
                                       r.getHeight()+2*outsetY/scaleY);
        }

        RenderedImage ri;
        ri = getSource().createRendering(new RenderContext(srcAt, r, rh));
        if (ri == null)
            return null;

        CachableRed cr = convertSourceCS(ri);

        // System.out.println("DevRect: " + devRect);

        if (!devRect.equals(cr.getBounds())) {
            // System.out.println("MisMatch Dev:" + devRect);
            // System.out.println("         CR :" + cr.getBounds());
            cr = new PadRed(cr, devRect, PadMode.ZERO_PAD, rh);
        }

        cr = new GaussianBlurRed8Bit(cr, sdx, sdy, rh);

        if ((resAt != null) && (!resAt.isIdentity()))
            cr = new AffineRed(cr, resAt, rh);

        return cr;
    }

    /**
     * Returns the region of input data is is required to generate
     * outputRgn.
     * @param srcIndex  The source to do the dependency calculation for.
     * @param outputRgn The region of output you are interested in
     *  generating dependencies for.  The is given in the user coordiate
     *  system for this node.
     * @return The region of input required.  This is in the user
     * coordinate system for the source indicated by srcIndex.
     */
    public Shape getDependencyRegion(int srcIndex, Rectangle2D outputRgn){
        if(srcIndex != 0)
            outputRgn = null;
        else {
            // There is only one source in GaussianBlur
            float dX = (float)(stdDeviationX*DSQRT2PI);
            float dY = (float)(stdDeviationY*DSQRT2PI);
            float radX = 3*dX/2;
            float radY = 3*dY/2;
            outputRgn = new Rectangle2D.Float
                            ((float)(outputRgn.getMinX()  -radX),
                             (float)(outputRgn.getMinY()  -radY),
                             (float)(outputRgn.getWidth() +2*radX),
                             (float)(outputRgn.getHeight()+2*radY));

            Rectangle2D bounds = getBounds2D();
            if (outputRgn.intersects(bounds) == false)
                return new Rectangle2D.Float();
            // Intersect with output region
            outputRgn = outputRgn.createIntersection(bounds);
        }

        return outputRgn;
    }

    /**
     * This calculates the region of output that is affected by a change
     * in a region of input.
     * @param srcIndex The input that inputRgn reflects changes in.
     * @param inputRgn the region of input that has changed, used to
     *  calculate the returned shape.  This is given in the user
     *  coordinate system of the source indicated by srcIndex.
     * @return The region of output that would be invalid given
     *  a change to inputRgn of the source selected by srcIndex.
     *  this is in the user coordinate system of this node.
     */
    public Shape getDirtyRegion(int srcIndex, Rectangle2D inputRgn){
        Rectangle2D dirtyRegion = null;
        if(srcIndex == 0){
            float dX = (float)(stdDeviationX*DSQRT2PI);
            float dY = (float)(stdDeviationY*DSQRT2PI);
            float radX = 3*dX/2;
            float radY = 3*dY/2;
            inputRgn = new Rectangle2D.Float
                            ((float)(inputRgn.getMinX()  -radX),
                             (float)(inputRgn.getMinY()  -radY),
                             (float)(inputRgn.getWidth() +2*radX),
                             (float)(inputRgn.getHeight()+2*radY));

            Rectangle2D bounds = getBounds2D();
            if (inputRgn.intersects(bounds) == false)
                return new Rectangle2D.Float();
            // Intersect with input region
            dirtyRegion = inputRgn.createIntersection(bounds);
        }

        return dirtyRegion;
    }


}
