/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package skyview.data;

import java.util.Arrays;
import nom.tam.fits.Header;
import nom.tam.fits.HeaderCardException;
import skyview.executive.Settings;
import skyview.geometry.DepthSampler;
import skyview.geometry.Sampler;
import skyview.process.Processor;
import skyview.survey.Image;

/**
 * Do a median smoothing for all pixels that differ from the median by
 * more then the threshold
 * Settings used:
 *     MedianSmoother=size[,threshold]
 *        Size gives the size of the box in which the median is computed
 *          This must be an odd integer >= 3
 *        Threshold is the change beyond which the pixel will be smoothed
 *          Assumed to be 0 if omitted which means that all pixels are smoothed.
 *     RatioThreshold
 *        If specified then the threshold will be computed as a ratio of the
 *        current and median values. E.g., if specified and threshold is 2 then
 *        Pixels <0.5 or >2.0 times the median value will be smoothed.
 *     SingleSided=(+|-)
 *        If specified should have the single character values + or - which
 *        indicates that only only values greater than or less than the median
 *        will be considered for smoothing.
 *
 * @author tmcglynn
 */
public class ThresholdMedianSmoother implements Processor {
    
    private boolean ratio;
    private boolean topOnly;
    private boolean bottomOnly;
    private int     size;
    private double  threshold = 0;
    private double  invThresh;
    private int     updates;
    private int     nx, ny, nz;
    private int     b;
    private int     half;
    private int     count;
    
    /** Parse the settings and set things up to do the processing */
    public ThresholdMedianSmoother() {
        
        ratio = Settings.has("RatioThreshold");
        String[] thresStr = Settings.getArray("MedianSmoother");
        size = Integer.parseInt(thresStr[0]);
        if (thresStr.length > 1) {
            threshold = Double.parseDouble(thresStr[1]);
        }
        String sided = Settings.get("SingleSided");
        topOnly    = "+".equals(sided);
        bottomOnly = "-".equals(sided);
        if (size < 3) {
            size = 3;            
        } else if (size%2 == 0) {
            size += 1;
        }
        b = size/2;
        if (threshold < 0) {
            threshold = 0;
        }
        
        if (ratio) {
            if (threshold == 0) {
                ratio = false;
            } else {
                if (threshold < 1) {
                    threshold = 1/threshold;
                }
            
                invThresh = 1/threshold;
            }
        } else {
            invThresh = -threshold;
        }
        
        half = (size*size)/2;
        
        // Check if we are doing one-sided thresholding.
        // If so adjust the boundaries to include
        // everything on one side.
        
        if (topOnly) {  // No filtering below the median.
            if (ratio) {
                invThresh = 0;  
            } else {
                invThresh = -1.e20;
            }
        } else if (bottomOnly) { // No filtering above the median.
            threshold = 1.e20;  // Works as ratio or linear limit.
        }
        
        
    }
    
    
    /** Do the median smoothing -- possibly with thresholding.
     * 
     * @param inputs   Input images (not used)
     * @param output   Output image
     * @param source   Mapping from input to output (not used)
     * @param samp     Sampler (not used)
     * @param dsamp    Depth sampler (not used)
     */

    public void process(Image[] inputs, Image output, int[] source, Sampler samp, DepthSampler dsamp) {
        double[] data = output.getDataArray();
        double[] copy = data.clone();
        
        nx = output.getWidth();
        ny = output.getHeight();
        nz = output.getDepth();
        
        if (nx < size || ny < size) {
            throw new IllegalArgumentException("Image too small for filter");
        }
        
        double[] arr = new double[size*size];
     
        int plane = 0;
        
        // In the following xc,yc,zc are the coordinates
        //    of the pixel we are smoothing.
        // x and y are the corners of the box for which
        //   we are computing the median.
        // bx and by are the coordinates within the current
        //   median box
        
        System.out.println("Sizes:"+nx+" "+ny+" "+nz);
        for (int zc=0; zc<nz; zc += 1) {
            plane = nx*ny*zc;
            for (int yc=0; yc<ny; yc += 1) {
                int y = yc - b;
                if (y < 0) y=0;
                if (yc >= ny-size) {
                    y = ny-size;
                }
                for (int xc=0; xc<nx; xc += 1) {
                    int x = xc-b;
                    if (x < 0) {
                        x=0;
                    }
                    if (xc >= nx-size) {
                        x=nx-size;
                    }
                    double value = data[plane+yc*nx + xc];
                    int n = 0;
                    double sum = 0;
                    for (int by=0; by<size; by += 1) {
                        int offset = plane + (y+by)*nx + x;
                        for (int bx=0; bx<size; bx += 1) {
                            sum += data[offset+bx];
                            arr[n] = data[offset+bx];
                            n += 1;
                            
                        }
                    }
                    actOn(xc, yc, zc, arr, value, copy);
                }
                
            }
        }
        // Copy back the smoothed data.
        System.arraycopy(copy, 0, data, 0, nx*ny*nz);
        
    }
    /** Do any requested smoothing action.
     * 
     * @param xc,yc,zc  The coordinates of the pixel
     * @param array     The array of values around the data
     * @param value     The input value of the pixel
     * @param copy      The copy of the input array
     */
    void actOn(int xc, int yc, int zc, double[] array, double value, double[] copy) {
        int offset = nx*ny*zc + nx*yc + xc;
        
        Arrays.sort(array);
        double median = array[half];
        if (!ratio) {
            if (median+invThresh > value || median + threshold < value ) {
                count += 1;
                copy[offset] = median;
            }            
        } else {
            if (median*invThresh > value || median*threshold < value) {
                count += 1;
                copy[offset] = median;
            }
        }
    }
    
    /** Add history records to the FITS header */
                
    public void updateHeader(Header header) {
        try {
            header.insertHistory("Smoothed with "+this.getClass().getName());
            header.insertHistory("   Number of pixels smoothed:"+count);
            if (threshold != 0 || invThresh != 0) {
                if (ratio) {
                    header.insertHistory("   Ratio thresholding");                    
                    header.insertHistory("   No filtering for ratios v/median:"+invThresh+" to "+threshold);
                }
                header.insertHistory("   No filtering for deltas v-median:"+invThresh+" to "+threshold);
            }
        } catch (HeaderCardException e) {
            System.err.println("Error writing to header");
        }
    }
    
    /** Indicate what processing is to be done. */

    public String getDescription() {
        if (threshold == 0) {
            return "Median filter with size:"+size;
        } else {
            String name =  "Threshold Median filter: Size="+size+" Threshold="+threshold;
            if (ratio) {
                name += " Ratio";
            }
            if (topOnly || bottomOnly) {
                name += " Sided:"+ (topOnly?'+':'-');
            }
            return name;
        }
    }

    public String getName() {
        return "Threshold Median Filter";
    }
    
}
