package skyview.survey;

import skyview.survey.Survey;
import skyview.survey.Image;
import skyview.survey.ImageFactory;

import skyview.geometry.Position;

import skyview.executive.Settings;

import nom.tam.fits.Header;

import javax.xml.parsers.SAXParserFactory;
import javax.xml.parsers.SAXParser;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;

import java.util.ArrayList;
import java.util.regex.Pattern;

import java.io.InputStream;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPInputStream;
import nom.tam.fits.HeaderCard;
import skyview.request.TextReplacer;

/** This class defines a survey based upon an XML file
 *  which contains the metadata and image information for the survey.
 */
public class XMLSurvey implements Survey {
    
    /** The XML file that defines the survey */
    private String xmlFile;
    
    /** The default size of images. */
    private double surveySize;
    
    /** The list of image strings */
    private ArrayList<String> images;
    
    /** The class the is called to add survey specific settings.
     */
    private class SettingsCallBack extends DefaultHandler {
	
	/** Buffer to accumulate text into */
	private StringBuffer buf;
	
	/** Are we in an active element? */
	private boolean active = false;
	
	/** Are we in the survey settings? */
	private boolean inSettings = true;
	
	/** Are we in the metatable? */
	private boolean inMeta = false;
	
	
        public void startElement(String uri, String localName, String qName, Attributes attrib) {
	    
	    String lq = qName.toLowerCase();
	    
	    if (lq.equals("settings")  || 
		lq.equals("metatable") || 
		lq.equals("name")      ||
		lq.equals("shortname") ||
		lq.equals("onlinetext") ) {
		inSettings = true;
	    }
	    if (lq.equals("metatable")) {
		inMeta = true;
	    }
	    if (inSettings) {
	        active = true;
		buf    = new StringBuffer();
	    }
        }
    
        public void endElement(String uri, String localName, String qName) {
	    
	    String lq = qName.toLowerCase();
	    if (lq.equals("settings")   || 
		lq.equals("metatable")  || 
		lq.equals("name")       ||
		lq.equals("onlinetext") 
		) {
		inSettings = false;
		active     = false;
	    }
	    
	    // This means we finished a setting.
	    if (active  || lq.equals("name") || lq.equals("onlinetext") ) {
	        active = false;
		String s = new String(buf).trim();
		qName = qName.toLowerCase();
		if (inMeta) {
		    qName = "_meta_"+qName;
		}
		
		// Don't override settings that the user has specified.
		if (s.length()> 0) {
		    Settings.suggest(qName, s);
		}
	    }
	    
	    if (lq.equals("metatable") ) {
		inMeta = false;
	    }
        }

        public void characters(char[] arr, int start, int len) {
	    if (active) {
	        buf.append(arr, start, len);
	    }
        }
    }
    /** The class the is called to find images in the Survey
     *  XML file.
     */
    protected class ImageFinderCallBack extends DefaultHandler {
	
	/** Buffer to accumulate text into */
	private StringBuffer buf;
	
	/** Are we in an active element? */
	private boolean active = false;
	
	/** The RA, Dec and size that the user is requesting. */
	private double ra, dec, requestSize;
	
	/** Used to break up the image strings */
	private Pattern pat = Pattern.compile("\\s+");
	
	/** Are we in the Images area? */
	private boolean inImages = false;
	
	/** Is this the first Image? */
	private boolean firstImage = true;
	
	/** The position of the center of the output image */
	private Position pos;
	
	/** Do we want to get images? */
	private boolean needImages;
        
        /** Remember the scale when we come across it */
        private double surveyScale;
        
        
        /** Requested level for hierarchical survey. */
        private int requestLevel;
        
        /** Scale requested for image */
        private double imageScale;
        
        /** The ImageGenerator if supplied */
        private ImageGenerator generator;        
        private String imageGeneratorString;
        
        
	private void updatePosition() {
	    try {
	        double[] coords = pos.getCoordinates(Settings.get("SurveyCoordinateSystem"));
	        ra     = coords[0];
	        dec    = coords[1];
	    } catch (Exception e ) {
		System.err.println("Error with SurveyCoordinateSystem!"+Settings.get("SurveyCoordinateSystem"));
		throw new Error(e);
	    }
	}
	
	ImageFinderCallBack(Position pos, double size, double scale, boolean needImages) {
	    this.pos = pos;
	    this.needImages = needImages;
            this.imageScale = scale;
	    updatePosition();
	    
	    this.requestSize = size;
	}
    
        public void startElement(String uri, String localName, String qName, Attributes attrib) {
            
            // We want to get the scale if we have a hierarchical image 
	    if (inImages  || qName.equals("Scale")) {
		active = true;
		buf = new StringBuffer();
	    }
	    if (qName.equals("Images")) {
                String levelStr = attrib.getValue("level");
                int currentLevel = 0;
                if (levelStr != null  && levelStr.length() > 0) {
                    currentLevel = Integer.parseInt(levelStr);
                }
                if (currentLevel <= requestLevel) {
		    inImages = true;
                }
                
	    }
        }
        
        /** Set the scale for a hierarchical survey.
         *  Note that if we don't call this, the surveyLevel stays at 0
         *  which is what non-hierarchical surveys run at.
         * @param scale The scale string for the survey.
         */
        private void setRequestLevel(String scale) {        
            surveyScale = Double.parseDouble(scale);
            if (surveyScale != 0) {
                double ratio = imageScale/surveyScale;
                String scaleFactor = Settings.get("ScalingFactor", "2");
                double sf = Double.parseDouble(scaleFactor);
                requestLevel = (int) (Math.log(ratio)/Math.log(sf));
                if (Settings.has("SurveyLevel")) {
                    requestLevel = Integer.parseInt(Settings.get("SurveyLevel"));
                }
                if (requestLevel < 0) {
                    requestLevel = 0;
                }
                Settings. put("_surveyLevel", ""+requestLevel);
            }
        }
    
        public void endElement(String uri, String localName, String qName) {
            

            if (inImages && qName.equals("Images")) {
                finishParsing();
                throw new ParsingTermination();
            }
            
	    if (active) {
	        active = false;
		String s = new String(buf).trim();
		
                if (qName.equals("Scale")) {
                    // Check that we don't do this twice since sometimes
                    // we have Metadata/Scale rather than Metadata/PixelScle
                    // in survey files.
                    if (surveyScale == 0) {
                        try {
                            setRequestLevel(s);
                        } catch (Exception e) {
                            System.err.println("Error parsing scale:"+s);
                        }
                    }
                }
	        if (qName.equals("Image")  && needImages) {
		    
		    // Check if this image is close enough to be a candidate
		    // for mosaicking.
		    
		    if (firstImage) {
			surveySize = Double.parseDouble(Settings.get("ImageSize"));
			firstImage = false;
		    }
		    // Could cause problems if filenames have white space in them.
		    // Might be better to have subfields in the <Image> element.
		    String[] tokens = pat.split(s);
		    double xRA = 0, xDec = 0;
		    try {
		        xRA  = Double.parseDouble(tokens[1]);
		        xDec = Double.parseDouble(tokens[2]);
		    } catch (Exception e) {
			throw new Error(e);
		    }
		    double distance = 
		      skyview.geometry.Util.sphdistDeg(ra, dec, xRA, xDec);
		    
		    // Coefficient below probably could be 1/sqrt(2) for diagonals along squares,
		    // but we make it a little larger as a safety factor
		    if (distance < (surveySize + requestSize)) {
		        images.add(tokens[0]);
		    }
		    
		} else if (qName.equals("ImageGenerator")) {
		    
		    // This is the name of a class that can generate image names dynamically.
		    generator = 
		      (skyview.survey.ImageGenerator) skyview.util.Utilities.newInstance(
		         s, "skyview.survey");
		    if (generator == null) {
			throw new Error("Unable to create image generator:"+s);
		    }
                    imageGeneratorString = s;
                    // Defer the actual generation of images until we
                    // actually leave the Images element.
		    
	        } else {
		    // Everything else goes into the Settings.  However
		    // unlike elements in the <Settings> area, we don't
		    // defer to what's already there, we replace it.
		    Settings.put(qName, s);
		    // Following Images are in the given coordinate system
		    if (qName.toLowerCase().equals("surveycoordinatesystem")) {
			updatePosition();
		    }
		}
	    }
        }
        
        void finishParsing() {
            try {
                if (generator != null) {
		    generator.getImages(ra, dec, requestSize, images);
                }
            } catch (Exception e) {
                System.err.println("Unable to invoke ImageGenerator:"+imageGeneratorString+"\nException: "+e);
                e.printStackTrace(System.err);                        
            }

        }

        public void characters(char[] arr, int start, int len) {
	    if (active) {
	        buf.append(arr, start, len);
	    }
        }
    }
    private Map<String,String> metavalues;
    
    /** Used to get metadata from survey */
    private class MetaCallback extends DefaultHandler {
        
	/** Buffer to accumulate text into */
	private StringBuffer buf;
	
	/** Are we in an active element? */
	private boolean active = false;
	
        
        /** Are we inside an element inside of which we want to record
         *  all values?
         */
        private boolean inside = false;
        
        MetaCallback() {
            // Case insensitive hash.
            metavalues = new HashMap<String,String>(){
                public String get(String input) {
                    if (input == null) {
                        return super.get(null);
                    } else {
                        return super.get(input.toLowerCase());
                    }
                }
                public  String put(String key, String val) {
                    if (key != null) {
                        key = key.toLowerCase();
                    }
                    return super.put(key,val);
                }
            };
        }
	    
        public void startElement(String uri, String localName, String qName, Attributes attrib) {
            if (inside || qName.equals("ShortName")  || qName.equals("Title")) {
		active=true;
		buf = new StringBuffer();
	    }
	    // Include the survey metadata in the output.
	    if (qName.equals("MetaTable")  || qName.equals("Settings")) {
		inside = true;
	    }
            if (qName.equals("Images")) {
                throw new ParsingTermination();
            }
        }
    
        public void endElement(String uri, String localName, String qName) {	    
	    if (active) {
	        active = false;
		String s = new String(buf).trim();
                metavalues.put(qName, s);
            } else if (qName.equals("MetaTable") || qName.equals("Settings")) {
                inside = false;
            }
        }
        public void characters(char[] arr, int start, int len) {
	    if (active) {
	        buf.append(arr, start, len);
	    }
        }

    }
    
    /** The class is used when we update an image generated from 
     *  a survey.
     */
    private class HeaderUpdateCallBack extends DefaultHandler {
	
	/** Buffer to accumulate text into */
	private StringBuffer buf;
	
	/** Are we in an active element? */
	private boolean active = false;
	
	/** Is this a metafield? */
	private boolean meta = false;
	
	/** Is this the first metadata field? */
	private boolean firstMeta = true;
	
	/** The FITS header to be updated */
	private Header h;
	
        private Pattern pat = Pattern.compile("\\n");
	
	
	HeaderUpdateCallBack(Header fitsHeader) {
	    this.h = fitsHeader;
	}
    
        public void startElement(String uri, String localName, String qName, Attributes attrib) {
	    
	    if (meta || qName.equals("FITS")) {
		active=true;
		buf = new StringBuffer();
	    }
	    // Include the survey metadata in the output.
	    if (qName.equals("MetaTable")) {
		meta = true;
	    }
        }
    
        public void endElement(String uri, String localName, String qName) {
	    
	  try {
	    if (active) {
	        active = false;
		String s = new String(buf).trim();
	        if (meta) {
		    
		    if (firstMeta) {
			h.insertComment("");
			h.insertComment(" SkyView Survey metadata ");
			h.insertComment("");
		        firstMeta = false;
		    }
		    
		    // The metadata may include HTML of various kinds.
		    // Let's get rid of that...
		    s = skyview.survey.Util.replace(s, "<[^>]*>", "", true);
		    s = skyview.survey.Util.replace(s, "&amp;", "&", true);
		    s = skyview.survey.Util.replace(s, "&gt;", ">", true);
		    s = skyview.survey.Util.replace(s, "&lt;", "<", true);
		    s = skyview.survey.Util.replace(s, "\n", " ", true);
		    
		    String comHead = qName+":";
		    if (comHead.length() < 13) {
			comHead = comHead + "               ".substring(0,13-comHead.length());
		    }
		    
		    String comment = comHead+s;
		    
		    while (comment != null && comment.length() > 0) {
			
			if (comment.length() > 70) {
			    String next    = "          "+comment.substring(70);
			    String current = comment.substring(0,70);
			    h.insertComment(current);
			    comment = next;
			    
			} else {
			    
			    h.insertComment(comment);
			    comment = null;
			}
		    }
		}
		
		if (qName.equals("FITS")) {
		    h.insertComment("");
		    h.insertComment("Survey specific cards");
		    h.insertComment("");
		    // Add cards specifically for this survey.
		    String[] tokens = pat.split(s);                    
		    for (String tok: tokens) {
                        if (tok.length() == 0) {
                            tok = " ";
                        }
			h.addLine(HeaderCard.create(tok));	
		    }
		}
	    }
	    if (qName.equals("MetaTable")) {
		meta = false;
	    }
	  } catch(nom.tam.fits.FitsException e) {
	      throw new Error("Unexpected FITS exception in HeaderUpdateCallBack:"+e);
	  }
        }

        public void characters(char[] arr, int start, int len) {
	    if (active) {
	        buf.append(arr, start, len);
	    }
        }
    }

    /** Create a survey whose characteristics are given in 
     *  an XML file.
     */
    public XMLSurvey(String file) {
	this.xmlFile = file;
    }
    
    /** Get the name of the compontent */
    public String getName() {
	return "Survey:XML";
    }
    
    /** Get a description of the component */
    public String getDescription() {
	return "A survey defined by an XML file which contains the metadata and image descriptions";
    }

    /** Find candidate images from this survey.
     *  @param pos   A position object.
     *  @param size  The size (in radians) over which we should look for candidates.
     */
    public Image[] getImages(Position pos, double size, double scale) throws Exception {  
	
	
	/** Get the coordinates in the native coordinate system of the
	 *  survey.  If none is specified this defaults to J2000.
	 */
        SAXParser sp = SAXParserFactory.newInstance().newSAXParser();
	images = new ArrayList<String>();
	
	boolean needImages = true;
	
	if (
	  Settings.has("MaxRequestSize") && 
	  Settings.has("LargeImage") && 
	  Double.parseDouble(Settings.get("MaxRequestSize")) < size) {
	    String[] fields = Settings.getArray("LargeImage");
	    for (String fld: fields) {
		images.add(fld);
	    }
	    needImages = false;
        }
	// This should fill images with the strings for any images we want.
	// If we don't need images we may still need other info from
	// the <Images> area.
        try {
            doParse(sp, getFinderCallBack(pos, size, scale, needImages));
        } catch (ParsingTermination pt) {
            // Normal successful exit when not parsing all of Survey file.
        }
	
	String imageFactory = Settings.get("ImageFactory");
	
	if (images.size() == 0) {
	    return new Image[0];
	} else {
	    Image[] list       = new Image[images.size()];
	    
	    // Create the image factory
	    ImageFactory imFac = (ImageFactory) 
	      skyview.util.Utilities.newInstance(imageFactory, "skyview.survey");
	    if (imFac == null) {
		throw new Error("Unable to create image factory");
	    }
	    for (int i=0; i<images.size(); i += 1) {
		String s = images.get(i);
		list[i]  = imFac.factory(s);
	    }
	    return list;
	}
    }
    
    protected ImageFinderCallBack getFinderCallBack(Position pos, double size,  double scale, boolean needImages) {    
        return new XMLSurvey.ImageFinderCallBack(pos, size, scale, needImages);
    }

    
    /** Update a FITS header with information from the XML file */
    
    public void updateHeader(Header h) {
        XMLSurvey.HeaderUpdateCallBack dh = null;
        try {
            SAXParser sp = SAXParserFactory.newInstance().newSAXParser();
	    dh = new XMLSurvey.HeaderUpdateCallBack(h);
            doParse(sp, new XMLSurvey.HeaderUpdateCallBack(h));
        } catch(Exception e) {
	    throw new Error("Error updating header:"+e);
        }
    }
    
    /** Get the metadata for the survey */
    public Map<String,String> getMetadata() {
	try {
            SAXParser sp = SAXParserFactory.newInstance().newSAXParser();
	    // This should fill images with the strings for any images we want.
            doParse(sp, new XMLSurvey.MetaCallback());
         } catch(Exception e) {
            if (! (e instanceof ParsingTermination)) {
	        throw new Error("Error parsing metadata:"+xmlFile+"\n"+e);
            }
        }
        return metavalues;
    }
    
    /** Update the system settings */
    public void updateSettings() {
	try {
            SAXParser sp = SAXParserFactory.newInstance().newSAXParser();
	    // This should fill images with the strings for any images we want.
            doParse(sp, new XMLSurvey.SettingsCallBack());
         } catch(Exception e) {
	    throw new Error("Error updating header when reading file:"+xmlFile+"\n"+e);
        }
    }
    
    /** Run a parser */
    protected void doParse(SAXParser sp, DefaultHandler handler) throws Exception {
	java.io.Reader is = getSurveyReader(xmlFile);
	sp.parse(new InputSource(is), handler);
	is.close();
    }

    /** Get a buffered reader associated with the survey
     *  given a string name.  If the name includes a ? it is
     *  assumed that the stream should be filtered.
     */
    public static java.io.Reader getSurveyReader(String file) throws java.io.IOException {
        if (file.indexOf("?") > 0) {
            Map<String,String> parameters = new HashMap<String,String>();
            // Apparently this is a parametrized XML file.
            String[] parts = file.split("\\?", 2);
            String base = parts[0];
            String parms = parts[1];
            String[] params = parms.split("\\&");
            for (String param: params) {
                String[] elems = param.split("=", 2);
                parameters.put(elems[0], elems[1]);
            }
            
            return new TextReplacer(parameters, new InputStreamReader(decomp(base)));
        } else {
            return new InputStreamReader(decomp(file));
        }
    }
    
    private static InputStream decomp(String file) throws java.io.IOException {
        InputStream is = Util.getResourceOrFile(file);
        if (file.endsWith(".gz")) {
            is = new GZIPInputStream(is);
        }
        return is;
    }
}
