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

import io.scif.Format;
import io.scif.FormatException;
import io.scif.Metadata;
import io.scif.SCIFIO;
import io.scif.config.SCIFIOConfig;
import io.scif.util.FormatTools;

import java.io.IOException;

import net.imagej.ImgPlus;
import net.imagej.axis.CalibratedAxis;
import net.imglib2.img.Img;
import net.imglib2.img.array.ArrayImg;
import net.imglib2.img.basictypeaccess.PlanarAccess;
import net.imglib2.img.basictypeaccess.array.ArrayDataAccess;
import net.imglib2.img.basictypeaccess.array.ByteArray;
import net.imglib2.img.basictypeaccess.array.CharArray;
import net.imglib2.img.basictypeaccess.array.DoubleArray;
import net.imglib2.img.basictypeaccess.array.FloatArray;
import net.imglib2.img.basictypeaccess.array.IntArray;
import net.imglib2.img.basictypeaccess.array.LongArray;
import net.imglib2.img.basictypeaccess.array.ShortArray;
import net.imglib2.type.NativeType;
import net.imglib2.type.Type;
import net.imglib2.type.numeric.RealType;
import net.imglib2.type.numeric.integer.ByteType;
import net.imglib2.type.numeric.integer.IntType;
import net.imglib2.type.numeric.integer.ShortType;
import net.imglib2.type.numeric.integer.UnsignedByteType;
import net.imglib2.type.numeric.integer.UnsignedIntType;
import net.imglib2.type.numeric.integer.UnsignedShortType;
import net.imglib2.type.numeric.real.DoubleType;
import net.imglib2.type.numeric.real.FloatType;

import org.scijava.io.location.Location;
import org.scijava.plugin.Plugin;
import org.scijava.service.AbstractService;
import org.scijava.service.Service;
import org.scijava.util.Bytes;

/**
 * Helper methods for converting between SCIFIO and ImgLib2 data structures.
 *
 * @author Stephan Preibisch
 * @author Stephan Saalfeld
 * @author Curtis Rueden
 */
@Plugin(type = Service.class)
public class DefaultImgUtilityService extends AbstractService implements
	ImgUtilityService
{

	// -- Fields --

	private SCIFIO scifio = null;

	// -- ImgUtilityService methods --

	/** Compiles an N-dimensional list of axis lengths from the given reader. */
	@Override
	public long[] getDimLengths(final Metadata m, final int imageIndex,
		final SCIFIOConfig config)
	{

		final long[] dimLengths = m.get(imageIndex).getAxesLengths();

		final ImageRegion region = config.imgOpenerGetRegion();

		for (int i = 0; i < dimLengths.length; i++) {

			if (region != null && i < region.size()) {
				final Range range = region.getRange(m.get(imageIndex).getAxis(i)
					.type());
				if (range != null) {
					dimLengths[i] = range.size();
				}
			}
		}
		return dimLengths;
	}

	@Override
	public long[] getConstrainedLengths(final Metadata m, final int imageIndex,
		final SCIFIOConfig config)
	{
		final long[] lengths = getDimLengths(m, imageIndex, config);

		final ImageRegion r = config.imgOpenerGetRegion();

		if (r != null) {
			// set each dimension length = the number of entries for that axis
			for (final CalibratedAxis t : m.get(0).getAxes()) {
				final Range range = r.getRange(t.type());
				if (range != null) lengths[m.get(0).getAxisIndex(t)] = range.size();
			}
		}

		return lengths;
	}

	/**
	 * @param source - the location of the dataset to assess
	 * @return The number of images in the specified dataset.
	 */
	@Override
	public int getImageCount(final Location source) throws ImgIOException {
		try {
			final Format format = scifio().format().getFormat(source);
			return format.createParser().parse(source).getImageCount();
		}
		catch (FormatException | IOException e) {
			throw new ImgIOException(e);
		}
	}

	/** Obtains planar access instance backing the given img, if any. */
	@Override
	@SuppressWarnings("unchecked")
	public PlanarAccess<ArrayDataAccess<?>> getPlanarAccess(
		final ImgPlus<?> img)
	{
		if (img.getImg() instanceof PlanarAccess) {
			return (PlanarAccess<ArrayDataAccess<?>>) img.getImg();
		}
		return null;
	}

	/** Obtains array access instance backing the given img, if any. */
	@Override
	public ArrayImg<?, ?> getArrayAccess(final ImgPlus<?> img) {
		if (img.getImg() instanceof ArrayImg) {
			return (ArrayImg<?, ?>) img.getImg();
		}
		return null;
	}

	/** Converts SCIFIO pixel type to ImgLib2 Type object. */
	@Override
	public Type<?> makeType(final int pixelType) {
		final Type<?> type;
		switch (pixelType) {
			case FormatTools.UINT8:
				type = new UnsignedByteType();
				break;
			case FormatTools.INT8:
				type = new ByteType();
				break;
			case FormatTools.UINT16:
				type = new UnsignedShortType();
				break;
			case FormatTools.INT16:
				type = new ShortType();
				break;
			case FormatTools.UINT32:
				type = new UnsignedIntType();
				break;
			case FormatTools.INT32:
				type = new IntType();
				break;
			case FormatTools.FLOAT:
				type = new FloatType();
				break;
			case FormatTools.DOUBLE:
				type = new DoubleType();
				break;
			default:
				type = null;
		}
		return type;
	}

	/**
	 * Converts ImgLib2 Type object to SCIFIO pixel type.
	 */
	@Override
	public int makeType(final Object type) throws ImgIOException {
		int pixelType = FormatTools.UINT8;
		if (type instanceof UnsignedByteType) {
			pixelType = FormatTools.UINT8;
		}
		else if (type instanceof ByteType) {
			pixelType = FormatTools.INT8;
		}
		else if (type instanceof UnsignedShortType) {
			pixelType = FormatTools.UINT16;
		}
		else if (type instanceof ShortType) {
			pixelType = FormatTools.INT16;
		}
		else if (type instanceof UnsignedIntType) {
			pixelType = FormatTools.UINT32;
		}
		else if (type instanceof IntType) {
			pixelType = FormatTools.INT32;
		}
		else if (type instanceof FloatType) {
			pixelType = FormatTools.FLOAT;
		}
		else if (type instanceof DoubleType) {
			pixelType = FormatTools.DOUBLE;
		}
		else {
			throw new ImgIOException("Pixel type not supported. " +
				"Please convert your image to a supported type.");
		}

		return pixelType;
	}

	/** Wraps raw primitive array in ImgLib2 Array object. */
	@Override
	public ArrayDataAccess<?> makeArray(final Object array) {
		final ArrayDataAccess<?> access;
		if (array instanceof byte[]) {
			access = new ByteArray((byte[]) array);
		}
		else if (array instanceof char[]) {
			access = new CharArray((char[]) array);
		}
		else if (array instanceof double[]) {
			access = new DoubleArray((double[]) array);
		}
		else if (array instanceof int[]) {
			access = new IntArray((int[]) array);
		}
		else if (array instanceof float[]) {
			access = new FloatArray((float[]) array);
		}
		else if (array instanceof short[]) {
			access = new ShortArray((short[]) array);
		}
		else if (array instanceof long[]) {
			access = new LongArray((long[]) array);
		}
		else access = null;
		return access;
	}

	/**
	 * see isCompressible(ImgPlus)
	 */
	@Override
	public <T extends RealType<T> & NativeType<T>> boolean isCompressible(
		final Img<T> img)
	{
		return isCompressible(ImgPlus.wrap(img));
	}

	/**
	 * Currently there are limits as to what types of Images can be saved. All
	 * images must ultimately adhere to an, at most, five-dimensional structure
	 * using the known axes X, Y, Z, Channel and Time. Unknown axes (U) can
	 * potentially be handled by coercing to the Channel axis. For example, X Y Z
	 * U C U T would be valid, as would X Y Z U T. But X Y C Z U T would not, as
	 * the unknown axis can not be compressed with Channel. This method will
	 * return true if the axes of the provided image can be represented with a
	 * valid 5D String, and false otherwise.
	 */
	@Override
	public <T extends RealType<T> & NativeType<T>> boolean isCompressible(
		final ImgPlus<T> img)
	{

		final CalibratedAxis[] axes = new CalibratedAxis[img.numDimensions()];
		img.axes(axes);

		final long[] axisLengths = new long[5];
		final long[] oldLengths = new long[img.numDimensions()];

		img.dimensions(oldLengths);

		// true if this img contains an axis that will need to be compressed
		boolean foundUnknown = false;

		for (int i = 0; i < axes.length; i++) {
			final CalibratedAxis axis = axes[i];

			switch (axis.type().getLabel().toUpperCase().charAt(0)) {
				case 'X':
				case 'Y':
				case 'Z':
				case 'C':
				case 'T':
					break;
				default:
					if (oldLengths[i] > 1) foundUnknown = true;
			}
		}

		if (!foundUnknown) return false;

		// This ImgPlus had unknown axes of size > 1, so we will check to see if
		// they can be compressed
		final String dimOrder = guessDimOrder(axes, oldLengths, axisLengths);

		return (dimOrder != null);
	}

	@Override
	public String guessDimOrder(final CalibratedAxis[] axes,
		final long[] dimLengths, final long[] newLengths)
	{
		String oldOrder = "";
		String newOrder = "";

		// initialize newLengths to be 1 for simpler multiplication logic later
		for (int i = 0; i < newLengths.length; i++) {
			newLengths[i] = 1;
		}

		// Signifies if the given axis is present in the dimension order,
		// X=0, Y=1, Z=2, C=3, T=4
		final boolean[] haveDim = new boolean[5];

		// number of "blocks" of unknown axes, e.g. YUUUZU = 2
		int contiguousUnknown = 0;

		// how many axis slots we have to work with
		int missingAxisCount = 0;

		// flag to determine how many contiguous blocks of unknowns present
		boolean unknownBlock = false;

		// first pass to determine which axes are missing and how many
		// unknown blocks are present.
		// We build oldOrder to iterate over on pass 2, for convenience
		for (int i = 0; i < axes.length; i++) {
			switch (axes[i].type().getLabel().toUpperCase().charAt(0)) {
				case 'X':
					oldOrder += "X";
					haveDim[0] = true;
					unknownBlock = false;
					break;
				case 'Y':
					oldOrder += "Y";
					haveDim[1] = true;
					unknownBlock = false;
					break;
				case 'Z':
					oldOrder += "Z";
					haveDim[2] = true;
					unknownBlock = false;
					break;
				case 'C':
					oldOrder += "C";
					haveDim[3] = true;
					unknownBlock = false;
					break;
				case 'T':
					oldOrder += "T";
					haveDim[4] = true;
					unknownBlock = false;
					break;
				default:
					oldOrder += "U";

					// dimensions of size 1 can be skipped, and only will
					// be considered in pass 2 if the number of missing axes is
					// greater than the number of contiguous unknown chunks found
					if (dimLengths[i] > 1) {
						if (!unknownBlock) {
							unknownBlock = true;
							contiguousUnknown++;
						}
					}
					break;
			}
		}

		// determine how many axes are missing
		for (final boolean d : haveDim) {
			if (!d) missingAxisCount++;
		}

		// check to see if we can make a valid dimension ordering
		if (contiguousUnknown > missingAxisCount) {
			return null;
		}

		int axesPlaced = 0;
		unknownBlock = false;

		// Flag to determine if the current unknownBlock was started by
		// an unknown of size 1.
		boolean sizeOneUnknown = false;

		// Second pass to assign new ordering and calculate lengths
		for (int i = 0; i < axes.length; i++) {
			switch (oldOrder.charAt(0)) {
				case 'U':
					// dimensions of size 1 have no effect on the ordering
					if (dimLengths[i] > 1 || contiguousUnknown < missingAxisCount) {
						if (!unknownBlock) {
							unknownBlock = true;

							// length of this unknown == 1
							if (contiguousUnknown < missingAxisCount) {
								contiguousUnknown++;
								sizeOneUnknown = true;
							}

							// assign a label to this dimension
							if (!haveDim[0]) {
								newOrder += "X";
								haveDim[0] = true;
							}
							else if (!haveDim[1]) {
								newOrder += "Y";
								haveDim[1] = true;
							}
							else if (!haveDim[2]) {
								newOrder += "Z";
								haveDim[2] = true;
							}
							else if (!haveDim[3]) {
								newOrder += "C";
								haveDim[3] = true;
							}
							else if (!haveDim[4]) {
								newOrder += "T";
								haveDim[4] = true;
							}
						}
						else if (dimLengths[i] > 1 && sizeOneUnknown) {
							// we are in a block of unknowns that was started by
							// one of size 1, but contains an unknown of size > 1,
							// thus was double counted (once in pass 1, once in pass
							// 2)
							sizeOneUnknown = false;
							contiguousUnknown--;
						}
						newLengths[axesPlaced] *= dimLengths[i];
					}
					break;
				default:
					// "cap" the current unknown block
					if (unknownBlock) {
						axesPlaced++;
						unknownBlock = false;
						sizeOneUnknown = false;
					}

					newOrder += oldOrder.charAt(i);
					newLengths[axesPlaced] = dimLengths[i];
					axesPlaced++;
					break;
			}
		}

		// append any remaining missing axes
		// only have to update order string, as lengths are already 1
		for (int i = 0; i < haveDim.length; i++) {
			if (!haveDim[i]) {
				switch (i) {
					case 0:
						newOrder += "X";
						break;
					case 1:
						newOrder += "Y";
						break;
					case 2:
						newOrder += "Z";
						break;
					case 3:
						newOrder += "C";
						break;
					case 4:
						newOrder += "T";
						break;
				}
			}
		}

		return newOrder;
	}

	@Override
	public double decodeWord(final byte[] plane, final int index,
		final int pixelType, final boolean little)
	{
		final double value;
		switch (pixelType) {
			case FormatTools.UINT8:
				value = plane[index] & 0xff;
				break;
			case FormatTools.INT8:
				value = plane[index];
				break;
			case FormatTools.UINT16:
				value = Bytes.toShort(plane, 2 * index, 2, little) & 0xffff;
				break;
			case FormatTools.INT16:
				value = Bytes.toShort(plane, 2 * index, 2, little);
				break;
			case FormatTools.UINT32:
				value = Bytes.toInt(plane, 4 * index, 4, little) & 0xffffffffL;
				break;
			case FormatTools.INT32:
				value = Bytes.toInt(plane, 4 * index, 4, little);
				break;
			case FormatTools.FLOAT:
				value = Bytes.toFloat(plane, 4 * index, 4, little);
				break;
			case FormatTools.DOUBLE:
				value = Bytes.toDouble(plane, 8 * index, 8, little);
				break;
			default:
				value = Double.NaN;
		}
		return value;
	}

	@Override
	public <T> SCIFIOImgPlus<T> makeSCIFIOImgPlus(final Img<T> img) {
		if (img instanceof SCIFIOImgPlus) return (SCIFIOImgPlus<T>) img;
		if (img instanceof ImgPlus) {
			return new SCIFIOImgPlus<>((ImgPlus<T>) img);
		}
		return new SCIFIOImgPlus<>(img);
	}

	// -- Helper Methods --

	private SCIFIO scifio() {
		if (scifio == null) scifio = new SCIFIO(getContext());
		return scifio;
	}
}