package imseProc.gmds;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.text.DateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;
import java.util.Map.Entry;
import oneLiners.OneLiners;
import signals.gmds.GMDSSignal;
import descriptors.gmds.GMDSSignalDesc;
import mds.GMDSFetcher;
import imseProc.core.ByteBufferImage;
import imseProc.core.DoubleFlatArrayImage;
import imseProc.core.IMSEProc;
import imseProc.core.Img;
import imseProc.core.ImgSink;
import imseProc.core.Triggerable;

public class GMDSSink extends GMDSPipe implements ImgSink, Triggerable {
	
	private boolean autoSaveOnImageChange = false; 
	private boolean autoSaveOnSetChange = false;
	private boolean saveOnAbortTrigger = false;
	private boolean triggerStartOnSaveComplete = false;
	
	private int currentWriting = -1;

	private int imagesInChangeID[] = null;
	
	public GMDSSink() {
		super();
	}

	public GMDSSink(GMDSFetcher gmds) {
		super(gmds);
	}
	
 	@Override
	public void imageChanged(int idx) { 
		if(autoSaveOnImageChange)
			save();
	}

	@Override
	public void notifySourceChanged() {
		if(autoSaveOnSetChange)
			save();
	}	
	
	/** Save the current image set */
	public void save(){
		IMSEProc.ensureFinalUpdate(this, new Runnable() { @Override public void run() { doSequenceSaveCheck(); } }); 
	}
	
	/** The actual save operation, run by the workers */
	private void doSequenceSaveCheck() {
		if(connectedSource == null) return;
		int nImages = connectedSource.getNumImages();
						
		//make sure everyone knows where it has been written
		connectedSource.setSeriesMetaData("gmds/experiment", experiment);
		connectedSource.setSeriesMetaData("gmds/pulse", pulse);
		
		GMDSSignalDesc imgDesc = new GMDSSignalDesc(pulse, experiment, path + "_COUNT");
		GMDSSignal countSig = new GMDSSignal(imgDesc, new int[]{ nImages });
		gmds.writeToCache(countSig);	
		        
		if(imagesInChangeID == null)
			imagesInChangeID = new int[nImages];
		else if(imagesInChangeID.length != nImages){
			imagesInChangeID = Arrays.copyOf(imagesInChangeID, nImages);
		}

		int maxImgNo = -1;
		for(int i=0; i < nImages; i++){
			Img imageIn = connectedSource.getImage(i);
			if(imageIn == null || !imageIn.isRangeValid())
				continue;
			
			maxImgNo = i;
			
			if(imageIn.getChangeID() != imagesInChangeID[i]){
			
				System.out.println("CISSink: Img["+i+"] has changed or been modified, writing to MDS+.");
				
				currentWriting = i;
				updateAllControllers();
				
				writeImage(i, imageIn);
								
				currentWriting = -1;
				updateAllControllers();

			}else{
				System.out.println("CISSink: Img["+i+"] has not changed.");
			}
		}
		
		//rewrite count signal with the max valid images we actually found
		System.out.println("CISSink: Valid images up to #" + maxImgNo + " written.");
		imgDesc = new GMDSSignalDesc(pulse, experiment, path + "_COUNT");
		countSig = new GMDSSignal(imgDesc, new int[]{ maxImgNo + 1 });
		gmds.writeToCache(countSig);	
		
		
		doSaveMetaData();
		
		if(saveOnAbortTrigger){
			pulse++;
			String notes = getNotes(experiment, pulse);
			if("[EMPTY]".equalsIgnoreCase(notes)){
				setNotes(experiment, pulse, 
						"Created by auto-advance at "
						+ DateFormat.getDateTimeInstance(
								DateFormat.SHORT, 
								DateFormat.SHORT).format(new Date()));
			}
			updateAllControllers();
		}
		
		if(triggerStartOnSaveComplete){
			//connectedSource.triggerStart();
		}		
	}
	
	private void doSaveMetaData() {
		
		Map<String, Object> seriesDataMap = connectedSource.getCompleteSeriesMetaDataMap();
		for(Entry<String, Object> entry: seriesDataMap.entrySet()){
			String key = entry.getKey();
			try{
				String containingPath = path.replaceFirst("/[^/]+$","");
				//we store the meta data along the images, in the same path
				//to avoid metadata collisions, paths should be different
				writeMetaData(containingPath + "/seriesData/" + key, entry.getValue());
			}catch(RuntimeException err){
				System.err.println("MDS+ error saving series meta data '"+key+"':");
				err.printStackTrace();
			}
		}
	}
	
	private void writeMetaData(String name, Object data) {
		
		//deal with rank 0 entities
		if(data instanceof Byte) 
			data = new Byte[]{ (Byte)data };
		else if(data instanceof Integer) 
			data = new Integer[]{ (Integer)data };
		else if(data instanceof Short) 
			data = new Short[]{ (Short)data };		
		else if(data instanceof Long) 
			data = new Long[]{ (Long)data };		
		else if(data instanceof Float) 
			data = new Float[]{ (Float)data };		
		else if(data instanceof Double) 
			data = new Double[]{ (Double)data };
		else if(data instanceof Boolean) 
			data = new Boolean[]{ (Boolean)data };
		else if(data instanceof String) 
			data = new String[]{ (String)data };
		
		GMDSSignalDesc sigDesc = new GMDSSignalDesc(pulse, experiment, name);
		
		GMDSSignal sig = new GMDSSignal(sigDesc, data);

		gmds.writeToCache(sig);
	}

	private void writeImage(int index, Img img){
		GMDSSignalDesc imgDesc = new GMDSSignalDesc(pulse, experiment, path + "_" + index);
		
		GMDSSignal imgSig = null;
		
		if(img == null){
			imgSig = new GMDSSignal(imgDesc, new short[0][0]); 
		}else{
			
			try{
				img.startReading();
			}catch(InterruptedException e){
				System.err.println("CISSink: writeImage("+index+") interrupted");
		        return;
			}
			
			try{
				if(img instanceof DoubleFlatArrayImage){
					imgSig = new GMDSSignal(imgDesc, 
									OneLiners.unflatten(
											((DoubleFlatArrayImage) img).getFlatArray(), 
											img.getHeight(), 
											img.getWidth()));
					
				}else if(img instanceof ByteBufferImage && ((ByteBufferImage)img).getBytesPerPixel() == 2){					
				
					short imageData[][] = new short[img.getHeight()][img.getWidth()];
					ByteBuffer bBuff = ((ByteBufferImage)img).getReadOnlyBuffer();
					if(bBuff.position() > 0){
						System.err.println("CISSink WARNING: ByteBufferImage at non-zero position: " + bBuff.position());
						bBuff.position(0);							
					}
					bBuff.order(ByteOrder.LITTLE_ENDIAN);
					ShortBuffer sBuff = bBuff.asShortBuffer();						
						
					for(int iY=0; iY < img.getHeight(); iY++) {
						sBuff.get(imageData[iY]);
					}
					imgSig = new GMDSSignal(imgDesc, imageData);
					
				}else{		
					try{
						img.startReading(); try{
							double imageData[][] = new double[img.getHeight()][img.getWidth()];
							
							for(int iY=0; iY < img.getHeight(); iY++) {
								for(int iX=0; iX < img.getWidth(); iX++) {
										imageData[iY][iX] = img.getPixelValue(iX, iY);
								}
							}
							imgSig = new GMDSSignal(imgDesc, imageData);
						}finally{ img.endReading(); }
					}catch(InterruptedException e){ e.printStackTrace(); }
				}
			}finally{
				img.endReading();
			}
			
		}
		
		if(imgSig != null){			
			gmds.writeToCache(imgSig);
		}
		
	}
		
	public void setData(String experiment, int pulse, String path){
		this.experiment = experiment;
		this.pulse = pulse;
		this.path = path;
		updateAllControllers();
	}
	
	public boolean getAutoSaveOnImageChange(){ return this.autoSaveOnImageChange; } 
	
	public boolean getAutoSaveOnSetChange(){ return this.autoSaveOnSetChange; }
	
	public void setAutoSave(boolean onImageChange, boolean onSetChange, 
			boolean saveOnAbortTrigger, boolean triggerStartOnSaveComplete){
		this.autoSaveOnImageChange = onImageChange; 
		this.autoSaveOnSetChange = onSetChange;
		this.saveOnAbortTrigger = saveOnAbortTrigger;
		this.triggerStartOnSaveComplete = triggerStartOnSaveComplete;
		updateAllControllers();
	}

	@Override
	public int[] getLoadSaveStatus() { 
		return new int[]{ 
				currentWriting,  
				(connectedSource == null) ? 0 : connectedSource.getNumImages(), 
			}; 
	};
	
	@Override
	public void triggerAbort() {
		super.triggerAbort();
		if(saveOnAbortTrigger){
			save();
		}
	}

}
