package comedi;

import java.io.FileNotFoundException;

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.DoubleBuffer;
import java.nio.IntBuffer;
import java.nio.channels.FileChannel;


import binaryMatrixFile.BinaryMatrixFile;

import jafama.FastMath;

import comedi.ComediRawDataStore;
import comedi.ComediAsyncReader;

/**
 * Provides a converted sectioned view of the raw ring buffer in 
 * ComediRawDataStore that updates on a regular basis
 * 
 * The raw buffer might be circular or not.
 * It might be huge or not.
 * 
 * We might want to restrict the amount of it we look at, by:
 * 	 	only look at the end (strict nx and x1)
 * 		by only looking at the start (restrict nx and x0)
 * 		only look at every n'th (restrict dx)
 * 		look at a particular piece (restrict x0 and x1)
 * 
 * etc...
 * some of that is implemented, some not
 * 
 * @author oliford
 */
public class ComediDataView implements Runnable {
	private static final long serialVersionUID = 3280193143019651604L;
	
	private int graphUpdatePeriod = 1000; //ms
	
	private Comparable seriesKey;
	
	private DoubleBuffer dBuf;
	
	private ComediRawDataStore rawStore;
	
	private Thread thread;
	private boolean death = false;
		
	/** Out view restrictions. anything < 0 means unrestricted */
	private int restrictX0 = -1;
	private int restrictX1 = -1; 
	private int restrictNX = -1;	
	private int restrictDX = -1;
	
	private int x0, x1, dx;
	
	private int nChans;
	
	private DataViewChangedNotification notify;
	
	private boolean forceUpdate = false;
		
	public ComediDataView(ComediRawDataStore rawStore, DataViewChangedNotification notify) {
		this.rawStore = rawStore;
		this.notify = notify;
	}
	
	public void startUpdateThread(){
		if(thread != null && thread.isAlive())
			throw new RuntimeException("Conversion thread already active");
		
		thread = new Thread(this);
		thread.start();
	}
	
	public void abort(boolean awaitDeath) {
		death = true;
		thread.interrupt();
		if(awaitDeath){
			while(thread != null && thread.isAlive()){
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) { }
			};
		}
	}
	
	public boolean isActive(){ return thread != null && thread.isAlive() && !death; }
	
	public void setRestrictions(int x0, int x1, int nx, int dx){
		restrictX0 = x0;
		restrictX1 = x1;
		restrictNX = nx;
		restrictDX = dx;
	}
	
	public void forceUpdate() {
		forceUpdate = true;
		if(thread != null && thread.isAlive()){
			thread.interrupt();
		}else{
			doUpdate();
		}
		
	}
	
	public void run() {
		
		while(!death){
			doUpdate();

			try {
				if(graphUpdatePeriod > 0){
					Thread.sleep(graphUpdatePeriod);
				}else{
					Thread.sleep(Long.MAX_VALUE);
				}
			} catch (InterruptedException e) { }			
		}
	}	
	
	private void doUpdate(){
		//synchronized (rawStore) {
			synchronized (this) {
				
				int nChans = rawStore.nChans;	
				int x0, x1;
				if(rawStore instanceof ComediRawReadBuffer){
					x0 = ((ComediRawReadBuffer)rawStore).firstScanNum;
					x1 = Math.max(rawStore.nextScanNum - 1, x0);
				}else{
					x0 = 0;
					x1 = rawStore.buffLength;
				}	
					
				if(restrictX0 > x0)
					x0 = restrictX0;
				if(restrictX1 >= 0 && restrictX1 < x1)
					x1 = restrictX1;
				
				int dx = Math.max(restrictDX, 1);
				int nx = (x1 - x0) / dx;
				
				if(restrictNX > 0 && restrictNX < nx) {
					nx = restrictNX;
				}
				
				x0 = x1 - nx*dx;
				
				int i0 = 0;
				
				if(!forceUpdate && this.nChans == nChans && this.x0 == x0 && this.x1 == x1 && this.dx == dx)
					return; //nothing changed
				
				forceUpdate = false;
								
				if(nx <= 0){
					this.nChans = nChans;
					this.x0 = x0;
					this.x1 = x1;
					this.dx = dx;
					dBuf = null;
					return;
				}
				
				ByteBuffer bBuf = ByteBuffer.allocateDirect(nx * nChans * 8);
				dBuf = bBuf.asDoubleBuffer();
				
				for(int i = i0; i < nx; i++){
					int x = x0 + i*dx;
					for(int j=0; j < nChans; j++){
						double val = rawStore.getPhysDouble(x, j);
						dBuf.put(val);
					}
				}
				
				this.nChans = nChans;
				this.x0 = x0;
				this.x1 = x1;
				this.dx = dx;			
			}		
		//}
		
		if(notify != null)
			notify.dataViewChanged();
	}	
	
	public DoubleBuffer getBuffer() { return dBuf; }	
	public int getNChans() { return nChans; }
	public int getX0() { return x0; }
	public int getX1() { return x1; }
	public int getDX() { return dx; }
	
	public double get(int x, int chan){ 
		return (dBuf == null) ? Double.NaN : dBuf.get(x * nChans + chan);
	}
	
	public void save(String fileName) {
		try {
			int nx = ((x1 - x0) / dx);
			
			FileOutputStream fout = new FileOutputStream(fileName);
			FileChannel fc = fout.getChannel();
			
			ByteBuffer bBuf = ByteBuffer.allocate((nChans + 1) * 8);
			
			bBuf.putInt(nx);
			bBuf.putInt(nChans+1);
			bBuf.flip();
			fc.write(bBuf);
			
			bBuf.clear();
			
			synchronized (this) {
				DoubleBuffer vBuf = dBuf.asReadOnlyBuffer();
				vBuf.rewind();
				vBuf.limit(dBuf.capacity());
				for(int i=0; i < nx; i++){
					bBuf.putDouble(x0 + i * dx);
					for(int j=0; j < nChans; j++)
						bBuf.putDouble(vBuf.get());
					bBuf.flip();
					fc.write(bBuf);
					bBuf.clear();
				}
						
			}
			
			fc.close();
			fout.close();
			
		} catch (IOException err) {
			err.printStackTrace();
		}
	}

	public void setUpdatePeriod(int graphUpdatePeriod) {
		this.graphUpdatePeriod = graphUpdatePeriod;
	}

	public ComediRawDataStore getRawStore() { return rawStore;	}
}
