package comedi;


import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.util.List;

import binaryMatrixFile.BinaryMatrixFile;


import comediJNI.Comedi;
import comediJNI.ComediDef;
import comediJNI.ComediJNIException;

/**
 * Async Reader
 * 
 * Sets up an asynchronous command with an attached ComediRawDataStore
 * then continously tries to fill the data store.
 *  
 *  Currently only for digital outputs
 */
public class OLDComediAsyncWriter implements Runnable {
	
	private Thread thread;
	
	private int subdev;
	
	private String deviceFileName;
	private String clockDeviceFileName;
	private int clockSubdev;
	/** If -1, internal trigger is ifred as soon as we have enough data in the buffer */ 
	private int startTriggerSource;
	
	private int chans[];
	private int ranges[];
	private int aref;
	private int periodNS;
	
	private static int chanlist[];// = new int[256];
	
	private boolean death = false;
	
	private ComediRawWriteBuffer data;
	
	public OLDComediAsyncWriter(String deviceFileName, int subdev, 
							String clockDeviceFileName, int clockSubdev, 
							ComediRawWriteBuffer rawStore, int ranges[], int aref, int startTriggerSource){
		this.deviceFileName = deviceFileName;
		this.clockDeviceFileName = clockDeviceFileName;
		this.clockSubdev = clockSubdev;
		this.startTriggerSource = startTriggerSource; 
		this.subdev = subdev;
		this.data = rawStore;
		this.chans = data.chans.clone();
		this.periodNS = data.periodNS;		
		this.ranges = ranges;
		this.aref = aref;		
		
		thread = new Thread(this);
		thread.start();
		thread.setPriority(Thread.MAX_PRIORITY);
	}
	
	public void initChanlist(long dev){

		/*int subdev_flags;
		synchronized (Comedi.syncObj) {
			subdev_flags = Comedi.get_subdevice_flags(dev, subdev);						
		}*/
		
		/*
		int bitsPerSampleDev;
		if( ((subdev_flags & ComediDef.SDF_PACKED) != 0)){
			//packet data, channels are each 1 bit (i.e. it's digital)
			bitsPerSampleDev = 1;
			//nScans = data.buffLength.capacity() / 4;
		}else if((subdev_flags & ComediDef.SDF_LSAMPL) != 0){
			//32-bit data per channel
			bitsPerSampleDev = 32;
			//nScans = buff.capacity() / chans.length / 4;
		}else{
			//16-bit data per channel
			bitsPerSampleDev = 16;
			//nScans = buff.capacity() / chans.length / 2;			
		}
		
		if(data.bitsPerSample != bitsPerSampleDev)
			throw new IllegalArgumentException("Bits per sample of provided buffer ("+data.bitsPerSample+
												") does not match device ("+bitsPerSampleDev+")");
		*/
	
		chanlist = new int[chans.length];
		if(data.bitsPerSample > 1){
			ComediDef.Range range_info[] = new ComediDef.Range[chans.length];
			int maxdata[] = new int[chans.length];

			/* Set up channel list */
			for(int i = 0; i < chans.length; i++){
				chanlist[i] = ComediDef.CR_PACK(chans[i], ranges[i], aref);
				range_info[i] = new ComediDef.Range();
				Comedi.get_range(dev, subdev, chans[i], ranges[i], range_info[i]);
				maxdata[i] = Comedi.get_maxdata(dev, subdev, chans[i]);
				System.out.println("chan " + i + 
						": chanlist = "+chanlist[i] + 
						"\tmin = " + range_info[i].min + 
						"\tmax = " + range_info[i].max + 
						"\tunit = " + range_info[i].unit + 
						"\tmaxdata = " + maxdata[i]);
			}
		}else{
			for(int i = 0; i < chans.length; i++)
				chanlist[i] = chans[i];	
		}

	}
	
	public void abort(boolean waitForDeath){
		death = true;
		if(thread != null && thread.isAlive()){
			thread.interrupt();			
			if(waitForDeath){
				while(thread.isAlive()){
					try{
						Thread.sleep(10);
					}catch(InterruptedException e){ }
				}
			}
		}
	}
	
	public boolean isActive(){ 
		return (thread != null) ? thread.isAlive() : false;
	};
	
	private void checkIOConfig(long dev){
		int stype = Comedi.get_subdevice_type(dev, subdev);
		switch(stype){
		case ComediDef.COMEDI_SUBD_AO:
		case ComediDef.COMEDI_SUBD_DO:
			break;	//that's just fine
		case ComediDef.COMEDI_SUBD_DIO:
			//we need to config the channels as input

			for(int i=0; i < chans.length; i++){
				int ret = Comedi.dio_config(dev, subdev, chans[i], ComediDef.COMEDI_OUTPUT);
				if(ret < 0)
					throw new ComediJNIException("dio_config");
			}
			
			break;
		default:
			throw new ComediException("Subdevice " + subdev + " has type not known to be an output");
		}
	}
	
	private void stopClock(){
		long clockdev = Comedi.open(clockDeviceFileName);
		if(clockdev == 0)
			throw new ComediJNIException("comedi_open(clock device)");			
		
		int ret = Comedi.reset(clockdev, clockSubdev);
		if(ret < 0)
			throw new ComediJNIException("reset");

		ret = Comedi.close(clockdev);
		if(ret < 0)
			System.err.println("Comedi.close(clockdev) failed with " + ret);

	}
	/** Setup a simple output clock */
	private void setupClock(){
		
		int ret;
		int counter_mode;
		final int clock_period_ns = 50;	/* 20MHz clock */
		int up_ticks, down_ticks;
		
		long clockdev = Comedi.open(clockDeviceFileName);
		if(clockdev == 0)
			throw new ComediJNIException("comedi_open(clock device)");			
		
		ret = Comedi.reset(clockdev, clockSubdev);
		if(ret < 0)
			throw new ComediJNIException("reset");

		ret = Comedi.set_gate_source(clockdev, clockSubdev, 0, 0, 
				ComediDef.NI_GPCT_DISABLED_GATE_SELECT | ComediDef.CR_EDGE);	
		if(ret < 0)
			throw new ComediJNIException("set_gate_source");

		ret = Comedi.set_gate_source(clockdev, clockSubdev, 0, 1, ComediDef.NI_GPCT_DISABLED_GATE_SELECT | ComediDef.CR_EDGE);
		if(ret < 0) {
			System.err.println("Failed to set second gate source.  This is expected for older boards " +
								"(e-series, etc.) that don't have a second gate.\n");
		}

		counter_mode = ComediDef.NI_GPCT_COUNTING_MODE_NORMAL_BITS;
		// toggle output on terminal count
		counter_mode |= ComediDef.NI_GPCT_OUTPUT_TC_TOGGLE_BITS;
		// load on terminal count
		counter_mode |= ComediDef.NI_GPCT_LOADING_ON_TC_BIT;
		// alternate the reload source between the load a and load b registers
		counter_mode |= ComediDef.NI_GPCT_RELOAD_SOURCE_SWITCHING_BITS;
		// count down
		counter_mode |= ComediDef.NI_GPCT_COUNTING_DIRECTION_DOWN_BITS;
		// initialize load source as load b register
		counter_mode |= ComediDef.NI_GPCT_LOAD_B_SELECT_BIT;
		// don't stop on terminal count
		counter_mode |= ComediDef.NI_GPCT_STOP_ON_GATE_BITS;
		// don't disarm on terminal count or gate signal
		counter_mode |= ComediDef.NI_GPCT_NO_HARDWARE_DISARM_BITS;
		
		ret = Comedi.set_counter_mode(clockdev, clockSubdev, 0, counter_mode);
		if(ret < 0)
			throw new ComediJNIException("set_counter_mode");

		/* 20MHz clock */
		ret = Comedi.set_clock_source(clockdev, clockSubdev, 0, ComediDef.NI_GPCT_TIMEBASE_1_CLOCK_SRC_BITS, clock_period_ns);
		if(ret < 0)
			throw new ComediJNIException("set_clock_source");

		//up_ticks = (uptimeNS + clock_period_ns / 2) / clock_period_ns;
		up_ticks = (periodNS/2 + clock_period_ns / 2) / clock_period_ns;
		down_ticks = (periodNS + clock_period_ns / 2) / clock_period_ns - up_ticks;
		
		/* set initial counter value by writing to channel 0 */
		ret = Comedi.data_write(clockdev, clockSubdev, 0, 0, 0, down_ticks);
		if(ret < 0)
			throw new ComediJNIException("data_write");

		/* set "load a" register to the number of clock ticks the counter output should remain low
		by writing to channel 1. */
		Comedi.data_write(clockdev, clockSubdev, 1, 0, 0, down_ticks);
		if(ret < 0)
			throw new ComediJNIException("data_write");

		/* set "load b" register to the number of clock ticks the counter output should remain high
		by writing to channel 2 */
		Comedi.data_write(clockdev, clockSubdev, 2, 0, 0, up_ticks);
		if(ret < 0)
			throw new ComediJNIException("data_write");

		ret = Comedi.arm(clockdev, clockSubdev, ComediDef.NI_GPCT_ARM_IMMEDIATE);
		if(ret < 0)
			throw new ComediJNIException("arm");

		ret = Comedi.close(clockdev);
		if(ret < 0)
			System.err.println("Comedi.close(clockdev) failed with " + ret);

	}
	
	private void doCommand(long dev) {
		int ret;
		
		ComediDef.Cmd cmd = new ComediDef.Cmd();
		
		cmd = new ComediDef.Cmd();
		cmd.subdev = subdev;
		cmd.flags = ComediDef.CMDF_WRITE;
		if(startTriggerSource >= 0){
			cmd.start_src = ComediDef.TRIG_EXT;
			cmd.start_arg = ComediDef.NI_EXT_PFI(1) |  ComediDef.CR_EDGE;
		}else{
			cmd.start_src = ComediDef.TRIG_INT;
			cmd.start_arg = 0;			
		}
		cmd.scan_begin_src = ComediDef.TRIG_EXT;
		cmd.scan_begin_arg = ComediDef.NI_CDIO_SCAN_BEGIN_SRC_G0_OUT; // clock source
		cmd.convert_src = ComediDef.TRIG_NOW;
		cmd.convert_arg = 0;
		cmd.scan_end_src = ComediDef.TRIG_COUNT;
		cmd.scan_end_arg = chans.length;
		cmd.stop_src = ComediDef.TRIG_NONE;
		cmd.stop_arg = 0;
		//cmd.stop_src = ComediDef.TRIG_COUNT; //ComediDef.TRIG_NONE;
		//cmd.stop_arg = nScans;
		cmd.chanlist = chanlist;
		cmd.chanlist_len = chanlist.length;
		
		ret = Comedi.command_test(dev, cmd);
		if(ret < 0)
			throw new ComediJNIException("comedi_command_test(#1)="+ret);
	
		ret = Comedi.command_test(dev, cmd);
		if(ret < 0)
			throw new ComediJNIException("comedi_command_test(#2)="+ret);
	
		ret = Comedi.command_test(dev, cmd);
		if(ret != 0)
			throw new ComediJNIException("comedi_command_test(#3)="+ret);
		
		cmd.dump();

		ret = Comedi.command(dev, cmd);
		if(ret < 0)
			throw new ComediJNIException("comedi_command");
	
	}
		
	@Override
	public void run() {
					
		long dev;
		synchronized (Comedi.syncObj) {
			stopClock();
			
			dev = Comedi.open(deviceFileName);
			if(dev == 0)
				throw new ComediJNIException("comedi_open(write device)");
		}
		
		try{

			FileDescriptor fd;
			synchronized (Comedi.syncObj) {
				initChanlist(dev);								
				checkIOConfig(dev);
				doCommand(dev);
				fd = Comedi.getFileDesc(dev);
			}
		
			FileOutputStream writeFileStream = new FileOutputStream(fd);	
			FileChannel fileChan = writeFileStream.getChannel();
			
			if(!fd.valid()){
				throw new ComediException("Bad file descriptor for Comedi device read");
			}
						
			int kBuffSize = Comedi.get_buffer_size(dev, subdev);
			int totalBytesWritten = 0; //load half the buffer first
			boolean softTriggered = false;
			while(!death){
				//write as much as we can (this blocks)
				int bytesWritten = data.writeMoreData(fileChan);
												
				if(bytesWritten < 0){
					throw new ComediException("Write channel stream ended.");
				}
								
				totalBytesWritten += bytesWritten;
				
				//fire soft trigger when enough data is in buffer
				if(startTriggerSource < 0 && !softTriggered && (totalBytesWritten > kBuffSize/2 || bytesWritten == 0)){
					System.out.println("Some data loaded, firing software start trigger.");
					synchronized (Comedi.syncObj) {
						int ret = Comedi.internal_trigger(dev, subdev, 0);
						if(ret < 0)
							throw new ComediJNIException("comedi_internal_trigger");
						setupClock();
						
						softTriggered = true;
					}
				}
				
				if(bytesWritten == 0){
					//If the buffer runs out, the output stops before all the buffer has actually
					//been written, so we need some padding. We can safely just keep writing the last scan
					//until either something external aborts us, or we've written the whole buffer through
					//at which point it ought to be safe to stop
										
					ByteBuffer bBuf = data.getLastScan();
					bBuf.limit(bBuf.capacity());
					int nRunOutScans = kBuffSize/bBuf.capacity();
					System.out.println("Write complete, padding buffer with "+nRunOutScans+" times last scan.");
					for(int i=0; i < nRunOutScans; i++){
						bBuf.rewind();
						fileChan.write(bBuf);
					}
					
					System.out.println("Buffer padding filled. Waiting for buffer to run out.");
								
					try{
						
						while(Comedi.get_buffer_contents(dev, subdev) > 0){							
							Thread.sleep(100);
						}
					}catch(InterruptedException err){ }
					
					System.out.println("Buffer padding ran out. Writing complete.");
					
					return;
				}
				
			}
				
			System.out.println("Write thread terminated externally.");
		}catch(ClosedByInterruptException err){
			System.err.println("Incomplete write aborted.");
			//no stack trace, this one is the normal user stopped it error 
			
		}catch(Exception err){
			System.err.println("Incomplete write aborted:");
			err.printStackTrace();
			
		}finally{
			System.out.print("Writer terminating, cancelling comedi command... ");
			synchronized (Comedi.syncObj) {
				Comedi.cancel(dev, subdev);
				int ret = Comedi.close(dev);
				if(ret < 0)
					System.err.println("ComediAsyncWriter.abort(): Comedi.close returned " + ret);
				
			}				
			System.out.println("Done");
		}
		
		
	}
	
	public ComediRawWriteBuffer getStore() { return data; }

}
