/*-
 * #%L
 * Fiji's plugin for colocalization analysis.
 * %%
 * Copyright (C) 2009 - 2017 Fiji developers.
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #L%
 */
package sc.fiji.coloc.gadgets;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import net.imglib2.RandomAccessibleInterval;
import net.imglib2.algorithm.math.ImageStatistics;
import net.imglib2.type.logic.BitType;
import net.imglib2.type.numeric.RealType;

import sc.fiji.coloc.algorithms.Algorithm;
import sc.fiji.coloc.algorithms.AutoThresholdRegression;
import sc.fiji.coloc.algorithms.InputCheck;
import sc.fiji.coloc.algorithms.MissingPreconditionException;

/**
 * The DataContainer keeps all the source data, jobName, pre-processing results
 * and algorithm results that have been computed. It allows a
 * ResultsHandler implementation to get most of its content
 * and makes the source image and channel information available
 * to a client.
 *
 * @param <T>
 */
public class DataContainer<T extends RealType< T >> {
	// enumeration of different mask types, include labels
	public enum MaskType {
		Regular("ROI"),
		Irregular("mask image"),
		None("none");

		private final String label;

		MaskType(String label) {
			this.label = label;
		}

		public String label() {
			return label;
		}
	}
	// some general image statistics
	private double meanCh1, meanCh2, minCh1, maxCh1, minCh2, maxCh2, integralCh1, integralCh2;
	// The source images that the results are based on
	private RandomAccessibleInterval<T> sourceImage1, sourceImage2;
	// The names of the two source images
	private String sourceImage1Name, sourceImage2Name;
	// The name of the colocalisation run job
	public String jobName;
	// The mask for the images
	private RandomAccessibleInterval<BitType> mask;
	// Type of the used mask
	protected MaskType maskType;
	// the hash code integer of the mask object
	private int maskHash;
	// The channels of the source images that the result relate to
	private int ch1, ch2;
	// The mask's bounding box
	protected long[] maskBBSize = null;
	protected long[] maskBBOffset = null;

	InputCheck<T> inputCheck = null;
	AutoThresholdRegression<T> autoThreshold = null;

	// a list that contains all added algorithms
	List< Algorithm<T> > algorithms = new ArrayList< Algorithm<T> >();

	/**
	 * Creates a new {@link DataContainer} for a specific image channel
	 * combination. We create default thresholds here that are the max and min of
	 * the data type of the source image channels.
	 *
	 * @param src1 The channel one image source
	 * @param src2 The channel two image source
	 * @param ch1 The channel one image channel
	 * @param ch2 The channel two image channel
	 */
	public DataContainer(RandomAccessibleInterval<T> src1,
			RandomAccessibleInterval<T> src2, int ch1, int ch2,
			String name1, String name2) {
		sourceImage1 = src1;
		sourceImage2 = src2;
		sourceImage1Name = name1;
		sourceImage2Name = name2;

		// create a mask that is true at all pixels.
		final long[] dims = new long[src1.numDimensions()];
		src1.dimensions(dims);
		mask = MaskFactory.createMask(dims, true);
		this.ch1 = ch1;
		this.ch2 = ch2;
		// fill mask dimension information, here the whole image
		maskBBOffset = new long[mask.numDimensions()];
		Arrays.fill(maskBBOffset, 0);
		maskBBSize = new long[mask.numDimensions()];
		mask.dimensions(maskBBSize);
		// indicated that there is actually no mask
		maskType = MaskType.None;

		maskHash = mask.hashCode();
		// create a jobName so ResultHandler instances can all use the same object
		// for the job name.
		jobName = "Colocalization_of_" + sourceImage1Name + "_versus_" + sourceImage2Name + "_" + maskHash;

		calculateStatistics();
	}

	/**
	 * Creates a new {@link DataContainer} for a specific set of image and
	 * channel combination. It will give access to the image according to
	 * the mask passed. It is expected that the mask is of the same size
	 * as an image slice. Default thresholds, min, max and mean will be set
	 * according to the mask as well.
	 *
	 * @param src1 The channel one image source
	 * @param src2 The channel two image source
	 * @param ch1 The channel one image channel
	 * @param ch2 The channel two image channel
	 * @param mask The mask to use
	 * @param offset The offset of the ROI in each dimension
	 * @param size The size of the ROI in each dimension
	 * @throws MissingPreconditionException
	 */
	public DataContainer(RandomAccessibleInterval<T> src1,
			RandomAccessibleInterval<T> src2, int ch1, int ch2,
			String name1, String name2, 
			final RandomAccessibleInterval<T> mask,
			final long[] offset, final long[] size)
			throws MissingPreconditionException {
		sourceImage1 = src1;
		sourceImage2 = src2;
		this.ch1 = ch1;
		this.ch2 = ch2;
		sourceImage1Name = name1;
		sourceImage2Name = name2;

		final int numDims = src1.numDimensions();
		maskBBOffset = new long[numDims];
		maskBBSize = new long[numDims];
		final long[] dim = new long[numDims];
		src1.dimensions(dim);
		this.mask = MaskFactory.createMask(dim.clone(), mask);

		// this constructor supports irregular masks
		maskType = MaskType.Irregular;
		adjustRoiOffset(offset, maskBBOffset, dim);
		adjustRoiSize(size, maskBBSize, dim, maskBBOffset);

		maskHash = mask.hashCode();
		// create a jobName so ResultHandler instances can all use the same
		// object for the job name.
		jobName = "Colocalization_of_" + sourceImage1Name + "_versus_" + sourceImage2Name + "_" + maskHash;

		calculateStatistics();
	}

	/**
	 * Creates a new {@link DataContainer} for a specific set of image and
	 * channel combination. It will give access to the image according to
	 * the region of interest (ROI) passed. Default thresholds, min, max and
	 * mean will be set according to the ROI as well.
	 *
	 * @param src1 The channel one image source
	 * @param src2 The channel two image source
	 * @param ch1 The channel one image channel
	 * @param ch2 The channel two image channel
	 * @param offset The offset of the ROI in each dimension
	 * @param size The size of the ROI in each dimension
	 */
	public DataContainer(RandomAccessibleInterval<T> src1,
			RandomAccessibleInterval<T> src2, int ch1, int ch2,
			String name1, String name2,
			final long[] offset, final long size[])
			throws MissingPreconditionException {
		sourceImage1 = src1;
		sourceImage2 = src2;
		sourceImage1Name = name1;
		sourceImage2Name = name2;
		
		final int numDims = src1.numDimensions();
		final long[] dim = new long[numDims];
		src1.dimensions(dim);
		long[] roiOffset = new long[numDims];
		long[] roiSize = new long[numDims];

		adjustRoiOffset(offset, roiOffset, dim);
		adjustRoiSize(size, roiSize, dim, roiOffset);

		// create a mask that is valid everywhere
		mask = MaskFactory.createMask(dim, roiOffset, roiSize);
		maskBBOffset = roiOffset.clone();
		maskBBSize = roiSize.clone();
		// this constructor only supports regular masks
		maskType = MaskType.Regular;

		this.ch1 = ch1;
		this.ch2 = ch2;

		maskHash = mask.hashCode();
		// create a jobName so ResultHandler instances can all use the same
		// object for the job name.
		jobName = "Colocalization_of_" + sourceImage1Name + "_versus_" + sourceImage2Name + "_" + maskHash;

		calculateStatistics();
	}

	protected void calculateStatistics() {
		meanCh1 = ImageStatistics.getImageMean(sourceImage1, mask);
		meanCh2 = ImageStatistics.getImageMean(sourceImage2, mask);
		minCh1 = ImageStatistics.getImageMin(sourceImage1, mask).getRealDouble();
		minCh2 = ImageStatistics.getImageMin(sourceImage2, mask).getRealDouble();
		maxCh1 = ImageStatistics.getImageMax(sourceImage1, mask).getRealDouble();
		maxCh2 = ImageStatistics.getImageMax(sourceImage2, mask).getRealDouble();
		integralCh1 = ImageStatistics.getImageIntegral(sourceImage1, mask);
		integralCh2 = ImageStatistics.getImageIntegral(sourceImage2, mask);
	}

	/**
	 * 	Make sure that the ROI offset has the same dimensionality
	 *	as the image. The method fills it up with zeros if needed.
	 *
	 * @param oldOffset The offset with the original dimensionality
	 * @param newOffset The output array with the new dimensionality
	 * @param dimensions An array of the dimensions
	 * @throws MissingPreconditionException
	 */
	protected void adjustRoiOffset(long[] oldOffset, long[] newOffset, long[] dimensions)
			throws MissingPreconditionException {
		for (int i=0; i<newOffset.length; ++i) {
			if (i < oldOffset.length) {
				if (oldOffset[i] > dimensions[i])
					throw new MissingPreconditionException("Dimension " + i + " of ROI offset is larger than image dimension.");
				newOffset[i] = oldOffset[i];
			} else {
				newOffset[i] = 0;
			}
		}
	}

	/**
	 * Transforms a ROI size array to a dimensionality. The method
	 * fill up with image (dimension - offset in that dimension) if
	 * needed.
	 *
	 * @param oldSize Size array of old dimensionality
	 * @param newSize Output size array of new dimensionality
	 * @param dimensions Dimensions representing the new dimensionality
	 * @param offset Offset of the new dimensionality
	 * @throws MissingPreconditionException
	 */
	protected void adjustRoiSize(long[] oldSize, long[] newSize, long[] dimensions, long[] offset)
			throws MissingPreconditionException {
		for (int i=0; i<newSize.length; ++i) {
			if (i < oldSize.length) {
				if (oldSize[i] > (dimensions[i] - offset[i]))
					throw new MissingPreconditionException("Dimension " + i + " of ROI size is larger than what fits in.");
				newSize[i] = oldSize[i];
			} else {
				newSize[i] = dimensions[i] - offset[i];
			}
		}
	}

	public RandomAccessibleInterval<T> getSourceImage1() {
		return sourceImage1;
	}

	public RandomAccessibleInterval<T> getSourceImage2() {
		return sourceImage2;
	}

	public String getSourceImage1Name() {
		return "Ch1_" + sourceImage1Name;
	}

	public String getSourceImage2Name() {
		return "Ch2_" + sourceImage2Name;
	}

	public String getSourceCh1Name() {
		return sourceImage1Name;
	}

	public String getSourceCh2Name() {
		return sourceImage2Name;
	}

	public String getJobName() {
		return jobName;
	}

	public RandomAccessibleInterval<BitType> getMask() {
		return mask;
	}

	public long[] getMaskBBOffset() {
		return maskBBOffset.clone();
	}

	public long[] getMaskBBSize() {
		return maskBBSize.clone();
	}

	public MaskType getMaskType(){
		return maskType;
	}

	public int getMaskID(){
		return maskHash;
	}

	public int getCh1() {
		return ch1;
	}

	public int getCh2() {
		return ch2;
	}
	public double getMeanCh1() {
		return meanCh1;
	}

	public double getMeanCh2() {
		return meanCh2;
	}

	public double getMinCh1() {
		return minCh1;
	}

	public double getMaxCh1() {
		return maxCh1;
	}

	public double getMinCh2() {
		return minCh2;
	}

	public double getMaxCh2() {
		return maxCh2;
	}

	public double getIntegralCh1() {
		return integralCh1;
	}

	public double getIntegralCh2() {
		return integralCh2;
	}

	public InputCheck<T> getInputCheck() {
		return inputCheck;
	}

	public Algorithm<T> setInputCheck(InputCheck<T> inputCheck) {
		this.inputCheck = inputCheck;
		return inputCheck;
	}

	public AutoThresholdRegression<T> getAutoThreshold() {
		return autoThreshold;
	}

	public Algorithm<T> setAutoThreshold(AutoThresholdRegression<T> autoThreshold) {
		this.autoThreshold = autoThreshold;
		return autoThreshold;
	}
}