package imseProc.sensicam;

import imseProc.core.ByteBufferImage;


import imseProc.core.IMSEProc;
import imseProc.core.ImagePipeController;
import imseProc.core.Img;
import imseProc.core.ImgPipe;
import imseProc.core.ImgSource;
import imseProc.core.Triggerable;
import imseProc.noiseGen.NoiseGenSWTControl;

import java.lang.reflect.Array;
import java.nio.ByteBuffer;
import java.util.Arrays;

import org.eclipse.swt.widgets.Composite;

import sensicamJNI.CamTypes;
import sensicamJNI.SenbufDev;
import sensicamJNI.SencamDef;
import sensicamJNI.Sensicam;
import sensicamJNI.SensicamException;

/** Sensicam Image Source. 
 * All the actual driver'ey code is in the thread in SensicamCapture
 *  
 * @author oliford
 */
public class SensicamSource extends ImgPipe implements ImgSource, Triggerable {
	
	private ByteBufferImage images[];
	
	private int nCaptured;
	
	/** The image capturer, we should only have one of these */  
	private SensicamCapture capture;
	
	/** The config used/to be used to capture the images in this source */
	private SensicamConfig config;
	
	private boolean autoExposure = false;
	private SensicamConfig triggerConfig;
	
	public SensicamSource(SensicamCapture capture, SensicamConfig config) {
		this.capture = capture;
		this.config = (config != null) ? config : new SensicamConfig();
	}
	
	public SensicamSource() {
		capture = new SensicamCapture();
		config = new SensicamConfig();
	}

	@Override	
	public int getNumImages() { return images == null ? 0 : images.length; }

	@Override
	public Img getImage(int imgIdx) {		
		if(images != null && imgIdx >= 0 && imgIdx < images.length )
			return images[imgIdx];
		else
			return null;
	}
	
	@Override
	public Img[] getImageSet() { return images; }
		
	/** Called by SensicamCapture. Don't take long over this! */
	public void newImageArray(ByteBufferImage images[]){
		this.images = images;
		notifyImageSetChanged();
		updateAllControllers();
	}

	/** Called by SensicamCapture. Don't take long over this! */
	public void imageCaptured(int imageIndex) {

		images[imageIndex].imageChanged(true);
		if(imageIndex >= nCaptured)
			nCaptured = imageIndex+1;
		
		updateAllControllers();
	}
	
	private long lastAutoExposure = 0;
	private long autoExposureMinPeriod = 50; //ms
	private double autoExposureMinLevel = 0.6;

	private double maxSum = 0;
	private int samplesInSum;

	@Override
	public void imageRangesDone(int imgIdx) {               
		super.imageRangesDone(imgIdx);

		if(!autoExposure)
			return;

		//we can only do this if the exposure and the range information match.
		// Any overwriting of the image by SensicamCapture after calcRanges() did it's thing.
		// will have marked the image invalid.
		Img img = images[imgIdx];
	
		double max = img.getMax();
		boolean rangeValid = img.isRangeValid(); 
		Integer exp = (Integer)getImageMetaData(SensicamCapture.EXPOSURE_METADATA_NAME, imgIdx);
		
		if(Double.isNaN(maxSum) || !rangeValid){
			System.out.println("AutoExposure: Rejected image due to invalid range information.\n");					
			return;
		}		
		
		if(exp != config.exposureMS){
			System.out.println("AutoExposure: Rejected image from an old exposure setting: Img="+exp+", Current="+config.exposureMS);
				return; //reject because we already made a change since this image was recorded
		}
		
		maxSum += max;
		samplesInSum++;
			
		long now = System.currentTimeMillis();
		if((now - lastAutoExposure) > (autoExposureMinPeriod + 2 * config.exposureMS)){
			double saturationVal = img.getMaxPossibleValue();
			double currentLevel = (maxSum / samplesInSum) / saturationVal;
			
			//double currentExp = config.exposureMS + (config.exposureNS / 1e6);
			double currentExp = exp + (config.exposureNS / 1e6);
			
			double targetLevel = 1.0 - 0.5*(1.0 - autoExposureMinLevel);
			
			//currentVal = actualLight * currentExp;
			//actualLight = currentLevel / currentExp; 
			
			double targetExp = Double.NaN;
			
			if(currentLevel >= 0.99){
				targetExp = 0.6 * currentExp; //try to undershoot to get a value quickly
			}else if(currentLevel <= autoExposureMinLevel){
				double r = (targetLevel / currentLevel);
				targetExp = currentExp * Math.min(r, 3.0);
			}
			
			if(!Double.isNaN(targetExp)){
				System.out.println("AutoExposure: Change, \n" +
						"samplesInSum = " + samplesInSum + "\n" +
						"saturationVal = " + saturationVal + "\n" +
						"currentLevel = " + currentLevel + "\n" +
						"targetLevel = " + targetLevel + "\n" +
						"currentExp = " + currentExp + "\n" +
						"targetExp = " + targetExp + "\n");
				
				int targetExpMS = (int)targetExp;
				if(config.exposureMS != targetExpMS){
					config.exposureMS = targetExpMS; 
					updateAllControllers();
					capture.inCaptureModifyConfig(config);
				}
			}
			
			lastAutoExposure = System.currentTimeMillis();
			maxSum = 0;
			samplesInSum = 0;
		}	
	}
	
	@Override
	public SensicamSource clone() {
		return new SensicamSource(capture, config.clone());
	}

	public void startCapture(SensicamConfig config){
		this.config = config;
		this.nCaptured = 0;
		
		lastAutoExposure = System.currentTimeMillis();
		
		capture.startCapture(this, config, images);
	}
	
	public void modifyConfig(SensicamConfig cfg){
		if(isCapturing()){
			capture.inCaptureModifyConfig(cfg);
			this.config = cfg;
		}
	}
	
	public void abortCapture(){ capture.stopCapture(false); }
	
	public String getSourceStatus(){ return nCaptured + " of " + config.nImagesToCapture + " captured. S/W Trigger " + ((triggerConfig == null) ? "not armed" : "armed"); }	

	public String getCaptureStatus(){ return capture.getStatus(); }
	
	public String getCCDInfo(){ return capture.getCCDInfo(); }
	
	public boolean isCapturing(){ return capture.isCapturing(); }
	
	public SensicamConfig getConfig() { return config;	}
	
	public void configChanged() { 
		updateAllControllers();
	}
	
	
	@Override @SuppressWarnings("rawtypes")
	public ImagePipeController createPipeController(Class interfacingClass, Object args[], boolean asSink) {
		ImagePipeController controller = null;
		if(interfacingClass == Composite.class){
			controller = new SensicamSWTControl((Composite)args[0], (Integer)args[1], this);
			controllers.add(controller);
		}
		return controller;
	}

	public void statusChanged() {
		new Thread(new Runnable() {
			@Override
			public void run() {
				updateAllControllers();
			}
		}).start();
	}

	public void setAutoExposure(boolean enable){ this.autoExposure = enable; }
	public boolean getAutoExposure(){ return this.autoExposure; }

	public void armTrigger(SensicamConfig cfg) {
		triggerConfig = cfg;
	}
	
	@Override
	public void triggerStart() {		
		if(triggerConfig != null){
			startCapture(triggerConfig);
		}
		
		super.triggerStart(); //and trigger all the sinks
	}
	
	public void triggerAbort() {
		abortCapture();
		super.triggerAbort();		
	}

	public void releaseMemory() {
		capture.stopCapture(true);

		if(images != null){
			for(int i=0; i < images.length; i++){
				if(images[i] != null)
					images[i].destroy();
			}
		}
		System.gc();
		images = null;
		
		notifyImageSetChanged();
		updateAllControllers();
	}


	@Override
	public boolean isIdle() { return capture == null || !capture.isCapturing(); }
}
