package com.srbenoit.microscopy;

import java.awt.image.BufferedImage;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Level;
import javax.imageio.ImageIO;
import javax.swing.JOptionPane;
import com.srbenoit.filter.AbstractFilter;
import com.srbenoit.filter.FilterException;
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.StringPipeItem;
import com.srbenoit.filter.items.TimeSeriesPipeItem;
import com.srbenoit.util.LocalTime;
import loci.formats.ClassList;
import loci.formats.IFormatReader;
import loci.formats.ImageReader;
import loci.formats.in.MetamorphTiffReader;

/**
 * A filter to scan a directory for a set of MetaMorph image series, prompt the user to select a
 * series from those available, load the series files, then prompt the user to designate
 * subsequences of the available frames.
 */
public class MetaMorphReaderFilter extends AbstractFilter {

    /** version number for serialization */
    private static final long serialVersionUID = 5382801139981082490L;

    /** Sequence name property */
    private static final String SEQ_NAME = "SequenceName";

    /** zero-length string array for list to array conversion */
    private static final String[] STRING_0 = new String[0];

    /**
     * Constructs a new <code>MetaMorphReaderFilter</code>.
     */
    public MetaMorphReaderFilter() {

        super("MetaMorph Series Reader", MetaMorphReaderFilter.class.getName());

        this.outputs.add(new FilterOutput(StringPipeItem.class, "The name of the sequence",
                "sequence_name"));
        this.outputs.add(new FilterOutput(TimeSeriesPipeItem.class, "A series of time stamps",
                "time_series"));
        this.outputs.add(new FilterOutput(ImageArrayPipeItem.class,
                "The raw images from Metamorph", "raw_images"));
        makeRenderer();

        ImageIO.scanForPlugins();
    }

    /**
     * Duplicates the filter including all of its settings, but returns an independent object.
     *
     * @return  the duplicated object
     */
    @Override public AbstractFilter duplicate() {

        return new MetaMorphReaderFilter();
    }

    /**
     * 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 {

        String[] sequences;
        String name;
        StringPipeItem string;
        TimeSeriesPipeItem series;
        ImageArrayPipeItem images;
        BufferedImage[][] raw;

        validateInputs(pipe);
        executor.indicateProgress(1);

        // Scan for available sequences
        sequences = getSequenceNames(pipe.getDir());

        if (sequences.length == 0) {
            throw new FilterException("No metaMorph sequences to read.");
        }

        executor.indicateProgress(2);

        // Figure out which sequence we're analyzing
        name = identifySequence(sequences);

        if (name == null) {
            throw new FilterException("No MetaMorph sequence to read.");
        }

        executor.indicateProgress(3);

        string = new StringPipeItem(this.outputs.get(0).getKey(), "Sequence name", pipe);
        string.setData(name);
        pipe.add(string);
        executor.indicateProgress(4);

        series = new TimeSeriesPipeItem(this.outputs.get(1).getKey(), "Time series", pipe);
        pipe.add(series);
        executor.indicateProgress(5);

        raw = runFilter(executor, pipe.getDir(), name, series);

        if (raw == null) {
            throw new FilterException("Unable to load raw images.");
        }

        images = new ImageArrayPipeItem(this.outputs.get(2).getKey(), "Raw images (TIF format)",
                pipe, "t", "z", raw.length, raw[0].length, "tif");
        pipe.add(images);
        executor.indicateProgress(79);

        for (int x = 0; x < raw.length; x++) {

            for (int y = 0; y < raw[x].length; y++) {
                images.setImage(x, y, raw[x][y]);
            }
        }

        executor.indicateProgress(80);

        if (!executor.isCancelled()) {
            pipe.save(executor);
        }

        executor.indicateProgress(100);
    }

    /**
     * Gets the names of all video sequences in the directory.
     *
     * @param   sourceDir  the directory in which to scan
     * @return  the names of the sequences
     */
    private String[] getSequenceNames(final File sourceDir) {

        List<String> names;
        File[] files;
        String name;
        String lower;
        int index;

        files = sourceDir.listFiles();
        names = new ArrayList<String>(files.length);

        for (File file : files) {
            name = file.getName();
            lower = name.toLowerCase(Locale.US);
            index = lower.indexOf(".nd");

            if (index != -1) {
                name = name.substring(0, index);

                if (!names.contains(name)) {
                    names.add(name);
                }
            }
        }

        return names.toArray(STRING_0);
    }

    /**
     * Given a nonempty list of available sequence names, determine which we want to process.
     *
     * @param   sequences  the list of available sequence names
     * @return  the selected sequence name
     * @throws  FilterException  if the user canceled during sequence selection
     */
    private String identifySequence(final String[] sequences) throws FilterException {

        String old;
        String name;

        old = getProperty(SEQ_NAME);

        if (sequences.length > 1) {

            if (old == null) {
                name = promptForSeqname(sequences);

                if (name == null) {
                    throw new FilterException("No metaMorph sequence to read.");
                }

                setProperty(SEQ_NAME, name);
            } else {
                boolean hit = false;
                name = old;

                for (String test : sequences) {

                    if (name.equals(test)) {
                        hit = true;

                        break;
                    }
                }

                if (!hit) {

                    // The name in the attributes is not valid
                    name = promptForSeqname(sequences);

                    if (name == null) {
                        throw new FilterException("No metaMorph sequence to read.");
                    }

                    setProperty(SEQ_NAME, name);
                }
            }
        } else {
            name = sequences[0];

            if ((old == null) || (!old.equals(name))) {
                setProperty(SEQ_NAME, name);
            }
        }

        return name;
    }

    /**
     * Prompts the user to choose from a list of available sequences.
     *
     * @param   sequences  the list of names of the available sequences
     * @return  the selected sequence name
     */
    private String promptForSeqname(final String[] sequences) {

        int index;
        String name;

        index = JOptionPane.showOptionDialog(null, "Which sequence would you like to analyze?",
                "Load MetaMorph series", JOptionPane.OK_CANCEL_OPTION,
                JOptionPane.QUESTION_MESSAGE, null, sequences, null);

        if (index == JOptionPane.CLOSED_OPTION) {
            name = null;
        } else {
            name = sequences[index];
        }

        return name;
    }

    /**
     * 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   sourceDir  the directory from which to load Metamorph files
     * @param   seqName    the sequence name to read
     * @param   series     the time series to build as data values are read
     * @return  the array of extracted images
     * @throws  FilterException  if the filter cannot complete
     */
    private BufferedImage[][] runFilter(final FilterTreeExecutor executor, final File sourceDir,
        final String seqName, final TimeSeriesPipeItem series) throws FilterException {

        List<File> files;
        List<File> actual;
        String test;
        String lower;
        boolean found;
        BufferedImage[][] images;

        // Get the list of files we should scan
        files = listFiles(sourceDir, seqName);
        executor.indicateProgress(6);

        if (files.isEmpty()) {
            throw new FilterException("No metaMorph data files found.");
        }

        // Get an ordered list of the files representing time points
        actual = new ArrayList<File>(files.size());

        for (int t = 1; t <= files.size(); t++) {

            if (executor.isCancelled()) {
                break;
            }

            test = "_t" + t + ".tif";
            found = false;

            for (File file : files) {
                lower = file.getName().toLowerCase(Locale.getDefault());

                if (lower.endsWith(test)) {
                    actual.add(file);
                    found = true;

                    break;
                }
            }

            if (!found) {
                break;
            }
        }

        executor.indicateProgress(7);

        if (actual.isEmpty()) {
            throw new FilterException("No metaMorph data files found.");
        }

        images = new BufferedImage[actual.size()][];

        // Load the time points
        if (!executor.isCancelled()) {

            for (int t = 1; t <= actual.size(); t++) {
                images[t - 1] = loadTimePoint(actual.get(t - 1), t, series);
                executor.indicateProgress(8 + (t * 70 / actual.size()));
            }
        }

        // Set up time point subsequences

        return images;
    }

    /**
     * Loads a TIF file which may contain more than one image plane.
     *
     * @param   file       the file to read
     * @param   timeIndex  the time index
     * @param   series     the time series to build as data values are read
     * @return  the set of loaded Z planes for the specified time point
     * @throws  FilterException  if the filter cannot complete
     */
    private BufferedImage[] loadTimePoint(final File file, final int timeIndex,
        final TimeSeriesPipeItem series) throws FilterException {

        ClassList<IFormatReader> list;
        ImageReader reader;
        int numPlanes;
        int width;
        int height;
        int bpp;
        int channels;
        int bytesPer;
        byte[] data;
        int[] combined;
        int type;
        BufferedImage[] planes;
        int index;
        Map<String, Object> meta;
        Object obj;
        LocalTime time;

        list = new ClassList<IFormatReader>(IFormatReader.class);
        list.addClass(MetamorphTiffReader.class);

        reader = new ImageReader(list);

        try {
            reader.setId(file.getAbsolutePath());

            width = reader.getSizeX();
            height = reader.getSizeY();
            bpp = reader.getBitsPerPixel();

            // Extract the time of the exposure
            meta = reader.getGlobalMetadata();

            if (meta == null) {
                time = new LocalTime();
                time.setMillis(timeIndex);
            } else {
                obj = meta.get("acquisition-time-local");

                if (obj == null) {
                    obj = meta.get("DateTime");

                    if (obj == null) {
                        obj = meta.get("modification-time-local");
                    }
                }

                if (obj == null) {
                    time = new LocalTime();
                    time.setMillis(timeIndex);
                } else {
                    time = extractTime(obj.toString());
                }
            }

            series.addTimePoint(time);

            bytesPer = (bpp + 7) / 8;
            channels = reader.getRGBChannelCount();
            numPlanes = reader.getImageCount();
            planes = new BufferedImage[numPlanes];

            for (int z = 0; z < numPlanes; z++) {

                data = reader.openBytes(z);
                combined = new int[data.length / bytesPer]; // NOPMD SRB

                index = 0;

                for (int i = 0; i < combined.length; i++) {

                    combined[i] = data[index] & 0x00FF;

                    for (int j = 1; j < bytesPer; j++) {
                        combined[i] += (data[index + j] & 0x00FF) << (8 * j);
                    }

                    index += bytesPer;
                }

                if (channels == 1) {

                    switch (bytesPer) {

                    case 1:
                        type = BufferedImage.TYPE_BYTE_GRAY;

                        break;

                    case 2:
                        type = BufferedImage.TYPE_USHORT_GRAY;

                        break;

                    default:

                        continue;
                    }
                } else if (channels == 3) {

                    switch (bytesPer) {

                    case 3:
                        type = BufferedImage.TYPE_INT_RGB;

                        break;

                    case 4:
                        type = BufferedImage.TYPE_INT_ARGB;

                        break;

                    default:

                        continue;
                    }
                } else {
                    continue;
                }

                planes[z] = new BufferedImage(width, height, type); // NOPMD SRB

                index = 0;

                for (int y = 0; y < height; y++) {

                    for (int x = 0; x < width; x++) {
                        planes[z].getRaster().setSample(x, y, 0, combined[index]);
                        index++;
                    }
                }
            }

        } catch (Exception e) {
            LOG.log(Level.WARNING, "Exception reading file", e);
            throw new FilterException("Exception reading file", e);
        }

        return planes;
    }

    /**
     * Retrieves a list of the source files in the target directory for the target sequence.
     *
     * @param   dir   the directory in which to search
     * @param   name  the sequence name
     * @return  the list of files
     */
    private List<File> listFiles(final File dir, final String name) {

        List<File> list;
        File[] files;
        String lower;

        // list the files and cull those that are not relevant
        files = dir.listFiles();

        list = new ArrayList<File>(files.length);

        for (int i = 0; i < files.length; i++) {
            lower = files[i].getName().toLowerCase(Locale.getDefault());

            if (files[i].getName().startsWith(name) && (!files[i].isDirectory())
                    && (!lower.endsWith(".nd")) && (!lower.contains("_thumb_"))) {
                list.add(files[i]);
            }
        }

        return list;
    }

    /**
     * Converts a timestamp string into a <code>LocalTime</code>. The input data will be in the
     * format: '20100902 08:09:13.787'.
     *
     * @param   time  the time string to parses
     * @return  the fixed time
     * @throws  FilterException  if the timestamp cannot be parsed
     */
    private LocalTime extractTime(final String time) throws FilterException {

        LocalTime fixed;

        fixed = new LocalTime();

        if ((time.length() >= 19) && (time.charAt(8) == ' ') && (time.charAt(11) == ':')
                && (time.charAt(14) == ':') && (time.charAt(17) == '.')) {

            try {
                fixed.setYear(Integer.parseInt(time.substring(0, 4)));
                fixed.setMonth(Integer.parseInt(time.substring(4, 6)));
                fixed.setDay(Integer.parseInt(time.substring(6, 8)));
                fixed.setHour(Integer.parseInt(time.substring(9, 11)));
                fixed.setMinute(Integer.parseInt(time.substring(12, 14)));
                fixed.setSecond(Integer.parseInt(time.substring(15, 17)));
                fixed.setMillis(Integer.parseInt(time.substring(18)));
            } catch (NumberFormatException e) {
                throw new FilterException("Invalid timestamp: '" + time + "'", e);
            }
        } else {
            throw new FilterException("Invalid timestamp: '" + time + "'");
        }

        return fixed;
    }

    /**
     * Generates the string representation of the filter.
     *
     * @return  the string representation
     */
    @Override public String toString() {

        return "MetaMorphReaderFilter";
    }
}
