var os = require("os"),
fs = require("fs"),
path = require("path"),
child_process = require("child_process"),
help = "node " + path.basename(__filename) + " node ffprobe.js i=clipboard|directory|file|file.txt \\"
+ os.EOL + "[rename_hevc] [convert_hevc] [rotate=90|180|270] \\"
+ os.EOL + "[max_probes=10] [max_converts=1] [max_rotates=10] \\"
+ os.EOL + "[h264_dir=\"directory\"] [h264_dir_only]"
+ os.EOL
+ os.EOL + "rename_hevc   - if video is HEVC then rename (%file_name%~hevc.mp4)"
+ os.EOL + "convert_hevc  - if video is HEVC then convert a H264 version (%file_name%~h264.mp4)"
+ os.EOL
+ os.EOL + "max_probes    - max amount of simultaneous file probes"
+ os.EOL + "max_converts  - max amount of simultaneous file converts"
+ os.EOL + "max_rotates   - max amount of simultaneous file rotates"
+ os.EOL
+ os.EOL + "h264_dir      - link H264 videos to given directory"
+ os.EOL + "h264_dir_only - any newly generated videos are saved only to given directory"
+ os.EOL + "              - a hard inode link is attempted, falls back to using symbolic",
file_tag_delimiter = "~",
file_tag_default = "",
file_tag_verify = "verify",
file_tag_hevc = "hevc",
file_tag_h264 = "h264",
file_tag_r90 = "r90",
file_tag_r180 = "r180",
file_tag_r270 = "r270",
file_tag_error = "error",
file_tags = new Set([file_tag_verify,file_tag_hevc,file_tag_h264]),
file_tags_rotate = new Set([file_tag_r90,file_tag_r180,file_tag_r270]),
rename_hevc = false,
convert_hevc = false,
rotate = false,
rotate_deg = 90,
rotate_tag = file_tag_r90,
h264_dir = false,
h264_dir_only = false,
//h264_directory = process.cwd() + path.sep + "download",
h264_directory = __dirname + path.sep + "h264",
probed_files = new Set(),
converted_files = new Set(),
rotated_files = new Set(),
files_to_probe = [],
files_to_convert = [],
files_to_rotate = [],
max_probes = 10,
max_converts = 1,
max_rotates = 10,
active_probes = 0,
active_converts = 0,
active_rotates = 0,
ffmpeg,
ffprobe,
ffmpeg_path,
ffprobe_path,
//ffmpeg_path = "ffmpeg",
//ffprobe_path = "ffprobe",
debug = false,
running = true;
function file_name(video, file_tag, alt_dir=false, name_base=false) {
	var file_name = (alt_dir ? h264_directory : video.dir) + path.sep
	+ (name_base ? video.name_base : video.name)
	+ (file_tag.length > 0 ? file_tag_delimiter + file_tag : "")
	+ video.ext;
	if (video.windows_long_path !== false) {
//		https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
//		file_name = "\\\\\?\\" + h264_directory + path.sep + file_name;
		file_name = path.toNamespacedPath(file_name);
	}
	return file_name;
}
function queue_probe_file(file, video, callback) {
	if (!running) return;
	if (active_probes >= max_probes) {
		files_to_probe.push(arguments);
	} else {
		active_probes += 1;
		probe_file(file, video, function(exists) {
			active_probes -= 1;
			if (files_to_probe.length > 0) queue_probe_file(...files_to_probe.shift());
			if (exists && callback !== undefined) callback();
		});
	}
}
function queue_convert_file(input_file, output_file, video, callback) {
	if (!running) return;
	if (active_converts >= max_converts) {
		files_to_convert.push(arguments);
	} else {
		active_converts += 1;
		video.file_tag = file_tag_h264;
		convert_file(input_file, output_file, video, function(exists) {
			active_converts -= 1;
			if (files_to_convert.length > 0) queue_convert_file(...files_to_convert.shift());
			if (exists && callback !== undefined) callback();
			if (exists) process_file(output_file);
		});
	}
}
function queue_rotate_file(input_file, output_file, video, callback) {
	if (!running) return;
	if (active_rotates >= max_rotates) {
		files_to_rotate.push(arguments);
	} else {
		active_rotates += 1;
		video.file_tag = file_tag_h264;
		rotate_file(input_file, output_file, video, function(exists) {
			active_rotates -= 1;
			if (files_to_rotate.length > 0) queue_rotate_file(...files_to_rotate.shift());
			if (exists && callback !== undefined) callback();
		});
	}
}
function probe_file(file, video, queue_next) {
	if (!running || probed_files.has(file)) {
		queue_next();
	} else {
		probed_files.add(file);
		if (debug) console.log("probe_file: " + file);
		var rename_error_file = function() {
			var error_file = file_name(video, file_tag_error);
			fs.rename(file = video.file, error_file, function(error) {
				if (error !== null) {
					console.error(error);
				} else {
					console.log("renamed: " + error_file);
				}
			});
		};
		ffprobe(file, {path: ffprobe_path}, function(error, response) {
			if (error !== null) {
				console.error(error);
				if (video.windows_long_path === false) {
					video.windows_long_path = true;
					queue_probe_file(path.toNamespacedPath(file), video);
				} else {
					video.windows_long_path = false;
					rename_error_file();
				}
			} else if (typeof response !== "object" || response === null
			|| typeof response.streams !== "object" || response.streams === null
			|| response.streams.length === 0) {
				rename_error_file();
			} else {
				if (video.windows_long_path !== false) {
					console.log("path too long: " + file);
				}
				for (var is_hevc = false, j = 0; j < response.streams.length; j++) {
					if (is_hevc = response.streams[j].codec_type === "video" && response.streams[j].codec_name === "hevc") {
						break;
					}
				}
				if (debug && is_hevc) console.log("HEVC: " + file);
				var new_file_tag = is_hevc && rename_hevc ? file_tag_hevc : video.file_tag === file_tag_h264 ? file_tag_h264 : file_tag_default;
				if (video.file_tag !== new_file_tag) {
					fs.rename(file, file = file_name(video, new_file_tag), function(error) {
						console.log("renamed: " + file);
						if (error !== null) {
							console.error(error);
						} else if (is_hevc === false) {
							process_file_h264(file, video);
						} else if (convert_hevc) {
							queue_convert_file(file, file_name(video, file_tag_h264, h264_dir_only), video);
						}
					});
				} else if (is_hevc === false) {
					process_file_h264(file, video);
				} else if (convert_hevc) {
					queue_convert_file(file, file_name(video, file_tag_h264, h264_dir_only), video);
				}
			}
			queue_next(true);
		});
	}
}
function convert_file(input_file, output_file, video, queue_next) {
	if (!running || probed_files.has(output_file) || converted_files.has(output_file)) {
		queue_next();
	} else {
		converted_files.add(output_file);
		if (debug) console.log("convert_file: " + file);
		fs.access(output_file, function(error) {
			if (debug) console.log("test_file_exists: " + output_file);
			if (error !== null) {
				if (error.code !== "ENOENT") {
					console.error(error);
					queue_next();
				} else {
					console.log("converting: " + input_file);
					var proc = child_process.spawn(ffmpeg_path, [
						"-i", input_file,
						"-map", "0",

						"-c:v", "libx264",
//						"-crf", "18",
						"-crf", "23",
//						"-preset", "veryslow",
						"-preset", "veryfast",

//						"-c:v", "h264_nvenc",
//						"-cq", "21",

						"-pix_fmt", "yuv420p",
						"-b:v", "0",
						"-c:a", "copy",
						"-movflags", "+faststart+use_metadata_tags",
						output_file
					], {stdio: "ignore", detached: true}); // unlink it from the node process, in case that's closed for some reason while ffmpeg is still converting
					proc.on("close", function() {
						console.log("converted: " + output_file);
						queue_next(true);
					});
				}
			} else {
				queue_next(true);
			}
		});
	}
}
function rotate_file(input_file, output_file, video, queue_next) {
	if (!running || probed_files.has(output_file) || rotated_files.has(output_file)) {
		queue_next();
	} else {
		rotated_files.add(output_file);
		if (debug) console.log("rotate_file: " + file);
		fs.access(output_file, function(error) {
			if (debug) console.log("test_file_exists: " + output_file);
			if (error !== null) {
				if (error.code !== "ENOENT") {
					console.error(error);
					queue_next();
				} else {
					console.log("rotating: " + input_file);
					var proc = child_process.spawn(ffmpeg_path, [
						"-i", input_file,
						"-c", "copy",
						"-movflags", "+faststart+use_metadata_tags",
						"-metadata:s:v", "rotate=" + rotate_deg,
						output_file
					], {stdio: "ignore", detached: true}); // unlink it from the node process, in case that's closed for some reason while ffmpeg is still converting
					proc.on("close", function() {
						console.log("rotated: " + output_file);
						queue_next(true);
					});
				}
			} else {
				queue_next(true);
			}
		});
	}
}
function parse_items(items) {
	items.trim().split(/[\r\n]+/).forEach(function(item) {
		parse_item(item);
	});
}
function parse_item(item, item_stats=null) {
	if (item_stats === null) {
		item = fs.realpathSync(item);
		item_stats = fs.lstatSync(item);
	}
	if (item_stats.isSymbolicLink()) {
		item = fs.readlinkSync(item);
		item_stats = fs.lstatSync(item);
	}
	if (item_stats.isDirectory()) {
		fs.readdir(item, {withFileTypes: true}, function(error, files) {
			if (error !== null) {
				console.error(error);
			} else {
				files.forEach(function(file) {
					parse_item(item + path.sep + file.name, file);
				});
			}
		});
	} else {
		process_file(item);
	}
}
function process_file(file) {
	if (path.extname(file) === ".mp4") {
		var
		name = path.basename(file, path.extname(file)),
		name_base = name,
		name_split = name_base.split(file_tag_delimiter),
		rotate_tag,
		file_tag = file_tag_default;
		if (name_split.length > 1) {
			var name_tag = name_split.pop();
			if (name_tag === file_tag_error) {
				return
			}
			if (file_tags_rotate.has(name_tag)) {
				name = path.basename(name, file_tag_delimiter + name_tag),
				name_base = name,
				rotate_tag = name_tag,
				name_tag = name_split.pop();
			}
			if (file_tags.has(name_tag)) {
				file_tag = name_tag,
				name = path.basename(name, file_tag_delimiter + name_tag);
			}
		}
		queue_probe_file(file, {
			dir: path.dirname(file),
			ext: path.extname(file),
			name: name,
			name_base: name_base,
			file: file,
			rotate_tag: rotate_tag,
			file_tag: file_tag,
			windows_long_path: false
		});
	}
}
function process_file_h264(file, video) {
	if (debug) console.log("H264: " + file);
	if (h264_dir) link_file_h264(file);
	if (rotate && video.rotate_tag !== rotate_tag) {
		video.rotate_tag = rotate_tag;
		var rotate_file = file_name(video, rotate_tag, h264_dir_only, rotate);
		queue_rotate_file(file, rotate_file, video, function() {
			if (h264_dir) link_file_h264(rotate_file);
		});
	}
}
function link_file_h264(input_file) {
	var output_file = h264_directory + path.sep + path.basename(input_file);
	if (input_file === output_file) return;
	if (debug) console.log("link_file: " + output_file);
	fs.link(input_file, output_file, function(error) {
		if (error === null) {
			console.log("hardlink created: " + output_file);
		} else if (error.code === "EXDEV") {
			fs.symlink(input_file, output_file, "file", function(error) {
				if (error === null) {
					console.log("symlink created: " + output_file);
				} else if (error.code !== "EEXIST") {
					console.error(error);
				} else if (debug) {
					console.log("link_exists: " + output_file);
				}
			});
		} else if (error.code !== "EEXIST") {
			console.error(error);
		} else if (debug) {
			console.log("link_exists: " + output_file);
		}
	});
}
function ensureDirectorySync(directory) {
	if ((directory = path.normalize(directory)).lastIndexOf(path.sep) + path.sep.length >= directory.length) {
		directory = directory.substring(0, directory.length - path.sep.length);
	}
	try {
		var dir_stats = fs.lstatSync(directory);
		if (dir_stats.isSymbolicLink()) {
			directory = fs.readlinkSync(directory);
			dir_stats = fs.lstatSync(directory);
		}
		if (!dir_stats.isDirectory()) {
			throw new Error("Path is not a directory.");
		}
	} catch (e) {
		if (e.code !== "ENOENT") {
			throw e;
		}
		for (var i = directory.length + path.sep.length, e, t; (i -= path.sep.length) > -1
		&& (i = directory.lastIndexOf(path.sep, i)) > -1
		&& !fs.existsSync(t = directory.substring(0, i));) {}
		if (i > -1) {
			i += path.sep.length;
			e = t.length;
		} else if (directory.substring(0, path.sep.length) === path.sep) {
			t = "";
			i = path.sep.length;
			e = 0;
		} else {//root
			t = "." + path.sep;
			i = 0;
			e = 0;
		}
		for (; (i = directory.indexOf(path.sep, i)) > -1; e = i, i += path.sep.length) {
			fs.mkdirSync(t += directory.substring(e, i));
		}
		fs.mkdirSync(t + directory.substring(e));
	}
}
function parseArgv(arguments) {
	var i = 2, e, r = {};
	while (i < arguments.length) {
		e = arguments[i].indexOf("=");
		if (e === -1) {
			r[arguments[i]] = "";
		} else {
			r[arguments[i].substring(0, e)] = arguments[i].substring(e + 1);
		}
		i++;
	}
	return r;
}
process.on("SIGINT", function() {
	running = false;
	console.log("SIGINT");
});
(function() {
	var i, a = parseArgv(process.argv);
	if (a.i === undefined) {
		console.log(help);
	} else {
		debug = a.debug !== undefined,
		rename_hevc = a.rename_hevc !== undefined,
		convert_hevc = a.convert_hevc !== undefined,
		rotate = a.rotate !== undefined,
		h264_dir = a.h264_dir !== undefined,
		h264_dir_only = h264_dir && a.h264_dir_only !== undefined,
		ffprobe = require("ffprobe");
		if (ffprobe_path === undefined || (convert_hevc && ffmpeg_path === undefined)) {
			ffmpeg = require("ffmpeg-ffprobe-static");
		}
		if (convert_hevc) {
			ffmpeg_path = ffmpeg_path !== undefined ? ffmpeg_path : ffmpeg.ffmpegPath;
		}
		ffprobe_path = ffprobe_path !== undefined ? ffprobe_path : ffmpeg.ffprobePath;
		if (rotate && a.rotate.length > 0) {
			switch(parseInt(a.rotate, 10)) {
				case 90: {
					rotate_deg = 90,
					rotate_tag = file_tag_r90;
					break;
				}
				case 180: {
					rotate_deg = 180,
					rotate_tag = file_tag_r180;
					break;
				}
				case 270: {
					rotate_deg = 270,
					rotate_tag = file_tag_r270;
					break;
				}
				default: {
					rotate = false;
					break;
				}
			}
		}
		if ((a.max_probes !== undefined) && (a.max_probes.length > 0) && (!isNaN(i = parseInt(a.max_probes, 10)))) {
			max_probes = Math.min(Math.max(i, 1), 100);
		}
		if ((a.max_converts !== undefined) && (a.max_converts.length > 0) && (!isNaN(i = parseInt(a.max_converts, 10)))) {
			max_converts = Math.min(Math.max(i, 1), 100);
		}
		if ((a.max_rotates !== undefined) && (a.max_rotates.length > 0) && (!isNaN(i = parseInt(a.max_rotates, 10)))) {
			max_rotates = Math.min(Math.max(i, 1), 100);
		}
		try {
			if (h264_dir) {
				if (a.h264_dir !== "") {
//					h264_directory = (path.isAbsolute(a.h264_dir) ? "" : process.cwd() + path.sep) + a.h264_dir;
					h264_directory = (path.isAbsolute(a.h264_dir) ? "" : __dirname + path.sep) + a.h264_dir;
				}
				ensureDirectorySync(h264_directory);
			}
			if (a.i === "clipboard") {
				parse_items((require("clipboardy")).readSync());
			} else if (path.extname(a.i) === ".txt") {
				parse_items(fs.readFileSync(a.i, {flag: "r", encoding: "utf8"}));
			} else {
				parse_item(a.i);
			}
		} catch (e) {
			console.error(e);
		}
	}
})();