package com.srbenoit.microscopy;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JSeparator;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import com.srbenoit.filter.AbstractFilter;
import com.srbenoit.filter.FilterTree;
import com.srbenoit.filter.FilterTreeExecutor;
import com.srbenoit.filter.FilterTreeExecutorListener;
import com.srbenoit.filter.FilterTreeIO;
import com.srbenoit.filter.FilterTreePanel;
import com.srbenoit.filter.GraphFileFilter;
import com.srbenoit.log.LoggedObject;
import com.srbenoit.ui.UIUtilities;
import com.srbenoit.util.ResourceLoader;

/**
 * A class that supports creating a new filter tree (analysis), opening an existing filter tree,
 * editing the tree and saving changes, and executing an open tree.
 */
public class AnalysisApp extends LoggedObject implements ActionListener,
    FilterTreeExecutorListener, Runnable {

    /** a file filter for the graph tree file extension */
    private static final GraphFileFilter FILTER;

    /** the list of classes the user can choose from */
    private final transient List<Class<? extends AbstractFilter>> classes;

    /** the frame */
    private transient JFrame frame;

    /** the filter tree */
    private transient FilterTree tree;

    /** the panel */
    private transient FilterTreePanel panel;

    /** the file location of the currently open graph */
    private transient File file;

    /** the last directory opened */
    private transient File lastDir;

    /** class to serialize/deserialize filter trees */
    private final transient FilterTreeIO ser;

    /** the text above the progress bar */
    private transient JLabel progressText;

    /** the progress bar */
    private transient JProgressBar progress;

    /** the execute menu item (disabled while executing) */
    private transient JMenuItem execute;

    /** the abort menu item (enabled while executing) */
    private transient JMenuItem abort;

    /** the executor that is running the current process */
    private FilterTreeExecutor executor;

    /** icon for the play button in enabled state */
    private ImageIcon playNormal;

    /** icon for the play button in disabled state */
    private ImageIcon playDisabled;

    /** icon for the stop button in enabled state */
    private ImageIcon stopNormal;

    /** icon for the stop button in disabled state */
    private ImageIcon stopDisabled;

    /** play/execute button */
    private JButton play;

    /** stop/abort button */
    private JButton stop;

    static {
        FILTER = new GraphFileFilter();
    }

    /**
     * Constructs a new <code>AnalysisApp</code>.
     *
     * @param  filterClasses  the list of classes the user can choose from
     */
    public AnalysisApp(final List<Class<? extends AbstractFilter>> filterClasses) {

        this.classes = filterClasses;
        this.file = null;
        this.lastDir = null;
        this.ser = new FilterTreeIO();

        this.tree = new FilterTree();
    }

    /**
     * Called in the AWT event dispatcher thread to construct the GUI.
     */
    public void run() {

        AbstractFilter[] filters;
        Font font;
        JPanel content;
        JMenuBar bar;
        JMenu menu;
        JMenuItem item;
        JPanel prog;
        JPanel inner;
        BufferedImage img;

        buildFilterTree();

        this.frame = new JFrame("Generalized Video Microscopy Analysis");
        this.frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);

        content = new JPanel(new BorderLayout());
        this.frame.setContentPane(content);

        this.panel = new FilterTreePanel(classes, this.tree);
        this.panel.setBackground(Color.WHITE);

        filters = new AbstractFilter[this.classes.size()];

        for (int i = 0; i < filters.length; i++) {
            filters[i] = AbstractFilter.getInstance(this.classes.get(i));
        }

        content.add(this.panel, BorderLayout.CENTER);

        bar = new JMenuBar();
        this.frame.setJMenuBar(bar);
        menu = new JMenu("File");
        font = menu.getFont();
        font = font.deriveFont(12.0f);
        menu.setFont(font);
        bar.add(menu);
        item = new JMenuItem("New Process");
        item.setActionCommand("new");
        item.addActionListener(this);
        item.setFont(font);
        menu.add(item);
        item = new JMenuItem("Open Process...");
        item.setActionCommand("open");
        item.addActionListener(this);
        item.setFont(font);
        menu.add(item);
        item = new JMenuItem("Save Process");
        item.setActionCommand("save");
        item.addActionListener(this);
        item.setFont(font);
        menu.add(item);
        item = new JMenuItem("Save Process As...");
        item.setActionCommand("saveas");
        item.addActionListener(this);
        item.setFont(font);
        menu.add(item);
        menu.add(new JSeparator());
        item = new JMenuItem("Exit");
        item.setActionCommand("exit");
        item.addActionListener(this);
        item.setFont(font);
        menu.add(item);

        menu = new JMenu("Process");
        menu.setFont(font);
        bar.add(menu);
        item = new JMenuItem("Execute...");
        item.setActionCommand("exec");
        item.addActionListener(this);
        item.setFont(font);
        menu.add(item);
        this.execute = item;
        item = new JMenuItem("Abort");
        item.setActionCommand("abort");
        item.addActionListener(this);
        item.setFont(font);
        item.setEnabled(false);
        menu.add(item);
        this.abort = item;

        this.progressText = new JLabel(" ");
        this.progress = new JProgressBar(0, 100);

        prog = new JPanel(new BorderLayout());
        prog.setBorder(BorderFactory.createCompoundBorder(
                BorderFactory.createEmptyBorder(1, 1, 1, 1), BorderFactory.createEtchedBorder()));

        inner = new JPanel(new BorderLayout(3, 3));
        inner.setBorder(BorderFactory.createEmptyBorder(2, 5, 5, 5));
        inner.add(this.progressText, BorderLayout.NORTH);
        inner.add(this.progress, BorderLayout.SOUTH);
        prog.add(inner, BorderLayout.CENTER);
        content.add(prog, BorderLayout.SOUTH);

        img = ResourceLoader.loadImage(AnalysisApp.class, "images/play-normal.png");
        this.playNormal = new ImageIcon(img);
        img = ResourceLoader.loadImage(AnalysisApp.class, "images/play-disabled.png");
        this.playDisabled = new ImageIcon(img);

        img = ResourceLoader.loadImage(AnalysisApp.class, "images/stop-normal.png");
        this.stopNormal = new ImageIcon(img);
        img = ResourceLoader.loadImage(AnalysisApp.class, "images/stop-disabled.png");
        this.stopDisabled = new ImageIcon(img);

        this.play = new JButton(this.playNormal);
        this.play.setActionCommand("exec");
        this.play.addActionListener(this);

        this.stop = new JButton(this.stopDisabled);
        this.stop.setActionCommand("abort");
        this.stop.addActionListener(this);
        this.stop.setEnabled(false);

        inner = new JPanel(new BorderLayout(3, 3));
        inner.add(this.play, BorderLayout.WEST);
        inner.add(this.stop, BorderLayout.EAST);
        prog.add(inner, BorderLayout.WEST);

        this.frame.pack();
        UIUtilities.positionFrame(this.frame, 0.1, 0.1);
        this.frame.setVisible(true);
    }

    /**
     * Builds the filter tree that the program will begin with. This should be stored in a file
     * system, but for now is hard coded.
     */
    private void buildFilterTree() {

        MetaMorphReaderFilter metamorph;
        IntensityAutoLevelerFilter leveler;
        ZPlaneMergerFilter merger;
        SmootherFilter smoother;
        MotionCompensationFilter stabilizer;
        MaximaFinderFilter maxima;
        TrajectoryFilter trajectory;
        DiffusionAnalysisFilter analysis;
        QuicktimeBuilderFilter quicktime1;
        QuicktimeBuilderFilter quicktime2;

        metamorph = new MetaMorphReaderFilter();
        leveler = new IntensityAutoLevelerFilter();
        merger = new ZPlaneMergerFilter();
        smoother = new SmootherFilter();
        stabilizer = new MotionCompensationFilter();
        quicktime1 = new QuicktimeBuilderFilter();
        maxima = new MaximaFinderFilter();
        trajectory = new TrajectoryFilter();
        analysis = new DiffusionAnalysisFilter();
        quicktime2 = new QuicktimeBuilderFilter();

        leveler.getInputFormat(0).setKey(metamorph.getOutputFormat(2).getKey());
        merger.getInputFormat(0).setKey(leveler.getOutputFormat(0).getKey());
        smoother.getInputFormat(0).setKey(merger.getOutputFormat(0).getKey());
        stabilizer.getInputFormat(0).setKey(smoother.getOutputFormat(0).getKey());
        quicktime1.getInputFormat(0).setKey(stabilizer.getOutputFormat(0).getKey());
        maxima.getInputFormat(0).setKey(stabilizer.getOutputFormat(0).getKey());
        trajectory.getInputFormat(0).setKey(stabilizer.getOutputFormat(0).getKey());
        trajectory.getInputFormat(1).setKey(maxima.getOutputFormat(1).getKey());
        analysis.getInputFormat(0).setKey(stabilizer.getOutputFormat(0).getKey());
        analysis.getInputFormat(1).setKey(trajectory.getOutputFormat(0).getKey());
        quicktime2.getInputFormat(0).setKey(trajectory.getOutputFormat(1).getKey());

        this.tree.addFilter(metamorph);
        this.tree.addFilter(leveler);
        this.tree.addFilter(merger);
        this.tree.addFilter(smoother);
        this.tree.addFilter(stabilizer);
        this.tree.addFilter(quicktime1);
        this.tree.addFilter(maxima);
        this.tree.addFilter(trajectory);
        this.tree.addFilter(analysis);
        this.tree.addFilter(quicktime2);
    }

    /**
     * Handles action events generated by the menu items.
     *
     * @param  evt  the action event
     */
    public void actionPerformed(final ActionEvent evt) {

        String cmd;
        int option;

        cmd = evt.getActionCommand();

        if ("new".equals(cmd)) {

            if (this.panel.isDirty()) {
                option = promptForSave("New Filter Graph");

                if (option == JOptionPane.YES_OPTION) {
                    doSave();
                    this.panel.clear();
                } else if (option == JOptionPane.NO_OPTION) {
                    this.panel.clear();
                }
            } else {
                this.panel.clear();
            }
        } else if ("open".equals(cmd)) {

            if (this.panel.isDirty()) {
                option = promptForSave("Open Filter Graph");

                if (option == JOptionPane.YES_OPTION) {
                    doSave();
                    doOpen();
                } else if (option == JOptionPane.NO_OPTION) {
                    doOpen();
                }
            } else {
                doOpen();
            }
        } else if ("save".equals(cmd)) {

            if (this.panel.isDirty()) {
                doSave();
            }
        } else if ("saveas".equals(cmd)) {

            doSaveAs();
        } else if ("exec".equals(cmd)) {
            doExecute();
        } else if ("abort".equals(cmd)) {
            doAbort();
        } else if ("exit".equals(cmd)) {

            if (this.panel.isDirty()) {
                option = promptForSave("Exit");

                if (option == JOptionPane.YES_OPTION) {
                    doSave();
                    doExit();
                } else if (option == JOptionPane.NO_OPTION) {
                    doExit();
                }
            } else {
                doExit();
            }
        }
    }

    /**
     * Prompts the user for a file to load, then attempts to parse that file into a filter tree. If
     * successful, the panel is configured with the filter tree.
     */
    private void doOpen() {

        JFileChooser chooser;
        int result;
        File selected;
        String content;
        String[] msg;

        chooser = new JFileChooser();

        if (this.lastDir != null) {
            chooser.setCurrentDirectory(this.lastDir);
        }

        chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
        chooser.setAcceptAllFileFilterUsed(true);
        chooser.setFileFilter(FILTER);
        result = chooser.showOpenDialog(this.panel);

        if (result == JFileChooser.APPROVE_OPTION) {
            selected = chooser.getSelectedFile();
            this.lastDir = selected.getParentFile();

            // Try to load the file
            content = ResourceLoader.loadFile(selected);

            try {
                this.tree = this.ser.deserialize(content, this.classes);
                this.panel.setTree(this.tree);
                this.file = selected;
                this.panel.repaint();
            } catch (Exception e) {
                msg = new String[] { "Unable to load this filter tree", e.getMessage() };
                JOptionPane.showMessageDialog(this.panel, msg, "Open Filter Tree",
                    JOptionPane.ERROR_MESSAGE);
            }
        }
    }

    /**
     * If there is a file loaded, saves the current filter tree to that file. If there is no file
     * loaded (the current filter tree has not been saved yet), this method acts as if <code>
     * doSaveAs</code> had been called.
     */
    private void doSave() {

        String content;
        FileOutputStream out;
        String[] msg;

        if (this.file == null) {
            doSaveAs();
        } else {
            content = this.ser.serialize(this.tree);

            try {
                out = new FileOutputStream(this.file);
                out.write(content.getBytes());
                out.close();
                this.panel.setDirty(false);
            } catch (Exception e) {
                msg = new String[] { "Unable to save this filter tree", e.getMessage() };
                JOptionPane.showMessageDialog(this.panel, msg, "Save Filter Tree",
                    JOptionPane.ERROR_MESSAGE);
            }
        }
    }

    /**
     * Prompts the user for a file in which to save the current filter graph.
     */
    private void doSaveAs() {

        JFileChooser chooser;
        int result;
        File selected;
        String content;
        FileOutputStream out;
        String[] msg;
        boolean approved;

        chooser = new JFileChooser();

        if (this.lastDir != null) {
            chooser.setCurrentDirectory(this.lastDir);
        }

        chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
        chooser.setAcceptAllFileFilterUsed(true);
        chooser.setFileFilter(FILTER);
        result = chooser.showSaveDialog(this.panel);

        if (result == JFileChooser.APPROVE_OPTION) {
            selected = chooser.getSelectedFile();
            this.lastDir = selected.getParentFile();

            // Add the extension ".ftree" if no extension was given
            if (!selected.getName().contains(".")) {
                selected = new File(this.lastDir, selected.getName() + ".ftree");
            }

            if (selected.exists()) {
                approved = (JOptionPane.showConfirmDialog(this.panel, "Overwrite existing file?",
                            "Save Filte Tree", JOptionPane.OK_CANCEL_OPTION)
                        == JOptionPane.OK_OPTION);
            } else {
                approved = true;
            }

            if (approved) {
                content = this.ser.serialize(this.tree);

                try {
                    out = new FileOutputStream(selected);
                    out.write(content.getBytes());
                    out.close();
                    this.panel.setDirty(false);
                    this.file = selected;
                } catch (Exception e) {
                    msg = new String[] { "Unable to save this filter tree", e.getMessage() };
                    JOptionPane.showMessageDialog(this.panel, msg, "Save Filter Tree",
                        JOptionPane.ERROR_MESSAGE);
                }
            }
        }
    }

    /**
     * Exits the application (takes no action to preserve unsaved data).
     */
    private void doExit() {

        this.frame.setVisible(false);
        this.frame.dispose();
    }

    /**
     * Executes the current process.
     */
    private void doExecute() {

        this.panel.setEnabled(false);

        this.execute.setEnabled(false);
        this.play.setEnabled(false);
        this.play.setIcon(this.playDisabled);

        this.abort.setEnabled(true);
        this.stop.setEnabled(true);
        this.stop.setIcon(this.stopNormal);

        this.progressText.setText("Initializing");

        this.executor = new FilterTreeExecutor(this.tree, this.panel);
        this.executor.addListener(this);
        this.executor.execute();
    }

    /**
     * Aborts the current process.
     */
    private void doAbort() {

        if (this.executor != null) {
            this.executor.cancel(false);
        }
    }

    /**
     * Called on the AWT event dispatcher thread after the filter tree has been completely
     * executed.
     */
    public void done() {

        if (this.executor != null) {

            if (this.executor.isCancelled()) {
                this.progressText.setText("Canceled");
            } else {
                this.progressText.setText("Finished");
            }

            this.progress.setValue(0);
            this.executor = null;
        }

        this.execute.setEnabled(true);
        this.play.setEnabled(true);
        this.play.setIcon(this.playNormal);

        this.abort.setEnabled(false);
        this.stop.setEnabled(false);
        this.stop.setIcon(this.stopDisabled);

        this.panel.setEnabled(true);
    }

    /**
     * Called on the AWT event dispatcher thread to provide notification of a progress update.
     *
     * @param  filter  the filter being executed
     * @param  index   the 0-based index of the filter being executed
     * @param  total   the total number of filters in the tree
     */
    public void process(final AbstractFilter filter, final int index, final int total) {

        // No action
    }

    /**
     * This method gets called when a bound property is changed.
     *
     * @param  evt  A PropertyChangeEvent object describing the event source and the property that
     *              has changed.
     */

    public void propertyChange(final PropertyChangeEvent evt) {

        String name;
        Object value;

        name = evt.getPropertyName();

        if ("progress".equals(name)) {
            value = evt.getNewValue();

            if (value instanceof Integer) {
                this.progress.setValue(((Integer) value).intValue());
            } else {
                LOG.log(Level.WARNING, "Invalid object type in progress update: {0}",
                    evt.getNewValue().getClass().getName());
            }
        } else if ("error".equals(name)) {
            JOptionPane.showMessageDialog(null, evt.getNewValue(), "Filter Error",
                JOptionPane.WARNING_MESSAGE);
        } else if ("filter".equals(name)) {
            this.progressText.setText(evt.getNewValue().toString());
        }
    }

    /**
     * Displays a prompt to the user asking whether the current filter graph should be saved before
     * its data is flushed.
     *
     * @param   title  the title of the prompt dialog
     * @return  the user's choice - one of <code>JOptionPane.YES_OPTION</code>, <code>
     *          JOptionPane.NO_OPTION</code>, r <code>JOptionPane.CANCEL_OPTION</code>
     */
    private int promptForSave(final String title) {

        return JOptionPane.showConfirmDialog(this.panel, "Save the current filter graph?", title,
                JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
    }

    /**
     * Main method to display the frame.
     *
     * @param  args  command-line arguments (ignored)
     */
    public static void main(final String... args) {

        List<Class<? extends AbstractFilter>> classes;

        classes = new ArrayList<Class<? extends AbstractFilter>>(10);

        classes.add(MetaMorphReaderFilter.class);
        classes.add(IntensityAutoLevelerFilter.class);
        classes.add(ZPlaneMergerFilter.class);
        classes.add(SmootherFilter.class);
        classes.add(MotionCompensationFilter.class);
        classes.add(MaximaFinderFilter.class);
        classes.add(TrajectoryFilter.class);
        classes.add(DiffusionAnalysisFilter.class);
        classes.add(QuicktimeBuilderFilter.class);

        SwingUtilities.invokeLater(new AnalysisApp(classes));
    }
}
