/*****************************************************************************
 * 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.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.5 2003/04/11 13:58:00 vhardy 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) &&
                               hints.VALUE_RENDER_QUALITY.equals
                               (hints.get(hints.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) &&
                               hints.VALUE_RENDER_QUALITY.equals
                               (hints.get(hints.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();
        ColorModel dstCM = 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);
    }
}
