package com.srbenoit.microscopy;

import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
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;
import com.srbenoit.filter.items.ImagePoint;
import com.srbenoit.filter.items.PointSetArrayPipeItem;
import com.srbenoit.filter.items.Trajectory;
import com.srbenoit.filter.items.TrajectoryListPipeItem;

/**
 * A filter that generates a set of trajectories from a set of maxima point arrays.
 */
public class TrajectoryFilter extends AbstractFilter {

    /** version number for serialization */
    private static final long serialVersionUID = 9105886485480586655L;

    /** width of border around thumbnails */
    private static final int BORDER = 15;

    /** the set of paths we have found so far */
    private final List<Trajectory> paths;

    /**
     * Constructs a new <code>TrajectoryFilter</code>.
     */
    public TrajectoryFilter() {

        super("Trajectory Assembler", TrajectoryFilter.class.getName());

        this.inputs.add(new FilterInput(ImageArrayPipeItem.class, "Motion-compensated images"));
        this.inputs.add(new FilterInput(PointSetArrayPipeItem.class,
                "Points of maximal intensity"));
        this.outputs.add(new FilterOutput(TrajectoryListPipeItem.class, "Assembled trajectories",
                "trajectories"));
        this.outputs.add(new FilterOutput(ImageArrayPipeItem.class,
                "Thumbnail images around each trajectory", "thumbnails"));
        makeRenderer();

        this.paths = new ArrayList<Trajectory>(10);
    }

    /**
     * Duplicates the filter including all of its settings, but returns an independent object.
     *
     * @return  the duplicated object
     */
    @Override public AbstractFilter duplicate() {

        return new TrajectoryFilter();
    }

    /**
     * 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 images;
        PointSetArrayPipeItem maxima;

        validateInputs(pipe);
        executor.indicateProgress(1);

        images = (ImageArrayPipeItem) pipe.get(this.inputs.get(0).getKey());
        maxima = (PointSetArrayPipeItem) pipe.get(this.inputs.get(1).getKey());
        executor.indicateProgress(2);

        runFilter(executor, pipe, images, maxima);

        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  images    the images to process
     * @param  maxima    the maxima to assemble into a trajectories
     */
    private void runFilter(final FilterTreeExecutor executor, final Pipe pipe,
        final ImageArrayPipeItem images, final PointSetArrayPipeItem maxima) {

        TrajectoryListPipeItem trajectories;
        ImageArrayPipeItem thumbnails;
        File outfile;
        Trajectory traj;
        FileWriter writer;
        ImagePoint point;
        Point relative;

        trajectories = new TrajectoryListPipeItem(this.outputs.get(0).getKey(),
                "Trajectories of maxima", pipe);
        pipe.add(trajectories);

        // Do the trajectory analysis before we filter the frames
        aggregateExtremaVectors(executor, maxima, trajectories);

        thumbnails = new ImageArrayPipeItem(this.outputs.get(1).getKey(),
                "Thumbnails surrounding each maxima (PNG format)", pipe, "t", "traj",
                images.getXSize(), this.paths.size(), "png");
        pipe.add(thumbnails);

        // Mark up the images with the trajectories
        for (int i = 0; i < this.paths.size(); i++) {

            if (executor.isCancelled()) {
                break;
            }

            executor.indicateProgress(50 + ((i * 20) / this.paths.size()));
            makeThumbnails(i, this.paths.get(i), images, thumbnails);
        }

        if (!executor.isCancelled()) {
            outfile = new File(pipe.getDir(), "trajectories.csv");

            try {
                writer = new FileWriter(outfile);

                writer.write(
                    "Trajectory, Time Index, Plane, X, Y, Vel X, Vel Y, Tissue Vel X, Tissue Vel Y, Relative X, Relative Y\r\n");

                for (int i = 0; i < this.paths.size(); i++) {
                    executor.indicateProgress(70 + ((i * 10) / this.paths.size()));

                    traj = this.paths.get(i);

                    for (int j = 0; j < traj.numPoints(); j++) {

                        point = traj.getPoint(j);
                        relative = traj.getRelative(j);

                        writer.write((i + 1) + ", " + traj.getTimePoint(j) + ", "
                            + traj.getPlane(j) + ", " + point.getXPos() + ", " + point.getYPos()
                            + ", " + point.getXVel() + ", " + point.getYVel() + ", "
                            + point.getXAmbientVel() + ", " + point.getYAmbientVel() + ", "
                            + relative.x + ", " + relative.y + "\r\n");
                    }
                }

                writer.close();
            } catch (IOException e) {
                LOG.log(Level.WARNING, "Exception writing trajectories.csv file", e);
            }
        }
    }

    /**
     * Searches for lists of maxima that can be connected and extract trajectories.
     *
     * @param  executor      the <code>FilterTreeExecutor</code> that is executing the filter
     * @param  maxima        the array of maxima values and vectors
     * @param  trajectories  the list to which to add discovered trajectories
     */
    public void aggregateExtremaVectors(final FilterTreeExecutor executor,
        final PointSetArrayPipeItem maxima, final TrajectoryListPipeItem trajectories) {

        ImagePoint point;
        boolean found;
        Trajectory traj;
        int numTraj;
        Rectangle extents;

        for (int time = 0; time < (maxima.getXSize() - 1); time++) {

            if (executor.isCancelled()) {
                break;
            }

            executor.indicateProgress(5 + ((time * 45) / (maxima.getXSize() - 1)));

            for (int plane = 0; plane < maxima.getYSize(); plane++) {

                for (int i = 0; i < maxima.getNumPoints(time, plane); i++) {
                    point = maxima.getPoint(time, plane, i);

                    // See if the point can tie in with an existing trajectory
                    found = false;

                    // Test within the same plane first...
                    for (Trajectory path : this.paths) {

                        if ((path.getCurrentX() == point.getXPos())
                                && (path.getCurrentY() == point.getYPos())
                                && (path.getCurrentPlane() == plane)) {

                            found = true;
                            path.addPoint(time, plane, point);

                            break;
                        }
                    }

                    if (!found) {

                        // Test adjacent planes next...
                        for (Trajectory path : this.paths) {

                            if ((path.getCurrentX() == point.getXPos())
                                    && (path.getCurrentY() == point.getYPos())
                                    && ((path.getCurrentPlane() == (plane - 1))
                                        || (path.getCurrentPlane() == (plane + 1)))) {

                                found = true;
                                path.addPoint(time, plane, point);

                                break;
                            }
                        }
                    }

                    // If no existing trajectory matched, start a new one
                    if (!found) {
                        traj = new Trajectory(); // NOPMD SRB
                        traj.addPoint(time, plane, point);
                        this.paths.add(traj);
                    }
                }
            }
        }

        // Delete any trajectories of less than 10 steps or whose total motion is less than 5
        if (!executor.isCancelled()) {
            numTraj = this.paths.size();

            for (int i = numTraj - 1; i >= 0; i--) {
                traj = this.paths.get(i);
                extents = traj.extents();

                if (traj.points.size() < 10) {
                    this.paths.remove(i);
                } else if ((extents.width < 5) && (extents.height < 5)) {
                    this.paths.remove(i);
                } else {
                    trajectories.addTrajectory(traj);
                }
            }
        }
    }

    /**
     * Generates the thumbnail frames for a trajectory.
     *
     * @param  index       the index of the trajectory
     * @param  traj        the trajectory whose thumbnail frames are to be generated
     * @param  srcImages   the source images (with maxima marked)
     * @param  destImages  an image array in which to add the thumbnail frames
     */
    private void makeThumbnails(final int index, final Trajectory traj,
        final ImageArrayPipeItem srcImages, final ImageArrayPipeItem destImages) {

        Rectangle extents;
        ImagePoint point;
        Point relative;
        BufferedImage src;
        BufferedImage dest;
        Graphics2D grx;
        int xPos;
        int yPos;

        extents = traj.extents();

        for (int i = 0; i < traj.numPoints(); i++) {

            point = traj.getPoint(i);
            relative = traj.getRelative(i);

            src = srcImages.getImage(traj.getTimePoint(i), traj.getPlane(i));
            dest = new BufferedImage(extents.width + (2 * BORDER), // NOPMD SRB
                    extents.height + (2 * BORDER), BufferedImage.TYPE_INT_RGB);
            grx = (Graphics2D) dest.getGraphics();

            xPos = -point.getXPos() + (relative.x - extents.x) + BORDER;
            yPos = -point.getYPos() + (relative.y - extents.y) + BORDER;

            grx.drawImage(src, xPos, yPos, null);
            destImages.setImage(traj.getTimePoint(i), index, dest);
        }
    }

    /**
     * Generates the string representation of the filter.
     *
     * @return  the string representation
     */
    @Override public String toString() {

        return "TrajectoryFilter";
    }
}
