// ParVagues sample watcher — the SuperCollider half.
//
// Installs an OSC responder so `tools/sample-watcher.py` can register a newly
// dropped sample pack into the RUNNING rig. No reboot, no interruption: with
// lazy loading on, registering a bank reads WAV headers only, and the audio is
// read on first hit like any other bank.
//
// Loaded from start_and_midi.scd at boot, and safe to evaluate on its own mid
// session (OSCdef replaces its own key, so re-evaluating never stacks
// responders).
//
//   /pv/ping                     -> replies /pv/pong <nBanks>
//   /pv/loadBank <path> [<naming>]  -> loads one folder as a bank
//                                      naming: "basename" (default) | "nodash"
//
// SAFETY, in order of how badly each one bites:
//
// 1. doNotReadYet is FORCED BACK TO TRUE around every load. It is the only
//    gate on the on-demand read path — with it false, a freshly registered
//    bank is never read at all. Not slow: permanently silent. That cost two
//    evenings in July 2026.
// 2. Paths are refused unless they sit under a known sample root, so a stray
//    OSC packet cannot make the rig read arbitrary parts of the disk.
// 3. The responder REPLIES. A watcher that fires into the void and reports
//    success is worse than no watcher, so every load is acknowledged with the
//    file count SuperCollider actually saw — which is the only number that
//    proves the bank is playable.

(
~pvSampleRoots = [
	"/home/pln/.local/share/SuperCollider/downloaded-quarks/Dirt-Samples",
	"/home/pln/Work/Sound/Samples/extra",
	"/home/pln/Work/Sound/Samples/tidal-drum-machines/machines"
];

~pvBankCount = { ~dirt.soundLibrary.buffers.size };

OSCdef(\pvPing, { |msg, time, addr|
	addr.sendMsg('/pv/pong', ~pvBankCount.value);
}, '/pv/ping');

OSCdef(\pvLoadBank, { |msg, time, addr|
	var path   = msg[1].asString;
	var naming = if(msg.size > 2) { msg[2].asString } { "basename" };
	var name, ok, before, after;

	ok = ~pvSampleRoots.any { |r| path.beginsWith(r) };

	if(ok.not) {
		"pv: REFUSED (outside the sample roots): %".format(path).warn;
		addr.sendMsg('/pv/loaded', path, -1, "outside sample roots");
	} {
		if(File.exists(path).not) {
			"pv: REFUSED (no such folder): %".format(path).warn;
			addr.sendMsg('/pv/loaded', path, -1, "no such folder");
		} {
			// Do NOT try to derive the bank name from the path. PathName's
			// folderName returns the PARENT for a directory path, and the
			// default namingFunction is the `basename` PRIMITIVE applied to a
			// path with a trailing separator — behaviour we would be guessing
			// at. Ask the library what changed instead: whatever key appears
			// IS the bank name, by construction.
			before = ~dirt.soundLibrary.buffers.keys.copy;

			// Force lazy mode for the duration. See safety note 1.
			~dirt.doNotReadYet = true;
			if(naming == "nodash") {
				~dirt.loadSoundFiles(path,
					namingFunction: { |x| x.basename.replace("-", "") });
			} {
				~dirt.loadSoundFiles(path);
			};
			~dirt.doNotReadYet = true;

			after = ~dirt.soundLibrary.buffers.keys.select { |k|
				before.includes(k).not
			};

			if(after.size == 1) {
				name = after.asArray[0];
				~pvLast = ~dirt.soundLibrary.buffers.at(name).size;
				"pv: loaded bank '%' — % files".format(name, ~pvLast).postln;
				addr.sendMsg('/pv/loaded', path, ~pvLast, name.asString);
			} {
				// Zero means the name already existed and this REPLACED it —
				// which the watcher refuses to do on purpose, so say so rather
				// than reporting a success nobody asked for.
				"pv: % added % new bank name(s) — expected exactly 1".format(
					path, after.size).warn;
				addr.sendMsg('/pv/loaded', path, -1,
					"added % names, expected 1".format(after.size));
			};
		};
	};
}, '/pv/loadBank');

"pv: sample watcher responder armed (/pv/ping, /pv/loadBank) — % banks".format(
	~pvBankCount.value).postln;
)
