import java.io.IOException;
import java.util.Random;

import com.proatech.bioera.external.ExternalInitIf;
import com.proatech.bioera.external.ExternalSinkIf;
import com.proatech.bioera.external.ExternalSourceIf;

/**
 * This class is an example implementation of ExternalElement, it implements
 * interfaces: ExternalSourceIf, ExternalSinkIf, ExternalInitIf.
 * 
 * 1. It creates an element with one input and output channel. 2. It multiplies
 * input signal by 2.
 * 
 */
public class KT88_BioEra implements ExternalSourceIf, ExternalInitIf,
		ExternalSinkIf {

	private int comPortNo = 3;
	
	private int maxValue = 325;
	
	private boolean upSample = false;
	private int upSampleRate = 256;
	
	private double upSampleLastIdx;
	private int[] upSampleBuffer;
	private int[][] buffer;
	private int available;
	private int numSending;
	private KT88 kt88;
	private ReaderThread readerThread;

	public KT88_BioEra() {
		CreateKT88();
		buffer = new int[1000][kt88.NoChannels];
		upSampleBuffer = new int[kt88.NoChannels];
		available = 0;
		numSending = 0;
		upSampleLastIdx = 0;

	}

	// //////////// Definition of ExternalSourceIf interface
	// /////////////////////////

	/**
	 * If your signal has constant rate it should be set here. If it is set to
	 * -1, , then the information is taken from the element connected to input
	 * (in the design)
	 */
	public float getOutputDataRate() {
		return upSample ? upSampleRate : 100;
	}

	/**
	 * If your signal has constant maximum digital value then it should be set
	 * here. If it is set to Integer.MAX_VALUE, then the information is taken
	 * from the element connected to input (in the design)
	 */
	public int getDigitalMax() {
		return 0x7ff;
	}

	/**
	 * If your signal has constant maximum digital value then it should be set
	 * here. If it is set to Integer.MIN_VALUE, then the information is taken
	 * from the element connected to input (in design)
	 */
	public int getDigitalMin() {
		return -0x7ff;
	}

	/**
	 * Define number of output channels. The design has to be restarted if this
	 * value changes.
	 */
	public int getOutputChannelCount() {
		return KT88.NoChannels;
	}

	/**
	 * If your signal has constant maximum physical value then it should be set
	 * here. If it is set to Float.POSITIVE_INFINITY, then the information is
	 * taken from the element connected to input (in design)
	 */
	public float getPhysicalMax() {
		return maxValue;
	}

	/**
	 * If your signal has constant minimum physical value then it should be set
	 * here. If it is set to Float.MIN_VALUE, then the information is taken from
	 * the element connected to input (in design)
	 */
	public float getPhysicalMin() {
		return -maxValue;
	}

	/**
	 * If your signal has a physical unit it should be set here. If it is set to
	 * null, then the information is taken from the element connected to input
	 * (in design)
	 */
	public String getPhysicalUnit() {
		return "uV"; //"╡V"
	}


	/**
	 * This method is described in the ExternalSourceIf interface
	 */
	public int processOutputData(int channel, int[] destinationBuffer) {
		try {

			// System.out.print("i"+channel+", "+destinationBuffer.length);

			
			int idx = 0;
			double upSampleIdx = upSampleLastIdx;

			if (numSending > 0) {

				if (upSample) {
					
					double upSampleStep = 100.0/upSampleRate;
					
					// go through available sample excluding last one
					for (; idx < destinationBuffer.length && upSampleIdx + 1 < numSending; upSampleIdx+=upSampleStep, idx++) {
						
						int prev = upSampleIdx < 0
								? upSampleBuffer[channel]
								: buffer[(int)upSampleIdx][channel];
						
						int next = buffer[(int)upSampleIdx + 1][channel];
						
						double x = upSampleIdx - (int)upSampleIdx;
						
						destinationBuffer[idx] = (int)(prev * (1.0 - x) + next * x);
						
						//System.out.println("ch:"+channel+", upSN:"+upSampleIdx+", prev:"+prev+", next:"+next+", res:"+destinationBuffer[idx]);
					}
					
				} else {
					
					for (; idx < destinationBuffer.length && idx < numSending; idx++) {
						destinationBuffer[idx] = buffer[idx][channel];
					}
				}
			}
			
			if (channel == 0) { // end of round. remove sent samples from buffer

				int numSent = idx; // number of samples sent from buffer
				
				if (upSample) {
					
					numSent = numSending == 0 ? 0 : (int)upSampleIdx + 1; // number of samples sent from buffer + 1 for the last one
					
					if (numSent>0)
						for(int i=0; i<kt88.NoChannels; i++) // store last sample in the upSampleBuffer
							upSampleBuffer[i] = buffer[numSent - 1][i];
					
					upSampleLastIdx = upSampleIdx - numSent;
					
					//System.out.println("upSampleLastIdx:"+upSampleLastIdx);
				}
				
				synchronized (buffer) {
					
					//System.out.println("(0) available:"+available+", numSending:"+numSending+", numSent:"+numSent);
					
					for (int j = numSent; j < available; j++) // move unsent samples to start of buffer
						buffer[j - numSent] = buffer[j];
					available -= numSent;
					
					numSending = available; // next round send 'available' number of samples
					
					//System.out.println("(1) available:"+available+", numSending:"+numSending+", numSent:"+numSent);
				}

				
			}
			
/*
			int n = 5;
			
			for (int i = 0; i < n; i++) {
				destinationBuffer[i] = (int) (Math
						.sin(((double) (sim_i + i)) / 2) * 100);
			}
			
			if (channel == 0) sim_i += n;

			num = n;
			Thread.sleep(3);
			*/
			
			//System.out.println(", "+available+", "+num);
			return idx;

		} catch (Exception e) {
			System.err.println("KT88_BioEra. Error processing output");
			e.printStackTrace();
		}
		return 0;
	}

	/**
	 * This method is described in the ExternalSourceIf interface
	 */
	public int processOutputData(int channel, float[] destinationBuffer) {
		return 0;
	}

	/**
	 * This method is described in the ExternalSourceIf interface
	 */	
	public int processOutputData(int channel, double[] destinationBuffer) {
		return 0;
	}

	// ///////////////// Definition of the ExternalInitIf interface
	// ////////////////////

	/**
	 * Called one time when the element is created.
	 */
	public void init() throws java.io.IOException {
	}

	/**
	 * Called whenever element is started.
	 */
	public void start() {
		try {

			System.out.println("KT88_BioEra. Start");

			kt88.OpenDevice();

			available = 0;
			numSending = 0;
			upSampleLastIdx = 0;

			if (readerThread == null || !readerThread.isAlive()) {
				readerThread = new ReaderThread();
				readerThread.start();
			} else
				readerThread.resumeReading();

		} catch (Exception e) {
			System.err.println("KT88_BioEra. Error during start");
			e.printStackTrace();
		}

	}

	/**
	 * Called whenever element is stopped.
	 */
	public void stop() {
		try {
			System.out.println("KT88_BioEra. Stop");

			if (readerThread != null) {
				readerThread.quit();

				while (readerThread.isAlive())
					try {
						Thread.sleep(10);
					} catch (Exception e) {
					}
			}
			
			kt88.CloseDevice();

		} catch (Exception e) {
			System.err.println("KT88_BioEra. Error during stop");
			e.printStackTrace();
		}

	}

	/**
	 * Called whenever element is paused.
	 */
	public void pause() throws java.io.IOException {

		stop();
	}

	/**
	 * Called whenever element is reinited.
	 */
	public void reinit() throws java.io.IOException {
		System.out.println("KT88_BioEra. Element reinited");
	}

	/**
	 * Called whenever element is resumed.
	 */
	public void resume() throws java.io.IOException {
		start();
	}

	/**
	 * Called one time when element is removed from the design (or exit).
	 */
	public void destroy() {
		System.out.println("KT88_BioEra. Element destroyed");
	}

	/**
	 * Define number of input channels. The design has to be restarted if this
	 * value changes.
	 */
	public int getInputChannelCount() {
		return 1;
	}

	/**
	 * This method is called only in scalar ExternalElement when there is data
	 * delivered to input pipe It must return the number of samples processed,
	 * or Integer.MAX_VALUE if all data should be dropped.
	 */
	public int processInputData(int channel, int[] source, int count) {
		try {
			if (count > 0) {
				int newPort = source[count - 1];
				if (newPort != comPortNo) {
					PortChanged(newPort);
				}
			}
		} catch (Exception e) {
			System.err.println("KT88_BioEra. error processing input data");
			e.printStackTrace();
		}
		// This means that all data has been processed, it could also return the
		// 'available' value.
		return Integer.MAX_VALUE;
	}

	/**
	 * This method is called only in F_ExternalElement when there is data
	 * delivered to input pipe It must return the number of samples processed,
	 * or Integer.MAX_VALUE if all data should be dropped.
	 */
	public int processInputData(int channel, float[] source, int count) {
		// This means that all data has been processed, it could also return the
		// 'available' value.
		return Integer.MAX_VALUE;
	}

	/**
	 * This method is called only in D_ExternalElement when there is data
	 * delivered to input pipe It must return the number of samples processed,
	 * or Integer.MAX_VALUE if all data should be dropped.
	 */
	public int processInputData(int channel, double[] source, int count) {
		// This means that all data has been processed, it could also return the
		// 'available' value.
		return Integer.MAX_VALUE;
	}

	private void PortChanged(int newPortNo) {
		comPortNo = newPortNo;
		System.out.println("KT88_BioEra. Port changed to "+newPortNo);
		CreateKT88();
	}

	private void CreateKT88() {
		boolean wasOpened = kt88!=null && kt88.isOpen();
		if (wasOpened) 
			stop();

		try {
			kt88 = new KT88("COM" + comPortNo);
		} catch (Exception e) {
			System.err.println("KT88_BioEra. Error creating KT88");
			e.printStackTrace();
		}
		if (wasOpened)
			start();
	}

	private class ReaderThread extends Thread {
		private int state = 0; // 0 - stopped, 1 - running, 2 - paused

		public void run() {
			state = 1;
			int[] packet = null;
			while (true) {
				switch (state) {
				case 0: // stopped
					// System.out.print("stopped");
					return;

				case 1: // running
					// System.out.print("running...");
					if (kt88.isOpen() && packet == null)
						try {
							// System.out.print("reading...");
							packet = kt88.ReadPacket();
						} catch (Exception e) {
							System.err.println("KT88_BioEra. error getting packet from KT88");
							e.printStackTrace();
						}

					synchronized (buffer) {
						if (packet != null && available < buffer.length) {
							// System.out.print("saved");
							buffer[available++] = packet;
							packet = null;

						}
					}
					if (packet != null) {
						//System.out.print("no data or buffer full");
						try {
							Thread.sleep(10);
						} catch (Exception e) {
						}
					}
					// System.out.println(": "+available);
					break;

				case 2: // paused
					// System.out.println("paused: "+available);
					try {
						Thread.sleep(100);
					} catch (Exception e) {
					}
					break;
				}
			}

		}

		public void pauseReading() {
			state = 2;
		}

		public void resumeReading() {
			state = 1;
		}

		public void quit() {
			state = 0;
		}
	}

}
