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.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 analogue inputs
 */
public class ComediAsyncReader implements Runnable {
	
	private Thread thread;
	
	private long dev;
	
	private int subdev;
	
	private int nScans = -1;	
	private int chans[];
	private int ranges[];
	private int aref;
	private int periodNS;
	
	private static int chanlist[];// = new int[256];
	
	private boolean death = false;
	
	private ComediRawReadBuffer data;
	
	public ComediAsyncReader(String deviceFileName, int subdev, int chans[], int ranges[], int aref, int nScans, int periodNS, int bufLenInScans){
		this.subdev = subdev;
		this.chans = chans;		
		this.ranges = ranges;
		this.aref = aref;
		this.periodNS = periodNS;
		this.nScans = nScans;
		
		synchronized (Comedi.syncObj) {
			this.dev = Comedi.open(deviceFileName);
			if(dev == 0)
				throw new ComediJNIException("comedi_open");			
		}
		
		initChanlistAndBuffer(bufLenInScans);
		
		thread = new Thread(this);
		thread.start();
		thread.setPriority(Thread.MAX_PRIORITY);
	}
	
	public void initChanlistAndBuffer(int bufLenInScans){
		
		chanlist = new int[chans.length];
		ComediDef.Range range_info[] = new ComediDef.Range[chans.length];
		int maxdata[] = new int[chans.length];

		// Print numbers for clipped inputs
		synchronized (Comedi.syncObj) {
			Comedi.set_global_oor_behavior(ComediDef.COMEDI_OOR_NUMBER);
		}

		/* 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]);
		}

		int subdev_flags;
		synchronized (Comedi.syncObj) {
			subdev_flags = Comedi.get_subdevice_flags(dev, subdev);						
		}

		int bitsPerSample = ((subdev_flags & ComediDef.SDF_PACKED) != 0) ? 1 : 
							(((subdev_flags & ComediDef.SDF_LSAMPL) != 0) ? 32 : 16);
		
		data = new ComediRawReadBuffer(bufLenInScans, bitsPerSample, range_info, maxdata);
		data.chans = chans.clone();
		data.periodNS = periodNS;
		
	}
	
	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(){
		int stype = Comedi.get_subdevice_type(dev, subdev);
		switch(stype){
		case ComediDef.COMEDI_SUBD_AI:
			break;
		case ComediDef.COMEDI_SUBD_DI:
			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, i, ComediDef.COMEDI_INPUT);
				if(ret < 0)
					throw new ComediJNIException("dio_config");
			}
			
			break;
		default:
			throw new ComediException("Subdevice " + subdev + " has type not known to be an input");
		}
	}
	
	private void doCommand() {
		int ret;
		
		/* prepare_cmd_lib() uses a Comedilib routine to find a
		 * good command for the device.  prepare_cmd() explicitly
		 * creates a command, which may not work for your device. */
		ComediDef.Cmd cmd = new ComediDef.Cmd();
		
		prepare_cmd_lib(dev, subdev, nScans, chans.length, periodNS, cmd);
		//prepare_cmd(dev, subdev, nScans, chans.length, periodNS, cmd);

		System.out.println("command before testing:");
		cmd.dump();

		/* comedi_command_test() tests a command to see if the
		 * trigger sources and arguments are valid for the subdevice.
		 * If a trigger source is invalid, it will be logically ANDed
		 * with valid values (trigger sources are actually bitmasks),
		 * which may or may not result in a valid trigger source.
		 * If an argument is invalid, it will be adjusted to the
		 * nearest valid value.  In this way, for many commands, you
		 * can test it multiple times until it passes.  Typically,
		 * if you can't get a valid command in two tests, the original
		 * command wasn't specified very well. */
		ret = Comedi.command_test(dev, cmd);
		if(ret < 0)
			throw new ComediJNIException("comedi_command_test If EIO then this subdevice doesn't support commands");
		

		System.err.println("first test returned "+ ret + " " + ComediDef.cmdtest_messages[ret]);
		cmd.dump();

		ret = Comedi.command_test(dev, cmd);
		if(ret < 0){
			throw new ComediJNIException("comedi_command_test");
		}

		
		System.err.println("second test returned " + ret + " " + ComediDef.cmdtest_messages[ret]);
		if(ret != 0){
			cmd.dump();
			throw new ComediException("Error preparing command");
		}
		
	
		/* start the command */
		ret = Comedi.command(dev, cmd);
		if(ret < 0)
			throw new ComediJNIException("comedi_command");
		
	}
		
	@Override
	public void run() {
				
		try{
			FileDescriptor fd;
			synchronized (Comedi.syncObj) {
				checkIOConfig();
				doCommand();
			}
			
			try{
				
				synchronized (Comedi.syncObj) {
					fd = Comedi.getFileDesc(dev);
				}
			
				FileInputStream readFileStream = new FileInputStream(fd);	
				FileChannel fileChan = readFileStream.getChannel();
				
				if(!fd.valid()){
					synchronized (Comedi.syncObj) {
						Comedi.cancel(dev, subdev);
					}
					//comDev.readerTerminated();
					throw new ComediException("Bad file descriptor for Comedi device read");
				}
		
				while(!death){
					//read as much as is available[options.n_chan+1]
					int bytesRead = data.readMoreData(fileChan);
					
					if(bytesRead == 0){
						try{
							Thread.sleep(10);
						}catch(InterruptedException e){ }
					}
					
					if(bytesRead < 0){
						throw new ComediException("Read channel stream ended before expected.");
					}

					if(data.nextScanNum >= nScans){
						System.out.println("Read complete.");
						return;
					}
					
				}
					
				System.out.println("Read thread terminated externally.");
			}catch(ClosedByInterruptException err){
				System.err.println("Incomplete read aborted.");
				//no stack trace, this one is the normal user stopped it error 
				
			}catch(Exception err){
				System.err.println("Incomplete read aborted:");
				err.printStackTrace();
				
			}finally{
				System.out.print("Reader terminating, cancelling comedi command... ");
				synchronized (Comedi.syncObj) {
					Comedi.cancel(dev, subdev);
				}				
				System.out.println("Done");
			}
			
		}finally{
			//comDev.readerTerminated();
			/*try {
				fo.close();
			} catch (IOException e) { }*/
		}
		
	}
	
	/**
	 * This prepares a command in a pretty generic way.  We ask the
	 * library to create a stock command that supports periodic
	 * sampling of data, then modify the parts we want. */
	private void prepare_cmd_lib(long dev, int subdevice, int n_scan, int n_chan, int scan_period_nanosec, ComediDef.Cmd cmd){
		int ret;

		/* This comedilib function will get us a generic timed
		 * command for a particular board.  If it returns -1,
		 * that's bad. */
		ret = Comedi.get_cmd_generic_timed(dev, subdevice, cmd, n_chan, scan_period_nanosec);
		if(ret<0)
			throw new ComediJNIException("comedi_get_cmd_generic_timed failed");

		/* Modify parts of the command */
		cmd.chanlist = chanlist;
		cmd.chanlist_len = n_chan;
		if(cmd.stop_src == ComediDef.TRIG_COUNT) 
			cmd.stop_arg = n_scan;

	}
	
	
	/*
	 * Set up a command by hand.  This will not work on some devices.
	 * There is no single command that will work on all devices.
	 */
	private int prepare_cmd(long dev, int subdevice, int n_scan, int n_chan, int period_nanosec, ComediDef.Cmd cmd){
		
		/* the subdevice that the command is sent to */
		cmd.subdev =	subdevice;

		/* flags */
		cmd.flags = 0;

		/* Wake up at the end of every scan */
		//cmd->flags |= TRIG_WAKE_EOS;

		/* Use a real-time interrupt, if available */
		//cmd->flags |= TRIG_RT;

		/* each event requires a trigger, which is specified
		   by a source and an argument.  For example, to specify
		   an external digital line 3 as a source, you would use
		   src=TRIG_EXT and arg=3. */

		/* The start of acquisition is controlled by start_src.
		 * TRIG_NOW:     The start_src event occurs start_arg nanoseconds
		 *               after comedi_command() is called.  Currently,
		 *               only start_arg=0 is supported.
		 * TRIG_FOLLOW:  (For an output device.)  The start_src event occurs
		 *               when data is written to the buffer.
		 * TRIG_EXT:     start event occurs when an external trigger
		 *               signal occurs, e.g., a rising edge of a digital
		 *               line.  start_arg chooses the particular digital
		 *               line.
		 * TRIG_INT:     start event occurs on a Comedi internal signal,
		 *               which is typically caused by an INSN_TRIG
		 *               instruction.
		 */
		cmd.start_src =	ComediDef.TRIG_NOW;
		cmd.start_arg =	0;

		/* The timing of the beginning of each scan is controlled by
		 * scan_begin.
		 * TRIG_TIMER:   scan_begin events occur periodically.
		 *               The time between scan_begin events is
		 *               convert_arg nanoseconds.
		 * TRIG_EXT:     scan_begin events occur when an external trigger
		 *               signal occurs, e.g., a rising edge of a digital
		 *               line.  scan_begin_arg chooses the particular digital
		 *               line.
		 * TRIG_FOLLOW:  scan_begin events occur immediately after a scan_end
		 *               event occurs.
		 * The scan_begin_arg that we use here may not be supported exactly
		 * by the device, but it will be adjusted to the nearest supported
		 * value by comedi_command_test(). */
		cmd.scan_begin_src =	ComediDef.TRIG_TIMER;
		cmd.scan_begin_arg = period_nanosec;		/* in ns */

		/* The timing between each sample in a scan is controlled by convert.
		 * TRIG_TIMER:   Conversion events occur periodically.
		 *               The time between convert events is
		 *               convert_arg nanoseconds.
		 * TRIG_EXT:     Conversion events occur when an external trigger
		 *               signal occurs, e.g., a rising edge of a digital
		 *               line.  convert_arg chooses the particular digital
		 *               line.
		 * TRIG_NOW:     All conversion events in a scan occur simultaneously.
		 * Even though it is invalid, we specify 1 ns here.  It will be
		 * adjusted later to a valid value by comedi_command_test() */
		cmd.convert_src =	ComediDef.TRIG_TIMER;
		cmd.convert_arg =	1;		/* in ns */

		/* The end of each scan is almost always specified using
		 * TRIG_COUNT, with the argument being the same as the
		 * number of channels in the chanlist.  You could probably
		 * find a device that allows something else, but it would
		 * be strange. */
		cmd.scan_end_src =	ComediDef.TRIG_COUNT;
		cmd.scan_end_arg =	n_chan;		/* number of channels */

		/* The end of acquisition is controlled by stop_src and
		 * stop_arg.
		 * TRIG_COUNT:  stop acquisition after stop_arg scans.
		 * TRIG_NONE:   continuous acquisition, until stopped using
		 *              comedi_cancel()
		 * */
		cmd.stop_src =		ComediDef.TRIG_COUNT;
		cmd.stop_arg =		n_scan;

		/* the channel list determined which channels are sampled.
		   In general, chanlist_len is the same as scan_end_arg.  Most
		   boards require this.  */
		cmd.chanlist =		chanlist;
		cmd.chanlist_len =	n_chan;

		return 0;
	}

	public ComediRawReadBuffer getStore() { return data; }

}
