// @ts-check
/* eslint-disable no-var */
/* 
Include console.js to any application to display a semi-transparent overlay
with contents of the web console. This is useful for environments where the
console is not available. This code should work in almost any browser and does
not need to be transpiled.

A query string after console.js can be used to set primitive settings values (such as depth, fontSize, disableShadow, defaultColor).

Example use:
<script type="text/javascript" language="javascript" src="js/console.js?fontSize=14"></script>
*/
'use strict';

(function () {
	var SETTINGS = {
		/** Maximal depth of displayed object, if depth is exhausted remaining values are rendered inline. */
		depth: 3,

		/** Which console methods are displayed on screen, currently supported methods are: error, warn, info, log. */
		patchedMethods: /** @type {ConsoleMethod[]} */ (['error', 'warn', 'info', 'log']),

		/** Dictionary of console method => color, for each patched method. */
		colors: /** @type {Record<string, string>} */ ({
			error: 'lightcoral',
			warn: 'gold',
		}),

		/** Default color of text, if colors does not contain value for a method. */
		defaultColor: 'white',

		/** Font size of the console */
		fontSize: '14px',

		/** Disable text-shadow in the console, this may make the text harder to read. */
		disableShadow: false,

		/**
		 * Polyfill ErrorEvent.error property, this is disabled by default. This polyfill is not entirely spec compliant and may cause memory
		 * leaks in the application.
		 */
		polyfillErrorStack: false,

		/**
		 * Maximum number of lines in the log, after exceeded this amount oldest logs are removed
		 */
		maxChildCount: 200,
	};

	/** @param {string} name */
	function getParameterByName(name) {
		name = name.replace(/[\[\]]/g, '\\$&');
		var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
			results = regex.exec(/** @type {HTMLScriptElement} */ (document.currentScript).src);
		if (!results) {
			return null;
		}
		if (!results[2]) {
			return '';
		}
		return decodeURIComponent(results[2].replace(/\+/g, ' '));
	}

	if (document.currentScript) {
		SETTINGS.depth = Number(getParameterByName('depth')) || SETTINGS.depth;
		SETTINGS.fontSize = Number(getParameterByName('fontSize')) ? Number(getParameterByName('fontSize')) + 'px' : SETTINGS.fontSize;
		SETTINGS.disableShadow = getParameterByName('disableShadow') !== null;
		SETTINGS.defaultColor = getParameterByName('defaultColor') || SETTINGS.defaultColor;
		SETTINGS.polyfillErrorStack = getParameterByName('polyfillErrorStack') !== null;
		SETTINGS.maxChildCount = Number(getParameterByName('maxChildCount')) || SETTINGS.maxChildCount;
	}

	if (SETTINGS.polyfillErrorStack) {
		var errorsMap = {};

		// Polyfill for Error constructor with stack for older browsers
		var ErrorOrig = Error;
		/** @param {string} message */
		var ErrorWithStack = function (message) {
			this.name = 'Error';
			if (!(this instanceof ErrorWithStack)) {
				return new ErrorWithStack(message);
			}
			var stack;
			try {
				throw new ErrorOrig();
			} catch (error) {
				stack = error.stack;
			}
			this.constructor = ErrorWithStack;
			this.message = message;
			this.stack = stack;
			errorsMap[message] = this;
		};
		ErrorWithStack.prototype = ErrorOrig.prototype;
		window.Error = /** @type {any} */ (ErrorWithStack);

		// Polyfill ErrorEvent.error of addEventListener('error') for older browsers
		window.addEventListener('error', function (event) {
			if (event.error === undefined) {
				// Not standardized. ErrorEvent.message can be prefixed with 'Error: ' or 'Uncaught Error: ' based on version of chrome
				var errorMessage = Object.keys(errorsMap).find(function (key) {
					return event.message.indexOf(key) !== -1;
				});
				if (errorMessage) {
					var error = errorsMap[errorMessage];
					delete errorsMap[errorMessage];
					// @ts-ignore error is not defined in ErrorEvent for old browsers so it's not read-only
					event.error = error;
				}
			}
		});
	}

	/** @type {[string, unknown][] | null} */
	var preMountQueue = [];

	/** @type {HTMLIFrameElement} */
	var iframe;

	/** @type {HTMLTableElement} */
	var log;

	/**
	 * @param {string} level
	 * @param {unknown} content
	 */
	function reportToScreen(level, content) {
		if (preMountQueue === null) {
			var color = SETTINGS.colors[level] || SETTINGS.defaultColor;

			var row = document.createElement('tr');

			row.style.color = color;

			['[' + level + ']:', formatAny(content, SETTINGS.depth)].forEach(function (itm) {
				var td = document.createElement('td');
				td.style.verticalAlign = 'top';

				var pre = document.createElement('pre');
				pre.style.margin = '0';

				pre.appendChild(typeof itm === 'string' ? document.createTextNode(itm) : itm);
				td.appendChild(pre);
				row.appendChild(td);
			});

			var firstChild = log.childNodes[0];

			log.insertBefore(row, firstChild);

			while (log.childElementCount > SETTINGS.maxChildCount) {
				log.childNodes[log.childNodes.length - 1].remove();
			}
		} else {
			preMountQueue.push([level, content]);
		}
	}

	/**
	 * @param {unknown} content
	 * @param {number} depth
	 * @returns {HTMLElement}
	 */
	function formatAny(content, depth) {
		var indent = SETTINGS.depth - depth;

		if (depth === SETTINGS.depth && Array.isArray(content) && content.length === 1 && typeof content[0] === 'string') {
			return block([content[0]]);
		}

		if (depth <= 0 || isInlineable(content, depth)) {
			try {
				return block([
					JSON.stringify(content, function (_, v) {
						return typeof v === 'bigint' ? v.toString() + 'n' : v;
					}),
				]);
			} catch (e) {
				return block(['{ ... }']);
			}
		}

		/** @type {(string | HTMLElement)[]} */
		var lines = [];

		if (Array.isArray(content)) {
			lines.push('[\n');
			content.forEach(function (itm) {
				lines.push(indentWS(indent + 1), formatAny(itm, depth - 1), '\n');
			});
			lines.push(']');
			return block(lines);
		}

		if (content instanceof Error) {
			lines.push('Error {\n');

			lines.push(block([indentWS(indent + 1), 'name: "' + content.name + '"\n']));
			lines.push(block([indentWS(indent + 1), 'message: "' + content.message + '"\n']));

			var stackStr = 'stack: "' + String(content.stack).replace(new RegExp('\n', 'g'), '\n' + indentWS(indent + 1)) + '"\n';
			lines.push(block([indentWS(indent + 1), stackStr]));
			lines.push('}');
			return block(lines);
		}

		if (content instanceof ErrorEvent) {
			lines.push('ErrorEvent {');

			lines.push(block([indentWS(indent + 1), 'message: "' + content.message + '"']));
			lines.push(block([indentWS(indent + 1), 'filename: "' + content.filename.substr(content.filename.length - 50) + '"']));
			lines.push(block([indentWS(indent + 1), 'lineno:colno: ' + content.lineno + ':' + content.colno]));
			lines.push(block([indentWS(indent + 1), 'error: ', formatAny(content.error, depth - 1)]));
			lines.push('}');
			return block(lines);
		}

		if (typeof content === 'bigint') {
			return block([content + 'n']);
		}

		if (typeof content === 'symbol' || content === undefined) {
			return block([String(content)]);
		}

		if (typeof content === 'function') {
			var name = content.name;
			if (!name) {
				name = 'function';
			}
			return block([name + '()']);
		}

		if (content instanceof Date) {
			lines.push('Date(', content.toISOString(), ')');
			return block(lines);
		}

		if (content && typeof content === 'object') {
			if (!isPlainObject(content)) {
				var proto = Object.getPrototypeOf(content);
				if (proto.constructor) {
					lines.push(proto.constructor.name, ' ');
				}
			}

			lines.push('{\n');
			Object.keys(content).forEach(function (key) {
				lines.push(block([indentWS(indent + 1), key + ': ', formatAny(content[key], depth - 1), '\n']));
			});
			lines.push(indentWS(indent) + '}');
			return block(lines);
		}

		try {
			return block([JSON.stringify(content)]);
		} catch (e) {
			return block(['{ Unable to display value ' + String(content) + ' }']);
		}
	}

	/** @param {(string | HTMLElement)[]} lines */
	function block(lines) {
		var el = document.createElement('span');
		el.style.whiteSpace = 'pre';

		lines.forEach(function (itm) {
			el.appendChild(typeof itm === 'string' ? document.createTextNode(itm) : itm);
		});
		return el;
	}

	/** @param {number} i */
	function indentWS(i) {
		return repeatString('  ', i);
	}

	/**
	 * All console keys which values are callable
	 * @typedef {{ [K in keyof Console]: Console[K] extends (...x: any[]) => any ? K : never}[keyof Console]} ConsoleMethod
	 */

	/**
	 * @param {ConsoleMethod} level
	 */
	function patch(level) {
		var original = window.console[level];

		var func = function () {
			var args = Array.prototype.slice.call(arguments);
			reportToScreen(level, args);
			original.apply(console, args);
		};
		window.console[level] = func.bind(window.console);
	}

	/**
	 * @param {unknown} content
	 * @param {number} depth
	 * @returns {boolean}
	 */
	function isInlineable(content, depth) {
		if (depth <= 0) {
			return false;
		}

		if (Array.isArray(content)) {
			return content.every(function (c) {
				return isInlineable(c, depth - 1);
			});
		}

		if (isPlainObject(content)) {
			return Object.keys(content).every(function (k) {
				return isInlineable(content[k], depth - 1);
			});
		}

		return typeof content === 'boolean' || typeof content === 'number' || typeof content === 'string' || content === null;
	}

	/**
	 * @param {unknown} value
	 * @returns {value is object}
	 */
	function isPlainObject(value) {
		if (typeof value !== 'object' || value === null) {
			return false;
		}

		var proto = Object.getPrototypeOf(value);
		return proto !== null && Object.getPrototypeOf(proto) === null;
	}

	/**
	 * @param {string} str
	 * @param {number} count
	 */
	function repeatString(str, count) {
		var result = '';
		for (var i = 0; i < count; i++) {
			result += str;
		}
		return result;
	}

	SETTINGS.patchedMethods.forEach(function (m) {
		patch(m);
	});

	function init() {
		if (!preMountQueue) {
			return;
		}

		iframe = document.createElement('iframe');
		document.body.appendChild(iframe);

		iframe.style.position = 'absolute';
		iframe.style.top = '0';
		iframe.style.left = '0';
		iframe.style.right = '0';
		iframe.style.bottom = '0';
		iframe.style.zIndex = '99999';
		iframe.style.width = '100%';
		iframe.style.height = '100%';
		iframe.style.border = 'none';
		iframe.style.fontSize = SETTINGS.fontSize;
		iframe.style.fontFamily = "'courier new',monospace";
		iframe.style.pointerEvents = 'none';

		log = document.createElement('table');

		log.style.position = 'absolute';
		log.style.top = '0';
		log.style.left = '0';
		log.style.right = '0';
		log.style.bottom = '0';
		log.style.zIndex = '99999';
		if (!SETTINGS.disableShadow) {
			log.style.textShadow = repeatString('0px 0px 6px #000, ', 10) + '0px 0px 8px #000';
		}
		log.style.color = 'white';
		log.style.whiteSpace = 'pre';
		log.style.fontSize = SETTINGS.fontSize;

		function onIframeLoad() {
			if (!preMountQueue || !iframe.contentDocument) {
				throw new Error('Unreachable');
			}

			iframe.contentDocument.body.appendChild(log);

			var q = preMountQueue;
			preMountQueue = null;

			q.forEach(function (entry) {
				reportToScreen(entry[0], entry[1]);
			});
		}

		if (iframe.contentDocument && iframe.contentDocument.readyState === 'complete') {
			onIframeLoad();
		} else {
			iframe.addEventListener('load', onIframeLoad);
		}
	}

	var documentReady = document.readyState === 'complete' || document.readyState === 'interactive';

	if (documentReady) {
		init();
	} else {
		window.addEventListener('DOMContentLoaded', function () {
			init();
		});
	}

	window.addEventListener('error', function () {
		var args = Array.prototype.slice.call(arguments);
		reportToScreen('onerror', args);
	});

	reportToScreen('ready', 'On screen console has loaded');
})();
