let fs = require('fs');

let commands = [];
let cmdindex = {};	// name -> [cmd docs...], keeps duplicates/variants
let channels = [];
let chanindex = {};
let ios = [];
let ioindex = {};
let drvs = [];
let drvindex = {};
let drvdefines = {};	// drv.name -> [#if lines], keeps duplicates/variants
let flags = [];
let flagindex = {};
let cnsts = [];
let cnstindex = {};
let evnts = [];

let inFile = 0;

let oldStyleFlagTitles = [];

function makeDriverDefineText(defineLines) {
	// defineLines: array of "define" strings - generated by def2text.
	if (!defineLines || defineLines.length < 1) {
		return '';
	}
	let uniq = [];
	for (let i = 0; i < defineLines.length; i++) {
		let d = defineLines[i];
		if (!d) continue;
		if (uniq.indexOf(d) < 0) uniq.push(d);
	}
	if (uniq.length < 1) return '';
//console.error(uniq);
	let joined = uniq.map(x=>'"<b>'+x+'</b>"').join(' or ');
	return 'Enabled by defining ' + joined + ' for your platform in [obk_config.h](https://github.com/openshwprojects/OpenBK7231T_App/blob/main/src/obk_config.h) ';
}

// "extract" C macros matching #define statements 
//
//    even though now there's only "#if" or "#if defined" it handles 
//    I tried to make it more general and handle not only
//    #if, #ifdef, #ifndef, and #if defined(...) conditions
//    but also more complex conditions with && or || or ! 
//
// Actually converts an #if line to an array of condition strings, which can later be converted to a string by 
// the above function "makeDriverDefineText()"
//
// #if A						→ Returns array: ["A"]
// #if defined(B)					→ Returns array: ["B"]
// #if A && B						→ Returns array: ["A && B"]
// #if A || B || (C && !D)				→ Returns array: ["A", "B", "(C && !D)"]
// #ifndef X						→ Returns array: ["!X"]
// #ifdef DEBUG						→ Returns array: ["DEBUG"]
// #if !(A && B) || (C && !D)				→ Returns array: ["!(A && B)", "(C && !D)"]
// #if defined(FEATURE_A) || defined(FEATURE_B)		→ Returns array: ["FEATURE_A", "FEATURE_B"]
function def2text(defline) {
    let line = defline.trim();

    // Handle #ifdef
    if (line.startsWith('#ifdef ')) return [line.substring(7).trim()];
    // Handle #ifndef
    if (line.startsWith('#ifndef ')) return ['!' + line.substring(8).trim()];

    // Handle #if
    if (line.startsWith('#if ')) {
        let condition = line.substring(4).trim();

        // Replace "defined(X)" or "defined X" with "X"
        condition = condition.replace(/defined\s*\(?\s*(\w+)\s*\)?/g, '$1');

        // Split by || while respecting parentheses
        let orParts = splitByOr(condition);

        return orParts.length > 1 ? orParts.map(p => p.trim()) : [condition];
    }
    return [line];
}
// Helper for handling defines containing multiple checks combined with "||"
// they shall be treated like single conditions (will be concatenated later) 
// It splits a string by || while respecting parentheses
function splitByOr(str) {
    let parts = [], current = '', depth = 0;

    for (let i = 0; i < str.length; i++) {
        let char = str[i];

        if (char === '(') depth++;
        else if (char === ')') depth--;
        else if (char === '|' && str[i+1] === '|' && depth === 0) {
            parts.push(current.trim());
            current = '';
            i++; // Skip the second |
            continue;
        }
        current += char;
    }

    if (current.trim()) parts.push(current.trim());

    return parts.length > 0 ? parts : [str];
}




function mytrim(text) {
	text = text.trim();
	if (text.startsWith('"')) text = text.slice(1, -1);
	return text;
}

function writeIfChanged(fname, data) {
	if (fs.existsSync(fname)) {
		const existing = fs.readFileSync(fname, "utf8");
		if (existing.replace(/\r/g, "") === data.replace(/\r/g, "")) {
			// console.log("Skipping " + fname + " - no changes");
			return;
		}
	}
	fs.writeFileSync(fname, data);
	console.log('wrote ' + fname);
}


function getFolder(name, cb) {
	//console.error('dir:'+name);

	let list = fs.readdirSync(name);
	for (let i = 0; i < list.length; i++) {
		let file = name + '/' + list[i];
		let s = fs.statSync(file);

		if (s.isDirectory()) {
			getFolder(file, cb);
		} else {
			let sourceFile = file.toLowerCase().endsWith('.c') || file.toLowerCase().endsWith('.cpp');
			let headerFile = file.toLowerCase().endsWith('.h');
			if (sourceFile || headerFile) {
				inFile++;
				//console.error('file:'+file);
				let data = fs.readFileSync(file);
				let text = data.toString('utf-8');
				let lines = text.split('\n');
				let newlines = [];
				let modified = 0;
				for (let i = 0; i < lines.length; i++) {
					let line = lines[i].trim();
					// like CMD_RegisterCommand("SetChannel", "", CMD_SetChannel, "qqqqq0", NULL);

					//parse enum starting with "typedef enum channelType_e {"
					/*
					if (headerFile && line.startsWith('typedef enum channelType_e {')) {
						newlines.push(lines[i]);
						let j;
						for (j = i; j < lines.length; j++) {
							let line2raw = lines[j];
							let line2 = line2raw.trim();
							if (line2.startsWith('//chandetail:')) {
								let commentlines = [];
								let j2;
								for (j2 = j; j2 < lines.length; j2++) {
									let l = lines[j2].trim();
									if (l.startsWith('//chandetail:')) {
										l = l.slice('//chandetail:'.length);
										commentlines.push(l);
										newlines.push(lines[j2]);
									} else {
										break;
									}
								}
								// move our parsing forward to skip all found
								j = j2;
								let json = commentlines.join('\n');
								try {
									let chan = JSON.parse(json);
									if (chanindex[chan.name]) {
										console.error('duplicate command docs at file: ' + file + ' line: ' + line);
										console.error(line);
									} else {
										channels.push(chan);
										console.log("Loading existing doc for " + chan.name);
										chanindex[chan.name] = chan;
									}
								} catch (e) {
									console.error('error in json at file: ' + file + ' line: ' + line + ' er ' + e);
									console.error(json);
								}
							} else if (line2.startsWith('//')) {
								newlines.push(line2);
								continue;
							} else if (line2.startsWith("ChType_")) {
								let chanName = line2.substr("ChType_".length);
								chanName = chanName.replace(/,$/, "");

								console.log("Channel type name: " + chanName);

								let chan = {
									name: chanName,
									title: chanName,
									descr: mytrim("TODO"),
									enum: "ChType_"+chanName,
									file: file.slice(6),
									driver: mytrim(""),
								};


								if (!chanindex[chan.name]) {
									console.log("Creating doc for " + chan.name);
									// it did not have a doc line before
									let json = JSON.stringify(chan);
									// insert CR at "fn":
									json = json.split('"title":');
									json = json.join('\n"title":');
									json = json.split('"descr":');
									json = json.join('\n"descr":');
									json = json.split('"enum":');
									json = json.join('\n"enum":');
									json = json.split('"file":');
									json = json.join('\n"file":');
									json = json.split('"driver":');
									json = json.join('\n"driver":');
									let jsonlines = json.split('\n');
									for (let j = 0; j < jsonlines.length; j++) {
										jsonlines[j] = '\t//chandetail:' + jsonlines[j];
									}
									newlines.push(...jsonlines);
									modified++;
									channels.push(chan);
									chanindex[chan.name] = chan;
								}
								newlines.push(lines[j]);
							}
							if (line2.endsWith('} channelType_t;')) {
								//newlines.push(line2raw);
								break;
							}
						}
						i = j;
					}
					*/
					if (headerFile && line.startsWith('typedef enum channelType_e {')) {
						newlines.push(lines[i]);
						let j;
						for (j = i; j < lines.length; j++) {
							let line2raw = lines[j];
							let line2 = line2raw.trim();
							if (line2.startsWith('//chandetail:')) {
								let commentlines = [];
								let j2;
								for (j2 = j; j2 < lines.length; j2++) {
									let l = lines[j2].trim();
									if (l.startsWith('//chandetail:')) {
										l = l.slice('//chandetail:'.length);
										commentlines.push(l);
										newlines.push(lines[j2]);
									} else {
										break;
									}
								}
								// move our parsing forward to skip all found
								// we are allready BEHIND comments when we used break, 
								// so we need to skip to j2-1 to handle the line in next loop 
								j = j2 - 1;
								let json = commentlines.join('\n');
								try {
									let chan = JSON.parse(json);
									if (chanindex[chan.name]) {
										console.error('duplicate channel docs at file: ' + file + ' line: ' + line);
										console.error(line);
									} else {
										channels.push(chan);
										//console.log("Loading existing doc for " + chan.name);
										chanindex[chan.name] = chan;
									}
								} catch (e) {
									console.error('error in json at file: ' + file + ' line: ' + line + ' er ' + e);
									console.error(json);
								}
							} else if (line2.startsWith('//')) {
								newlines.push(line2);
								continue;
							} else if (line2.startsWith("ChType_")) {
								let chanName = line2.substr("ChType_".length);
								chanName = chanName.replace(/,$/, "");

								console.log("Channel type name: " + chanName);

								let chan = {
									name: chanName,
									title: chanName,
									descr: mytrim("TODO"),
									enum: "ChType_" + chanName,
									file: file.slice(6),
									driver: mytrim(""),
								};


								if (!chanindex[chan.name]) {
									console.log("Creating doc for " + chan.name);
									// it did not have a doc line before
									let json = JSON.stringify(chan);
									// insert CR at "fn":
									json = json.split('"title":');
									json = json.join('\n"title":');
									json = json.split('"descr":');
									json = json.join('\n"descr":');
									json = json.split('"enum":');
									json = json.join('\n"enum":');
									json = json.split('"file":');
									json = json.join('\n"file":');
									json = json.split('"driver":');
									json = json.join('\n"driver":');
									let jsonlines = json.split('\n');
									for (let j = 0; j < jsonlines.length; j++) {
										jsonlines[j] = '\t//chandetail:' + jsonlines[j];
									}
									newlines.push(...jsonlines);
									modified++;
									channels.push(chan);
									chanindex[chan.name] = chan;
								}
								newlines.push(lines[j]);
							}
							if (line2.endsWith('} channelType_t;')) {
								//newlines.push(line2raw);
								break;
							}
						}
						i = j;
					}
					if (sourceFile && line.startsWith('const char* g_obk_flagNames[] = {')) {
						newlines.push(lines[i]);
						let j;
						for (j = i; j < lines.length; j++) {
							let line2raw = lines[j];
							let line2 = line2raw.trim();
							// console.log("line2 " + line2);
							if (line2.startsWith('//')) {
								newlines.push(line2);
								continue;
							} else if (line2.startsWith("\"[")) {
								let flagTitle = line2.trim().replace(/^"|"$/g, '');
								//   console.log("Old style flag title: " + flagTitle);
								if (flagTitle.endsWith('",')) {
									flagTitle = flagTitle.slice(0, -2);
								}
								if (flagTitle.endsWith(',')) {
									flagTitle = flagTitle.slice(0, -1);
								}
								oldStyleFlagTitles.push(flagTitle);

								newlines.push(lines[j]);
							}
							if (line2.endsWith('};')) {
								//newlines.push(line2raw);
								break;
							}
						}
						i = j;
					}
					if (headerFile && line.startsWith('#define')) {
						const parts = line.split(/[\s\t]+/);

						if (parts && parts.length > 2) {
							// The first part is the #define directive, which can be ignored
							const flagName = parts[1]; // OBK_FLAG_MQTT_BROADCASTLEDPARAMSTOGETHER
							const flagValue = parts[2]; // 0
							// console.log("Define " + flagName);
							if (flagName.startsWith("OBK_FLAG_")) {

								let flag = {
									index: flagValue,
									enum: flagName,
									title: "todo",
									file: file.slice(6),
								};
								if (!flagindex[flag.index]) {
									// console.log("Creating flag " + flag.enum + " with index " + flag.index);
									flags.push(flag);
									flagindex[flag.index] = flag;
								}
							}
						}
					}
					// test for events (in cmd_eventHandlers.c)
					if (sourceFile && line.startsWith('int EVENT_ParseEventName')) {
						let j;
						let event;
						let foundif = "";
						for (j = i; j < lines.length; j++) {
							let line2raw = lines[j];
							let line2 = line2raw.trim();

							if (line2.endsWith('return CMD_EVENT_NONE;')) {
								break;
							}
							if (line2.startsWith("#if ")) {
								foundif = line2.replace(/^#if /, "");
							}
							if (line2.startsWith("#endif")) {
								foundif = "";
							}
							if (/if.*str[ni]*cmp/.test(line2)) {
								let Ename = line2.replace(/if.*str[ni]*cmp\(s,[ ]*"/, "").replace(/"[^"]*$/, '');
								if (Ename == 'channel') {
									Ename = Ename.replace("channel", "channel<X>"); // special case for channels and its number (...atoi ...)
								}
								let l = lines[++j].trim();
								i++;
								let CMD = l.replace(/^.*return /, "").replace(";", "");
								if (CMD.startsWith('CMD_EVENT_CHANGE_CHANNEL0')) {
									CMD = CMD.replace(/[ ]*\+[ ]*atoi.*/, '<X>'); // special case for channels and its number (...atoi ...)
								}
								let event = {
									name: Ename,
									CMD: CMD,
								};

								if (foundif != "") {
									event.ifdef = foundif;
								}
								evnts.push(event);
							}
						}
						i += j;
					}
					if (headerFile && line.startsWith('typedef enum ioRole_e {')) {
						newlines.push(lines[i]);
						let j;
						for (j = i; j < lines.length; j++) {
							let line2raw = lines[j];
							let line2 = line2raw.trim();
							if (line2.startsWith('//iodetail:')) {
								let commentlines = [];
								let j2;
								for (j2 = j; j2 < lines.length; j2++) {
									let l = lines[j2].trim();
									if (l.startsWith('//iodetail:')) {
										l = l.slice('//iodetail:'.length);
										commentlines.push(l);
										newlines.push(lines[j2]);
									} else {
										break;
									}
								}
								// move our parsing forward to skip all found
								// we are allready BEHIND comments when we used break, 
								// so we need to skip to j2-1 to handle the line in next loop 
								j = j2 - 1;
								let json = commentlines.join('\n');
								try {
									let io = JSON.parse(json);
									if (ioindex[io.name]) {
										console.error('duplicate io docs at file: ' + file + ' line: ' + line);
										console.error(line);
									} else {
										ios.push(io);
										ioindex[io.name] = io;
									}
								} catch (e) {
									console.error('error in json at file: ' + file + ' line: ' + line + ' er ' + e);
									console.error(json);
								}
							} else if (line2.startsWith('//')) {
								newlines.push(line2);
								continue;
							} else if (line2.startsWith("IOR_")) {
								let ioName = line2.substr("IOR_".length);
								ioName = ioName.replace(/,$/, "");

								console.log("ionel type name: " + ioName);

								let io = {
									name: ioName,
									title: ioName,
									descr: mytrim("TODO"),
									enum: "IOR_" + ioName,
									file: file.slice(6),
									driver: mytrim(""),
								};


								if (!ioindex[io.name]) {
									// it did not have a doc line before
									let json = JSON.stringify(io);
									// insert CR at "fn":
									json = json.split('"title":');
									json = json.join('\n"title":');
									json = json.split('"descr":');
									json = json.join('\n"descr":');
									json = json.split('"enum":');
									json = json.join('\n"enum":');
									json = json.split('"file":');
									json = json.join('\n"file":');
									json = json.split('"driver":');
									json = json.join('\n"driver":');
									let jsonlines = json.split('\n');
									for (let j = 0; j < jsonlines.length; j++) {
										jsonlines[j] = '\t//iodetail:' + jsonlines[j];
									}
									newlines.push(...jsonlines);
									modified++;
									ios.push(io);
									ioindex[io.name] = io;
								}
								newlines.push(lines[j]);
							}
							if (line2.endsWith('} ioRole_t;')) {
								//newlines.push(line2raw);
								break;
							}
						}
						i = j;
					}
					if (sourceFile && line.startsWith('static driver_t g_drivers[] = {')) {
						newlines.push(lines[i]);
						let j;
						let lasthash = "nothing yet";
						for (j = i; j < lines.length; j++) {
							let line2raw = lines[j];
							let line2 = line2raw.trim();
							if (line2.startsWith('#if')) {
								// try finding #if ENABLE_DRIVER_XY so we can use it in case of a duplicate driver
								lasthash = line2;
							}
							if (line2.startsWith('//drvdetail:')) {
								let commentlines = [];
								let j2;
								for (j2 = j; j2 < lines.length; j2++) {
									let l = lines[j2].trim();
									if (l.startsWith('//drvdetail:')) {
										l = l.slice('//drvdetail:'.length);
										commentlines.push(l);
										newlines.push(lines[j2]);
									} else {
										break;
									}
								}
								// move our parsing forward to skip all found
								// we are allready BEHIND comments when we used break, 
								// so we need to skip to j2-1 to handle the line in next loop 
								j = j2 - 1;
								let json = commentlines.join('\n');
								try {
									let drv = JSON.parse(json);
									if (drvindex[drv.name]) {
										console.error('duplicate driver docs (in "' + line + '") for drv.name="' + drv.name + '" at file: ' + file + '  --  actual line:' + line2);
										console.error('\tlast "#if" statement: "' + lasthash + '"' + '\n\tfirst defined with "#if" statement: "' + drvdefines[drv.name] + '"');
										// Compare without mutating the stored object (otherwise "define" can get deleted and leak as "undefined" into docs).
										let tmpcmd = Object.assign({}, drvindex[drv.name]);
										delete tmpcmd.define;
										let drvNoDefine = Object.assign({}, drv);
										delete drvNoDefine.define;
										if (JSON.stringify(tmpcmd) == JSON.stringify(drvNoDefine)) {
											console.error('\tshould be safe to ignore, because documentation is equal!');
											// Add new defines, so the docs remain accurate.
											if (!drvdefines[drv.name]) drvdefines[drv.name] = [];	// this should never happen, but just in case ...
											drvdefines[drv.name].push(...def2text(lasthash));	// makeDriverDefineText will take care of possible duplicates, so just add all defines
											drvindex[drv.name].define = makeDriverDefineText(drvdefines[drv.name]);
										} else {
											console.error('\tFirst found:\n\t\t"' + JSON.stringify(tmpcmd).replace(/,\"/g, "\n\t\t\t\"") + '"\n\tactual:\n\t\t"' + JSON.stringify(drvNoDefine).replace(/,\"/g, "\n\t\t\t\"") + '"');
										}
										//console.error(line);
									} else { // so we found first/only driver
										drvdefines[drv.name] = [];
										drvdefines[drv.name].push(...def2text(lasthash));
										drv.define = makeDriverDefineText(drvdefines[drv.name]);
										drvs.push(drv);
										drvindex[drv.name] = drv;
									}
								} catch (e) {
									console.error('error in json at file: ' + file + ' line: ' + line + ' er ' + e);
									console.error(json);
								}
							} else if (line2.startsWith('//')) {
								newlines.push(line2raw);
								continue;
							} else if (line2.startsWith('#')) {
								newlines.push(line2);
								continue;
							} else {
								//  console.log("line2 name: " + line2);
								const regex = /"([^"]+)"/; // This regular expression matches anything between quotes, but excludes the quotes themselves
								const startQuote = line2.indexOf('"') + 1; // Find the index of the first double-quote and add 1 to skip over it
								const endQuote = line2.indexOf('"', startQuote); // Find the index of the second double-quote starting from the first double-quote index
								let drvName = line2.substring(startQuote, endQuote); // Extract the substring between the first and second double-quotes

								if (endQuote != -1) {
									// drvName = drvName.replace(/,$/, "");

									console.log("drv type name: " + drvName);

									let drv = {
										name: drvName,
										title: drvName,
										descr: mytrim("TODO"),
										requires: mytrim(""),
									};


									if (!drvindex[drv.name]) {
										// it did not have a doc line before
										let json = JSON.stringify(drv);
										// insert CR at "fn":
										json = json.split('"title":');
										json = json.join('\n"title":');
										json = json.split('"descr":');
										json = json.join('\n"descr":');
										json = json.split('"requires":');
										json = json.join('\n"requires":');
										let jsonlines = json.split('\n');
										for (let j = 0; j < jsonlines.length; j++) {
											jsonlines[j] = '\t//drvdetail:' + jsonlines[j];
										}
										newlines.push(...jsonlines);
										modified++;
										drvs.push(drv);
										drvindex[drv.name] = drv;
									}
									newlines.push(lines[j]);
									// we found a driver definition before, so if its not a "one liner" we already copied before
									// we need to copy the next lines, until we find the closing "},"
									if (!lines[j].trim().endsWith('},')) {
										let j2;
										for (j2 = j + 1; j2 < lines.length; j2++) {
											let l = lines[j2].trim();
											if (l.endsWith('},')) {
												newlines.push(lines[j2]);
												break;
											}
											else {
												newlines.push(lines[j2]);
											}
										}
										// move our parsing forward to skip all found
										// arguments,  
										// so we need to skip to j2-1 to handle the line in next loop 
										j = j2;
									}



								}
							}
							if (line2.endsWith('};')) {
								//newlines.push(line2raw);
								break;
							}
						}
						i = j;
					}

					if (sourceFile && line.startsWith('const constant_t g_constants[] = {')) {
						newlines.push(lines[i]);
						let j;
						for (j = i; j < lines.length; j++) {
							let line2raw = lines[j];
							let line2 = line2raw.trim();
							if (line2.startsWith('//cnstdetail:')) {
								let commentlines = [];
								let j2;
								for (j2 = j; j2 < lines.length; j2++) {
									let l = lines[j2].trim();
									if (l.startsWith('//cnstdetail:')) {
										l = l.slice('//cnstdetail:'.length);
										commentlines.push(l);
										newlines.push(lines[j2]);
									} else {
										break;
									}
								}
								// move our parsing forward to skip all found
								// we are allready BEHIND comments when we used break, 
								// so we need to skip to j2-1 to handle the line in next loop 
								j = j2 - 1;
								let json = commentlines.join('\n');
								try {
									let cnst = JSON.parse(json);
									if (cnstindex[cnst.name]) {
										console.error('duplicate constant docs at file: ' + file + ' line: ' + line);
										console.error(line);
									} else {
										cnsts.push(cnst);
										cnstindex[cnst.name] = cnst;
									}
								} catch (e) {
									console.error('error in json at file: ' + file + ' line: ' + line + ' er ' + e);
									console.error(json);
								}
							} else if (line2.startsWith('//')) {
								newlines.push(line2);
								continue;
							} else if (line2.startsWith('#')) {
								newlines.push(line2);
								continue;
							} else {
								//  console.log("line2 name: " + line2);
								const regex = /"([^"]+)"/; // This regular expression matches anything between quotes, but excludes the quotes themselves
								const startQuote = line2.indexOf('"') + 1; // Find the index of the first double-quote and add 1 to skip over it
								const endQuote = line2.indexOf('"', startQuote); // Find the index of the second double-quote starting from the first double-quote index
								let cnstName = line2.substring(startQuote, endQuote); // Extract the substring between the first and second double-quotes

								if (endQuote != -1) {
									// cnstName = cnstName.replace(/,$/, "");

									console.log("cnst type name: " + cnstName);

									let cnst = {
										name: cnstName,
										title: cnstName,
										descr: mytrim("TODO"),
										requires: mytrim(""),
									};


									if (!cnstindex[cnst.name]) {
										// it did not have a doc line before
										let json = JSON.stringify(cnst);
										// insert CR at "fn":
										json = json.split('"title":');
										json = json.join('\n"title":');
										json = json.split('"descr":');
										json = json.join('\n"descr":');
										json = json.split('"requires":');
										json = json.join('\n"requires":');
										let jsonlines = json.split('\n');
										for (let j = 0; j < jsonlines.length; j++) {
											jsonlines[j] = '\t//cnstdetail:' + jsonlines[j];
										}
										newlines.push(...jsonlines);
										modified++;
										cnsts.push(cnst);
										cnstindex[cnst.name] = cnst;
									}
									newlines.push(lines[j]);
								}
							}
							if (line2.endsWith('};')) {
								//newlines.push(line2raw);
								break;
							}
						}
						i = j;
					}
					if (sourceFile && line.startsWith('//cmddetail:')) {
						let commentlines = [];
						let j;
						for (j = i; j < lines.length; j++) {
							let l = lines[j].trim();
							if (l.startsWith('//cmddetail:')) {
								l = l.slice('//cmddetail:'.length);
								commentlines.push(l);
								newlines.push(lines[j]);
							} else {
								break;
							}
						}
						// move our parsing forward to skip all found
						i = j;
						let json = commentlines.join('\n');
						try {
							let cmd = JSON.parse(json);

							// sanity: verify declared file matches the file we actually parsed
							if (cmd.file !== file.slice(6)) {
								console.error('!!!! Posible wrong file location for command "' + cmd.name + '": found in file: "' + file.slice(6) + '" but claimes file: "' + cmd.file + '" - please verify! !!!!')
								console.error('\t Posible fix: sed -i \'\'' + (i - 3) + ',' + (i - 1) + ' { /cmddetail:\\"fn\\":\\"' + cmd.fn + '\"/ s%' + cmd.file + "%" + file.slice(6) + '%} \'\'  src/' + file.slice(6))
								console.error('\t test posible fix: sed -n \'\'' + (i - 3) + ',' + (i - 1) + ' {/cmddetail:\\"fn\\":\\"' + cmd.fn + '\"/ s%' + cmd.file + "%" + file.slice(6) + '% p }\'\'  src/' + file.slice(6))
							}

							if (cmdindex[cmd.name]) {
								console.error('duplicate command "' + cmd.name + '" docs at file: ' + file + ' line: ' + line + '\n\tfirst seen in "' + cmdindex[cmd.name].file + '"');

								// compare without mutating the first-seen object
								let tmp = Object.assign({}, cmdindex[cmd.name]);
								tmp.file = cmd.file;
								if (JSON.stringify(tmp) == JSON.stringify(cmd)) console.error('\tshould be safe to ignore, they are equal beside the file name!');
								else {
									console.error('\tFirst found:\n\t\t"' + JSON.stringify(cmdindex[cmd.name]).replace(/,\"/g, "\n\t\t\t\"") + '"\n\tactual:\n\t\t"' + JSON.stringify(cmd).replace(/,\"/g, "\n\t\t\t\""));
								}
								cmdindex[cmd.name].push(cmd);
							} else {
								// first time we've seen this command name
								cmdindex[cmd.name] =[cmd];
							}

							commands.push(cmd);
						} catch (e) {
							console.error('error in json at file: ' + file + ' line: ' + line + ' er ' + e);
							console.error(json);
						}
					}

					// i may have changed...
					line = lines[i].trim();

					if (sourceFile && line.startsWith('CMD_RegisterCommand(')) {
						line = line.slice('CMD_RegisterCommand('.length);
						parts = line.split(',');
						//cmddetail:{"name":"SetChannel", "args":"TODO", "fn":"CMD_SetChannel", "descr":"qqqqq0", "example":"", "file":"");
						//
						// CMD registration command line
						// CMD_RegisterCommand("SetChannel", CMD_SetChannel, NULL);


						let cmd = {
							name: mytrim(parts[0]),
							args: mytrim("TODO"),
							descr: mytrim(""),
							fn: mytrim(parts[1]),
							file: file.slice(6),
							requires: "",
							examples: "",
						};

						//if (cmd.descr !== 'NULL'){
						//    console.log('replace "'+cmd.descr+'" with NULL');
						//   lines[i] = lines[i].replace(', NULL, NULL);', ', NULL);');
						//   modified++;
						//}
						//
						// 20250908 only used once to fix broken auto-generated fn entries with "NULL);" entry
						//if (cmdindex[cmd.name] && cmdindex[cmd.name].fn == 'NULL);'){
						//    console.log('replace fn "'+cmdindex[cmd.name].fn+'" with ' + cmd.fn);
						//    console.log('newlines i-2 =' +newlines[i-2]);
						//    cmdindex[cmd.name].fn = cmd.fn;
						//    newlines[i-2] = newlines[i-2].replace('NULL);', cmd.fn);
						//    console.log('newlines i-2 =' +newlines[i-2]);
						//   modified++;
						//}

						// if there are multiple cmddetail entries for the same name, only compare
						// against the one that belongs to this file to avoid false positives
						let docSameFile = null;
						if (cmdindex[cmd.name]) {
							docSameFile = cmdindex[cmd.name].find(x => x.file === cmd.file);
						}
						if (docSameFile && docSameFile.fn !== cmd.fn) {
							console.log('!!!!  "' + cmd.name + '" in file ' + cmd.file + ' -- fn "' + docSameFile.fn + '"  != called function "' + cmd.fn + '"  !!!!');
							console.log('	 possible fix: sed -i \'\'' + (i - 3) + ',' + (i - 1) + ' { /cmddetail:\"fn\":\"' + docSameFile.fn + '\"/ s%' + docSameFile.fn + "%" + cmd.fn + '% }\'\'  src/' + file.slice(6));
							console.log('	 test possible fix: sed -n \'\'' + (i - 3) + ',' + (i - 1) + ' { /cmddetail:\"fn\":\"' + docSameFile.fn + '\"/ s%' + docSameFile.fn + "%" + cmd.fn + '% p }\'\'  src/' + file.slice(6));
						}

						if (!cmdindex[cmd.name] || cmdindex[cmd.name].length < 1) {
							// it did not have a doc line before
							let json = JSON.stringify(cmd);
							// insert CR at "fn":
							json = json.split('"descr":');
							json = json.join('\n"descr":');
							json = json.split('"fn":');
							json = json.join('\n"fn":');
							json = json.split('"examples":');
							json = json.join('\n"examples":');
							let jsonlines = json.split('\n');
							for (let j = 0; j < jsonlines.length; j++) {
								jsonlines[j] = '\t//cmddetail:' + jsonlines[j];
							}
							newlines.push(...jsonlines);
							modified++;
							commands.push(cmd);
							cmdindex[cmd.name] = [cmd];
						}
					}

					newlines.push(lines[i]);
				}
				if (modified) {
					let newdata = newlines.join('\n');
					try {
						// write new file as "proposal", so we can choose, if/how to change source file
						writeIfChanged(file + ".getcommands", newdata);
					} catch (e) {
						console.error('failed to update ' + file);
					}
				}

				inFile--;

				if (!inFile) {
					//if (cb) cb();
				}
			}
		}
	}
}

// match flag titles


console.log('starting');

function removeGetCommandsFiles(dir) {
	let list = fs.readdirSync(dir);
	for (let i = 0; i < list.length; i++) {
		let filename = list[i];
		let file = dir + '/' + filename;
		let s = fs.statSync(file);
		if (s.isDirectory()) {
			removeGetCommandsFiles(file);
		} else {
			if (filename.endsWith('.getcommands')) {
				fs.unlinkSync(file);
				console.log('Removed ' + file);
			}
		}
	}
}

removeGetCommandsFiles('./src');

getFolder('./src');


function readJSONFile(path) {
	console.log("Going to read " + path);
	const data = fs.readFileSync(path, 'utf8');
	const jsonData = JSON.parse(data);
	return jsonData;
}

let faq = readJSONFile("docs/json/faq.json");
let generic = readJSONFile("docs/json/generic.json");
let commandExamples = readJSONFile("docs/json/commandExamples.json");
let autoexecExamples = readJSONFile("docs/json/autoexecExamples.json");
let scriptExamples = readJSONFile("docs/json/scriptExamples.json");
let mqttTopics = readJSONFile("docs/json/mqttTopics.json");
let subpages = readJSONFile("docs/json/subpages.json");

for (let i = 0; i < scriptExamples.length; i++) {
	let path = "docs/" + scriptExamples[i].file;
	const data = fs.readFileSync(path, 'utf8');
	scriptExamples[i].fileText = data;
}
for (let i = 0; i < autoexecExamples.length; i++) {
	let path = "docs/" + autoexecExamples[i].file;
	const data = fs.readFileSync(path, 'utf8');
	autoexecExamples[i].fileText = data;
}

let faqmdshort =
	`# FAQ (Frequently Asked Questions)
Here is the latest, up to date FAQ.
This file was autogenerated by running 'node scripts/getcommands.js' in the repository.
All questions/answers were taken from json file.

`;
let commandExamplesmdshort =
	`# Console command examples
This file was autogenerated by running 'node scripts/getcommands.js' in the repository.
All command examples were taken from json file.

`;
let autoexecsmdshort =
	`# Example 'autoexec.bat' files


`;
let scriptsmdshort =
	`# Example script files


`;

let channelsmdshort =
	`# Channel Types
Here is the latest, up to date Channel Types.
This file was autogenerated by running 'node scripts/getcommands.js' in the repository.
All descriptions were taken from code.
Do not add anything here, as it will overwritten with next rebuild.
| ChannelType     |  Description  |
|:------------- | -------:|
`;

let iosmdshort =
	`# IO Pin Roles
Here is the latest, up to date IO Roles.
This file was autogenerated by running 'node scripts/getcommands.js' in the repository.
All descriptions were taken from code.
Do not add anything here, as it will overwritten with next rebuild.
| RoleName     |  Description  |
|:------------- | -------:|
`;


let flagsmdshort =
	`# Flags
Here is the latest, up to date Flags list.
This file was autogenerated by running 'node scripts/getcommands.js' in the repository.
All descriptions were taken from code.
Do not add anything here, as it will overwritten with next rebuild.
| ID |   Description  |
|:--| -------:|
`;
let driversmdshort =
	`# Drivers
Here is the latest, up to date drivers list.
This file was autogenerated by running 'node scripts/getcommands.js' in the repository.
All descriptions were taken from code.
Remember that some drivers might not be yet enabled on certain platforms,
but we can enable them for you per request. Some drivers might also be WIP.
Do not add anything here, as it will overwritten with next rebuild.
| Driver        | Description  |
|:------------- |:----- |
`;

let constantsmdshort =
	`# Constants (script variables)
Here is the latest, up to date constants list.
This file was autogenerated by running 'node scripts/getcommands.js' in the repository.
All descriptions were taken from code.
Constants can be accessed in commands, so things like setChannel 15 2*$CH14+5 can work.
Do not add anything here, as it will overwritten with next rebuild.
| Code        | Description  |
|:------------- | -----:|
`;
let mdshort =
	`# Commands
Here is the latest, up to date command list.
This file was autogenerated by running 'node scripts/getcommands.js' in the repository.
All descriptions were taken from code.<br><br>
Please remember that some commands might require starting a related driver first, so, for example, you must first do 'startDriver DoorSensor' and then 'DSEdge 1'.
Also remember that commands can be put in autoexec.bat to run at startup (see Web Application->LittleFS tab).
<br><br>
Do not add anything here, as it will overwritten with next rebuild.
| Command        | Arguments          | Description  |
|:------------- |:------------- |:----- |
`;

let mdlong =
	`# Commands
Here is the latest, up to date command list.
This file was autogenerated by running 'node scripts/getcommands.js' in the repository.
All descriptions were taken from code.
Do not add anything here, as it will overwritten with next rebuild.
| Command        | Arguments          | Description  | Location |
|:------------- |:-------------:|:----- |:------ |
`;

let mqttText =
	`# MQTT topics of published variables
Some MQTT variables are being published only at the startup, some are published periodically (if you enable "broadcast every N seconds" flag, default time is one minute, customizable with command mqtt_broadcastInterval), some are published only when a given value is changed. Below is the table of used publish topics (TODO: add full descriptions)

Hint: in HA, you can use MQTT wildcard to listen to multiple publishes. OBK_DEV_NAME/#

Publishes send by OBK device:
| Topic        | Sample Value          | Description  |
|:------------- |:------------- | -----:|
`;

let evntsmdshort =
	`# Events
Here is the latest, up to date documentation of possible events (Work in progress).
This file was autogenerated by running 'node scripts/getcommands.js' in the repository.
All descriptions were taken from code.
Do not add anything here, as it will overwritten with next rebuild.
| Event     |  Eventcmd  |
|:------------- |:------- |
`;
for (let i = 0; i < evnts.length; i++) {
	let ev = evnts[i];


	let textshort = `| ${ev.name.replace("<", "&lt;").replace(">", "&gt;")} | ${ev.CMD.replace("<", "&lt;").replace(">", "&gt;")}${ev.ifdef ? '\nonly if defined:\n' + ev.ifdef.replace(/\|/g, "\\|") : ''} |`;
	// allow multi-row entries in table entries.
	textshort = textshort.replace(/\n/g, '<br/>');
	evntsmdshort += textshort + '\n';
}

evntsmdshort += '\n';




function genReadMore(name) {
	let textmore = `See also [${name} on forum](https://www.elektroda.com/rtvforum/find.php?q=${name}).`;
	return textmore;
}
for (let i = 0; i < mqttTopics.publishes.length; i++) {
	let pub = mqttTopics.publishes[i];
	let textshort = `| ${pub.topic} | ${pub.example} | ${pub.description} |`;

	// allow multi-row entries in table entries.
	textshort = textshort.replace(/\n/g, '<br/>');

	mqttText += textshort;
	mqttText += '\n';
}

mqttText +=
	`

Publishes received by OBK device:
| Topic        | Sample Value          | Description  |
|:------------- |:------------- | -----:|
`;
let publishes_and_listens = mqttTopics.listens.concat(mqttTopics.publishes);
for (let i = 0; i < mqttTopics.listens.length; i++) {
	let pub = mqttTopics.listens[i];

	let textshort = `| ${pub.topic} | ${pub.example} | ${pub.description} |`;

	// allow multi-row entries in table entries.
	textshort = textshort.replace(/\n/g, '<br/>');

	mqttText += textshort;
	mqttText += '\n';
}



for (let i = 0; i < flags.length; i++) {
	let flag = flags[i];
	//console.log("Test flag " + i + " vs len " + oldStyleFlagTitles.length);
	if (i < oldStyleFlagTitles.length) {
		//console.log("Matched flag " + i);
		flag.descr = oldStyleFlagTitles[i];
	}
}

// Sort commands by Name alphabetically
commands.sort((a, b) => {
	if (a.name.toUpperCase() < b.name.toUpperCase())
		return -1;
	if (a.name.toUpperCase() > b.name.toUpperCase())
		return 1;
	return 0;
});
function formatDesc(descBasic) {
	if (descBasic != '' && !descBasic.endsWith('.')) {
		descBasic += '.';
	}
	if (descBasic.length > 0) {
		descBasic = descBasic.charAt(0).toUpperCase() + descBasic.slice(1);
	}
	return descBasic;
}
for (let i = 0; i < commands.length; i++) {
	/* like:
	| Command        | Arguments          | Description  |
	| ------------- |:-------------:| -----:|
	| setChannel     | [ChannelIndex][ChannelValue] | Sets a raw channel to given value. Relay channels are using 1 and 0 values. PWM channels are within [0,100] range. Do not use this for LED control, because there is a better and more advanced LED driver with dimming and configuration memory (remembers setting after on/off), LED driver commands has "led_" prefix. |
	| addChannel     | [ChannelIndex][ValueToAdd][ClampMin][ClampMax] | Ads a given value to the channel. Can be used to change PWM brightness. Clamp min and max arguments are optional. |
	*/
	let cmd = commands[i];


	let descMore = "<br/><br/>" + genReadMore(cmd.name);
	let descBasic = formatDesc(cmd.descr);
	let textshort = `| <b>${cmd.name}</b>${cmd.requires ? '<span style="font-size:0.8em;">\nwith <i>' + cmd.requires.replace(' (','</i>\n(')+'</span>' : ''} | ${cmd.args} | ${descBasic}${cmd.examples ? '<br/><br/>Example: ' + cmd.examples : ''}${descMore} |`;
	let textlong = `| <b>${cmd.name}</b>${cmd.requires ? '<span style="font-size:0.8em;">\nwith <i>' + cmd.requires.replace(' (','</i>\n(')+'</span>' : ''} | ${cmd.args}| ${descBasic}${cmd.examples ? '<br/><br/>Example: ' + cmd.examples : ''}${descMore} | File: ${cmd.file}\nFunction: ${cmd.fn} |`;

	// allow multi-row entries in table entries.
	textshort = textshort.replace(/\n/g, '<br/>');
	textlong = textlong.replace(/\n/g, '<br/>');

	mdshort += textshort;
	mdshort += '\n';
	mdlong += textlong;
	mdlong += '\n';
}

mdshort += '\n';
mdlong += '\n';


for (let i = 0; i < scriptExamples.length; i++) {
	let scr = scriptExamples[i];

	scriptsmdshort += "<b>" + scr.title + "</b>";
	scriptsmdshort += '\n';
	scriptsmdshort += '<br>';
	scriptsmdshort += "" + scr.description + "";
	scriptsmdshort += '\n';
	scriptsmdshort += '<br>';
	scriptsmdshort += 'Requirements:';
	scriptsmdshort += '\n';
	for (let j = 0; j < scr.requirements.length; j++) {
		scriptsmdshort += '- ' + scr.requirements[j] + '<br>';
		scriptsmdshort += '\n';
	}
	scriptsmdshort += '\n';
	scriptsmdshort += "```";
	scriptsmdshort += scr.fileText;
	scriptsmdshort += '\n';
	scriptsmdshort += "```";
	scriptsmdshort += '\n';

	scriptsmdshort += '\n';
	scriptsmdshort += '\n';
}
for (let i = 0; i < autoexecExamples.length; i++) {
	let exe = autoexecExamples[i];

	autoexecsmdshort += "" + exe.title + "";
	autoexecsmdshort += '\n';
	autoexecsmdshort += '<br>';
	autoexecsmdshort += '\n';
	autoexecsmdshort += "```c++";
	autoexecsmdshort += '\n';
	autoexecsmdshort += exe.fileText;
	autoexecsmdshort += '\n';
	autoexecsmdshort += "```";
	autoexecsmdshort += '\n';

	autoexecsmdshort += '\n';
	autoexecsmdshort += '\n';
}

for (let i = 0; i < faq.length; i++) {
	let q = faq[i];

	faqmdshort += "**Question:** *" + q.question + "*";
	faqmdshort += '<br>';
	faqmdshort += "**A:** " + q.answer;
	faqmdshort += '\n';

	faqmdshort += '\n';
	faqmdshort += '\n';
}

for (let i = 0; i < commandExamples.length; i++) {
	let ex = commandExamples[i];

	commandExamplesmdshort += "" + ex.description;
	commandExamplesmdshort += '\n';
	commandExamplesmdshort += '<br>';
	commandExamplesmdshort += "```" + ex.command + "```";
	commandExamplesmdshort += '<br>';
	commandExamplesmdshort += '\n';

	commandExamplesmdshort += '\n';
	commandExamplesmdshort += '\n';
	commandExamplesmdshort += '\n';
}

for (let i = 0; i < cnsts.length; i++) {
	let cn = cnsts[i];

	let textshort = `| ${cn.name} |  ${cn.descr} |`;

	// allow multi-row entries in table entries.
	textshort = textshort.replace(/\n/g, '<br/>');

	constantsmdshort += textshort;
	constantsmdshort += '\n';
}

for (let i = 0; i < drvs.length; i++) {
	let drv = drvs[i];

	let descMore = "<br/>" + genReadMore(drv.name);
	let descBasic = formatDesc(drv.descr);
	let textshort = `| ${drv.name} |  ${descBasic}\n${drv.define}(for Details see [here](https://www.elektroda.com/rtvforum/topic4033833.html)).${descMore} |`;

	// allow multi-row entries in table entries.
	textshort = textshort.replace(/\n/g, '<br/>');

	driversmdshort += textshort;
	driversmdshort += '\n';
}

for (let i = 0; i < channels.length; i++) {
	let chan = channels[i];

	let textshort = `| ${chan.name} |  ${chan.descr} |`;

	// allow multi-row entries in table entries.
	textshort = textshort.replace(/\n/g, '<br/>');

	channelsmdshort += textshort;
	channelsmdshort += '\n';
}

for (let i = 0; i < flags.length; i++) {
	let flag = flags[i];

	// let textshort = `| ${flag.index} | ${flag.enum} |  ${flag.descr} |`;
	let textshort = `| ${flag.index} | ${flag.descr} |`;

	// allow multi-row entries in table entries.
	textshort = textshort.replace(/\n/g, '<br/>');

	flagsmdshort += textshort;
	flagsmdshort += '\n';
}

for (let i = 0; i < ios.length; i++) {
	let io = ios[i];

	let textshort = `| ${io.name} | ${io.descr} |`;

	// allow multi-row entries in table entries.
	textshort = textshort.replace(/\n/g, '<br/>');

	iosmdshort += textshort;
	iosmdshort += '\n';
}

const dirPath = 'docs/json';
if (!fs.existsSync(dirPath)) {
	fs.mkdirSync(dirPath, { recursive: true });
}


let links = [];

function writeDocMD_Array(name, content, json, label, bWriteJSON, desc) {
	let fullName = "docs/" + name + ".md";

	writeIfChanged(fullName, content);

	if (json) {
		console.log('There are ' + json.length + " " + name);
	}
	if (json && bWriteJSON) {
		let jsonName = `${dirPath}/${name}.json`;
		writeIfChanged(jsonName, JSON.stringify(json, null, 2));
	}

	let link = {
		name: name,
		content: content,
		json: json,
		fullName: fullName,
		label: label,
		desc: desc
	};
	links.push(link);
}

function writeDocMD_Page(page) {
	let fullName = "docs/" + page.mdName + ".md";
	let pageText = "# " + page.topic + "\n" + page.content;
	writeIfChanged(fullName, pageText);

	page.fullMDPath = fullName;
}

writeDocMD_Array('ioRoles', iosmdshort, ios, "IO/Pin Roles", true, generic.pins);
writeDocMD_Array('flags', flagsmdshort, flags, "Flags", true, generic.flags);
writeDocMD_Array('drivers', driversmdshort, drvs, "Drivers", true, generic.drivers);
writeDocMD_Array('constants', constantsmdshort, cnsts, "Script constants", true, generic.constants);
writeDocMD_Array('channelTypes', channelsmdshort, channels, "Channel Types", true, generic.channels);
writeDocMD_Array('faq', faqmdshort, faq, "FAQ", false, generic.faq);
writeDocMD_Array('commands', mdshort, commands, "Console/Script commands", true, generic.commands);
writeDocMD_Array('commandExamples', commandExamplesmdshort, commandExamples, "Command Examples", false, generic.commandExamples);
writeDocMD_Array('autoexecExamples', autoexecsmdshort, autoexecExamples, "Autoexec.bat examples (configs)", false, generic.autoexecExamples);
writeDocMD_Array('mqttTopics', mqttText, publishes_and_listens, "MQTT Topics", false, generic.mqttTopics);
writeDocMD_Array('scriptExamples', scriptsmdshort, scriptExamples, "Script examples", false, generic.scriptExamples);
writeDocMD_Array('commands-extended', mdlong, commands, "Console/Script commands [Extended Edition]", false, "More details on commands.")
writeDocMD_Array('events', evntsmdshort, evnts, "Events", true, generic.events);

let links_md =
	`# Documentation
Here is the latest, up to date documentation.
This file was autogenerated by running 'node scripts/getcommands.js' in the repository.
The descriptions were taken from code and from the JSON files, and then converted to MarkDown.
Do not add anything here, as it will overwritten with next rebuild.`


links_md += `\n`
links_md += `\n`
links_md += `# Basic doc pages \n`

for (let i = 0; i < subpages.length; i++) {
	let page = subpages[i];
	writeDocMD_Page(page);
	let shortDesc = page.shortDesc;
	let base = "https://github.com/openshwprojects/OpenBK7231T_App/blob/main/";
	let url = base + page.fullMDPath;
	let textshort = `- [${page.topic}](${url}) - ${shortDesc}`;

	// allow multi-row entries in table entries.
	textshort = textshort.replace(/\n/g, '<br/>');

	links_md += textshort;
	links_md += '\n';
}

links_md += `\n`
links_md += `# Doc tables `

links_md += `
| Section        | Comment        |
|:------------- |------:|
`;


for (let i = 0; i < links.length; i++) {
	let link = links[i];
	let base = "https://github.com/openshwprojects/OpenBK7231T_App/blob/main/";
	let url = base + link.fullName;
	let total = link.json.length;
	let desc = link.desc;
	let textshort = `| [${link.label}](${url}) (${total} total) | ${desc} |`;

	// allow multi-row entries in table entries.
	textshort = textshort.replace(/\n/g, '<br/>');

	links_md += textshort;
	links_md += '\n';
}


writeDocMD_Array('README', links_md);
