//(C)2007 Attila Kovacs <attila@submm.caltech.edu>

//package crush.util;

import java.io.*;
import java.text.*;
import java.util.*;


public final class ArrayUtil {

	// A bunch of FFT related stuff here based on Numerical Recipes FFT routines.
	//double[][] getChunks(double[] data, long chunkSize) {}

	public static void print(double[] data) { print(data, System.out, new DecimalFormat("0.000000E0")); }

	public static void print(double[] data, DecimalFormat df) { print(data, System.out, df); }

	public static void print(double[] data, PrintStream out, DecimalFormat df) {
		for(int i=0; i<data.length; i++) out.println(df.format(data[i]));
	}

	public static double median(double[] data) { return median(data, 0, data.length); }

	public static double median(double[] data, int fromIndex, int toIndex) {
		Arrays.sort(data, fromIndex, toIndex);
		int n = toIndex - fromIndex;
		return n % 2 == 0 ? 0.5 * (data[fromIndex + n/2-1] + data[fromIndex + n/2]) : data[fromIndex + (n-1)/2];
	}

	public static float median(float[] data) { return median(data, 0, data.length); }

	public static float median(float[] data, int fromIndex, int toIndex) {
		Arrays.sort(data, fromIndex, toIndex);
		int n = toIndex - fromIndex;
		return n % 2 == 0 ? 0.5F * (data[fromIndex + n/2-1] + data[fromIndex + n/2]) : data[fromIndex + (n-1)/2];
	}

	public static double median(WeightedPoint[] data) { return median(data, 0, data.length); }
	
	public static double median(WeightedPoint[] data, int fromIndex, int toIndex) {
		return smartMedian(data, fromIndex, toIndex, 1.0);		
	}
	
	public static double smartMedian(WeightedPoint[] data, int fromIndex, int toIndex, double maxDependence) {
		if(toIndex - fromIndex == 1) return data[fromIndex].value;

		Arrays.sort(data, fromIndex, toIndex);

		// wt is the sum of all weights
		// wi is the integral sum including the current point.
		double wt = 0.0, wmax = 0.0;
		
		for(int i=fromIndex; i<toIndex; i++) {
			final double w = data[i].weight;
			if(w > wmax) wmax = w;
			if(w > 0.0) wt += w;
		}

		// If a single datum dominates the data, then return the weighted mean...
		if(wmax >= maxDependence * wt) {
			double sum=0.0, sumw=0.0;
			for(int i=fromIndex; i<toIndex; i++) {
				final double w = data[i].weight;
				if(w > 0.0) {
					sum += w * data[i].value;
					sumw += w;
				}
			}
			return sum/sumw;
		}
		
		// If all weights are zero return the arithmetic median...
		// This should never happen, but just in case...
		if(wt == 0.0) {
			int n = toIndex - fromIndex;
			return n % 2 == 0 ? 0.5F * (data[fromIndex + n/2-1].value + data[fromIndex + n/2].value) : data[fromIndex + (n-1)/2].value;
		}


		final double midw = wt / 2.0; 
		int ig = fromIndex; 
		
		WeightedPoint last = WeightedPoint.NaN;
		WeightedPoint point = data[fromIndex];

		double wi = point.weight;
		
		
		while(wi < midw) if(data[++ig].weight > 0.0) {
			last = point;
			point = data[ig];	    
			wi += 0.5 * (last.weight + point.weight);    
		}
		
		double wplus = wi;
		double wminus = wi - 0.5 * (last.weight + point.weight);
		
		double w1 = (wplus - midw) / (wplus + wminus);
		return w1 * last.value + (1.0-w1) * point.value;
	}

	// Interpret integral normalized spectrum as signal amplitudes
	public static double[] amplitude(Complex[] data) {
		double[] amp = new double[data.length];
		for(int i=0; i<data.length; i++) amp[i] = data[i].length();
		amp[0] /= 2.0;
		amp[amp.length - 1] /= 2.0;
		return amp;
	}

	// Interpret integral normalized spectrum as noise amplitudes
	public static double[] noiseAmplitude(Complex[] data) {
		double[] amp = new double[data.length];
		for(int i=0; i<data.length; i++) amp[i] = data[i].length();
		return amp;
	}

	// Interpret integral normalized spectrum as signal power
	public static double[] norm(Complex[] data) {
		double[] norm = new double[data.length];
		for(int i=0; i<data.length; i++) norm[i] = data[i].norm();
		norm[0] /= 4.0;
		norm[norm.length - 1] /= 4.0;	
		return norm;
	}

	// Interpret integral normalized spectrum as noise power
	public static double[] noiseNorm(Complex[] data) {
		double[] norm = new double[data.length];
		for(int i=0; i<data.length; i++) norm[i] = data[i].norm();
		return norm;
	}

	public static double[] real(Complex[] data) {
		double[] real = new double[data.length];
		for(int i=0; i<data.length; i++) real[i] = data[i].x;
		return real;
	}

	public static double[] re(Complex[] data) { return real(data); }

	public static double[] imaginary(Complex[] data) {
		double[] imaginary = new double[data.length];
		for(int i=0; i<data.length; i++) imaginary[i] = data[i].y;
		return imaginary;
	}


	public static double[] im(Complex[] data) { return imaginary(data); }

	public static int getPaddedFFTSize(int size) {
		int n=1;	
		while(n < size) n<<=1;
		return n;
	}

	public static int getTruncatedFFTSize(int size) {
		int n=1;	
		while(n <= size) n<<=1;
		return n>>1;
	}

	public static double[] truncateForFFT(double[] data) {
		return padWithZeros(data, getTruncatedFFTSize(data.length));
	}

	public static Vector2D[] truncateForFFT(Complex[] data) {
		int n = getTruncatedFFTSize(data.length);	
		return data.length == n ? data : padWithZeros(data, n);
	}

	public static double[] padForFFT(double[] data) {
		int n = getPaddedFFTSize(data.length);
		return data.length == n ? data : padWithZeros(data, n);
	}

	public static Complex[] padForFFT(Complex[] data) {
		return padWithZeros(data, getPaddedFFTSize(data.length));
	}

	public static double[] padWithZeros(double[] data, int n) {
		double[] padded = new double[n];

		int N = Math.min(data.length, n);

		System.arraycopy(data, 0, padded, 0, N);

		return padded;
	}

	public static Complex[] padWithZeros(Complex[] data, int n) {
		Complex[] padded = new Complex[n];

		int N = Math.min(data.length, n);

		System.arraycopy(data, 0, padded, 0, N);
		//for(int i=0; i<N; i++) padded[i] = (Complex) data[i].clone();

		return padded;
	}

	public static String[] getWindowFunctionNames() {
		return new String[] { "Rectangular", "Hamming", "Hann", "Blackman", "Nutall", 
				"Blackman-Harris", "Blackman-Nutall", "Flat Top"};
	}

	public static double getEquivalentWindowWidth(String name) {
		if(name.equalsIgnoreCase("Rectangular") || name.equalsIgnoreCase("Box")) return 1.0;
		if(name.equalsIgnoreCase("Hamming")) return 1.37;
		if(name.equalsIgnoreCase("Hann")) return 1.50;
		if(name.equalsIgnoreCase("Blackman")) return 1.73;
		if(name.equalsIgnoreCase("Nutall")) return 2.02;
		if(name.equalsIgnoreCase("Blackman-Harris")) return 2.01;
		if(name.equalsIgnoreCase("Blackman-Nutall")) return 1.98;
		if(name.equalsIgnoreCase("Flat Top")) return 3.77;
		return -1.0;
	}

	public static double[] getWindowFunction(String name, int n) {
		if(name.equalsIgnoreCase("Rectangular") || name.equalsIgnoreCase("Box")) return getRectangularWindow(n);
		if(name.equalsIgnoreCase("Hamming")) return getHammingWindow(n);
		if(name.equalsIgnoreCase("Hann")) return getHannWindow(n);
		if(name.equalsIgnoreCase("Blackman")) return getBlackmanWindow(n);
		if(name.equalsIgnoreCase("Nutall")) return getNutallWindow(n);
		if(name.equalsIgnoreCase("Blackman-Harris")) return getBlackmanHarrisWindow(n);
		if(name.equalsIgnoreCase("Blackman-Nutall")) return getBlackmanNutallWindow(n);
		if(name.equalsIgnoreCase("Flat Top")) return getFlatTopWindow(n);
		return null;
	}

	// W = 1.00
	// -15dB
	public static double[] getRectangularWindow(int n) {
		return getWindowFunction(n, new double[] { 1.0 });
	}

	// W = 1.37
	// -40dB
	public static double[] getHammingWindow(int n) {
		return getWindowFunction(n, new double[] { 0.53836, -0.46164 });
	}

	// W = 1.50
	// -30dB
	public static double[] getHannWindow(int n) {
		return getWindowFunction(n, new double[] { 0.5, -0.5 });
	}

	// W = 1.73
	// -60dB
	public static double[] getBlackmanWindow(int n) {
		return getWindowFunction(n, new double[] { 0.5, -0.5 });
	}

	// W = 2.02
	// -100dB
	public static double[] getNutallWindow(int n) {
		return getWindowFunction(n, new double[] { 0.355768, -0.487396, 0.144232, -0.012604 });
	}

	// W = 2.01
	// -100dB
	public static double[] getBlackmanHarrisWindow(int n) {
		return getWindowFunction(n, new double[] { 0.35875, -0.48829, 0.14128, -0.01168 });
	}

	// W = 1.98
	// -100dB
	public static double[] getBlackmanNutallWindow(int n) {
		return getWindowFunction(n, new double[] { 0.3635819, -0.4891775, 0.1365995, -0.0106411 });
	}

	// W = 3.77
	// -70dB
	public static double[] getFlatTopWindow(int n) {
		return getWindowFunction(n, new double[] { 1.0, -1.93, 1.29, -0.388, 0.032 });
	}

	// normalize to sum w^2
	// Needed for provididng true PSD estimates...
	public static double[] getWindowFunction(int n, double[] coeff) {
		double[] w = new double[n];
		double norm = 0.0;
		for(int i=0; i<n; i++) {
			for(int k=0; k<coeff.length; k++) w[i] += coeff[k] * Math.cos(2.0*k*Math.PI * i/(n-1));
			norm += w[i]*w[i];
		}
		for(int i=0; i<n; i++) w[i] /= norm;

		return w;
	}

	public static double[] convolveViaFFT(double[] data, double[] beam) { 
		Complex[] A = FFT(data);
		int N = A.length-1;

		Complex[] B = FFT(beam, 2*N);

		for(int i=0; i<N+1; i++) {
			Complex AA = A[i], BB = B[i];
			double re = AA.x * BB.x - AA.y * BB.y;
			AA.y = AA.y * BB.x + AA.x * BB.y;
			AA.x = re;
		}

		return backFFT(A);
	}

	public static double[] autoCorrelateViaFFT(double[] data) { return convolveViaFFT(data, data); }

	public static double[] averagePower(double[] data, int windowSize) {
		if(data.length < windowSize) return averagePower(padWithZeros(data, windowSize), windowSize);
		return averagePower(data, getHammingWindow(windowSize));
	}

	public static double[] averagePower(double[] data, double[] w) {
		// figure out the FFT size
		int n=1;	
		int windowSize = w.length;
		while(n < windowSize) n<<=1;
		double[] block = new double[n];

		// Create the cumulated spectrum array
		double[] spectrum = null;

		int start = 0, N = 0;
		while(start + windowSize <= data.length) {

			for(int i=0,j=start; i<windowSize; i++,j++) block[i] = w[i] * data[j];	    

			Complex[] blockSpectrum = FFT(block);

			if(spectrum == null) spectrum = new double[blockSpectrum.length];

			for(int i=0; i<spectrum.length; i++) spectrum[i] += blockSpectrum[i].norm();

			start += windowSize/2;

			N++;
		}

		// Should not used aplitude normalization here but power...
		double norm = windowSize*windowSize / N;

		for(int i=0; i<spectrum.length; i++) spectrum[i] *= norm;

		return spectrum;	
	}

	public static Complex[] overlapFFT(double[] data, int windowSize) {
		if(data.length < windowSize) return overlapFFT(padWithZeros(data, windowSize), windowSize);

		// figure out the FFT size
		int n=1;	
		while(n < windowSize) n<<=1;
		double[] block = new double[n];

		// Create the cumulated spectrum array
		Complex[] spectrum = null;

		int start = 0, N=0;
		while(start + windowSize <= data.length) {	    

			for(int i=0,j=start; i<windowSize; i++,j++) block[i] = data[j];	    

			Complex[] blockSpectrum = FFT(block);

			if(spectrum == null) spectrum = blockSpectrum;
			else for(int i=0; i<spectrum.length; i++) spectrum[i].add(blockSpectrum[i]);

			start += windowSize/2;

			N++;
		}

		double norm = 1.0 / N;

		for(int i=0; i<spectrum.length; i++) spectrum[i].scale(norm);

		return spectrum;	
	}

	public static Complex[] FFT(double[] data) { return FFT(data, getPaddedFFTSize(data.length)); }

	public static double[][] backFFT(Complex[][] spectrum) {
		return null;
	}

	public static Complex[] FFT(double[] data, int n) {
		double[] tdata = padWithZeros(data, n);
		int N = n/2;

		Complex[] fdata = new Complex[N+1];
		for(int i=0; i<N+1; i++) fdata[i] = new Complex();

		uncheckedFFT(tdata, fdata);

		return fdata;
	}

	// This performs the fft on tdata, and puts the
	// apmlitude normalized frequency data into the supplied Complex array.
	// but the tdata is also destroyed in the process...
	public static void uncheckedFFT(double[] data, Complex[] fdata) {
		final int n = data.length;
		final int N = n/2;

		realft(data, n, true);

		fdata[0].x = data[0] / N;
		fdata[N].x = data[1] / N;

		for(int i=1,ii=2; i<N; i++) {
			fdata[i].x = data[ii++] / N;
			fdata[i].y = data[ii++] / N;
		}
	}

	public static void uncheckedBackFFT(Complex[] fdata, double[] data) {
		final int n = data.length;
		final int N = n/2;

		data[0] = fdata[0].x;
		data[1] = fdata[N].x;

		for(int i=1,ii=2; i<N; i++) {
			data[ii++] = fdata[i].x;
			data[ii++] = fdata[i].y;
		}

		realft(data, n, false);
	}



	public static double[] backFFT(Complex[] spectrum) {
		return backFFT(spectrum, getPaddedFFTSize(spectrum.length-1));
	}

	public static double[] backFFT(Complex[] spectrum, int n) {
		double[] tdata = new double[2 * n];
		uncheckedBackFFT(spectrum, tdata);	
		return tdata;
	}

	public static void uncheckedFFT(Complex[] data, boolean isForward, double[] temp) {
		if(temp == null) temp = new double[2*data.length];
		else if(temp.length != 2*data.length) temp = new double[2*data.length];

		for(int i=0, ii=0; i<data.length; i++) {
			temp[ii++] = data[i].x;
			temp[ii++] = data[i].y;
		}

		four1(temp, temp.length>>1, isForward);

		double norm = isForward ? 1.0 / data.length : 1.0;

		for(int i=0, ii=0; i<data.length; i++) {
			data[i].x = norm * temp[ii++];
			data[i].y = norm * temp[ii++];
		}	    
	}

	public static void uncheckedFFT(Complex[][] data, boolean isForward) {
		Complex[] ctemp = null;
		double[] dtemp = null;
		uncheckedFFT(data, isForward, ctemp, dtemp);
	}

	public static void uncheckedFFT(Complex[][] data, boolean isForward, Complex[] temp, double[] dtemp) {
		int nx = data.length;
		int ny = data[0].length;

		if(temp == null) temp = new Complex[nx];
		else if(temp.length != nx) temp = new Complex[nx];

		for(int i=0; i<nx; i++) {
			if(temp[i] == null) temp[i] = new Complex();
			uncheckedFFT(data[i], isForward, dtemp);
		}

		for(int j=0; j<ny; j++) {
			for(int i=0; i<nx; i++) temp[i] = data[i][j];
			uncheckedFFT(temp, isForward, dtemp);
			for(int i=0; i<nx; i++) data[i][j] = temp[i];
		}
	}

	public static Complex[][] loadFFT(double[][] data) {
		Complex[][] cdata = null;
		return loadFFT(data, cdata);
	}

	public static Complex[][] loadFFT(double[][] data, Complex[][] cdata) {
		int nx = data.length;
		int ny = data[0].length;

		int nu = getPaddedFFTSize(nx);
		int nv = getPaddedFFTSize(ny);

		if(cdata == null) cdata = new Complex[nu][nv];
		else if(cdata.length != nu || cdata[0].length != nv) cdata = new Complex[nu][nv];

		for(int i=0; i<nu; i++) for(int j=0; j<nv; j++) {
			if(cdata[i][j] == null) cdata[i][j] = new Complex();
			cdata[i][j].x = (i<nx && j<ny) ? data[i][j] : 0.0;
			cdata[i][j].y = 0.0;
		} 

		return cdata;
	}

	public static void unloadFFT(Complex[][] cdata, double[][] data) {
		int nx = data.length;
		int ny = data[0].length;

		for(int i=0; i<nx; i++) for(int j=0; j<ny; j++) data[i][j] = cdata[i][j].x;
	}

	public static double[][] amplitude(Complex[][] data) {
		int nx = data.length;
		int ny = data[0].length;

		double[][] a = new double[nx][ny];

		for(int i=0; i<nx; i++) for(int j=0; j<ny; j++) a[i][j] = data[i][j].length();

		// Change from integral to amplitide normalization
		for(int i=0; i<nx; i++) {
			a[i][0] /= 2.0;
			a[i][ny-1] /= 2.0;
		}

		for(int j=0; j<ny; j++) {
			a[0][j] /= 2.0;
			a[nx-1][j] /= 2.0;
		}

		return a;
	}

	public static double[][] norm(Complex[][] data) {
		int nx = data.length;
		int ny = data[0].length;

		double[][] a = new double[nx][ny];

		for(int i=0; i<nx; i++) for(int j=0; j<ny; j++) a[i][j] = data[i][j].norm();

		return a;
	}



	public static void realft(final double data[], final int n, final boolean forward) {
		double c1=0.5, c2, theta;

		theta=Math.PI/(double) (n>>1);

		if(forward) {
			c2 = -0.5;
			four1(data,n>>1, true);
		} 
		else {
			c2=0.5;
			theta = -theta;
		}

		double wtemp=Math.sin(0.5*theta);
		final double wpr = -2.0*wtemp*wtemp;
		final double wpi=Math.sin(theta);
		double wr=1.0+wpr;
		double wi=wpi;
		final int np1=n+1;

		double h1r, h1i;

		// ii = i-1
		final int imax = n>>2;
		for(int i=1;i<imax;i++) {
			final int i1 = i + i;
			final int i2 = i1 + 1;
			final int i3 = np1 - i2;
			final int i4 = i3 + 1;

			h1r=c1*(data[i1]+data[i3]);
			h1i=c1*(data[i2]-data[i4]);
			final double h2r = -c2*(data[i2]+data[i4]);
			final double h2i=c2*(data[i1]-data[i3]);
			data[i1]=h1r+wr*h2r-wi*h2i;
			data[i2]=h1i+wr*h2i+wi*h2r;
			data[i3]=h1r-wr*h2r+wi*h2i;
			data[i4]=-h1i+wr*h2i+wi*h2r;
			wr=(wtemp=wr)*wpr-wi*wpi+wr;
			wi=wi*wpr+wtemp*wpi+wi;
		}
		if(forward) {
			data[0] = (h1r=data[0])+data[1];
			data[1] = h1r-data[1];
		} 
		else {
			data[0]=c1*((h1r=data[0])+data[1]);
			data[1]=c1*(h1r-data[1]);
			four1(data,n>>1, false);
		}
	}


	public static void four1(final double data[], final int nn, final boolean forward) {
		double temp;

		final int isign = forward ? 1: -1;

		final int n=nn << 1;
		int j=1;

		for(int i=1;i<n;i+=2) {
			if (j > i) {		
				temp = data[i-1]; data[i-1] = data[j-1]; data[j-1] = temp;
				temp = data[i]; data[i] = data[j]; data[j] = temp;
			}
			int m=n >> 1;
		
			while(m >= 2 && j > m) {
				j -= m;
				m >>= 1;
			}
			j += m;
		}

		int mmax=2;

		while (n > mmax) {
			final int istep=mmax << 1;
			final double theta=isign*(2.0*Math.PI/mmax);
			double wtemp=Math.sin(0.5*theta);
			final double wpr = -2.0*wtemp*wtemp;
			final double wpi=Math.sin(theta);
			double wr=1.0;
			double wi=0.0;
			for (int m=1;m<mmax;m+=2) {
				for (int i=m;i<=n;i+=istep) {
					j=i+mmax;

					final double tempr=wr*data[j-1]-wi*data[j];
					final double tempi=wr*data[j]+wi*data[j-1];
					data[j-1]=data[i-1]-tempr;
					data[j]=data[i]-tempi;
					data[i-1] += tempr;
					data[i] += tempi;
				}
				wr=(wtemp=wr)*wpr-wi*wpi+wr;
				wi=wi*wpr+wtemp*wpi+wi;
			}
			mmax=istep;
			Thread.yield();
		}
	} 
}
