package skyview.geometry.projecter;

import static java.lang.Math.PI;

import skyview.geometry.Projecter;
import skyview.geometry.Deprojecter;
import skyview.geometry.Transformer;
import skyview.geometry.Util;


/**
 *  The HEALPix project projects the sky into equal area pixels where
 *  the centers of the pixels are lined up on constant values of
 *  declination.  The nominal HEALPix projection is to the plane and
 *  the pixels are shaped like diamonds in this plane.  Since we want
 *  to have square (or at least rectangular) pixels we often use a
 *  rotated plane where we have rotated by 45 degrees and also
 *  rescaled the tiles (only slightly) to be unit squares.  This
 *  is the normalized or oblique projection.
 * 
 * 
 *  In the oblique projection we arrange the 12 base squares into the
 *  following orientation
 *  <code>
 *     3       
 *     40
 *     851
 *      962
 *       A7
 *        B
 *  </code>
 * Thus the 12 data squares can be enclosed in a 6x4 array including another
 * 12 unused squares.  The diagonal stripe continues where each of the three
 * rows repeats 01230123..., 456745674567..., 89AB89AB89AB... indefinitely
 * and we are free to pick the most convenient arrangement.
 * <p>
 * An alternative arrangement might be.
 * <code>
 *     40  
 *     851 
 *      962
 *       A73
 *        B
 * </code>
 * where the data squares can be enclosed in a 5x5 array.  (This
 * is similar to the Calabretta arrangement except that they would
 * repeat tile 4 below tile 3.  Note that we use a bend dexter rather
 * than the bend sinister in Calabretta since we treat the longitude
 * coordinate are increasing to the right.)
 * <p>
 * The actual transformations to and from the coordinate plane
 * are carried out using the static methods proj and deproj which
 * are called by the relevant method of Hpx and HpxDeproj.  Note
 * that HpxDeproj is included as a static class.
 * <p>
 * HEALPix is a true transformation so this transformation function
 * does not depend upon the input order (i.e., the number of pixels
 * in the pixelization).
 * This does affect ancillary functions (notably cvtPixel) which are
 * used when individual pixels are to be considered rather than the
 * geometric transformation between sphere and plane.
 * 
 *  <p>
 * The nominal HEALPix Projection runs from 0-2 PI in x and is fully filled
 * between 0 and +- Pi/4 in y.  It has triangular teeth that extend from the
 * filled region to Pi/2 and cover half the vertical region between PI/4 and PI/2.
 * Thus the total area covered by the projections is 2 PI * (PI/2 + 1/2 * PI/2)
 * A = 2 PI * 3/4 PI.  In principle since this is an area conserving transformation
 * we might expect the total area to be 4 PI.  However to allow these convenient boundaries
 * to the map the nominal project expands pixels by a factor of 3 PI/8.  The nominal
 * area of the projection is A=3 PI^2/2
 * A = 3 PI^2 /2.  So the area of the pixels expands by a factor of 3 PI/8 relative
 * to the area on the unit sphere.  A=14.8 so we shrink the tiles slightly when
 * we work in the oblique normalized projection where the tiles are unit squares
 * and the total area is exactly 12.
 *
 * <p>
 * Given that the total area is 3*PI^2/2, the area of each tile is 
 *    3*PI^2 / 24 =  PI^2/8
 * Thus each of the tile sides is sqrt(A/12) = PI/SQRT(8) = PI/ (2 SQRT(2))
 * in the nominal HEALPix projection (in which the tiles are oriented as diamonds)
 * 
 */

public class Hpx extends Projecter {
    
    /** We treat the HEALPix projection
     *  as  4x6 array of tiles -- some not used.  This array
     *  indexes the tile numbers with -1 indicating
     *  that that tile is not used in the projection.
     *  Note that we are starting at the bottom left and
     *  working our way right and then up (so that the following
     *  array declaration is upside down from the perspective of the projection.)
     */
    private int[] tileNum  = {
       -1, -1, -1, 11,
       -1, -1, 10,  7,
       -1,  9,  6,  2,
	8,  5,  1, -1,
	4,  0, -1, -1,
	3, -1, -1, -1,
    };
        
    /** The coordinates of the corners of squares 0 to 11. */   
    static double[] botLeftX={-1,  0,  1, -2, -2, -1,  0,  1, -2, -1,  0,  1};
    static double[] botLeftY={ 1,  0, -1,  2,  1,  0, -1, -2,  0, -1, -2, -3};
    
    private static final double[] error= {Double.NaN, Double.NaN, Double.NaN};
    
    /* The offsets are chosen with respect to an origin at the corner
     *  shared by squares 5 and 6.  This is at the position (3 pi/4, 0)
     *  in the nominal HEALPIX x,y plane.
     */
    private double[] zeroPoint = {0.75*PI, 0};
    
    // Recall sin(45)=cos(45) = 1/sqrt(2).  This will
    // be used in the rotation.
    private double isqrt2 = 1./Math.sqrt(2);
    
    private double pi2 = Math.PI/2;
    
    /** Number of pixels on one site of the 12 base tiles. */
    private long nSide;
    
    /** Total number of pixels in the map for the current order */
    private long nPixel;
    
    /** Number of pixels in one of the 12 base tiles.
     *  nPixel = 12*nSq = 12*(nSide*nSide) = 12*(2^order * 2^order)
     */
    private long nSq;
    
    /** The order of the HEALPix pixels */
    private int order;

    /** 1./nSide */
    private double sqDelta;
    
    /** Length of square in nominal HEALPix projection */
    private final double squareLength = PI/2 * isqrt2;
    
    private final double invLength = 1/squareLength;
    
    // Remember that 1/sqrt(2) applies in both directions for the rotation.
    private final double rotScale     = invLength * isqrt2;
    private final double invRotScale  = isqrt2/invLength;
    
    static private final double oneMinus = 1 - 3*Math.ulp(1.);
    
    private HpxStraddle straddler;
    
    /** Default to the 512x512 squares */
    public Hpx() {
	this(9);
    }
        
    /** Interleaver to do calculations */
    private Interleave weaver;
    
    
    /** @param order The power of two giving the number of pixels
     *             along an edge of a square.  The total number
     *             of pixels in the projection is 12 * Math.pow(2, 2*order)
     */
    public Hpx(int order) {
	setOrder(order);
    }
    
    /** Get the size of the HEALPix pixels in the projection frame. */
    public double getHealpixScale() {
        return sqDelta*invLength;
    }
    
    /** Set up the base geometry of the HEALPix projection for the
     *  given order.
     * @param order 
     */
    public void setOrder(int order) {
	this.order = order;
	
	nSide   = (long) Math.pow(2, order);
	nSq     = nSide * nSide;
	nPixel  = 12*nSq;
	sqDelta = 1./nSide;
        if (order >= 10) {
            weaver = new Interleave(10);
        } else {
            weaver = new Interleave(order);                
        }
    }
    
    public Interleave getInterleave() {
        return weaver;
    }
    
    
    public String getName() {
	return "Hpx";
    }
    
    public String getDescription() {
	return "HEALPix projection";
    }
    
    public Deprojecter inverse() {
	return new HpxDeproj();
    }
    
    
    public boolean isInverse(Transformer t) {
	return false;
// We'd like this to cancel out with HPXDeproj
// but then the scaler can send data outside the bounds of
// the underlying image.
//	return t instanceof HpxDeproj;
    }

    /** Decide whether this is in the valid field of the HEALPix projection.
     *  This uses the geometry noted above.
     * @param plane
     * @return 
     */
    public boolean validPosition(double[] plane) {
        if (!super.validPosition(plane)) {
            return false;
        }
        double[] norm = normCoords(plane);
        // Get the tile indices.
        int x = (int) Math.floor(norm[0]);
        int y = (int) Math.floor(norm[1]);
        
        // Get the index of the tile rembering that the first tile
        // starts at [-2,-3]
        int tile = (4*(y+3) + (x+2));
        
	return 
          // It's within our 4x6 tiling
          x >= -2 && x < 2 && 
          y >= -3 && y < 3 &&
          // ... and it's not a blank tile
          tileNum[tile] >= 0
                ;
    }
	  
    /** Get the unscaled (i.e., unit tiles) lower left corner in the oblique projection */
    public double[] getOblCorner(long pix) {
	
	if (pix < 0 || pix >= nPixel) {
	    return new double[]{Double.NaN, Double.NaN};
	}
	
        // Which of the 12 main tiles are we?
	int square = (int)(pix/nSq);
	
        // Find the index within that tile.
	long    rem   = pix % nSq;
        
        // Find the pixel within the square.
        int[]   index = weaver.indices(rem);
        double  x = index[0]*sqDelta + botLeftX[square];
        double  y = index[1]*sqDelta + botLeftY[square];        
	return new double[]{x,y};
    }
	    
    
    /** Get the scaled corners of a pixel in the nominal HEALPix projection */
    public double[][] getCorners(long pix) {
	
	double[][] z = new double[4][2];
	
	z[0]    = getOblCorner(pix);
	z[1][0] = z[0][0] + sqDelta;
	z[1][1] = z[0][1];
	z[2][0] = z[1][0];
	z[2][1] = z[0][1] + sqDelta;
	z[3][0] = z[0][0];
	z[3][1] = z[2][1];
	
	for (int i=0; i<z.length; i += 1) {
	    double[] t = z[i];
	    
	    double   u = t[0];
	    double   v = t[1];
	    
	    t[0] = squareLength*(isqrt2*u - isqrt2*v) + zeroPoint[0]; 
	    if (t[0] > 2*PI) {
		t[0] -= 2*PI;
	    } else if (t[0] < 0) {
		t[0] += 2*PI;
	    }
	    
	    t[1] = squareLength*(isqrt2*u + isqrt2*v) + zeroPoint[1];
	}
	return z;
    }

    /** Given an X-Y in the nominal HEALPix projection, return
     *  the unit vector on the sphere.
     */
    public static void deproj(double[] in, double[] unit) {
	
	double x = in[0];
	double y = in[1];
	if (x < 0) {
	    x += 2*PI;
	}
	if (x > 2*PI) {
	    x -= 2*PI;
	}
	
	// Check that we are in the valid region.
	if (Math.abs(y) > 0.5*PI) {
	    System.arraycopy(error, 0, unit, 0, error.length);
	    return;
	}
	if (Math.abs(y) > 0.25*PI) {
	    double posit = (x/PI) % 0.5; 
	    double yt    = Math.abs(y)/PI;
	    
	    // Add a small delta to allow for roundoff.
	    if (yt > (0.5-Math.abs(posit-0.25))+1.e-13 ) {
		System.arraycopy(error, 0, unit, 0, error.length);
	        return;
	    }
	}
	
	double ra, sdec;
	if (Math.abs(y) < PI/4) {
	    ra   = x;
	    sdec = (8*y/(3*PI));
	    
	} else {
	    
	    double yabs = Math.abs(y);
	    double xt   = x % (PI/2);
	    ra  = x - (yabs - PI/4)/(yabs-PI/2) * (xt - PI/4);
	    if (Double.isNaN(ra)) {
		ra = 0;
	    }
	    sdec = (1 - (2-4*yabs/PI)*(2-4*yabs/PI)/3 ) * y / yabs;
	    if (sdec > 1) {
		sdec =  1;
	    } else if (sdec < -1) {
		sdec = -1;
	    }
	}
	double cdec = Math.sqrt(1-sdec*sdec);
	unit[0] = Math.cos(ra)*cdec;
	unit[1] = Math.sin(ra)*cdec;
	unit[2] = sdec;
	return;
    }
	    

    public static void proj(double[] unit, double[] proj) {
	
	if (Math.abs(unit[2]) < 2./3) {
	    proj[0] = Math.atan2(unit[1], unit[0]);
	    if (proj[0] < 0) {
		proj[0] += 2*PI;
	    }
	    proj[1] = 3*PI/8 * unit[2];
	    
	} else {
	    double phi = Math.atan2(unit[1], unit[0]);
	    if (phi < 0) {
		phi += 2*PI;
	    }
	    
	    double phit = phi % (PI/2);
	    
	    double z    = unit[2];
	    double sign = 1;
	    if (z < 0) {
		z    = -z;
		sign = -1;
	    }
	    
	    double sigma = sign*(2-Math.sqrt(3*(1-z)));
	    proj[0] = phi - (Math.abs(sigma)-1)*(phit-PI/4);
	    proj[1] = PI*sigma/4;
	}
	double x = proj[0]/Math.PI;
	double y = proj[1]/Math.PI;
	// We move the right half of tile 4 and all of tile 3 back by 2 PI
        // so that the standard region is appropriate for the 4x6 region in the
	// rotated coordinates.
	if (x >= 1.5 && y > 1.75-x)  {
	    proj[0] -= 2*Math.PI;
	}
    }
    
    public void transform(double[] sphere, double[] plane) {
	proj(sphere, plane);
    }
    
    /** Find the pixel that includes the given position.
     * 
     * @param pos The position in the nominal HEALPix projection plane */
    
    public long getPixel(double[] pos) {
	// Move to standard rotation center
        double[] uv = normCoords(pos);		
	return getObliquePixel(uv[0], uv[1]);
    }
    
    
    /** Given the coordinates in the normalized oblique
     *  projection, find the pixel number.
     */
    public long getObliquePixel(double u, double v) {
	
        double[] c = new double[]{u,v};
        
        // Note that normTile may modify the elements of c slightly
        // if we are on an edge.
        int tile = normTile(c);
	
	long pix = nSq*tile;
        
        double xSq = Math.floor(c[0]);
        double ySq = Math.floor(c[1]);
	
	int x = (int)((c[0] - xSq)/sqDelta);
	int y = (int)((c[1] - ySq)/sqDelta);
        pix += weaver.pixel(x, y);
	return pix;
    }

    /** Try to fix roundoff errors that move us
     *  off of edges.  We only worry about unrounding
     *  that moves to just under an integer value
     *  because it is the floor value of the pixel
     *  that determines which tile it is in. 
     * @return 
     */
    double unround(double x) {
        if (x - Math.floor(x) > oneMinus) {
            x = Math.ceil(x);
        }
        return x;
    }
    
    public double[] normCoords(double[] position) {
        
        double x=position[0], y=position[1];
        double u,v;
        
        // Subtract out the zero point
        x -= zeroPoint[0];
        y -= zeroPoint[1];        
                	
	// Now rotate to the oblique plan.
	u = ( x+y)*rotScale;
	v = (-x+y)*rotScale;

        u = unround(u);
        v = unround(v);
        
        return new double[]{u,v};
    }
    
    public double[] denorm(double[] position) {
        
        double u = position[0]*invRotScale;
        double v = position[1]*invRotScale;
        
        double x = u - v;
        double y = u + v;
        
        x += zeroPoint[0];
        y += zeroPoint[1];
        
        return new double[]{x,y};
    }

    /** Return the tile number corresponding to the nominal projection location. */
    public int tile(double[] position) {
        return normTile(normCoords(position));
    }
    
    /** Return the tile number corresponding to the normalized coordinate location. */
    public int normTile(double[] norm) {
        
        
        // Get the integer x,y with reference to the bottom
        // left of our nominal 4x6 array of tiles.
        int x = (int)Math.floor(norm[0]) + 2;
        int y = (int)Math.floor(norm[1]) + 3;
        
        
        
        int indx = 4*y + x;
        int tile = -1;
        if (x>=0 && x < 4 && y >= 0 && y < 6) {
            tile = tileNum[indx];
        }
        if (tile < 0) {
            // Note that fixEdges will have slightly modified
            // the value of norm if it returns true to get us
            // off a non-included edge of region.
            fixEdges(norm);
        }
        return tile;
    }
    
    /** Handle data along the edges of the tiles.  We subtract a tiny increment
     *  to get off the edge.  Note that because of the way we
     *  truncate, we only need to worry about the upper or right edges.
     * @param norm The normalized coordinates.
     * @return 
     */
    boolean fixEdges(double[] norm) {
        
        double x = norm[0], y=norm[1];
        boolean dx=false, dy=false;

        // First check the horizontal boundaries.
        if (y == 3  || y == 2 || y == 1 || y == 0) {
            if (x == 2-y) {  // Corner.
                dx = true; dy = true;
                
            } else if (x >= 1-y && x < 2-y) {  // Horizontal line
                dy = true;
            }

        // Now check the internal vertical boundaries.
        } else if ( (x == -1 || x == 0 || x == 1)  &&
                    (y >= (1-x) && y < (2-x) ) )  {  // Vertical line
            dx = true;            
        
        // Check the right edge.  This includes one tile similar to the
        // two in the check above and two tiles where we have a straddle.
            
        } else if (x == 2  && y >= -3 && y < 0) {
            dx = true;
        }
                
        if (dx) {
            norm[0] -= 1.e-12;
        }
        if (dy) {
            norm[1] -= 1.e-12;
        }       
        return dx || dy;
    }
    
    
    // Given an x-y in the nominal HEALPix projection, calculate
    // the X,Y pixel number for the given location.
    // This depends on the order of the HEALPix.
    public double[] rotateAndScale(double[] position) {
        double[] rot = normCoords(position);
        rot[0]   /= sqDelta;
        rot[1]   /= sqDelta;
        return rot;
    }
    
    
    /** Get the number of pixels on each side of one of the 12 main HEALPix tiles.
     *  Note that while nSide is stored internally as a long to minimize
     *  long/int transformations, it must be in the range of an int.
     * @return 
     */
    public long getNSide() {
        return nSide;
    }
    
    
    /** This method converts a pixel number based on the assumption
     *  that we have a simple two-d image map, into the nested HEALPix
     *  pixel number. This routine assumes that the input pixel
     *  numbers are associated with a (4 nSide)x(6 nSide) virtual
     *  image.  Note that this is assumed to be in the oblique frame.
     */
    public long cvtPixel(long pixel) {
	
	long    ix = pixel % (4 * nSide);
	long    iy = pixel / (4 * nSide);
	
	
	double px = (double)(ix)/nSide;
	double py = (double)(iy)/nSide;
	
	long result = getObliquePixel(px-2, py-3);
	
	return result;
    }
    
    // Methods for handling straddling.  The content is in the HpxStraddle class.
    
    /** Can a region straddle in the projection  -- and do
     *  we have code that can address this?
     */
    public boolean straddleable() {
        return true;                
    }
    
    /** Does this region specified by the points straddle? */    
    public boolean straddle(double[][] xy) {
        haveStraddler();
        return straddler.straddle(xy);
    }
    
    /** Decompose a straddling region into multiple
     *  non-straddling regions.
     */
    public double[][][] straddleComponents(double[][] xy) {
        haveStraddler();
        return straddler.straddleComponents(xy);
    }
    
    /** Defer getting the straddler until we need it */
    private void haveStraddler() {
        if (straddler == null) {
            straddler = new HpxStraddle(this);
        }
    }
    
	    
    public static void main(String[] args) {
	
        System.out.println("Hpx testing");
	Hpx ob = new Hpx(Integer.parseInt(args[0]));
        System.out.println("The number of pixels per side is:"+ob.nSide);
        System.out.println("Looking at unrounding params:"+oneMinus+" "+(1-oneMinus)+" "+3*Math.ulp(1.));
        double ra = Double.parseDouble(args[1]);
        double dec = Double.parseDouble(args[2]);
        System.out.println("Order:"+args[0]+ " Input position:"+ra+" "+dec);
        double[] unit = Util.unit(Math.toRadians(ra), Math.toRadians(dec));
        double[] coords = ob.transform(unit);
        System.out.println("Transforms to:"+Math.toDegrees(coords[0])+" "+Math.toDegrees(coords[1]));
        double[] norm = ob.normCoords(coords);
        System.out.println("Normalized this is:"+norm[0]+" "+norm[1]);
        System.out.println("This is in pixel:"+ob.getPixel(coords));
        System.out.println("This is in tile:"+ob.tile(coords));
        double[] test = ob.rotateAndScale(coords);
        System.out.println("We get major tile indices of:"+coords[0]/ob.nSide+" "+coords[1]/ob.nSide);
        System.out.println("This is in the overall image pixel :"+test[0]+" "+test[1]+" relative to 0,0 at 135,0");
	
    }
    
    public static class HpxDeproj extends Deprojecter {
	
	public String getName() {
	    return "HpxDeproj";
	}
	
	public String getDescription() {
	    return "HEALPix deprojector";
	}
	
	public Transformer inverse() {
	    return new Hpx();
	}
	
	public boolean isInverse(Transformer t) {
	    return false;
//  Would like to do the following but seems to cause things
//  to run outside proper bounds
//	    return t instanceof Hpx;
	}
	
	public void transform(double[] plane, double[] sphere) {
	    deproj(plane, sphere);
        }
    }
}
