package com.srbenoit.microscopy;

import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import com.srbenoit.filter.AbstractFilter;
import com.srbenoit.filter.FilterException;
import com.srbenoit.filter.FilterInput;
import com.srbenoit.filter.FilterOutput;
import com.srbenoit.filter.FilterTreeExecutor;
import com.srbenoit.filter.Pipe;
import com.srbenoit.filter.items.ImageArrayPipeItem;

/**
 * A filter that applies a Gaussian smoothing kernel to every image in an image array.
 */
public class SmootherFilter extends AbstractFilter {

    /** version number for serialization */
    private static final long serialVersionUID = 9105886485480586654L;

    /** size of kernel in smoothing operation. */
    private static final int KERNEL_SIZE = 5;

    /**
     * Constructs a new <code>SmootherFilter</code>.
     */
    public SmootherFilter() {

        super("Gaussian Smoother", SmootherFilter.class.getName());

        this.inputs.add(new FilterInput(ImageArrayPipeItem.class, "Images to smooth"));
        this.outputs.add(new FilterOutput(ImageArrayPipeItem.class, "Smoothed imags",
                "smoothed_images"));
        makeRenderer();
    }

    /**
     * Duplicates the filter including all of its settings, but returns an independent object.
     *
     * @return  the duplicated object
     */
    @Override public AbstractFilter duplicate() {

        return new SmootherFilter();
    }

    /**
     * Performs the filter operation.
     *
     * @param   executor  the <code>FilterTreeExecutor</code> that is executing the filter
     * @param   pipe      a pipe containing the input data items
     * @throws  FilterException  if the filter cannot complete
     */
    @Override public void filter(final FilterTreeExecutor executor, final Pipe pipe)
        throws FilterException {

        ImageArrayPipeItem input;
        ImageArrayPipeItem output;

        validateInputs(pipe);
        executor.indicateProgress(1);

        input = (ImageArrayPipeItem) pipe.get(this.inputs.get(0).getKey());
        executor.indicateProgress(2);

        // First two pipe items are passed through to the output - the image
        // item must be re-created since the number of Z planes changes
        output = new ImageArrayPipeItem(this.outputs.get(0).getKey(),
                "Gaussian-smoothed images (PNG format)", pipe, input.getXLabel(),
                input.getYLabel(), input.getXSize(), input.getYSize(), "png");
        pipe.add(output);
        executor.indicateProgress(3);

        runFilter(executor, input, output);

        executor.indicateProgress(80);

        if (!executor.isCancelled()) {
            pipe.save(executor);
        }

        executor.indicateProgress(100);
    }

    /**
     * Runs the filter, reading the source Metamorph TIF files and extracting an array of images,
     * the first dimension of which is time, and the second dimension of which is z plane.
     *
     * @param  executor  the <code>FilterTreeExecutor</code> that is executing the filter
     * @param  pipe      a pipe containing the input data items
     * @param  input     the images to process
     * @param  output    the image array in which to store the processed images
     */
    private void runFilter(final FilterTreeExecutor executor, final ImageArrayPipeItem input,
        final ImageArrayPipeItem output) {

        ConvolveOp convOp;
        int width;
        int height;
        BufferedImage orig;
        BufferedImage fixed;
        BufferedImage dest;

        convOp = makeKernel();

        width = input.getXSize();
        height = input.getYSize();

        // Scan each image to get mean intensity
        for (int x = 0; x < width; x++) {

            for (int y = 0; y < height; y++) {

                if (executor.isCancelled()) {
                    break;
                }

                executor.indicateProgress(5 + (75 * ((x * height) + y) / (width * height)));

                orig = input.getImage(x, y);
                fixed = new BufferedImage(orig.getWidth(), orig.getHeight(), // NOPMD SRB
                        BufferedImage.TYPE_INT_RGB);
                fixed.getGraphics().drawImage(orig, 0, 0, null);
                dest = new BufferedImage(orig.getWidth(), orig.getHeight(), // NOPMD SRB
                        BufferedImage.TYPE_INT_RGB);
                convOp.filter(fixed, dest);
                output.setImage(x, y, dest);
            }
        }
    }

    /**
     * Builds the Gaussian kernel that will be used to convolve the image.
     *
     * @return  the convolution kernel
     */
    private ConvolveOp makeKernel() {

        float[] data;
        double gVal;
        float total;

        // Build the kernel
        // First, we compute a 2-D Gaussian kernel (SIGMA = 1)
        // G(x,y) = (1/(2 Pi)) EXP (-x^2-y^2)
        data = new float[KERNEL_SIZE * KERNEL_SIZE];
        total = 0.0f;

        for (int x = 0; x < KERNEL_SIZE; x++) {

            for (int y = 0; y < KERNEL_SIZE; y++) {
                gVal = (1 / (2 * Math.PI)) + Math.exp(-Math.pow(x - 3, 2) - Math.pow(y - 3, 2));
                data[(y * KERNEL_SIZE) + x] = (float) gVal;
                total += data[(y * KERNEL_SIZE) + x];
            }
        }

        // Normalize the kernel
        for (int x = 0; x < KERNEL_SIZE; x++) {

            for (int y = 0; y < KERNEL_SIZE; y++) {
                data[(y * KERNEL_SIZE) + x] /= total;
            }
        }

        return new ConvolveOp(new Kernel(KERNEL_SIZE, KERNEL_SIZE, data), ConvolveOp.EDGE_NO_OP,
                null);
    }

    /**
     * Generates the string representation of the filter.
     *
     * @return  the string representation
     */
    @Override public String toString() {

        return "SmootherFilter";
    }
}
