package skyview.survey;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import nom.tam.fits.BasicHDU;
import nom.tam.fits.Fits;
import nom.tam.fits.Header;
import nom.tam.fits.TableHDU;

/**
 * Read a MOC and support tests to see if pixels/tiles are available.
 * @author tmcglynn
 */
public class MocReader {
    
    /** The MOC file name */
    private String file;
    
    /** The MOC data */
    long[] longData;
    
    /** The length of the longData array, i.e., the number of MOC entries */
    int   dim;
    
    /** The maximum order for the moc */
    int   maxOrder;
    
    /** For each order in the MOC a descriptor for where the data is found.
     *  This is stored in increasing -- though not necessary contiguous -- order.
     */
    List<OrderDesc> data;
    
    
    /** The structure using in the data List */    
    class OrderDesc {
        int order;   // The order being described.
        int start;   // The offset from the beginning of the longData array.
        int len;     // The number of entries for this order
        OrderDesc(int order, int start, int len) {
            this.order = order;
            this.start = start;
            this.len   = len;
        }
    }
    
    public static void main(String[] args) throws Exception {
        String file;
        if (args.length > 0) {
            file = args[0];
        } else {
            file = "/Home/home1/tam/WHITE.moc";
        }
        long start0 = System.nanoTime();
        MocReader mr = new MocReader(file);
        long start1 = System.nanoTime();
        long start2 = System.nanoTime();
        mr.show();
        long start3 = System.nanoTime();
        for (int i=1; i<args.length; i += 2) {
            System.out.println("Testing order "+args[i]+" pixel:"+args[i+1]);
            boolean result = mr.test(Integer.parseInt(args[i]), Long.parseLong(args[i+1]));
            System.out.println("    Result: "+result);
        }
    }
    
    /** Describe basic characteristics of the MOC */
    public void show() {
        long total = 0;
        System.out.println("\nMOC Analysis.");
        System.out.println("   Maximum order:           "+maxOrder);
        System.out.println("   Total number of entries: "+dim);
        System.out.println("   Breakdown by Order:");
        System.out.printf("      %8s: %12s %12s\n", "Order","Offset","Count");
                
        for (OrderDesc d: data) {
            System.out.printf("      %8d: %12d %12d\n", d.order, d.start, d.len);
        }
        System.out.println();
        
        for (OrderDesc d: data) {
            System.out.println("\n MOC for order "+d.order);
            for (int i=0; i<d.len; i += 1) {
                System.out.printf("%10d", longData[i+d.start]);
                if ((i+1) % 6 == 0) {
                    System.out.println();
                }
            }
        }
    }
    
    /** Create a MocReader for a given file */
    public MocReader(String file) throws Exception {
        this.file = file;
        
        Fits f = new Fits(file);
        BasicHDU bhdu = f.getHDU(1);  // MOC is in first extension
        Header hdr = bhdu.getHeader();
        
        // Do a few checks 
        if (!hdr.getStringValue("PIXTYPE").equals("HEALPIX")) {
            throw new Exception("Invalid PIXTYPE");
        }
        if (!hdr.getStringValue("ORDERING").equals("NUNIQ")) {
            throw new Exception("Not NUNIQ ordering");
        }
        if (!hdr.getStringValue("XTENSION").equals("BINTABLE")) {
            throw new Exception("Extension is not binary table");
        }
        
        // MOC can be either int's or long's.  If the former we will convert.
        String format = hdr.getStringValue("TFORM1");
        boolean usingLong;
        if (format.equals("J") || format.equals("1J")) {
            usingLong = false;
        } else if (format.equals("K") || format.equals("1K")) {
            usingLong = true;
        } else {
            throw new Exception("Unable to interpret data format for column:"+format);        
        }
        
        maxOrder = hdr.getIntValue("MOCORDER", -1);
        if (maxOrder < 0 || maxOrder > 30) {
            throw new Exception("Invalid or missing order:"+maxOrder);
        }
        
        TableHDU tab = (TableHDU) bhdu;
        if (usingLong) {
            longData = (long[])tab.getColumn(0);
            dim = longData.length;
        } else {
            int[] intData;
            intData  = (int[])tab.getColumn(0);
            dim = intData.length;
            longData = new long[dim];
            for (int i=0; i<dim; i += 1) {
                longData[i] = intData[i];
            }
        }
        f.close();
        data = parse();        
    }
    
    /** Analyze the MOC to find out where the data
     *  for each order is found.  This populates the data List.
     */
    public final List<OrderDesc> parse() {
        
        if (dim == 0) {
            return null;
        }
        
        List<OrderDesc> orders    = new ArrayList<OrderDesc>();

        
        int  lastOrder = -1;
        int  order  = 0;
        long maxKey = 16; // The first key of the next order. 
        long minKey = 4;  // The first key of the current order.
        
        long lastKey    = -1;
        int orderStart  =  0;
        
        boolean newOrder = true;
        
        for (int i=0; i<dim; i += 1) {
            
            long key = longData[i];            
            
            while (key >= maxKey) {
                if (lastKey >= 0) {
                    // The length is the current index - start index since we
                    // are 1 beyond the end of the current order.
                    int orderLen = (i-orderStart);
                    orders.add(new OrderDesc(order, orderStart, orderLen));                    
                    lastKey = -1;
                }
                
                // We've got to jump at least one order.
                order  += 1;
                minKey  = maxKey;
                maxKey *= 4;
                newOrder = true;
                orderStart = i;
            }
            
            // Get the actual index of the pixel for the current order.
            longData[i] -= minKey;
            lastKey = longData[i];            
        }
        // Recall the dim is the value i would have if it were still in scope.
        int orderLen = dim - orderStart;
        orders.add(new OrderDesc(order, orderStart, orderLen));
        return orders;
    }
    
    /** See if there is any overlap between the requested pixel
     *  and the data as indicated in the MOC.
     * @param order  The order of the requested pixel.
     * @param pixel  The NESTED index of the requested pixel.
     * @return whether there is any coverage in the region of the requested pixel.
     */
    public boolean test(int order, long pixel) {
        
        // Loop over the orders in increasing order.
        // If the current order is less than or equal to the pixel order, then
        // we just need to check the single pixel that covers the pixel we want,
        // otherwise we need to check if any of a range of pixels is included in
        // the MOC for the current order.
        for (OrderDesc d: data) {
             if (d.order <= order) {
                 // See if this pixel is included in the MOC directly
                 // or is part of a larger pixel that is included.
                 // Adjust the pixel number to be tested.
                 
                 long tpix = pixel;
                 for (int i=d.order; i<order; i += 1) {
                     tpix /= 4;
                 }
                 if (checkForPixel(d, tpix)) {
                     return true;
                 }
                 
             } else {
                 // See if any piece of this pixel is included
                 // in a high order MOC
                 long tpix = pixel;
                 int range = 1;
                 // Find the first pixel index to be looked at and
                 // the number of pixels in the range.
                 for (int i=order; i<d.order; i += 1) {
                     tpix  *= 4;
                     range *= 4;
                 }
                 if (checkForRange(d, tpix, range)) {
                     return true;
                 }
             }
        }
        // Not found at any level!
        return false;        
    }
    
    /** Check if this pixel is included in the MOC explicitly.
     * 
     * @param d     The descriptor for this order of the MOC.
     * @param pix   The pixel desired.
     * @return      True if the pixel is found.
     */
    boolean checkForPixel(OrderDesc d, long pix) {
        int i = Arrays.binarySearch(longData, d.start, d.start+d.len, pix);
        return i >= 0;  // Binary search returns negative values if not found.
    }
    
    /** Check that at least one of a range of pixels is included in the MOC.
     * 
     * @param d     The Order descriptor for the particular order within the MOC
     * @param pix   The start of the pixel range we are interested in.
     * @param range The length of the pixel range we are interested in.
     * @return True if any pixels in this range are in the MOC (for this order).
     */
    boolean checkForRange(OrderDesc d, long pix, int range) {
        int i = Arrays.binarySearch(longData, d.start, d.start+d.len, pix);
        if (i >= 0) {
            // First pixel in the range is found.
            return true;
        }
        
        // Doesn't have the first point in the range, but we can look to see
        // if the next element would be.
        int insertionPoint = -i - 1;  // This should be the insertion point, i.e.,
                                      // he first element of the array larger than pix.
        if (insertionPoint >= d.start+d.len) {
            return false;  // After the end of the array
        }
        // See if the pixel at the insertion point is in the
        // range.  If not then no pixel in the range is included
        // in this order of the MOC since it is the smallest
        // value larger than the requested pixel.
        return longData[insertionPoint] < (pix + range);
    }
    

}
