
package skyview.geometry.projecter;

import java.util.ArrayList;
import java.util.List;
import skyview.geometry.sampler.Clip;

/**
 * This class handles straddling in the Healpix
 * projection.  It is intended to handle small pixel straddling,
 * not so much lines more than a few degrees in size.
 * 
 * Recall the normalized version of the HEALPix projection
 * where there are 12 unit tiles arranged so:
 * 
 * 3
 * 4  0
 * 8  5  1
 *    9  6  2
 *      10  7
 *         11
 * The top right corners of tiles 3, 0, 1 and 2 join at the north pole.
 * The bottom left corners of tiles 8,9,10,11 join at the south pole.  The
 * line on the right edge of tile 3 is identified with the line at the top edge
 * of tile 0 and so forth.  If we simply join lines in in the projection naively
 * the line can easily leave and re-enter the valid legion because of this
 * jaggedness near the pole.  The other straddling is similar to what we
 * see in the Car projection since the left edge of tile 3 is identified with the right
 * edge of tile 7 and similarly for tiles 8 and 11.
 * @author tmcglynn
 */
public class HpxStraddle extends Straddle {
       
    /** The HEALPix projection.  We will only be interested in this
     *  as a projection, not as a pixelization, so the order does not
     *  enter into any calculations here.
     */
    private Hpx hpx;
    
    /** A clipper to use to clip to the tile boundaries. */
    private Clip clipper;
    
    // First two elements are coordinates and third indicates
    // top or bottom.  
    private double[][] corners = {{-1,2, 1}, {0,1, 1}, {1,0, 1},
                                  {-1,0, -1}, {0,-1,-1},{1,-2,-1}};
    

    /* Keep track of the different kind of straddles to ensure that we've
     *  seen apprpropriate numbers of each.
     */
    private int northStraddles;
    private int southStraddles;
    private int edgeStraddles;
    
    /** Get the statistics on the kind of straddles processed by this
     *  objects as an array of {north, south, edge}
     */
    public int[] getStraddleStats() {
        return new int[]{northStraddles, southStraddles, edgeStraddles};
    }

    /** Create a straddler associated with the projection */
    public HpxStraddle(Hpx proj) {
        this.hpx = proj;
    }
    
    /** Does any segment in this sequence of points
     *  stadddle.  Note that the points array is
     *  input as points[2][n] since we want to be
     *  able to extract x/y arrays.
     */
    public boolean straddle(double[][] points) {
        
        // Note limits:  We want to check the segment between the
        // last point and the first too.
        for (int i=1; i <= points[0].length; i += 1) {
            int i0 = i-1;
            
            int i1 = i % points[0].length;
            double[] p0 = new double[] {points[0][i0], points[1][i0]};
            double[] p1 = new double[] {points[0][i1], points[1][i1]};
            if (segmentStraddle(p0, p1)) {
                return true;
            }        
        }
        // No segment straddles.
        return false;        
    }
    
    /** Does the line between the two points straddle? */
    public boolean segmentStraddle(double[] x1, double[] x2) {
        
        double[] n1 = hpx.normCoords(x1);
        double[] n2 = hpx.normCoords(x2);
                
        int tile1 = hpx.tile(x1);
        int tile2 = hpx.tile(x2);
        if (tile1 == tile2) {
            return false;
        }
        
        // Check for edge straddles.
        if ((tile1 == 3 || tile1 == 4 || tile1 == 8)  &&
            (tile2 == 2 || tile2 == 7 || tile2 == 11)) {
            return true;
        }
               
        if ((tile2 == 3 || tile2 == 4 || tile2 == 8)  &&
            (tile1 == 2 || tile1 == 7 || tile1 == 11)) {
            return true;
        }
        
        // Ensure that n1 is lower than n2.
        // Flip them if not.
        if (n1[1] > n2[1]) {
            double tmp = n1[0];
            n1[0] = n2[0];
            n2[0] = tmp;
            
            tmp = n1[1];
            n1[1] = n2[1];
            n2[1] = tmp;
        }

        // Compute the distance and slope between the points.
        double d2 = dist2(n1,n2);
        double slp = slope(n1,n2);
        
        
        if (slp < 0) {  
            // If the slope is positive then the line cannot leave and re-enter the occupied region
            // given the way we arrange the tiles.  Since both ends are valid, a positive
            // slope means no straddling.
            
            // Check to see if we might be cutting one of the corners...
            for (int i = 0; i < corners.length; i += 1) {

                double[] corn = corners[i];
                
                
                // We have ensured that n1 is lower than
                // n2, so if the corner is below n1 we needn't
                // consider it.
                if (corn[1] > n1[1]) {
                    
                    if (dist2(n1, corn) < d2) {
                        
                        // Corner is closer than other point, so
                        // we could potentially leave and re-enter.
                        // So now check the slope.
                        
                        double cslp = slope(n1, corn);
                        
                        // Depending upon whether we are looking at the
                        // corners on the top or bottom, we need
                        // the slope to the corner to be greater or less than
                        // the slope to the other point.
                        
                        if (corn[2] == 1) {  // Top edge of covered region.
                            if  (slp < cslp) {
                                 // I.e., Slope to other point is more negative/steeper than slope to corner.
                                 return true;
                            }
                        } else if (slp > cslp) {  // Bottom edge of covered region.
                            // Shallower negative number
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }    
    
    /** Square of distance between points */
    double dist2(double[] x1, double[] x2) {
             return 
                     (x1[0]-x2[0])*(x1[0]-x2[0]) +
                     (x1[1]-x2[1])*(x1[1]-x2[1]);
                     
    }

    /** Slope between points */
    double slope(double[] x1, double[] x2) {
        double  dx = x2[0] - x1[0];
        if (dx == 0) {
            return Double.POSITIVE_INFINITY;
        }
        double dy = x2[1] - x1[1];
        return dy/dx;
    }

    /** Given figure which may straddle, return
     *  a set of figures which do not.  Note that figures
     *  are given in [2][n] arrays (i.e, arrays of x and y separately)
     *  The returned array is [m][2][*] where m is the number of 
     *  subfigures the input was broken into, each of which can have
     *  a variable number of vertices (&gt;=3).
     */
    public double[][][] straddleComponents(double[][] vertices) {
        int nv = vertices[0].length;
        
        int[] tiles = new int[12];
        
        for (int i=0; i<nv; i += 1) {
            int tile = hpx.tile(new double[]{vertices[0][i], vertices[1][i]});
            if (tile < 0) {
                System.err.println("HpxStraddle: Off edge of tile in straddle");
                return null;
            }
            // Mark the tile as included.
            tiles[tile] = 1;
        }
        
        // We have three possible straddle issues, being near either pole (i.e.,
        // straddling across the triangular teeth of the projection), or
        // crossing edge of the tiles at at the top left or bottom right of the figure. 
        
        // Do we have two tiles near a pole?
        boolean southPole = tiles[8]+tiles[9]+tiles[10]+tiles[11] > 1;
        boolean northPole = tiles[0]+tiles[1]+tiles[2]+tiles[3] > 1;
        
        // Do we have tiles from both the top left and bottom right?
        boolean edge      = (tiles[3]+tiles[4]+tiles[8]) > 0   &&
                            (tiles[2]+tiles[7]+tiles[11]) > 0;

        // We are only handling 'small' pixels so we can't have a pixel that
        // has corners both above 41 degrees north and also below 41 degrees south.
        if (southPole && (tiles[0] > 0  || tiles[1] > 0  || tiles[2] > 0 || tiles[3] > 0)) {
            System.err.println("HpxStraddle: Near both poles simultaneosuly.  Unable to straddle");
            return null;
        }
        if (northPole && (tiles[8] > 0  || tiles[9] > 0  || tiles[10] > 0 || tiles[11] > 0)) {
            System.err.println("HpxStraddle: Near both poles simultaneosuly.  Unable to straddle");
            return null;
        }
        
        // This is subtle, but the segments leaving the non-pole pixels do not need to be
        // shadowed.
        if ( (northPole|southPole) && edge && 
            (tiles[7] == 0 && tiles[4] == 0)) { 
            edge = false;  // Can handle this just with the pole.
        }
        
        if (northPole) {
            northStraddles += 1;            
        } else if (southPole) {
            southStraddles += 1;
        } else if (edge) {
            edgeStraddles += 1;
        }
        
        // Now determine which tiles we are going to look for components in.
        // In principle at the pole it's easy to run through a tile so we need
        // to look at all four tiles near the pole.
        boolean[] chkTiles = new boolean[12];
        if (northPole) {
            chkTiles[0] = chkTiles[1] = chkTiles[2] = chkTiles[3] = true;
        }
        if (southPole) {
            chkTiles[8] = chkTiles[9] = chkTiles[10] = chkTiles[11] = true;
        }
        
        // Now add in any tiles on which there are pixels.  Note that it's possible
        // to include an internal corner so that it goes through a tile for
        // which there is no point, but if we only have small regions, then
        // such a figure will not straddle.  We assume that we checked
        // that the figure straddled beforce calling this.
        
        for (int i=0; i<12; i += 1) {
            if (tiles[i] > 0) {
                chkTiles[i] = true;
            }
        }
        
        // Don't know how many we will have...
        List<double[][]> regions = new ArrayList<double[][]>();

        // Convert the standard projection values into normalized oblique coordinates.
        double[][] normVert = new double[2][nv];
        for (int i=0; i<nv; i += 1) {
            double[] ipnt = new double[]{vertices[0][i], vertices[1][i]};
            double[] pnt = hpx.normCoords(ipnt);
            normVert[0][i] = pnt[0];
            normVert[1][i] = pnt[1];
        }
        
        for (int tile=0; tile<12; tile += 1) {
            
            if (chkTiles[tile]) {
                double[][] region = clipToTile(tile, northPole, southPole, edge, normVert);
                if (region != null) {
                    int n = region[0].length;
                    double[][] xreg = new double[2][n];
                    
                    // Convert back to standard projection coordinates.
                    for (int i=0; i<n; i += 1) {
                        double[] xpos = hpx.denorm(new double[]{region[0][i], region[1][i]});                                
                        xreg[0][i] = xpos[0];
                        xreg[1][i] = xpos[1];
                    }
                    regions.add(xreg);
                }
            }
        }
        
        // Convert to fixed array.
        double[][][] result = new double[regions.size()][][];
        for (int i=0; i<regions.size(); i += 1) {
            result[i] = regions.get(i);
        }
        return result;
    }
    
    /** See if we have any region on the requested tile.
     *  The input vertices are the normalized oblique coordinates, but
     *  we return points in the standard projection coordinates.
     * @param tile
     * @param north
     * @param south
     * @param edge
     * @param vertices
     * @return 
     */
    double[][] clipToTile(int tile, boolean north, boolean south, boolean edge, double[][] vertices) {
        
        int nv = vertices[0].length;
        double tx = hpx.botLeftX[tile];
        double ty = hpx.botLeftY[tile];
            
        double[][] xvert = new double[2][nv];
        
        if (tile < 4 && north) {
            // We should only have six tiles to worry about.
            // The four pole tiles and the two equatorial tiles attached
            // to this tile.
            // We create shadow points in the other pole tiles at the appropriate location.
            // and then we clip to this tile.
            for (int i=0; i<nv; i += 1) {
               int vtile = hpx.normTile(new double[]{vertices[0][i], vertices[1][i]});
               if (vtile == tile || vtile > 4) {
                   xvert[0][i] = vertices[0][i];
                   xvert[1][i] = vertices[1][i];
               } else {
                   int delta = tile - vtile;
                   if (delta < 0) {
                       delta += 4;
                   }
                   
                   double x = vertices[0][i] - hpx.botLeftX[vtile];
                   double y = vertices[1][i] - hpx.botLeftY[vtile];
                   
                   // The goal here is to leave tile alone but wrap the other
                   // three corner tiles into a 2x2 square to determine the shadow pixels.
                   if (delta == 1) {
                       xvert[0][i] = tx + y;
                       xvert[1][i] = ty + 2 - x;
                   } else if (delta == 2) {
                       xvert[0][i] = tx + 2 - x;
                       xvert[1][i] = ty + 2 - y;
                   } else { // 3
                       xvert[0][i] = tx + 2 - y;
                       xvert[1][i] = ty + x;
                   }
               }
            }
            
        } else if (tile > 7 && south) {
            
            // Same as above but for south pole.
            for (int i=0; i<nv; i += 1) {
               int vtile = hpx.normTile(new double[]{vertices[0][i], vertices[1][i]});
               if (vtile == tile || vtile < 8) {
                   xvert[0][i] = vertices[0][i];
                   xvert[1][i] = vertices[1][i];
               } else {
                   
                   int delta = tile - vtile;
                   if (delta < 0) {
                       delta += 4;
                   }
                   
                   double x = vertices[0][i] - hpx.botLeftX[vtile];
                   double y = vertices[1][i] - hpx.botLeftY[vtile];
                   if (delta == 1) {
                       xvert[0][i] = tx - y;
                       xvert[1][i] = ty + x;
                   } else if (delta == 2) {
                       xvert[0][i] = tx - x;
                       xvert[1][i] = ty - y;
                   } else { // 3
                       xvert[0][i] = tx + y;
                       xvert[1][i] = ty - x;
                   }
               }
            }
        } else if (edge && 
              (tile == 2 || tile == 3 || tile == 4 || tile == 7 || tile == 8 || tile == 11) ) {
            // Handle wraparounds.
            boolean top = (tile == 3 || tile == 4 || tile == 8);
            // Edge pixels are just shifted diagonally 4.  The
            // direction depends upon whether tile is at the top left or bottom right.
            for (int i=0; i<nv; i += 1) {
                double delta = 0;
                int vtile = hpx.normTile(new double[]{vertices[0][i], vertices[1][i]});
                if (top && (vtile == 2 || vtile == 7 || vtile == 11)) {
                    delta = 4;
                } else if (!top && (vtile == 3 || vtile == 4 || vtile == 8)) {
                    delta = -4;
                }
                xvert[0][i] = vertices[0][i] - delta;
                xvert[1][i] = vertices[1][i] + delta;
            }
        } else {
            for (int i=0; i<nv; i += 1) {
                xvert[0][i] = vertices[0][i];
                xvert[1][i] = vertices[1][i];
            }
        }
        
            
        return clip(tx, ty, xvert);
    }
    
    double[][] clip(double x, double y, double[][] xvert) {
                
        if (clipper == null) {
            clipper = new Clip();
        }
        double[] xo = new double[10];
        double[] yo = new double[10];
        
        int n = clipper.rectClip(xvert[0].length, xvert[0], xvert[1], xo, yo, x, y, x+1, y+1);
        if (n <= 0) {
            return null;
        } else {
            double[][] result = new double[2][n];
            System.arraycopy(xo, 0, result[0], 0, n);
            System.arraycopy(yo, 0, result[1], 0, n);
            return result;
        }
    }    
}
