package imseProc.dacControl;

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

import javax.xml.stream.events.StartDocument;

import oneLiners.OneLiners;

import org.eclipse.swt.widgets.Composite;

import otherSupport.SettingsManager;
import otherSupport.bufferControl.DirectBufferControl;

import imseProc.aqui.AquisitionSWTController;
import imseProc.core.ImagePipeController;
import imseProc.core.ImgPipe;
import imseProc.core.ImgSink;
import imseProc.core.Triggerable;

import comedi.ComediAsyncReader;
import comedi.ComediAsyncWriter;
import comedi.ComediRawDataStore;
import comedi.ComediRawWriteBuffer;
import comedi.RawStoreChangedNotification;
import comediJNI.ComediDef;

/** Output of waveforms on digital output to control camera, stepper and FLC */
public class DACTimingHanger extends ImgPipe implements ImgSink, Triggerable, RawStoreChangedNotification  {
	//doesn't seem to work on DIO unless you use them all
	public static final int chans[] = new int[]{
		0,1,2,3,4,5,6,7
		//CH_CAM_TRIG, CH_FLC_DRIVE, 
		//CH_STEPPER1_STEP, CH_STEPPER1_DIR, 
		//CH_STEPPER2_STEP, CH_STEPPER2_DIR
	};

	private ComediAsyncWriter comediWriter;
	private DACTimingSWTController swtController;
	
	private DACTimingConfig config;
	private HashMap<String, Object> localMetaDataMap;
	
	/** The output sequence buffer for one frame */
	private ComediRawWriteBuffer rawStore; 
	
	private boolean triggerEnabled = false;
	
	@Override
	public void rawStoreChanged() {
		//Notified of changes in write status
		//all we really have todo here is update the controllers for status
		updateAllControllers();
	}
	
	@Override
	public ImagePipeController createPipeController(Class interfacingClass, Object args[], boolean asSink) {
		if(interfacingClass == Composite.class){
			if(swtController == null){
				swtController = new DACTimingSWTController(this, (Composite)args[0], (Integer)args[1]);
				controllers.add(swtController);
			}
			return swtController;
		}else
			return null;
	}
	
	public double getCameraTotalTimeMS(){
		if(connectedSource == null)
			return -1;
		Number num = (Number) connectedSource.getSeriesMetaData("totalFrameTimeMS");
		if(num == null)
			return -1;
		return num.doubleValue();
	}

	public void setConfig(DACTimingConfig config) {
		this.config = config;
		createData();
		createMetaData();

		updateAllControllers();
	}
	
	private void createData(){
		if(comediWriter != null){
			comediWriter.abort(false);
			comediWriter = null;
		}
		
		int expsPerFrame = (config.flcMode == DACTimingConfig.FLC_INTERLACE ? 2 : 1);
		
		long minPeriodNS = config.stepperPeriodNS * config.nStepsPerFrame + 
						expsPerFrame * (config.exposeHighPeriodNS + config.exposeLowPeriodNS);
		
		if(minPeriodNS > config.framePeriodNS)
			throw new IllegalArgumentException("Frame period ("+config.framePeriodNS+" ns) is shorter than minimum " +
												"period ("+minPeriodNS+" ns) required for exposures and steps");
		
		int nFrames = config.nFramesPerCycle * config.nCycles;
		
		//now make the frame data
		long frameLen = (long)(config.framePeriodNS / config.clockPeriodNS);
		long cycleLen = frameLen * config.nFramesPerCycle + config.nStepsPerCycle * (long)(config.stepperPeriodNS / config.clockPeriodNS);
		long fullLen = cycleLen * config.nCycles;
		
		if(fullLen > Integer.MAX_VALUE)
			throw new IllegalArgumentException("Too much data (>4GB");
		
		ByteBuffer bBuf = DirectBufferControl.allocateDirect((int)(fullLen*4));
		bBuf.order(ByteOrder.LITTLE_ENDIAN);
		IntBuffer iBuf = bBuf.asIntBuffer();
		
		long delayPeriodNS = config.framePeriodNS 
							- (config.exposeHighPeriodNS + config.exposeLowPeriodNS) * expsPerFrame
							- ((long)config.nStepsPerFrame*(long)config.stepperPeriodNS); 
		
		int nExpHigh = (int)(config.exposeHighPeriodNS / config.clockPeriodNS);
		int nExpLow = (int)(config.exposeLowPeriodNS / config.clockPeriodNS);
		int nHalfStep = (int)((long)config.stepperPeriodNS / config.clockPeriodNS / 2);
		int nDelay = (int)(delayPeriodNS / config.clockPeriodNS);
	
		
		boolean frameStepDir = config.frameStepReverse;
	
		for(int iC=0; iC < config.nCycles; iC++) {
			for(int i=0; i < config.nFramesPerCycle; i++) {			
				
				byte val = 0; //this should get inited by the for loop, but eclipse can't tell
				for(int iFLC=0; iFLC < 2; iFLC++){ //low then high			
					//we might only want high or low
					if((iFLC == 0 && config.flcMode == DACTimingConfig.FLC_HIGH) ||
							(iFLC == 1 && config.flcMode == DACTimingConfig.FLC_LOW))
						continue;
					
					val = (byte)(iFLC * (1 << config.CH_FLC_DRIVE) | (1 << config.CH_CAM_TRIG));
					for(int j=0; j < nExpHigh; j++)
						iBuf.put(val);
						
					val = (byte)(iFLC * (1 << config.CH_FLC_DRIVE));
					for(int j=0; j < nExpLow; j++)
						iBuf.put(val);
						
				}
				
				if(i < (config.nFramesPerCycle-1)){
					val |= frameStepDir ? 0 : (1 << DACTimingConfig.CH_DIRECTION[config.frameStepperSelect]); //high appears to be forward atm
					val |= (1 << DACTimingConfig.CH_STEP[config.frameStepperSelect]); //start with step high			
						
					//now the frame steps
					for(int j=0; j < 2*config.nStepsPerFrame; j++){ //low then high
						for(int k=0; k < nHalfStep; k++)
							iBuf.put(val);
						
						val ^= (1 << DACTimingConfig.CH_STEP[config.frameStepperSelect]); //toggle step						
					}
				
					//wait out the rest of the frame (but not if it's the last frame of a cycle)
					for(int j=0; j < nDelay; j++){
						iBuf.put(val);
					}
				}
				
			} //end of cycle
			
			byte val = 0;
			val |= config.cycleStepReverse ? 0 : (1 << DACTimingConfig.CH_DIRECTION[config.cycleStepperSelect]); //high appears to be forward atm
			val |= (1 << DACTimingConfig.CH_STEP[config.cycleStepperSelect]); //start with step high			
					
			//now the cycle steps
			for(int j=0; j < 2*config.nStepsPerCycle; j++){ //low then high
				for(int k=0; k < nHalfStep; k++)
					iBuf.put(val);
				
				val ^= (1 << DACTimingConfig.CH_STEP[config.cycleStepperSelect]); //toggle step						
			}
			
			if(config.alternateFrameStepDir)
				frameStepDir = !frameStepDir;
			
			//now do the missing frame end delay
			for(int j=0; j < nDelay; j++){
				iBuf.put(val);
			}
			
		} 
				
		rawStore = new ComediRawWriteBuffer(bBuf, 1, new ComediDef.Range[chans.length], null, 1);
		rawStore.chans = chans.clone();
		rawStore.periodNS = (int)config.clockPeriodNS;
		
		//Now create
			
	}
	
	/** Make the image sequences traces of the stepper pos, FLC state etc */
	private void createMetaData(){
		long exposureTimeNS[];
		int frameStepperPosExp[], flcPosExp[];
		int frameStepperPosFrame[];
		int cycleStepperPosExp[];
		int cycleStepperPosFrame[];
		long frameTimeNS[];
		int nFrames = config.nCycles * config.nFramesPerCycle;
		
		if(config.flcMode == DACTimingConfig.FLC_INTERLACE){
			exposureTimeNS = new long[2*nFrames];
			frameStepperPosExp = new int[2*nFrames];
			flcPosExp = new int[2*nFrames];				
			frameTimeNS = new long[nFrames];
			frameStepperPosFrame = new int[nFrames];
			for(int iC=0; iC < config.nFramesPerCycle; iC++){
				for(int i=0; i < config.nFramesPerCycle; i++){
					frameTimeNS[i] = i * config.framePeriodNS;
					frameStepperPosFrame[i] = i * config.nStepsPerFrame;
					
					exposureTimeNS[2*i] = i * config.framePeriodNS;
					frameStepperPosExp[2*i] = i * config.nStepsPerFrame;
					flcPosExp[2*i] = 0;
					
					exposureTimeNS[2*i+1] = exposureTimeNS[2*i] + config.exposeHighPeriodNS + config.exposeLowPeriodNS;
					frameStepperPosExp[2*i+1] = frameStepperPosExp[2*i];
					flcPosExp[2*i+1] = 1;
				}
			}
				
		}else{
			exposureTimeNS = new long[nFrames];
			frameStepperPosExp = new int[nFrames];
			flcPosExp = new int[nFrames];				
			for(int i=0; i < nFrames; i++){
				exposureTimeNS[i] = i * config.framePeriodNS;
				frameStepperPosExp[i] = i * config.nStepsPerFrame;
				flcPosExp[i] = (config.flcMode == DACTimingConfig.FLC_HIGH ? 1 : 0);				
			}
			frameTimeNS = exposureTimeNS.clone();
			frameStepperPosFrame = frameStepperPosExp.clone();
				
		}
		
		localMetaDataMap = new HashMap<String, Object>();
		localMetaDataMap.put("dacTiming/exposureTimeNS", exposureTimeNS);
		localMetaDataMap.put("dacTiming/frameStepperPosExp", frameStepperPosExp);
		localMetaDataMap.put("dacTiming/flcPos", flcPosExp);
		localMetaDataMap.put("dacTiming/frameTimeNS", frameTimeNS);
		localMetaDataMap.put("dacTiming/frameStepperPosFrame", frameStepperPosFrame);
	}
	
	public ComediRawDataStore getStore(){ return rawStore; }
	
	public void armTrigger(){
		triggerEnabled = true;
	}
	
	public void start(){
		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"));
				
		comediWriter = new ComediAsyncWriter(comediDev + "_subd" + subdevDIO, subdevDIO, 
												comediDev +"_subd" + subdevCLK, subdevCLK, 
												rawStore, null, 0, config.trigSourcePFI);
		
		//*/
	
		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.pfi", "7"));
			
		comediWriter = new ComediAsyncWriter(comediDev, subdevDIO, subdevCLK, subdevPFI, 
												rawStore, null, 0,												
												config.trigSourcePFI);
//*/
		triggerEnabled = false;
		
		if(connectedSource != null){
			connectedSource.addMetaDataMap(config.toMap("dacTiming/config"));
			connectedSource.addMetaDataMap(localMetaDataMap);
		}
	}
	
	public void abort() {
		if(comediWriter != null)
			comediWriter.abort(false);		
	}

	@Override
	public void triggerStart() {
		super.triggerStart();
		if(triggerEnabled)
			start();
	}

	@Override
	public void triggerAbort() {
		super.triggerAbort();
		abort();
	}
	
	@Override
	public void destroy() {
		
		super.destroy();
		
		if(comediWriter != null)
			comediWriter.abort(false);
	}

	public boolean isActive() { return comediWriter != null && comediWriter.isActive(); }

	@Override
	public void readWriteComplete() { } //don't care

	@Override
	public void imageChanged(int idx) { } //don't care

	public boolean isIdle(){ return false; }
}
