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 signals.mast.IDAM;
import descriptors.*;
import descriptors.JET.*;
import descriptors.mast.IDAMDesc;

/** Fetches IDAM Signals from the MAST 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 MastMDSFetcher 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 (fuslwn.culham.ukaea.org.uk:8000) */
	public MastMDSFetcher(String cachePath) {
		this("auto", -1, cachePath, false);		
	}
	
	/** Create signal fetcher through MDS connection to the 
	 * default JET MDS+ server (fuslwn.culham.ukaea.org.uk:8000) */
	public MastMDSFetcher(String cachePath, boolean connectImmeadiately) {
		this("auto", -1, cachePath, connectImmeadiately);		
	}
	
	/** Create signal fetcher through specified MDS+ connection 
	 * @param hostAndPort host:port*/
	public MastMDSFetcher(String cacheRoot, String hostAndPort) {		
		this( hostAndPort, -1, cacheRoot, false);		
	}
	
	/** Create signal fetcher through specified MDS+ connection */
	public MastMDSFetcher(String host, int port, String cacheRoot) {
		this(host, port, cacheRoot, false);
	}
	
	/** Create signal fetcher through specified MDS+ connection */
	public MastMDSFetcher(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) ){
			/* 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 = 8766;
			}else{
				host = "fuslwn.culham.ukaea.org.uk";
				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("MastMDSFetcher 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);
						
		return getSig(new IDAMDesc(path), overrideCaches);
	}
	
	/** 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 IDAMDesc){
				sig = new IDAM(cacheRoot, (IDAMDesc)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;
		}
	}
	
	/** Load a signal from the disk cache */
	private Signal getEFromDisk(SignalDesc desc){
		Signal sig;
		
		if(cacheRoot == null)
			return null;
		
		try{
			if(desc instanceof IDAMDesc){
				sig = new IDAM(cacheRoot, (IDAMDesc)desc,1);			
			}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();
		}
	}

	/** Writes a signal to cache */
	public void writeToCacheE(Signal sig){
		if(cacheRoot == null || writeInhibit)
			throw new RuntimeException("Cannot write signal to cache: No cacheRoot or write-inhibit active.");
		
		try{
			sig.writeToCacheE(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 IDAMDesc){
				IDAMDesc idamDesc = ((IDAMDesc)desc);
				String uid;
				if(idamDesc.getUid().length() > 0)
					throw new RuntimeException("IDAM signal descriptor contains user id, not yet implemented");									
			
				String seq = idamDesc.getSeq() != 0 ? "/"+idamDesc.getSeq() : ""; // FIXME: Not sure MAST seq numbering starts at 0
				dataRequestStr = "_sig=" + (idamDesc.isErrorSignal() ? "idamgeterrorapi":"idamgetapi") + "(\"" + idamDesc.getSignalName() + "\",\""+ idamDesc.getPulse() + seq+"\")";	
			
			}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 mdsMakeDataDouble = mdsConn.makeRequest("_status = idamsetproperty(\"get_datadble\")");
			MDSData mdsMakeDimDouble = mdsConn.makeRequest("_status = idamsetproperty(\"get_dimdble\")");
			MDSData mdsData = mdsConn.makeRequest(dataRequestStr);
			
			//status check
			if (!mdsData.isStatusOK() )
				throw new RuntimeException("Could not read signal '"+desc+"': " + mdsData.GetErrorString());				
			
			if(!quiet) System.out.print("OK, ");
			
			if(mdsData.isString())			
				throw new RuntimeException("Request for signal '"+desc+"' returned a string: '"+mdsData.String()+"'");
			
			if(mdsData.dims.length <= 0)			
				throw new RuntimeException("Request for signal '"+desc+"' returned empty. (doesn't exist?)");
			
			int rank = mdsData.getRank();
			
			
			// Hack: When IDAM 3D, assume 2D
			if (rank == 3) {
				mdsData.dims = new int[] { mdsData.dims[0], mdsData.dims[1] };
				rank = 2;
			}
			
			//If a 2D PPF, transpose the data since the PPF has X index changing fastest 
			if(desc instanceof PPFDesc && rank == 2)				
				mdsData.rotate2DData();
			
			if(desc instanceof IDAMDesc && rank == 2)				
				mdsData.rotate2DData();
			
			int dataDims[] = mdsData.getDimsFastestLast();			
			Object data, t, x = null;
							
			//The actual data came in the reply to the initial request						
			data = mdsData.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...");				
			mdsData = mdsConn.makeRequest("dim_of(_sig,"+(rank-1)+")");
			
			if (!mdsData.isStatusOK() )
				throw new RuntimeException("Could not T vector of signal '"+desc+"': " + mdsData.GetErrorString());				
			
			if(!quiet) System.out.print("OK, ");
			
			if(mdsData.getRank() != 1)
				throw new MdsPlusException("Wrong number of dims for time vector of signal '"
											+desc+"' ("+mdsData.getRank()+")");
	
			if( mdsData.getDimsFastestLast()[0] != dataDims[(rank-1)])	
				throw new MdsPlusException("Number of elements in time vector of signal '"
						+ desc + "' (" + mdsData.getDimsFastestLast()[0] + ") does not match length of that"
						+ "dimension of data ("	+ dataDims[(rank-1)]+")");

			
			t = mdsData.getArray();		
			
			if(rank > 1){				
				if(!quiet) System.out.print("xVec...");
				mdsData = mdsConn.makeRequest("dim_of(_sig,0)");
				
				if (!mdsData.isStatusOK() )
					throw new RuntimeException("Could not X vector of signal '"+desc+"': " + mdsData.GetErrorString());				
				
				if(!quiet) System.out.print("OK, ");
				
				if(mdsData.getRank() != 1)
					throw new MdsPlusException("Wrong number of dims for X vector of signal '"
												+desc+"' ("+mdsData.getRank()+")");
				
				if( mdsData.getDimsFastestLast()[0] != dataDims[0])	
					throw new MdsPlusException("Number of elements in X vector of signal '"
							+ desc + "' (" + mdsData.getDimsFastestLast()[0] + ") does not match length of that"
							+ "dimension of data ("	+ dataDims[0]+")");
				
				x = mdsData.getArray();
			}
			
			if(desc instanceof IDAMDesc){
				IDAMDesc idamfDesc = (IDAMDesc)desc;
				if(rank == 1){
					sig = new IDAM(idamfDesc, data, t);
				}else if(rank == 2){						
					sig = new IDAM(idamfDesc, data, x, t);
				}else
					throw new RuntimeException("Data for signal '"+desc+"' from MDS+ is "+rank+"D, was expecting 1D or 2D for PPF");								
			}else{ 
				throw new RuntimeException("Unknown descriptor: "+desc.getClass().getName());
			}

			
			if(!quiet) System.out.println("All Done.");
				
			
		} catch(MdsPlusException err){ 
			throw new RuntimeException("Cannot get "+desc+" from MDSPlus: "+err.toString());
		}
		
		return sig;
	}
	
	/** 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 ensureOnDisk(String path) {
		getSig(path);
	}
	
	
}
