package skyview.survey;

/** Translate a simple survey name into an XML file.
 * 
 *  Surveys can come from any of three sources:
 *   <ul>
 *     <li> If the resource survey.manifest
 *      is available, each line in the manifest defines a survey XML description.
 *      As a system resource this file will be searched for in all of the
 *      places where a class might be looked for.  When SkyView is
 *      executed within a JAR file the survey manifest will be included.
 *     <li> The user can specify a base directory where
 *      all XML files are assumed to describe SkyView surveys.
 *     <li> The user can specify a survey on the command line
 *      as surveyxml=file
 *    </ul>
 *   If a survey is defined in multiple locations, the later definition
 *   (in terms of this listing) supercedes the earlier.  Thus users
 *   can override the default definitions of surveys.
 *   
 */

import skyview.survey.XMLSurvey;
import skyview.executive.Settings;

import java.io.File;
import java.io.FileInputStream;
import java.io.BufferedInputStream;
import java.io.FilenameFilter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.InputStream;
import java.io.Reader;
import java.io.Serializable;
import java.net.URL;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import javax.xml.parsers.SAXParserFactory;
import javax.xml.parsers.SAXParser;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.Attributes;
import java.util.regex.Pattern;


/** This class provides functionality to translate survey
 *  names into associated survey description files.
 * @author tmcglynn
 */
public class XMLSurveyFinder implements SurveyFinder {
    
    
    public static class SurveyLink implements Serializable {
        String name;
        long   time;
    } 
    
    /** Default location for survey manifest */
    private static String  	defaultSurveyManifest = "survey.manifest";
    /** Cached names file. */
    private static String       defaultSurveyHash     = "surveynames.obj";
    
    /** Filter to get only the XML files */
    private FilenameFilter 	filter = new FilenameFilter() {
	public boolean accept(File file, String name) {
	    return name.endsWith(".xml") || name.endsWith(".XML");
	}
    };
    
    /** Hashmap binding shortname to XML file */
    private HashMap<String,SurveyLink> hash = new HashMap<String,SurveyLink>();

    /** Set up the SurveyFinder and populate the map.  Overloaded to pass a flag for whether the context is simply to dump the survey manifest. */
    public XMLSurveyFinder() {
	this(false);
    }
    public XMLSurveyFinder(boolean dump) {
	getSurveysFromManifest(dump);  // Must be done first since it may reset hash.
	getSurveysFromRoot();
	getSurveysFromUser();
    }

   
    /** Create a cached translation between survey names and files.
     *  This can be used to save a little time when running SkyView.
     *  Note that the hash that is written was already created in the
     *  XMLSurveyFinder constructor above.
     */
    public void dump() {
        try {
            java.io.ObjectOutputStream jos = new java.io.ObjectOutputStream(
                    new java.io.FileOutputStream(defaultSurveyHash));
            jos.writeObject(hash);
            jos.close();
        } catch (java.io.IOException e) {
            System.err.println("Unable to create name hash file:"+defaultSurveyHash);
            e.printStackTrace(System.err);
        }
    }
    
    public void read(String file) {
        try {
            java.io.ObjectInputStream iis = new java.io.ObjectInputStream(
                    new java.io.FileInputStream(file)
            );

            hash = (HashMap<String, SurveyLink>) iis.readObject();
            Set<String> sort = new TreeSet(hash.keySet());
            for (String key : sort) {
                String padding = "                        ".substring(0, 24 - key.length());
                
                System.out.println(key + ":" + padding + hash.get(key).name + " (" + new java.util.Date(hash.get(key).time)+")");
            }
        } catch (Exception e) {
            System.err.println("Error reading hash:" + e);
            e.printStackTrace(System.out);
        }
    }
     
    /** Get the surveys in the document root area */
    protected void getSurveysFromRoot() {
	
	String[] roots = Settings.getArray("xmlroot");
	
        for (String root: roots) {
	    File     f      = new File(root);
	    String[] match  = f.list(filter);
	
	    for (String survey: match) {
	        process(root+"/"+survey);
	    }
	}
    }
    
    /** Get user specified surveys */
    protected void getSurveysFromUser() {
	
	String userSurveys= Settings.get("surveyxml");
	if (userSurveys != null) {
	    
	    Pattern comma    = Pattern.compile(",");
	    String[] surveys = comma.split(userSurveys);
	    for (int i=0; i<surveys.length; i += 1) {
		process(surveys[i]);
	    }
	}
    }

    /** Get surveys from a user manifest.  This
     *  is how SkyView-in-a-Jar gets its surveys.
     *  Result is that the XMLSurveyFinder's hash object is filled.
     *  Overloaded for case of dump */
    protected void getSurveysFromMainifest() {
	getSurveysFromManifest(false);
    }
    protected void getSurveysFromManifest(boolean dump) {
	// See if there is a local cache, a remote cache at
	// XMLURLPREFIX, or a local manifest file to read directly.
	// Unless dump=True, in which case, go straight to last.  
	boolean cacheUsed = false;
	try { 
	    if (dump == false) {
		// Tries local first (e.g., web interface) then tries remote if local not found (e.g., skyview-in-a-jar):
		cacheUsed= loadSurveyCacheFromFile(localCacheName()) || loadSurveyCacheFromUrl(remoteCacheName())  ;
	    }
	    if ( ! cacheUsed) {
		// If dump was specified or no local or remote cache
		// found (e.g., skyview-in-a-jar with no internet
		// connection), read local manifest
		manifestToHash();
	    }
	} catch (Exception e) {
	    System.err.println("Error loading surveys from manifest: "+e+"\n Processing continues");
	    // Continue with whatever surveys have already been loaded.
	}
    }

    /** Get path to the local cache using the path to the jar file
     * being executed.
     */
    protected String localCacheName() {
	try {
            String path = XMLSurveyFinder.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
            if (path.length() > 2 && path.charAt(0) == '/'  && path.charAt(2) == ':') {
                path = path.substring(1);
            }

	    Path codepath = Paths.get(path);  
	    Path localpath = Paths.get(System.getProperty("user.dir"));
	    Path xmlfileprefix=localpath.relativize(codepath).getParent();
	    if (xmlfileprefix == null) {
		// E.g., running skyview-in-a-jar, if the jar
		// is sitting in the run directory, they are
		// the same, and there is no parent directory.
		// Result is null and needs to be "./".  
		xmlfileprefix=Paths.get("./");
	    }
	    // Get manifestName just so that we have any subdirectory
	    // that might change.  Then replace the manifest filename
	    // with the hash file name.
	    String hashFile = xmlfileprefix.resolve(manifestName().replace(defaultSurveyManifest,defaultSurveyHash)).toString() ; 
	    return hashFile;
	} catch (Exception e) {
	    System.err.println("Error getting local cache name: "+e+"\n Processing continues");
	    return null; 
	}
    }



    protected String remoteCacheName() {
	// Get manifestName just so that we have any subdirectory
	// that might change.  Then replace the manifest filename
	// with the hash file name and append to base URL.
	String filename=manifestName().replace(defaultSurveyManifest,defaultSurveyHash);
	try {
	    //  Constructor URL(context,spec) MUST have trailing slash
	    //  in XMLURLPREFIX.  Make sure here.
	    String xmlurlprefix=Settings.get("XMLURLPREFIX");
	    if (!xmlurlprefix.endsWith("/")) {  xmlurlprefix+="/";}
	    String url=new URL( new URL(xmlurlprefix), filename).toString();  
	    return url;
	} catch (Exception e) {
	    System.err.println("ERROR trying to construct remoteCacheName from XMLURLPREFIX="+Settings.get("XMLURLPREFIX")+" and filename="+filename+":  "+e);
	    return null;
	}
    }



    protected String manifestName(){
	try{
	    String manifest = Settings.get("surveymanifest");
	    if (manifest == null) {
		manifest = defaultSurveyManifest;
	    }
	    return manifest;
	} catch (Exception e) {
	    System.err.println("ERROR getting the name of the survey manifest:  "+e);
	    return null; 
	}
    }

    /** Read XML files to populate survey hash in memory.
     */
    protected void manifestToHash(){
	String manifest=manifestName();
	try{
	    InputStream is = skyview.survey.Util.getResourceOrFile(manifest);
	    if (is == null) {
		return;
	    }
	    BufferedReader br = new BufferedReader(new InputStreamReader(is));
	    String survey;
	    while ( (survey = br.readLine() ) != null) {
		survey=survey.trim();
		if (survey.length() == 0 || survey.charAt(0) == '#') {
		    continue;
		}
		process(survey);
	    }
	    
	} catch (Exception e) {
	    System.err.println("Error loading surveys from manifest: "+e+"\n Processing continues");
	    // Continue with whatever surveys have already been loaded.
	}
    }


    protected boolean loadSurveyCacheFromUrl(String url){
	boolean cacheFound=false;
	try{ 
	   InputStream is = new URL(url).openStream();
	    if (is!=null){
		hash = (HashMap<String, SurveyLink>) new java.io.ObjectInputStream(is).readObject();
		cacheFound = true;
	    }
	} catch (Exception e) {
	    System.err.println("ERROR trying to read cache at url="+url);
	}
	return cacheFound;
    }



    protected boolean loadSurveyCacheFromFile(String hashFile){
	boolean cacheFound=false;
	try {
	    InputStream is = skyview.survey.Util.getResourceOrFile(hashFile);
	    if (is!=null) {
		hash = (HashMap<String, SurveyLink>) new java.io.ObjectInputStream(is).readObject();
		cacheFound = true;
	    }
	} catch (Exception e) {
	    System.err.println("ERROR trying to read hashFile="+hashFile);
	}
	return cacheFound;
    }





    /** Do we have this survey? */
    public Survey find(String shortName) {
	String fileName =  findFile(shortName);
	if (fileName == null) {
	    return null;
	} else {
	    return new XMLSurvey(fileName);
	}
    }
    
    /** Find the survey file given the short name */
    public String findFile(String shortName) {

        SurveyLink lnk = hash.get(shortName.toLowerCase());
        if (lnk == null) {
            return null;
        }
        // Get just the actual file name
        String suffix = "";        
        String name  = lnk.name.replaceAll("\\?.*", "");
        if (!name.equals(lnk.name)) {
            suffix = lnk.name.substring(name.length());
        }
        // If direct pointer to file just do it!
        if (new File(name).exists()) {
            return lnk.name;
        }
        
        // Look for the file in the standard saved location.
        // This is where the Web interface will find it.
        Path basePrefix;
	// This returns the path to the running jar.  This path should be relative to that.  (And toURI has to be inside a try.)
	try{
            String path=XMLSurveyFinder.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
            if (path.length() > 2 && path.charAt(0) == '/'  && path.charAt(2) == ':') {
                path = path.substring(1);                
            } else {
            }
            
	    basePrefix = Paths.get(path).getParent();
	} catch(Exception e){
            throw new IllegalArgumentException("Unable to determine path to file:  "+e);
        }
        String fileLoc = basePrefix.resolve(name).toString();
        if (!new File(fileLoc).exists()) {
	    
            // Look in the local cache.
            basePrefix = Paths.get(Settings.get("DFT_cache"));
            String cacheLoc = basePrefix.resolve(name).toString();        
            File cacheFile = new File(cacheLoc);
            if (!cacheFile.exists() ||  cacheFile.lastModified() < lnk.time) {
            
                // Either the file doesn't exist or has been superceded.
                // Copy a new file from the SkyView host.
            
                // First check to see if the appropriate directory exists.
                String cacheDir = cacheLoc.substring(0,cacheLoc.lastIndexOf(File.separator));
                if (!new File(cacheDir).exists()) {
                    if (!new File(cacheDir).mkdirs())  {
                        throw new IllegalArgumentException("Unable to make cache directory:"+cacheDir);
                    }
                }
                String urlLoc = Settings.get("XMLURLPREFIX")+name;
                
                try {
                    System.err.println("   Downloading survey file for survey:"+shortName+" URL:"+urlLoc);
                    Util.getURL(urlLoc, cacheLoc);
                } catch(Exception e){
                    throw new IllegalArgumentException("Unable to download file:"+
                        urlLoc+" to "+cacheLoc+".\n"+e, e);
                }
            }
        }
        return basePrefix.resolve(name).toString()+suffix;        
    }
    
    /** Parse a single file */
    private void process(String name) {
	
	try {
            Reader is = XMLSurvey.getSurveyReader(name);
            File sFile = new File(name);
            long mTime = Long.MIN_VALUE;
            if (sFile.exists()) {
                mTime = sFile.lastModified();
            } else {
                mTime = new Date().getTime();
            }
	
            SAXParser sp = SAXParserFactory.newInstance().newSAXParser();
	    // This should fill up the names.
	
            sp.parse(new InputSource(is), 
	  	     new XMLSurveyFinder.NameCallBack(name, mTime)
		    );
	    is.close();
	    
	} catch(ParsingTermination e){
	} catch (Exception e) {
	    e.printStackTrace(System.err);
	    throw new Error("Unexpected exception parsing file: "+name+" in SurveyFinder:"+e);
	    
	}
        
    }
    
    /** What surveys do we know about? */
    public String[] getSurveys() {
	return (String[])hash.keySet().toArray(new String[0]);
    }
    
    /** This is the class the does the parsing. */
    private class NameCallBack extends DefaultHandler {
	
	/** Buffer to accumulate text into */
	private StringBuffer buf;
	
	/** Are we in an active element? */
	private boolean active = false;
	
	/** The file */
	String fileName;	
        
        /** Mod date */
        long mTime;
       
	NameCallBack(String file, long time) {
	    this.fileName = file;
            this.mTime = time;
	}
	Pattern pat = Pattern.compile(",");

	    
	
        public void startElement(String uri, String localName, String qName, Attributes attrib) {
	    
	    if (qName.equals("ShortName")) {
	        active = true;
		buf    = new StringBuffer();
	    }
        }
    
        public void endElement(String uri, String localName, String qName) {
	    
	    if (qName.equals("ShortName")) {
		String name = new String(buf).trim();
		String[] names = pat.split(name);
		for (String cName: names) {
		    cName = cName.trim().toLowerCase();
                    if (hash.containsKey(cName)) {
                        System.err.println("Ambiguous survey short name:"+cName+"\n"+
                                           "  Defined in both:"+hash.get(cName).name+"\n"+
                                           "                  "+fileName);
                    }
                    SurveyLink lnk = new SurveyLink();
                    lnk.name = fileName;
                    lnk.time = mTime;
		    hash.put(cName, lnk);
		}
		// Terminate the parsing... Is there a better
		// way?
		throw new ParsingTermination();
	    }
        }

        public void characters(char[] arr, int start, int len) {
	    if (active) {
	        buf.append(arr, start, len);
	    }
        }
    }
    
    /** Main class.
     *  <br>
     *  With no arguments simply list all of the survey names.
     *  <br>
     *  With the single argument "dump": Create a cached names file
     *   that can be used to speed up the initialization.
     *  <br>
     *  With any other single argument: show the metadata for the selected survey
     *  <br>
     *  With multiple arguments
     *      survey metakey1 [metakey2...]
     *    show the requested metadata for the requested survey
     */
    public static void main(String[] args) {
        if (args.length == 0) {
            String[] surveys = new XMLSurveyFinder().getSurveys();
            java.util.Arrays.sort(surveys);
            for (String surv: surveys) {
                System.out.println(surv);
            }
        } else if (args.length == 1 && args[0].equals("dump")) {
            new XMLSurveyFinder(true).dump();
        } else if (args.length == 2 && args[0].equals("read")) {
            new XMLSurveyFinder().read(args[1]);
        } else {
            XMLSurvey  survey =(XMLSurvey) new XMLSurveyFinder().find(args[0]);
            Map<String,String> metadata = survey.getMetadata();
            if (args.length == 1) {
                for (String key: metadata.keySet()) {
                    System.out.println(key+": "+metadata.get(key));
                }
            } else {
                for (int i=1; i<args.length; i += 1) {
                    String key = args[i];
                    System.out.println(key+":"+metadata.get(key));
                }
            }
        }        
    }
}
