package imseProc.core;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Set;

/** Anything working on an image or image sequence.
 * 
 *  This implements the update processing code for both image source and image sinks. 
 *  
 *  */
public abstract class ImgPipe {
	
	/* ---------------- Stuff for all pipes ---------------- */
	protected LinkedList<ImagePipeController> controllers = new LinkedList<ImagePipeController>();
	
	public ImgPipe() {
		IMSEProc.addImgPipeInstance(this);
	}
		
	/** Called by the controller, when destroyed */
	public void controllerDestroyed(ImagePipeController controller) {
		controllers.remove(controller);
		//if(controllers.size() == 0)
			//destroy();
	}
	
	/** Creates a controller that can control this source, whos interfacing object will be of the given type */
	public ImagePipeController createPipeController(Class interfacingClass, Object args[], boolean asSink) { return null; }
		
	protected void updateAllControllers(){
		IMSEProc.ensureFinalUpdate(controllers, new Runnable() {
			@Override
			public void run() {
				for(ImagePipeController controller : controllers)
					controller.generalControllerUpdate();	
			}
		});		
	}
	
	@Override
	public String toString() { return getClass().getCanonicalName() + "[" + Integer.toHexString(hashCode()) + "]"; }
	
	public String toShortString() { 
		String hhc = Integer.toHexString(hashCode());
		return getClass().getSimpleName() + "[" + hhc.substring(hhc.length()-3, hhc.length()) + "]"; 
	}
	
	public List<ImagePipeController> getControllers(){ return controllers; }
		
	/* ---------------- Stuff for source ---------------- */
	
	private LinkedList<ImgSink> connectedSinks = new LinkedList<ImgSink>();
	
	protected HashMap<String, Object> seriesMetaData = new HashMap<String, Object>();
		
	private final void assertIsSource(){
		if(!(this instanceof ImgSource))
			throw new RuntimeException("This (" + this.getClass().getSimpleName() + ") isn't an image source");		
	}
	
	public ImagePipeController createSourceController(Class interfacingClass, Object args[]){
		return createPipeController(interfacingClass, args, false);
	}
	
	public void addSink(ImgSink sink){
		assertIsSource();
		if(sink == this)
			throw new RuntimeException("Cannot to connect pipe to itself.");
		connectedSinks.add(sink);
		
	}
	
	public void removeSink(ImgSink sink){
		assertIsSource();
		connectedSinks.remove(sink);		
	}
	
	public List<ImgSink> getConnectedSinks(){
		assertIsSource();
		return connectedSinks;
	}
	
	private String notifyImageSetChangedSyncObj = "notifyImageSetChangedSyncObj";
	protected void notifyImageSetChanged(){
		assertIsSource();
		//we have two of these in this class, so use the list as the hashing object
		IMSEProc.ensureFinalUpdate(notifyImageSetChangedSyncObj, new Runnable() {	
			@Override
			public void run() {
				for(ImgSink sink : connectedSinks)
					sink.notifySourceChanged();
			}
		});		
	}
	
	private String notifyImageChangedSyncObj = "notifyImageChangedSyncObj";
	public void imageRangesDone(int imgIdx){
		assertIsSource();
		final int idx = imgIdx;
		//we have two of these in this class, so use the list as the hashing object
		IMSEProc.ensureFinalUpdate(notifyImageChangedSyncObj, new Runnable() { 
			@Override
			public void run() {
				for(ImgSink sink : connectedSinks)
					sink.imageChanged(idx);
			}
		});             
	}

	/** Returns meta data. If it doesn't exist in this, we look further up the image chain.
	 * Be careful to clone the object if you try to put it back in */
	public Object getSeriesMetaData(String id){
		Object ourMeta = seriesMetaData.get(id);
		if(ourMeta != null)
			return ourMeta;
		
		//no source, then no data
		if(!(this instanceof ImgSink) || connectedSource == null)
			return null;
		
		//otherwise attempt get from our source
		return connectedSource.getSeriesMetaData(id);
	}
	
	/** Also put meta data in this source */
	public void setSeriesMetaData(String id, Object object){
		if((this instanceof ImgSink) && connectedSource != null && connectedSource.getSeriesMetaData(id) == object){
			throw new RuntimeException("Attempted to set object for meta data '"+id+"' into img source '"+this.getClass().getSimpleName()+
							"', but object is the same as from further up the source chain. Clone the object!!");
		}		
		seriesMetaData.put(id, object);
	}
	
	/** Sets series meta data by image index, creating the array as necessary */
	public void setImageMetaData(String id, int imageIndex, Object data){
		assertIsSource();
		Object dataArray = seriesMetaData.get(id);
		if(dataArray == null || !dataArray.getClass().isArray()){
			dataArray = Array.newInstance(data.getClass(), ((ImgSource)this).getNumImages());
			seriesMetaData.put(id, dataArray);
		}
		Array.set(dataArray, imageIndex, data);
	}
	
	/** Gets image meta data by image index */
	public Object getImageMetaData(String id, int imageIndex){
		assertIsSource();
		Object objArray = getSeriesMetaData(id);
		return (objArray != null && objArray.getClass().isArray()) 
						? Array.get(objArray, imageIndex) 
						: null;		
	}
	
	/** Creates a NEW map of all metadata in this and all sources up the chain.
	 * Modifying this won't change anything (modifying the objects in it will break things, don't do it) */	
	public Map<String, Object> getCompleteSeriesMetaDataMap(){
		if((this instanceof ImgSink) && connectedSource != null){
			Map<String, Object> completeMap = connectedSource.getCompleteSeriesMetaDataMap();
			completeMap.putAll(seriesMetaData);
			return completeMap;
		}else{
			return (Map<String, Object>)seriesMetaData.clone();
		}
	}

	/** Add a map of metadata to the source's meta data map */
	public void addMetaDataMap(Map<String, Object> map){
		seriesMetaData.putAll(map);
	}
				
	/* ---------------- Stuff for sink ---------------- */
			
	protected int selectedSourceIndex = -1;
	protected ImgSource connectedSource = null;
	
	/** We can keep the list of images with which we have registered for change notifications.
	 * It should match the source's image array */
	private Img imagesRegisteredWith[] = new Img[0];
	
	private final void assertIsSink(){
		if(!(this instanceof ImgSink))
			throw new RuntimeException("This (" + this.getClass().getSimpleName() + ") isn't an image sink");		
	}
	
	
	public ImagePipeController createSinkController(Class interfacingClass, Object args[]){
		return createPipeController(interfacingClass, args, true);
	}
	
	/** For the sink: Called when the selected source image changes, either 
	 * because the source has changed, or because the selected index has changed */
	protected void selectedImageChanged(){ };

	/** Sets the source (so we need to (un)register with it) */
	public void setSource(ImgSource source){
		assertIsSink();
		if(source == this)
			throw new RuntimeException("Cannot to connect pipe to itself.");
		if(connectedSource != null)
			connectedSource.removeSink((ImgSink)this);
		
		this.connectedSource = source;
		
		if(connectedSource != null){
			connectedSource.addSink((ImgSink)this);
			
		}

		selectedImageChanged();
	}
	
	/** Sink side: Fetches the currently selected image in the connected source 
	 *   for sinks that operate on a single image */
	public Img getSelectedImageFromConnectedSource(){
		assertIsSink();
		if(connectedSource == null || selectedSourceIndex < 0)
			return null;
		return connectedSource.getImage(selectedSourceIndex);
	}
	
	/** Gets the source that this sink is connected to */
	public ImgSource getConnectedSource(){
		assertIsSink();
		return connectedSource;
	}
	
	/** Sets the 'currently selected' image of the source,
	 *   for sinks that operate on a single image */
	public void setSelectedSourceIndex(int index){
		assertIsSink();
		this.selectedSourceIndex = index;
		selectedImageChanged();
	}
	
	/** Get the 'currently selected' image of the source,
	 *   for sinks that operate on a single image */
	public int getSelectedSourceIndex(){
		assertIsSink();
		return this.selectedSourceIndex;
	}
	
	/** defined by ImgSink, called by source to notify us that it has changed */
	public void notifySourceChanged(){
		assertIsSink();
		selectedImageChanged();
	}
	
	public void destroy(){
		if(this instanceof ImgSink){
			if(connectedSource != null){
					connectedSource.removeSink((ImgSink)this);			
			}
			setSource(null);
		}else{
			while(!connectedSinks.isEmpty()){
				connectedSinks.remove().setSource(null);
			}
		}
		
		while(!controllers.isEmpty()){
			controllers.remove().destroy();
		}
	}
	
	public void triggerStart(){
		if(this instanceof ImgSource) {
			for(ImgSink sink : connectedSinks){
				if(sink instanceof Triggerable)
					((Triggerable)sink).triggerStart();
			}
		}
	}
	
	public void triggerAbort(){
		if(this instanceof ImgSource) {
			for(ImgSink sink : connectedSinks){
				if(sink instanceof Triggerable)
					((Triggerable)sink).triggerAbort();
			}
		}
	}
}
