package com.srbenoit.filter;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.SwingWorker;
import com.srbenoit.log.LogMgr;

/**
 * A subclass of <code>SwingWorker</code> that executes the filters in a filter tree and allows
 * filters to interact with a GUI as they proceed.
 */
public class FilterTreeExecutor extends SwingWorker<Object, Object> {

    /** the working directory for filters in the tree */
    private transient File dir;

    /** the filter tree to execute */
    private final transient FilterTree tree;

    /** the panel that renders the filter tree */
    private final transient FilterTreePanel panel;

    /** a list of listeners registered to receive progress notifications */
    private final transient List<FilterTreeExecutorListener> listeners;

    /** a log to which to write diagnostic messages */
    protected static final Logger LOG;

    static {
        LOG = LogMgr.getLogger();
    }

    /**
     * Constructs a new <code>FilterTreeExecutor</code>.
     *
     * @param  theTree   the filter tree to execute
     * @param  thePanel  the panel that renders the filter tree
     */
    public FilterTreeExecutor(final FilterTree theTree, final FilterTreePanel thePanel) {

        super();

        this.tree = theTree;
        this.panel = thePanel;

        this.dir = null;
        this.listeners = new ArrayList<FilterTreeExecutorListener>(2);
    }

    /**
     * Adds a listener that will be notified of progress updates in the execution of the filter
     * tree.
     *
     * @param  listener  the listener to add
     */
    public void addListener(final FilterTreeExecutorListener listener) {

        synchronized (this.listeners) {
            this.listeners.add(listener);
        }

        addPropertyChangeListener(listener);
    }

    /**
     * Removes a listener that was previously registered with <code>addListener</code>.
     *
     * @param  listener  the listener to remove
     */
    public void removeListener(final FilterTreeExecutorListener listener) {

        removePropertyChangeListener(listener);

        synchronized (this.listeners) {
            this.listeners.remove(listener);
        }
    }

    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * <p>This method is executed only once, and is executed in a background thread.
     *
     * @return  the computed result
     */
    @Override protected Object doInBackground() {

        Pipe pipe;
        List<AbstractFilter> todo;
        AbstractFilter filter;
        int count;
        boolean done;
        boolean valid;
        boolean found;
        boolean ready;
        FilterInput inp;
        FilterOutput out;

        // Get the base directory
        this.dir = new DirectoryAsker().askDir("Source Directory");

        if (this.dir != null) {
            pipe = new Pipe(this.dir);
            pipe.load(); // Load whatever has been done already

            // Build the to-do list of filters
            count = this.tree.getNumFilters();
            todo = new ArrayList<AbstractFilter>(count);

            valid = true;

            for (int i = 0; i < count; i++) {

                if (isCancelled()) {
                    valid = false;

                    break;
                }

                filter = this.tree.getFilter(i);

                // See if the filter's outputs already exist
                if (filter.getNumOutputs() == 0) {
                    done = false;
                } else {
                    done = true;

                    for (int outIdx = 0; outIdx < filter.getNumOutputs(); outIdx++) {
                        out = filter.getOutputFormat(outIdx);

                        if (!pipe.hasItem(out.getKey(), out.type)) {
                            done = false;

                            if (pipe.get(out.getKey()) != null) { // NOPMD SRB
                                valid = false;
                            }
                        }
                    }
                }

                if (done) {
                    filter.setState(FilterState.COMPLETED);
                } else {
                    todo.add(filter);
                }
            }

            if (valid) {

                // Run the filters in the to-do list, in order to the extent possible
                try {

                    while (!todo.isEmpty()) {

                        LOG.log(Level.INFO, "{0} filters remain to execute", todo.size());

                        // Find the first filter for which we have all needed inputs
                        found = false;

                        for (AbstractFilter test : todo) {

                            ready = true;

                            for (int inIdx = 0; inIdx < test.getNumInputs(); inIdx++) {
                                inp = test.getInputFormat(inIdx);

                                if (!pipe.hasItem(inp.getKey(), inp.type)) {
                                    ready = false;

                                    break;
                                }
                            }

                            if (ready) {
                                LOG.log(Level.INFO, "Running {0}", test.getName());
                                found = true;
                                publish(test, Integer.valueOf(count - todo.size()),
                                    Integer.valueOf(count));
                                firePropertyChange("filter", null, test.getName());
                                test.setState(FilterState.RUNNING);
                                this.panel.repaint();
                                test.filter(this, pipe);
                                LOG.log(Level.INFO, "{0} completed", test.getName());
                                test.setState(FilterState.COMPLETED);
                                todo.remove(test);
                                this.panel.repaint();

                                break;
                            }
                        }

                        if (!found) {
                            firePropertyChange("error", null, "No filters are ready to execute");

                            break;
                        }

                        if (isCancelled()) {
                            LOG.log(Level.INFO, "Filter tree execution was canceled");

                            break;
                        }
                    }
                } catch (Exception ex) {
                    LOG.log(Level.SEVERE, "Exception while executing filter", ex);
                    firePropertyChange("error", null, "Exception while executing filter");
                }
            }
        }

        LOG.info("doInBackground is terminating");

        return null;
    }

    /**
     * Called after the <code>doInBackground</code> method completes.
     *
     * <p>Called on the AWT event thread after the <code>doInBackground</code> method completes.
     */
    @Override protected void done() {

        synchronized (this.listeners) {

            for (FilterTreeExecutorListener list : this.listeners) {
                list.done();
            }
        }
    }

    /**
     * Provides notification of a progress update.
     *
     * <p>Called on the AWT event thread each time <code>publish</code> is called from within
     * <code>doInBackground</code>.
     *
     * @param  updateList  A list that should contain some multiple of three objects, where each
     *                     group of three is (in the following order) an <code>
     *                     AbstractFilter</code> (the filter being executed), an <code>
     *                     Integer</code> (the 0-based index of the current filter), and an <code>
     *                     Integer</code> (the number of filters in the tree).
     */
    @Override protected void process(final List<Object> updateList) {

        int size;
        Object obj0;
        Object obj1;
        Object obj2;

        size = updateList.size();

        for (int i = 0; i < size; i += 3) {
            obj0 = updateList.get(i);
            obj1 = updateList.get(i + 1);
            obj2 = updateList.get(i + 2);

            if ((obj0 instanceof AbstractFilter) && (obj1 instanceof Integer)
                    && (obj2 instanceof Integer)) {

                synchronized (this.listeners) {

                    for (FilterTreeExecutorListener list : this.listeners) {
                        list.process((AbstractFilter) obj0, ((Integer) obj1).intValue(),
                            ((Integer) obj2).intValue());
                    }
                }
            }
        }

    }

    /**
     * Provides an update on the progress of the filter to the user while it is executing. This
     * update is in the form of a state string, like "Processing image", and a progress value (from
     * 0 to 100).
     *
     * @param  percent  an integer that indicates percent complete (clipped to the range 0-100)
     */
    public void indicateProgress(final int percent) {

        int actual;

        if (percent < 0) {
            actual = 0;
        } else if (percent > 100) {
            actual = 100;
        } else {
            actual = percent;
        }

        setProgress(actual);

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            // No action
        }
    }
}
