package imseProc.core;

import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

/** Standard image processing pipe for a large set of images
 * with all the same dimensions. */
public abstract class ImgProcPipe extends ImgPipe implements ImgSource, ImgSink {
	private static final int controllerUpdatePeriod = 500;
	
	protected Class<Img> imageClass;
	protected Img imagesOut[];
	
	/** Sum of change IDs of all input images for each output image */
	protected int sourceChangeIDHash[] = new int[0];
	
	protected boolean autoCalc = true;
	
	/** Anything changed that should cause a full reprocess */
	protected boolean settingsChanged = false;
	
	/** How far through the image series doCalc() currently is */
	protected int calcProgress = -1;
	
	/** If true, only the 'selected' image of the source is processed
	 * otherwise they all are and the same number of output iamges is given. */
	protected boolean calcSelectedImageOnly = false;
	
	protected int inWidth, inHeight, outWidth, outHeight;
			
	/**
	 * @param initSet Zero length array of target image type
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public ImgProcPipe(Class imageClass) {
		this.imageClass = imageClass;
		imagesOut = (Img[])Array.newInstance(imageClass, 0);
	}
	
	@SuppressWarnings({ "rawtypes" })
	public ImgProcPipe(Class imageClass, ImgSource source, int selectedIndex) {
		this(imageClass);
		setSource(source);
		setSelectedSourceIndex(selectedIndex);
		calc();
	}
	
	public void calc() {
		IMSEProc.ensureFinalUpdate(this, new Runnable() { @Override public void run() { doCalcScan(); } });
	}
		
	/** Guaranteed to be called only once at a time */
	private void doCalcScan(){
		if(connectedSource == null){
			//source disconnected, wipe everything
			seriesMetaData = new HashMap<String, Object>();
			if(checkOutputSet(0)){
				sourceChangeIDHash = new int[imagesOut.length]; //everything needs to be redone
	        	 notifyImageSetChanged();
			}
			return;
		}
		
		//make sure we give the same meta data map as the source
		//seriesMetaData = connectedSource.getSeriesMetaDataMap();
		seriesMetaData = new HashMap<String, Object>();
		
		boolean settingsHadChanged = settingsChanged;
		settingsChanged = false;
		
		int nImagesIn = connectedSource.getNumImages();
		
		//first we need to find the width/height
		inWidth = -1;
		
		//try the selected image first
		Img img = getSelectedImageFromConnectedSource();
		if(img != null){
			inWidth = img.getWidth();
			inHeight = img.getHeight();			
		}
		if(inWidth < 0){ //otherwise scan them
			for(int i=0; i < nImagesIn; i++){
				img = connectedSource.getImage(i);
				if(img.getWidth() >= 0){
					inWidth = img.getWidth();
					inHeight = img.getHeight();
					break;
				}
			}
		}
		if(inWidth < 0){ //still nothing?
			System.err.println(getClass().getSimpleName() + ": No valid source images.");
			seriesMetaData = new HashMap<String, Object>();
			if(checkOutputSet(0)){
				sourceChangeIDHash = new int[imagesOut.length]; //everything needs to be redone
	        	 notifyImageSetChanged();
			}
			return;
		}
		
		if(checkOutputSet(calcSelectedImageOnly ? 1 : nImagesIn)){
			sourceChangeIDHash = new int[imagesOut.length]; //everything needs to be redone
        	notifyImageSetChanged();
		}
		
		preCalc(settingsHadChanged);
		
		long t0 = System.currentTimeMillis();
outImg:	for(int i=0; i < imagesOut.length; i++){
			calcProgress = i;
			updateAllControllers();
			
			//if settings have changed again, drop out and 
			//ensureFinalUpdate() will pick it up again
			if(settingsChanged) 
				break;
			
			int inIdx[] = sourceIndices(calcSelectedImageOnly ? getSelectedSourceIndex() : i); 
				
			int newChangeHash = 0;	
			Img sourceSet[] = new Img[inIdx.length];
			for(int j=0; j < inIdx.length; j++){
				sourceSet[j] = connectedSource.getImage(inIdx[j]);			
				if(sourceSet[j] == null || sourceSet[j].isDestroyed() || !sourceSet[j].isRangeValid()){
					imagesOut[i].invalidate();
					continue outImg;
				}	
				newChangeHash = 31*newChangeHash + sourceSet[j].getChangeID();				
			}
			
			if(settingsHadChanged || newChangeHash != sourceChangeIDHash[i]){
				attemptCalc(imagesOut[i], sourceSet, settingsHadChanged);
				
				sourceChangeIDHash[i] = newChangeHash;
			}
			
			if((System.currentTimeMillis() - t0) > controllerUpdatePeriod){
				updateAllControllers();
				t0 = System.currentTimeMillis();
			}
		}
				
		calcProgress = -1;
		updateAllControllers();
	}
	
	/** Calls doCalc() with write lock on the output image and catches interruptions */
	protected void attemptCalc(Img imageOut, Img sourceSet[], boolean settingsHadChanged){
		try{
			imageOut.startWriting();
			try{
				if(!doCalc(imageOut, sourceSet, settingsHadChanged)){
					imageOut.invalidate();
					return;
				}
				
			}finally{
				imageOut.endWriting();
			}
		}catch (InterruptedException e) {
			System.err.println("ImgProcPipe.doCalc(): Interrupted waiting for image lock (maybe reading or writing)");
			e.printStackTrace();
			imageOut.invalidate();
		}
		imageOut.imageChanged(false);
	}
	
	protected abstract boolean doCalc(Img imageOut, Img sourceSet[], boolean settingsHadChanged) throws InterruptedException;
	
	/** @return the indices of the source images reuired for a given output image */
	protected abstract int[] sourceIndices(int outIdx);

	/** Reallocate the output set as required based on input set. 
	 * @return true if output set was reallocated */
	protected abstract boolean checkOutputSet(int nImagesIn);
	
	protected void preCalc(boolean settingsHadChanged){ 
		//If our output set is a different length to the input set,
		//we will have to reorder everything in the metadata that looks like 
		//an array to the new frame order
		
		//hmm, unfortunately, trailing images get deleted, so things arn't really the same length as the image sequence anyway
		
		/*int nSrcImages = connectedSource.getNumImages();
		int nOutImages = getNumImages();
		
		if(nOutImages == nSrcImages)
			return; //don't need todo anything, sets are the same size
		
		Map<String, Object> upchainMap = connectedSource.getCompleteSeriesMetaDataMap();
		for(Entry<String, Object> entry : upchainMap.entrySet()){
			String id = entry.getKey();
			Object origArray = entry.getValue();
			
			if(!origArray.getClass().isArray() || Array.getLength(origArray) != nSrcImages)
				continue; //not an array, or not a series aligned with the frames
			
			Object newArray = Array.newInstance(Array.get(origArray, 0).getClass(), nOutImages);
			for(int outIdx=0; outIdx < nOutImages; outIdx++){
				int inIdxs[] = sourceIndices(outIdx);
				int inIdx = inIdxs[inIdxs.length/2]; //roughly the middle one?
				Array.set(newArray, outIdx, Array.get(origArray, inIdx));
			}
		}
		*/
		
	}

	@Override
	public void imageChanged(int idx) { 
		if(autoCalc){
			calc();
		}
 	}
	
	protected void invalidateAllOutput(){
		if(imagesOut == null)
			return;
		for(int i=0; i < imagesOut.length; i++){
			if(imagesOut[i] != null){
				imagesOut[i].invalidate();
				imageRangesDone(i); //notify sinks of the change in image 
			}
		}
	}
	
	public void setAutoCalc(boolean autoCalc) {
		if(!this.autoCalc && autoCalc){
			this.autoCalc = true;
			updateAllControllers();
			calc();			
		}else{
			this.autoCalc = autoCalc;
			updateAllControllers();
		}
	}
	public boolean getAutoCalc() { return this.autoCalc; }

	public void setCalcSelectedImageOnly(boolean calcSelectedImageOnly) {
		this.calcSelectedImageOnly = calcSelectedImageOnly;
		updateAllControllers();
		if(autoCalc)
			calc();
	}
	public boolean isCalcSelectedImageOnly() { return this.calcSelectedImageOnly; }

	@Override
	public abstract ImgProcPipe clone();

	@Override
	public void destroy() {
		for(int i=0; i < imagesOut.length; i++){
			if(imagesOut[i] != null && !imagesOut[i].isDestroyed())
				imagesOut[i].destroy();
		}
		
		//relase out memory just in case someone holds on to us
		//but if we actually null these, doCalc()s still in the calc queue can get caught out
		imagesOut = (Img[])Array.newInstance(imageClass, 0);		
		sourceChangeIDHash = new int[0];
		super.destroy();
	}
	

	public int getCalcProgress() { return calcProgress; }
	
	@Override
	protected void selectedImageChanged() {
		if(calcSelectedImageOnly && autoCalc){
			calc();
		}
	}
	
	@Override
	public void notifySourceChanged() {
		super.notifySourceChanged();
		if(autoCalc)
			calc();
	}
	
	@Override
	public int getNumImages() {
		return (imagesOut != null) ? imagesOut.length : 0;
	}

	@Override
	public Img getImage(int imgIdx) {
		return (imgIdx >= 0 && imgIdx < imagesOut.length) ? imagesOut[imgIdx] : null;
	}
	
	@Override
	public Img[] getImageSet() { return imagesOut; }

	@Override
	public void setSource(ImgSource source) {
		super.setSource(source);
		calc();
	}
	
	@Override
	public boolean isIdle() { 
		return !IMSEProc.isUpdateInProgress(this);
	}
}
