package imseProc.arduinoComm;


import imseProc.core.IMSEProc;
import imseProc.core.ImgSink;
import imseProc.core.ImgSource;
import imseProc.dacControl.DACTimingConfig;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.util.HashMap;
import java.util.Timer;
import java.util.TimerTask;

import binaryMatrixFile.BinaryMatrixWriter;

import oneLiners.OneLiners;
import otherSupport.SettingsManager;
import otherSupport.bufferControl.DirectBufferControl;
import comedi.ComediAsyncWriter;
import comedi.ComediRawDataStore;
import comedi.ComediRawWriteBuffer;
import comedi.RawStoreChangedNotification;
import comediJNI.ComediDef;

/** Sets up sequences in the Arduino and manages the triggering of the fast advance line
 * and the camera via the NI DAC.
 * 
 * @author oliford
 */
public class SequenceCtrl implements RawStoreChangedNotification {
	public static final byte FAST_CMD_ADVANCE = (byte)0xF0;
	public static final byte FAST_CMD_ABORT = (byte)0xF1;
	
	private ArduinoCommHanger proc;
	private SequenceConfig config;
	
	//For control of local DigitalOut which does the timing
	public static final int chans[] = new int[]{ 0,1,2,3,4,5,6,7 }; //this seems to need all 8
	private ComediAsyncWriter comediWriter;
	private ComediRawWriteBuffer rawStore; 
	
	private long lastSequenceEndTime = -1;
	
	/** Is the arduino in sequence */
	private boolean remoteInSequence; 
	
	private boolean softTriggerArmed = false;
	
	public SequenceCtrl(ArduinoCommHanger proc) {
		this.proc = proc;
	}
	
	public void armSoftTrigger(boolean enable, SequenceConfig config){
		this.softTriggerArmed = enable;
		if(enable)
			this.config = config;
	}
	
	public void softTriggerStart(){
		if(softTriggerArmed){		
			initSequence(null);
		}
	}
	
	public void softTriggerAbort(){
		abort();
	}
	
	public void initSequence(SequenceConfig newConfig) {
		if(comediWriter != null){
			comediWriter.abort(false);
			comediWriter = null;
		}
		
		if(newConfig != null){
			this.config = newConfig;
		}
		
		if(config == null){
			System.err.println("initSequence(): No config to start");
		}
		
		calcSequence(config);
		buildDigitalOutData();
		
		//make sure watchdog timeout is longer than sequence (though we can't account for trigger delay)
		int watchdogTimeMS = proc.getWatchdogTimeout();
		if(watchdogTimeMS < (int)(config.totalTimeUS * 1.1 / 1000)){
			proc.setWatchdogTimeout((int)(config.totalTimeUS * 1.1 / 1000));
		}
		//do one last ping first		
		proc.serialComm().send("\\PING");
		try {
			Thread.sleep(100); //hackery hackery
		} catch (InterruptedException e) { }
		
		proc.serialComm().send("SEQ-INIT"
				+ this.config.nFrames + "," 
				+ this.config.flcMode + "," 
				+ this.config.nStepsPerFrame + "," 
				+ (this.config.lineTrigger ? '1' : '0'));
		//this sometimes doesn't respond
			
		try {
			Thread.sleep(100); //hackery hackery
		} catch (InterruptedException e) { }
		startLocal();
		
		
	}
	
	public void calcSequence(SequenceConfig c){
		
		c.exposuresPerFrame = (c.flcMode == SequenceConfig.FLC_INTERLACE) ? 2 : 1;
		
		c.exposureCLKs = c.exposureTimeUS*1000 / c.clockPeriodNS;
		c.postExposure1CLKs = c.postExposureTimeUS*1000 / c.clockPeriodNS;
		c.postExposure2CLKs = c.postExposureTimeUS*1000 / c.clockPeriodNS;
		
		c.expPulseCLKs = Math.max(2, SequenceConfig.EXPOSURE_PULSE_LENGTH_NS / c.clockPeriodNS);
		c.advPulseCLKs = Math.max(2, SequenceConfig.FAST_ADVANCE_PULSE_LENGTH_NS / c.clockPeriodNS);
		
		//For the second or only exposure, the stepping time which starts as soon
		//as the exposure is finished, probably takes longer than the given post exposure time		
		if(c.nStepsPerFrame > 0){
			long steppingTimeCLKs = (c.stepPeriodUS*1000 / c.clockPeriodNS) * (c.nStepsPerFrame + 5); //a little bit more for stabilisation
			if(steppingTimeCLKs > c.postExposure2CLKs){
				c.postExposure2CLKs = steppingTimeCLKs;
			}
		}
		
		c.frameTimeCLKs = ((c.exposuresPerFrame == 2) ? (c.exposureCLKs + c.postExposure1CLKs) : 0)
							+ (c.exposureCLKs + c.postExposure2CLKs);
		
		c.startDelayCLKs = c.startDelayUS*1000/c.clockPeriodNS;
		
		c.fullLenCLKs = c.startDelayCLKs + c.frameTimeCLKs * c.nFrames;
		
		c.startPos = proc.getMotorPos();
		c.endPos = c.startPos + c.nFrames * c.nStepsPerFrame;
				
		//now refill times		
		c.frameTimeUS = c.frameTimeCLKs * (c.clockPeriodNS/1000);
		c.totalTimeUS = c.fullLenCLKs * (c.clockPeriodNS/1000);
	}
	
	private void buildDigitalOutData(){
			
		if(config.fullLenCLKs > Integer.MAX_VALUE)
			throw new IllegalArgumentException("Too much data (>4GB");
				
		double frameTime[] = new double[config.nFrames]; 
		double expTime[] = new double[config.exposuresPerFrame * config.nFrames]; 
		
		ByteBuffer bBuf = DirectBufferControl.allocateDirect((int)(config.fullLenCLKs*4));
		bBuf.order(ByteOrder.LITTLE_ENDIAN);
		IntBuffer iBuf = bBuf.asIntBuffer();
		
		
		for(int j=0; j < config.startDelayCLKs; j++){
			iBuf.put(SequenceConfig.CAM_TRIG_OFF | SequenceConfig.FAST_ADV_OFF);
		}
		
		for(int iF=0; iF < config.nFrames; iF++){
			frameTime[iF] = ((double)iBuf.position()) * config.clockPeriodNS / 1e9;
			
			for(int iE=0; iE < config.exposuresPerFrame; iE++){
				expTime[iF*config.exposuresPerFrame+iE] = ((double)iBuf.position()) * config.clockPeriodNS / 1e9;
				
				//exposure length, with pulse at start (it fires on rising edge) 
				for(int j = 0; j < config.exposureCLKs; j++){
					iBuf.put( ((j < config.expPulseCLKs) ? SequenceConfig.CAM_TRIG_ON : SequenceConfig.CAM_TRIG_OFF) 
							  | SequenceConfig.FAST_ADV_OFF );
				}
				
				//advance pulse and wait for post-exposure length
				long postExpCLKs = (iE < (config.exposuresPerFrame-1)) ? config.postExposure1CLKs : config.postExposure2CLKs;
				for(int j=0; j < postExpCLKs; j++){
					iBuf.put(SequenceConfig.CAM_TRIG_OFF |
							((config.lineTrigger && j < config.advPulseCLKs) ? SequenceConfig.FAST_ADV_ON : SequenceConfig.FAST_ADV_OFF) );
				}
			}
		}
		
		if(iBuf.position() != iBuf.capacity()){
			System.err.println("WARNING: Buffer not filled!");
		}
		
		ImgSource imgSrc = proc.getConnectedSource();
		if(imgSrc != null){
			imgSrc.setSeriesMetaData("/arduinoSeq/frameTime", frameTime);
			imgSrc.setSeriesMetaData("/arduinoSeq/expTime", expTime);
			
			imgSrc.setSeriesMetaData("/time", expTime.clone()); //this is a sensible standard definition of time
		}
		
		/*
		BinaryMatrixWriter bmw = new BinaryMatrixWriter("/tmp/seqOut.bin", 1);
		for(int i=0; i < iBuf.capacity(); i++){
			bmw.writeRow(iBuf.get(i));
		}
		bmw.close();
		*/

		rawStore = new ComediRawWriteBuffer(bBuf, 1, new ComediDef.Range[chans.length], null, 1);
		rawStore.chans = chans.clone();
		rawStore.periodNS = (int)config.clockPeriodNS;
		rawStore.setNotify(this);
	}
	
	/** Start the local DIO sequence */
	private void startLocal(){
		if(comediWriter != null){
			comediWriter.abort(false);
			comediWriter = null;
		}
		
		String comediDev = SettingsManager.defaultGlobal().getProperty("comedi.device", "/dev/comedi0");
		int subdevDIO = OneLiners.mustParseInt(SettingsManager.defaultGlobal().getProperty("comedi.subdevice.dio", "2"));
		int subdevCLK = OneLiners.mustParseInt(SettingsManager.defaultGlobal().getProperty("comedi.subdevice.clock", "11"));
		int subdevPFI = OneLiners.mustParseInt(SettingsManager.defaultGlobal().getProperty("comedi.subdevice.pfio", "7"));
				
		comediWriter = new ComediAsyncWriter(comediDev, subdevDIO, subdevCLK, subdevPFI, 
												rawStore, null, 0,
												config.startTriggerPFI);
				
		ImgSource src = proc.getConnectedSource();
		if(src != null){
			src.addMetaDataMap(config.toMap("arduinoSeq/config"));
		}
	}

	public void abort() {
		fastAbortSequence();
	}
	
	public void fastAbortSequence() {
		//serialComm.send("SEQ-STOP"); //slow way
		proc.serialComm().sendByte(FAST_CMD_ABORT);
		
		if(comediWriter	 != null)
			comediWriter.abort(false);
		
		if(restartRunner != null)
			restartRunner.abort();
	}

	public void requestSequenceRecord() {
		proc.serialComm().send("SEQ-RECD");
	}
	
	public void fastAdvanceSequence() {
		//serialComm.send("SEQ-ADVN");
		proc.serialComm().sendByte(FAST_CMD_ADVANCE);
	}

	public String getStatusString() {
		ComediRawDataStore rawStore = (comediWriter != null && comediWriter.isActive()) ? comediWriter.getStore() : null;
		
		return "Local: " + ((rawStore == null) ? "Inactive" : "Active, written " + 
				(rawStore.nextScanNum + " / " + 
						(((ComediRawWriteBuffer)rawStore).maxCycles * rawStore.buffLength)) ) +
			    ", Arduino: " + (remoteInSequence ? "Active" : "Inactive");
	}
	
	public void setArduinoInSeq(boolean active){ this.remoteInSequence = active; }
	
	public boolean isLocalActive(){ return (comediWriter != null); }
	public boolean isRemoteActive(){ return remoteInSequence; }

	@Override
	public void rawStoreChanged() {
		proc.updateAllControllers();		
	}

	@Override
	public void readWriteComplete() {
		if(restartRunner != null){
			restartRunner.abort();
		}
		if(config.abortTrigAtEnd){			
			proc.getConnectedSource().triggerAbort();			
		}
		if(config.restartPeriodMS > 0){
			restartRunner = new RestartRunner(config.restartPeriodMS);
		}		
	}
	
	private class RestartRunner implements Runnable {
		private Thread thread;
		private boolean abort = false;
		private long delayMS;
		
		public RestartRunner(long delayMS) {
			this.delayMS = delayMS;
			thread = new Thread(this);
			thread.start();
		}
		
		@Override
		public void run() {
			System.out.println("SEQ: RST: Restart runner started, will trigger in "+delayMS+" ms.");
			try {
				Thread.sleep(delayMS/2);
				
				if(comediWriter != null)
					comediWriter.abort(false);
				
				Thread.sleep(delayMS/2);			
				
			} catch (InterruptedException e) { }			
			if(abort){
				System.out.println("SEQ: RST: Restart timer aborted");
				return;
			}			 
			//initSequence(null);
			System.out.println("SEQ: RST: Restart runner triggering software start.");
			proc.getConnectedSource().triggerStart();
		}
		
		public void abort() {
			abort = true;
			System.out.println("SEQ: Aborting restart runner.");
			thread.interrupt();
		}
	}
	
	private RestartRunner restartRunner;

	public void recievedRecordDump(byte[] lastBinaryDump) {
		if(proc.getConnectedSource() == null)
			return;
		
		proc.getConnectedSource().setSeriesMetaData("/arduinoSeq/record/bindump", lastBinaryDump.clone());
		
		ByteBuffer bBuf = ByteBuffer.wrap(lastBinaryDump);
		bBuf.order(ByteOrder.LITTLE_ENDIAN);
		
		int lastFrame = bBuf.getShort(0);
		int nFrames = bBuf.getShort(2);
		int flcMode = bBuf.getShort(4);
		int nExps = (flcMode == 2) ? 2*nFrames : nFrames;
		
		int time[] = new int[nExps];
		int posX[] = new int[nExps];
		int posY[] = new int[nExps];
		
		for(int i=0; i < nExps; i++){
			time[i] = bBuf.getInt(6+i*12);
			posX[i] = bBuf.getInt(6+i*12+4);
			posY[i] = bBuf.getInt(6+i*12+8);
		}
		
		proc.getConnectedSource().setSeriesMetaData("/arduinoSeq/record/time", time);
		proc.getConnectedSource().setSeriesMetaData("/arduinoSeq/record/posX", posX);
		proc.getConnectedSource().setSeriesMetaData("/arduinoSeq/record/posY", posY);
		
	}
	
	
}
