/**
 **
 ** AvgTdExport.java - export a complete standalone test case for the
 ** clt_average_sensors CUDA kernel (16-sensor TD consolidation, roadmap
 ** step 2c): per-sensor TD data straight from the GPU, the CPU oracle's
 ** expected output (CuasTD.consolidateSensorsTD) and, when available,
 ** per-sensor tile XY from TpTask for the OOB/edge weighting extension.
 ** The C++ side (tile_processor_gpu/src/tests/test_avg_td.cu, debuggable in
 ** NSight) loads the directory, runs the kernel and self-checks bit-exactly.
 **
 ** Copyright (C) 2026 Elphel, Inc.
 **
 ** -----------------------------------------------------------------------------**
 **
 **  AvgTdExport.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/>.
 ** -----------------------------------------------------------------------------**
 **
 ** By Claude on 07/09/2026
 */
package com.elphel.imagej.gpu.testdata;

import java.io.IOException;

import com.elphel.imagej.cuas.rt.CuasTD;
import com.elphel.imagej.gpu.GPUTileProcessor;
import com.elphel.imagej.gpu.GpuQuad;
import com.elphel.imagej.gpu.TpTask;

public class AvgTdExport {
	/**
	 * Export everything the standalone clt_average_sensors test needs.
	 * Call with a GpuQuad holding a current 16-sensor TD (after
	 * convert_direct), e.g. next to CuasTD.validateConsolidation().
	 *
	 * @param gpuQuad    GPU instance with per-sensor CLT data
	 * @param use_ref    use the reference-scene TD buffers
	 * @param tp_tasks   task list used for the conversion (per-sensor tile XY
	 *                   for the OOB stage) or null to skip
	 * @param dir_path   test-case directory, e.g. <model>/testdata/avg_td
	 * @param comment    provenance line (scene timestamp etc.)
	 * @param debugLevel debug level
	 * @return dir_path on success
	 */
	public static String export(
			GpuQuad   gpuQuad,
			boolean   use_ref,
			TpTask [] tp_tasks,
			String    dir_path,
			String    comment,
			int       debugLevel) throws IOException {
		final int dtt = GPUTileProcessor.DTT_SIZE;
		final int [] wh =    gpuQuad.getWH(use_ref);
		final int tilesX =   wh[0] / dtt;
		final int tilesY =   wh[1] / dtt;
		final int colors =   gpuQuad.num_colors;
		final int num_sens = gpuQuad.num_cams;
		final int num_tiles = tilesY * tilesX * colors;

		final float [][] fclt = gpuQuad.getCltData(use_ref); // [num_sens][num_tiles*256]
		final int chunk = CuasTD.TD_CHUNK;                   // 256
		if (fclt[0].length != (num_tiles * chunk)) {
			throw new IllegalStateException("AvgTdExport: CLT length " + fclt[0].length +
					" != tiles " + num_tiles + " * " + chunk);
		}
		// CPU oracle: NaN-aware average + contributing-sensor counts
		final int [] counts = new int [num_tiles];
		final float [] avg = CuasTD.consolidateSensorsTD(fclt, counts);
		final float [] fcounts = new float [num_tiles];     // kernel outputs counts as f32
		for (int i = 0; i < num_tiles; i++) fcounts[i] = counts[i];

		final float [] td_sens = new float [num_sens * num_tiles * chunk];
		for (int nsens = 0; nsens < num_sens; nsens++) {
			System.arraycopy(fclt[nsens], 0, td_sens, nsens * num_tiles * chunk, num_tiles * chunk);
		}

		try (TestDataWriter tdw = new TestDataWriter(dir_path, "avg_td test case: " + comment)) {
			tdw.prm("num_sensors", num_sens);
			tdw.prm("tilesx",      tilesX);
			tdw.prm("tilesy",      tilesY);
			tdw.prm("colors",      colors);
			tdw.buf("td_sens",         td_sens, num_sens, tilesY, tilesX, colors, chunk);
			tdw.buf("expected_td_avg", avg,     tilesY, tilesX, colors, chunk);
			tdw.buf("expected_counts", fcounts, num_tiles);
			if (tp_tasks != null) { // inputs for the OOB/edge-weighting stage
				final int ntasks = tp_tasks.length;
				final int [] task_tilexy = new int [ntasks * 2];
				final float [] task_xy = new float [ntasks * num_sens * 2];
				for (int i = 0; i < ntasks; i++) {
					task_tilexy[2 * i]     = tp_tasks[i].getTileX();
					task_tilexy[2 * i + 1] = tp_tasks[i].getTileY();
					float [][] xy = tp_tasks[i].getXY(); // [num_sens][2] per-sensor px
					for (int nsens = 0; nsens < num_sens; nsens++) {
						task_xy[(i * num_sens + nsens) * 2]     = xy[nsens][0];
						task_xy[(i * num_sens + nsens) * 2 + 1] = xy[nsens][1];
					}
				}
				tdw.prm("img_width",  wh[0]);
				tdw.prm("img_height", wh[1]);
				tdw.buf("task_tilexy", task_tilexy, ntasks, 2);
				tdw.buf("task_xy",     task_xy,     ntasks, num_sens, 2);
			}
			if (debugLevel > -2) {
				System.out.println("AvgTdExport: wrote " + tdw.getDir() +
						" (" + num_sens + " sensors, " + tilesY + "x" + tilesX +
						"x" + colors + " tiles" +
						((tp_tasks != null) ? (", " + tp_tasks.length + " tasks") : "") + ")");
			}
			return tdw.getDir();
		}
	}

	/**
	 * Export a standalone test case for the clt_average_sensors v1 chain
	 * (roadmap 2c): index_consolidate (MB-pair tile selection) + OOB soft/hard
	 * priority + masked consolidation. The case carries the RAW flattened task
	 * streams (exact GPU struct layout, TpTask.asFloatArray) for BOTH motion-blur
	 * sets so the CUDA test exercises the real two-set indexing path, plus the
	 * CPU-oracle outputs: per-tile sensor masks (CuasTD.oobSensorMasks), the
	 * masked TD average and counts. Margins travel in the manifest (cases
	 * self-describing - frozen spec). By Claude on 07/12/2026.
	 *
	 * @param gpuQuad     GPU instance with a current per-sensor TD
	 * @param use_ref     use the reference-scene TD buffers
	 * @param tp_tasks    [2][]: task set 0 (required) + MB pair set (tp_tasks[1]
	 *                    may be null = no-MB mode, same as setInterTasksMotionBlur)
	 * @param soft_margin OOB soft margin, px (~12.0; 0 = disabled)
	 * @param hard_margin OOB hard margin, px (~8.0; 0 = disabled)
	 * @param dir_path    test-case directory, e.g. <model>/testdata/avg_td_oob
	 * @param comment     provenance line (scene timestamp etc.)
	 * @param debugLevel  debug level
	 * @return dir_path on success
	 */
	public static String exportOob(
			GpuQuad     gpuQuad,
			boolean     use_ref,
			TpTask [][] tp_tasks,
			double      soft_margin,
			double      hard_margin,
			String      dir_path,
			String      comment,
			int         debugLevel) throws IOException {
		final int dtt = GPUTileProcessor.DTT_SIZE;
		final int [] wh =    gpuQuad.getWH(use_ref);
		final int tilesX =   wh[0] / dtt;
		final int tilesY =   wh[1] / dtt;
		final int colors =   gpuQuad.num_colors;
		final int num_sens = gpuQuad.num_cams;
		final int num_tiles = tilesY * tilesX * colors;

		final float [][] fclt = gpuQuad.getCltData(use_ref);
		final int chunk = CuasTD.TD_CHUNK;
		if (fclt[0].length != (num_tiles * chunk)) {
			throw new IllegalStateException("AvgTdExport.exportOob: CLT length " + fclt[0].length +
					" != tiles " + num_tiles + " * " + chunk);
		}
		// CPU oracle: pass-1 (MB-pair presence) + pass-2 (OOB priority) masks, then
		// the masked NaN-aware average
		final int [] masks = CuasTD.oobSensorMasks(
				tp_tasks, num_sens, tilesX, tilesY, wh[0], wh[1], soft_margin, hard_margin);
		final int [] counts = new int [num_tiles];
		final float [] avg = CuasTD.consolidateSensorsTD(fclt, counts, masks, colors);
		final float [] fcounts = new float [num_tiles];
		for (int i = 0; i < num_tiles; i++) fcounts[i] = counts[i];
		int n_full = 0, n_partial = 0, n_poison = 0;
		final int all_mask = (1 << num_sens) - 1;
		for (int m : masks) {
			if (m == 0) n_poison++;
			else if (m == all_mask) n_full++;
			else n_partial++;
		}
		final float [] td_sens = new float [num_sens * num_tiles * chunk];
		for (int nsens = 0; nsens < num_sens; nsens++) {
			System.arraycopy(fclt[nsens], 0, td_sens, nsens * num_tiles * chunk, num_tiles * chunk);
		}
		try (TestDataWriter tdw = new TestDataWriter(dir_path, "avg_td_oob test case: " + comment)) {
			tdw.prm("num_sensors", num_sens);
			tdw.prm("tilesx",      tilesX);
			tdw.prm("tilesy",      tilesY);
			tdw.prm("colors",      colors);
			tdw.prm("img_width",   wh[0]);
			tdw.prm("img_height",  wh[1]);
			tdw.prm("soft_margin", soft_margin);
			tdw.prm("hard_margin", hard_margin);
			tdw.prm("has_mb",      ((tp_tasks.length > 1) && (tp_tasks[1] != null)) ? 1 : 0);
			tdw.buf("td_sens",             td_sens, num_sens, tilesY, tilesX, colors, chunk);
			for (int nset = 0; nset < 2; nset++) { // raw task streams, exact GPU layout
				if ((nset >= tp_tasks.length) || (tp_tasks[nset] == null)) continue;
				final TpTask [] tasks = tp_tasks[nset];
				final int task_size = tasks[0].getSize();
				final float [] ftasks = new float [tasks.length * task_size];
				for (int i = 0; i < tasks.length; i++) {
					tasks[i].asFloatArray(ftasks, i, false);
				}
				tdw.prm("task_size", task_size);
				tdw.buf("ftasks" + nset, ftasks, tasks.length, task_size);
			}
			tdw.buf("expected_sens_masks", masks,   tilesY, tilesX);
			tdw.buf("expected_td_avg",     avg,     tilesY, tilesX, colors, chunk);
			tdw.buf("expected_counts",     fcounts, num_tiles);
			if (debugLevel > -2) {
				System.out.println("AvgTdExport.exportOob: wrote " + tdw.getDir() +
						" (" + num_sens + " sensors, " + tilesY + "x" + tilesX + "x" + colors +
						" tiles, soft/hard=" + soft_margin + "/" + hard_margin +
						", masks: " + n_full + " full, " + n_partial + " partial, " +
						n_poison + " poisoned)");
			}
			return tdw.getDir();
		}
	}

	/**
	 * Import the C++ test results back and compare against the oracle
	 * reference stored in the same test-case directory (Java-side round-trip
	 * check; the C++ test already self-checks).
	 * @return max abs difference over finite elements (NaN-mismatch counts as
	 *         Double.POSITIVE_INFINITY)
	 */
	public static double compareResults(String dir_path, int debugLevel) throws IOException {
		TestDataReader expected = new TestDataReader(dir_path);
		TestDataReader results = TestDataReader.results(dir_path);
		double max_diff = 0.0;
		for (String name : new String [] {"td_avg", "counts"}) {
			if (!results.has(name) || !expected.has("expected_" + name)) continue;
			float [] r = results.getFloats(name);
			float [] e = expected.getFloats("expected_" + name);
			double md = 0.0;
			int nan_mismatch = 0;
			for (int i = 0; i < r.length; i++) {
				boolean rn = Float.isNaN(r[i]), en = Float.isNaN(e[i]);
				if (rn || en) {
					if (rn != en) nan_mismatch++;
					continue;
				}
				md = Math.max(md, Math.abs((double) r[i] - (double) e[i]));
			}
			if (nan_mismatch > 0) md = Double.POSITIVE_INFINITY;
			if (debugLevel > -2) {
				System.out.println("AvgTdExport.compareResults: '" + name +
						"' max|diff|=" + md + ", NaN-mismatch=" + nan_mismatch);
			}
			max_diff = Math.max(max_diff, md);
		}
		return max_diff;
	}
}
