/**
 **
 ** TestDataReader.java - read a standalone-GPU-test data directory back into
 ** Java: parses manifest.txt (or results/results_manifest.txt written by the
 ** C++ per-kernel test) and loads raw little-endian arrays, so kernel results
 ** can be compared against the oracle and inspected in ImageJ. Counterpart of
 ** TestDataWriter; contract in tile_processor_gpu/src/tests/testdata_format.md.
 **
 ** Copyright (C) 2026 Elphel, Inc.
 **
 ** -----------------------------------------------------------------------------**
 **
 **  TestDataReader.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.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class TestDataReader {
	public static class Entry {
		public String name;
		public String dtype; // "f32" | "i32"
		public int [] dims;  // row-major, last fastest
		public String file;  // relative to the manifest directory
	}

	private final File               dir;
	private final Map<String,Double> params = new HashMap<>();
	private final Map<String,Entry>  bufs   = new LinkedHashMap<>();

	/** Read <dir_path>/manifest.txt (a test-case input directory). */
	public TestDataReader(String dir_path) throws IOException {
		this(dir_path, "manifest.txt");
	}

	/** Read the C++ test results: <case_dir>/results/results_manifest.txt. */
	public static TestDataReader results(String case_dir) throws IOException {
		return new TestDataReader(case_dir + File.separator + "results",
				"results_manifest.txt");
	}

	private TestDataReader(String dir_path, String manifest_name) throws IOException {
		dir = new File(dir_path);
		File mf = new File(dir, manifest_name);
		List<String> lines = Files.readAllLines(mf.toPath());
		int lnum = 0;
		for (String line : lines) {
			lnum++;
			int hash = line.indexOf('#');
			if (hash >= 0) line = line.substring(0, hash);
			String [] tok = line.trim().split("\\s+");
			if ((tok.length == 1) && tok[0].isEmpty()) continue;
			if (tok[0].equals("prm")) {
				if (tok.length != 3) throw new IOException(badLine(mf, lnum));
				params.put(tok[1], Double.parseDouble(tok[2]));
			} else if (tok[0].equals("buf")) {
				if (tok.length < 5) throw new IOException(badLine(mf, lnum));
				Entry e = new Entry();
				e.name  = tok[1];
				e.dtype = tok[2];
				int nd  = Integer.parseInt(tok[3]);
				if (tok.length != (5 + nd)) throw new IOException(badLine(mf, lnum));
				e.dims = new int [nd];
				for (int i = 0; i < nd; i++) e.dims[i] = Integer.parseInt(tok[4 + i]);
				e.file = tok[4 + nd];
				bufs.put(e.name, e);
			} else {
				throw new IOException(badLine(mf, lnum));
			}
		}
	}

	private static String badLine(File mf, int lnum) {
		return "TestDataReader: bad manifest line " + lnum + " in " + mf.getPath();
	}

	public boolean has(String name)          { return bufs.containsKey(name); }
	public String [] getNames()              { return bufs.keySet().toArray(new String [0]); }
	public Entry getEntry(String name)       { return bufs.get(name); }
	public double param(String name, double dflt) {
		Double v = params.get(name);
		return (v == null) ? dflt : v;
	}
	public int paramInt(String name, int dflt) {
		return (int) Math.round(param(name, dflt));
	}
	public int [] dims(String name) {
		return entry(name).dims;
	}

	/** Load a buffer as float32 (must be dtype f32). */
	public float [] getFloats(String name) throws IOException {
		Entry e = entry(name);
		if (!e.dtype.equals("f32")) {
			throw new IOException("TestDataReader: '" + name + "' is " + e.dtype + ", not f32");
		}
		ByteBuffer bb = readRaw(e);
		float [] data = new float [bb.remaining() / 4];
		bb.asFloatBuffer().get(data);
		return data;
	}

	/** Load a buffer as int32 (must be dtype i32). */
	public int [] getInts(String name) throws IOException {
		Entry e = entry(name);
		if (!e.dtype.equals("i32")) {
			throw new IOException("TestDataReader: '" + name + "' is " + e.dtype + ", not i32");
		}
		ByteBuffer bb = readRaw(e);
		int [] data = new int [bb.remaining() / 4];
		bb.asIntBuffer().get(data);
		return data;
	}

	private Entry entry(String name) {
		Entry e = bufs.get(name);
		if (e == null) {
			throw new IllegalArgumentException("TestDataReader: no buffer '" + name +
					"' in " + dir.getPath());
		}
		return e;
	}

	private ByteBuffer readRaw(Entry e) throws IOException {
		byte [] raw = Files.readAllBytes(new File(dir, e.file).toPath());
		long expect = 4;
		for (int d : e.dims) expect *= d;
		if (raw.length != expect) {
			throw new IOException("TestDataReader: " + e.file + " is " + raw.length +
					" bytes, manifest dims say " + expect);
		}
		return ByteBuffer.wrap(raw).order(ByteOrder.LITTLE_ENDIAN);
	}
}
