import java.io.*;
import java.net.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;

/*

(c) Morten Sickel 2010 - 2011
Licenced under GNU GPL v3 or later



*/

/* 	
	TODO: The routines for data display and fetching of data are presently too
	closely coupled. The routines for fetching data from the server should maybe be 
	extracted into an own object 
	
	TODO: Define integration time by dragging rather than by left and right click
	
	TODO: display existing hits OK 31052011
	
*/

// $Id: Waterfall.java 762 2011-05-31 07:50:58Z radioecology $

class WaterfallException extends Exception{
	
}


class Waterfall extends JComponent {

/*
This class draws and updates the waterfall plot
*/
    public static final int HISPEED=80;  // update delay in ms for hi speed updates
/* TODO: 	Hispeed fetches more lines in one batch same delay as for LOWSPEED - 
			thereby lower load on the database server

*/
	public static final int LOWSPEED=800; 
	private BufferedImage bi;
	private URL ajaxserver;
	private static Boolean active=false;
	private Boolean plotkeV=true,hispeed=false;
	// PlotkeV: X scale in keV or channels
	// active: the updates are running
	private int marked=-1, datasetid=0,index=0, xaxheight=15, total=1,follower=1;
	// Total: Total number of spectra in the data set
	// Follower: Current last id of shown spectra
	int w=300, h=400, intchcol, endrepeats, nchan,inttime=0,intto=-1;
	private float chcol;
	public static Timer readtimer;
	private int[] indexes;
	private float[][] values; // backingstore for the values in the spectrum
	private double a=0.1644,b=6.1225; 
	// default calibration values
	// May be set do different values for 
	// other instruments or new calibrations
	
	public int getTotal(){
		return total;
	}
	
	public int getFollower(){
		return follower;
	}
	
	
	public boolean hispeed(){
		return hispeed;
	}
	
	public boolean hispeed(boolean set){
		hispeed=set;
		return hispeed;
	}
	
	public void calibrate(String calibdata){
	// The calibration data may come in as two space separated floating point numbers 
		String [] temp = null;
		System.out.println(calibdata);
		temp=calibdata.split(" ");
		a=Float.valueOf(temp[1].trim()).floatValue();
		b=Float.valueOf(temp[0].trim()).floatValue();
	}
	
		
	public void calibrate(float newa, float newb){
	// sets the calibration values directly
		a=newa;
		b=newb;
	}
	
	
	public void setPlotkeV(boolean keV){
		plotkeV=keV;
	}
	
	public void toggleActive(){
		active=!active;
	}
	
	public boolean isActive(){
		return active;
	}
	
	
	public int getH(){
		return h-xaxheight;
	}
	
	public int getDatasetid(){
		return datasetid;
	}
	
	public int getMarked(){
		return marked;
	}
	
	public void removeMarked(){
		marked=-1;
	}
	
	public int getInttime(){
		return inttime;
	}


		
	public int getIndex(){
		return index;
	}
	
    public Waterfall(int dataset, int setnchan) {
		nchan=setnchan;
		datasetid=dataset;
		bi = new BufferedImage(w,h,BufferedImage.TYPE_INT_RGB);
        this.addMouseListener(new wfMouseadapter());
		/* 	The number of rows in the plot may be (well now is) less than
			the number of channels in the spectrum. intchcol is the number of 
			channels that is to be downsampled to one row. chcol (a floating point 
			value) is used to find which channel to use as the startingpoint for
			a given row.
		*/
		chcol=(float)nchan/w; // must cast nchan to get out a float
		intchcol=Math.round(chcol);
        readtimer = new Timer(LOWSPEED, new TimerListener(this));
		readtimer.start();
		indexes=new int[h];
		values=new float[h][nchan];
		repaint();
		drawGrid();
    }

	public void setDatasetid(int dataset){
		datasetid=dataset;
	}
	
	private float[] transposechannels(float[] chs){
		return chs;
	}
	
	
	
	
	public void drawnew(int startidx,boolean abs){
	/* 
		Fetches a new dataset and plots it
	*/
		fetchnew(startidx,abs);
		Graphics2D wf=bi.createGraphics();
		wf.setColor(Color.BLACK);
		wf.fillRect(0,0,w,h);
		for(int i=0;i<getH();i++){
			newline(i,values[i]);
		}
		if (marked>=0){
			markSelected();
		}	
		drawGrid();
		repaint();
	}		

	
	
	
	
	public void fetchnew(int startidx,boolean abs){
    /*	
		This is really a special case of fetchrows
		Fetches data for a new plot up to a given index 
	*/
		int oldindex=index;
		String oldtimestamp=WaterfallApplet.timestamp.getText();
		try {
			String fetchid;
			fetchid=abs?"id=":"nth=";
			// abs is set to true if an absolute id is used, false if it is an index within the dataset
			if(!(abs) && (total-startidx < h-50)){
					startidx=startidx-(h-50);
					// Else an empty plot will be shown with all the data "above" the plot.
			}
			fetchid=fetchid+Integer.toString(startidx);
			//ajaxserver = new URL(WaterfallApplet.codebase+"/ajaxserver.php?a=fetchset&cosmic=no&"+fetchid+"&datasetid="+Integer.toString(datasetid)+"&total="+Integer.toString(total) );
			ajaxserver = new URL(WaterfallApplet.codebase+"/ajaxserver.php?a=fetchset&cosmic=no&"+fetchid+"&datasetid="+Integer.toString(datasetid) );
			URLConnection conn=ajaxserver.openConnection();
			BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
			/** The returned data should be on the format
			* Timestamp  on the first line
			* last id on the second line
			* each following line is a complete spectrum with comma separated values
			*/
			// TODO: Dekoble fra wfa
			WaterfallApplet.timestamp.setText(in.readLine()); // reads timestamp
			index=Integer.valueOf(in.readLine().trim()).intValue(); // reads Id
			total=Integer.valueOf(in.readLine().trim()).intValue();
			follower=Integer.valueOf(in.readLine().trim()).intValue();
			// TODO: Rett opp her - dekoble fra wfa, se til at det funker ved scrolling
			System.out.println("tot:"+total+"foll:"+follower+"<");
			WaterfallApplet.infolabel.setText(total==follower?"End of data set":"");
			int row =0;
			String decodedString;
			while (((decodedString = in.readLine()) != null) && (row<h)) {
				try
				{
					float[] chs=decodeline(decodedString);	
					indexes[row]=Math.round(chs[0]); // id for the row
					System.arraycopy(chs, 1, chs, 0, chs.length-1); // Shifts out the ids
					values[row]=chs;
					row++;
				}
				catch (NumberFormatException e){e.printStackTrace();}
				
			}
			in.close();
			for(int i=row;i<h-1;i++){
			// Clear out the rest of values if not totally filled
				for(int j=0;j<nchan;j++){
					values[i][j]=0;
				}
			}
		}
		catch (NumberFormatException e){
		
			// TODO dekoble fra wfa
			WaterfallApplet.infolabel.setText("End of data set");
			WaterfallApplet.timestamp.setText(oldtimestamp); 
			index=oldindex;
			System.out.println("Invalid index read, index set to :"+index);
		}
		catch(MalformedURLException e) {e.printStackTrace();}
		catch(IOException e){e.printStackTrace();}
	}
		
	
	public float[] getRow(int rowid){ 
		try{
			// fetching data from backingstore 
			rowid=lookupMark(rowid);
		}
		catch(WaterfallException wfe) {System.out.println("Finner ikke aktuelt spekter ("+rowid+")");}
		return values[rowid];
		
	}
   
   
	private float[] decodeline(String line){
		String [] temp = null;
		temp=line.split(",");
		float[] chs = new float[nchan];
		for(int j=0;j<temp.length;j++){ 
			chs[j] = Float.valueOf(temp[j].trim()).floatValue();
		}
		return chs;
	}
	
	public void fetchRows(int n){
    /*	
		Fetches data for a plot up to a given index 
		TODO: Combine with fetchnew!
	*/
		int oldindex=index;
		// System.out.println("oldindex:"+index);
		// TODO dekoble fra wfa:
		String oldtimestamp=WaterfallApplet.timestamp.getText();
		try {
			ajaxserver = new URL(WaterfallApplet.codebase+"/ajaxserver.php?a=fetchset&cosmic=no&id=" +Integer.toString(index)+"&datasetid="+Integer.toString(datasetid)+"&limit="+Integer.toString(n)+"&sort=asc" );
			URLConnection conn=ajaxserver.openConnection();
			BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
			/** The returned data should be on the format
			* Timestamp  on the first line
			* last id on the second line
			* each following line is a complete spectrum with comma separated values
			*/
			WaterfallApplet.timestamp.setText(in.readLine()); // reads timestamp
			index=Integer.valueOf(in.readLine().trim()).intValue(); // reads Id
			System.out.println("Id:"+index);
			total=Integer.valueOf(in.readLine().trim()).intValue();
			follower=Integer.valueOf(in.readLine().trim()).intValue();
			WaterfallApplet.infolabel.setText("");
			if (oldindex == index){ // no new data	
				WaterfallApplet.infolabel.setText("End of data set");
				endrepeats++;
				if(endrepeats>10){ // Pauses data updates if nothing new for 10 updates
					active=false; 								// Stops the calls for new data
					WaterfallApplet.BtRunwf.setSelected(false);	 // "pops out" the run button
				}
				return ; // nothing more to do here
			}

			endrepeats=0; 
			// System.out.println("index:"+index);
			String decodedString;
			while (((decodedString = in.readLine()) != null)) {
				try
				{
					float[] chs=decodeline(decodedString);
					int idx=Math.round(chs[0]); // id for the row
					System.arraycopy(chs, 1, chs, 0, chs.length-1); // Shifts out the id
					drawshift(idx, chs); // shifts the new data sets into the backing store and draws the plot.
				}
				catch (NumberFormatException e){e.printStackTrace();}
				
			}
			in.close();
		}
		catch (NumberFormatException e){
			WaterfallApplet.infolabel.setText("End of data set");
			WaterfallApplet.timestamp.setText(oldtimestamp); 
			index=oldindex;
			System.out.println("Invalid index read, index set to :"+index);
			endrepeats++;
			if(endrepeats>10){ // Pauses data updates if nothing new for 10 updates
				active=false; 								// Stops the calls for new data
				WaterfallApplet.BtRunwf.setSelected(false);	 // "pops out" the run button
			}
		}
		catch(MalformedURLException e) {e.printStackTrace();}
		catch(IOException e){e.printStackTrace();}
	}
		
	
	
	
	public void drawshift(int idx, float[] chs){
		System.arraycopy(indexes, 0, indexes, 1, indexes.length-1); 
		System.arraycopy(values,  0, values,  1,  values.length-1);
		indexes[0]=idx;
		values[0]=chs;
		newline(-1,chs); // put in a new line at the top, shift the rest down
	}
	
	public void drawshift(float[] chs){
		System.arraycopy(indexes, 0, indexes, 1, indexes.length-1); 
		System.arraycopy(values,  0, values,  1,  values.length-1);
		indexes[0]=index;
		values[0]=chs;
		newline(-1,chs); // put in a new line at the top, shift the rest down
	}

	
	private void newline(Integer row, float[] chs){
		/* 
			TODO: The first number in the array is now indicating a hit. 1 = hit, 0 = no hit
		*/
		/* Draws a new line in the waterfall plot at row# row. If the new row is to be 
		inserted at the top so that the rest of the plot is shifted one line down, row is to be set to a negative value
		*/
		if (row<0){ // To flag that a new row is put in on the top to put the rest of the dataset one row down
			for (Integer i=0;i<w;i++) { // Moves the entire picture one row down. may be an easier way?
				for (int j=getH()-2;j>=0;j--){ // Goes from the bottom and up since the bottom row is the one to be discarded
					bi.setRGB(i,j+1,bi.getRGB(i,j));
				}
			}
			row=0; // New row to come in at the (now empty) top row
		}
		float max=Legend.getMax();
		boolean marked=chs[0]==1; // A hit exists at the active row
		System.arraycopy(chs, 1, chs, 0, chs.length-1); // Shifts out the mark
		for(Integer i=0;i<w;i++){ // Draws the new line, either in the spectrum or at the top
			float value=chs[Math.round(i*chcol)];
			if(Legend.getLog() && value>0){
				value=(float)Math.log(value);
			}
			if (value>=max){
				bi.setRGB(i,row, Legend.makeARGB(0, 255, 255, 255));}
			else{
				try{
					bi.setRGB(i,row,Legend.colors[Math.round(value/max*Legend.NCOLORS)+1]);
				}
				catch(ArrayIndexOutOfBoundsException e){
					System.out.println("Exception Newline() : Value:"+value+"|max:"+max+"|ncol:"+Legend.NCOLORS+"|");
				}
			}
		}
		if(marked){
			// A marker exists here. Draws a gray line at the end of the spectre.
			Graphics2D wf=bi.createGraphics();
			wf.setColor(Color.GRAY); // Possible to make a color with alpha channel here?
			wf.drawLine(w-20,row,w,row);
		}
	}
	
	public void markSelected(){ // marks (if any) selected line
		try{
			int y=lookupMark(marked);
			Graphics2D wf=bi.createGraphics();
			wf.setColor(Color.GRAY); // custom color with alpha ch?
			if(marked >= 0 && marked < index && marked>indexes[getH()]){ // if there is a mark somewhere on the plot
				wf.drawLine(0,y,w,y);
			}	
			if(inttime>0 && intto < index && intto>indexes[getH()]){
				y=lookupMark(intto);
				wf.drawLine(0,y,w,y);
				repaint();
			}
		}
		catch(WaterfallException wfe) {System.out.println("Kan ikke justere posisjon");}
	}
	
	
	public void drawGrid(){
		// marks some channels 100, 200 ... 500
		Graphics2D wf=bi.createGraphics();
		wf.setColor(Color.LIGHT_GRAY);
		int max=plotkeV?7:6;
		wf.fillRect(0,h-xaxheight,w,h);
		int mult=plotkeV?500:100;
		for(int i=1;i<max;i++){
			int ch=plotkeV?(int)((i*500-b)/a):i*100;
			int ptx = Math.round(ch/chcol); // 
			wf.setColor(Color.GRAY);
			wf.drawLine(ptx,0,ptx,h-xaxheight);
			wf.setColor(Color.BLACK);
			wf.drawString(Integer.toString(i*mult),ptx-15,h-2);
		}
		String unit=plotkeV?"keV":"Ch";
		wf.drawString(unit,0,h-2);
		

	}

	public Dimension getPreferredSize() {
        return new Dimension(w, h);
    }

    public void paintComponent(Graphics g) {
		g.drawImage(bi,10,10,null);
		drawGrid();
		
    }
	
	class wfMouseadapter extends MouseAdapter{
		public void mouseClicked(MouseEvent e) { 	
			try{
			if (datasetid >0){ 
				if((e.getButton()==MouseEvent.BUTTON3) && (marked>0)){
				// Right click. Defines an integral relative to last left click
				// if(marked>0){
				// Defines an integral relative to last left click
					if (inttime > 0  && lookupMark(intto)>0){ 
						// Removes old mark
						newline(lookupMark(marked+inttime),getRow(marked+inttime)); // Puts the data back where the old line was
					}
					intto=indexes[e.getY()-10];
					if(intto>marked){
					// The integration time is always calculated from the first mark
						inttime=intto-marked;
					}else{
						inttime=marked-intto;
						marked=intto;
					}
					markSelected(); // Draws the mark
				}else{ 
				if(e.getButton()==MouseEvent.BUTTON1){
				//	System.out.print("Left:");
				// Left click, sets a new mark
					if(e.getY()>=10){
						setMouseMark(e.getY()-10); // stores the new mark
					}
				}}
				
			}
		}
		catch(WaterfallException wfe) {System.out.println("Kan ikke justere posisjon");}
		}
	} // class wfMouseAdapter	
	
	
	public int lookupMark(int markidx) throws WaterfallException { 
	
	// Returns the location of a given specter Throws an exception if not found
		int retvalue = -1;
		for (int i=0;i<indexes.length && retvalue <0;i++){
			retvalue=indexes[i]==markidx?i:retvalue;
		}
		if (retvalue==-1){ throw new WaterfallException();}
		System.out.println("idx:"+indexes[retvalue]);
		//System.out.println("lookupmark->"+retvalue);
		return retvalue;
	}
	
	public int nextMark(int mark, boolean up) throws WaterfallException{
		// Moves to the next mark in a series. The ids may not be continious

		int i=lookupMark(mark);
		i=up?i+1:i-1;
		return indexes[i];
	}
	
	
	
	
	void setMouseMark(int markline){
	try{
		setmarked(indexes[markline]); // Translates the image coordinate to a specterid
	}
	catch (ArrayIndexOutOfBoundsException e){System.out.println("line: "+markline);}
	}
	
	public void setmarked(int newmark){
	
		
		int oldmark=marked;
		int oldinttime=inttime;
		// System.out.println("index:"+index+"|indexes[0]:"+indexes[0]+"|");
		marked=newmark;
		try{
			// System.out.println("oldmark:"+oldmark);
			newline(lookupMark(oldmark),getRow(oldmark)); // Puts the data back where the old line was
		}
		catch(WaterfallException wfe) {System.out.println("Ikke gammelt merke");}
		try{
			// Removes any old integration time marker
			newline(lookupMark(oldmark+inttime),getRow(oldmark+inttime)); // Puts the data back where the old line was
		}
		catch(WaterfallException wfe) {System.out.println("Ikke gammelt integrasjonstid");}

		// Enables the buttons
		WaterfallApplet.BtSaveHit.setEnabled(true);
		WaterfallApplet.BtLink.setEnabled(true);
		WaterfallApplet.BtUp.setEnabled(true);
		WaterfallApplet.BtDn.setEnabled(true);
		//		System.out.println("enabled buttons");
		inttime=0;
		markSelected();
		repaint();
	
	}
}



class TimerListener implements ActionListener{

/*
Helper class for timed updates
*/
	Waterfall wf;
	public TimerListener(Waterfall initwf){
		wf=initwf;
	}
	public void actionPerformed(ActionEvent e){
		if (wf.isActive()){
			if(wf.hispeed()){
				wf.fetchRows(10);
			}else{
				wf.fetchRows(1);
			}
			wf.repaint();
	
		}
	}
}

