/*

 ============================================================================
                   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.ext.awt.image.rendered;

import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.color.ColorSpace;
import java.awt.image.ColorModel;
import java.awt.image.ConvolveOp;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferInt;
import java.awt.image.DirectColorModel;
import java.awt.image.Kernel;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.SinglePixelPackedSampleModel;
import java.awt.image.WritableRaster;

import org.apache.batik.ext.awt.image.GraphicsUtil;

/**
 * This implementation of RenderableImage will render its input
 * GraphicsNode on demand for tiles.
 *
 * @author <a href="mailto:vincent.hardy@eng.sun.com">Vincent Hardy</a>
 * @version $Id: GaussianBlurRed8Bit.java,v 1.7 2003/08/09 16:58:42 deweese Exp $
 */
public class GaussianBlurRed8Bit extends AbstractRed {

    int xinset, yinset;
    double stdDevX, stdDevY;
    RenderingHints hints;
    ConvolveOp [] convOp = new ConvolveOp [2];
    int dX, dY;

    /**
     * Construct a blurred version of <tt>src</tt>, by blurring with a
     * gaussian kernel with standard Deviation of <tt>stdDev</tt> pixels.
     * @param src The source image to blur
     * @param stdDev The Standard Deviation of the Gaussian kernel.
     * @param rh     Rendering hints.
     */
    public GaussianBlurRed8Bit(CachableRed    src, 
                               double         stdDev, 
                               RenderingHints rh) {
        this(src, stdDev, stdDev, rh);
    }

    /**
     * Construct a blurred version of <tt>src</tt>, by blurring with a
     * gaussian kernel with standard Deviation of <tt>stdDev</tt> pixels.
     * @param src The source image to blur
     * @param stdDevX The Standard Deviation of the Gaussian kernel in X
     * @param stdDevY The Standard Deviation of the Gaussian kernel in Y
     * @param rh     Rendering hints.
     */
    public GaussianBlurRed8Bit(CachableRed src, 
                               double stdDevX, double stdDevY,
                               RenderingHints rh) {
        super(); // Remember to call super.init()

        this.stdDevX = stdDevX;
        this.stdDevY = stdDevY;
        this.hints   = rh;

        xinset = surroundPixels(stdDevX, rh);
        yinset = surroundPixels(stdDevY, rh);

        Rectangle myBounds = src.getBounds();
        myBounds.x      += xinset;
        myBounds.y      += yinset;
        myBounds.width  -= 2*xinset;
        myBounds.height -= 2*yinset;
        if ((myBounds.width <= 0) ||
            (myBounds.height <= 0)) {
            myBounds.width=0;
            myBounds.height=0;
        }

        ColorModel cm  = fixColorModel(src);
        SampleModel sm = src.getSampleModel();
        int tw = sm.getWidth();
        int th = sm.getHeight();
        if (tw > myBounds.width)  tw = myBounds.width;
        if (th > myBounds.height) th = myBounds.height;
        sm = cm.createCompatibleSampleModel(tw, th);

        init(src, myBounds, cm, sm, 
             src.getTileGridXOffset()+xinset,
             src.getTileGridYOffset()+yinset, null);

        boolean highQuality = ((hints != null) &&
                               RenderingHints.VALUE_RENDER_QUALITY.equals
                               (hints.get(RenderingHints.KEY_RENDERING)));

        // System.out.println("StdDev: " + stdDevX + "x" + stdDevY);
        if ((xinset != 0) && ((stdDevX < 2) || highQuality))
            convOp[0] = new ConvolveOp(makeQualityKernelX(xinset*2+1));
        else
            dX = (int)Math.floor(DSQRT2PI*stdDevX+0.5f);

        if ((yinset != 0) && ((stdDevY < 2) || highQuality))
            convOp[1] = new ConvolveOp(makeQualityKernelY(yinset*2+1));
        else
            dY = (int)Math.floor(DSQRT2PI*stdDevY+0.5f);
    }

    /**
     * Constant: sqrt(2*PI)
     */
    static final float SQRT2PI = (float)Math.sqrt(2*Math.PI);

    /**
     * Constant: 3*sqrt(2*PI)/4
     */
    static final float DSQRT2PI = SQRT2PI*3f/4f;

    /**
     * Constant: precision used in computation of the Kernel radius
     */
    static final float precision = 0.499f;

    /**
     * Calculate the number of surround pixels required for a given
     * standard Deviation.
     */
    public static int surroundPixels(double stdDev) {
        return surroundPixels(stdDev, null);
    }

    /**
     * Calculate the number of surround pixels required for a given
     * standard Deviation.  Also takes into account rendering quality
     * hint.  
     */ 
    public static int surroundPixels(double stdDev, RenderingHints hints) {
        boolean highQuality = ((hints != null) &&
                               RenderingHints.VALUE_RENDER_QUALITY.equals
                               (hints.get(RenderingHints.KEY_RENDERING)));

        if ((stdDev < 2) || highQuality) {
            // Start with 1/2 the zero box enery.   
            float areaSum = (float)(0.5/(stdDev*SQRT2PI));
            int i=0;
            while (areaSum < precision) {
                areaSum += (float)(Math.pow(Math.E, -i*i/(2*stdDev*stdDev)) /
                                   (stdDev*SQRT2PI));
                i++;
            }

            return i;
        }

        //compute d
        int diam = (int)Math.floor(DSQRT2PI*stdDev+0.5f);
        if (diam%2 == 0) 
            return diam-1 + diam/2; // even case
        else
            return diam-2 + diam/2;   // Odd case
    }

    /*
     * Here we compute the data for the one-dimensional kernel of
     * length '2*(radius-1) + 1'
     *
     * @param radius stdDeviationX or stdDeviationY.
     * @see #makeQualityKernels */
    private float [] computeQualityKernelData(int len, double stdDev){
        final float kernelData[] = new float [len];
        
        int mid = len/2;
        float sum = 0; // Used to normalise the kernel
        for(int i=0; i<len; i++){
            kernelData[i] = (float)(Math.pow(Math.E, -(i-mid)*(i-mid)/
                                             (2*stdDev*stdDev)) /
                                    (SQRT2PI*stdDev));
            sum += kernelData[i];
        }

        // Normalise: make elements sum to 1
        for (int i=0; i<len; i++)
            kernelData[i] /= sum;

        return kernelData;
    }

    private Kernel makeQualityKernelX(int len) {
        return new Kernel(len, 1, computeQualityKernelData(len, stdDevX));
    }

    private Kernel makeQualityKernelY(int len) {
        return new Kernel(1, len, computeQualityKernelData(len, stdDevY));
    }

    public WritableRaster copyData(WritableRaster wr) {
        // Get my source.
        CachableRed src = (CachableRed)getSources().get(0);

        Rectangle r = wr.getBounds();
        r.x      -=   xinset; 
        r.y      -=   yinset;
        r.width  += 2*xinset;
        r.height += 2*yinset;

        // System.out.println("Gaussian GenR: " + wr);
        // System.out.println("SrcReq: " + r);

        ColorModel srcCM = src.getColorModel();

        WritableRaster tmpR1=null, tmpR2=null;

        tmpR1 = srcCM.createCompatibleWritableRaster(r.width, r.height);
        {
            WritableRaster fill;
            fill = tmpR1.createWritableTranslatedChild(r.x, r.y);
            src.copyData(fill);
        }
        if (srcCM.hasAlpha() && !srcCM.isAlphaPremultiplied()) 
            GraphicsUtil.coerceData(tmpR1, srcCM, true);

        // For the blur box approx we can use dest as our intermediate
        // otherwise we let it default to null which means we create a new
        // one...

        // this lets the Vertical conv know how much is junk, so it
        // doesn't bother to convolve the top and bottom edges
        int skipX;
	// long t1 = System.currentTimeMillis();
        if (xinset == 0) {
            skipX = 0;
        } else if (convOp[0] != null) {
            tmpR2 = getColorModel().createCompatibleWritableRaster
                (r.width, r.height);
            tmpR2 = convOp[0].filter(tmpR1, tmpR2);
            skipX = convOp[0].getKernel().getXOrigin();

            // Swap them...
            WritableRaster tmp = tmpR1;
            tmpR1 = tmpR2;
            tmpR2 = tmp;
        } else {
            if ((dX&0x01) == 0){
                tmpR1 = boxFilterH(tmpR1, tmpR1, 0,    0,   dX,   dX/2);
                tmpR1 = boxFilterH(tmpR1, tmpR1, dX/2, 0,   dX,   dX/2-1);
                tmpR1 = boxFilterH(tmpR1, tmpR1, dX-1, 0,   dX+1, dX/2);
                skipX = dX-1 + dX/2;
            } else {
                tmpR1 = boxFilterH(tmpR1, tmpR1, 0,    0,   dX, dX/2);
                tmpR1 = boxFilterH(tmpR1, tmpR1, dX/2, 0,   dX, dX/2);
                tmpR1 = boxFilterH(tmpR1, tmpR1, dX-2, 0,   dX, dX/2);
                skipX = dX-2 + dX/2;
            }
        }

        if (yinset == 0) {
            tmpR2 = tmpR1;
        } else if (convOp[1] != null) {
            if (tmpR2 == null) {
                tmpR2 = getColorModel().createCompatibleWritableRaster
                    (r.width, r.height);
            }
            tmpR2 = convOp[1].filter(tmpR1, tmpR2);
        } else {
            if ((dY&0x01) == 0){
                tmpR1 = boxFilterV(tmpR1, tmpR1, skipX, 0,    dY,   dY/2);
                tmpR1 = boxFilterV(tmpR1, tmpR1, skipX, dY/2, dY,   dY/2-1);
                tmpR1 = boxFilterV(tmpR1, tmpR1, skipX, dY-1, dY+1, dY/2);
            }
            else {
                tmpR1 = boxFilterV(tmpR1, tmpR1, skipX, 0,    dY, dY/2);
                tmpR1 = boxFilterV(tmpR1, tmpR1, skipX, dY/2, dY, dY/2);
                tmpR1 = boxFilterV(tmpR1, tmpR1, skipX, dY-2, dY, dY/2);
            }
            tmpR2 = tmpR1;
        }
	// long t2 = System.currentTimeMillis();
	// System.out.println("Time: " + (t2-t1) +
	// 		      (((convOp[0] != null) || (convOp[1] != null))?
	// 		       " ConvOp":""));
        // System.out.println("Rasters  WR :" + wr.getBounds());
        // System.out.println("         tmp:" + tmpR2.getBounds());
        // System.out.println("      bounds:" + getBounds());
        // System.out.println("       skipX:" + skipX + 
        //                    " dx:" + dX + " Dy: " + dY);
        tmpR2 = tmpR2.createWritableTranslatedChild(r.x, r.y);
        GraphicsUtil.copyData(tmpR2, wr);

        return wr;
    }

    private WritableRaster boxFilterH(Raster src, WritableRaster dest,
                                      int skipX, int skipY, 
                                      int boxSz, int loc) {

        final int w = src.getWidth();
        final int h = src.getHeight();

          // Check if the raster is wide enough to do _any_ work
        if (w < (2*skipX)+boxSz) return dest;
        if (h < (2*skipY))       return dest;

        final SinglePixelPackedSampleModel srcSPPSM = 
            (SinglePixelPackedSampleModel)src.getSampleModel();

        final SinglePixelPackedSampleModel dstSPPSM = 
            (SinglePixelPackedSampleModel)dest.getSampleModel();
        
        // Stride is the distance between two consecutive column elements,
        // in the one-dimention dataBuffer
        final int srcScanStride = srcSPPSM.getScanlineStride();
        final int dstScanStride = dstSPPSM.getScanlineStride();

        // Access the integer buffer for each image.
        DataBufferInt srcDB = (DataBufferInt)src.getDataBuffer();
        DataBufferInt dstDB = (DataBufferInt)dest.getDataBuffer();

        // Offset defines where in the stack the real data begin
        final int srcOff 
            = (srcDB.getOffset() + 
               srcSPPSM.getOffset
               (src.getMinX()-src.getSampleModelTranslateX(),
                src.getMinY()-src.getSampleModelTranslateY()));
        final int dstOff 
            = (dstDB.getOffset() +
               dstSPPSM.getOffset
               (dest.getMinX()-dest.getSampleModelTranslateX(),
                dest.getMinY()-dest.getSampleModelTranslateY()));

        // Access the pixel value array
        final int srcPixels [] = srcDB.getBankData()[0];
        final int destPixels[] = dstDB.getBankData()[0];

        final int [] buffer = new int [boxSz];
	int curr, prev;

          // Fixed point normalization factor (8.24)
        int scale = (1<<24)/boxSz;
        
        /*
         * System.out.println("Info: srcOff: " + srcOff + 
         *                    " x: " + skipX +
         *                    " y: " + skipY +
         *                    " w: " + w +
         *                    " h: " + h +
         *                    " boxSz " + boxSz +
         *                    " srcStride: " + srcScanStride);
         */

        for (int y=skipY; y<(h-skipY); y++) {
            int sp     = srcOff + y*srcScanStride;
            int dp     = dstOff + y*dstScanStride;
            int rowEnd = sp + (w-skipX);

            int k    = 0;
            int sumA = 0;
            int sumR = 0;
            int sumG = 0;
            int sumB = 0;

            sp += skipX;
            int end  = sp+boxSz;

            while (sp < end) {
                curr = buffer[k] = srcPixels[sp];
                sumA += (curr>>> 24);
                sumR += (curr >> 16)&0xFF;
                sumG += (curr >>  8)&0xFF;
                sumB += (curr	   )&0xFF;
                k++;
                sp++;
            }

            dp += skipX + loc;
            prev = destPixels[dp] = (( (sumA*scale)&0xFF000000)       |
				     (((sumR*scale)&0xFF000000)>>>8)  |
				     (((sumG*scale)&0xFF000000)>>>16) |
				     (((sumB*scale)&0xFF000000)>>>24));
            dp++;
            k=0;
            while (sp < rowEnd) {
		curr = buffer[k];
		if (curr == srcPixels[sp]) {
		    destPixels[dp] = prev;
		} else {
		    sumA -= (curr>>> 24);
		    sumR -= (curr >> 16)&0xFF;
		    sumG -= (curr >>  8)&0xFF;
		    sumB -= (curr      )&0xFF;

		    curr = buffer[k] = srcPixels[sp];

		    sumA += (curr>>> 24);
		    sumR += (curr >> 16)&0xFF;
		    sumG += (curr >>  8)&0xFF;
		    sumB += (curr      )&0xFF;
		    prev = destPixels[dp] = (( (sumA*scale)&0xFF000000)       |
					     (((sumR*scale)&0xFF000000)>>>8)  |
					     (((sumG*scale)&0xFF000000)>>>16) |
					     (((sumB*scale)&0xFF000000)>>>24));
		}
                k = (k+1)%boxSz;
                sp++;
                dp++;
            }
        }
        return dest;
    }

    private WritableRaster boxFilterV(Raster src, WritableRaster dest,
                                      int skipX, int skipY, 
                                      int boxSz, int loc) {

        final int w = src.getWidth();
        final int h = src.getHeight();

          // Check if the raster is wide enough to do _any_ work
        if (w < (2*skipX))       return dest;
        if (h < (2*skipY)+boxSz) return dest;

        final SinglePixelPackedSampleModel srcSPPSM = 
            (SinglePixelPackedSampleModel)src.getSampleModel();

        final SinglePixelPackedSampleModel dstSPPSM = 
            (SinglePixelPackedSampleModel)dest.getSampleModel();
        
        // Stride is the distance between two consecutive column elements,
        // in the one-dimention dataBuffer
        final int srcScanStride = srcSPPSM.getScanlineStride();
        final int dstScanStride = dstSPPSM.getScanlineStride();

        // Access the integer buffer for each image.
        DataBufferInt srcDB = (DataBufferInt)src.getDataBuffer();
        DataBufferInt dstDB = (DataBufferInt)dest.getDataBuffer();

        // Offset defines where in the stack the real data begin
        final int srcOff 
            = (srcDB.getOffset() + 
               srcSPPSM.getOffset
               (src.getMinX()-src.getSampleModelTranslateX(),
                src.getMinY()-src.getSampleModelTranslateY()));
        final int dstOff 
            = (dstDB.getOffset() +
               dstSPPSM.getOffset
               (dest.getMinX()-dest.getSampleModelTranslateX(),
                dest.getMinY()-dest.getSampleModelTranslateY()));


        // Access the pixel value array
        final int srcPixels [] = srcDB.getBankData()[0];
        final int destPixels[] = dstDB.getBankData()[0];

        final int [] buffer = new int [boxSz];
	int curr, prev;

          // Fixed point normalization factor (8.24)
        final int scale = (1<<24)/boxSz;

        /*
         * System.out.println("Info: srcOff: " + srcOff + 
         *                    " x: " + skipX +
         *                    " y: " + skipY +
         *                    " w: " + w +
         *                    " h: " + h +
         *                    " boxSz " + boxSz +
         *                    " srcStride: " + srcScanStride);
         */

        for (int x=skipX; x<(w-skipX); x++) {
            int sp = srcOff + x;
            int dp = dstOff + x;
            int colEnd = sp + (h-skipY)*srcScanStride;

            int k=0;
            int sumA = 0;
            int sumR = 0;
            int sumG = 0;
            int sumB = 0;

            sp += skipY*srcScanStride;
            int end  = sp+(boxSz*srcScanStride);

            while (sp < end) {
                curr = buffer[k] = srcPixels[sp];
                sumA += (curr>>> 24);
                sumR += (curr >> 16)&0xFF;
                sumG += (curr >>  8)&0xFF;
                sumB += (curr 	   )&0xFF;
                k++;
                sp+=srcScanStride;
            }


            dp += (skipY + loc)*dstScanStride;
            prev = destPixels[dp] = (( (sumA*scale)&0xFF000000)       |
				     (((sumR*scale)&0xFF000000)>>>8)  |
				     (((sumG*scale)&0xFF000000)>>>16) |
				     (((sumB*scale)&0xFF000000)>>>24));
            dp+=dstScanStride;
            k=0;
            while (sp < colEnd) {
		curr = buffer[k];
		if (curr == srcPixels[sp]) {
		    destPixels[dp] = prev;
		} else {
		    sumA -= (curr>>> 24);
		    sumR -= (curr >> 16)&0xFF;
		    sumG -= (curr >>  8)&0xFF;
		    sumB -= (curr      )&0xFF;

		    curr = buffer[k] = srcPixels[sp];

		    sumA += (curr>>> 24);
		    sumR += (curr >> 16)&0xFF;
		    sumG += (curr >>  8)&0xFF;
		    sumB += (curr      )&0xFF;
		    prev = destPixels[dp] = (( (sumA*scale)&0xFF000000)       |
					     (((sumR*scale)&0xFF000000)>>>8)  |
					     (((sumG*scale)&0xFF000000)>>>16) |
					     (((sumB*scale)&0xFF000000)>>>24));
		}
                k = (k+1)%boxSz;
                sp+=srcScanStride;
                dp+=dstScanStride;
            }
        }
        return dest;
    }

    protected static ColorModel fixColorModel(CachableRed src) {
        ColorModel  cm = src.getColorModel();

        int b = src.getSampleModel().getNumBands();
        int [] masks = new int[4];
        switch (b) {
        case 1:
            masks[0] = 0xFF;
            break;
        case 2:
            masks[0] = 0x00FF;
            masks[3] = 0xFF00;
            break;
        case 3:
            masks[0] = 0xFF0000;
            masks[1] = 0x00FF00;
            masks[2] = 0x0000FF;
            break;
        case 4:
            masks[0] = 0x00FF0000;
            masks[1] = 0x0000FF00;
            masks[2] = 0x000000FF;
            masks[3] = 0xFF000000;
            break;
        default:
            throw new IllegalArgumentException
                ("GaussianBlurRed8Bit only supports one to four band images");
        }
        ColorSpace cs = cm.getColorSpace();
        return new DirectColorModel(cs, 8*b, masks[0], masks[1], 
                                    masks[2], masks[3],
                                    true, DataBuffer.TYPE_INT);
    }
}
