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

package com.lightcrafts.model.ImageEditor;

import com.lightcrafts.jai.JAIContext;
import com.lightcrafts.jai.LCROIShape;
import com.lightcrafts.jai.operator.LCMSColorConvertDescriptor;
import com.lightcrafts.jai.opimage.BlendOpImage;
import com.lightcrafts.jai.opimage.RGBColorSelectionMaskOpImage;
import com.lightcrafts.jai.utils.Functions;
import com.lightcrafts.jai.utils.Transform;
import com.lightcrafts.mediax.jai.*;
import com.lightcrafts.model.*;
import com.lightcrafts.utils.ColorScience;
import com.lightcrafts.utils.LCMS_ColorSpace;
import com.lightcrafts.utils.LCMS;

import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.Raster;
import java.awt.image.renderable.ParameterBlock;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;

public abstract class BlendedOperation extends GenericOperationImpl implements Cloneable {
    private double opacity = 1.0;
    private String blendingMode = "Normal";

    private LCROIShape mask = null;

    private PlanarImage colorSelectionMask = null;
    private RGBColorSelection lastColorSelection = null;

    protected boolean colorInputOnly = false;

    protected AffineTransform lastTransform = null;

    public BlendedOperation clone(Rendering rendering) {
        try {
            BlendedOperation object = (BlendedOperation) this.clone();
            object.rendering = rendering;
            // object.mask = null;
            object.operation = null;
            object.lastTransform = null;
            return object;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

    private boolean validRegion(Region region) {
        return region != null
               && region.getOuterShape().getBounds().height > 0
               && region.getOuterShape().getBounds().width > 0;
    }

    public boolean hasMask() {
        return mask != null;
    }

    public void setRegion(Region region) {
        if (validRegion(region)) {
            mask = new LCROIShape(region, rendering.getInputTransform());
        } else if (region == null || region.getContours().size() == 0)
            mask = null;
        lastTransform = rendering.getInputTransform();
        super.setRegion(region);
    }

    public void setRegionInverted(boolean inverted) {
        super.setRegionInverted(inverted);
    }

    // Define the List of LayerModes statically so they can be vended
    // by ImageEditorEngine:
    static List<LayerModeImpl> blendingModes;

    static {
        blendingModes = new ArrayList<LayerModeImpl>();

        for (BlendOpImage.BlendingMode b : BlendOpImage.BlendingMode.values())
            blendingModes.add(new LayerModeImpl(b.getName()));
    }

    BlendedOperation(Rendering rendering, OperationType type) {
        super(rendering, type);
    }

    // Support for color picker in color based selections

    private Point2D clickPoint = null;
    private Color selectedColor = null;

    static float arctan2(float y, float x) {
        final float coeff_1 = (float) Math.PI / 4;
        final float coeff_2 = 3 * coeff_1;
        final float abs_y = Math.abs(y) + 1e-10f;      // kludge to prevent 0/0 condition
        float angle;

        if (x >= 0) {
            float r = (x - abs_y) / (x + abs_y);
            angle = coeff_1 - coeff_1 * r;
        } else {
            float r = (x + abs_y) / (abs_y - x);
            angle = coeff_2 - coeff_1 * r;
        }

        return y < 0 ? -angle : angle;
    }

    public static float hue(float r, float g, float b) {
        float x = r - (g+b) / 2;
        float y = ((g-b) * (float) Math.sqrt(3) / 2);
        return arctan2(y, x) + (float) Math.PI;
    }

    public void setColorSelection(RGBColorSelection selection) {
        super.setColorSelection(selection);
    }

    public PlanarImage getColorSelectionMask() {
        return colorSelectionMask;
    }

    public RGBColorSelection getColorSelectionAt(Point2D p) {
        System.out.println("setColorSelection(): " + p);
        this.clickPoint = p;
        settingsChanged();

        if (selectedColor != null) {
            float r = selectedColor.getRed() / (float) 0xff;
            float g = selectedColor.getGreen() / (float) 0xff;
            float b = selectedColor.getBlue() / (float) 0xff;

            selectedColor = null;

            float feather = 0.1f;

            float luminosity = (float) (Math.log1p(0xff * ColorScience.Wr * r +
                                                   0xff * ColorScience.Wg * g +
                                                   0xff * ColorScience.Wb * b) / (8 * Math.log(2)));

            float minLuminosity = Math.max(luminosity-feather, 0);
            float minLuminosityFeather = Math.min(minLuminosity, feather);

            float maxLuminosity = Math.min(luminosity+feather, 1);
            float maxLuminosityFeather = Math.min(1-maxLuminosity, feather);

            return new RGBColorSelection(r, g, b, 0.4f,
                                         minLuminosity, minLuminosityFeather,
                                         maxLuminosity, maxLuminosityFeather,
                                         false, true, true);
        }
        return new RGBColorSelection();
    }

    abstract class BlendedTransform extends Transform {
        PlanarImage back;
        SoftReference<PlanarImage> softFront = new SoftReference<PlanarImage>(null);
        SoftReference<PlanarImage> softBlender = new SoftReference<PlanarImage>(null);
        SoftReference<PlanarImage> softResult = new SoftReference<PlanarImage>(null);

        int[] pointToPixel(Point2D p) {
            if (p != null) {
                Point2D pp = rendering.getTransform().transform(p, null);

                int x = (int) pp.getX();
                int y = (int) pp.getY();

                if (rendering.getScaleFactor() > 1) {
                    x /= rendering.getScaleFactor();
                    y /= rendering.getScaleFactor();
                }

                if (back.getBounds().contains(x, y)) {
                    int tx = back.XToTileX(x);
                    int ty = back.YToTileY(y);

                    Raster tile = back.getTile(tx, ty);

                    int[] pixel = null;

                    int averagePixels = 3;

                    if (averagePixels > 1) {
                        Rectangle tileBounds = tile.getBounds();
                        Rectangle sampleRect = new Rectangle(x - averagePixels / 2,
                                                             y - averagePixels / 2,
                                                             averagePixels,
                                                             averagePixels);

                        Rectangle intersection = tileBounds.intersection(sampleRect);

                        pixel = new int[]{0, 0, 0};
                        int currentPixel[] = new int[3];

                        for (int i = intersection.x; i < intersection.x + intersection.width; i++)
                            for (int j = intersection.y; j < intersection.y + intersection.height; j++) {
                                currentPixel = tile.getPixel(i, j, currentPixel);
                                for (int k = 0; k < 3; k++)
                                    pixel[k] = (pixel[k] + currentPixel[k]) / 2;
                            }
                    } else
                        pixel = tile.getPixel(x, y, pixel);

                    return pixel;
                }
            }
            return null;
        }

        abstract public PlanarImage setFront();

        public void dispose() {
            back.removeSinks();
            back.dispose();
            back = null;
            mask = null;
            cachedImage = null;
        }

        private RenderedOp createBlender(PlanarImage front) {
            // Overlay result on the original image
            ParameterBlock pb = new ParameterBlock();
            pb.addSource(front);
            pb.addSource(back);
            pb.add(blendingMode);
            pb.add(new Double(invertedRegion ? -opacity : opacity));
            pb.add(mask);
            pb.add(colorSelectionMask);

            // we don't know what front might generate, specify the output format to be the same as the input
            RenderingHints formatHints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, Functions.getImageLayout(back));
            RenderedOp blender = JAI.create("Blend", pb, formatHints);
            // blender.setProperty(JAIContext.PERSISTENT_CACHE_TAG, Boolean.TRUE);
            return blender;
        }

        BlendedTransform(PlanarImage source) {
            back = source;
        }

        public void setSource(Object source) {
            if (source != back) {
                back.removeSinks();
                back.dispose();
                back = (PlanarImage) source;
                changed = true;
            }
        }

        private CachedImage cachedImage = null;

        public PlanarImage render() {
            /*
                we have to update before any render, this is necessary otherwise
                we would not propagate changes from one resolution to the other

                NB: make sure that operations cache their state efficiently
            */
            if (colorInputOnly && back.getColorModel().getNumComponents() != 3)
                return back;

            PlanarImage newRendering = update();

            if (cachedImage == null || newRendering != cachedImage.getRendering())
                cachedImage = new CachedImage(newRendering, scale);

            return cachedImage;
        }

        public class CachedImage extends PlanarImage {
            private final TileCache cache = JAIContext.fileCache;
            private SoftReference<PlanarImage> softRendering = new SoftReference<PlanarImage>(null);
            final float scale;

            public CachedImage(PlanarImage rendering, float scale) {
                super(new ImageLayout(rendering), null, null);
                setProperty(JAIContext.PERSISTENT_CACHE_TAG, Boolean.TRUE);
                softRendering = new SoftReference<PlanarImage>(rendering);
                this.scale = scale;
                // System.out.println("new CachedImage " + BlendedOperation.this.getClass());
            }

            public PlanarImage getRendering() {
                return softRendering != null ? softRendering.get() : null;
            }

            public Raster getTile(int tileX, int tileY) {
                Raster tile = cache.getTile(this, tileX, tileY);

                if (tile == null) {
                    PlanarImage rendering = softRendering.get();

                    if (rendering == null) {
                        System.out.println("rendering null..." + BlendedOperation.this.getClass());
                        rendering = update();
                        softRendering = new SoftReference<PlanarImage>(rendering);
                    }

                    tile = rendering.getTile(tileX, tileY);
                    cache.add(this, tileX, tileY, tile);
                }

                return tile;
            }
        }

        /*
            Note: ever change sources or parameters in JAI pipelines, it is the slowest thing of all,
            just rebuild everything from scratch
        */

        public PlanarImage update() {
            if (clickPoint != null) {
                int pixel[] = pointToPixel(clickPoint);
                if (pixel != null) {
                    int r = pixel[0] / 256;
                    int g = pixel[1] / 256;
                    int b = pixel[2] / 256;

                    selectedColor = new Color(r, g, b);
                }
                clickPoint = null;
            }

            if (!colorInputOnly || back.getColorModel().getNumComponents() == 3) {
                boolean newFront = false;

                PlanarImage front = softFront.get();

                if (front == null || changed) {
                    front = setFront();
                    softFront = new SoftReference<PlanarImage>(front);
                    newFront = true;
                }

                PlanarImage result = softResult.get();

                if (newFront || result == null) {
                    RGBColorSelection colorSelection = getColorSelection();

                    if (opacity != 1 || blendingMode != "Normal" || validRegion(getRegion())
                        || (colorSelection != null && !colorSelection.isAllSelected())) {
                        RenderedOp blender = (RenderedOp) softBlender.get();

                        if (validRegion(getRegion())
                            && (!rendering.getInputTransform().equals(lastTransform)
                                || (blender != null && blender.getParameters().get(2) != mask))) {
                            mask = new LCROIShape(getRegion(), rendering.getInputTransform());
                            blender = null;
                        } else if (getRegion() == null)
                            mask = null;

                        if (colorSelection != null && !colorSelection.isAllSelected()
                            && (!colorSelection.equals(lastColorSelection)
                                || !rendering.getInputTransform().equals(lastTransform)
                                || (blender != null && blender.getParameters().get(3) != colorSelectionMask))) {

                            PlanarImage labImage = Functions.toColorSpace(back, new LCMS_ColorSpace(new LCMS.LABProfile()),
                                                              LCMSColorConvertDescriptor.RELATIVE_COLORIMETRIC, null);
                            ParameterBlock pb = new ParameterBlock();
                            pb.addSource(labImage);
                            pb.add(new int[]{1, 2});
                            RenderedOp abImage = JAI.create("bandselect", pb, null);

                            pb = new ParameterBlock();
                            pb.addSource(back);
                            pb.add(new double[][]{{ColorScience.Wr, ColorScience.Wg, ColorScience.Wb, 0}});
                            PlanarImage monochrome = JAI.create("BandCombine", pb, null);

                            RenderingHints layoutHints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, Functions.getImageLayout(labImage));
                            // layoutHints.add(JAIContext.noCacheHint);
                            pb = new ParameterBlock();
                            pb.addSource(monochrome);
                            pb.addSource(abImage);
                            PlanarImage maskImage = JAI.create("BandMerge", pb, layoutHints);

                            colorSelectionMask = new RGBColorSelectionMaskOpImage(maskImage, getColorSelection(), null);

                            ParameterBlock maskPB;

//                            KernelJAI morph = new KernelJAI(3, 3, new float[]{1, 1, 1, 1, 1, 1, 1, 1, 1});
//
//                            maskPB = new ParameterBlock();
//                            maskPB.addSource(colorSelectionMask);
//                            maskPB.add(morph);
//                            colorSelectionMask = JAI.create("Erode", maskPB, null);
//
//                            maskPB = new ParameterBlock();
//                            maskPB.addSource(colorSelectionMask);
//                            maskPB.add(morph);
//                            colorSelectionMask = JAI.create("Dilate", maskPB, null);

                            RenderingHints extenderHints = new RenderingHints(JAI.KEY_BORDER_EXTENDER,
                                                                              BorderExtender.createInstance(BorderExtender.BORDER_COPY));
                            KernelJAI kernel = Functions.getGaussKernel(0.5 * scale);
                            maskPB = new ParameterBlock();
                            maskPB.addSource(colorSelectionMask);
                            maskPB.add(kernel);
                            colorSelectionMask = JAI.create("Convolve", maskPB, extenderHints);

                            lastColorSelection = colorSelection;
                        } else if (colorSelection == null || colorSelection.isAllSelected())
                            colorSelectionMask = null;

                        lastTransform = rendering.getInputTransform();

                        softResult = softBlender = new SoftReference<PlanarImage>(blender = createBlender(front));
                        return blender;
                    } else {
                        softResult = softFront;
                        return front;
                    }
                } else
                    return result;
            } else
                return back;
        }
    }

    abstract protected BlendedTransform createBlendedOp(PlanarImage source);

    protected Transform createOp(PlanarImage source) {
        return createBlendedOp(source);
    }

    public void setLayerConfig(LayerConfig layer) {
        if (blendingMode != layer.getMode().getName() || opacity != layer.getOpacity()) {
            blendingMode = layer.getMode().getName();
            opacity = layer.getOpacity();
            settingsChanged();
        }
    }

    public LayerConfig getDefaultLayerConfig() {
        return new LayerConfig(new LayerModeImpl("Normal"), 1.);
    }
}
