/**
 **
 ** 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();
		}
	}

	/**
	 * 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;
	}
}
