package com.srbenoit.microscopy;

import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
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 takes an image array and adjusts the brightness and contrast to equalize intensity
 * values over the entire range.
 */
public class IntensityAutoLevelerFilter extends AbstractFilter {

    /** version number for serialization */
    private static final long serialVersionUID = -3172221340075098136L;

    /**
     * Constructs a new <code>IntensityAutoLevelerFilter</code>.
     */
    public IntensityAutoLevelerFilter() {

        super("Intensity auto-leveler", IntensityAutoLevelerFilter.class.getName());

        this.inputs.add(new FilterInput(ImageArrayPipeItem.class, "Raw images"));
        this.outputs.add(new FilterOutput(ImageArrayPipeItem.class, "Intensity-leveled images",
                "leveled_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 IntensityAutoLevelerFilter();
    }

    /**
     * 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);

        // Install a dummy image array to test for persisted data
        output = new ImageArrayPipeItem(this.outputs.get(0).getKey(),
                "Intensity-leveled images (PNG format)", pipe, "t", "z", 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  input     the images to process
     * @param  output    the destination in which to store processed images
     */
    private void runFilter(final FilterTreeExecutor executor, final ImageArrayPipeItem input,
        final ImageArrayPipeItem output) {

        int width;
        int height;
        double[][] imageMeans;
        double totalInImage;
        double total;
        double totalMean;
        WritableRaster raster;
        int sample;
        int count;
        int totalCount;
        int min;
        int max;
        BufferedImage dest;
        WritableRaster newRaster;
        int pixel;
        double scale;

        width = input.getXSize();
        height = input.getYSize();

        // Scan each image to get mean intensity
        imageMeans = new double[width][height];
        total = 0;
        totalCount = 0;
        min = Integer.MAX_VALUE;
        max = 0;

        for (int x = 0; x < width; x++) {

            for (int y = 0; y < height; y++) {

                if (executor.isCancelled()) {
                    break;
                }

                executor.indicateProgress(4 + (40 * ((x * height) + y) / (width * height)));

                totalInImage = 0;
                count = 0;
                raster = input.getImage(x, y).getRaster();

                for (int xPix = 0; xPix < raster.getWidth(); xPix++) {

                    for (int yPix = 0; yPix < raster.getHeight(); yPix++) {

                        for (int b = 0; b < raster.getNumBands(); b++) {
                            sample = raster.getSample(xPix, yPix, b);

                            if (sample > max) {
                                max = sample;
                            }

                            if (sample < min) {
                                min = sample;
                            }

                            totalInImage += sample;
                            total += sample;
                            count++;
                            totalCount++;
                        }
                    }
                }

                imageMeans[x][y] = totalInImage / count;
            }
        }

        if (!executor.isCancelled()) {
            totalMean = total / totalCount;

            for (int x = 0; x < width; x++) {

                for (int y = 0; y < height; y++) {

                    if (executor.isCancelled()) {
                        break;
                    }

                    executor.indicateProgress(45 + (35 * ((x * height) + y) / (width * height)));

                    // Determine a scale to map the entire intensity range into 0-255
                    scale = 255.0 * (totalMean / imageMeans[x][y]) / (max - min);

                    raster = input.getImage(x, y).getRaster();
                    dest = new BufferedImage(raster.getWidth(), raster.getHeight(), // NOPMD SRB
                            BufferedImage.TYPE_INT_RGB);

                    newRaster = dest.getRaster();

                    if (raster.getNumBands() == 1) {

                        for (int xPix = 0; xPix < raster.getWidth(); xPix++) {

                            for (int yPix = 0; yPix < raster.getHeight(); yPix++) {
                                sample = raster.getSample(xPix, yPix, 0);
                                pixel = (int) ((sample - min) * scale);
                                pixel = (int) ((sample - min) * scale);

                                if (pixel > 255) {
                                    pixel = 255;
                                }

                                for (int b = 0; b < newRaster.getNumBands(); b++) {
                                    newRaster.setSample(xPix, yPix, b, pixel);
                                }
                            }
                        }
                    } else if (raster.getNumBands() == 3) {

                        for (int xPix = 0; xPix < raster.getWidth(); xPix++) {

                            for (int yPix = 0; yPix < raster.getHeight(); yPix++) {

                                for (int b = 0; b < 3; b++) {
                                    sample = raster.getSample(xPix, yPix, b);
                                    pixel = (int) ((sample - min) * scale);

                                    if (pixel > 255) { // NOPMD SRB
                                        pixel = 255;
                                    }

                                    newRaster.setSample(xPix, yPix, b, pixel);
                                }
                            }
                        }
                    }

                    output.setImage(x, y, dest);
                }
            }
        }
    }

    /**
     * Generates the string representation of the filter.
     *
     * @return  the string representation
     */
    @Override public String toString() {

        return "IntensityAutoLevelerFilter";
    }
}
