package imseProc.graph;

import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import imseProc.core.IMSEProc;
import imseProc.core.ImagePipeController;
import imseProc.core.Img;
import imseProc.core.ImgPipe;
import imseProc.core.ImgSink;
import imseProc.graph.shapeFit.FuncFitter;

import org.eclipse.swt.widgets.Composite;

import algorithmrepository.Interpolation1D;
import algorithmrepository.LinearInterpolation1D;

/** The image processor part of the GraphUtil (as distinct from the SWT GUI part)
 *  It handles the basic arithmetic and anything that's non GUI specific
 *  All the state should be held here since mulitple controllers might 
 */
public class GraphUtilProcessor extends ImgPipe implements ImgSink {
	public static final String scanDirNames[] = new String[]{ "X", "Y", "T", "MetaSeries" };
	public static final String timebaseNames[] = new String[]{ "Image Num", "MetaSeries" };
	
	private int imagesInChangeID[] = new int[0];
	private int selectedImageChangeID = -1;	
	private boolean selChanged = true;
	private boolean subX0 = false;
	private boolean subY0 = false;
	private String scanMetaSelect = "";
	private boolean includeNaNInAvg = false;
	private boolean idle = true;
		
	/** Might generalise this to an interface later */
	private GraphUtilSWTControl swtGraphControl;
	
	/** Display profile vs x at given y */
	public static final int SCAN_DIR_X = 0;
	/** Display profile vs y at given x */
	public static final int SCAN_DIR_Y = 1;
	/** Display profile vs x averaged over ROI */
	public static final int SCAN_DIR_T = 2;
	/** Display profile of some series meta data */
	public static final int SCAN_META_SERIES = 3;
	
	private int scanType = SCAN_DIR_X;
	
	public static final int TIMEBASE_IMAGE_NUM = 0;
	public static final int TIMEBASE_META_SERIES = 1;
	
	private int timebaseType = TIMEBASE_IMAGE_NUM; 
	private String timebaseMetaSeries = null;
	private String timebaseMetaInterp = null;
		
	private boolean roiAvg = false;

	private int roiX0 = -1, roiY0 = -1, roiW, roiH;
	private int posX = -1, posY = -1;
	private double val = Double.NaN;
	
	private Series imageSeries = null;
	private List<Series> seriesList;
	
	private FuncFitter funcFitter = new FuncFitter();
	private ExtSignalsPlotter signalPlotter = new ExtSignalsPlotter(this);
			
	public GraphUtilProcessor() {
		setSource(null);
	}
	
	@Override
	protected void selectedImageChanged() {
		calc();		
	}
	
	public void setRect(int x0, int y0, int w, int h) {
		this.roiX0 = x0; this.roiY0 = y0;
		this.roiW = w; this.roiH = h;
		selChanged = true;
		//System.out.println("GrapthUtil rect (" + x0 + ", " + y0 + " " + w +" x " + h + ")");
		calc();
	}

	public void setPoint(int x, int y) {
		this.posX = x; this.posY = y;
		selChanged = true;
		//System.out.println("GrapthUtil pos (" + x + ", " + y + "), val = ");// + ((image == null) ? "null" : image.getPixelValue(x, y)));
		calc();
	}
	
	@Override
	public void imageChanged(int idx) {
		calc();
	}
		
	public void calc(){
		idle = false;
		IMSEProc.ensureFinalUpdate(this, new Runnable() { @Override public void run() { doCalc();  } });
	}
	

	
	private void calcImageSeries(){ 
		
		Img image = getSelectedImageFromConnectedSource();
		
		if(image == null || image.isDestroyed()){ //|| (scanType == SCAN_DIR_T && !image.isRangeValid())){ 
			imageSeries = null;
			updateAllControllers();
			return; 
		}
		
		try{ 
			image.startReading(); 
		}catch(InterruptedException e){
			imageSeries = null;
			return; 
		}
		
		try{			
			int w = image.getWidth(), h = image.getHeight();
			int nImages = connectedSource.getNumImages();
			
			boolean anythingChanged = selChanged || funcFitter.needsUpdate() || signalPlotter.needsUpdate();				
			
			//see if we actually need to update
			if(scanType == SCAN_DIR_T){
				//just all images
				if(imagesInChangeID.length != nImages){
					imagesInChangeID = Arrays.copyOf(imagesInChangeID, nImages);
					anythingChanged = true;
				}
				for(int i=0; i < nImages; i++){
					Img imgT = connectedSource.getImage(i);
					int changeID = (imgT != null) ? imgT.getChangeID() : 0;
					if(imagesInChangeID[i] != changeID){
						anythingChanged = true;
						imagesInChangeID[i] = changeID;
					}
				}
			}else{
				//just check the selected one
				int changeID = (image != null) ? image.getChangeID() : 0;
				if(selectedImageChangeID != changeID){
					anythingChanged = true;
					selectedImageChangeID = changeID;
				}
			}
			
			if(imageSeries != null && !anythingChanged)
				return;
			
			imageSeries = new Series();
			
			selChanged = false;
				
			if(!roiAvg){				
				switch(scanType){
					case SCAN_DIR_X:
						imageSeries.name = "scan X";
						imageSeries.x = new double[w];
						imageSeries.data = new double[w];
						for(int x=0; x < w; x++){
							imageSeries.x[x] = x;
							imageSeries.data[x] = image.getPixelValue(x, posY);
						}
						break;
					case SCAN_DIR_Y:
						imageSeries.name = "scan Y";
						imageSeries.x = new double[h];
						imageSeries.data = new double[h];
						for(int y=0; y < h; y++){
							imageSeries.x[y] = y;
							imageSeries.data[y] = image.getPixelValue(posX, y);
						}
						break;
					case SCAN_DIR_T:
						imageSeries.name = "scan T";
						imageSeries.x = new double[nImages];
						imageSeries.data = new double[nImages];
						for(int t=0; t < nImages; t++){
							Img imgT = connectedSource.getImage(t);
							imageSeries.x[t] = t;							
							if(imgT == null || !imgT.isRangeValid() || posX >= imgT.getWidth() || posY >= imgT.getHeight()){
								imageSeries.data[t] = Double.NaN;
							}else{
								imageSeries.data[t] = imgT.getPixelValue(posX, posY);
							}	
						}
						break;
					case SCAN_META_SERIES:
						double ret[][] = scanFromMetaData(connectedSource.getSeriesMetaData(scanMetaSelect));
						imageSeries.name = "Metadata series " + scanMetaSelect;
						imageSeries.x = ret[0];
						imageSeries.data = ret[1];
						break;
						
					default:
						imageSeries.name = "???";
						imageSeries.x = null;
						imageSeries.data = null;
				}
			}else{
				switch(scanType){
					case SCAN_DIR_X:
						imageSeries.name = "scan X ROI";
						imageSeries.x = new double[roiW];
						imageSeries.data = new double[roiW];
						for(int iX=0; iX < roiW; iX++){		
							int n = 0;
							for(int y=roiY0; y < (roiY0 + roiH); y++){
								imageSeries.x[iX] = roiX0 + iX;
								double val =  image.getPixelValue(roiX0 + iX, y);
								if(includeNaNInAvg || !Double.isNaN(val)){
									imageSeries.data[iX] += val;
									n++;
								}
							}
							imageSeries.data[iX] /= n;
						}
						break;
					case SCAN_DIR_Y:
						imageSeries.name = "scan Y ROI";
						imageSeries.x = new double[roiH];
						imageSeries.data = new double[roiH];
						for(int iY=0; iY < roiH; iY++){		
							int n=0;
							for(int x=roiX0; x < (roiX0 + roiW); x++){
								imageSeries.x[iY] = roiY0 + iY;
								double val = image.getPixelValue(x, roiY0 + iY);
								if(includeNaNInAvg || !Double.isNaN(val)){
									imageSeries.data[iY] += val;
									n++;
								}
							}
							imageSeries.data[iY] /= n;
						}
						break;
					case SCAN_DIR_T:
						imageSeries.name = "scan T ROI";
						imageSeries.x = new double[nImages];
						imageSeries.data = new double[nImages];
						for(int t=0; t < nImages; t++){
							Img imgT = connectedSource.getImage(t);
							imageSeries.x[t] = t;
							int n = 0;
							for(int x=roiX0; x < (roiX0 + roiW); x++){
								for(int y=roiY0; y < (roiY0 + roiH); y++){
									if(imgT == null || !imgT.isRangeValid() || x >= imgT.getWidth() || y >= imgT.getHeight()){
										imageSeries.data[t] = Double.NaN;
									}else{
										double val = imgT.getPixelValue(x, y);
										if(includeNaNInAvg || !Double.isNaN(val)){
											imageSeries.data[t] += val;
											n++;
										}
									}
								}
							}
							imageSeries.data[t] /= n;
						}
						break;
					default:
						imageSeries.name = "??? ROI";
						imageSeries.x = null;
						imageSeries.data = null;
						return; 
				}
			}
		
			imageSeries.min = Double.POSITIVE_INFINITY;
			imageSeries.max = Double.NEGATIVE_INFINITY;
			imageSeries.sum = 0;
			for(int i=0;i < imageSeries.data.length; i++){
				if(imageSeries.data[i] > imageSeries.max) imageSeries.max = imageSeries.data[i];
				if(imageSeries.data[i] < imageSeries.min) imageSeries.min = imageSeries.data[i];
				imageSeries.sum += imageSeries.data[i];			
			}
			
			if(posX >= 0 && posY >=0 && posX < image.getWidth() && posY < image.getHeight())
				this.val = image.getPixelValue(posX, posY);
			else
				this.val = Double.NaN;
			
		}catch(IndexOutOfBoundsException err){
			System.err.println("GraphUtil: Index out of bounds on image, ROI=("+roiX0+", "+roiY0+")-("
					+ (roiX0+roiW-1)+", "+(roiY0+roiH-1)+"), POS=("+posX+","+posY+")");
		}finally{
			image.endReading();
		}
		
		//if(scanType != SCAN_DIR_X && scanType != SCAN_DIR_Y){
			
			//a meta sequence entry gives a number for each image 
			if(timebaseMetaInterp != null){
				double ret[][] = scanFromMetaData(connectedSource.getSeriesMetaData(timebaseMetaInterp));
				imageSeries.x = ret[1];
			}
			
			//a meta series array with two entry can also be given, which is used to interpolate
			//the first (or just image number) into something else
			if(timebaseMetaSeries != null){
				double ret[][] = scanFromMetaData(connectedSource.getSeriesMetaData(timebaseMetaSeries));
				if(ret != null && ret.length > 1 && ret[0].length > 1){
					LinearInterpolation1D interp = new LinearInterpolation1D(ret[0], ret[1], Interpolation1D.EXTRAPOLATE_CONSTANT_END_KNOT, 0);
					imageSeries.x = interp.eval(imageSeries.x);
				}
			}
		//}
		
		if(subX0){
			for(int i=imageSeries.x.length-1; i >= 0; i--){
				imageSeries.x[i] -= imageSeries.x[0];
			}
		}
		if(subY0){
			for(int i=imageSeries.data.length-1; i >=0 ; i--){
				imageSeries.data[i] -= imageSeries.data[0];
			}
		}
		
		return;
	}
	
	private void doCalc(){
	
		
		seriesList = new LinkedList<Series>();
		
		calcImageSeries();
		
		seriesList.add(imageSeries);
		
		if(imageSeries != null){
			funcFitter.doFit(imageSeries);
			List<Series> fitSeriesList = funcFitter.getSeriesList();
			if(fitSeriesList != null)
				seriesList.addAll(fitSeriesList);
		}
		
		if(scanType == SCAN_DIR_T){
			List<Series> extSeriesList = signalPlotter.getSeriesList();
			if(extSeriesList != null)
				seriesList.addAll(extSeriesList);
		}
				
		updateAllControllers();
		idle = true;
	}
	
	private double[][] scanFromMetaData(Object seriesMetaData) {
		
		try{
			if(seriesMetaData.getClass().isArray()){
				int n = Array.getLength(seriesMetaData);
				double ret[][] = new double[2][n];
				for(int i=0; i < n; i++){
					fillMetaDataEntry(ret, i, Array.get(seriesMetaData, i));
				}
				return ret;
			}else if(seriesMetaData instanceof Collection<?>){
				Collection<?> oColl = (Collection<?>)seriesMetaData;
				int n = oColl.size();
				double ret[][] = new double[2][n];
				int i=0;
				for(Object o : oColl){
					fillMetaDataEntry(ret, i++, o);
				}
				return ret;
			}else{
				System.err.println("Meta data '"+scanMetaSelect+"' not an array or list.");
				return new double[2][0];
			}
			
		}catch(RuntimeException err){
			System.err.println("Probably conversion error for meta data '"+scanMetaSelect+"': " + err.getMessage());
			return new double[2][0];
		}
	}
	
	private void fillMetaDataEntry(double d[][], int i, Object o){
		if(o instanceof Number){
			d[0][i] = i;
			d[1][i] = ((Number)o).doubleValue();
			
		}else if(o instanceof List<?>){
			List<?> oList = (List<?>)o;
			if(oList.size() > 1){
				d[0][i] = ((Number)oList.get(0)).doubleValue();
				d[1][i] = ((Number)oList.get(1)).doubleValue();
			}else{
				d[0][i] = i;
				d[1][i] = ((Number)oList.get(0)).doubleValue();
			}
		}else if(o.getClass().isArray()){
			
			if(Array.getLength(o) > 1){				
				d[0][i] = Array.getDouble(o, 0);
				d[1][i] = Array.getDouble(o, 1);
			}else{
				d[0][i] = i;
				d[1][i] = Array.getDouble(o, 0);				
			}
		}
				
	}

	@Override
	public ImagePipeController createPipeController(Class interfacingClass, Object args[], boolean asSink) {
		if(interfacingClass == Composite.class){
			if(swtGraphControl == null){
				swtGraphControl = new GraphUtilSWTControl(this, (Composite)args[0], (Integer)args[1]);
				controllers.add(swtGraphControl);
			}
			return swtGraphControl;
		}else
			return null;
	}

	public void setScanType(int scanType, boolean roiAvg, boolean includeNaNInAvg, 
						boolean subtractX0, boolean subtractY0) { 
		this.scanType = scanType;
		this.roiAvg = roiAvg;
		this.subX0 = subtractX0;
		this.subY0 = subtractY0;
		this.includeNaNInAvg = includeNaNInAvg;
		selChanged = true;
		calc(); 
	}
	
	public int getScanDir(){ return scanType; }
	public int getTimebaseType(){ return timebaseType; }
	public boolean isScanROIAvg(){ return roiAvg; }
	public boolean getSubtractX0(){ return subX0; }
	public boolean getSubtractY0(){ return subY0; }
	public boolean getIncludeNaNInAvg(){ return includeNaNInAvg; }
	
	public int getPosX(){ return posX; }
	public int getPosY(){ return posY; }
	public int getROIX0(){ return roiX0; }
	public int getROIY0(){ return roiY0; }
	public int getROIWidth(){ return roiW; }
	public int getROIHeight(){ return roiH; }
	
	public double getValue(){ return val; }
	public double getMin(){ return (imageSeries == null) ? Double.NaN : imageSeries.min; }
	public double getMax(){ return (imageSeries == null) ? Double.NaN : imageSeries.max; }
	public double getMean(){ return (imageSeries == null) ? Double.NaN : (imageSeries.sum / (roiW * roiH)); }
	public double getSum(){ return (imageSeries == null) ? Double.NaN : imageSeries.sum; }
	
	public List<Series> getSeriesList(){ return seriesList; }
	
	public void setMetaDataNameV(int scanType, String metaDataName){ 
		this.scanMetaSelect = metaDataName;
		this.scanType = scanType;
		selChanged = true;
		calc();
	}	

	public void setTimebase(String metaInterpName, String metaSeriesName){
		if(this.timebaseMetaSeries == metaSeriesName &&
				this.timebaseMetaInterp == metaInterpName)
			return;
		
		this.timebaseMetaSeries = metaSeriesName;
		this.timebaseMetaInterp = metaInterpName;
		selChanged = true;
		calc();
	}	
	
	public Set<String> getMetaSeriesNames(){
		if(connectedSource == null)
			return null;
		
		Map<String, Object> map = connectedSource.getCompleteSeriesMetaDataMap();
		return (map != null) ? map.keySet() : null;
		
	}
	
	public FuncFitter getFitter(){ return funcFitter; }
	public ExtSignalsPlotter getSignalPlotter(){ return signalPlotter; }
	
	@Override
	public void destroy() {
		super.destroy();
	}
	
	@Override
	public void updateAllControllers() { //make public for subProcs
		super.updateAllControllers();
	}
	
	@Override
	public boolean isIdle() { return idle;	}
}
