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

package com.lightcrafts.model.ImageEditor;

import com.lightcrafts.jai.utils.Functions;
import com.lightcrafts.jai.utils.Transform;
import com.lightcrafts.jai.JAIContext;
import com.lightcrafts.model.LayerConfig;
import com.lightcrafts.model.OperationType;
import com.lightcrafts.model.SliderConfig;
import com.lightcrafts.utils.ColorScience;

import com.lightcrafts.mediax.jai.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.DataBuffer;
import java.awt.image.renderable.ParameterBlock;
import java.text.DecimalFormat;

/**
 * Created by IntelliJ IDEA.
 * User: fabio
 * Date: Mar 30, 2005
 * Time: 9:55:56 AM
 * To change this template use File | Settings | File Templates.
 */
public class ContrastMaskOperation extends BlendedOperation {
    private double radius = 128;
    private double gamma = 2.2;
    private KernelJAI kernel = null;
    private short[] tableDataUShort = new short[0x10000];
    private byte[] tableDataByte = new byte[0x100];
    private LookupTableJAI byteLut = null;
    private LookupTableJAI ushortLut = null;

    public ContrastMaskOperation(Rendering rendering) {
        super(rendering, type);

        DecimalFormat format = new DecimalFormat("0.0");

        addSliderKey("Radius");
        setSliderConfig("Radius", new SliderConfig(0.1, 500, radius, .1, true, format));
        addSliderKey("Gamma");
        setSliderConfig("Gamma", new SliderConfig(0.2, 5, gamma, .1, false, format));
    }

    public boolean neutralDefault() {
        return false;
    }

    static final OperationType type = new OperationTypeImpl("Contrast Mask");

    public void setSliderValue(String key, double value) {
        value = roundValue(key, value);
        
        if (key == "Radius" && radius != value) {
            radius = value;
            kernel = null;
        } else if (key == "Gamma" && gamma != value) {
            gamma = value;
            byteLut = null;
            ushortLut = null;
        } else
            return;

        super.setSliderValue(key, value);
    }

    private LookupTableJAI computeGammaTable(int dataType) {
        if (dataType == DataBuffer.TYPE_BYTE) {
            if (byteLut != null)
                return byteLut;
            for (int i = 0; i < tableDataByte.length; i++) {
                tableDataByte[i] = (byte) (0xFF * Math.pow(i / (double) 0xFF, gamma) + 0.5);
            }
            return byteLut = new LookupTableJAI(tableDataByte);
        } else {
            if (ushortLut != null)
                return ushortLut;
            for (int i = 0; i < tableDataUShort.length; i++) {
                tableDataUShort[i] = (short) (0xFFFF * Math.pow(i / (double) 0xFFFF, gamma) + 0.5);
            }
            return ushortLut = new LookupTableJAI(tableDataUShort, true);
        }
    }

    private class ContrastMask extends BlendedTransform {
        RenderedOp gammaCurve;

        ContrastMask(PlanarImage source) {
            super(source);
        }

        public PlanarImage setFront() {
            double[][] transform = {
                { ColorScience.Wr, ColorScience.Wg, ColorScience.Wb, 0 }
            };

            // Calculate a blurred desautuated inverted version of the source as a mask
            double newRadius = radius * scale;
            double rescale = 1;
            int divideByTwo = 1;

            while (newRadius > 7) {
                newRadius /= 2;
                rescale /= 2;
                divideByTwo *= 2;
            }

            RenderingHints hints = new RenderingHints(JAI.KEY_BORDER_EXTENDER,
                                                      BorderExtender.createInstance(BorderExtender.BORDER_COPY));
            Interpolation interp = Interpolation.getInstance(Interpolation.INTERP_BILINEAR);

            PlanarImage scaleDown;
            if (rescale != 1) {
                float scaleX = (float) Math.floor(rescale * back.getWidth()) / (float) back.getWidth();
                float scaleY = (float) Math.floor(rescale * back.getHeight()) / (float) back.getHeight();

                ParameterBlock pb = new ParameterBlock();
                pb.addSource(back);
                pb.add(AffineTransform.getScaleInstance(scaleX, scaleY));
                pb.add(interp);
                RenderingHints layoutHints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT,
                                                            new ImageLayout(0, 0,
                                                                            Math.max(JAIContext.TILE_WIDTH/divideByTwo, 8),
                                                                            Math.max(JAIContext.TILE_HEIGHT/divideByTwo, 8),
                                                                            null, null));
                layoutHints.add(hints);
                layoutHints.add(JAIContext.noCacheHint);
                scaleDown = JAI.create("Affine", pb, layoutHints);
            } else
                scaleDown = back;

            if (scaleDown.getColorModel().getNumComponents() == 3) {
                ParameterBlock pb = new ParameterBlock();
                pb.addSource(scaleDown);
                pb.add(transform);
                scaleDown = JAI.create("BandCombine", pb, JAIContext.noCacheHint);  // Desaturate, single banded
            }

            scaleDown = JAI.create("Not", scaleDown, JAIContext.noCacheHint);       // Invert
            LookupTableJAI table = computeGammaTable(scaleDown.getSampleModel().getDataType());
            ParameterBlock pb = new ParameterBlock();
            pb.addSource(scaleDown);
            pb.add(table);
            // we cache this since convolution scans its input multiple times
            gammaCurve = JAI.create("lookup", pb, null /*JAIContext.noCacheHint*/);

            kernel = Functions.getGaussKernel(newRadius);
            pb = new ParameterBlock();
            pb.addSource(gammaCurve);
            pb.add(kernel);
            // RenderingHints convolveHints = new RenderingHints(hints);
            // convolveHints.add(JAIContext.noCacheHint);
            RenderedOp blur = JAI.create("LCSeparableConvolve", pb, hints);    // Gaussian Blur

            if (rescale != 1) {
                pb = new ParameterBlock();
                pb.addSource(blur);
                pb.add(AffineTransform.getScaleInstance(back.getWidth() / (double) scaleDown.getWidth(),
                                                        back.getHeight() / (double) scaleDown.getHeight()));
                pb.add(interp);
                RenderingHints resultLayoutHints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT,
                                                                  new ImageLayout(0, 0,
                                                                                  JAIContext.TILE_WIDTH,
                                                                                  JAIContext.TILE_HEIGHT,
                                                                                  null, null));
                resultLayoutHints.add(hints);
                resultLayoutHints.add(JAIContext.noCacheHint);
                return JAI.create("Affine", pb, resultLayoutHints);
            } else
                return blur;
        }
    }

    protected void updateOp(Transform op) {
        op.update();
    }

    protected BlendedTransform createBlendedOp(PlanarImage source) {
        return new ContrastMaskOperation.ContrastMask(source);
    }

    public OperationType getType() {
        return type;
    }

    public LayerConfig getDefaultLayerConfig() {
        return new LayerConfig(new LayerModeImpl("Soft Light"), .5);
    }
}
