var os = require("os"),
fs = require("fs"),
path = require("path"),
url = require("url"),
http = require("http"),
https = require("https"),
zlib = require("zlib"),
cookie = "",
//cookie = "sid_tt=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX; sessionid=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
user_agent = "Mozilla/5.0 AppleWebKit/537.36 Chrome/87.0.4280.88 Safari/537.36",
help = "node " + path.basename(__filename) + " [m=\"metadata_file.json\"] [save_empty] [v] [n=100] [error_delay=1000] [c=1] [u=\"" + user_agent + "\"] [debug] \\"
+ os.EOL + "[cookie=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX] = [j=\"sid_tt=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX; sessionid=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\"]"
+ os.EOL + "m - save metadata to file - if not passed an output file then uses a default construction"
+ os.EOL + "v - unfiltered metadata"
+ os.EOL + "n - re-attempt requests on server error - takes a max amount of errors before a success to ignore (0 = no limit)"
+ os.EOL + "c - content_type (1=following)"
+ os.EOL + "j - private cookie from \".tiktok.com\""
+ os.EOL + "u - user agent"
+ os.EOL
+ os.EOL + "debug - shows http requests"
+ os.EOL + "cookie - content of the sid_tt/sessionid cookie"
+ os.EOL + "save_empty - save output files to disk even if the contents are empty"
+ os.EOL + "error_delay - interval between re-attempts with flag \"n\" (ms)",
metadata,
metadata_verbose = false,
ignore_errors = false,
//ignore_errors_delay = 1000,
ignore_errors_delay = 0,
ignore_errors_limit = 100,
ignore_errors_count = 0,
debug = false,
save_empty = false,
content_type = 1,
running = true,
timeout = null,
deviceId = generateDeviceId();
function generateBase62(length) {
	var i = 0, r = "", base62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
	while (i < length) {
		r += base62.charAt(Math.floor(Math.random() * 62));
		i++;
	}
	return r;
}
function generateDeviceId() {
	var i = 1, a = new Array(19);//Math.floor(Math.random() * 9) + 11
	a[0] = 54;//Math.floor(Math.random() * 9) + 49
	while (i < a.length) {
		a[i++] = Math.floor(Math.random() * 10) + 48;
	}
	return String.fromCharCode.apply(null, a);
}
function generateVerify() {
	var i = 0, d = new Date(), t = d.getTime() - d.getTimezoneOffset() * 60000, r = "", base62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
	do {
		switch (i) {
			case 8:
			case 13:
			case 18:
			case 23:
				r += "_";
				break;
			case 14:
				r += "4";
				break;
			default:
				r += base62.charAt(Math.floor(Math.random() * 62));
				break;
		}
		i++;
	} while (i < 36);
	return "verify_" + t.toString(36) + "_" + r;
}
function generateCookie() {
	var t = generateDeviceId();
	return "sessionid=; tt_webid=" + t + "; tt_webid_v2=" + t + "; tt_csrf_token=" + generateBase62(24) + "; s_v_web_id=" + generateVerify();
}
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;
}
function requestGet(uri, headers, callback) {
	try {
		var u = typeof uri === "string" ? new URL(uri) : uri, h, r;
		if (u.protocol === "http:") {
			h = http;
		} else if (u.protocol === "https:") {
			h = https;
		} else {
			process.nextTick(callback, new Error("Only http and https is supported."));
			return;
		}
		if (debug) console.log("http/get: " + String(uri));
		r = h.request({
			method: "GET",
			host: u.host,
			port: +u.port || 0,
			path: u.pathname + u.search,//+ u.hash
			timeout: 10000,
			headers: {
				...headers,
				"Cookie": cookie + (cookie.length > 0 ? "; " : "") + generateCookie(),
				"User-Agent": user_agent,
				"Referer": "https://www.tiktok.com/"
			}
		}, function(response) {
			if (callback === null) {
				return;
			} else if (response.statusCode >= 300 && response.statusCode < 400) {
				var l = response.headers["location"];
				if (l === undefined) {
					callback(new Error("Redirection without location."));
					callback = null;
				} else {
					try {
						if (debug) console.log("http/" + response.statusCode + ": " + String(uri));
						requestGet(new URL(l, String(uri)), headers, callback);
					} catch (e) {
						callback(e);
						callback = null;
					}
				}
				return;
			} else if (response.statusCode !== 200) {
				callback(new Error(response.statusMessage));
				callback = null;
				return;
			}
			if (debug) console.log("http/" + response.statusCode + (!isNaN(+response.headers["content-length"]) ? " (" + response.headers["content-length"] + " bytes)" : "") + ": " + String(uri));
			callback(null, response, r, String(uri));
			callback = null;
		}).on("error", function(error) {
			if (callback === null) {
				return;
			}
			callback(error);
			callback = null;
		}).on("timeout", function() {
			r.destroy();
			console.error("timed_out: " + String(uri));
		}).end();
	} catch (e) {
		process.nextTick(callback, e);
		return;
	}
}
function requestGetJson(uri, headers, callback) {
	requestGet(uri, {
		...headers,
		"Accept-Encoding": "gzip, deflate, *"
	}, function(error, response, request) {
		if (error !== null) {
			callback(error);
			return;
		}
		var data_stream, d = "";
		switch(response.headers["content-encoding"]) {
			case "gzip":
				data_stream = zlib.createGunzip();
				response.pipe(data_stream);
				break;
			case "deflate":
				data_stream = zlib.createInflate();
				response.pipe(data_stream);
				break;
			default:
				data_stream = response;
				data_stream.setEncoding("utf8");
				break;
		}
		data_stream.on("data", function(data) {
			d += data;
		}).on("error", function(error) {
			if (callback === null) {
				return;
			}
			callback(error);
			callback = null;
		}).on("end", function() {
			if (callback === null) {
				return;
			}
			try {
				var d2 = d.replace(/([\[:])?(?<![\d.]|\\".)(\d{15,})([,\}\]])/g, "$1\"$2\"$3"); // rancid
				callback(null, JSON.parse(d2));
			} catch (e) {
				try {
					callback(null, JSON.parse(d));
//					console.error(new Error("RegEx for preserving big numbers failed"));
//					fs.writeFileSync("/" + path.basename(__filename) + "_" + output_time + "_pass.json", d);
//					fs.writeFileSync("/" + path.basename(__filename) + "_" + output_time + "_fail.json", d2);
				} catch (e) {
					callback(e);
				}
			}
			callback = null;
		});
	});
}
function requestAweme(uri, callback) {
	var f = function(error, repeat) {
		if (repeat > 9 || !running) {
			callback(error !== null ? error : new Error("Exited unexpectedly."));
			return;
		}
		requestGetJson(uri, {}, function(error, data) {
			if (error !== null) {
				f(error, repeat + 1);
			} else if (typeof data !== "object" || data === null || typeof data.status_code !== "number") {
				f(new Error("Unexpected response."), repeat + 1);
			} else if (data.status_code !== 0) { // success
				f(awemeError(data), repeat + 1);
			} else {
				callback(null, data);
			}
		});
	};
	f(null, 0);
}
function awemeError(data) {
	return new Error(data.status_code + (typeof data.status_msg === "string" && data.status_msg.length > 0 ? " (" + data.status_msg + ")" : ""));
}
function getLives() {
	var f0 = function() {
		if (running) {
			requestAweme("https://webcast.tiktok.com/webcast/feed/"
			+ "?aid=1988"
			+ "&channel_id=42" // slatfatf
			+ "&content_type=" + content_type
			+ "&device_id=" + deviceId
			+ "&req_from=pc_web_suggested_host", f1);
		}
	}, f1 = function(error, data) {
		if (error !== null) {
			if (running && ignore_errors && (ignore_errors_limit === 0 || ignore_errors_count < ignore_errors_limit)) {
				ignore_errors_count += 1;
				console.error("ignoring: " + error.toString() + " | " + ignore_errors_count + (ignore_errors_limit !== 0 ? " / " + ignore_errors_limit : ""));
				setTimeout(f0, ignore_errors_delay);
			} else {
				console.error(error);
			}
		} else if (typeof data !== "object" || data === null
		|| typeof data.extra !== "object" || data.extra === null
		|| typeof data.extra.max_time !== "number"
		|| typeof data.extra.has_more !== "boolean") {
			console.error(new Error("Unexpected response."));
			console.error(data);
		} else {
			if (typeof data.data === "object" && data.data !== null) {
				for (var j = 0; j < data.data.length; j++) {
					if (metadata_verbose) {
						metadata.push(data.data[j]);
					} else {
						metadata.push(obj_filter(data.data[j], example_metadata));
					}
				}
			}
			metadata_save_to_file();
		}
	}
	f0();
}
function obj_filter(obj_in, obj_match) {
	var obj_out = {};
	for (const [key, value] of Object.entries(obj_in)) {
		if (obj_match.hasOwnProperty(key)) {
			if (typeof value === "object") {
				obj_out[key] = obj_filter(value, obj_match[key]);
			} else {
				if (key === "rtmp_pull_url") {
					obj_out[key] = value.replace(/(?<!_or4)\.flv$/, "_or4.flv");
				} else {
					obj_out[key] = value;
				}
			}
		}
	}
	return obj_out;
}
function sanitizeFilename(value) {
	return value.replace(/[\x00-\x1f"*/:<>?\\|]/g, "");
}
function createTimeToFilename(value) {
	var t = (new Date(value * 1000)).toISOString();
	return t.substring(0, t.length === 24 ? 19 : 22).replace(/[-:]/g, "") + "Z";
}
function metadata_file_name(a, params) {
	var obj_out = [];
//	obj_out.push(path.basename(__filename, path.extname(__filename)));
	obj_out.push(path.basename(__filename));
	obj_out.push(createTimeToFilename(Date.now()/1000));
	for (const [key, value] of Object.entries(params.split(""))) {
		if (a[value] !== undefined) {
			if (a[value] === "") {
				obj_out.push(value);
			} else {
				obj_out.push([value,sanitizeFilename(a[value])].join("="));
			}
		}
	}
	return obj_out.join("_") + ".json";
}
function metadata_save_to_file() {
	if (save_empty || metadata.length > 0) {
		fs.writeFile(metadata_file, JSON.stringify(metadata), function (error) {
			if (error !== null) throw error;
			console.log("output file: " + metadata_file);
			console.log("item count: " + metadata.length);
		});
	}
}
process.on("SIGINT", function() {
	running = false;
	if (timeout !== null) {
		clearTimeout(timeout);
		timeout = null;
	}
});
(function() {
	var a = parseArgv(process.argv), i;
	if (a.j !== undefined) {
		cookie = a.j;
	}
	if (a.cookie !== undefined) {
		cookie = ["sid_tt", "sessionid", "sessionid_ss"].map(v => v + "=" + a.cookie).join("; ");
	}
	if (a.u !== undefined) {
		user_agent = a.u;
	}
	if ((a.n !== undefined) && (a.n.length > 0) && (!isNaN(i = parseInt(a.n, 10)))) {
		ignore_errors_limit = Math.max(i, 0);
	}
	if ((a.error_delay !== undefined) && (a.error_delay.length > 0) && (!isNaN(i = parseInt(a.error_delay, 10)))) {
		ignore_errors_delay = Math.max(i, 0);
	}
	if ((a.c !== undefined) && (a.c.length > 0) && (!isNaN(i = parseInt(a.c, 10)))) {
		content_type = i;
	}
	ignore_errors = ignore_errors || a.n !== undefined,
	debug = debug || a.debug !== undefined,
	save_empty = save_empty || a.save_empty !== undefined,
	metadata = [],
	metadata_verbose = metadata_verbose || a.v !== undefined;
	if (a.m !== undefined && a.m.length > 0) {
		metadata_file = a.m;
	} else {
		metadata_file = metadata_file_name(a, "");
	}
	example_metadata =
	{
		"data": {
			"create_time": 0,
			"id_str": "",
			"owner": {
				"bio_description": "",
				"display_id": "",
				"id_str": "",
				"nickname": ""
			},
			"start_time": 0,
			"status": 0,
			"stream_url": {
				"rtmp_pull_url": ""
			},
			"title": "",
			"user_count": 0
		},
		"live_reason": "",
		"rid": "",
		"type": 0
	};
	getLives();
})();