package ca.ucalgary.seahawk.util;

import org.biomoby.registry.meta.*;

import java.io.*;
import java.net.*;
import java.util.Properties;

/**
 * A class for storing user preferences affecting the behaviour of Seahawk.
 */

public class SeahawkOptions{
    public static final String CACHE_POLICY_PROP = "cacheExpiryInHours";
    public static final String REFERRER_POLICY_PROP = "sendReferrerHeader";
    public static final String TEMP_DIR_PROP = "tempDataDir";
    public static final String REGISTRY_SYNONYM_PROP = "registrySynonym";
    public static final String REGISTRY_ENDPOINT_PROP = "registryEndpoint";
    public static final String REGISTRY_NAMESPACE_PROP = "registryNamespace";
    public static final String CONVERTER_HOST_PROP = "documentConverterHost";
    public static final String CONVERTER_PORT_PROP = "documentConverterPort";
    public static final String CUSTOM_REGISTRY_SYNONYM = "custom";

    public static final String PROPS_FILE_NAME = "seahawkUserPrefs.xml";
    public static final String PROPS_FILE_COMMENT = "This is the only file Seahawk stores on your system indefinitely.\n" +
	"If you delete it, your preferences will be reset the next time you run Seahawk.";
    public static final String PROPS_FILE_ENCODING = "UTF-8";
    public static final String PROPS_FILE_SYS_PROPERTY = "seahawk.prefs";

    private static Registry registry = null;
    private static File tmpDir = new File(System.getProperty("java.io.tmpdir"));
    private static boolean referrerPolicy = true;
    private static double cacheExpiryInHours = 24.0;
    // Used OpenOffice to convert MS Office docs to RTF, currently a Solaris zone on moby.ucalgary.ca
    private static String documentConverterHost = "136.159.169.81";  
    private static int documentConverterPort = 8100;

    /**
     * Sets the options based on a shortcircuiting set of 
     * rules (PROPS_FILE_SYS_PROPERTY, java.io.tmpdir/PROPS_FILE_NAME),
     * using a properties file saved in the location saveSettings() puts it.
     */
    public static void restoreSettings(){
	String sysProp = System.getProperty(PROPS_FILE_SYS_PROPERTY);
	if(sysProp != null && sysProp.length() > 0){
	    URL url = null;

	    // Try as file, resource, and URL
	    File overrideFile = new File(sysProp);
	    if(overrideFile.exists()){
		if(!overrideFile.isFile()){
		    System.err.println("Could not load '"+sysProp+"'. It exists, but is not a file.");
		}
		else if(!overrideFile.canRead()){
		    System.err.println("Could not load file '"+sysProp+"'. It exists, but is not readable.");
		}
		else{
		    try{
			url = overrideFile.toURI().toURL();
		    } catch(Exception e){
			e.printStackTrace();
			System.err.println("Could not load file '"+sysProp+
					   "'. It exists, but could not be converted to a URL.");
		    }
		}
	    }

	    if(url == null){
		url = (new SeahawkOptions()).getClass().getClassLoader().getResource(sysProp);
	    }

	    if(url == null){
		try{
		    url = new URL(sysProp);
		} catch(MalformedURLException murle){
		}
	    }

	    if(url == null){
		System.err.println("The preferences file specified via the system property " + 
				   PROPS_FILE_SYS_PROPERTY + " could not be resolved to a " +
				   "valid file, resopurce or URL.  Using the default preferences file.");
	    }
	    else{
		try{
		    restoreSettings(url);
		    return;
		} catch(Exception e){
		    e.printStackTrace();
		    System.err.println("There was an error loading the preferences file specified (" +
				       PROPS_FILE_SYS_PROPERTY + "). " +
				       "Loading the default preferences file instead.");
		}
		
	    }
	}  //end sysProp defined
	File defaultsFile = getDefaultsFile();
	if(defaultsFile.exists()){
	    try{
		restoreSettings(defaultsFile.toURI().toURL());
	    } catch(Exception e){
		System.err.println("There was an error loading the preferences file (" +
				   defaultsFile + "), using default values instead.");
	    }
	}
    }

    /**
     * Sets the options based on the contents of the file (created with saveSettings()).
     */
    public static void restoreSettings(URL u) throws Exception{
	Properties properties = new Properties();
	properties.loadFromXML(u.openStream());

	String boolString = properties.getProperty(REFERRER_POLICY_PROP);
	if(boolString != null && boolString.length() > 0){
	    referrerPolicy = Boolean.parseBoolean(boolString);
	} 
	else{
	    System.err.println("No referrer header policy (" +REFERRER_POLICY_PROP+ 
			       ") provided in the Seahawk config file (" + 
			       u + "), using default of \"true\" " +
			       "(will send referrer service header to services).");
	}

	String floatString = properties.getProperty(CACHE_POLICY_PROP);
	if(floatString != null && floatString.length() > 0){
	    double hours = cacheExpiryInHours;
	    try{
		hours = Double.parseDouble(floatString);
	    } catch(NumberFormatException nfe){
		nfe.printStackTrace();
		System.err.println("Ignoring the cache expiry policy (" +CACHE_POLICY_PROP+ 
				   ") provided in the Seahawk config file (" + 
				   u + "), it is not in valid floating-point number format: " + 
				   floatString);
	    }
	    setCacheExpiry(hours);
	}

	String dirPath = properties.getProperty(TEMP_DIR_PROP);
	if(dirPath != null && dirPath.length() > 0){
	    try{
		setTempDir(new File(dirPath));
	    } catch(Exception e){
		e.printStackTrace();
		System.err.println("Ignoring the temporary data directory location (" + TEMP_DIR_PROP+ 
				   ") provided in the Seahawk config file (" + 
				   u + "), it is not in valid (either can't be created, or written to): " + 
				   dirPath);
	    }
	}

	String regSynonym = properties.getProperty(REGISTRY_SYNONYM_PROP);
	String regNamespace = properties.getProperty(REGISTRY_NAMESPACE_PROP);

	String regEndpoint = properties.getProperty(REGISTRY_ENDPOINT_PROP);
	URL endpointURL = null;
	if(regEndpoint != null && regEndpoint.length() > 0){
	    try{
		endpointURL = new URL(regEndpoint);
	    } catch(MalformedURLException murle){
		murle.printStackTrace();
		System.err.println("Ignoring the registry endpoint (" + REGISTRY_ENDPOINT_PROP + 
				   ") provided in the Seahawk config file (" + 
				   u + "), it is not in valid URL format: " + 
				   regEndpoint);
	    }
	}

	String convHost = properties.getProperty(CONVERTER_HOST_PROP);	
	if(convHost != null && convHost.length() > 0){
	    setDocConverterHost(convHost);
	}
	String intString = properties.getProperty(CONVERTER_PORT_PROP);
	if(intString != null && intString.length() > 0){
	    int portNum = documentConverterPort;
	    try{
		portNum = Integer.parseInt(intString);
	    } catch(NumberFormatException nfe){
		nfe.printStackTrace();
		System.err.println("Ignoring the document converter port number (" +CONVERTER_PORT_PROP+ 
				   ") provided in the Seahawk config file (" + 
				   u + "), it is not in valid integer number format: " + 
				   intString);
	    }
	    setDocConverterPort(portNum);
	}

	// We determine the registry first based on the alias, then fallback on the endpoint
	// This should be the LAST thing we do (note the return statement in the for loop)
	if(regSynonym != null && regSynonym.length() > 0){
	    // See if it's one we know about
	    RegistriesList regList = new RegistriesList();
	    for(Registry reg: regList.getAll()){
		if(regSynonym.equals(reg.getSynonym())){
		    setRegistry(reg);
		    return;
		}
	    }
	    if(endpointURL == null){
		System.err.println("The registry alias '" + regSynonym + "' specified in the " +
				   "config file (" + u.toString() + ") is unknown to Seahawk, and " +
				   "a valid registry endpoint was not specified.  Using the " +
				   "default registry instead.");
	    }
	    else if(regNamespace == null || regNamespace.length() == 0){
		System.err.println("The registry alias '" + regSynonym + "' specified in the " +
				   "config file (" + u.toString() + ") is unknown to Seahawk, and " +
				   "a registry namespace was not specified.  Using the " +
				   "default registry instead.");
	    }
	    else{
		setRegistry(new Registry(regSynonym, regEndpoint, regNamespace));
	    }
	}
	else{
	    if(endpointURL == null){
		System.err.println("A valid registry endpoint was not specified.  Using the " +
				   "default registry instead.");
	    }
	    else if(regNamespace == null || regNamespace.length() == 0){
		System.err.println("A valid registry namespace (parameter " + REGISTRY_NAMESPACE_PROP + 
				   " in file " + u + ") was not specified.  Using the " +
				   "default registry instead.");
	    }
	    else{
		setRegistry(new Registry(CUSTOM_REGISTRY_SYNONYM, regEndpoint, regNamespace));
	    }
	}
    }

    /**
     * Store the settings in text form that can be understood by restoreSettings().
     */
    public static void saveSettings(OutputStream os) throws IOException{
	Properties properties = new Properties();
	properties.setProperty(CACHE_POLICY_PROP, ""+cacheExpiryInHours);
	properties.setProperty(REFERRER_POLICY_PROP, ""+referrerPolicy);
	properties.setProperty(TEMP_DIR_PROP, tmpDir.getCanonicalFile().getPath());
	properties.setProperty(CONVERTER_HOST_PROP, documentConverterHost);
	properties.setProperty(CONVERTER_PORT_PROP, ""+documentConverterPort);
	if(registry != null){
	    properties.setProperty(REGISTRY_SYNONYM_PROP, registry.getSynonym());	
	    properties.setProperty(REGISTRY_ENDPOINT_PROP, registry.getEndpoint());
	    properties.setProperty(REGISTRY_NAMESPACE_PROP, registry.getNamespace());
	}

	properties.storeToXML(os,
			      PROPS_FILE_COMMENT,
			      PROPS_FILE_ENCODING);
    }

    /**
     * Saves the current values of the preferences to a default location that restoreSettings() will use too.
     *
     * @return true of the save was successful, false otherwise
     */
    public static boolean saveSettings(){
	File defaultsFile = getDefaultsFile();
	File defaultsParentFile = defaultsFile.getParentFile();
	if(defaultsParentFile != null &&
	   !defaultsParentFile.exists()){
	    if(!defaultsParentFile.mkdirs()){
		System.err.println("Warning: the preferences file (" + defaultsFile + 
				   ") is not writeable because its parent directory " +
				   "could not be created, preferences will be lost when " +
				   "Seahawk terminates.");
		return false;
	    }
	}
	if(defaultsFile.isDirectory()){
	    System.err.println("Warning: the preferences file (" + defaultsFile + 
			       ") is not writeable because a directory with the same " +
			       "name already exists, preferences will be lost when " +
			       "Seahawk terminates.");
	    return false;
	}
	if(defaultsFile.exists() && !defaultsFile.canWrite()){
	    System.err.println("Warning: the preferences file (" + defaultsFile + 
			       ") is not writeable, preferences will be lost when " +
			       "Seahawk terminates.");
	    return false;
	}

	try{
	    FileOutputStream fileOutputStream = new FileOutputStream(defaultsFile);
	    saveSettings(fileOutputStream);
	} catch(Exception e){
	    e.printStackTrace();
	    System.err.println("Warning: could not save preferences (file " + defaultsFile + 
			       "), they will be lost when Seahawk terminates.");
	    return false;
	}
	return true;
    }

    /**
     * @return the file system location where preferences are stored (whether the file currently exists or not)
     */
    public static File getDefaultsFile(){
	return new File(System.getProperty("user.home"), PROPS_FILE_NAME);
    }

    /**
     * @param hours the number of hours cached data (e.g. ontologies & service lookups) are valid for, 0 or negative numbers meaning until Seahawk is closed
     */
    public static void setCacheExpiry(double hours){
	cacheExpiryInHours = hours < 0 || hours == Double.NaN ? 0 : hours;
    }

    /**
     * @return the number of hours cached data (e.g. ontologies & service lookups) are valid for
     */
    public static double getCacheExpiry(){
	return cacheExpiryInHours;
    }

    /**
     * @return the Moby registry that will be used by default to resolve services, data types, etc.
     */
    public static Registry getRegistry(){
	return registry;
    }

    /**
     * @param reg the Moby registry that should be used by default to resolve services, data types, etc.
     */
    public static void setRegistry(Registry reg){
	registry = reg;
    }

    /**
     * @return where temporary files are stored
     */
    public static File getTempDir(){
	return tmpDir;
    }

    /**
     * @param dir directory where temporary files should be stored
     *
     * @throws Exception if the parameter is not a directory, or cannot be created or written to 
     */
    public static void setTempDir(File dir) throws Exception{
	if(!dir.exists()){
	    dir.mkdirs();
        }
	if(!dir.isDirectory()){
	    throw new IOException("The temporary data path given (" + dir.toString() + 
				  ") is not a directory as required");
	}
	if(!dir.canWrite()){
	    throw new IOException("The temporary data path given (" + dir.toString() + 
				  ") is not writeable as required");
	}
	if(!dir.canRead()){
	    throw new IOException("The temporary data path given (" + dir.toString() + 
				  ") is not readable as required");
	}
	tmpDir = dir;		
	RegistryCache.setTempDir(dir);
    }

    /**
     * @param send true if Seahawk should send a referrer service HTTP header to services, otherwise false
     */
    public static void setSendReferrerPolicy(boolean send){
	referrerPolicy = send;
    }

    /**
     * @return true if Seahawk should send a referrer service HTTP header to services, otherwise false
     */
    public static boolean getSendReferrerPolicy(){
	return referrerPolicy;
    }

    /**
     * @param port positive number representing where the OpenOffice file converter is listening
     */
    public static void setDocConverterPort(int port) throws Exception{
	if(port < 0){
	    throw new Exception("The port number for the document converter must be greater " +
				"than 0, but an attempt was made to set it to "+port);
	}
	documentConverterPort = port;
    }

    /**
     * @return the port number on which OpenOffice is listening, waiting to do document format conversion
     */
    public static int getDocConverterPort(){
	return documentConverterPort;
    }

    /**
     * For more details on how to run your own file converter, see http://www.artofsolving.com/node/10
     *
     * @param hostname machine on which an OpenOffice file converter is running
     */
    public static void setDocConverterHost(String hostname){
	if(hostname == null ||
	   hostname.length() == 0){
	    hostname = null;
	}
	documentConverterHost = hostname;
    }

    /**
     * May be null (in which case conversions will not be available)
     */
    public static String getDocConverterHost(){
	return documentConverterHost;
    }
}
