import { getMatches } from "../file-matching.js";
import { parseProperties } from "../parsers/properties-parser.js";
import { defineSource } from "../source.js";
import { nonEmptyString, optionalUrl } from "../utilities/schema-primitives.js";
import { splitCommaSeparated } from "../utilities/template-helpers.js";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { z } from "zod";
//#region src/lib/sources/processing-library-properties.ts
const processingLibraryPropertiesAuthorEntrySchema = z.object({
	name: z.string(),
	url: z.string().optional()
});
/** Canonical Processing library categories. */
const CANONICAL_CATEGORIES = [
	"3D",
	"Animation",
	"Compilations",
	"Data",
	"Fabrication",
	"Geometry",
	"GUI",
	"Hardware",
	"I/O",
	"Language",
	"Math",
	"Simulation",
	"Sound",
	"Utilities",
	"Typography",
	"Video & Vision"
];
/** Map from letters-only lowercase to canonical category form. */
const CATEGORY_MAP = new Map(CANONICAL_CATEGORIES.map((cat) => [cat.replaceAll(/[^a-z]/gi, "").toLowerCase(), cat]));
CATEGORY_MAP.set("3d", "3D");
const processingLibraryPropertiesSchema = z.object({
	authors: z.array(processingLibraryPropertiesAuthorEntrySchema),
	categories: z.array(z.enum(CANONICAL_CATEGORIES)),
	download: optionalUrl,
	id: nonEmptyString,
	maxRevision: z.number(),
	minRevision: z.number(),
	name: nonEmptyString,
	paragraph: nonEmptyString,
	prettyVersion: nonEmptyString,
	raw: z.record(z.string(), z.string()),
	sentence: nonEmptyString,
	type: nonEmptyString,
	url: optionalUrl,
	version: z.number()
});
/**
* Parse a Processing `library.properties` content string into a structured object.
*/
function parse(content) {
	const raw = parseProperties(content);
	const versionRaw = get(raw, "version") ?? "0";
	const versionParsed = Number.parseInt(versionRaw, 10);
	const prettyVersionRaw = get(raw, "prettyVersion");
	const prettyVersion = prettyVersionRaw ? stripInlineComment(prettyVersionRaw) : void 0;
	const minRevisionRaw = get(raw, "minRevision");
	const minParsed = minRevisionRaw ? Number.parseInt(minRevisionRaw, 10) : 0;
	const maxRevisionRaw = get(raw, "maxRevision");
	const maxParsed = maxRevisionRaw ? Number.parseInt(maxRevisionRaw, 10) : 0;
	return processingLibraryPropertiesSchema.parse({
		authors: parseAuthors(get(raw, "authors") ?? get(raw, "authorList") ?? ""),
		categories: parseCategories(get(raw, "categories") ?? get(raw, "category") ?? ""),
		download: nonEmpty(get(raw, "download")),
		id: nonEmpty(get(raw, "id")),
		maxRevision: Number.isNaN(maxParsed) ? 0 : maxParsed,
		minRevision: Number.isNaN(minParsed) ? 0 : minParsed,
		name: nonEmpty(get(raw, "name")),
		paragraph: nonEmpty(get(raw, "paragraph")),
		prettyVersion: nonEmpty(prettyVersion),
		raw,
		sentence: nonEmpty(get(raw, "sentence")),
		type: nonEmpty(get(raw, "type")),
		url: nonEmpty(unescapeUrl(get(raw, "url") ?? "")),
		version: Number.isNaN(versionParsed) ? 0 : versionParsed
	});
}
/** Helper to get a value from the raw record. */
function get(raw, key) {
	return raw[key];
}
/** Return a trimmed string, or undefined if empty/whitespace-only. */
function nonEmpty(value) {
	if (value === void 0) return void 0;
	const trimmed = value.trim();
	return trimmed.length > 0 ? trimmed : void 0;
}
/**
* Strip trailing inline comments from a value.
* Matches the pattern ` # comment text` (space-hash-space).
*/
function stripInlineComment(value) {
	const index = value.indexOf(" # ");
	return index === -1 ? value : value.slice(0, index).trim();
}
/**
* Unescape backslash-escaped colons in URLs.
* Some Processing fixtures use `https\://` instead of `https://`.
*/
function unescapeUrl(value) {
	return value.replaceAll(String.raw`\:`, ":");
}
/**
* Parse a Processing authors/authorList value into AuthorEntry[].
* Preserves original order of appearance.
*/
function parseAuthors(value) {
	const trimmed = value.trim();
	if (trimmed.length === 0) return [];
	const results = [];
	let cursor = 0;
	while (cursor < trimmed.length) {
		const bracketStart = trimmed.indexOf("[", cursor);
		if (bracketStart === -1) {
			addPlainAuthors(trimmed.slice(cursor), results);
			break;
		}
		if (bracketStart > cursor) addPlainAuthors(trimmed.slice(cursor, bracketStart), results);
		const bracketEnd = trimmed.indexOf("]", bracketStart);
		if (bracketEnd === -1) {
			addPlainAuthors(trimmed.slice(bracketStart), results);
			break;
		}
		const parenStart = trimmed.indexOf("(", bracketEnd);
		if (parenStart === -1 || parenStart !== bracketEnd + 1) {
			addPlainAuthors(trimmed.slice(bracketStart, bracketEnd + 1), results);
			cursor = bracketEnd + 1;
			continue;
		}
		const parenEnd = trimmed.indexOf(")", parenStart);
		if (parenEnd === -1) {
			addPlainAuthors(trimmed.slice(bracketStart), results);
			break;
		}
		const namesPart = trimmed.slice(bracketStart + 1, bracketEnd).trim();
		const url = nonEmpty(unescapeUrl(trimmed.slice(parenStart + 1, parenEnd).trim()));
		const names = splitCommaSeparated(namesPart);
		for (const name of names) results.push({
			name,
			url
		});
		cursor = parenEnd + 1;
	}
	return results;
}
/**
* Split plain text on ` and `, `,`, `&` to extract author names.
* Filters out bare "others" entries.
*/
function addPlainAuthors(text, results) {
	const parts = text.split(/\band\b|,|&/).map((s) => s.trim()).filter((s) => s.length > 0 && s.toLowerCase() !== "others");
	for (const name of parts) results.push({
		name,
		url: void 0
	});
}
/**
* Parse and normalize a comma-separated categories value.
*/
function parseCategories(value) {
	const trimmed = value.trim();
	if (trimmed.length === 0) return [];
	const results = [];
	for (const part of trimmed.split(",")) {
		const stripped = part.trim().replaceAll(/^"|"$/g, "").trim();
		if (stripped.length === 0) continue;
		const key = stripped.replaceAll(/[^a-z0-9]/gi, "").toLowerCase();
		if (key.length === 0) continue;
		const canonical = CATEGORY_MAP.get(key);
		if (canonical && !results.includes(canonical)) results.push(canonical);
	}
	return results;
}
/** Processing-specific fields that distinguish from Arduino. */
const PROCESSING_SPECIFIC_FIELDS = new Set([
	"authorList",
	"authors",
	"dependencies",
	"minrevision",
	"prettyversion"
]);
/** Arduino-exclusive fields that rule out Processing. */
const ARDUINO_EXCLUSIVE_FIELDS = new Set([
	"architectures",
	"depends",
	"maintainer"
]);
/**
* Validate that a library.properties file is Processing (not Arduino).
*/
function isProcessingLibraryProperties(content) {
	const raw = parseProperties(content);
	const keys = new Set(Object.keys(raw).map((k) => k.toLowerCase()));
	if (!keys.has("name") || !keys.has("version")) return false;
	for (const field of ARDUINO_EXCLUSIVE_FIELDS) if (keys.has(field)) return false;
	for (const field of PROCESSING_SPECIFIC_FIELDS) if (keys.has(field)) return true;
	return false;
}
const processingLibraryPropertiesSource = defineSource({
	async discover(context) {
		return getMatches(context.options, ["library.properties"]);
	},
	key: "processingLibraryProperties",
	async parse(input, context) {
		const content = await readFile(resolve(context.options.path, input), "utf8");
		if (!isProcessingLibraryProperties(content)) return;
		return {
			data: parse(content),
			source: input
		};
	},
	phase: 1
});
//#endregion
export { processingLibraryPropertiesSource };
