/* Copyright (C) 2005-2011 Fabio Riccardi */

package com.lightcrafts.jai.opimage;

import com.lightcrafts.model.Region;
import com.lightcrafts.model.Contour;
import com.lightcrafts.jai.JAIContext;
import com.lightcrafts.jai.LCROIShape;
import com.lightcrafts.jai.utils.Functions;
import com.lightcrafts.utils.SoftValueHashMap;

import com.lightcrafts.mediax.jai.*;
import java.awt.image.*;
import java.awt.image.renderable.ParameterBlock;
import java.awt.color.ColorSpace;
import java.awt.*;
import java.awt.geom.*;
import java.util.*;

/**
 * Created by IntelliJ IDEA.
 * User: fabio
 * Date: May 24, 2005
 * Time: 4:20:31 PM
 * To change this template use File | Settings | File Templates.
 */
public class ShapedMask extends PlanarImage {
    Region region;
    LCROIShape shape;

    public static Rectangle getOuterBounds(Region region, AffineTransform transform) {
        Rectangle outerBounds = null;
        Iterator it = region.getContours().iterator();
        while (it.hasNext()) {
            Contour c = (Contour) it.next();

            AffineTransform combined = transform;
            if (c.getTranslation() != null) {
                combined = AffineTransform.getTranslateInstance(c.getTranslation().getX(), c.getTranslation().getY());
                combined.preConcatenate(transform);
            }

            Rectangle cBounds = new Rectangle(c.getOuterShape().getBounds());
            cBounds = combined.createTransformedShape(cBounds).getBounds();

            if (outerBounds == null)
                outerBounds = cBounds;
            else
                outerBounds = outerBounds.union(cBounds);
        }

        return outerBounds;
    }

    static ImageLayout createLayout(Region region, AffineTransform transform) {
        Rectangle regionBounds = getOuterBounds(region, transform);

        SampleModel graySm = RasterFactory.createPixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, regionBounds.width, regionBounds.height, 1);
        ColorModel grayCm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);

        ImageLayout layout = new ImageLayout(regionBounds.x, regionBounds.y, regionBounds.width, regionBounds.height,
                                             regionBounds.x, regionBounds.y, JAIContext.TILE_WIDTH, JAIContext.TILE_HEIGHT,
                                             graySm, grayCm);

        return layout;
    }

    static ImageLayout createLayout(Rectangle regionBounds) {
        SampleModel graySm = RasterFactory.createPixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, regionBounds.width, regionBounds.height, 1);
        ColorModel grayCm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);

        ImageLayout layout = new ImageLayout(regionBounds.x, regionBounds.y, regionBounds.width, regionBounds.height,
                                             regionBounds.x, regionBounds.y, JAIContext.TILE_WIDTH, JAIContext.TILE_HEIGHT,
                                             graySm, grayCm);

        return layout;
    }

    public ShapedMask(Region region, LCROIShape shape) {
        super(createLayout(region, shape.getTransform()), null, null);

        this.region = region;
        this.shape = shape;
    }

    static private Shape[] createBlurs(Shape shape, int width) {
        java.util.List blurs = new LinkedList();
        do {
            Stroke stroke = new BasicStroke(2 * width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
            Shape blur = stroke.createStrokedShape(shape);
            blurs.add(blur);
            width /= 2;
        } while (width > 0);

        int size = blurs.size();
        Shape[] result = new Shape[size];
        Iterator it = blurs.iterator();
        int i = 0;
        while (it.hasNext())
            result[size - 1 - i++] = (Shape) it.next();
        return result;
    }

    private static Map bitmaps = Collections.synchronizedMap(new WeakHashMap());

    private static BorderExtender extender = BorderExtender.createInstance(BorderExtender.BORDER_COPY);

    private static class ScaledImage {
        float scale = 1;
        float tx = 0, ty = 0;
        PlanarImage image = null;
    }


    private static void drawBlurs(Graphics2D g2d, Shape shape, float contourWidth) {
        g2d.setColor(Color.white);
        g2d.fill(shape);

        // Draw the blurs in shades of gray:
        if (contourWidth > 1) {
            int width = (int) Math.round(contourWidth);
            Shape[] blurs = createBlurs(shape, width);

            int count = blurs.length;
            Area shapeArea = new Area(shape);
            for (int n = count - 1; n >= 0; n--) {
                Shape blur = blurs[n];
                Area semiBlur = new Area(blur);
                semiBlur.intersect(shapeArea);
                float value = n / (float) (count + 1);
                Color color = new Color(value, value, value);
                g2d.setColor(color);
                g2d.fill(semiBlur);
            }
        }

    }

    private static ScaledImage createContourImage(Contour contour) {
        Shape shape = contour.getOuterShape();

        // limit the blur radius to 7 pixels, use intelligent rescaling to emulate larger blurs
        float contourWidth = contour.getWidth();
        float widthScale = 1;
        int divideByTwo = 1;
        while (contourWidth > 7 * 4) {
            contourWidth /= 2;
            widthScale /= 2;
            divideByTwo *= 2;
        }

        if (widthScale < 1)
            shape = AffineTransform.getScaleInstance(widthScale, widthScale).createTransformedShape(shape);

        // We create the supporting BufferedImage contourWidth larger than
        // the outer shape bounds to allow for proper tapering of the blur

        Rectangle bounds = shape.getBounds();
        bounds.grow((int) contourWidth, (int) contourWidth);

        RenderedImage supportImage = new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_BYTE_GRAY);

        Graphics2D g2d = ((BufferedImage) supportImage).createGraphics();

        g2d.setTransform(AffineTransform.getTranslateInstance(-bounds.x, -bounds.y));

        drawBlurs(g2d, shape, contourWidth);

        g2d.dispose();

        supportImage = new TiledImage(supportImage,
                                      Math.max(JAIContext.TILE_WIDTH / divideByTwo, 8),
                                      Math.max(JAIContext.TILE_HEIGHT / divideByTwo, 8));

        ScaledImage contourImage = new ScaledImage();

        if (contourWidth > 1) {
            KernelJAI kernel = Functions.getGaussKernel(contourWidth/4);
            ParameterBlock pb = new ParameterBlock();
            pb.addSource(supportImage);
            pb.add(kernel);
            RenderingHints hints = new RenderingHints(JAI.KEY_BORDER_EXTENDER, extender);

            contourImage.image = JAI.create("LCSeparableConvolve", pb, hints);
        } else
            contourImage.image = (PlanarImage) supportImage;

        contourImage.scale = widthScale;

        contourImage.tx = bounds.x;
        contourImage.ty = bounds.y;

        return contourImage;
    }

    private static synchronized ScaledImage getContourImage(Contour contour) {
        ScaledImage contourImage = (ScaledImage) bitmaps.get(contour);

        if (contourImage == null) {
            if ((contourImage = (ScaledImage) bitmaps.get(contour)) == null) {
                contourImage = createContourImage(contour);
                bitmaps.put(contour, contourImage);
            }
        }

        return contourImage;
    }

    static class RasterImage extends PlanarImage {
        private Raster raster;

        RasterImage(Raster r, ColorModel colorModel) {
            super(new ImageLayout(r.getMinX(),
                                  r.getMinY(),
                                  r.getWidth(),
                                  r.getHeight(),
                                  r.getMinX(),
                                  r.getMinY(),
                                  r.getWidth(),
                                  r.getHeight(),
                                  r.getSampleModel(),
                                  colorModel), null, null);
            raster = r;
        }

        public Raster getTile(int tileX, int tileY) {
            return raster;
        }
    }

    private static Map expandedMasks = new SoftValueHashMap();

    private static class AffinedImage {
        PlanarImage image;
        AffineTransform transform;

        AffinedImage(PlanarImage image, AffineTransform transform) {
            this.image = image;
            this.transform = transform;
        }

        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof AffinedImage)) return false;

            final AffinedImage affinedImage = (AffinedImage) o;

            if (image != null ? !image.equals(affinedImage.image) : affinedImage.image != null) return false;
            if (transform != null ? !transform.equals(affinedImage.transform) : affinedImage.transform != null) return false;

            return true;
        }

        public int hashCode() {
            int result;
            result = (image != null ? image.hashCode() : 0);
            result = 29 * result + (transform != null ? transform.hashCode() : 0);
            return result;
        }
    }

    public Raster getData(Rectangle rect) {
        Iterator it = region.getContours().iterator();

        // SampleModel sampleModel = RasterFactory.createPixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, rect.width, rect.height, 1);
        ColorModel colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY),
                                                        false, false,
                                                        Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
        SampleModel sampleModel = colorModel.createCompatibleSampleModel(rect.width, rect.height);

        TiledImage ti = new TiledImage(rect.x, rect.y, rect.width, rect.height,
                                       rect.x, rect.y, sampleModel, colorModel);

        WritableRaster result = (WritableRaster) ti.getData();

        boolean overlay = false;

        while (it.hasNext()) {
            Contour c = (Contour) it.next();
            AffineTransform combined = shape.getTransform();
            if (c.getTranslation() != null) {
                combined = AffineTransform.getTranslateInstance(c.getTranslation().getX(),
                                                                c.getTranslation().getY());
                combined.preConcatenate(shape.getTransform());
            }

            // Take the blur tapering into account
            Rectangle bounds = c.getOuterShape().getBounds();
            bounds.grow((int) c.getWidth(), (int) c.getWidth());

            if (combined.createTransformedShape(bounds).intersects(rect)) {
                ScaledImage scaledImage = getContourImage(c);
                PlanarImage maskImage = scaledImage.image;

                if (!combined.isIdentity() || scaledImage.scale < 1 || scaledImage.tx != 0 || scaledImage.ty != 0) {
                    AffineTransform transform = new AffineTransform(combined);

                    if (scaledImage.scale < 1) {
                        float scaleX = (float) Math.floor(maskImage.getWidth() / scaledImage.scale) / (float) maskImage.getWidth();
                        float scaleY = (float) Math.floor(maskImage.getHeight() / scaledImage.scale) / (float) maskImage.getHeight();

                        transform.concatenate(AffineTransform.getScaleInstance(scaleX, scaleY));
                    }

                    if (scaledImage.tx != 0 || scaledImage.ty != 0)
                        transform.concatenate(AffineTransform.getTranslateInstance(scaledImage.tx, scaledImage.ty));

                    Rectangle scaledBounds = transform.createTransformedShape(maskImage.getBounds()).getBounds();

                    // Avoid scaling underflows resulting into exeptions
                    if (scaledBounds.width < 3 || scaledBounds.height < 3)
                        continue;

                    synchronized (expandedMasks) {
                        AffinedImage key = new AffinedImage(maskImage, transform);
                        PlanarImage affinedImage = (PlanarImage) expandedMasks.get(key);
                        if (affinedImage == null) {
                            RenderingHints hints = new RenderingHints(JAI.KEY_BORDER_EXTENDER,
                                                                      BorderExtender.createInstance(BorderExtender.BORDER_COPY));
                            // hints.add(JAIContext.noCacheHint);
                            Interpolation interp = Interpolation.getInstance(Interpolation.INTERP_BILINEAR);
                            ParameterBlock params = new ParameterBlock();
                            params.addSource(maskImage);
                            params.add(transform);
                            params.add(interp);
                            maskImage = JAI.create("Affine", params, hints);
                            expandedMasks.put(key, maskImage);
                        } else {
                            maskImage = affinedImage;
                        }
                    }
                }

                if (maskImage.getBounds().intersects(rect)) {
                    Rectangle itx = maskImage.getBounds().intersection(rect);

                    byte resultData[];
                    if (!overlay) {
                        resultData = (byte[]) maskImage.getData(itx).getDataElements(itx.x, itx.y, itx.width, itx.height, null);
                        overlay = true;
                    } else {
                        resultData = (byte[]) result.getDataElements(itx.x, itx.y, itx.width, itx.height, null);
                        byte currentData[] = (byte[]) maskImage.getData(itx).getDataElements(itx.x, itx.y, itx.width, itx.height, null);

                        // blend overlapping regions using Porter-Duff alpha compositing: ar = a1 * (1 - a2) + a2
                        for (int i = 0; i < resultData.length; i++) {
                            int current = currentData[i] & 0xFF;
                            if (current != 0) {
                                int cumulative = resultData[i] & 0xFF;
                                if (cumulative != 0) {
                                    resultData[i] = (byte) ((cumulative * (0xff - current)) / 0x100 + current);
                                } else
                                    resultData[i] = (byte) current;
                            }
                        }
                    }

                    result.setDataElements(itx.x, itx.y, itx.width, itx.height, resultData);
                }
            }
        }

        return result;
    }

    public Raster getTile(int tileX, int tileY) {
        return getData(new Rectangle(tileXToX(tileX), tileYToY(tileY), getTileWidth(), getTileHeight()));
    }
}
