package mds;

import java.io.*;
import java.nio.ByteOrder;
import java.lang.reflect.Array;
import java.util.Vector;

import oneLiners.DataConvertPureJava;
import oneLiners.OneLiners;

import base.SignalFetcher;

import signals.*;
import signals.JET.*;
import descriptors.*;
import descriptors.JET.*;

/** Fetches JPF and PFF Signals from the JET Datastore via the MDS+ interface.
 * 
 * The MDS+ interface is used only to make calls to the jpf() ppf() and jpfraw() function.
 * 
 * The JET MDS+ tree layout is completely nuts and so is not used. 
 * 
 * @author oliford <codes(at)oliford.co.uk>
 *
 */
public class JetMDSFetcher implements SignalFetcher {
	public static final int requestTimeout = 30000; //give up after 30secs
	
	
	Vector<Signal> memCache = new Vector<Signal>();
	boolean useMemoryCache = false;

	/** Flag to supress warnings and status info */
	private boolean quiet = false;
	
	public boolean isQuiet() {
		return quiet;
	}

	public void setQuiet(boolean quiet) {
		this.quiet = quiet;
	}

	/** Flag to inhibit writing of signals to disk cache */
	private boolean writeInhibit = false;
	
	public boolean isWriteInhibited() {
		return writeInhibit;
	}

	public void setWriteInhibit(boolean writeInhibit) {
		this.writeInhibit = writeInhibit;
	}

	public void setUseMemoryCache(boolean useMemoryCache) {
		this.useMemoryCache = useMemoryCache;
	}
	
	/** Path to disk cache root */
	private String cacheRoot;
	
	//private MDSConnection mdsConn;
	private MdsPlus mdsConn;
	
	public MdsPlus pinchMDSConnection(){
		return mdsConn;
	}

	/** Create signal fetcher through MDS connection to the 
	 * default JET MDS+ server (mdsplus.jet.efda.org:8000) */
	public JetMDSFetcher(String cachePath) {
		this("auto", -1, cachePath, false);		
	}
	
	/** Create signal fetcher through MDS connection to the 
	 * default JET MDS+ server (mdsplus.jet.efda.org:8000) */
	public JetMDSFetcher(String cachePath, boolean connectImmeadiately) {
		this("auto", -1, cachePath, connectImmeadiately);		
	}
	
	/** Create signal fetcher through specified MDS+ connection 
	 * @param hostAndPort host:port*/
	public JetMDSFetcher(String cacheRoot, String hostAndPort) {		
		this( hostAndPort, -1, cacheRoot, false);		
	}
	
	/** Create signal fetcher through specified MDS+ connection */
	public JetMDSFetcher(String host, int port, String cacheRoot) {
		this(host, port, cacheRoot, false);
	}
	
	/** Create signal fetcher through specified MDS+ connection */
	public JetMDSFetcher(String host, int port, String cacheRoot, boolean connectImmeadiately) {
		if(cacheRoot != null){
			cacheRoot = stripMultiSlashes(cacheRoot);
			if(cacheRoot.endsWith("/"))			
				this.cacheRoot = cacheRoot;
			else
				this.cacheRoot = cacheRoot + "/";
		}
		
		/* BTW: If you're using this from 'minerva' - this is done in MinervaSettings as well */ 
		if( "auto".equalsIgnoreCase(host) ){
			
			try {				
				Class<?> minervaSettingsClass = Class.forName("seed.minerva.MinervaSettings");				
				Object minervaSettings = minervaSettingsClass.getMethod("instance", (Class<?>[]) null).invoke(null, (Object[]) null);				
				String url = (String) minervaSettingsClass.getMethod("getMDSServer", (Class<?>[]) null).invoke(minervaSettings, (Object[]) null);				
				String[] parts = url.split(":");
				host = parts[0];
				port = Integer.parseInt(parts[1]);				
			} catch (Exception ex) { // No minerva compilation
				
				/* If in windows, bounce off of ws-services
				 *  because windows' TCP stack is broken somehow
				 * and it can't connect to the real server quickly. 
				 */ //FIXME: Find out what this is really
				String osname = System.getProperty("os.name");
				if( osname.startsWith("Windows") ){
					host = "wsservices-1.jet.uk";
					port = 8765;
				}else{
					host = "mdsplus.jet.efda.org";
					port = 8000;
				}				
			}			
		}
		
		if(host != null && host.length() > 0){
			if(port < 0){
				String parts[] = host.split(":");
				if(parts.length != 2)
					throw new IllegalArgumentException("port was not specified or < 0 and host isn't 'host:port'");
				host = parts[0];
				port = Integer.parseInt(parts[1]);
			}
			//mdsConn = new MDSConnection(host, port);			
			mdsConn = new MdsPlus(host, port, quiet, connectImmeadiately, requestTimeout);
		} else
			mdsConn = null;
		
		if(cacheRoot != null){
			//attempt to read write Inhibit file, if it exists, disable cache writing		
			try{
				//FileInputStream inhibitFile =
				new FileInputStream(cacheRoot + "writeInhibit");
				
				if(!quiet)
					System.out.println("JetMDSFetcher disk cache write inhibit enabled.");
				writeInhibit = true;
			}catch(FileNotFoundException err){ writeInhibit = false; }
		} else 
			writeInhibit = true;
	}
	
	/** removes multiple slashes from a path */
	private static String stripMultiSlashes(String in){
		String tmp;
		do{			
			tmp = in;
			in = tmp.replaceAll("//", "/");		
		}while(in.length() < tmp.length());
		
		return in;		
	}
	
	@Override
	/** Cleanup - close MDS+ connection */
	protected void finalize() throws Throwable {
		destroy();
		super.finalize();		
	}
	
	/** Cleanup - close MDS+ connection */
	public void destroy(){
		if( mdsConn != null )
			mdsConn.close();
	}

	/** Gets the specified signal from memory, disk or MDS+ */
	public Signal getSig(String path) {	
		return getSig(path, false);
	}
	

	/** Gets the specified signal from memory, disk or MDS+ */
	public Signal getSig(String path, boolean overrideCaches) {
		//we only handle specific signal types, so we can work which it is
		path = stripMultiSlashes(path);
		
		String parts[] = path.split("/");
		
		int j=-1;
		while(parts[++j].length() == 0);
		
		if( parts[j].matches("^[0-9]*$") ) { //all numbers then we have pulse straight out
			System.out.println("No machine specified, assuming JET");
		}else{
			if( !(parts[j].equalsIgnoreCase("jet")
					|| parts[j].equalsIgnoreCase("jet-offline")) ) 
					throw new IllegalArgumentException("Not a jet or jet-offline pulse");
			
			while(parts[++j].length() == 0);
		}
		
		//this one is the pulse
		
		while(parts[++j].length() == 0);
					
		if("ppf".equalsIgnoreCase((String)parts[j].subSequence(0, 3)) )
			return getSig(new PPFDesc(path), overrideCaches);
		else if("jpf".equalsIgnoreCase(parts[j]))
			return getSig(new JPFDesc(path), overrideCaches);
		
		throw new IllegalArgumentException("JET signals must have 3rd level of path starting with 'jpf' or 'ppf'");
	}
	
	/** Gets the specified signal from memory, disk or MDS+ */
	public Signal getSig(SignalDesc signalDesc) {
		return getSig(signalDesc, false);
	}
	
	/** Gets the specified signal from memory, disk or MDS+.
	 * If overrideCaches is true, the signal is fetched from MDS+ */
	public Signal getSig(SignalDesc desc, boolean overrideCaches) {
		Signal sig;
		
		if(!overrideCaches){		//if we're using the cache
			//see if it's in memory.
			sig = getFromMemory(desc);	
			if(sig != null)return sig;
			
			sig = getFromDisk(desc);
			if(sig != null)return sig;
		}
			
		//otherwise get from MDSPlus
		sig = getFromMDS(desc);
		if(sig != null)return sig;		
			
		throw new RuntimeException("Failed to get data from memory, disk or MDSPlus for signal: " + desc);		
				
	}
	
	private Signal getFromMemory(SignalDesc signalDesc){
		int i;
		for(i=0;i<memCache.size();i++)
			if(memCache.elementAt(i).getDescriptor().path().equalsIgnoreCase(signalDesc.path()))
				return memCache.elementAt(i);
		return null;
	}
	
	/** Load a signal from the disk cache */
	private Signal getFromDisk(SignalDesc desc){
		Signal sig;
		
		if(cacheRoot == null)
			return null;
		
		try{
			if(desc instanceof PPFDesc){
				sig = new PPF(cacheRoot, (PPFDesc)desc);
			}else if(desc instanceof JPFDesc){
				sig = new JPF(cacheRoot, (JPFDesc)desc);
			}else
				return null;
					
			if (useMemoryCache) addToMemory(sig);	//add it to the memory cache
			return sig;			//and return happy
		}catch(FileNotFoundException e){
			return null;
		}catch(IOException e){
			System.err.println("WARNING: IO ERROR trying to read cache HDF5 file for signal '"+desc+"': "+
					e.getMessage());
			return null;
		}
	}
	
	/** Adds a signal to the fetcher's memory cache */
	private void addToMemory(Signal sig){
		//look for any data objects with the same path and remove them
		for(int i=0;i<memCache.size();i++)
			if(memCache.elementAt(i).getDescriptor().path().equalsIgnoreCase(sig.getDescriptor().path()))
				memCache.remove(i);
		
		//add this new one
		memCache.add(sig);		
	}
	
	/** Frees a signal from memory */
	public void freeFromMemory(Signal sig){
		for(int i=0;i<memCache.size();i++)
			if(memCache.elementAt(i).getDescriptor().path().equalsIgnoreCase(sig.getDescriptor().path()))
				memCache.remove(i);
	}
		
	/** Writes a signal to cache */
	public void writeToCache(Signal sig){
		if(cacheRoot == null || writeInhibit)
			throw new RuntimeException("Cannot write signal to cache: No cacheRoot or write-inhibit active.");
		
		try{
			sig.writeToCache(cacheRoot);
		}catch(IOException err){ //not critical, but ought to make some noise
			System.err.println("WARNING: Supposed to be caching but "+cacheRoot+
					sig.getDescriptor().path()+" couldn't be written:\n" + err.getMessage());
			err.printStackTrace();
		}
	}

	/** Fetches a signal from MDS+ */
	private Signal getFromMDS(SignalDesc desc){
		Signal sig;
		if(mdsConn==null)
			return null; //if we're not connected, give up
		
		sig = doMDSGetSignal(desc);
		
		//add the object to memory
		if (useMemoryCache) addToMemory(sig);
		
		//if we got it and we're caching, write the object to disk, for future
		if(sig != null && cacheRoot != null && !writeInhibit)
			writeToCache(sig);
			
		return sig;
	}
	
	/** Fetches a PPF datatype from MDS+ */
	private Signal doMDSGetSignal(SignalDesc desc){
		Signal sig;
		
		if(!quiet)
			System.out.print("MDSPlus request for '"+desc+"': ");
		
		try{
			String dataRequestStr;
			
			//If a UID is specified, we need to make a call to ppfuid() 
			if(desc instanceof PPFDesc){
				PPFDesc ppfDesc = ((PPFDesc)desc);
				String uid;
				uid = (ppfDesc.getUid().length() > 0) ? ppfDesc.getUid() : "JETPPF";
				
				if(!quiet)
					System.out.print("PPFUID('"+uid+"')...");
				MDSData mdsDesc = mdsConn.makeRequest("ppfuid('"+uid+"')");
				if(!quiet)
					System.out.println("Done(" + mdsDesc.status + "), ");
				
				int seq = ppfDesc.getSeq();
				dataRequestStr = "_sig=ppf(\"" + ppfDesc.getDda() + "/" + ppfDesc.getDtype() + ((seq > 0) ? ("/" + seq) : "") +  "\"," + ppfDesc.getPulse() + ")";
				
			}else if(desc instanceof JPFDesc){
				JPFDesc jpfDesc = ((JPFDesc)desc);
				
				if(jpfDesc.isRaw())
					dataRequestStr = "_sig=jpfraw(\"";
				else
					dataRequestStr = "_sig=jpf(\"";
				
				dataRequestStr += (jpfDesc.isOffline() ? "off/" : "") + jpfDesc.getSubSystem() + "/" + jpfDesc.getSignal() +"\"," + jpfDesc.getPulse()+")";
					
			}else
				throw new IllegalArgumentException("Unknown type of signal '"+desc+"'");
				
			//now call ppf() to request the actual data (which is returned, and so stored in _sig server side)
			if(!quiet) System.out.print("Signal call...");
			MDSData mdsDesc = mdsConn.makeRequest(dataRequestStr);
			
			//status check
			if (!mdsDesc.isStatusOK() )
				throw new RuntimeException("Could not read signal '"+desc+"': " + mdsDesc.GetErrorString());				
			
			if(mdsDesc.isString()){
				if(!quiet) System.out.println("failed(String:"+mdsDesc.String()+").");
				throw new RuntimeException("Request for signal '"+desc+"' returned a string: '"+mdsDesc.String()+"'");
			}
			
			if(mdsDesc.dims.length <= 0){
				if(!quiet) System.out.println("failed(dims=0).");
				throw new RuntimeException("Request for signal '"+desc+"' returned empty. (doesn't exist?)");
			}
			
			int rank = mdsDesc.getRank();
			
			if(!quiet) System.out.print("OK, ");
			
			//If a 2D PPF, transpose the data since the PPF has X index changing fastest 
			if(desc instanceof PPFDesc && rank == 2)				
				mdsDesc.rotate2DData();
			
			int dataDims[] = mdsDesc.getDimsFastestLast();			
			Object data, t, x = null;
			
			
			//Raw JPFS are a very special case
			if(desc instanceof JPFDesc && ((JPFDesc)desc).isRaw()){ 
					//for some reason the MDS+ system thinks the raw jpf return is in integers
					//well... it's just not. We want it REALLY RAW. so just get the byte[] array
					//from inside the MDS+ data descriptor and ignore everything else it says.
				
					//Also we don't try to request the time vector, as it makes no sense 					
					sig = new JPF((JPFDesc)desc, mdsDesc.data);
										
			}else{ //we go for do things mostly the same of JPFs and PPFs
				
				//The actual data came in the reply to the initial request						
				data = mdsDesc.getArray();

				//Time vector is the deepest dimension (since it's usually longest this makes most sense in java too) 
				if(!quiet) System.out.print("tVec...");				
				mdsDesc = mdsConn.makeRequest("dim_of(_sig,"+(rank-1)+")");
				
				if (!mdsDesc.isStatusOK() ){
					if(!quiet) System.out.println("Failed(No tvec).");
					throw new RuntimeException("Could not T vector of signal '"+desc+"': " + mdsDesc.GetErrorString());
				}
				
				if(!quiet) System.out.print("OK, ");
				
				if(mdsDesc.getRank() != 1){
					if(!quiet) System.out.println("Failed(Bad tvec).");
					throw new MdsPlusException("Wrong number of dims for time vector of signal '"
												+desc+"' ("+mdsDesc.getRank()+")");
				}
				
				
				// FIXME: The following statement will take the first elements in the time vector
				// if the dimensions do not match between data and time vectors. The original version
				// throws an exeption (commented out below). It is changed to allow for a common mistake 
				// when the magnetics JPFs are written, for example jpf/75210/DA/CDE-C101.
				// It seems that when this happens the time vector is also stored as a double[]
				if( mdsDesc.getDimsFastestLast()[0] != dataDims[(rank-1)]) {
					System.err.println("Number of elements in time vector of signal '"
							+ desc + "' (" + mdsDesc.getDimsFastestLast()[0] + ") does not match length of that"
							+ "dimension of data ("	+ dataDims[(rank-1)]+")");
					System.err.println("First "+(dataDims[(rank-1)])+" elements will be used. Should you be reading with #raw?");
					Object tObject = mdsDesc.getArray();
					if (tObject instanceof double[]) {
						double[] tArray = (double[]) tObject;
						if (data instanceof double[]) {
							double[] tNew = new double[dataDims[(rank-1)]];
							for(int i=0;i<tNew.length;++i) {
								tNew[i] = tArray[i];
							}
							t = tNew;							
						} else {
							float[] tNew = new float[dataDims[(rank-1)]];
							for(int i=0;i<tNew.length;++i) {
								tNew[i] = (float) tArray[i];
							}
							t = tNew;														
						}
					} else 	if (tObject instanceof float[]) {
						float[] tArray = (float[]) tObject;
						if (data instanceof double[]) {
							double[] tNew = new double[dataDims[(rank-1)]];
							for(int i=0;i<tNew.length;++i) {
								tNew[i] = tArray[i];
							}
							t = tNew;							
						} else {
							float[] tNew = new float[dataDims[(rank-1)]];
							for(int i=0;i<tNew.length;++i) {
								tNew[i] = (float) tArray[i];
							}
							t = tNew;														
						}
					} else {
						throw new MdsPlusException("Exception when trying to adjust for differing sizes of data and time dimensions");
					}
				} else {
					t = mdsDesc.getArray();		
				}
									
				/*
				if( mdsDesc.getDimsFastestLast()[0] != dataDims[(rank-1)]) 
					throw new MdsPlusException("Number of elements in time vector of signal '"
							+ desc + "' (" + mdsDesc.getDimsFastestLast()[0] + ") does not match length of that"
							+ "dimension of data ("	+ dataDims[(rank-1)]+")");								
				
				t = mdsDesc.getArray();
				*/	
				
				if(rank > 1){				
					if(!quiet) System.out.print("xVec...");
					mdsDesc = mdsConn.makeRequest("dim_of(_sig,0)");
					
					if (!mdsDesc.isStatusOK() )
						throw new RuntimeException("Could not X vector of signal '"+desc+"': " + mdsDesc.GetErrorString());				
					
					if(!quiet) System.out.print("OK, ");
					
					if(mdsDesc.getRank() != 1)
						throw new MdsPlusException("Wrong number of dims for X vector of signal '"
													+desc+"' ("+mdsDesc.getRank()+")");
					
					if( mdsDesc.getDimsFastestLast()[0] != dataDims[0])	
						throw new MdsPlusException("Number of elements in X vector of signal '"
								+ desc + "' (" + mdsDesc.getDimsFastestLast()[0] + ") does not match length of that"
								+ "dimension of data ("	+ dataDims[0]+")");
					
					x = mdsDesc.getArray();
				}
				
				if(desc instanceof PPFDesc){
					PPFDesc ppfDesc = (PPFDesc)desc;
					if(rank == 1){
						sig = new PPF(ppfDesc, data, t);
					}else if(rank == 2){						
						sig = new PPF(ppfDesc, data, x, t);
					}else
						throw new RuntimeException("Data for signal '"+desc+"' from MDS+ is "+rank+"D, was expecting 1D or 2D for PPF");
					
					getPPFExtraInfo((PPF)sig);
									
				}else{ //else if JPF - (niether JPF/PPF case was caught earlier)
					if(rank != 1)				
						throw new RuntimeException("Data for signal '"+desc+"' from MDS+ is "+rank+"D, was expecting 1D for JPF");
										
					sig = new JPF((JPFDesc)desc, data, t);				
				}
			}
			
			if(!quiet) System.out.println("All Done.");
				
			
		} catch(MdsPlusException err){
			if(!quiet)
				System.out.println("FAILED.");
			throw new RuntimeException("Cannot get "+desc+" from MDSPlus: "+err.toString());
		}
		
		return sig;
	}
	
	private void getPPFExtraInfo(PPF sig){
		PPFDesc ppfDesc = sig.getDescriptor();
		
		if(!quiet)
			System.out.print("PPFDTI() ... ");
		MDSData mdsDesc = mdsConn.makeRequest(				
				//System already init'ed by the ppf() call in doMDSGetSignal 
				// and possibly with UID so leave the init out here:
				//"build_call(*, *, \"ppfgo_\", 0, 0, 0, 0)," + 
				
				//Setup MDS+ space variables with correct amount of space
				"_cmt=\"                        \"," +	// Comment
				"_ierr=-1," +							// PPF System return code
				" _nx=-1," +							// No. X Points
				" _nt=-1," +							// No. Y Points
				"_fmt=\" \"," +							// Format 'F' or 'I' for float/int
				" _units=\"        \"," +				// 'Units' string
				"_sstat=-1," +							// System status flag
				" _ustat=-1," +							// User status flag
				
				//Make the call to PPFDTI() for datatype information
				"build_call(*, *, \"ppfdti_\"," +
						"long(" + ppfDesc.getPulse() + ")," +
						"long(" + ppfDesc.getSeq() + ")," +
						"\"" + ppfDesc.getDdaFourCharUCase() + "\"," +
						"\"" + ppfDesc.getDtypeFourCharUCase() + "\"," +
						"ref(_nx), ref(_nt)," +
						"ref(_fmt), ref(_units)," +
						"ref(_cmt), ref(_sstat)," +
						"ref(_ustat), ref(_ierr))," +
						
				//Return all the integer things this time
				"[_ierr,_sstat,_ustat]");
		
		if (!mdsDesc.isStatusOK() )
			throw new RuntimeException("Could not get PPF datatype info for signal '"+ppfDesc+"': " + mdsDesc.GetErrorString());
		
		if(!quiet) System.out.print("OK, ");
		
		int ret[] = (int[])mdsDesc.getArray();
		
		if(ret[0] != 0)
			throw new RuntimeException("Could not get PPF datatype info for signal '"+ppfDesc+"': ppfdti() returned IERR = " + ret[0]);				
		
		sig.setSystemStatusFlag(ret[1]);
		sig.setUserStatusFlag(ret[2]);
		
		mdsDesc = mdsConn.makeRequest("_cmt"); //Fetch the comment from MDS+ space
		sig.setComment(mdsDesc.String());
		
		mdsDesc = mdsConn.makeRequest("_units"); // Fetch the units string
		sig.setDataUnits(mdsDesc.String());
	}
	
	public int getLatestPulse(){
		MDSData mdsDesc = mdsConn.makeRequest(
				"build_call(*, *, \"ppfgo_\", 0, 0, 0, 0)," +
				" _maxShot=0," +
				"build_call(*, *, \"pdmsht_\", ref(_maxShot))," +
				"_maxShot");
		
		int i[] = (int[])mdsDesc.getArray();
		return i[0];
	}
	
	/** Get the path being used to for the signal disk cache */ 
	public String getCacheRoot() {
		return cacheRoot;
	}
	
	/** Determine if the fetcher will save the signals fetched to disk */
	public boolean isCachingToDisk(){
		//we will if we have a cacheRoot and the write inhibit isn't active
		return cacheRoot != null && !writeInhibit;
	}
	
	/** Determine if the fetcher will save the signals fetched to memory */
	public boolean isCachingToMemory(){
		//we will if we have a cacheRoot and the write inhibit isn't active
		return useMemoryCache;
	}

	public final void dumpMemoryCache(){
		for(Signal s : memCache) {
			System.out.println(s);
		}		
	}
	
	public void freeFromMemory(String path) {
		freeFromMemory(getSig(path));
	}
	
	public void ensureOnDisk(String path) {
		getSig(path);
	}
	
	
	/* LEGACY CODE - to directly replace JetMDSReader */
	/** @deprecated Legacy code to directly replace JetMDSReader */
	public byte[] getRawJPF(String path){
		//The raw requirement should be in the path so getData should end up as byte[]
		return (byte [])((Signal)getSig(path)).getData();
	}

	/** @deprecated Legacy code to directly replace JetMDSReader */
	public double[][] get2DData(String path) {
		return OneLiners.transpose((double [][])((Signal)getSig(path)).getDataAsType(double.class));
	}
	
	/** @deprecated Legacy code to directly replace JetMDSReader */
	public double[] get1DData(String path) {
		return (double [])(getSig(path)).getDataAsType(double.class);
	}
	
	/** @deprecated Legacy code to directly replace JetMDSReader */
	public double[] getTimeVector(String path) {
		Signal s = getSig(path);
		return (double [])s.getCoordsAsType(s.getRank() - 1, double.class);
	}
	
	/** @deprecated Legacy code to directly replace JetMDSReader */
	public double[] getXVector(String path) {
		return (double [])(getSig(path)).getCoordsAsType(0, double.class);
	}
	

	
}
