/*****************************************************************************
 * 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.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.color.ColorSpace;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
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.RenderedImage;
import java.awt.image.SinglePixelPackedSampleModel;
import java.awt.image.WritableRaster;
import java.awt.image.renderable.RenderContext;

import org.apache.batik.ext.awt.image.GraphicsUtil;
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.BufferedImageCachableRed;
import org.apache.batik.ext.awt.image.rendered.CachableRed;
import org.apache.batik.ext.awt.image.rendered.PadRed;

/**
 * Convolves an image with a convolution matrix.
 *
 * Known limitations:
 *   Does not support bias other than zero - pending 16bit pathway
 *   Does not support edgeMode="wrap" - pending Tile code.
 *
 * @author <a href="mailto:Thomas.DeWeeese@Kodak.com">Thomas DeWeese</a>
 * @version $Id: ConvolveMatrixRable8Bit.java,v 1.7 2003/04/11 13:57:47 vhardy Exp $
 */
public class ConvolveMatrixRable8Bit
    extends    AbstractColorInterpolationRable
    implements ConvolveMatrixRable {

    Kernel kernel;
    Point  target;
    float bias;
    boolean kernelHasNegValues;
    PadMode edgeMode;
    float [] kernelUnitLength = new float[2];

    boolean preserveAlpha = false;;

    public ConvolveMatrixRable8Bit(Filter source) {
        super(source);
    }

    public Filter getSource() {
        return (Filter)getSources().get(0);
    }

    public void setSource(Filter src) {
        init(src);
    }


    /**
     * Returns the Convolution Kernel in use
     */
    public Kernel getKernel() {
        return kernel;
    }

    /**
     * Sets the Convolution Kernel to use.
     * @param k Kernel to use for convolution.
     */
    public void setKernel(Kernel k) {
        touch();
        this.kernel = k;
        kernelHasNegValues = false;
        float [] kv = k.getKernelData(null);
        for (int i=0; i<kv.length; i++)
            if (kv[i] < 0) {
                kernelHasNegValues = true;
                break;
            }
    }

    public Point getTarget() {
        return (Point)target.clone();
    }

    public void setTarget(Point pt) {
        touch();
        this.target = (Point)pt.clone();
    }

    /**
     * Returns the shift value to apply to the result of convolution
     */
    public double getBias() {
        return bias;
    }

    /**
     * Returns the shift value to apply to the result of convolution
     */
    public void setBias(double bias) {
        touch();
        this.bias = (float)bias;
    }

    /**
     * Returns the current edge handling mode.
     */
    public PadMode getEdgeMode() {
        return edgeMode;
    }

    /**
     * Sets the current edge handling mode.
     */
    public void setEdgeMode(PadMode edgeMode) {
        touch();
        this.edgeMode = edgeMode;
    }

    /**
     * Returns the [x,y] distance in user space between kernel values
     */
    public double [] getKernelUnitLength() {
        if (kernelUnitLength == null)
            return null;

        double [] ret = new double[2];
        ret[0] = kernelUnitLength[0];
        ret[1] = kernelUnitLength[1];
        return ret;
    }

    /**
     * Sets the [x,y] distance in user space between kernel values
     * If set to zero then device space will be used.
     */
    public void setKernelUnitLength(double [] kernelUnitLength) {
        touch();
        if (kernelUnitLength == null) {
            this.kernelUnitLength = null;
            return;
        }

        if (this.kernelUnitLength == null)
            this.kernelUnitLength = new float[2];

        this.kernelUnitLength[0] = (float)kernelUnitLength[0];
        this.kernelUnitLength[1] = (float)kernelUnitLength[1];
    }

    /**
     * Returns false if the convolution should affect the Alpha channel
     */
    public boolean getPreserveAlpha() {
        return preserveAlpha;
    }

    /**
     * Sets Alpha channel handling.
     * A value of False indicates that the convolution should apply to
     * the Alpha Channel
     */
    public void setPreserveAlpha(boolean preserveAlpha) {
        touch();
        this.preserveAlpha = preserveAlpha;
    }


    public void fixAlpha(BufferedImage bi) {
        if ((!bi.getColorModel().hasAlpha()) ||
            (!bi.isAlphaPremultiplied()))
            // No need to fix alpha if it isn't premultiplied...
            return;
        if (GraphicsUtil.is_INT_PACK_Data(bi.getSampleModel(), true)) 
            fixAlpha_INT_PACK(bi.getRaster());
        else
            fixAlpha_FALLBACK(bi.getRaster());
    }

    public void fixAlpha_INT_PACK(WritableRaster wr) {
        SinglePixelPackedSampleModel sppsm;
        sppsm = (SinglePixelPackedSampleModel)wr.getSampleModel();

        final int width = wr.getWidth();

        final int scanStride = sppsm.getScanlineStride();
        DataBufferInt db = (DataBufferInt)wr.getDataBuffer();
        final int base
            = (db.getOffset() +
               sppsm.getOffset(wr.getMinX()-wr.getSampleModelTranslateX(),
                               wr.getMinY()-wr.getSampleModelTranslateY()));
        int pixel, a, v;
        // Access the pixel data array
        final int pixels[] = db.getBankData()[0];
        for (int y=0; y<wr.getHeight(); y++) {
            int sp = base + y*scanStride;
            final int end = sp + width;
            while (sp < end) {
                pixel = pixels[sp];
                a = pixel>>>24;
                v = (pixel>>16)&0xFF;
                if (a < v) a = v;
                v = (pixel>> 8)&0xFF;
                if (a < v) a = v;
                v = (pixel    )&0xFF;
                if (a < v) a = v;
                pixels[sp] = (pixel&0x00FFFFFF) | (a << 24);
                sp++;
            }
        }
    }

    public void fixAlpha_FALLBACK(WritableRaster wr) {
        int x0=wr.getMinX();
        int w =wr.getWidth();
        int y0=wr.getMinY();
        int y1=y0 + wr.getHeight()-1;
        int bands = wr.getNumBands();
        int a, x, y, b, i;
        int [] pixel = null;
        for (y=y0; y<=y1; y++) {
            pixel = wr.getPixels(x0, y, w, 1, pixel);
            i=0;
            for (x=0; x<w; x++) {
                a=pixel[i];
                for (b=1; b<bands; b++)
                    if (pixel[i+b] > a) a = pixel[i+b];
                pixel[i+bands-1] = a;
                i+=bands;
            }
            wr.setPixels(x0, y, w, 1, pixel);
        }
    }

    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.  This
        // represents the complete scaling value from user to an
        // intermediate space that is scaled similarly to device
        // space.
        double scaleX = Math.sqrt(sx*sx + shy*shy);
        double scaleY = Math.sqrt(sy*sy + shx*shx);

        // These values represent the scale factor to the intermediate
        // coordinate system where we will apply our convolution.
        if (kernelUnitLength != null) {
            if (kernelUnitLength[0] > 0.0)
                scaleX = 1/kernelUnitLength[0];

            if (kernelUnitLength[1] > 0.0)
                scaleY = 1/kernelUnitLength[1];
        }

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

        Rectangle2D r = aoi.getBounds2D();

        int kw = kernel.getWidth();
        int kh = kernel.getHeight();
        int kx = target.x;
        int ky = target.y;

        // Grow the region in usr space.
        {
            double rx0 = r.getX() -(kx/scaleX);
            double ry0 = r.getY() -(ky/scaleY);
            double rx1 = rx0 + r.getWidth()  + (kw-1)/scaleX;
            double ry1 = ry0 + r.getHeight() + (kh-1)/scaleY;
            r = new Rectangle2D.Double(Math.floor(rx0),
                                       Math.floor(ry0),
                                       Math.ceil (rx1-Math.floor(rx0)),
                                       Math.ceil (ry1-Math.floor(ry0)));
        }
        // This will be the affine transform between our usr space and
        // an intermediate space which is scaled according to
        // kernelUnitLength and is axially aligned with our user
        // space.
        AffineTransform srcAt
            = AffineTransform.getScaleInstance(scaleX, scaleY);

        // This is the affine transform between our intermediate
        // coordinate space (where the convolution takes place) and
        // the real device space, or null (if we don't need an
        // intermediate space).

        // The shear/rotation simply divides out the
        // common scale factor in the matrix.
        AffineTransform resAt = new AffineTransform(sx/scaleX, shy/scaleX,
                                                    shx/scaleY, sy/scaleY,
                                                    tx, ty);

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

        // org.apache.batik.test.gvt.ImageDisplay.printImage
        //     ("Padded Image", ri,
        //      new Rectangle(ri.getMinX()+22,ri.getMinY()+38,5,5));

        CachableRed cr = convertSourceCS(ri);

        Shape devShape = srcAt.createTransformedShape(aoi);
        Rectangle2D devRect = devShape.getBounds2D();
        r = devRect;
        r = new Rectangle2D.Double(Math.floor(r.getX()-kx),
                                   Math.floor(r.getY()-ky),
                                   Math.ceil (r.getX()+r.getWidth())-
                                   Math.floor(r.getX())+(kw-1),
                                   Math.ceil (r.getY()+r.getHeight())-
                                   Math.floor(r.getY())+(kh-1));

        if (!r.getBounds().equals(cr.getBounds())) {
            if (edgeMode == PadMode.WRAP)
                throw new IllegalArgumentException
                    ("edgeMode=\"wrap\" is not supported by ConvolveMatrix.");
            cr = new PadRed(cr, r.getBounds(), edgeMode, rh);
        }

        // org.apache.batik.test.gvt.ImageDisplay.printImage
        //     ("Padded Image", cr,
        //      new Rectangle(cr.getMinX()+23,cr.getMinY()+39,5,5));

        if (bias != 0.0)
            throw new IllegalArgumentException
                ("Only bias equal to zero is supported in ConvolveMatrix.");

        BufferedImageOp op = new ConvolveOp(kernel,
                                            ConvolveOp.EDGE_NO_OP,
                                            rh);

        ColorModel cm = cr.getColorModel();

        // OK this is a bit of a cheat. We Pull the DataBuffer out of
        // The read-only raster that getData gives us. And use it to
        // build a WritableRaster.  This avoids a copy of the data.
        Raster rr = cr.getData();
        WritableRaster wr = GraphicsUtil.makeRasterWritable(rr, 0, 0);

        // Here we update the translate to account for the phase shift
        // (if any) introduced by setting targetX, targetY in SVG.
        int phaseShiftX = target.x - kernel.getXOrigin();
        int phaseShiftY = target.y - kernel.getYOrigin();
        int destX = (int)(r.getX() + phaseShiftX);
        int destY = (int)(r.getY() + phaseShiftY);

        BufferedImage destBI;
        if (!preserveAlpha) {
            // Force the data to be premultiplied since often the JDK
            // code doesn't properly premultiply the values...
            cm = GraphicsUtil.coerceData(wr, cm, true);

            BufferedImage srcBI;
            srcBI = new BufferedImage(cm, wr, cm.isAlphaPremultiplied(), null);

            // Easy case just apply the op...
            destBI = op.filter(srcBI, null);

            if (kernelHasNegValues) {
                // When the kernel has negative values it's possible
                // for the resultant image to have alpha values less
                // than the associated color values this will lead to
                // problems later when we try to display the image so
                // we fix this here.
                fixAlpha(destBI);
            }

        } else {
            BufferedImage srcBI;
            srcBI = new BufferedImage(cm, wr, cm.isAlphaPremultiplied(), null);

            // Construct a linear sRGB cm without alpha...
            cm = new DirectColorModel(ColorSpace.getInstance
                                      (ColorSpace.CS_LINEAR_RGB), 24,
                                      0x00FF0000, 0x0000FF00,
                                      0x000000FF, 0x0, false,
                                      DataBuffer.TYPE_INT);



            // Create an image with that color model
            BufferedImage tmpSrcBI = new BufferedImage
                (cm, cm.createCompatibleWritableRaster(wr.getWidth(),
                                                       wr.getHeight()),
                 cm.isAlphaPremultiplied(), null);

            // Copy the color data (no alpha) to that image
            // (dividing out alpha if needed).
            GraphicsUtil.copyData(srcBI, tmpSrcBI);

            // org.apache.batik.test.gvt.ImageDisplay.showImage
            //   ("tmpSrcBI: ", tmpSrcBI);

            // Get a linear sRGB Premult ColorModel
            ColorModel dstCM = GraphicsUtil.Linear_sRGB_Unpre;
            // Construct out output image around that ColorModel
            destBI = new BufferedImage
                (dstCM, dstCM.createCompatibleWritableRaster(wr.getWidth(),
                                                             wr.getHeight()),
                 dstCM.isAlphaPremultiplied(), null);

            // Construct another image on the same data buffer but without
            // an alpha channel.

            // Create the Raster (note we are using 'cm' again).
            WritableRaster dstWR =
                Raster.createWritableRaster
                (cm.createCompatibleSampleModel(wr.getWidth(), wr.getHeight()),
                 destBI.getRaster().getDataBuffer(),
                 new Point(0,0));

            // Create the BufferedImage.
            BufferedImage tmpDstBI = new BufferedImage
                (cm, dstWR, cm.isAlphaPremultiplied(), null);

            // Filter between the two image without alpha.
            tmpDstBI = op.filter(tmpSrcBI, tmpDstBI);

            // org.apache.batik.test.gvt.ImageDisplay.showImage
            //   ("tmpDstBI: ", tmpDstBI);

            // Copy the alpha channel into the result (note the color
            // channels are still unpremult.
            Rectangle srcRect = wr.getBounds();
            Rectangle dstRect = new Rectangle(srcRect.x-phaseShiftX,
                                              srcRect.y-phaseShiftY,
                                              srcRect.width, srcRect.height);
            GraphicsUtil.copyBand(wr, srcRect, wr.getNumBands()-1,
                                  destBI.getRaster(), dstRect,
                                  destBI.getRaster().getNumBands()-1);
        }

        // Wrap it as a CachableRed
        cr = new BufferedImageCachableRed(destBI, destX, destY);

        // org.apache.batik.test.gvt.ImageDisplay.printImage
        //     ("Cropped Image", cr,
        //      new Rectangle(cr.getMinX()+22,cr.getMinY()+38,5,5));
        // org.apache.batik.test.gvt.ImageDisplay.printImage
        //     ("Cropped sRGB", GraphicsUtil.convertTosRGB(cr),
        //      new Rectangle(cr.getMinX()+22,cr.getMinY()+38,5,5));

        // Make sure to crop junk from edges.
        cr = new PadRed(cr, devRect.getBounds(), PadMode.ZERO_PAD, rh);

        // If we need to scale/rotate/translate the result do so now...
        if (!resAt.isIdentity())
            cr = new AffineRed(cr, resAt, null);

        // return the result.
        return cr;
    }

}
