package skyview.survey;

import heasarc.db.votable.Base64;
import heasarc.db.votable.StreamToJava;
import skyview.executive.Settings;
import skyview.survey.Image;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;

import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;

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

/** This class gets a set of candidates from a SIAP request */
public class SIAPGenerator implements ImageGenerator {
    
    /** The descriptions of the images we are interested in */
    java.util.ArrayList<String> spells = new java.util.ArrayList<String>();

    /** Find the base URL for this SIAP service */
    protected String getBaseURL() {
	return Settings.get("SiapURL");
    }
    
    String filterField;
    String filterValue;
    boolean filtering;
    
    boolean transforming;
    String[] transStrings;
    
    
    /** Get images from a SIAP service */
    public void getImages(double ra, double dec, double size, java.util.ArrayList<String> spells)  {
        
	
	String urlString = getBaseURL();
	filterField      = Settings.get("SIAPFilterField");
	filterValue      = Settings.get("SIAPFilterValue");
	filtering        = filterField != null && filterValue != null;
        
        transStrings = Settings.getArray("SIAPURLTransform");
        if (transStrings != null && transStrings.length == 2) {
            transforming = true;
        }
        
	int timeout      = 15 * 1000;  // Default to 15 seconds.
	if (Settings.has("SIATimeout") ) {
	    try {
		timeout = Integer.parseInt("SIATimeout")*1000;
	    } catch (Exception e) {}
	}
        
	if (!urlString.endsWith("?")  && !urlString.endsWith("&")) {
            if (urlString.indexOf("?") > 0) {
                urlString += "&";
            } else {
                urlString += "?";
            }
        }
        size *= 1.4;
        if (Settings.has("SiapAddSize")) {
            size += Double.parseDouble(Settings.get("SiapAddSize"));
        }
	urlString  = addPosToURL(urlString, ra, dec, size);
	System.err.println("  SIAP request URL:"+urlString + 
              " timeout is " + timeout );
	
      try {
	URL siaURL = new URL(urlString);
	URLConnection conn = siaURL.openConnection();
	conn.setReadTimeout(timeout);	  
        BufferedInputStream   bi = new BufferedInputStream(conn.getInputStream());
	ByteArrayOutputStream bo = new ByteArrayOutputStream(32768);
	
	byte[] buf = new byte[32768];
	int len;
	while ( (len=bi.read(buf)) > 0) {
	    bo.write(buf, 0, len);
	}
	bi.close();
	bo.close();
	
        String response = bo.toString();
	response = response.replaceAll("<!DOCTYPE.*", "");
	    
	java.io.ByteArrayInputStream byi = new java.io.ByteArrayInputStream(response.getBytes());
	try {
            SAXParser sp = SAXParserFactory.newInstance().newSAXParser();
	    // This should fill images with the strings for any images we want.
            sp.parse(byi, new SIAPGenerator.SIAPParserCallBack(spells));
	    
        } catch(Exception e) {
            e.printStackTrace(System.err);
	    throw new Error("Error parsing SIAP:"+e);
        }
      } catch (java.net.SocketTimeoutException e) {
            throw new RuntimeException("\nTimeout querying SIAP URL " + urlString +"\n" );

      } catch (Exception e) {
	  throw new Error("Unable to do IO in SIAP processing:"+e);
      }
    }
    
    protected String addPosToURL(String base, double ra, double dec, double size) {
        return base + "POS="+ra+","+dec+"&SIZE="+size;	        
    }
    
    /** Do regular expression transformation if requested.
     *  Other classes can override but should probably call
     *  this first unless they explicitly handle this.
     */ 
    protected String updateURL(String input) {
        if (transforming) {
            input = input.replaceAll(transStrings[0], transStrings[1]);
        }
        return input;
    }

    private class SIAPParserCallBack extends DefaultHandler {
	
	
	/** Buffer to accumulate text into */
	private StringBuffer buf;
	
	/** Are we in an active element? */
	private boolean active = false;
	
	private int fieldCount = 0;
	
	private java.util.HashMap<String, Integer> fields = new java.util.HashMap<String, Integer>();
        // We now that values will store Strings, but we'll be using it as an argument in a
        // function that is sometimes called with a list that may include other types.
	private java.util.ArrayList<Object> values = new java.util.ArrayList<Object>();
	
	private String proj    = Settings.get("SIAPProjection");
	private String csys    = Settings.get("SIAPCoordinates");
	private String naxis   = Settings.get("SIAPNaxis");
	private String scaling = Settings.get("SIAPScaling");
	private String maxImageString = Settings.get("SIAPMaxImages");
        private String siapRE = null;
        private String siapResult = null;
	private int maxImages;
	private int imageCount = 0;
	
	
	java.util.ArrayList<String> spells;
	
	SIAPParserCallBack(java.util.ArrayList<String> spells) {
	    this.spells = spells;
	    if (maxImageString != null) {
		maxImages = Integer.parseInt(maxImageString);
	    }
	}
        
        /** This class is used in the STREAM parsing */
        class ColDef {
            String name, type, array, nullValue;
            ColDef(String name, String type, String array, String nullValue) {
                this.name=name; this.type = type; this.array=array; this.nullValue=nullValue;
            }
        }
        List<ColDef> cols = new ArrayList<ColDef>();
        boolean binary    = false;
        int     row       = 0;
	
        public void startElement(String uri, String localName, String qName, Attributes attrib) {
	    
	    if (qName.equals("FIELD")) {
		String ucd = attrib.getValue("ucd");
		if(ucd != null && ucd.length() > 0) {
		    fields.put(ucd.toUpperCase(), fieldCount);
		}
		String name = attrib.getValue("name");
		if (name != null && name.length() > 0) {
		    fields.put(name.toUpperCase(), fieldCount);
		}
		
		String id = attrib.getValue("ID");
		if (id != null && id.length() > 0) {
		    fields.put(id.toUpperCase(), fieldCount);
		}
		cols.add(new ColDef(attrib.getValue("name"), attrib.getValue("datatype"), attrib.getValue("arraysize"), null));
		fieldCount += 1;
		
	    } else if (qName.equals("TR") ) {
		values.clear();			
	    } else if (qName.equals("TD")) {
	        active = true;
		buf    = new StringBuffer();
	    } else if (qName.equals("STREAM")) {
                active = true;
                binary = true;
                buf    = new StringBuffer();
            }
		
        }
    
	/** Find the value of a given field given
	 *  the UCD, name or ID of the field.
	 */
	private String getFieldValue(List<Object> values, String id) {
	    id = id.toUpperCase();
	    if (fields.containsKey(id)) {
		int i = fields.get(id);
                Object o = values.get(i);
                if (o == null) {
                    return "";
                } else if (o.getClass().isArray()) {
                    String ret = "";
                    String sep = "";
                    int len = Array.getLength(o);
                    for (int k=0; k<len; k += 1) {
                        ret += sep + Array.get(o,k);
                        sep  = " ";
                    }
                    return ret;
                } else {
		    return ""+values.get(i);
                }
	    } else {
		return null;
	    }
	}
	
        public void endElement(String uri, String localName, String qName) {
	    
            String s = null;
            
	    // This means we finished a setting.
	    if (active) {		
	        active = false;
		s = new String(buf).trim();
            }
            
	    if(qName.equals("TD")) {
		values.add(s);		    
            } else if (qName.equals("TR")) {
                checkRow(values);
            } else if (qName.equals("STREAM")) {
                Base64 stream = new Base64();
                stream.append(s);
                StreamToJava sj = new StreamToJava(stream.getAsStream());
                for (int i=0; i<cols.size(); i += 1) {
                    ColDef col = cols.get(i);
                    sj.addColumn(col.type, col.array, col.nullValue);
                }
                try {
                    List<List<Object>> tab = sj.readTable();
                    for (int i=0; i<tab.size(); i += 1) {
                        checkRow(tab.get(i));
                    }
                } catch (IOException e) {
                    System.err.println("Exception parsing VOTable Stream input:"+e);
                }
            }
        }
        
        
        void checkRow(List<Object> row) {
            if (filtering) {  // If there is a filter value then compare as appropriate.
                String filter = getFieldValue(row, filterField);

                if (filter != null && !filter.equals(filterValue)) {
                    return;
                }
            }

            // Check if this is a FITS file.
            String format = getFieldValue(row, "VOX:Image_Format").toLowerCase();
            // Just look for FITS somewhere in the format.
            if (format.indexOf("fits") < 0) {
                return;
            }

            // Initial values.
            proj = Settings.get("SIAPProjection");
            csys = Settings.get("SIAPCoordinates");
            naxis = Settings.get("SIAPNaxis");
            scaling = Settings.get("SIAPScaling");

            if (maxImageString != null) {
                if (imageCount >= maxImages) {
                    return;
                }
            }
            /**
             * Heres where all the work goes...
             */
            String spell = "";
            String url = getFieldValue(row, "VOX:Image_AccessReference");

            // By default does nothing but we allow other classes to override.
            url = updateURL(url);

            String file = getFieldValue(row, "VOX:File_Name");

            if (file == null) {
                file = url.substring(url.lastIndexOf('/') + 1);
            }

            String ra = getFieldValue(row, "POS_EQ_RA_MAIN");
            String dec = getFieldValue(row, "POS_EQ_DEC_MAIN");
            if (ra == null) {
                ra = getFieldValue(row, "pos.eq.ra;meta.main");
            }
            if (dec == null) {
                dec = getFieldValue(row, "pos.eq.dec;meta.main");
            }

            boolean invert = false;
            String projstr = getFieldValue(row, "VOX:WCS_CoordProjection");
            if (projstr != null) {
                // If Dec comes before RA we need to flip the order
                // of axes.
                invert = projstr.matches(".*(DEC|LAT).*(RA|LON).*");
            }

            if (projstr != null && projstr.length() == 3) {
                proj = projstr.substring(0, 1).toUpperCase() + projstr.substring(1).toLowerCase();
            }

            String refstr = getFieldValue(row, "VOX:WCS_CoordRefFrame");
            if (refstr != null) {
                refstr = refstr.toLowerCase();
                if (refstr.equals("icrs")) {
                    csys = "ICRS";
                } else if (refstr.equals("fk5")) {
                    csys = "J2000";
                } else if (refstr.equals("fk4")) {
                    csys = "B1950";
                } else if (refstr.equals("ecl")) {
                    csys = "E2000";
                } else if (refstr.equals("gal")) {
                    csys = "Galactic";
                }
            }
            // Probably should worry about Equinox field, but doesn't seem
            // to be used.

            String crval = mashVal(getFieldValue(row, "VOX:WCS_CoordRefValue"), invert, 2);
            if (crval != null && crval.indexOf(",") > 0) {
                String[] tCoords = crval.split(",");
                if (tCoords.length == 2) {
                    if (invert) {
                        ra = tCoords[1];
                        dec = tCoords[0];
                    } else {
                        ra = tCoords[0];
                        dec = tCoords[1];
                    }
                }
            }

            // The following may be set generally.  If so
            // don't query.
            String crpix = null;
            if (scaling == null) {
                scaling = mashVal(getFieldValue(row, "VOX:WCS_CDMatrix"), invert, 4);
                String cps = getFieldValue(row, "VOX:WCS_CoordRefPixel");
                if (cps != null) {
                    crpix = mashVal(cps, invert, 2);
                }
            }
            if (scaling == null) {
                scaling = mashVal(getFieldValue(row, "VOX:Image_Scale"), invert, 2);
            }

            if (naxis == null) {
                naxis = mashVal(getFieldValue(row, "VOX:Image_Naxis"), invert, 2);
            }

            if (crval == null) {
                crval = ra + "," + dec;
            }

            // Set up defaults for the current line.
            String lproj = proj;
            String lcsys = csys;

            if (lproj == null) {
                lproj = "Tan";
            }

            if (lcsys == null) {
                lcsys = "J2000";
            }

            spell = url + "," + file + "," + crval + "," + lproj + "," + lcsys + "," + naxis + "," + scaling;
            if (crpix != null) {
                spell += "," + crpix;
            }
            spells.add(spell);
            imageCount += 1;

        }
	
	/** Take the input string, split by spaces or commas
	 *  invert array if needed and join with spaces.
	 *  @param input   The string to be parsed
	 *  @param invert  Are the coordinates reversed?
	 *  @param count   The expected number of parameters.
	 */
	private String mashVal(String input, boolean invert, int count) {
	    
	    // This will work with vectors of dimension 2 and
	    // 2x2 matrices, but not more generally...
	    
	    if (input == null) {
		return null;
	    }
	    input = input.trim();
	    String[] tokens = input.split(" ");
	    if (tokens.length == 1) {
		tokens = input.split(",");
	    }
	    String output = "";
	    
	    int curr = 0;
	    int delta = 1;
	    
	    if (invert) {
		curr = count - 1;
		delta = -1;
	    }
	    
	    String sep = "";
	    for (int i=0; i<count; i += 1) {
		output += sep + tokens[curr];
		curr   += delta;
		sep    = ",";
	    }
	    if (tokens.length < count) {
		String last = tokens[tokens.length-1];
		for (int i=tokens.length; i<count; i += 1) {
		    output += sep + last;
		    sep     = ",";
		}
	    }
		    
	    return output;
	}
		    
        public void characters(char[] arr, int start, int len) {
	    if (active) {
	        buf.append(arr, start, len);
	    }
        }
	
    }
}
