/// Evolver Copyright 2000, Morgan McGuire
/// matrix@alum.mit.edu   http://graphics3d.com/matrix
public class Blur extends OneArgumentOperator implements ELOperator
{
    private double blurPercent = 0.04;

    public Blur()
    {
    }

    /**
     * This constructor is useful when using blur to implement other
     * filters.
     */
    public Blur(double blurPercent)
    {
	this.blurPercent = blurPercent;
    }

    public void apply(ELMatrix argument[], ELMatrix out, ELEnvironment environment)
    {
	int width = out.getWidth();
	int height = out.getHeight();

	if ((width == 128) && (height == 128)) {
	    if (blurPercent == 0.04) {
		fastApply(argument, out, environment);
		return;
	    } else if (blurPercent == 0.02) {
		fastApply2(argument, out, environment);
		return;
	    }
	}
	    
	// Grab the array right away, so that this method doesn't need to
	// be synchronized.
	float coefficients[] = this.coefficients;

	checkArguments(argument);
	ELMatrix arg = argument[0];

	// Compute the number of filter taps to use.  Round up so that
	// small images are blurred slightly *more*, not less.  Otherwise
	// nice features seen in small images don't propagate up.
	int numTaps = (int)Math.ceil(Math.min(out.getWidth(), out.getHeight()) * blurPercent);
    
	if ((numTaps & 1) == 0) {
	    // numTaps is even; it must be made odd
	    numTaps++;
	}

	if (numTaps <= 1) {
	    // There is only one tap; no blur occurs.
	    out.set(arg);
	    return;
	}

	// See if the coefficients array has a good length for us.
	if (coefficients.length != numTaps) {
	    // Resize the coefficients array.
	    coefficients = gaussian(numTaps);

	    // Store these coefficients back so they can be reused.
	    this.coefficients = coefficients;
	}

	// index of the middle filter coefficient
	int L = (numTaps / 2);

	// Perform the actual 2D blur in two 1D passes; one horizontal,
	// one vertical.

	float temp[] = new float[out.numElements];
	/*
	  System.out.println("coefficients = new float[]{");
	  for(int q = 0; q < coefficients.length; q++) {
	  System.out.print("" + coefficients[q] + "f, ");
	  }
	  System.out.println("};");
	*/

	// Loop over all color channels
	for(int c = 0; c < 3; c++) {
	    float outChannel[] = out.channel[c];
	    float inChannel[] = arg.channel[c];

	    // Horizontal pass, bluring inChannel -> temp
	    for(int y = height - 1; y >= 0; y--) {
		for(int x = width - 1; x >= 0; x--) {
		    float sum = coefficients[L] * inChannel[y * width + x];

		    for(int tap = L; tap >= 1; tap--) {
			int yoffset = y * width;
			sum += coefficients[L - tap] * inChannel[yoffset + ((x - tap + width) % width)];
			sum += coefficients[L + tap] * inChannel[yoffset + ((x + tap + width) % width)];
		    }
		    temp[y * width + x] = sum;
		}
	    }

	    // Vertical pass, blurring temp -> outChannel
	    for(int y = height - 1; y >= 0; y--) {
		for(int x = width - 1; x >= 0; x--) {
		    float sum = coefficients[L] * temp[y * width + x];

		    for(int tap = L; tap >= 1; tap--) {
			sum += coefficients[L - tap] * temp[((y - tap + height) % height) * width + x];
			sum += coefficients[L + tap] * temp[((y + tap + height) % height) * width + x];
		    }

		    outChannel[y * width + x] = sum;
		}
	    }

	}
    }


    // Assumes width = height = 128 for performance
    private void fastApply(ELMatrix argument[], ELMatrix out, ELEnvironment environment)
    {
	final int width = out.getWidth();
	final int height = out.getHeight();

	if (!((width == 128) && (height == 128))) {
	    throw new Error("Not available for optimization");
	}

	checkArguments(argument);
	final ELMatrix arg = argument[0];

	final float coefficients[] = new float[]{0.09921788f, 0.1422119f, 0.16772375f,
						 0.18169296f, 0.16772375f, 0.1422119f,
						 0.09921788f};

	// index of the middle filter coefficient
	final int L = 3;

	// Perform the actual 2D blur in two 1D passes; one horizontal,
	// one vertical.
	float temp[] = new float[out.numElements];

	final int widthSubOne = width - 1;
	final int heightSubOne = height - 1;
	final int logWidth = 7;
	final int logHeight = 7;

	// Loop over all color channels
	for(int c = 0; c < 3; c++) {
	    float outChannel[] = out.channel[c];
	    float inChannel[] = arg.channel[c];

	    // Horizontal pass, bluring inChannel -> temp
	    for(int y = height - 1; y >= 0; y--) {
		int yTimesWidth = y * width;
		int yoffset = yTimesWidth;
		for(int x = width - 1; x >= 0; x--) {
		    float sum = coefficients[L] * inChannel[yTimesWidth + x];

		    int xPlusWidth = x + width;
		    sum += coefficients[L - 3] * inChannel[yoffset + ((xPlusWidth - 3) & widthSubOne)];
		    sum += coefficients[L + 3] * inChannel[yoffset + ((xPlusWidth + 3) & widthSubOne)];
          
		    sum += coefficients[L - 2] * inChannel[yoffset + ((xPlusWidth - 2) & widthSubOne)];
		    sum += coefficients[L + 2] * inChannel[yoffset + ((xPlusWidth + 2) & widthSubOne)];
          
		    sum += coefficients[L - 1] * inChannel[yoffset + ((xPlusWidth - 1) & widthSubOne)];
		    sum += coefficients[L + 1] * inChannel[yoffset + ((xPlusWidth + 1) & widthSubOne)];

		    temp[y * width + x] = sum;
		}
	    }

	    // Vertical pass, blurring temp -> outChannel
	    for(int y = height - 1; y >= 0; y--) {
		int yTimesWidth = y * width;
		int yPlusHeight = y + height;
		for(int x = width - 1; x >= 0; x--) {
		    int offset = yTimesWidth + x;
		    float sum = coefficients[L] * temp[offset];

		    sum += coefficients[L - 3] * temp[(((yPlusHeight - 3) & heightSubOne) << logWidth) + x];
		    sum += coefficients[L + 3] * temp[(((yPlusHeight + 3) & heightSubOne) << logWidth) + x];
          
		    sum += coefficients[L - 2] * temp[(((yPlusHeight - 2) & heightSubOne) << logWidth) + x];
		    sum += coefficients[L + 2] * temp[(((yPlusHeight + 2) & heightSubOne) << logWidth) + x];
          
		    sum += coefficients[L - 1] * temp[(((yPlusHeight - 1) & heightSubOne) << logWidth) + x];
		    sum += coefficients[L + 1] * temp[(((yPlusHeight + 1) & heightSubOne) << logWidth) + x];

		    outChannel[offset] = sum;
		}
	    }

	}
    }

    // Assumes width = height = 128 for performance, blurPercentage = 0.02
    private void fastApply2(ELMatrix argument[], ELMatrix out, ELEnvironment environment)
    {
	final int width = out.getWidth();
	final int height = out.getHeight();

	if (!((width == 128) && (height == 128))) {
	    throw new Error("Not available for optimization");
	}

	checkArguments(argument);
	final ELMatrix arg = argument[0];

	float coefficients[] = new float[]{0.26101127f, 0.47797745f, 0.26101127f};

	// index of the middle filter coefficient
	final int L = 1;

	// Perform the actual 2D blur in two 1D passes; one horizontal,
	// one vertical.
	float temp[] = new float[out.numElements];

	final int widthSubOne = width - 1;
	final int heightSubOne = height - 1;
	final int logWidth = 7;
	final int logHeight = 7;

	// Loop over all color channels
	for(int c = 0; c < 3; c++) {
	    float outChannel[] = out.channel[c];
	    float inChannel[] = arg.channel[c];

	    // Horizontal pass, bluring inChannel -> temp
	    for(int y = height - 1; y >= 0; y--) {
		int yTimesWidth = y * width;
		int yoffset = yTimesWidth;
		for(int x = width - 1; x >= 0; x--) {
		    float sum = coefficients[L] * inChannel[yTimesWidth + x];

		    int xPlusWidth = x + width;
		    sum += coefficients[L - 1] * inChannel[yoffset + ((xPlusWidth - 1) & widthSubOne)];
		    sum += coefficients[L + 1] * inChannel[yoffset + ((xPlusWidth + 1) & widthSubOne)];

		    temp[y * width + x] = sum;
		}
	    }

	    // Vertical pass, blurring temp -> outChannel
	    for(int y = height - 1; y >= 0; y--) {
		int yTimesWidth = y * width;
		int yPlusHeight = y + height;
		for(int x = width - 1; x >= 0; x--) {
		    int offset = yTimesWidth + x;
		    float sum = coefficients[L] * temp[offset];
		    sum += coefficients[L - 1] * temp[(((yPlusHeight - 1) & heightSubOne) << logWidth) + x];
		    sum += coefficients[L + 1] * temp[(((yPlusHeight + 1) & heightSubOne) << logWidth) + x];

		    outChannel[offset] = sum;
		}
	    }

	}
    }

    /**
     * Filter coefficient cache.  This is the previous set of
     * coefficients used.  If this has the wrong number of coefficients,
     * it will be regenerated.
     */
    private float coefficients[] = new float[0];
  
    static private final float manyCoefficients[] = computeGaussian(10.0);

    /**
     * Produces a set of numTaps gaussian filter coefficients.
     */
    static float[] gaussian(int numTaps)
    {
	int L = (numTaps / 2);
	float coefficients[] = new float[numTaps];

	if ((numTaps & 1) == 0) {
	    throw new Error("Can't have an even number of taps.  numTaps = " + numTaps);
	}

	// Rescale from the large coefficient set to our set.
	double scale = (double)(manyCoefficients.length - 1) / L;
    
	for(int i = 0; i < L + 1; i++) {
	    int m = (int)Math.round(i * scale);
	    coefficients[L + i] = manyCoefficients[m];
	    coefficients[L - i] = coefficients[L + i];
	}

	// Normalize
	double sum = 0.0;
	for(int c = 0; c < coefficients.length; c++) {
	    sum += coefficients[c];
	}
    
	for(int c = 0; c < coefficients.length; c++) {
	    coefficients[c] /= sum;
	}

	return coefficients;
    }
  
    /**
     * Produces the right half of a normalized gaussian (i.e. the high
     * point on down), where the "deviation" is dev.
     */
    static private float[] computeGaussian(double dev)
    {
	int maxWeights = 12;
	double weight[] = new double[maxWeights];
	int numWeights = 0;
	double minWeight = 0.01;
    
	// Compute the weighting coefficients for one side of the
	// gaussian, starting at the middle and working towards the ends.
	// If these drop below our minimum threshold or we see more than
	// 100 of them, stop.
	for(numWeights = 0; (numWeights < maxWeights); numWeights++) {

	    double x = (numWeights / dev);
	    double y = Math.exp(-x * x / 2.0);
      
	    if (y > minWeight) {
		// This value is over the minimum threshold; include it in the set.
		weight[numWeights] = y;
	    } else {
		break;
	    }
	}

	normalize(weight, numWeights);

	// Find the point where we first cross the threshold
	// normalization.
	while ((numWeights > 0) &&
	       (weight[numWeights - 1] < minWeight)) {
	    numWeights--;
	}

	normalize(weight, numWeights);
 
	// Copy into an array of floats
	float result[] = new float[numWeights];
	for (int w = 0; w < numWeights; w++) {
	    result[w] = (float)weight[w];
	}
    
	return result;
    }

    /**
     * Normalizes a one-sided set of gaussian coefficients.
     */
    static private void normalize(double weight[], int numWeights)
    {
	double sum = 0.0;
	for(int w = 0; w < numWeights; w++) {
	    sum += weight[w];
	}
    
	// Double the sum, since the actual gaussian is 2 sided.  Element
	// zero counts exactly once, so subtract it off once we've doubled
	// the sum.
	sum = sum * 2.0 - weight[0];
	for(int w = 0; w < numWeights; w++) {
	    weight[w] /= sum;
	}
    }

    public String name()
    {
	return "Blur";
    }

    public String description()
    {
	return "Performs a gaussian blur on channels";
    }  

    protected String iconString()
    {
	return "Blr";
    }  
}
