package com.srbenoit.filter;

import java.io.File;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import com.srbenoit.log.LoggedObject;

/**
 * A item that can flow into and out of a pipe.
 */
public abstract class AbstractPipeItem extends LoggedObject {

    /** a zero-length class array to use when finding constructors */
    private static final Class<?>[] CLASS_3;

    /** a zero-length array of objects to facilitate array construction */
    private static final Object[] OBJECT_3;

    /** the pipe this item belongs to */
    private final transient Pipe pipe;

    /** a unique key for the item (unique within the pipe) */
    private final transient String key;

    /** the label for the item (a human friendly name) */
    private final transient String label;

    /** the files where this pipe item is persisted */
    private final List<PipeItemFileInfo> files;

    static {
        CLASS_3 = new Class<?>[] { String.class, String.class, Pipe.class };
        OBJECT_3 = new Object[3];
    }

    /**
     * Gets the constructor for a pipe item class that takes two <code>String</code> arguments and
     * a <code>Pipe</code> argument.
     *
     * @param   clazz  the filter class
     * @return  the constructor for instances of that filter
     */
    public static Constructor<? extends AbstractPipeItem> getConstructor(
        final Class<? extends AbstractPipeItem> clazz) {

        Constructor<? extends AbstractPipeItem> constr;

        try {
            constr = clazz.getConstructor(CLASS_3);
        } catch (Exception e) {
            LOG.log(Level.WARNING, "Unable to get constructor for " + clazz.getName(), e);
            constr = null;
        }

        return constr;
    }

    /**
     * Gets a new instance of a pipe item based on its class and string key and label.
     *
     * @param   clazz     the filter class
     * @param   theKey    the string key
     * @param   theLabel  the string label
     * @param   thePipe   the pipe in which the item is installed
     * @return  the new instance
     */
    public static AbstractPipeItem getInstance(final Class<? extends AbstractPipeItem> clazz,
        final String theKey, final String theLabel, final Pipe thePipe) {

        Constructor<? extends AbstractPipeItem> constr;
        AbstractPipeItem instance;

        try {
            constr = clazz.getConstructor(CLASS_3);

            synchronized (OBJECT_3) {
                OBJECT_3[0] = theKey;
                OBJECT_3[1] = theLabel;
                OBJECT_3[2] = thePipe;
                instance = constr.newInstance(OBJECT_3);
            }
        } catch (Exception e) {
            LOG.log(Level.WARNING, "Unable to create instance of " + clazz.getName(), e);
            instance = null;
        }

        return instance;
    }

    /**
     * Constructs a new <code>AbstractPipeItem</code>.
     *
     * @param  theKey    the unique key for the pipe item
     * @param  theLabel  the label for the item (a human friendly name)
     * @param  thePipe   the pipe in which this item is installed
     */
    public AbstractPipeItem(final String theKey, final String theLabel, final Pipe thePipe) {

        super();

        if (theKey == null) {
            throw new IllegalArgumentException("Key may not be null");
        }

        if (theLabel == null) {
            throw new IllegalArgumentException("Label may not be null");
        }

        if (thePipe == null) {
            throw new IllegalArgumentException("Pipe may not be null");
        }

        this.key = theKey;
        this.label = theLabel;
        this.pipe = thePipe;

        this.files = new ArrayList<PipeItemFileInfo>(10);
    }

    /**
     * Gets the unique key of the item. This will translate into the directory name in which the
     * item's files are stored, so it must be a valid directory name and should be meaningful in
     * the sense that the directory name should indicate its contents. For example, an item
     * containing a series of raw images might have a key "raw_images" and a label "Raw image files
     * (TIF format)".
     *
     * @return  the key
     */
    public String getKey() {

        return this.key;
    }

    /**
     * Gets the human-friendly name of the item. Think of this as a variable name, as opposed to
     * its data type (see <code>typeName</code>).
     *
     * @return  the label
     */
    public String getLabel() {

        return this.label;
    }

    /**
     * Gets the pipe this item is installed in.
     *
     * @return  the pipe
     */
    public Pipe getPipe() {

        return this.pipe;
    }

    /**
     * Gets a human-friendly name for the data type. For example, a list of sets of images
     * representing a time series of z-planes might return "Multi-plane image sequence".
     *
     * @return  the name of the data type this item represents
     */
    public abstract String typeName();

    /**
     * Resets the pipe item to a virgin (empty) state.
     */
    public abstract void reset();

    /**
     * Saves the item to a filesystem.
     *
     * @param   executor  the executor that is saving the pipe
     * @param   startPct  the starting progress percentage for the save operation
     * @param   endPct    the ending progress percentage for the save operation
     * @return  <code>true</code> on successful save; <code>false</code> on failure
     */
    public abstract boolean save(final FilterTreeExecutor executor, final int startPct,
        final int endPct);

    /**
     * Loads the item from the filesystem.
     *
     * @return  <code>true</code> if the load succeeded; <code>false</code> if not
     */
    public abstract boolean load();

    /**
     * Gets a subdirectory where this pipe item's data can be stored.
     *
     * @return  the subdirectory
     */
    public File getSubdir() {

        return new File(this.pipe.getDir(), this.key);
    }

    /**
     * Gets the list of the files where this pipe item's data is stored
     *
     * @return  the list of files (if dirty, this list may be incomplete)
     */
    public PipeItemFileInfo[] getFiles() {

        return this.files.toArray(new PipeItemFileInfo[0]);
    }

    /**
     * Gets a single file information object.
     *
     * @param   index  the index of the object
     * @return  the <code>PipeItemFileInfo</code>
     */
    public PipeItemFileInfo getFile(final int index) {

        return this.files.get(index);
    }

    /**
     * Adds a file information object.
     *
     * @param  info  the <code>PipeItemFileInfo</code> to add
     */
    public void addFile(final PipeItemFileInfo info) {

        this.files.add(info);
    }

    /**
     * Removes a file information object.
     *
     * @param  info  the <code>PipeItemFileInfo</code> to remove
     */
    public void removeFile(final PipeItemFileInfo info) {

        this.files.remove(info);
    }

    /**
     * Removes all but the first file in the files list.
     */
    public void removeAllFilesButFirst() {

        while (this.files.size() > 1) {
            this.files.remove(1);
        }
    }
}
