package org.biomoby.client;

import org.biomoby.registry.meta.RegistryCache;

import org.biomoby.shared.MobyException;

import java.io.*;
import java.util.*;

/**
 * Implements the functionality of caching by reusing identical calls to
 * MOBY central (e.g. asking multiple times what services take a DNA 
 * sequence in the gi namespace).  This is an in-memory cache in CentralImpl, 
 * but a filesystem cache backs it up here, so calls can be cached between 
 * JVM instances.  This class is also thread-safe, and avoids redundant 
 * concurrent calls to the central registry.
 */

public class CentralCachedCallsImpl extends CentralDigestCachedImpl{
    protected static String CHAR_ENCODING = "UTF-8";
    protected static final String SYNTAX_TYPE = "xml";
    protected static final long THE_EPOCH = 0;  // don't care when the call is made, it shouldn't affect the onto mapping of calls to IDs
    protected static final Properties PROPERTIES = null; // don't have any properties for the call

    protected Map<String,String> inProgressCalls; //<callKey,threadName> used to synchronize concurrent, redundant calls to Central

    /*************************************************************************
     * Default constructor. It connects to a default Moby registry
     * (as defined in {@link #DEFAULT_ENDPOINT}) using a default namespace
     * (as defined int {@link #DEFAULT_NAMESPACE}).
     *************************************************************************/
    public CentralCachedCallsImpl()
	throws MobyException {
	super (DEFAULT_ENDPOINT, DEFAULT_NAMESPACE);
	inProgressCalls = new HashMap<String,String>();
    }

    /*************************************************************************
     * Constructor allowing to specify which Moby Registry to use.
     *
     * @throws MobyException if 'endpoint' is not a valid URL, or if no
     *                          DOM parser is available
     *************************************************************************/
    public CentralCachedCallsImpl (String endpoint)
	throws MobyException {
	super (endpoint, DEFAULT_NAMESPACE);
	inProgressCalls = new HashMap<String,String>();
    }

    /*************************************************************************
     * Constructor allowing to specify which Moby Registry and what
     * namespace to use. If any of the parameters is null, its default
     * value is used instead.
     *<p>
     * @throws MobyException if 'endpoint' is not a valid URL, or if no
     *                          DOM parser was found
     *************************************************************************/
    public CentralCachedCallsImpl (String endpoint, String namespace)
	throws MobyException {
	super (endpoint, namespace);
	inProgressCalls = new HashMap<String,String>();
    }

    /**
     * The implementation of this method is smart enough that if the same call
     * is made more than once, even concurrently(!), we only go to the server once,
     * and use a cached value for all other invocations. 
     */
    protected Object doCall (String method, Object[] parameters)
	throws MobyException {

	Object result = null;

	String callKey = createId(method, parameters);
	if(getCacheMode()){

	    // It's in the cache?
	    if(existsInCache(callKey)){
		return getContents(callKey);
	    }

	    // The same request is already in progress, in another thread?
	    Object inProgressCall = null;
	    synchronized(inProgressCalls){
		String threadName = inProgressCalls.get(callKey);
		if(threadName == null){
		    // No one's currently doing this request...claim it for this thread
		    threadName = Thread.currentThread().getName();
		    inProgressCalls.put(callKey, threadName);
		}
		inProgressCall = threadName;
	    }

	    // The first thread making a call will block subsequent ones with the same callKey
	    synchronized(inProgressCall){
		// Should be true for subsequent calls
		if(existsInCache(callKey)){
		    return getContents(callKey);
		}
		// Should get here only if I'm the first caller for the callKey,
		// or subsequent call when previous doCall() for this callKey throws an exception
		// i.e. setContents() isn't called after doCall()
		try{
		    result = super.doCall(method, parameters);
		    setContents(callKey, result);
		} finally{
		    // Remove the blocker, regardless of whether an Exception was thrown or not.
		    inProgressCalls.remove(callKey);
		}
	    }
	}
	else{
	    result = super.doCall(method, parameters);
	}

	return result;
    }

    // check existence of a cached object
    public boolean existsInCache (String id) {
	if(!getCacheMode()){
	    return false;
	}
	if(super.existsInCache(id)){
	    return true;
	}
	
	File cachedDataFile = RegistryCache.getCentralCallFile(getRegistryEndpoint(), id);
	// See if it's in the disk cache, if so, load it to memory
	if(cachedDataFile != null){
	    try{
		Object cachedValue = loadDataFromFile(cachedDataFile);
		// Save the disk data to memory
		super.setContents(id, cachedValue);
	    } catch(Exception e){
		e.printStackTrace();
		System.err.println("Could not load data from cache file " + cachedDataFile);
		return false;
	    }
	    return true;
	}
	else{
	    return false;
	}
    }

    protected Object loadDataFromFile(File cacheFile) throws Exception{
	char[] contentsBuffer = new char[(int) cacheFile.length()];  // assumes filesize < 2GB
	FileReader cacheFileReader = new FileReader(cacheFile);
	cacheFileReader.read(contentsBuffer);	
	return new String(contentsBuffer);
    }

    protected void storeDataToFile(File cacheFile, Object data) throws Exception{
	if(data instanceof CharSequence){
	    FileWriter cacheFileWriter = new FileWriter(cacheFile);
	    cacheFileWriter.write(((CharSequence) data).toString());
	    cacheFileWriter.close();
	}
	else if(data instanceof Serializable){
	    throw new Exception("Serialization in cache not yet implemented");
	}
	else{
	    throw new Exception("Asked to serialize data that is neither a CharSequence, " +
				"nor serializable.  Found " + data.getClass().getName());
	}
    }

    // cache an object
    public void setContents (String id, java.lang.Object data) {
	if(!getCacheMode()){
	    return;
	}

	super.setContents(id, data);
	File cachedDataFile = RegistryCache.calcCentralCallFile(getRegistryEndpoint(), id);
	try{
	    storeDataToFile(cachedDataFile, data);
	} catch(Exception e){
	    System.err.println("Could not store data to cache file " + cachedDataFile);
	}
    }    

    protected String createId(String method, Object[] parameters){
	// Set the semantics to the call parameters's string representation concatenated
	// If we were to be pedantic, we should covert the xml into a canonical format
	// to eliminate issues of whitespace, single vs. double quotes, etc.
	StringBuffer semanticType = new StringBuffer();
	for(int i = 0; parameters != null && i < parameters.length; i++){
	    semanticType.append(parameters[i]);
	}

	return createId(method, semanticType.toString(), SYNTAX_TYPE, THE_EPOCH, PROPERTIES);
    }

    /**
     * Creates an ID of the parameters simply by concatenating them.
     */
    public String createId(String rootName,
			   String semanticType,
			   String syntaxType,
			   long lastModified,
			   Properties props){
	return (rootName+semanticType+syntaxType+lastModified+props);
    }
}
