/**
 ** ConditioningJna.java - Java/JNA correctness and timing gate for LWIR conditioning
 **
 ** Copyright (C) 2026 Elphel, Inc.
 **
 ** -----------------------------------------------------------------------------**
 **
 **  ConditioningJna.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.
 **
 **  ConditioningJna.java 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 ConditioningJna.java.  If not, see <http://www.gnu.org/licenses/>.
 ** -----------------------------------------------------------------------------**
 **
 */
package com.elphel.imagej.gpu.jna;

import java.util.Arrays;

import com.sun.jna.Native;
import com.sun.jna.Pointer;

/**
 * Full Java -> JNA -> NVRTC gate for the LWIR conditioning component. It runs
 * the production dimensions (16 x 640 x 512), verifies the double kernel
 * bit-for-bit against the current {@code CuasConditioning.conditionLinear()} linear
 * expression, verifies the float kernel bit-for-bit against {@link Math#fma},
 * characterizes float-map error, and measures both synchronized kernel calls
 * and the current 16-array JNA upload plus kernel path.
 *
 * Run after building libtileproc.so:
 * <pre>
 * java -Xmx2g -Djna.library.path=/home/elphel/git/tile_processor_gpu/jna \
 *   -cp target/classes:~/.m2/repository/net/java/dev/jna/jna/5.14.0/jna-5.14.0.jar \
 *   com.elphel.imagej.gpu.jna.ConditioningJna
 * </pre>
 * By Codex on 07/15/2026.
 */
public class ConditioningJna {
	private static void require(final TpJna lib, final int rc, final String what) {
		if (rc != 0) throw new IllegalStateException(what+" rc="+rc+": "+lib.tp_last_error());
	}

	private static long floatUlpDistance(final float a, final float b) {
		if (Float.isNaN(a) || Float.isNaN(b)) return (Float.isNaN(a) && Float.isNaN(b)) ? 0 : Long.MAX_VALUE;
		int ia = Float.floatToRawIntBits(a);
		int ib = Float.floatToRawIntBits(b);
		if (ia < 0) ia = 0x80000000 - ia;
		if (ib < 0) ib = 0x80000000 - ib;
		return Math.abs((long) ia - (long) ib);
	}

	private static void printTiming(final String name, final long [] ns) {
		final long [] sorted = ns.clone();
		Arrays.sort(sorted);
		final double p50 = sorted[(int) Math.floor(0.50 * (sorted.length - 1))] * 1.0e-6;
		final double p95 = sorted[(int) Math.floor(0.95 * (sorted.length - 1))] * 1.0e-6;
		final double p99 = sorted[(int) Math.floor(0.99 * (sorted.length - 1))] * 1.0e-6;
		final double max = sorted[sorted.length - 1] * 1.0e-6;
		System.out.printf("%s: p50=%.3f ms p95=%.3f ms p99=%.3f ms max=%.3f ms (%d frames)%n",
				name, p50, p95, p99, max, sorted.length);
	}

	public static void main(final String [] args) {
		final String srcdir = (args.length > 0) ? args[0] : "/home/elphel/git/tile_processor_gpu/src";
		final String devrt = (args.length > 1) ? args[1] :
				"/usr/local/cuda-12.8/targets/x86_64-linux/lib/libcudadevrt.a";
		final int cams = 16;
		final int width = 640;
		final int height = 512;
		final int pixels = width * height;
		final int total = cams * pixels;
		final double dc = -3512.375;
		final float [][] raw = new float [cams][pixels];
		final float [][] expectedDouble = new float [cams][pixels];
		final float [][] expectedFloat = new float [cams][pixels];
		final double [] scales = new double [cams];
		final double [] offsets = new double [cams];
		final double [] scales2 = new double [cams];
		final double [] fpn = new double [total];
		final float [] scaleMaps = new float [total];
		final float [] offsetMaps = new float [total];
		for (int c = 0; c < cams; c++) {
			scales[c] = 0.9785 + 0.0029 * c;
			offsets[c] = 20117.25 + 2.75 * c;
			for (int i = 0; i < pixels; i++) {
				final int k = c * pixels + i;
				raw[c][i] = (float) (20750.0 + 0.125 * (i % 97) - 3.5 * c + 0.03125 * ((i / width) % 13));
				fpn[k] = 0.03125 * ((i * 17 + c * 29) % 257 - 128) + 0.00031 * (i % 19);
				final double d = (double) raw[c][i] - offsets[c];
				expectedDouble[c][i] = (float) (scales2[c] * d * d + scales[c] * d - fpn[k] - dc);
				scaleMaps[k] = (float) scales[c];
				offsetMaps[k] = (float) (scales[c] * offsets[c] + fpn[k] + dc);
				expectedFloat[c][i] = Math.fma(raw[c][i], scaleMaps[k], -offsetMaps[k]);
			}
		}

		final TpJna lib = Native.load("tileproc", TpJna.class);
		final Pointer module = lib.tp_create_module(srcdir, devrt);
		if (module == null) throw new IllegalStateException("tp_create_module: "+lib.tp_last_error());
		Pointer proc = null;
		int badDouble = 0;
		int badFloat = 0;
		double maxModelDiff = 0.0;
		long maxModelUlp = 0;
		try {
			proc = lib.tp_proc_create(module);
			if (proc == null) throw new IllegalStateException("tp_proc_create: "+lib.tp_last_error());
			require(lib, lib.tp_proc_setup(proc, cams, 1, width, height, 0, 0), "tp_proc_setup");
			final long mapStart = System.nanoTime();
			require(lib, lib.tp_proc_set_conditioning_maps(proc, scaleMaps, offsetMaps, pixels),
					"tp_proc_set_conditioning_maps");
			final double mapMs = (System.nanoTime() - mapStart) * 1.0e-6;
			require(lib, lib.tp_proc_set_conditioning_oracle(proc, scales, offsets, scales2, fpn, pixels),
					"tp_proc_set_conditioning_oracle");

			for (int c = 0; c < cams; c++) require(lib, lib.tp_proc_set_image(proc, c, raw[c]), "set raw "+c);
			require(lib, lib.tp_proc_exec_conditioning(proc, 1, dc), "condition_lwir_d");
			final float [] got = new float [pixels];
			for (int c = 0; c < cams; c++) {
				require(lib, lib.tp_proc_get_image(proc, c, got), "get double "+c);
				for (int i = 0; i < pixels; i++) if (
						Float.floatToRawIntBits(got[i]) != Float.floatToRawIntBits(expectedDouble[c][i])) badDouble++;
			}

			for (int c = 0; c < cams; c++) require(lib, lib.tp_proc_set_image(proc, c, raw[c]), "set raw "+c);
			require(lib, lib.tp_proc_exec_conditioning(proc, 0, dc), "condition_lwir_f");
			for (int c = 0; c < cams; c++) {
				require(lib, lib.tp_proc_get_image(proc, c, got), "get float "+c);
				for (int i = 0; i < pixels; i++) {
					if (Float.floatToRawIntBits(got[i]) != Float.floatToRawIntBits(expectedFloat[c][i])) badFloat++;
					final double d = Math.abs((double) got[i] - expectedDouble[c][i]);
					if (d > maxModelDiff) maxModelDiff = d;
					final long ulp = floatUlpDistance(got[i], expectedDouble[c][i]);
					if (ulp > maxModelUlp) maxModelUlp = ulp;
				}
			}

			System.out.println("condition_lwir_d Java expression: "+total+" pixels, bit mismatches="+
					badDouble+" -> "+((badDouble == 0) ? "PASS" : "FAIL"));
			System.out.println("condition_lwir_f two-map fma: "+total+" pixels, bit mismatches="+
					badFloat+" -> "+((badFloat == 0) ? "PASS" : "FAIL"));
			System.out.printf("float two-map vs current double model: max|diff|=%.9g counts, max ULP=%d%n",
					maxModelDiff, maxModelUlp);
			System.out.printf("resident float-map upload (40 MiB through JNA): %.3f ms%n", mapMs);

			// Identity maps keep repeated kernel-only timing numerically stable while
			// preserving the exact memory traffic of the production calculation.
			Arrays.fill(scaleMaps, 1.0f);
			Arrays.fill(offsetMaps, 0.0f);
			require(lib, lib.tp_proc_set_conditioning_maps(proc, scaleMaps, offsetMaps, pixels),
					"set identity maps");
			for (int c = 0; c < cams; c++) require(lib, lib.tp_proc_set_image(proc, c, raw[c]), "timing raw "+c);
			for (int i = 0; i < 20; i++) require(lib, lib.tp_proc_exec_conditioning(proc, 0, dc), "kernel warmup");
			final long [] kernelNs = new long [200];
			for (int i = 0; i < kernelNs.length; i++) {
				final long start = System.nanoTime();
				require(lib, lib.tp_proc_exec_conditioning(proc, 0, dc), "kernel timing");
				kernelNs[i] = System.nanoTime() - start;
			}
			for (int i = 0; i < 10; i++) {
				for (int c = 0; c < cams; c++) require(lib, lib.tp_proc_set_image(proc, c, raw[c]), "upload warmup");
				require(lib, lib.tp_proc_exec_conditioning(proc, 0, dc), "upload warmup kernel");
			}
			final long [] uploadKernelNs = new long [100];
			for (int i = 0; i < uploadKernelNs.length; i++) {
				final long start = System.nanoTime();
				for (int c = 0; c < cams; c++) require(lib, lib.tp_proc_set_image(proc, c, raw[c]), "timing upload");
				require(lib, lib.tp_proc_exec_conditioning(proc, 0, dc), "timing upload kernel");
				uploadKernelNs[i] = System.nanoTime() - start;
			}
			printTiming("synchronized conditioning kernel", kernelNs);
			printTiming("16 JNA uploads + conditioning", uploadKernelNs);
			System.out.println("60 FPS frame budget: 16.667 ms");
		} finally {
			if (proc != null) lib.tp_proc_destroy(proc);
			lib.tp_destroy_module(module);
		}
		if ((badDouble != 0) || (badFloat != 0)) System.exit(1);
	}
}
