package skyview.survey;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.WeakHashMap;
import nom.tam.fits.Fits;
import nom.tam.fits.Header;
import nom.tam.fits.BasicHDU;
import nom.tam.fits.BinaryTable;
import nom.tam.fits.FitsException;
import nom.tam.fits.PaddingException;
import nom.tam.util.BufferedDataInputStream;

import skyview.geometry.TransformationException;
import skyview.geometry.WCS;
import skyview.geometry.Projection;
import skyview.geometry.CoordinateSystem;
import skyview.geometry.Scaler;

import skyview.geometry.projecter.Hpx;

import skyview.executive.Settings;

/** This class defines an image gotten by reading a HEALPix image
 *  where the pixels are in the nested pixel order.
 *  This assumes the FITS structures found in the WMAP data  but
 *  could be adapted to other orders as needed.
 */

public class HipsImage extends Image {
    
    final static int BUNCH_SIZE = 10000;
    final static int MIN_ORDER  = 3;  // Assume that we never have tiles of lower order.
    /** Base directory of HiPS */
    private String     hipsDir;
    
    private MocReader  moc;
    
    /** URL or file */
    private boolean remote = false;
    
    /** Healpix projecter/deporjecter */
    private Hpx        hpp;    
    
    /** Number of pixels on side of one of 12 tiles. */
    private int        nside;
    
    /** HiPS properties */
    Map<String,String> properties;
    
    /** Order to be used in extracting data -- i.e.,
     *  the Norder subdirectory */
     
    int                hipsOrder;

    /** The actual HEALPix order of the pixels to be analysed */
    int                pixelOrder;
    
    /** Number of pixels in file */
    int                filePixels;
    
    /** Data from all Sky Image */
    double[][]           allskyData;
    
    /** Use the allSky image if possible */
    boolean            allSky;
    
    
    /** Dimension of tile */
    int                tileSize;
    
    /** Where the HiPS data is stored;     
     */
    private HipsDataCache dataCache;
    
    /** Dimensions of All sky image */
    int allskyX, allskyY;
    
    
    /** Null tiles *.
     */
    private Set<Integer> nullTiles = new HashSet<Integer>();
    
    /** Create a image associated with a HiPS
     * @param directory  The base directory for the HiPS
     */
    public HipsImage(String directory) {
        
        hipsDir = directory;
        String lc = directory.toLowerCase();
        remote = lc.startsWith("http:") || lc.startsWith("https:") || 
                lc.startsWith("ftp:");
        if (!remote) {
            File f = new File(directory);
            if (!f.exists() || !f.isDirectory()) {
                throw new IllegalArgumentException("Unable to find HiPS directory:"+directory);
            }
        } else {
            // HiPS will all have the same file names so we need to ensure
            // the different surveys don't overwrite one another.
            // If we are doing multiple surveys, then this update should be
            // washed out after we finishing processing this survey,
            Settings.put("SaveBySurvey", "T");
        }
    }
    public void initialize(double scale) throws 
            TransformationException, IllegalArgumentException, IOException  {
        
        String propFile = hipsDir+"/properties";
        String mocFile  = hipsDir+"/Moc.fits";
        String lc = hipsDir.toLowerCase();
        if (lc.startsWith("http") || lc.startsWith("ftp:")) {
            Cacher cache = new Cacher();
            try {
                propFile = cache.getFile(propFile, "properties");
            } catch (Exception e) {
                // properties is mandatory and essential.
                throw new IOException("Unable to access remote MOC properties file"+e,e);                
            }
            try {
                mocFile = cache.getFile(mocFile, "Moc.fits");                
            } catch (Exception e) {
                // MOC is optional
                mocFile = null;
            }
        }
        
        Reader rdr = new FileReader(propFile);
        if (mocFile != null) {
            try {
                moc = new MocReader(mocFile); 
            } catch (Exception e) {
                moc = null;
            }
        }
        
        BufferedReader propRdr = new BufferedReader(rdr);
        
        String line;
        properties = new HashMap<String,String>();
        while ( (line = propRdr.readLine()) != null) {
            line = line.trim();
            if (line.startsWith("#")) {
                continue;
                
            }
            String[] fields = line.split("=", 2);
            if (fields.length != 2) {
                System.err.println("  Invalid record in properties:"+line);
                continue;
            }
            String key = fields[0].trim();
            String val = fields[1].trim();
            properties.put(key,val);
        }
        propRdr.close();
        
        setOrder(scale);
	Projection       proj = new Projection("Hpx");
        CoordinateSystem cs;
        if (properties.get("coordsys").equals("C")) {
            cs = CoordinateSystem.ICRS;
        } else {        
	    cs   = CoordinateSystem.Gal;
        }
	    
	// The 0,0 point of the oblique projection is at (+2,+2) squares.
	// This is at the point (3 pi/4, 0) in the original projection.
	Scaler s1 = new Scaler(-3*Math.PI/4, 0, 1, 0, 0, 1);
	    
	double isqrt2 = 1 / Math.sqrt(2);
	    
	// Now rotate by 45 degrees to get into the oblique projection.
	s1 = s1.add(new Scaler(0., 0., isqrt2, isqrt2, -isqrt2, isqrt2));
	    
	// Each square has a length of pi/sqrt(8), so pixels
	// have a length of pi/(nside*sqrt(8))
	double pixlen = Math.PI/(nside*Math.sqrt(8));
	    
	// The oblique projection is 4x6 squares, so the
	// center is 2x3 squares from the bottom left corner.
	double crpix1 = 2*nside;
	double crpix2 = 3*nside;
	    
	s1 = s1.add(new Scaler(crpix1, crpix2, 1/pixlen, 0, 0, 1/pixlen));
	    
	WCS myWCS = new WCS(cs, proj, s1);
		
	hpp = (Hpx) proj.getProjecter();
	
	hpp.setOrder(pixelOrder);
	initialize(null, myWCS, 4*nside, 6*nside, 1);
        setName(hipsDir+"/Norder"+hipsOrder);
    }

    /** Determine which order to sample at given the limits in the HiPS and
     *  the user requested scale.
     * @param scale Scale of desired pixels in radians.
     */
    public void setOrder(double scale) {
        scale = Math.toRadians(scale);
        
        // The number of pixels at a given scale is 12*nside^2.  Thus the
        // average are is 4 pi / (12*nside^2) = pi/(3*nside^2)
        // This gives a pixel scale of
        //     sqrt(pi/3)/nside = sqrt(pi/3)*2^(-order).  So
        // the order should be
        //      sqrt(pi/3)/2^order <~= scale
        
        //      2^order sqrt(pi/3)/scale.
        // If We take ln2 on both sides ln2 (x) = ln(x)/ln(2), but the ln(2) will cancel.
        //      order = ln(sqrt(pi/3)/scale) -- where scale is given in radians.
 
        
        if (scale <= 0) {
            throw new IllegalArgumentException("Illegal scale for HiPS image:"+scale);
        }
        
        // Get the maximum size of the image to be used...
        int tileOrder = Integer.parseInt(properties.get("hips_order"));
        tileSize  = Integer.parseInt(properties.get("hips_tile_width"));
        int tileSizeOrder = (int) Math.round( (Math.log(tileSize) / Math.log(2)) );
        int totalOrder = tileOrder+tileSizeOrder;
        
        // The side of a major HEALPix tile is Pi/sqrt(8).  We divide this by
        // the requested scale to get the number of pixels we want on the edge.
        double npix =  Math.PI/Math.sqrt(8)/scale + 1;
        
        int desiredOrder  = (int) Math.ceil(  Math.log(npix)/Math.log(2)  );
        
        if (desiredOrder <= 0) {
            throw new IllegalArgumentException("Scale to large for HiPS"+scale);            
        }
        if (desiredOrder > totalOrder) {
            desiredOrder = totalOrder;
        }
        
        
        pixelOrder = desiredOrder;
        if (pixelOrder < tileSizeOrder) {
            pixelOrder = tileSizeOrder;
        }
        hipsOrder = pixelOrder - tileSizeOrder;
        
        if (hipsOrder < MIN_ORDER) {
            
            allSky = true;
            if (pixelOrder > 6 + MIN_ORDER) {
                // User high resolution data.
                pixelOrder = 9 + MIN_ORDER;
                hipsOrder  = MIN_ORDER;
                allSky     = false;
            } else {
                pixelOrder = 6+MIN_ORDER;
            }
        }
        nside = (int) Math.pow(2, pixelOrder);
        filePixels = tileSize*tileSize;
    } 
    
    public int getWidth() {
	return 4*nside;
    }
    public int getHeight() {
	return 6*nside;
    }
    
    public int getDepth() {
	return 1;
    }
    
    
    /** Defer reading the data until it is asked for. */
    public double getData(long ipix) {
	
        
	// convert to healpix index.
	long npix = hpp.cvtPixel(ipix);
        if (allSky) {
            return getAllSkyPixel(npix);
        } else {
            return tiledPixel(npix);
        }
    }
    
    public double getAllSkyPixel(long npix) {
        int tilePixels = 64*64;
        int tile = (int)(npix / tilePixels);
        int zindex = (int) (npix % tilePixels);
        if (allskyData == null) {
            readAllSky();
        }
        int tileX = (tile%27) * 64;
        int tileY = (29-(tile/27)) * 64 - 1;
        int[] indices = hpp.getInterleave().indices(zindex);
        return allskyData[tileY - indices[0]][indices[1]+tileX];
    }
    
     
    int lastTile = -1;
    double[][] lastData;
    
    public double tiledPixel(long npix) {
        
        int tile   = (int)(npix/filePixels);
        
        int zindex = (int)(npix%filePixels);
        if (tile != lastTile) {
            lastTile = tile;
            if (dataCache == null) {
                dataCache = new HipsDataCache();
            }
            lastData = dataCache.get(tile);
            if (lastData == null) {
                lastData = readToCache(hipsOrder, tile/10000, tile);
                if (lastData == null) {
                    return Double.NaN;
                }
            }
        }
        if (lastData == null) {
            // Tile wasn't found
            return Double.NaN;
        }
        int[] indices = hpp.getInterleave().indices(zindex);
        int x = indices[0];
        int y = indices[1];
        return lastData[511-x][y];        
    }
        
    
    private double[][] readHipsFile(String file) {
        boolean remoteFile = true;
        if (Settings.has("LocalURL")) {
	    String[] fields = Settings.getArray("LocalURL");
	    if (hipsDir.startsWith(fields[0])) {
		String testfile = fields[1]+hipsDir.substring(fields[0].length())+"/"+file;                
		if (new File(testfile).exists()) {
                    file = testfile;
                    remoteFile = false;
                }
            }
        }
        Fits fts = null;
        try {
            if (remoteFile) {
                String lc = hipsDir.toLowerCase();
                if  (lc.startsWith("http")  || lc.startsWith("ftp")) {
                    Cacher cache = new Cacher();
                    file = cache.getFile(hipsDir+"/"+file, file);
                }
            }
            fts = new Fits(file);
          
            BasicHDU hdu;
            
            try {
                hdu = fts.readHDU();
            } catch (PaddingException e) {
                hdu = e.getTruncatedHDU();
            }
            Object o = hdu.getKernel();
            double[][] img = (double[][])nom.tam.util.ArrayFuncs.convertArray(o, double.class);
            fts.close();
            return img;
        } catch (Exception e) {
            return null;
        } catch (Error e) {
            return null;
        }
    }
    
    private void readAllSky() {
        String all = "Norder"+MIN_ORDER+"/Allsky.fits";
        allskyData = readHipsFile(all);
        if (allskyData != null) {
            allskyY = allskyData.length;
            allskyX = allskyData[0].length;
        }
    }
    
    private double[][] readToCache(int order, int bunch, int tile) {
        if (nullTiles.contains(tile)) {
            return null;
        }
        
        if (moc != null) {
            if (!moc.test(order, tile)) {
                nullTiles.add(tile);
                return null;
            }
        }
        
        double[][] data = readHipsFile("Norder"+order+"/Dir"+10000*bunch+"/Npix"+tile+".fits");
        if (data == null) {
            nullTiles.add(tile);
        } else {
            if (dataCache == null) {
                dataCache = new HipsDataCache();
            }
            dataCache.put(tile, data);
        }
       return data;
        
    }
    
    /** Probably should happen, but just in case we
     *  support the get array function.
     */
    public double[] getDataArray() {
        throw new IllegalArgumentException();
    }
    
    /** Support changing the data!  Probably won't use this...
     */
    public void setData(long npix, double val) {
        throw new IllegalArgumentException();	
    }
    
    
 
    /** This class maintains a cache of data from recently used
     *  files.  A total of maxSize files may be retained.
     */
    class HipsDataCache {
        double[][][] data;
        Map<Integer, Integer> cacheMap;
        Map<Integer, Integer> reverseMap;
        
        int size;
        int nextIndex;
        int maxSize = 16;
        
        HipsDataCache() {
            initialize();
        }
        
        HipsDataCache(int size) {
            maxSize = size;
            initialize();
        } 
        void initialize() {
            data = new double[maxSize][][];
            cacheMap = new HashMap<Integer,Integer>();
            reverseMap = new HashMap<Integer,Integer>();
            size = 0;
            nextIndex = 0;
        }
        
        double[][] get(Integer input) {
            Integer index = cacheMap.get(input);
            if (index == null) {
                return null;
            } else {
                return data[index];
            }
        }
        
        void put(Integer tileIndex, double[][] tile) {
            data[nextIndex] = tile;
            Integer oldTileIndex = reverseMap.get(nextIndex);
            if (oldTileIndex != null) {
                cacheMap.remove(oldTileIndex);
            }
            cacheMap.put(tileIndex, nextIndex);
            reverseMap.put(nextIndex, tileIndex);
            nextIndex += 1;
            if (nextIndex >= maxSize) {
                nextIndex = 0;
            }
        }
    }
}


