/*
 * #%L
 * SCIFIO library for reading and converting scientific file formats.
 * %%
 * Copyright (C) 2011 - 2020 SCIFIO developers.
 * %%
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * #L%
 */

package io.scif.filters;

import io.scif.Format;
import io.scif.FormatException;
import io.scif.ImageMetadata;
import io.scif.Metadata;
import io.scif.Plane;
import io.scif.Reader;
import io.scif.config.SCIFIOConfig;

import java.io.IOException;

import net.imglib2.FinalInterval;
import net.imglib2.Interval;

import org.scijava.Context;
import org.scijava.io.handle.DataHandle;
import org.scijava.io.location.Location;
import org.scijava.plugin.Parameter;
import org.scijava.plugin.Plugin;
import org.scijava.plugin.PluginService;

/**
 * Abstract superclass for all {@link io.scif.filters.Filter} that delegate to
 * {@link io.scif.Reader} instances.
 * <p>
 * NB: All concrete implementations of this interface should be annotated as
 * {@link Plugin} for automatic discovery.
 * </p>
 * <p>
 * NB: This class attempts to locate a type-matching MetadataWrapper to
 * protectively wrap the wrapped {@code Reader}'s Metadata. If none is found, a
 * reference to the {@code Reader's} Metadata itself is used.
 * </p>
 *
 * @author Mark Hiner
 * @see io.scif.Reader
 * @see io.scif.filters.Filter
 * @see io.scif.filters.AbstractMetadataWrapper
 */
public abstract class AbstractReaderFilter extends AbstractFilter<Reader>
	implements Reader
{

	// -- Fields --

	/* Need to wrap each Reader's Metadata separately */
	private Metadata wrappedMeta = null;

	private final Class<? extends MetadataWrapper> metaClass;

	@Parameter
	private PluginService pluginService;

	// -- Constructor --

	public AbstractReaderFilter() {
		this(null);
	}

	public AbstractReaderFilter(
		final Class<? extends MetadataWrapper> metaClass)
	{
		super(Reader.class);
		this.metaClass = metaClass;
	}

	// -- AbstractReaderFilter API Methods --

	/**
	 * Allows code to be executed regardless of which {@link #setSource} signature
	 * is called.
	 *
	 * @param source - Lowest common denominator of arguments in the
	 *          {@code setSource} series.
	 * @throws IOException
	 */
	protected void setSourceHelper(final Location source,
		final SCIFIOConfig config) throws IOException
	{
		final Location filterSource = getMetadata() == null ? null : getMetadata()
			.getSourceLocation();

		if (filterSource == null || !filterSource.equals(source)) {
			final Metadata meta = getParent().getMetadata();
			if (wrappedMeta instanceof MetadataWrapper) {
				((MetadataWrapper) wrappedMeta).wrap(meta);
			}
			else {
				wrappedMeta = meta;
			}
		}
	}

	/**
	 * Allows code to be executed regardless of which {@link #openPlane} signature
	 * is called.
	 */
	protected void openPlaneHelper() {

	}

	/**
	 * Allows code to be executed regardless of which {@link #readPlane} signature
	 * is called.
	 */
	protected void readPlaneHelper() {}

	/**
	 * Convenience accessor for the parent's Metadata
	 */
	protected Metadata getParentMeta() {
		return getParent().getMetadata();
	}

	// -- Filter API Methods --

	@Override
	public Class<?> target() {
		return io.scif.Reader.class;
	}

	@Override
	public void setParent(final Object parent) {
		super.setParent(parent);

		final Reader r = (Reader) parent;

		if (metaClass != null) {
			MetadataWrapper wrapper = null;
			try {
				wrappedMeta = wrapper = metaClass.newInstance();
				getContext().inject(wrapper);
				wrapper.wrap(r.getMetadata());
			}
			catch (InstantiationException | IllegalAccessException e) {
				log().error("Failed to create MetadataWrapper of type: " + metaClass,
					e);
			}
		}
		else {
			// No Filter-specific wrapper found
			wrappedMeta = r.getMetadata();
		}
		try {
			setSourceHelper(r.getCurrentLocation(), new SCIFIOConfig());
		}
		catch (final IOException exc) {
			log().error("Failed to create MetadataWrapper of type: " + metaClass,
				exc);
		}
	}

	@Override
	public boolean isCompatible(final Class<?> c) {
		return Reader.class.isAssignableFrom(c);
	}

	// -- Reader API Methods --

	@Override
	public Plane openPlane(final int imageIndex, final long planeIndex)
		throws FormatException, IOException
	{
		return openPlane(imageIndex, planeIndex, new SCIFIOConfig());
	}

	@Override
	public Plane openPlane(final int imageIndex, final long planeIndex,
		final Interval bounds) throws FormatException, IOException
	{
		return openPlane(imageIndex, planeIndex, bounds, new SCIFIOConfig());
	}

	@Override
	public Plane openPlane(final int imageIndex, final long planeIndex,
		final Plane plane) throws FormatException, IOException
	{
		return openPlane(imageIndex, planeIndex, plane, new SCIFIOConfig());
	}

	@Override
	public Plane openPlane(final int imageIndex, final long planeIndex,
		final Plane plane, final Interval bounds) throws FormatException,
		IOException
	{
		return openPlane(imageIndex, planeIndex, plane, bounds, new SCIFIOConfig());
	}

	@Override
	public Plane openPlane(final int imageIndex, final long planeIndex,
		final SCIFIOConfig config) throws FormatException, IOException
	{
		openPlaneHelper();
		return getParent().openPlane(imageIndex, planeIndex, config);
	}

	@Override
	public Plane openPlane(final int imageIndex, final long planeIndex,
		final Interval bounds, final SCIFIOConfig config) throws FormatException,
		IOException
	{
		openPlaneHelper();
		return getParent().openPlane(imageIndex, planeIndex, bounds, config);
	}

	@Override
	public Plane openPlane(final int imageIndex, final long planeIndex,
		final Plane plane, final SCIFIOConfig config) throws FormatException,
		IOException
	{
		openPlaneHelper();
		return getParent().openPlane(imageIndex, planeIndex, plane, config);
	}

	@Override
	public Plane openPlane(final int imageIndex, final long planeIndex,
		final Plane plane, final Interval bounds, final SCIFIOConfig config)
		throws FormatException, IOException
	{
		openPlaneHelper();
		return getParent().openPlane(imageIndex, planeIndex, plane, bounds, config);
	}

	@Override
	public int fileGroupOption(final Location id) throws FormatException,
		IOException
	{
		return getParent().fileGroupOption(id);
	}

	@Override
	public Location getCurrentLocation() {
		return getParent().getCurrentLocation();
	}

	@Override
	public String[] getDomains() {
		return getParent().getDomains();
	}

	@Override
	public DataHandle<Location> getHandle() {
		return getParent().getHandle();
	}

	@Override
	public long getOptimalTileWidth(final int imageIndex) {
		return getParent().getOptimalTileWidth(imageIndex);
	}

	@Override
	public long getOptimalTileHeight(final int imageIndex) {
		return getParent().getOptimalTileHeight(imageIndex);
	}

	@Override
	public void setMetadata(final Metadata meta) throws IOException {
		getParent().setMetadata(meta);

		if (wrappedMeta instanceof MetadataWrapper) ((MetadataWrapper) wrappedMeta)
			.wrap(meta);
		else wrappedMeta = meta;
	}

	@Override
	public Metadata getMetadata() {
		return wrappedMeta;
	}

	@Override
	public void setNormalized(final boolean normalize) {
		getParent().setNormalized(normalize);
	}

	@Override
	public boolean isNormalized() {
		return getParent().isNormalized();
	}

	@Override
	public boolean hasCompanionFiles() {
		return getParent().hasCompanionFiles();
	}

	@Override
	public void setSource(final Location loc) throws IOException {
		getParent().setSource(loc);
		setSourceHelper(loc, new SCIFIOConfig());
	}

	@Override
	public void setSource(final DataHandle<Location> handle) throws IOException {
		getParent().setSource(handle);
		setSourceHelper(handle.get(), new SCIFIOConfig());
	}

	@Override
	public void setSource(final Location loc, final SCIFIOConfig config)
		throws IOException
	{
		getParent().setSource(loc, config);
		setSourceHelper(loc, config);
	}

	@Override
	public void setSource(final DataHandle<Location> handle,
		final SCIFIOConfig config) throws IOException
	{
		getParent().setSource(handle, config);
		setSourceHelper(handle.get(), config);
	}

	@Override
	public void close(final boolean fileOnly) throws IOException {
		getParent().close(fileOnly);
		if (wrappedMeta != null) {
			wrappedMeta.close(fileOnly);
			wrappedMeta = null;
		}
		if (!fileOnly) cleanUp();
	}

	@Override
	public void close() throws IOException {
		close(false);
	}

	@Override
	public Plane readPlane(final DataHandle<Location> s, final int imageIndex,
		final Interval bounds, final Plane plane) throws IOException
	{
		readPlaneHelper();
		return getParent().readPlane(s, imageIndex, bounds, plane);
	}

	@Override
	public Plane readPlane(final DataHandle<Location> s, final int imageIndex,
		final Interval bounds, final int scanlinePad, final Plane plane)
		throws IOException
	{
		readPlaneHelper();
		return getParent().readPlane(s, imageIndex, bounds, scanlinePad, plane);
	}

	@Override
	public long getPlaneCount(final int imageIndex) {
		return getParent().getPlaneCount(imageIndex);
	}

	@Override
	public int getImageCount() {
		return getParent().getImageCount();
	}

	@Override
	public Plane createPlane(final Interval bounds) {
		return getParent().createPlane(bounds);
	}

	@Override
	public Plane createPlane(final ImageMetadata meta, final Interval bounds) {
		return getParent().createPlane(meta, bounds);
	}

	@Override
	public <P extends Plane> P castToTypedPlane(final Plane plane) {
		return getParent().<P> castToTypedPlane(plane);
	}

	// -- Groupable API Methods --

	@Override
	public boolean isSingleFile(final Location id) throws FormatException,
		IOException
	{
		return getParent().isSingleFile(id);
	}

	// -- HasFormat API Methods --

	@Override
	public Format getFormat() {
		return getParent().getFormat();
	}

	@Override
	public String getFormatName() {
		return getParent().getFormatName();
	}

	// -- Contextual API Methods --

	@Override
	public Context getContext() {
		return getParent().getContext();
	}

	@Override
	public void setContext(final Context ctx) {
		getParent().setContext(ctx);
	}

	// -- Helper methods --

	/*
	 * Returns true if this filter's metdata can be cast to
	 * ChannelFillerMetadata
	 */
	protected boolean metaCheck() {
		final Metadata meta = getMetadata();

		return metaClass.isAssignableFrom(meta.getClass());
	}

	/**
	 * Helper method that is always called by the {@link #close} method, if the
	 * {@code fileOnly} flag is false.
	 */
	protected void cleanUp() throws IOException {
		// No-op
	}

	protected Interval planarBounds(final int imageIndex) {
		final Interval bounds = //
			new FinalInterval(getMetadata().get(imageIndex).getAxesLengthsPlanar());
		return bounds;
	}
}