/**
 **
 ** CuasRender.java - dedicated CUAS RT renderer: conditioned raw-jp4 scenes
 ** rendered on the virtual-camera uniform grid at borrowed poses.
 **
 ** Copyright (C) 2026 Elphel, Inc.
 **
 ** -----------------------------------------------------------------------------**
 **
 **  CuasRender.java is free software: you can redistribute it and/or modify
 **  it under the terms of the GNU General Public License as published by
 **  the Free Software Foundation, either version 3 of the License, or
 **  (at your option) any later version.
 **
 **  This program is distributed in the hope that it will be useful,
 **  but WITHOUT ANY WARRANTY; without even the implied warranty of
 **  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 **  GNU General Public License for more details.
 **
 **  You should have received a copy of the GNU General Public License
 **  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ** -----------------------------------------------------------------------------**
 **
 */
package com.elphel.imagej.cuas.rt;

import java.util.ArrayList;

import com.elphel.imagej.cameras.CLTParameters;
import com.elphel.imagej.cuas.CuasMotion;
import com.elphel.imagej.gpu.GpuQuad;
import com.elphel.imagej.gpu.TpTask;
import com.elphel.imagej.tileprocessor.ErsCorrection;
import com.elphel.imagej.tileprocessor.ImageDtt;
import com.elphel.imagej.tileprocessor.OpticalFlow;
import com.elphel.imagej.tileprocessor.QuadCLT;
import com.elphel.imagej.tileprocessor.TwoQuadCLT;

import ij.ImagePlus;
import ij.ImageStack;

/**
 * The dedicated CUAS RT renderer (design ruling 2026-07-05, spec in
 * imagej-elphel-internal handoffs/2026-07-05_cuas_rt_dataflow_and_grids.md par.6):
 * replaces the legacy multi-purpose renderSceneSequence() for CUAS RT.
 *  - Grid: ALWAYS the virtual-camera uniform grid (no transformFromVirtual /
 *    transformCameraVew / conditionInitialDS in this path).
 *  - Disparity: the raw center DSI (the -INTER-INTRA-LMA vintage, same source as
 *    reliable_tiles) - ONE disparity source for all of RT.
 *  - Pixels: raw /jp4/*.tiff + CuasConditioning (conditionSceneToGpu) - never
 *    QuadCLT image products.
 *  - QuadCLT instances: BORROW-only (poses + ERS rates from scenes_poses, geometry,
 *    tile dims). Any new implied QuadCLT dependency must be raised with Andrey.
 *  - All methods STATIC.
 *
 * First delivered piece: the MINIMAL ingest+render sequence test (testRenderSequence)
 * that certifies conditionSceneToGpu + the virtual-grid render chain by generating
 * per-sensor + merged per-scene stacks directly comparable to the oracle
 * -CUAS-INDIVIDUAL-CUAS-DBG / -CUAS-MERGED-CUAS-DBG products. Today's lesson: this
 * step was jumped over (conditionSceneToGpu had a single caller inside the pose test
 * and was never image-certified standalone) - test and debug HERE first, only then
 * return to the pose test.
 *
 * By Claude on 07/05/2026, from Andrey's design.
 */
public class CuasRender {
	public static final double [] ZERO3 = {0.0, 0.0, 0.0};

	/**
	 * CUAS disparity conditioning for RENDERING (static, QuadCLT-free - the legacy
	 * conditionInitialDS takes a QuadCLT instance and is out per the RT rules).
	 * Requirements (Andrey 2026-07-05): disparity must be DEFINED FOR ALL TILES - the
	 * sky has nothing to match but is the background for drones, so it renders at
	 * infinity (disparity 0); NaN is acceptable only outside the image. The raw plain
	 * DSI slice has no NaN but carries garbage (crazy tiles < -1 in the sky, negatives
	 * near high-contrast posts) - all non-physical: clamp to >= 0.
	 * @param disparity plain disparity (slice 0 of -INTER-INTRA-LMA), NOT the LMA slice
	 *                  (which is NaN wherever the LMA did not fit - the whole sky)
	 * @return conditioned copy: NaN -> 0.0 (infinity), negatives -> 0.0
	 */
	public static double [] conditionDisparityCuas(final double [] disparity) {
		final double [] cond = new double [disparity.length];
		for (int i = 0; i < disparity.length; i++) {
			final double d = disparity[i];
			cond[i] = (Double.isNaN(d) || (d < 0.0)) ? 0.0 : d;
		}
		return cond;
	}

	/**
	 * Render ONE scene (already ingested to the GPU by CuasConditioning.conditionSceneToGpu)
	 * on the virtual-camera uniform grid at the given (borrowed or fitted) pose.
	 * No correlation - tasks + convert (erased CLT: NaN outside task tiles) + imclt.
	 * @param clt_parameters processing parameters (LPF sigmas, margin)
	 * @param image_dtt      ImageDtt bound to the center GPU (created once per sequence)
	 * @param center_CLT     virtual center (grid dims, ErsCorrection reference side)
	 * @param scene          scene instance (BORROW: geometry + its ErsCorrection with
	 *                       rates already set; image data must already be on the GPU)
	 * @param center_disparity raw center DSI disparity (virtual grid)
	 * @param scene_xyz      scene position (ZERO3 for CUAS rotation-only)
	 * @param scene_atr      scene orientation (borrowed/fitted)
	 * @param margin         tile margin
	 * @param mb_vectors     null (no motion-blur compensation) or [2][ntiles] per-tile blur
	 *                       vectors (for CUAS uniform over the scene: dpx/dt, dpy/dt from
	 *                       the wobble radius + RPM). With MB the convert runs TWICE (the
	 *                       shifted, negative-scale second pass removes the bolometer
	 *                       exponential tail; scale pair sums to 1, ratio from
	 *                       (1-pix time)/mb_tau, offset grows if scales would exceed
	 *                       mb_max_gain - all inside setInterTasksMotionBlur).
	 * @param mb_tau         bolometer time constant, s (imp.mb_tau, 0.008)
	 * @param mb_max_gain    max scale before the offset is stretched (imp.mb_max_gain_inter)
	 * @param debugLevel     debug level
	 * @return [17][width*height]: renders of the 16 sensors (grid-transformed to the
	 *         virtual grid) + [16] = the consolidated (NaN-aware weighted average)
	 *         merged render; null if no valid tasks
	 */
	public static float [][] renderSceneVirtual(
			final CLTParameters clt_parameters,
			final ImageDtt      image_dtt,
			final QuadCLT       center_CLT,
			final QuadCLT       scene,
			final double []     center_disparity,
			final double []     scene_xyz,
			final double []     scene_atr,
			final int           margin,
			final double [][]   mb_vectors,  // By Claude on 07/05/2026
			final double        mb_tau,      // By Claude on 07/05/2026
			final double        mb_max_gain, // By Claude on 07/05/2026
			final int           debugLevel) {
		final GpuQuad gpuQuad = center_CLT.getGPUQuad();
		// 1. virtual uniform grid -> scene grid at the pose (ERS per the instances' rates)
		final double [][] scene_pXpYD = OpticalFlow.transformToScenePxPyD(
				null, center_disparity, scene_xyz, scene_atr, scene, center_CLT);
		final double disparity_corr = clt_parameters.imp.disparity_corr + center_CLT.getDispInfinityRef();
		final int img_width = scene.getGeometryCorrection().getSensorWH()[0];
		if (mb_vectors != null) {
			// Motion-blur compensation: TWO task sets (original positive scale + shifted
			// negative scale), double convert_direct - the oracle machinery
			// (setInterTasksMotionBlur / interCorrTDMotionBlur). The FIRST convert erases
			// the CLT to NaN, so tiles absent from the task set reach the NaN-aware channel
			// consolidation (and the renders) as NaN - never stale data (Andrey 07/05/2026).
			final TpTask [][] tp_tasks2 = GpuQuad.setInterTasksMotionBlur(
					scene.getNumSensors(),
					img_width,
					false,                      // GPU calculates port coordinates
					scene_pXpYD,
					null,                       // selection: ALL tiles with valid disparity
					mb_tau,
					mb_max_gain,
					mb_vectors,
					scene.getGeometryCorrection(),
					disparity_corr,
					margin,
					null,                       // valid_tiles
					ImageDtt.THREADS_MAX);
			if ((tp_tasks2 == null) || (tp_tasks2[0] == null) || (tp_tasks2[0].length == 0)) {
				System.out.println("renderSceneVirtual(): no MB tasks for scene "+scene.getImageName());
				return null;
			}
			image_dtt.interCorrTDMotionBlur(
					clt_parameters.img_dtt,
					1,                          // erase_clt_first: NaN outside the task set // By Claude on 07/05/2026
					tp_tasks2,
					null,
					clt_parameters.gpu_sigma_r,
					clt_parameters.gpu_sigma_b,
					clt_parameters.gpu_sigma_g,
					clt_parameters.gpu_sigma_m,
					scene.isMonochrome() ? 1.0 : clt_parameters.gpu_sigma_rb_corr,
					clt_parameters.getGpuCorrSigma(scene.isMonochrome()),
					clt_parameters.getGpuCorrLoGSigma(scene.isMonochrome()),
					clt_parameters.corr_red,
					clt_parameters.corr_blue,
					0,                          // sensor_mask_inter = 0: convert only
					ImageDtt.THREADS_MAX,
					debugLevel);
		} else {
			final TpTask [] tp_tasks = GpuQuad.setInterTasks(
					scene.getNumSensors(),
					img_width,
					false,                          // GPU calculates port coordinates
					scene_pXpYD,                    // per-tile pX,pY,disparity
					null,                           // selection: ALL tiles with valid disparity
					scene.getGeometryCorrection(),
					disparity_corr,
					margin,
					null,                           // valid_tiles
					ImageDtt.THREADS_MAX);
			if ((tp_tasks == null) || (tp_tasks.length == 0)) {
				System.out.println("renderSceneVirtual(): no tasks for scene "+scene.getImageName());
				return null;
			}
			// 2. LPFs + tasks + offsets + convert (mask=0: no correlation)
			image_dtt.interCorrTD(
					clt_parameters.img_dtt,
					tp_tasks,
					null,
					clt_parameters.gpu_sigma_r,
					clt_parameters.gpu_sigma_b,
					clt_parameters.gpu_sigma_g,
					clt_parameters.gpu_sigma_m,
					scene.isMonochrome() ? 1.0 : clt_parameters.gpu_sigma_rb_corr,
					clt_parameters.getGpuCorrSigma(scene.isMonochrome()),
					clt_parameters.getGpuCorrLoGSigma(scene.isMonochrome()),
					clt_parameters.corr_red,
					clt_parameters.corr_blue,
					0,                              // sensor_mask_inter = 0: convert only
					ImageDtt.THREADS_MAX,
					debugLevel);
			// re-convert with erase_clt=1: NaN outside task tiles (interCorrTD converts with
			// erase=-1, leaving the previous scene's tiles as ghosts in a render)
			gpuQuad.execConvertDirect(false, null, 1);
		}
		// 3. per-sensor renders (virtual grid)
		final float [][] per_sensor = CuasMotion.perSensorImagesFromTD(gpuQuad, false);
		// 4. merged = the NaN-aware weighted average consolidated in TD, rendered via slot 0
		final float [][] fclt16 = gpuQuad.getCltData(false);
		final float [] avg_td = CuasTD.consolidateSensorsTD(fclt16, null);
		gpuQuad.setCltData(0, avg_td, false);
		final float [] merged = CuasMotion.perSensorImagesFromTD(gpuQuad, false)[0];
		final float [][] rslt = new float [per_sensor.length + 1][];
		System.arraycopy(per_sensor, 0, rslt, 0, per_sensor.length);
		rslt[per_sensor.length] = merged;
		return rslt;
	}

	/**
	 * MINIMAL standalone test of the RT ingest+render chain (runs INSTEAD of detection
	 * and INSTEAD of the pose test): for every scene, ingest raw /jp4/ via
	 * CuasConditioning.conditionSceneToGpu, borrow the STORED pose + STORED ERS rates
	 * (exactly the sources the oracle DBG renders use), render on the virtual uniform
	 * grid, and save one hyperstack:
	 *   <center>-CUAS-RT-RENDER.tiff  [t: s00..s15, merged][z: scenes]
	 * directly comparable (slice-by-slice, same borrowed poses) to the oracle
	 * -CUAS-INDIVIDUAL-CUAS-DBG / -CUAS-MERGED-CUAS-DBG debugging products.
	 * By Claude on 07/05/2026, from Andrey's plan: test and debug THIS first,
	 * only then return to the pose test.
	 */
	public static void testRenderSequence(
			final CLTParameters clt_parameters,
			final QuadCLT       center_CLT,
			final QuadCLT []    quadCLTs,
			final int           debugLevel) {
		final int margin = clt_parameters.imp.margin;
		final boolean is_aux = center_CLT.isAux();
		final double [][] center_dsi = center_CLT.dsi;
		// RENDERING disparity: the PLAIN slice conditioned to be defined for ALL tiles
		// (sky = infinity/0). Deliberately NOT gated by use_lma_dsi: the LMA slice is NaN
		// wherever the LMA did not fit (the whole sky) and would drop those tiles from the
		// render. By Claude on 07/05/2026, per Andrey.
		final double [] center_disparity = conditionDisparityCuas(
				center_dsi[is_aux ? TwoQuadCLT.DSI_DISPARITY_AUX : TwoQuadCLT.DSI_DISPARITY_MAIN]);
		final ErsCorrection ers_center = center_CLT.getErsCorrection();
		final ImageDtt image_dtt = new ImageDtt(
				center_CLT.getNumSensors(),
				clt_parameters.transform_size,
				clt_parameters.img_dtt,
				center_CLT.isAux(),
				center_CLT.isMonochrome(),
				center_CLT.isLwir(),
				clt_parameters.getScaleStrength(center_CLT.isAux()),
				center_CLT.getGPU());
		final int img_width =  center_CLT.getGPUQuad().getImageWidth();
		final int img_height = center_CLT.getGPUQuad().getImageHeight();
		final int num_sens =   center_CLT.getNumSensors();
		final int num_comps =  num_sens + 1; // 16 sensors + merged
		// Motion-blur compensation, gated by the same imp.mb_en as the oracle DBG renders
		// (mb_en OFF -> compare against the -NOMB oracle products). Vectors from the same
		// getMotionBlur the oracle uses, at the borrowed pose + stored rates. The identity
		// center grid is needed for it. By Claude on 07/05/2026.
		final boolean mb_en =       clt_parameters.imp.mb_en;
		final double  mb_tau =      clt_parameters.imp.mb_tau;
		final double  mb_max_gain = clt_parameters.imp.mb_max_gain_inter;
		final double [][] pXpYD_center = mb_en ? OpticalFlow.transformToScenePxPyD(
				null, center_disparity, ZERO3, ZERO3, center_CLT, center_CLT) : null;
		System.out.println("testRenderSequence(): motion-blur compensation "+
				(mb_en ? ("ON (tau="+mb_tau+" s, max gain "+mb_max_gain+") - compare vs the MB oracle DBG") :
					"OFF - compare vs the -NOMB oracle DBG"));
		// PER-SEQUENCE DC level for the scene conditioning - NOT per-scene: a per-scene sky
		// median oscillates with rotation (terrain sweeps the sky half; 234-count pk-pk at
		// exactly 1 rev period measured in THIS product, POSE-10 forensics) and downstream
		// temporal averages turn it into content ghosts. ONE constant per sequence
		// (CuasRT.appropriateCenter derived it from the borrowed center = the average
		// scene). By Claude on 07/05/2026, Andrey's rule.
		final CuasConditioning.Config cond_cfg = new CuasConditioning.Config();
		cond_cfg.dc_level = CuasRT.sequenceDcLevel(clt_parameters, center_CLT);
		if (Double.isNaN(cond_cfg.dc_level)) {
			System.out.println("testRenderSequence(): WARNING - no per-sequence DC level (RT-STATE missing, curt.dc_center==0):"+
					" falling back to the PER-SCENE sky median (rotation-synced DC flicker!)");
		}
		final ArrayList<float [][]> rendered = new ArrayList<float [][]>();
		final ArrayList<String>     ts_names = new ArrayList<String>();
		final float [] nan_slice = new float [img_width * img_height];
		java.util.Arrays.fill(nan_slice, Float.NaN);
		int num_ok = 0, num_fail = 0;
		for (int nscene = 0; nscene < quadCLTs.length; nscene++) {
			if (quadCLTs[nscene] == null) continue;
			final String ts_name = quadCLTs[nscene].getImageName();
			// BORROWED pose + BORROWED stored ERS rates - the same sources the oracle
			// DBG renders use (renderSceneSequence: getSceneXYZ/ATR + getSceneErsXYZ_dt/ATR_dt)
			final double [][] pose = ers_center.getSceneXYZATR(ts_name);
			final double [] ers_xyz_dt = ers_center.getSceneErsXYZ_dt(ts_name);
			final double [] ers_atr_dt = ers_center.getSceneErsATR_dt(ts_name);
			float [][] rslt = null;
			if (pose == null) {
				System.out.println("testRenderSequence(): scene "+nscene+" ("+ts_name+") has no stored pose - skipping");
			} else {
				if ((ers_xyz_dt == null) || (ers_atr_dt == null)) {
					System.out.println("testRenderSequence(): scene "+nscene+" ("+ts_name+") has no stored ERS rates - zeros");
				}
				// THE STEP UNDER TEST: raw /jp4/ -> conditioning -> GPU, motion (omegas +
				// velocities provision) entering through the ONE wrapper. Here the rates are
				// borrowed (stored); in production they come analytically from pose + rpm.
				final boolean raw_ok = CuasConditioning.conditionSceneToGpuCuas(
						quadCLTs[nscene],
						cond_cfg,             // Config: defaults + the PER-SEQUENCE dc_level // By Claude on 07/05/2026
						(ers_atr_dt != null) ? ers_atr_dt : ZERO3.clone(), // omegas
						(ers_xyz_dt != null) ? ers_xyz_dt : ZERO3.clone(), // velocities (provision)
						ImageDtt.THREADS_MAX,
						debugLevel - 2);
				if (raw_ok) {
					double [][] mb_vectors = null;
					if (mb_en) { // same oracle machinery + same (borrowed) rates as the DBG renders
						mb_vectors = OpticalFlow.getMotionBlur(
								center_CLT,          // QuadCLT        ref_scene,
								quadCLTs[nscene],    // QuadCLT        scene,
								pXpYD_center,        // double [][]    ref_pXpYD,
								pose[0],             // double []      camera_xyz,
								pose[1],             // double []      camera_atr,
								(ers_xyz_dt != null) ? ers_xyz_dt : ZERO3.clone(), // camera_xyz_dt,
								(ers_atr_dt != null) ? ers_atr_dt : ZERO3.clone(), // camera_atr_dt,
								0,                   // int            shrink_gaps,
								debugLevel - 2);     // int            debug_level
					}
					rslt = renderSceneVirtual(
							clt_parameters, image_dtt, center_CLT, quadCLTs[nscene],
							center_disparity, pose[0], pose[1], margin,
							mb_vectors, mb_tau, mb_max_gain, debugLevel - 2);
				} else {
					System.out.println("testRenderSequence(): scene "+nscene+" ("+ts_name+") raw-jp4 ingest FAILED");
				}
			}
			if (rslt != null) num_ok++; else num_fail++;
			rendered.add(rslt); // null OK - NaN slices keep z aligned
			ts_names.add(ts_name);
			if ((debugLevel > -4) && (((num_ok + num_fail) % 50) == 0)) {
				System.out.println("testRenderSequence(): rendered "+(num_ok+num_fail)+" scenes ("+num_fail+" failed)");
			}
		}
		// ---- Save: [t: s00..s15, merged][z: scenes] hyperstack ----
		final ImageStack stack = new ImageStack(img_width, img_height);
		for (int nc = 0; nc < num_comps; nc++) {
			final String comp = (nc < num_sens) ? String.format("s%02d", nc) : "merged";
			for (int ns = 0; ns < rendered.size(); ns++) {
				final float [][] r = rendered.get(ns);
				stack.addSlice(comp+":"+ts_names.get(ns), ((r != null) && (r[nc] != null)) ? r[nc] : nan_slice);
			}
		}
		final ImagePlus imp = new ImagePlus(center_CLT.getImageName()+"-CUAS-RT-RENDER", stack);
		imp.setDimensions(1, rendered.size(), num_comps);
		imp.setOpenAsHyperStack(true);
		imp.getProcessor().resetMinAndMax();
		center_CLT.saveImagePlusInModelDirectory(null, imp); // title as filename
		System.out.println("testRenderSequence(): scenes OK="+num_ok+", failed="+num_fail+
				", saved -CUAS-RT-RENDER ("+num_comps+" components x "+rendered.size()+" scenes)");
	}
}
