/*
 * #%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;

import io.scif.config.SCIFIOConfig;
import io.scif.util.FormatTools;
import io.scif.util.SCIFIOMetadataTools;

import java.io.IOException;

import net.imagej.axis.Axes;

import org.scijava.io.handle.DataHandle;
import org.scijava.io.handle.DataHandleService;
import org.scijava.io.location.Location;
import org.scijava.plugin.Parameter;
import net.imglib2.FinalInterval;
import net.imglib2.Interval;

/**
 * Abstract superclass of all SCIFIO {@link io.scif.Reader} implementations.
 *
 * @see io.scif.Reader
 * @see io.scif.HasFormat
 * @see io.scif.Metadata
 * @see io.scif.Plane
 * @author Mark Hiner
 */
public abstract class AbstractReader<M extends TypedMetadata, P extends DataPlane<?>>
	extends AbstractGroupable implements TypedReader<M, P>
{

	// -- Fields --
	@Parameter
	private DataHandleService handles;

	/** Metadata for the current image source. */
	private M metadata;

	/** Whether or not to normalize float data. */
	private boolean normalizeData;

	/** List of domains in which this format is used. */
	private String[] domains;

	private final Class<P> planeClass;

	// -- Constructors --

	/** Constructs a reader and stores a reference to its plane type */
	public AbstractReader(final Class<P> planeClass) {
		this.planeClass = planeClass;
	}

	// -- AbstractReader API Methods --

	/**
	 * Helper method to lazily create the domain array for this reader instance,
	 * to avoid constantly re-creating the array.
	 */
	protected abstract String[] createDomainArray();

	// -- Reader API Methods --

	// TODO Merge common Reader and Writer API methods

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

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

	@Override
	public P openPlane(final int imageIndex, final long planeIndex,
		final Plane plane) throws FormatException, IOException
	{
		return openPlane(imageIndex, planeIndex, this.<P> castToTypedPlane(plane));
	}

	@Override
	public P openPlane(final int imageIndex, final long planeIndex,
		final Plane plane, final Interval bounds) throws FormatException,
		IOException
	{
		return openPlane(imageIndex, planeIndex, this.<P> castToTypedPlane(plane),
			bounds);
	}

	@Override
	public P openPlane(final int imageIndex, final long planeIndex,
		final SCIFIOConfig config) throws FormatException, IOException
	{
		final Interval bounds = //
			new FinalInterval(metadata.get(imageIndex).getAxesLengthsPlanar());
		return openPlane(imageIndex, planeIndex, bounds, config);
	}

	@Override
	public P openPlane(final int imageIndex, final long planeIndex,
		final Interval bounds, final SCIFIOConfig config) throws FormatException,
		IOException
	{
		P plane = null;

		try {
			plane = createPlane(bounds);
		}
		catch (final IllegalArgumentException e) {
			throw new FormatException("Image plane too large. Only 2GB of data can " +
				"be extracted at one time. You can workaround the problem by opening " +
				"the plane in tiles; for further details, see: " +
				"http://www.openmicroscopy.org/site/support/faq/bio-formats/" +
				"i-see-an-outofmemory-or-negativearraysize-error-message-when-" +
				"attempting-to-open-an-svs-or-jpeg-2000-file.-what-does-this-mean", e);
		}

		return openPlane(imageIndex, planeIndex, plane, bounds, config);
	}

	@Override
	public Plane openPlane(final int imageIndex, final long planeIndex,
		final Plane plane, final SCIFIOConfig config) throws FormatException,
		IOException
	{
		return openPlane(imageIndex, planeIndex, this.<P> castToTypedPlane(plane),
			config);
	}

	@Override
	public Plane openPlane(final int imageIndex, final long planeIndex,
		final Plane plane, final Interval bounds, final SCIFIOConfig config)
		throws FormatException, IOException
	{
		return openPlane(imageIndex, planeIndex, this.<P> castToTypedPlane(plane),
			bounds, config);
	}

	@Override
	public Location getCurrentLocation() {
		return metadata == null ? null : metadata.getSourceLocation();
	}

	@Override
	public String[] getDomains() {
		if (domains == null) {
			domains = createDomainArray();
		}
		return domains;
	}

	@Override
	public DataHandle<Location> getHandle() {
		return metadata == null ? null : metadata.getSource();
	}

	@Override
	public long getOptimalTileWidth(final int imageIndex) {
		return metadata.get(imageIndex).getAxisLength(Axes.X);
	}

	@Override
	public long getOptimalTileHeight(final int imageIndex) {
		final int bpp = FormatTools.getBytesPerPixel(metadata.get(imageIndex)
			.getPixelType());

		final long width = metadata.get(imageIndex).getAxisLength(Axes.X);
		final long rgbcCount = metadata.get(imageIndex).getAxisLength(Axes.CHANNEL);

		final long maxHeight = (1024 * 1024) / (width * rgbcCount * bpp);
		return Math.min(maxHeight, metadata.get(imageIndex).getAxisLength(Axes.Y));
	}

	@Override
	public void setMetadata(final io.scif.Metadata meta) throws IOException {
		setMetadata(SCIFIOMetadataTools.<M> castMeta(meta));
	}

	@Override
	public M getMetadata() {
		return metadata;
	}

	@Override
	public void setNormalized(final boolean normalize) {
		normalizeData = normalize;
	}

	@Override
	public boolean isNormalized() {
		return normalizeData;
	}

	@Override
	public boolean hasCompanionFiles() {
		return false;
	}

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

	@Override
	public void setSource(final DataHandle<Location> stream) throws IOException {
		setSource(stream, new SCIFIOConfig());
	}

	@SuppressWarnings("resource")
	@Override
	public void setSource(final Location loc, final SCIFIOConfig config)
		throws IOException
	{

		// check if the same location is set again
		if (getCurrentLocation() != null && getCurrentLocation().equals(loc)) {
			if (getHandle() != null) {
				// only need to rewind the handle
				getHandle().seek(0);
			}
			return;
		}

		// new location
		close();

		DataHandle<Location> stream = null;
		// setting a new source
		try {
			stream = config.bufferedReadingEnabled() ? handles.readBuffer(loc)
				: handles.create(loc);
			if (stream == null) {
				// loc only
				setMetadata(getFormat().createParser().parse(loc, config));
			}
			else {
				setMetadata(getFormat().createParser().parse(stream, config));
				setSource(stream);
			}
		}
		catch (final FormatException e) {
			if (stream != null) {
				stream.close();
			}
			throw new IOException(e);
		}
	}

	@Override
	public void setSource(final DataHandle<Location> handle,
		final SCIFIOConfig config) throws IOException
	{
		final Location currentSource = getCurrentLocation();
		final Location newSource = handle.get();
		if (metadata != null && (currentSource == null || newSource == null ||
			!currentSource.equals(newSource))) close();

		if (metadata == null) {
			try {
				@SuppressWarnings("unchecked")
				final M meta = (M) getFormat().createParser().parse(handle, config);
				setMetadata(meta);
			}
			catch (final FormatException e) {
				throw new IOException(e);
			}
		}
	}

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

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

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

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

	@Override
	public <T extends Plane> T castToTypedPlane(final Plane plane) {
		if (!planeClass.isAssignableFrom(plane.getClass())) {
			throw new IllegalArgumentException("Incompatible plane types. " +
				"Attempted to cast: " + plane.getClass() + " to: " + planeClass);
		}

		@SuppressWarnings("unchecked")
		final T p = (T) plane;
		return p;
	}

	// -- TypedReader API --

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

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

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

	@Override
	public void setMetadata(final M meta) throws IOException {
		if (metadata != null && metadata != meta) {
			close();
		}

		if (metadata == null) metadata = meta;
	}

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

	@Override
	public P readPlane(final DataHandle<Location> s, final int imageIndex,
		final Interval bounds, final int scanlinePad, final P plane)
		throws IOException
	{
		final int bpp = FormatTools.getBytesPerPixel(metadata.get(imageIndex)
			.getPixelType());

		final byte[] bytes = plane.getBytes();
		final int xIndex = metadata.get(imageIndex).getAxisIndex(Axes.X);
		final int yIndex = metadata.get(imageIndex).getAxisIndex(Axes.Y);
		if (SCIFIOMetadataTools.wholePlane(imageIndex, metadata, bounds) &&
			scanlinePad == 0)
		{
			s.read(bytes);
		}
		else if (SCIFIOMetadataTools.wholeRow(imageIndex, metadata, bounds) &&
			scanlinePad == 0)
		{
			if (metadata.get(imageIndex).getInterleavedAxisCount() > 0) {
				int bytesToSkip = bpp;
				bytesToSkip *= bounds.max(xIndex);
				int bytesToRead = bytesToSkip;
				for (int i = 0; i < bounds.numDimensions(); i++) {
					if (i != xIndex) {
						if (i == yIndex) {
							bytesToSkip *= bounds.min(i);
						}
						else {
							bytesToSkip *= bounds.max(i);
						}
						bytesToRead *= bounds.max(i);
					}
				}
				s.skip(bytesToSkip);
				s.read(bytes, 0, bytesToRead);
			}
			else {
				final int rowLen = (int) (bpp * bounds.max(xIndex));
				final int h = (int) bounds.max(yIndex);
				final int y = (int) bounds.min(yIndex);
				long c = metadata.get(imageIndex).getAxisLength(Axes.CHANNEL);
				if (c <= 0 || !metadata.get(imageIndex).isMultichannel()) c = 1;
				for (int channel = 0; channel < c; channel++) {

					s.skipBytes(y * rowLen);
					s.read(bytes, channel * h * rowLen, h * rowLen);
					if (channel < c - 1) {
						// no need to skip bytes after reading final channel
						s.skipBytes((int) (metadata.get(imageIndex).getAxisLength(Axes.Y) -
							y - h) * rowLen);
					}
				}
			}
		}
		else {
			final int scanlineWidth = (int) metadata.get(imageIndex).getAxisLength(
				Axes.X) + scanlinePad;
			if (metadata.get(imageIndex).getInterleavedAxisCount() > 0) {
				long planeProduct = bpp;
				for (int i = 0; i < bounds.numDimensions(); i++) {
					if (i != xIndex && i != yIndex) planeProduct *= metadata.get(
						imageIndex).getAxisLength(i);
				}
				int bytesToSkip = scanlineWidth * (int) planeProduct;
				s.skipBytes((int) bounds.min(yIndex) * bytesToSkip);

				bytesToSkip = bpp;
				int bytesToRead = bytesToSkip;
				bytesToRead *= bounds.max(xIndex);
				bytesToRead *= planeProduct;
				bytesToSkip *= bounds.min(xIndex);
				bytesToSkip *= planeProduct;

				for (int row = 0; row <= bounds.max(yIndex); row++) {
					s.skipBytes(bytesToSkip);
					s.read(bytes, row * bytesToRead, bytesToRead);
					if (row < bounds.max(yIndex)) {
						// no need to skip bytes after reading final row
						s.skipBytes((int) (planeProduct * (scanlineWidth - bounds.dimension(
							xIndex))));
					}
				}
			}
			else {
				final long c = metadata.get(imageIndex).getAxisLength(Axes.CHANNEL);

				final int w = (int) bounds.max(xIndex);
				final int h = (int) bounds.max(yIndex);
				final int x = (int) bounds.min(xIndex);
				final int y = (int) bounds.min(yIndex);
				for (int channel = 0; channel < c; channel++) {
					s.skipBytes(y * scanlineWidth * bpp);
					for (int row = 0; row < h; row++) {
						s.skipBytes(x * bpp);
						s.read(bytes, channel * w * h * bpp + row * w * bpp, w * bpp);
						if (row < h - 1 || channel < c - 1) {
							// no need to skip bytes after reading final row of
							// final channel
							s.skipBytes(bpp * (scanlineWidth - w - x));
						}
					}
					if (channel < c - 1) {
						// no need to skip bytes after reading final channel
						s.skipBytes(scanlineWidth * bpp * (int) (metadata.get(imageIndex)
							.getAxisLength(Axes.Y) - y - h));
					}
				}
			}
		}
		return plane;
	}

	@Override
	public Class<P> getPlaneClass() {
		return planeClass;
	}

	// -- HasSource Format API --

	@Override
	public void close(final boolean fileOnly) throws IOException {
		if (metadata != null) metadata.close(fileOnly);

		if (!fileOnly) {
			metadata = null;
		}
	}
}