package com.elphel.imagej.cuas.rt;

import java.awt.Rectangle;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;

import com.elphel.imagej.common.ShowDoubleFloatArrays;
import com.elphel.imagej.cuas.rt.TemporalKernelGenerator.ConsolidationKernel;
import com.elphel.imagej.tileprocessor.ImageDtt;
import com.elphel.imagej.tileprocessor.TileNeibs;

import ij.ImagePlus;

public class CuasRTUtils {
	public static double [] IMP_MINMAX= {-5,5}; //default range for
	final double         psf_radius;
	final double         n_sigma; // for LoG kernel size
	final double []      kernel2d;
	final private int    kernel2d_rad;
	// contains pixel indices, approximately in reverse bit order for multithreading efficiency
	// for the center area only, does not need to care about borders 
	final private int [] indx_center_2d;
	// remaining near-border pixel indices
	final private int [] indx_margins_2d;
	// all pixels indices
	final private int [] indx_all_2d;
	// same for 3x3 velocities
	final private int    kernel3d3_rad;
	final private int [] indx_all_3d3;
	final private int [] indx_center_3d3;
	final private int [] indx_margins_3d3;

	final private int    pixel_decimate;
	final private int    velocity_decimate;
	final private int    velocity_radius;
	
	final private double [] temporal_weights;
	final private double [][][][][] kernel5d;
	final private int    kernel5d_rad;
	final private int [] indx_all_5d;
	final private int [] indx_center_5d;
	final private int [] indx_margins_5d;
	
	
	final ConsolidationKernel consolidationKernel;
	
	// balance kernel - center = +1, sum of other weights = -1.
	final private int    width;
	final private int    height;
//	final private int    tl_offset;
	public CuasRTUtils (
			double   psf_radius,
			double   n_sigma,
			int      kernel3d3_rad,
			double[] temporal_weights,
            int      pixel_decimate,
            int      velocity_decimate,
            int      velocity_radius,
			int      width,
			int      height) {
		this.psf_radius = psf_radius;
		this.width =  width;
		this.height = height;
		this.n_sigma = n_sigma; 
		// Setup 2D pre-filter kernel, based on LoG
		kernel2d_rad = getRadiusLoGKernel(psf_radius,n_sigma);
		kernel2d = getLoGKenel(psf_radius, kernel2d_rad);
		int [][] indices = getIndices(
				kernel2d_rad,
				width,
				height);
		indx_all_2d =      indices[0];
		indx_center_2d =   indices[1];
		indx_margins_2d =  indices[2];
		
		// Setup 3D kernel to generate coarse velocities and input to the next pyramid level (center output)
		this.kernel3d3_rad = kernel3d3_rad;
		indices = getIndices(
				kernel3d3_rad,
				width,
				height);
		indx_all_3d3 =     indices[0];
		indx_center_3d3 =  indices[1];
		indx_margins_3d3 = indices[2];
		
		// Setup 5D kernel for generating fine velocities at increased spatial resolution
		this.temporal_weights = temporal_weights.clone();
		normalize1d (this.temporal_weights);
		this.pixel_decimate =    pixel_decimate;
		this.velocity_decimate = velocity_decimate;
		this.velocity_radius =   velocity_radius;

		consolidationKernel = TemporalKernelGenerator.generateKernel(
				temporal_weights,  // double[] temporal_weights,
				pixel_decimate,    // int pixel_decimate,
				velocity_decimate, // int velocity_decimate,
				velocity_radius);  // int velocity_radius)
		kernel5d =     consolidationKernel.getKernel();
		kernel5d_rad = consolidationKernel.getSpatialRadius();
		indices = getIndices(
				kernel5d_rad,
				width,
				height);
		indx_all_5d =     indices[0];
		indx_center_5d =  indices[1];
		indx_margins_5d = indices[2];
	}
	
	public double getPsfRadius() {
		return psf_radius;
	}
	
	public int getKernel2dRadius() {
		return kernel2d_rad;
	}
	
	public int get3d3Radius() {
		return kernel3d3_rad;
	}

	public int getNumHist() {
		return kernel5d.length;
	}

	public double [] getKernel2d() {
		return kernel2d;
	}
	
	private void normalize1d(double [] data) {
		double s = 0;
		for (double d : data) s+= d;
		for (int i = 0; i < data.length; i++) {
			data[i] /= s;
		}
	}
			
	
	private static int [][] getIndices(
			int kernel_rad,
			int width,
			int height) {
		int kernel_size = 2 * kernel_rad+1;
		int [] i_all =     new int [width*height];
		int cent_width = width-kernel_size+1;
		int cent_height = height-kernel_size+1;
		int cent_size = cent_width*cent_height;
		int [] i_center =  new int [cent_size];
		int [] i_margins = new int [i_all.length - i_center.length];
		int n;
		for (n = 0; (1 << n) < i_all.length; n++);
		int shft = 32-n;
		int n_all = 1 << n;
//		int mask = n_all-1; 
		int iall = 0, icent=0, imarg=0;
		for (int i = 0; i < n_all; i++) {
			int ri = Integer.reverse(i)>>>shft;
			if (ri < i_all.length) {
				int y = ri/width;
				int x = ri%width;
				i_all[iall++] = ri;
				if ((x < kernel_rad) || (y < kernel_rad) || (x >= (width-kernel_rad)) ||  (y >= (height-kernel_rad))) {
					i_margins[imarg++] = ri;
				} else {
					i_center[icent++] = ri;
				}
			}
		}
		int [][] indices = {i_all, i_center, i_margins};
		return indices;
	}
	
	
	public void checkData(double [] data) {
		if (data.length != (width * height)) {
			throw new IllegalArgumentException("Data size = "+data.length+" does not match dimensions "+width+"*"+height+"="+(width*height));
		}
	}

	public void checkData(double [][] data) {
		if (data.length != (width * height)) {
			throw new IllegalArgumentException("Data size = "+data.length+" does not match dimensions "+width+"*"+height+"="+(width*height));
		}
	}
	
	public void checkKernel(double [] kernel, int rad) {
		int kernel_size = 2 * rad + 1;
		if (kernel.length != (kernel_size * kernel_size)) {
			throw new IllegalArgumentException("Kernel length = "+kernel.length+" does not match kernel size (2*"+kernel2d_rad+"+1) * (2*"+kernel2d_rad+"+1) ="+(kernel_size * kernel_size));
		}
	}
	
	public double [][][] convolve3d(
			final double [][][] data,         // outer index - historic layers, [0] - latest, second index - pixel, third - direction ([9])
			double [][][]     result_in){
		final int vel_size = 2*velocity_radius + 1;
		final int nlayers = data.length;
		final double [][][] result = (result_in != null) ? result_in : new double [width*height][pixel_decimate*pixel_decimate][vel_size * vel_size];
		final double [][][][][] kernel = consolidationKernel.getKernel();
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		final int [] coarse_vel_indx = consolidationKernel.getCoarseVIdx();
		final int decimate_size = pixel_decimate*pixel_decimate;
		for (int nhist = 0; nhist < nlayers; nhist++) {
			final int fhist = nhist;
			final int spat_rad = consolidationKernel.getSpatialRadius(); // may be later dependent on nhist for efficiency 
			final double [][][][] kernel_hist = kernel[fhist];
			final double [][]     data_hist = data[fhist];
			ai.set(0);
			// processing center area, no need to care about margins
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int nPix = ai.getAndIncrement(); nPix < indx_center_5d.length; nPix = ai.getAndIncrement()) {
							int ipix_dst = indx_center_5d[nPix];
							int target_x = ipix_dst % width; 
							int target_y = ipix_dst / width;
							for (int sub_idx = 0; sub_idx < decimate_size; sub_idx++) {
								for (int v_out_idx = 0; v_out_idx < kernel_hist.length; v_out_idx++) {
									int src_coarse_v = coarse_vel_indx[v_out_idx];
									if (src_coarse_v >=0) {
										double accumulated_flux = 0.0;
										// Outer spatial neighborhood limits defined by the pre-computed kernel
										for (int dy = -spat_rad; dy <= spat_rad; dy++) {
											int src_y = target_y + dy;
											for (int dx = -spat_rad; dx <= spat_rad; dx++) {
												int src_x = target_x + dx;
												// 1. Boundary check for source image coordinates - not here
												// 2. Locate the source macro-pixel index in your history array
												int src_pixel_idx = src_y * width + src_x;
												// 3. Pull historical energy from ONLY the mapped coarse velocity channel
												double historical_value = data_hist[src_pixel_idx][src_coarse_v];
												// 4. Multiply by the exact pre-calculated geometric weight for this sub-pixel layout
												int k_dy = dy + spat_rad;
												int k_dx = dx + spat_rad;
												double weight = kernel_hist[v_out_idx][sub_idx][k_dy][k_dx];
												accumulated_flux += historical_value * weight;
											}
										}
										result[ipix_dst][sub_idx][v_out_idx] += accumulated_flux;
									}
								}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
			
			ai.set(0);
			// processing center area, no need to care about margins
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						for (int nPix = ai.getAndIncrement(); nPix < indx_margins_5d.length; nPix = ai.getAndIncrement()) {
							int ipix_dst = indx_margins_5d[nPix];
							int target_x = ipix_dst % width; 
							int target_y = ipix_dst / width;
							for (int sub_idx = 0; sub_idx < decimate_size; sub_idx++) {
								for (int v_out_idx = 0; v_out_idx < kernel_hist.length; v_out_idx++) {
									int src_coarse_v = coarse_vel_indx[v_out_idx];
									if (src_coarse_v >=0) {
										double accumulated_flux = 0.0;
										// Outer spatial neighborhood limits defined by the pre-computed kernel
										for (int dy = -spat_rad; dy <= spat_rad; dy++) {
											int src_y = target_y + dy;
											for (int dx = -spat_rad; dx <= spat_rad; dx++) {
												int src_x = target_x + dx;
												// 1. Boundary check for source image coordinates
												if (src_x >= 0 && src_x < width && src_y >= 0 && src_y < height) {
													// 2. Locate the source macro-pixel index in your history array
													int src_pixel_idx = src_y * width + src_x;
													// 3. Pull historical energy from ONLY the mapped coarse velocity channel
													double historical_value = data_hist[src_pixel_idx][src_coarse_v];
													// 4. Multiply by the exact pre-calculated geometric weight for this sub-pixel layout
													int k_dy = dy + spat_rad;
													int k_dx = dx + spat_rad;
													double weight = kernel_hist[v_out_idx][sub_idx][k_dy][k_dx];
													accumulated_flux += historical_value * weight;
												}
											}
										}
										result[ipix_dst][sub_idx][v_out_idx] += accumulated_flux;
									}
								}
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);			
			
		}
		return result;
	}
	public double[][] convolve3D3LReLU(
			final double [] data,
			final double [] data_prev,
			double [][]     result_in,
			final int       mode_3d3,
			final double    curt_3d3_frac,  //  = 0.5; // for stronger signals give this fraction of the total (of 9) weight to the 1-of-9 winner
			final double    curt_3d3_thrsh, //  = 1.0; // scale down curt_3d3_frac for weaker signals (in ratio of min(data[now]/curt_3d3_thrsh,1.0)
			final double    curt_3d3_power, //  = 2.0; // in mode_3d3=2, redistribute coarse velocities weights as the specified power of their positive values (ignore negative ones)
			final double    w_now, // normally 0.5, so center will propagate to the next pyramid levels
			final double    w_prev,// normally 0.5, so center will propagate to the next pyramid levels
			final double    alpha,
			final int       debugLevel) {
		switch (mode_3d3) {
		case CuasDetectRT.CONV3D3_MODE_PLAIN:
			return convolve3D3LReLU(
				data,         // final double [] data,
				data_prev,    // final double [] data_prev,
				result_in,   // double [][]     result_in,
				w_now,       // final double    w_now, // normally 0.5, so center will propagate to the next pyramid levels
				w_prev,      // final double    w_prev,// normally 0.5, so center will propagate to the next pyramid levels
				alpha,       // final double    alpha,
				debugLevel); // final int       debugLevel) 
		case CuasDetectRT.CONV3D3_MODE_WINNER:
			return convolve3D3LReLU(
					data,           // final double [] data,
					data_prev,      // final double [] data_prev,
					result_in,      // double [][]     result_in,
					curt_3d3_frac,  // final double    curt_3d3_frac, //  =        0.5;   // for stronger signals give this fraction of the total (of 9) weight to the 1-of-9 winner
					curt_3d3_thrsh, //final double    curt_3d3_thrsh, //  =       1.0;   // scale down curt_3d3_frac for weaker signals (in ratio of min(data[now]/curt_3d3_thrsh,1.0)
					w_now,          // final double    w_now, // normally 0.5, so center will propagate to the next pyramid levels
					w_prev,         // final double    w_prev,// normally 0.5, so center will propagate to the next pyramid levels
					alpha,          // final double    alpha,
					debugLevel);    // final int       debugLevel) 
		case CuasDetectRT.CONV3D3_MODE_POWER:
			return convolve3D3LReLU(
					data,           // final double [] data,
					data_prev,      // final double [] data_prev,
					result_in,      // double [][]     result_in,
					curt_3d3_frac,  // final double    curt_3d3_frac, //  =        0.5;   // for stronger signals give this fraction of the total (of 9) weight to the 1-of-9 winner
					curt_3d3_thrsh, // final double    curt_3d3_thrsh, //  =       1.0;   // scale down curt_3d3_frac for weaker signals (in ratio of min(data[now]/curt_3d3_thrsh,1.0)
					curt_3d3_power, // final double    curt_3d3_power, //  = 2.0; // in mode_3d3=2, redistribute coarse velocities weights as the specified power of their positive values (ignore negative ones)
					w_now,          // final double    w_now, // normally 0.5, so center will propagate to the next pyramid levels
					w_prev,         // final double    w_prev,// normally 0.5, so center will propagate to the next pyramid levels
					alpha,          // final double    alpha,
					debugLevel);    // final int       debugLevel) 
		default:
			System.out.println ("convolve3D3LReLU(): mode_3d3="+mode_3d3+" is undefined");
			return null;
			
		}
	}	
	
	
	/**
	 * 
	 * @param data
	 * @param data_prev
	 * @param result_in
	 * @param curt_3d3_frac
	 * @param curt_3d3_thrsh
	 * @param w_now
	 * @param w_prev
	 * @param alpha
	 * @param debugLevel
	 * @return
	 */
	public double[][] convolve3D3LReLU(
			final double [] data,
			final double [] data_prev,
			double [][]     result_in,
			final double   curt_3d3_frac, //  =        0.5;   // for stronger signals give this fraction of the total (of 9) weight to the 1-of-9 winner
			final double   curt_3d3_thrsh, //  =       1.0;   // scale down curt_3d3_frac for weaker signals (in ratio of min(data[now]/curt_3d3_thrsh,1.0)
			final double    w_now, // normally 0.5, so center will propagate to the next pyramid levels
			final double    w_prev,// normally 0.5, so center will propagate to the next pyramid levels
			final double    alpha,
			final int       debugLevel) {
		final int kern_size_3d3 = 2 * kernel3d3_rad + 1;
		final double [][] result = (result_in != null) ? result_in : new double [data.length][kern_size_3d3*kern_size_3d3];
		int dbg_thresh = 10; // -4;
		checkData(data);
		checkData(data_prev);
		checkData(result);
		final int tl_offset =  kernel3d3_rad * (width+1);
		final int center_index = 2 * kernel3d3_rad * (kernel3d3_rad + 1);
		final int row_offset = width - (2*kernel3d3_rad +1);
		final int dbg_pix = (debugLevel > dbg_thresh) ? (300+ 173*width):-1;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		// processing center area, no need to care about margins
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					double [] coarse_vel = new double [kern_size_3d3*kern_size_3d3];
					for (int nPix = ai.getAndIncrement(); nPix < indx_center_3d3.length; nPix = ai.getAndIncrement()) {
						int ipix_dst = indx_center_3d3[nPix];
						if (ipix_dst == dbg_pix) {
							System.out.println("convolve3D3LReLU(): ipix_dst="+ipix_dst);	
						}
						int ipix_prev = ipix_dst- tl_offset;
						int ikern = kern_size_3d3*kern_size_3d3;
						Arrays.fill(coarse_vel, 0);
						double v0 = w_now * data[ipix_dst]; // will be added to all velocity outputs
						for (int iy = 0; iy < kern_size_3d3; iy++) {
							for (int ix = 0; ix < kern_size_3d3; ix++) {
								double v = data_prev[ipix_prev++] * w_prev + v0;
								coarse_vel[--ikern] = v;
							}
							ipix_prev += row_offset;
						}
						// velocity competition
						double frac = Math.min(curt_3d3_frac, data[ipix_dst]/curt_3d3_thrsh);
						if (frac > 0) {
							double sumv = 0;
							int imx = 0;
							double bestd=coarse_vel[imx];
							for (int i = 0; i < coarse_vel.length;i++) {
								double d = coarse_vel[i]; 
								sumv += d;
								coarse_vel[i] = d * (1.0 -frac);
								if (d > bestd) {
									imx = i;
									bestd= d;
								}
							}
							coarse_vel[imx] += sumv * frac;
						}
						// Apply LReLU
						for (int i = 0; i < coarse_vel.length;i++) {
							double v = coarse_vel[i]; 
							result[ipix_dst][i] = Math.max(v, alpha * v);		
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		// marginal area, check indices
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					TileNeibs tn = new TileNeibs(width, height);
					double [] coarse_vel = new double [kern_size_3d3*kern_size_3d3];
					for (int nPix = ai.getAndIncrement(); nPix < indx_margins_3d3.length; nPix = ai.getAndIncrement()) {
						int ipix_dst = indx_margins_3d3[nPix];
						int ikern = kern_size_3d3*kern_size_3d3;
						Arrays.fill(coarse_vel, 0);
						double v0 = w_now * data[ipix_dst]; // will be added to all velocity outputs
						for (int iy = 0; iy < kern_size_3d3; iy++) {
							int dy = iy-kernel3d3_rad;
							for (int ix = 0; ix < kern_size_3d3; ix++) {
								int dx = ix-kernel3d3_rad;
								int ipix_prev = tn.getNeibIndex(ipix_dst, dx, dy);
								double v = v0;
								if (ipix_prev >= 0) {
									v += data_prev[ipix_prev] * w_prev;
								}
								coarse_vel[--ikern] = v;
							}
						}
						// velocity competition
						double frac = Math.min(curt_3d3_frac, data[ipix_dst]/curt_3d3_thrsh);
						if (frac > 0) {
							double sumv = 0;
							int imx = 0;
							double bestd=coarse_vel[imx];
							for (int i = 0; i < coarse_vel.length;i++) {
								double d = coarse_vel[i]; 
								sumv += d;
								coarse_vel[i] = d * (1.0 -frac);
								if (d > bestd) {
									imx = i;
									bestd= d;
								}
							}
							coarse_vel[imx] += sumv * frac;
						}
						// Apply LReLU
						for (int i = 0; i < coarse_vel.length;i++) {
							double v = coarse_vel[i]; 
							result[ipix_dst][i] = Math.max(v, alpha * v);		
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return result;
	}
	
	
	
	public double[][] convolve3D3LReLU(
			final double [] data,
			final double [] data_prev,
			double [][]     result_in,
			final double    curt_3d3_frac, //  =        0.5;   // for stronger signals give this fraction of the total (of 9) weight to the 1-of-9 winner
			final double    curt_3d3_thrsh, //  =       1.0;   // scale down curt_3d3_frac for weaker signals (in ratio of min(data[now]/curt_3d3_thrsh,1.0)
			final double    curt_3d3_power, //  = 2.0; // in mode_3d3=2, redistribute coarse velocities weights as the specified power of their positive values (ignore negative ones)
			final double    w_now, // normally 0.5, so center will propagate to the next pyramid levels
			final double    w_prev,// normally 0.5, so center will propagate to the next pyramid levels
			final double    alpha,
			final int       debugLevel) {
		final int kern_size_3d3 = 2 * kernel3d3_rad + 1;
		final double [][] result = (result_in != null) ? result_in : new double [data.length][kern_size_3d3*kern_size_3d3];
		int dbg_thresh = 10; // -4;
		checkData(data);
		checkData(data_prev);
		checkData(result);
		final int tl_offset =  kernel3d3_rad * (width+1);
//		final int center_index = 2 * kernel3d3_rad * (kernel3d3_rad + 1);
		final int row_offset = width - (2*kernel3d3_rad +1);
		final int dbg_pix = (debugLevel > dbg_thresh) ? (300+ 173*width):-1;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		// processing center area, no need to care about margins
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					double [] coarse_vel =     new double [kern_size_3d3*kern_size_3d3];
					double [] coarse_vel_pwr = new double [kern_size_3d3*kern_size_3d3];
					for (int nPix = ai.getAndIncrement(); nPix < indx_center_3d3.length; nPix = ai.getAndIncrement()) {
						int ipix_dst = indx_center_3d3[nPix];
						if (ipix_dst == dbg_pix) {
							System.out.println("convolve3D3LReLU(): ipix_dst="+ipix_dst);	
						}
						int ipix_prev = ipix_dst- tl_offset;
						int ikern = kern_size_3d3*kern_size_3d3;
						Arrays.fill(coarse_vel, 0);
						Arrays.fill(coarse_vel_pwr, 0);
						double v0 = w_now * data[ipix_dst]; // will be added to all velocity outputs
						for (int iy = 0; iy < kern_size_3d3; iy++) {
							for (int ix = 0; ix < kern_size_3d3; ix++) {
								double v = data_prev[ipix_prev++] * w_prev + v0;
								coarse_vel[--ikern] = v;
							}
							ipix_prev += row_offset;
						}
						// velocity competition
						double frac = Math.min(curt_3d3_frac, data[ipix_dst]/curt_3d3_thrsh);
						if (frac > 0) {
							double sumv = 0;
							double sumv_pwr = 0;
							for (int i = 0; i < coarse_vel.length;i++) {
								double d = coarse_vel[i]; // includes v0;
								double d_pwr = (d > 0) ? Math.pow(d, curt_3d3_power) : 0;
								sumv += d;
								sumv_pwr += d_pwr; 
								coarse_vel_pwr[i] = d_pwr; 
							}
							double w = sumv * frac / sumv_pwr;
							if (sumv_pwr > 0) {
								for (int i = 0; i < coarse_vel.length;i++) {
									coarse_vel[i] = (1.0 -frac) * coarse_vel[i]  + w * coarse_vel_pwr[i]; 
								}								
							}
						}
						// Apply LReLU
						for (int i = 0; i < coarse_vel.length;i++) {
							double v = coarse_vel[i]; 
							result[ipix_dst][i] = Math.max(v, alpha * v);		
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		// marginal area, check indices
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					TileNeibs tn = new TileNeibs(width, height);
					double [] coarse_vel = new double [kern_size_3d3*kern_size_3d3];
					double [] coarse_vel_pwr = new double [kern_size_3d3*kern_size_3d3];
					for (int nPix = ai.getAndIncrement(); nPix < indx_margins_3d3.length; nPix = ai.getAndIncrement()) {
						int ipix_dst = indx_margins_3d3[nPix];
						int ikern = kern_size_3d3*kern_size_3d3;
						Arrays.fill(coarse_vel, 0);
						Arrays.fill(coarse_vel_pwr, 0);
						double v0 = w_now * data[ipix_dst]; // will be added to all velocity outputs
						for (int iy = 0; iy < kern_size_3d3; iy++) {
							int dy = iy-kernel3d3_rad;
							for (int ix = 0; ix < kern_size_3d3; ix++) {
								int dx = ix-kernel3d3_rad;
								int ipix_prev = tn.getNeibIndex(ipix_dst, dx, dy);
								double v = v0;
								if (ipix_prev >= 0) {
									v += data_prev[ipix_prev] * w_prev;
								}
								coarse_vel[--ikern] = v;
							}
						}
						// velocity competition
						double frac = Math.min(curt_3d3_frac, data[ipix_dst]/curt_3d3_thrsh);
						if (frac > 0) {
							double sumv = 0;
							double sumv_pwr = 0;
							for (int i = 0; i < coarse_vel.length;i++) {
								double d = coarse_vel[i]; // includes v0;
								double d_pwr = (d > 0) ? Math.pow(d, curt_3d3_power) : 0;
								sumv += d;
								sumv_pwr += d_pwr; 
								coarse_vel_pwr[i] = d_pwr; 
							}
							double w = sumv * frac / sumv_pwr;
							if (sumv_pwr > 0) {
								for (int i = 0; i < coarse_vel.length;i++) {
									coarse_vel[i] = (1.0 -frac) * coarse_vel[i]  + w * coarse_vel_pwr[i]; 
								}								
							}
						}
						// Apply LReLU
						for (int i = 0; i < coarse_vel.length;i++) {
							double v = coarse_vel[i]; 
							result[ipix_dst][i] = Math.max(v, alpha * v);		
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return result;
	}
	
	
	
	public double[][] convolve3D3LReLU(
			final double [] data,
			final double [] data_prev,
			double [][]     result_in,
			final double    w_now, // normally 0.5, so center will propagate to the next pyramid levels
			final double    w_prev,// normally 0.5, so center will propagate to the next pyramid levels
			final double    alpha,
			final int       debugLevel) {
		final int kern_size_3d3 = 2 * kernel3d3_rad + 1;
		final double [][] result = (result_in != null) ? result_in : new double [data.length][kern_size_3d3*kern_size_3d3];
		int dbg_thresh = 10; // -4;
		checkData(data);
		checkData(data_prev);
		checkData(result);
		final int tl_offset =  kernel3d3_rad * (width+1);
		final int row_offset = width - (2*kernel3d3_rad +1);
		final int dbg_pix = (debugLevel > dbg_thresh) ? (300+ 173*width):-1;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		// processing center area, no need to care about margins
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nPix = ai.getAndIncrement(); nPix < indx_center_3d3.length; nPix = ai.getAndIncrement()) {
						int ipix_dst = indx_center_3d3[nPix];
						if (ipix_dst == dbg_pix) {
							System.out.println("convolve3D3LReLU(): ipix_dst="+ipix_dst);	
						}
						int ipix_prev = ipix_dst- tl_offset;
						int ikern = kern_size_3d3*kern_size_3d3;
						double v0 = w_now * data[ipix_dst]; // will ve added to all velocity outputs
						for (int iy = 0; iy < kern_size_3d3; iy++) {
							for (int ix = 0; ix < kern_size_3d3; ix++) {
								double v = data_prev[ipix_prev++] * w_prev + v0;
								result[ipix_dst][--ikern] = Math.max(v, alpha * v);								
							}
							ipix_prev += row_offset;
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		// marginal area, check indices
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					TileNeibs tn = new TileNeibs(width, height);
					for (int nPix = ai.getAndIncrement(); nPix < indx_margins_3d3.length; nPix = ai.getAndIncrement()) {
						int ipix_dst = indx_margins_3d3[nPix];
						int ikern = kern_size_3d3*kern_size_3d3;
						double v0 = w_now * data[ipix_dst]; // will be added to all velocity outputs
						for (int iy = 0; iy < kern_size_3d3; iy++) {
							int dy = iy-kernel3d3_rad;
							for (int ix = 0; ix < kern_size_3d3; ix++) {
								int dx = ix-kernel3d3_rad;
								int ipix_prev = tn.getNeibIndex(ipix_dst, dx, dy);
								double v = v0;
								if (ipix_prev >= 0) {
									v += data_prev[ipix_prev] * w_prev;
								}
								result[ipix_dst][--ikern] = Math.max(v, alpha * v);								
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return result;
	}
	
	/**
	 * Combine (pairwise average)2 pixel arrays 		
	 * @param data_now  pixel data [width*height]
	 * @param data_prev pixel data [width*height]
	 * @param result_in allocated pixel array or null (to be allocated)
	 * @param alpha for LReLU (0 - classic ReLU, 1.0 - linear transfer, no ReLU)
	 * @return result pixel array, same as result_in if not null
	 */
	public double[] temporalAverageLReLU(
			final double [] data_now,
			final double [] data_prev,
			double []       result_in,
			final double    alpha) {
		// assuming symmetrical kernel, not inverting indices
		final double [] result = (result_in != null) ? result_in : new double [data_now.length];
		final int kernel_size = 2 * kernel2d_rad + 1;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		// processing center area, no need to care about margins
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nPix = ai.getAndIncrement(); nPix < indx_all_2d.length; nPix = ai.getAndIncrement()) {
						int ipix = indx_all_2d[nPix];
						double v = 0.5*(data_now[ipix]+data_prev[ipix]);
						result[ipix] = Math.max(v, alpha * v);
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return result;
	}
	
	
	
	public double[] convolve2DLReLU(
			final double [] data,
			final double [] kernel, // square
			double []       result_in,
			final double    alpha) {
		// assuming symmetrical kernel, not inverting indices
		final double [] result = (result_in != null) ? result_in : new double [data.length];
		// verify dimensions;
		checkData(data);
		checkData(result);
		checkKernel(kernel, kernel2d_rad);
		final int tl_offset = kernel2d_rad * (width+1);
		final int kernel_size = 2 * kernel2d_rad + 1;
		final int row_inc = width- kernel_size;
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
		// processing center area, no need to care about margins
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nPix = ai.getAndIncrement(); nPix < indx_center_2d.length; nPix = ai.getAndIncrement()) {
						int ipix_dst = indx_center_2d[nPix];
						int ipix_src = ipix_dst- tl_offset;
						int ikern = 0;
						double v = 0;
						for (int iy = 0; iy < kernel_size; iy++) {
							for (int ix = 0; ix < kernel_size; ix++) {
								v+=data[ipix_src++]*kernel[ikern++];
							}
							ipix_src+=row_inc;
						}
						result[ipix_dst] = Math.max(v, alpha * v);
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		// marginal area, check indices
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					TileNeibs tn = new TileNeibs(width, height);
					for (int nPix = ai.getAndIncrement(); nPix < indx_margins_2d.length; nPix = ai.getAndIncrement()) {
						int ipix_dst = indx_margins_2d[nPix];
						double v = 0;
						for (int iy = 0; iy < kernel_size; iy++) {
							int dy = iy-kernel2d_rad;
							for (int ix = 0; ix < kernel_size; ix++) {
								int dx = ix-kernel2d_rad;
								int ipix_src = tn.getNeibIndex(ipix_dst, dx, dy);
								if (ipix_src >= 0) {
									v+=data[ipix_src]*kernel[ix + iy*kernel_size];	
								}
							}
						}
						result[ipix_dst] = Math.max(v, alpha * v);
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return result;
	}
	
    /**
     * Calculates the integer radius for the LoG kernel based on the PSF radius
     * and the desired truncation boundary (n_sigma).
     */
    public static int getRadiusLoGKernel(double psf_radius, double n_sigma) {
        double sigma = psf_radius / Math.sqrt(2.0);
        return (int) Math.ceil(sigma * n_sigma);
    }

    /**
     * Generates a 1D flattened flux-normalized 2D Laplacian of Gaussian kernel.
     * The sum of positive elements is exactly 1.0, and the sum of negative elements is exactly -1.0.
     */
    public static double[] getLoGKenel(double psf_radius, int kernel_radius) {
        double sigma = psf_radius / Math.sqrt(2.0);
        double sigma2 = sigma * sigma;
       
        int width = 2 * kernel_radius + 1;
        double[] kernel = new double[width * width];
       
        double sumPos = 0.0;
        double sumNeg = 0.0;
       
        // 1. Generate raw inverted LoG shape and accumulate sums
        for (int y = -kernel_radius; y <= kernel_radius; y++) {
            for (int x = -kernel_radius; x <= kernel_radius; x++) {
                double r2 = x * x + y * y;
               
                // Positive center, negative surround for bright blob detection
                double raw = (1.0 - (r2 / (2.0 * sigma2))) * Math.exp(-r2 / (2.0 * sigma2));
               
                int idx = (y + kernel_radius) * width + (x + kernel_radius);
                kernel[idx] = raw;
               
                if (raw > 0) {
                    sumPos += raw;
                } else {
                    sumNeg += raw;
                }
            }
        }
       
        // 2. Apply strict flux-normalization
        double absSumNeg = Math.abs(sumNeg);
        for (int i = 0; i < kernel.length; i++) {
            if (kernel[i] > 0) {
                kernel[i] /= sumPos;
            } else if (kernel[i] < 0) {
                kernel[i] /= absSumNeg;
            }
        }
       
        return kernel;
    }

// debug methods
    public ImagePlus showKernel2d(String title) {
    	if (title==null) {
    		title = "kernel2d";
    	}
    	ImagePlus imp_kernel2d = ShowDoubleFloatArrays.makeArrays(
    			kernel2d,
				2 * kernel2d_rad + 1,
				2 * kernel2d_rad + 1,
				title);
    	imp_kernel2d.setDisplayRange(IMP_MINMAX[0],IMP_MINMAX[1]);
    	return imp_kernel2d;
    }
    
    public double [] render3d3Flat(double [][] coarse_vel) {
    	int width41 =  4 * width+1;
    	int height41 = 4 * height+1;
    	double [] render_flat = new double [width41 * height41];
    	Arrays.fill(render_flat, Double.NaN);
    	for (int npix=0; npix < coarse_vel.length; npix++) if (coarse_vel[npix] !=null){
    		int py = npix/width;
    		int px = npix%width;
    		int pix4 = (px * 4 + 1) + (py * 4 + 1) * width41;
    		int cvi = 0;
    		for (int dy = 0; dy < 3; dy++) {
        		for (int dx = 0; dx < 3; dx++) {
        			render_flat[pix4++] = coarse_vel[npix][cvi++];
        		}
        		pix4 += width41 - 3; // next row and 3 pixels back
    		}
    	}
    	return render_flat;
    }

    public double [][] render3d3Sliced(double [][] coarse_vel) {
    	double [][] render_sliced = new double [9][width * height];
    	for (double [] slice:render_sliced) {
    		Arrays.fill(slice, Double.NaN);
    	}
    	for (int npix=0; npix < coarse_vel.length; npix++) if (coarse_vel[npix] !=null){
    		for (int s = 0; s < coarse_vel[npix].length; s++) {
    			render_sliced[s][npix] = coarse_vel[npix][s];
    		}
    	}
    	return render_sliced;
    }
    
    public ImagePlus render3d3Hyperstack(
    		double [][][] coarse_vel_seq, // [nser][npix][nvel]
    		String []     scene_names,
    		String        title) {
    	String [] arrows9 = get9Arrows();
    	double [][][] render_sliced = new double [arrows9.length][coarse_vel_seq.length][width * height];
    	for (double [][] slices:render_sliced) {
    		for (double [] slice:slices) {
    			Arrays.fill(slice, Double.NaN);
    		}
    	}
    	for (int nseq = 0; nseq < coarse_vel_seq.length; nseq++) {
    		for (int npix=0; npix < coarse_vel_seq[nseq].length; npix++) if (coarse_vel_seq[nseq][npix] !=null){
    			for (int s = 0; s < coarse_vel_seq[nseq][npix].length; s++) {
    				render_sliced[s][nseq][npix] = coarse_vel_seq[nseq][npix][s];
    			}
    		}
    	}
		ImagePlus imp = ShowDoubleFloatArrays.showArraysHyperstack(
				render_sliced, // double[][][] pixels, 
				width,         // int          width, 
				title,         // String       title, "time_derivs-rt"+diff_time_rt+"-rxy"+diff_time_rxy,
				scene_names,   // all slices*frames titles or just slice titles or null
				arrows9,       // String []    frame_titles, // frame titles or null
				false);        // boolean      show)
		imp.setDisplayRange(IMP_MINMAX[0],IMP_MINMAX[1]);
    	return imp;
    }
    
    
    public String [] get9Arrows() {
    	String [] arrows= {
    			"\u2191\u2190",  // Up-Left   
    			"\u2191",        // Up        
    			"\u2192\u2191",  // Right-Up  
    			"\u2190",        // Left      
    			"\u25E6",        // Bullet    
    			"\u2192",        // Right     
    			"\u2193\u2190",  // Down-Left 
    			"\u2193",        // Down
    			"\u2192\u2193"}; // Right-Down
    	return arrows;
    }
    public String [] get9Arrows_diags() {
    	String [] arrows= {
    			"\u2196",  // Left-Up   
    			"\u2191",  // Up        
    			"\u2197",  // Right-Up  
    			"\u2190",  // Left      
    			"\u25E6",  // Bullet    
    			"\u2192",  // Right     
    			"\u2199",  // Left-Down
    			"\u2193",  // Down
    			"\u2198"};  // Right-Down
    	return arrows;
    }
    
    public ImagePlus showKernel5d(String title) {
    	// consolidationKernel 
    	// [num_subs][num_temps][pixels]
    	if (title == null) {
    		title = "kernel5d";
    	}
    	int spat_dim = consolidationKernel.getSpatialDim();
    	int coarse_dim = 3;
    	int coarse_num = coarse_dim*coarse_dim;
    	int render_dim = spat_dim * (coarse_dim+1);
    	int num_hist = kernel5d.length;
    	int num_vout = kernel5d[0].length;
    	int vout_dim = (int) Math.sqrt(num_vout);
    	int vout_rad = (vout_dim-1)/2;
    	
    	int num_sub =  kernel5d[0][0].length;
    	int sub_dim = (int) Math.sqrt(num_sub);
    	double [][][] rendered = new double [num_sub*num_hist][num_vout][(render_dim + 1) * (render_dim + 1)];
    	String [] top_titles =   new String[num_sub*num_hist];
    	String [] slice_titles = new String[num_vout];
    	for (double [][] slice_hist:rendered) {
    		for (double [] slice:slice_hist) {
    			Arrays.fill(slice, Double.NaN);
    		}
    	}
    	for (int nvout = 0; nvout < num_vout; nvout++) {
    		int vout_v = nvout / vout_dim - vout_rad ; 
    		int vout_h = nvout % vout_dim - vout_rad;
    		slice_titles[nvout] = nvout+ " ("+((vout_v >0) ?"+":"")+vout_v+":"+((vout_h >0) ?"+":"")+vout_h+")";
    	}
    	for (int nsub = 0; nsub < num_sub; nsub++) {
    		int sub_v = nsub / sub_dim; 
    		int sub_h = nsub % sub_dim;
        	for (int nhist = 0; nhist < num_hist; nhist++) {
        		int num_tslice = nsub * num_hist + nhist;
        		top_titles[num_tslice] = sub_v+":"+sub_h+" - " + nhist;
            	for (int nvout = 0; nvout < num_vout; nvout++) {
            		int coarse_index = consolidationKernel.getCoarseVIdx()[nvout];
            		for (int py = 0; py < spat_dim; py++) {
            			int py0_rend = py * (coarse_dim + 1) +1; // ) * render_dim;
            			for (int px = 0; px < spat_dim; px++) {
            				int px0_rend = px * (coarse_dim + 1) +1; //	
            				for (int coarse_i = 0; coarse_i < coarse_num; coarse_i++) {
            					double w = 0; // maybe opposite - use NaN here?
            					int coarse_v = coarse_i / coarse_dim; 
            					int coarse_h = coarse_i % coarse_dim;
            					if (coarse_i == coarse_index) {
            						w = kernel5d[nhist][nvout][nsub][py][px]; // unused will be 0, not NaN;
            					}
            					int pix_rend = (py0_rend + coarse_v) * render_dim + (px0_rend + coarse_h);
            					rendered[num_tslice][nvout][pix_rend] = w;
            				}
            			}
            		}
            	}        		
        	}    		
    	}
		ImagePlus imp = ShowDoubleFloatArrays.showArraysHyperstack(
				rendered,      // double[][][] pixels, 
				width,         // int          width, 
				title,         // String       title, "time_derivs-rt"+diff_time_rt+"-rxy"+diff_time_rxy,
				slice_titles,  // String []    titles, // all slices*frames titles or just slice titles or null
				top_titles,    // CuasMotionLMA.LMA_TITLES,  // String []    frame_titles, // frame titles or null
				false);        // boolean      show)
		imp.setDisplayRange(IMP_MINMAX[0],IMP_MINMAX[1]);
    	return imp;
    }
    // Display convolution result [vout][2*width*2*height]. Or hyperstack with x and y sliders?
    /**
     * Display a sequence of velocities per subpixel (or pixel if no decimation) layers as
     * a hyperstack. The bottom slider (outer index) selects the output velocity, the top slider
     * (and a scroll wheel) - scenes (or averaged scenes named by the last integrated) 
     * @param data [nscene][npix][nsubpix][nvelocity]
     * @param scene_titles scene names or null
     * @param title  image title or null
     * @return
     */
    public ImagePlus showConvKernel5d(
    		double [][][][] data,
    		String []       scene_titles,
    		String          title) {
    	int num_scenes = data.length;
    	int num_vout = kernel5d[0].length;
    	int vout_dim = (int) Math.sqrt(num_vout);
    	int vout_rad = (vout_dim-1)/2;
    	int num_sub =  kernel5d[0][0].length;
    	int sub_dim = (int) Math.sqrt(num_sub);
    	int full_with =   width *  sub_dim;
    	int full_height = height * sub_dim;
    	if (title==null) {
    		title = "OutputConvKernel5D";
    	}
    	if (scene_titles == null) {
    		scene_titles = new String[num_scenes];
    		for (int i = 0; i < scene_titles.length; i++) {
    			scene_titles[i]= String.format("scene-%03d", i);
    		}
    	}
		String [] v_titles = new String[num_vout];

    	for (int nvout = 0; nvout < num_vout; nvout++) {
    		int vout_v = nvout / vout_dim - vout_rad ; 
    		int vout_h = nvout % vout_dim - vout_rad;
    		v_titles[nvout] = nvout+ " ("+((vout_v >0) ?"+":"")+vout_v+":"+((vout_h >0) ?"+":"")+vout_h+")";
    	}
    	
    	double [][][] rendered = new double [num_vout][num_scenes][full_with*full_height];
    	for (double [][] d2 : rendered) for (double [] d1 : d2) Arrays.fill(d1, Double.NaN);
	
		ImagePlus imp = ShowDoubleFloatArrays.showArraysHyperstack(
				rendered,      // double[][][] pixels, 
				width,         // int          width, 
				title,         // String       title, "time_derivs-rt"+diff_time_rt+"-rxy"+diff_time_rxy,
				scene_titles,  // slice_titles,  // String []    titles, // all slices*frames titles or just slice titles or null
				v_titles,      // CuasMotionLMA.LMA_TITLES,  // String []    frame_titles, // frame titles or null
				false);        // boolean      show)
		imp.setDisplayRange(IMP_MINMAX[0],IMP_MINMAX[1]);
    	return imp;
    }

    /**
     * Render fine-velocity output for a pixel ROI as a flat image stack (one slice per scene).
     * For each pixel in the ROI a (vout_dim × vout_dim) velocity-map square is drawn.
     * Squares are tiled in the output image with a 1-pixel NaN separator column/row
     * (renders as visible grid in ImageJ). Sub-pixels (pixel_decimate > 1) tile inside each square.
     *
     * Output dimensions:
     *   width  = roi.width  * (vout_dim + 1) * sub_dim
     *   height = roi.height * (vout_dim + 1) * sub_dim
     *
     * @param data         [nscene][width*height][nsub][nvel] — output of convolve3d
     * @param roi          pixel rectangle to render (full-image coordinates)
     * @param scene_titles per-slice labels, or null
     * @param title        image title, or null
     */
    public ImagePlus showConvKernel5d(
    		double [][][][] data,
    		Rectangle       roi,
    		String []       scene_titles,
    		String          title) {
    	int num_scenes = data.length;
    	int num_vout   = kernel5d[0].length;
    	int vout_dim   = (int) Math.sqrt(num_vout);
    	int num_sub    = kernel5d[0][0].length;
    	int sub_dim    = (int) Math.sqrt(num_sub);

    	int roi_width  = roi.width  * (vout_dim + 1) * sub_dim;
    	int roi_height = roi.height * (vout_dim + 1) * sub_dim;
    	int block_w    = (vout_dim + 1) * sub_dim;
    	int block_h    = (vout_dim + 1) * sub_dim;

    	if (title == null) title = "CONV5D-ROI";
    	if (scene_titles == null) {
    		scene_titles = new String[num_scenes];
    		for (int i = 0; i < scene_titles.length; i++) {
    			scene_titles[i] = String.format("scene-%03d", i);
    		}
    	}

    	double [][] rendered = new double[num_scenes][roi_width * roi_height];
    	for (double [] slice : rendered) Arrays.fill(slice, Double.NaN);

    	for (int nscene = 0; nscene < num_scenes; nscene++) {
    		for (int py_roi = 0; py_roi < roi.height; py_roi++) {
    			int py_img = roi.y + py_roi;
    			if (py_img < 0 || py_img >= height) continue;
    			for (int px_roi = 0; px_roi < roi.width; px_roi++) {
    				int px_img = roi.x + px_roi;
    				if (px_img < 0 || px_img >= width) continue;
    				int pixel_idx = py_img * width + px_img;
    				int block_x   = px_roi * block_w;
    				int block_y   = py_roi * block_h;
    				double [][]   pix_data = data[nscene][pixel_idx]; // [nsub][nvel]
    				for (int sy = 0; sy < sub_dim; sy++) {
    					for (int sx = 0; sx < sub_dim; sx++) {
    						int sub_idx = sy * sub_dim + sx;
    						int slot_x  = block_x + sx * (vout_dim + 1);
    						int slot_y  = block_y + sy * (vout_dim + 1);
    						for (int vy = 0; vy < vout_dim; vy++) {
    							for (int vx = 0; vx < vout_dim; vx++) {
    								int v_out_idx = vy * vout_dim + vx;
    								int rx = slot_x + vx;
    								int ry = slot_y + vy;
    								rendered[nscene][ry * roi_width + rx] = pix_data[sub_idx][v_out_idx];
    							}
    						}
    					}
    				}
    			}
    		}
    	}

    	ImagePlus imp = ShowDoubleFloatArrays.makeArrays(
    			rendered,
    			roi_width,
    			roi_height,
    			title,
    			scene_titles);
    	imp.setDisplayRange(IMP_MINMAX[0], IMP_MINMAX[1]);
    	return imp;
    }

}
