package com.srbenoit.filter;

import java.io.File;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;
import com.srbenoit.log.LoggedObject;

/**
 * The base class for a filter. Filters take one or more inputs (each in a specified data format)
 * and produce outputs (again, in specified data formats). Filters draw their input data from a
 * <code>Pipe</code>, and may add their output data back to that <code>Pipe</code> for downstream
 * filters to draw from.
 */
public abstract class AbstractFilter extends LoggedObject {

    /** version number for serialization */
    private static final long serialVersionUID = -5289023315832548170L;

    /** a zero-length class array to use when finding constructors */
    private static final Class<?>[] CLASS_0;

    /** a zero-length array of objects to facilitate array construction */
    private static final Object[] OBJECT_0;

    /** a zero-length array of Strings to facilitate array construction */
    private static final String[] STRING_0;

    /** the file extension for TXT files */
    public final static String TXT_EXT = ".txt";

    /** the filename of the report file indicating step completion */
    public final static String REPORT = "report" + TXT_EXT;

    /** the human-friendly name of the filter */
    private final String name;

    /** the XML tag of the filter */
    private final String tag;

    /** a set of properties that can configure the filter */
    private final Properties properties;

    /** the current state of the filter */
    private FilterState state;

    /** the selection state of the filter */
    private boolean selected;

    /** the provisional state of the filter */
    private boolean provisional;

    /** the list of formats of required input data */
    protected final List<FilterInput> inputs;

    /** the list of the formats of output data */
    protected final List<FilterOutput> outputs;

    /** the renderer for the filter */
    private FilterRenderer renderer;

    static {
        CLASS_0 = new Class<?>[0];
        OBJECT_0 = new Object[0];
        STRING_0 = new String[0];
    }

    /**
     * Constructs a new <code>AbstractFilter</code>.
     *
     * @param  filterName  the name of the filter
     * @param  filterTag   the XML tag of the filter
     */
    public AbstractFilter(final String filterName, final String filterTag) {

        super();

        this.name = filterName;
        this.tag = filterTag;

        this.properties = new Properties();
        this.inputs = new ArrayList<FilterInput>(5);
        this.outputs = new ArrayList<FilterOutput>(5);
        this.state = FilterState.INERT;
        this.provisional = false;
        this.selected = false;
    }

    /**
     * Builds the renderer (must be called after inputs and outputs are configured).
     */
    protected void makeRenderer() {

        this.renderer = new FilterRenderer(this);
    }

    /**
     * Gets the renderer that will draw the font.
     *
     * @return  the renderer
     */
    public FilterRenderer getRenderer() {

        return this.renderer;
    }

    /**
     * Gets the no-argument constructor for a filter class.
     *
     * @param   clazz  the filter class
     * @return  the no-argument constructor for instances of that filter
     */
    public static Constructor<? extends AbstractFilter> getNoArgConstructor(
        final Class<? extends AbstractFilter> clazz) {

        Constructor<? extends AbstractFilter> constr;

        try {
            constr = clazz.getConstructor(CLASS_0);
        } catch (Exception e) {
            LOG.log(Level.WARNING,
                "Exception while getting filter constructor for '" + clazz.getName() + "'", e);
            constr = null;
        }

        return constr;
    }

    /**
     * Gets a new instance of a filter based on its class.
     *
     * @param   clazz  the filter class
     * @return  the new instance
     */
    public static AbstractFilter getInstance(final Class<? extends AbstractFilter> clazz) {

        Constructor<? extends AbstractFilter> constr;
        AbstractFilter instance;

        try {
            constr = clazz.getConstructor(CLASS_0);
            instance = constr.newInstance(OBJECT_0);
        } catch (Exception e) {
            LOG.log(Level.WARNING, "Exception while getting instance of '" + clazz.getName() + "'",
                e);
            instance = null;
        }

        return instance;
    }

    /**
     * Gets the filter name.
     *
     * @return  the filter name
     */
    public String getName() {

        return this.name;
    }

    /**
     * Gets the XML tag for the filter.
     *
     * @return  the XML tag
     */
    public String getTag() {

        return this.tag;
    }

    /**
     * Sets the value of a property.
     *
     * @param  key    the property key
     * @param  value  the value to set
     */
    public void setProperty(final String key, final String value) {

        this.properties.setProperty(key, value);
    }

    /**
     * Gets the value of a property.
     *
     * @param   key  the property key
     * @return  the value, or <code>null</code> if the value has not been set
     */
    public String getProperty(final String key) {

        return this.properties.getProperty(key);
    }

    /**
     * Gets the keys for all properties in the filter.
     *
     * @return  the list of keys
     */
    public String[] getPropertyKeys() {

        return this.properties.keySet().toArray(STRING_0);
    }

    /**
     * Gets the list of keys for all properties supported by the filter
     *
     * @return  the list of keys
     */
    public String[] getSupportedPropertyKeys() {

        return new String[0];
    }

    /**
     * Gets the number of input data formats required by this filter.
     *
     * @return  the number of input data objects
     */
    public int getNumInputs() {

        return this.inputs.size();
    }

    /**
     * Gets the number of output data formats generated by this filter (assuming only the required
     * inputs are provided - non-required inputs may be passed through to the output, increasing
     * their number).
     *
     * @return  the number of output data objects
     */
    public int getNumOutputs() {

        return this.outputs.size();
    }

    /**
     * Gets an input data format required by this filter.
     *
     * @param   index  the index of the format to retrieve
     * @return  the input format
     */
    public FilterInput getInputFormat(final int index) {

        return this.inputs.get(index);
    }

    /**
     * Gets an output data format generated by this filter.
     *
     * @param   index  the index of the format to retrieve
     * @return  the output format
     */
    public FilterOutput getOutputFormat(final int index) {

        return this.outputs.get(index);
    }

    /**
     * Duplicates the filter including all of its settings, but returns an independent object.
     *
     * @return  the duplicated object
     */
    public abstract AbstractFilter duplicate();

    /**
     * Performs the filter operation.
     *
     * @param   executor  the <code>FilterTreeExecutor</code> that is executing the filter
     * @param   pipe      the pipe from which to draw input data items and to which to add output
     *                    data items
     * @throws  FilterException  if the filter cannot complete
     */
    public abstract void filter(FilterTreeExecutor executor, Pipe pipe) throws FilterException;

    /**
     * Validates that the pipe contains all required input data items.
     *
     * @param   pipe  the pipe containing the input data items to validate
     * @throws  FilterException  if the inputs are invalid
     */
    protected void validateInputs(final Pipe pipe) throws FilterException {

        AbstractPipeItem item;

        if (pipe == null) {
            throw new FilterException("Pipe passed to validateInputs must not be null");
        } else {

            // Scan our required inputs testing for presence if each in the pipe
            for (FilterInput input : this.inputs) {
                item = pipe.get(input.getKey());

                if (item == null) {
                    throw new FilterException("Input data did not contain a '"
                        + input.type.getSimpleName() + "' input named '" + input.getKey() + "'");
                }

                if (!item.getClass().equals(input.type)) {
                    throw new FilterException("Input data did not contain a '"
                        + input.type.getSimpleName() + "' input named '" + input.getKey() + "'");
                }
            }
        }
    }

    /**
     * Tests whether another object is equal to this one. To be equal, the other object must be an
     * <code>AbstractObject</code>, must have the same tag and name and the superclass, inputs,
     * outputs, and children list objects must all be equal.
     *
     * @param   obj  the object to test
     * @return  <code>true</code> if the object is equal to this object
     */
    @Override public boolean equals(final Object obj) {

        AbstractFilter filter;
        boolean equal;

        if (obj instanceof AbstractFilter) {
            filter = (AbstractFilter) obj;

            equal = super.equals(obj) && this.name.equals(filter.getName())
                && this.tag.equals(filter.getTag()) && this.inputs.equals(filter.inputs)
                && this.outputs.equals(filter.outputs);
        } else {
            equal = false;
        }

        return equal;
    }

    /**
     * Gets the hash code for this object.
     *
     * @return  the hash code
     */
    @Override public int hashCode() {

        return super.hashCode() + this.name.hashCode() + this.tag.hashCode()
            + this.inputs.hashCode() + this.outputs.hashCode();
    }

    /**
     * Tests whether a completion report exists in the target directory.
     *
     * @param   dir  the directory in which to look for the report
     * @return  <code>true</code> if the report was present; <code>false</code> if not
     */
    protected boolean testForCompletionReport(final File dir) {

        return new File(dir, REPORT).exists();
    }

    /**
     * Sets the state of the filter.
     *
     * @param  newState  the new state
     */
    public void setState(final FilterState newState) {

        this.state = newState;
    }

    /**
     * Gets the state of the filter.
     *
     * @return  the state
     */
    public FilterState getState() {

        return this.state;
    }

    /**
     * Sets the selection state of the filter.
     *
     * @param  isSelected  <code>true</code> if the filter is selected; <code>false</code> if not
     */
    public void setSelected(final boolean isSelected) {

        this.selected = isSelected;
    }

    /**
     * Gets the selection state of the filter.
     *
     * @return  <code>true</code> if the filter is selected; <code>false</code> if not
     */
    public boolean isSelected() {

        return this.selected;
    }

    /**
     * Sets the provisional state of the filter.
     *
     * @param  isProvisional  <code>true</code> if the filter is provisional; <code>false</code> if
     *                        not
     */
    public void setProvisional(final boolean isProvisional) {

        this.provisional = isProvisional;
    }

    /**
     * Gets the provisional state of the filter.
     *
     * @return  <code>true</code> if the filter is provisional; <code>false</code> if not
     */
    public boolean isProvisional() {

        return this.provisional;
    }
}
