/**
 * This file is part of the Tracy (https://tracy.nette.org)
 */

const
	COLLAPSE_COUNT = 7,
	COLLAPSE_COUNT_TOP = 14,
	TYPE_ARRAY = 'a',
	TYPE_OBJECT = 'o',
	TYPE_RESOURCE = 'r',
	PROP_VIRTUAL = 4,
	PROP_PRIVATE = 2;

const
	HINT_CTRL = 'Ctrl-Click to open in editor',
	HINT_ALT = 'Alt-Click to expand/collapse all child nodes';

class Dumper
{
	static init(context) {
		// full lazy
		(context || document).querySelectorAll('[data-tracy-snapshot][data-tracy-dump]').forEach((pre) => { // <pre>
			let snapshot = JSON.parse(pre.getAttribute('data-tracy-snapshot'));
			pre.removeAttribute('data-tracy-snapshot');
			pre.appendChild(build(JSON.parse(pre.getAttribute('data-tracy-dump')), snapshot, pre.classList.contains('tracy-collapsed')));
			pre.removeAttribute('data-tracy-dump');
			pre.classList.remove('tracy-collapsed');
		});

		// snapshots
		(context || document).querySelectorAll('meta[itemprop=tracy-snapshot]').forEach((meta) => {
			let snapshot = JSON.parse(meta.getAttribute('content'));
			meta.parentElement.querySelectorAll('[data-tracy-dump]').forEach((pre) => { // <pre>
				if (pre.closest('[data-tracy-snapshot]')) { // ignore unrelated <span data-tracy-dump>
					return;
				}
				pre.appendChild(build(JSON.parse(pre.getAttribute('data-tracy-dump')), snapshot, pre.classList.contains('tracy-collapsed')));
				pre.removeAttribute('data-tracy-dump');
				pre.classList.remove('tracy-collapsed');
			});
			// <meta> must be left for debug bar panel content
		});

		if (Dumper.inited) {
			return;
		}
		Dumper.inited = true;

		document.documentElement.addEventListener('click', (e) => {
			let el;
			// enables <span data-tracy-href=""> & ctrl key
			if (e.ctrlKey && (el = e.target.closest('[data-tracy-href]'))) {
				location.href = el.getAttribute('data-tracy-href');
				return false;
			}

		});

		document.documentElement.addEventListener('tracy-beforetoggle', (e) => {
			let el;
			// initializes lazy <span data-tracy-dump> inside <pre data-tracy-snapshot>
			if ((el = e.target.closest('[data-tracy-snapshot]'))) {
				let snapshot = JSON.parse(el.getAttribute('data-tracy-snapshot'));
				el.removeAttribute('data-tracy-snapshot');
				el.querySelectorAll('[data-tracy-dump]').forEach((toggler) => {
					if (!toggler.nextSibling) {
						toggler.after(document.createTextNode('\n')); // enforce \n after toggler
					}
					toggler.nextSibling.after(buildStruct(JSON.parse(toggler.getAttribute('data-tracy-dump')), snapshot, toggler, true, []));
					toggler.removeAttribute('data-tracy-dump');
				});
			}
		});

		document.documentElement.addEventListener('tracy-toggle', (e) => {
			if (!e.target.matches('.tracy-dump *')) {
				return;
			}

			let cont = e.detail.relatedTarget;
			let origE = e.detail.originalEvent;

			if (origE && origE.usedIds) { // triggered by expandChild()
				toggleChildren(cont, origE.usedIds);
				return;

			} else if (origE && origE.altKey && cont.querySelector('.tracy-toggle')) { // triggered by alt key
				if (e.detail.collapsed) { // reopen
					e.target.classList.toggle('tracy-collapsed', false);
					cont.classList.toggle('tracy-collapsed', false);
					e.detail.collapsed = false;
				}

				let expand = e.target.tracyAltExpand = !e.target.tracyAltExpand;
				toggleChildren(cont, expand ? {} : false);
			}

			cont.classList.toggle('tracy-dump-flash', !e.detail.collapsed);
		});

		document.documentElement.addEventListener('animationend', (e) => {
			if (e.animationName === 'tracy-dump-flash') {
				e.target.classList.toggle('tracy-dump-flash', false);
			}
		});

		document.addEventListener('mouseover', (e) => {
			if (!e.target.matches('.tracy-dump *')) {
				return;
			}

			let el;

			if (e.target.matches('.tracy-dump-hash') && (el = e.target.closest('tracy-div'))) {
				el.querySelectorAll('.tracy-dump-hash').forEach((el) => {
					if (el.textContent === e.target.textContent) {
						el.classList.add('tracy-dump-highlight');
					}
				});
				return;
			}

			if ((el = e.target.closest('.tracy-toggle')) && !el.title) {
				el.title = HINT_ALT;
			}
		});

		document.addEventListener('mouseout', (e) => {
			if (e.target.matches('.tracy-dump-hash')) {
				document.querySelectorAll('.tracy-dump-hash.tracy-dump-highlight').forEach((el) => {
					el.classList.remove('tracy-dump-highlight');
				});
			}
		});

		Tracy.Toggle.init();
	}
}


function build(data, repository, collapsed, parentIds, keyType) {
	let id, type = data === null ? 'null' : typeof data,
		collapseCount = collapsed === null ? COLLAPSE_COUNT : COLLAPSE_COUNT_TOP;

	if (type === 'null' || type === 'number' || type === 'boolean') {
		return createEl(null, null, [
			createEl(
				'span',
				{'class': 'tracy-dump-' + type.replace('ean', '')},
				[data + '']
			)
		]);

	} else if (type === 'string') {
		data = {
			string: data.replace(/&/g, '&amp;').replace(/</g, '&lt;'),
			length: [...data].length
		};

	} else if (Array.isArray(data)) {
		data = {array: null, items: data};

	} else if (data.ref) {
		id = data.ref;
		data = repository[id];
		if (!data) {
			throw new UnknownEntityException;
		}
	}


	if (data.string !== undefined || data.bin !== undefined) {
		let s = data.string === undefined ? data.bin : data.string;
		if (keyType === TYPE_ARRAY) {
			return createEl(null, null, [
				createEl(
					'span',
					{'class': 'tracy-dump-string'},
					{html: '<span class="tracy-dump-lq">\'</span>' + s + '<span>\'</span>'}
				),
			]);

		} else if (keyType !== undefined) {
			if (type !== 'string') {
				s = '<span class="tracy-dump-lq">\'</span>' + s + '<span>\'</span>';
			}

			const classes = [
				'tracy-dump-public',
				'tracy-dump-protected',
				'tracy-dump-private',
				'tracy-dump-dynamic',
				'tracy-dump-virtual',
			];
			return createEl(null, null, [
				createEl(
					'span',
					{
						'class': classes[typeof keyType === 'string' ? PROP_PRIVATE : keyType],
						'title': typeof keyType === 'string' ? 'declared in ' + keyType : null,
					},
					{html: s}
				),
			]);
		}

		let count = (s.match(/\n/g) || []).length;
		if (count) {
			let collapsed = count >= COLLAPSE_COUNT;
			return createEl(null, null, [
				createEl('span', {'class': collapsed ? 'tracy-toggle tracy-collapsed' : 'tracy-toggle'}, ['string']),
				'\n',
				createEl(
					'div',
					{
						'class': 'tracy-dump-string' + (collapsed ? ' tracy-collapsed' : ''),
						'title': data.length + (data.bin ? ' bytes' : ' characters'),
					},
					{html: '<span class="tracy-dump-lq">\'</span>' + s + '<span>\'</span>'}
				),
			]);
		}

		return createEl(null, null, [
			createEl(
				'span',
				{
					'class': 'tracy-dump-string',
					'title': data.length + (data.bin ? ' bytes' : ' characters'),
				},
				{html: '<span>\'</span>' + s + '<span>\'</span>'}
			),
		]);

	} else if (data.number) {
		return createEl(null, null, [
			createEl('span', {'class': 'tracy-dump-number'}, [data.number])
		]);

	} else if (data.text !== undefined) {
		return createEl(null, null, [
			createEl('span', {class: 'tracy-dump-virtual'}, [data.text])
		]);

	} else { // object || resource || array
		let pos, nameEl;
		nameEl = data.object && (pos = data.object.lastIndexOf('\\')) > 0
			? [data.object.substr(0, pos + 1), createEl('b', null, [data.object.substr(pos + 1)])]
			: [data.object || data.resource];

		let span = data.array !== undefined
			? [
				createEl('span', {'class': 'tracy-dump-array'}, ['array']),
				' (' + (data.length || data.items.length) + ')'
			]
			: [
				createEl('span', {
					'class': data.object ? 'tracy-dump-object' : 'tracy-dump-resource',
					title: data.editor ? 'Declared in file ' + data.editor.file + ' on line ' + data.editor.line + (data.editor.url ? '\n' + HINT_CTRL : '') + '\n' + HINT_ALT : null,
					'data-tracy-href': data.editor ? data.editor.url : null
				}, nameEl),
				...(id ? [' ', createEl('span', {'class': 'tracy-dump-hash'}, [data.resource ? '@' + id.substr(1) : '#' + id])] : [])
			];

		parentIds = parentIds ? parentIds.slice() : [];
		let recursive = id && parentIds.indexOf(id) > -1;
		parentIds.push(id);

		if (recursive || !data.items || !data.items.length) {
			span.push(recursive ? ' RECURSION' : (!data.items || data.items.length ? ' …' : ''));
			return createEl(null, null, span);
		}

		collapsed = collapsed === true || data.collapsed || (data.items && data.items.length >= collapseCount);
		let toggle = createEl('span', {'class': collapsed ? 'tracy-toggle tracy-collapsed' : 'tracy-toggle'}, span);

		return createEl(null, null, [
			toggle,
			'\n',
			buildStruct(data, repository, toggle, collapsed, parentIds),
		]);
	}
}


function buildStruct(data, repository, toggle, collapsed, parentIds) {
	if (Array.isArray(data)) {
		data = {items: data};

	} else if (data.ref) {
		parentIds = parentIds.slice();
		parentIds.push(data.ref);
		data = repository[data.ref];
	}

	let cut = data.items && data.length > data.items.length;
	let type = data.object ? TYPE_OBJECT : data.resource ? TYPE_RESOURCE : TYPE_ARRAY;
	let div = createEl('div', {'class': collapsed ? 'tracy-collapsed' : null});

	if (collapsed) {
		let handler;
		toggle.addEventListener('tracy-toggle', handler = function() {
			toggle.removeEventListener('tracy-toggle', handler);
			createItems(div, data.items, type, repository, parentIds, null);
			if (cut) {
				createEl(div, null, ['…\n']);
			}
		});
	} else {
		createItems(div, data.items, type, repository, parentIds, true);
		if (cut) {
			createEl(div, null, ['…\n']);
		}
	}

	return div;
}


function createEl(el, attrs, content) {
	if (!(el instanceof Node)) {
		el = el ? document.createElement(el) : document.createDocumentFragment();
	}
	for (let id in attrs || {}) {
		if (attrs[id] !== null) {
			el.setAttribute(id, attrs[id]);
		}
	}

	if (content && content.html !== undefined) {
		el.innerHTML = content.html;
		return el;
	}

	content = content || [];
	el.append(...content.filter((child) => (child !== null)));
	return el;
}


function createItems(el, items, type, repository, parentIds, collapsed) {
	let key, val, vis, ref, i, tmp;

	for (i = 0; i < items.length; i++) {
		if (type === TYPE_ARRAY) {
			[key, val, ref] = items[i];
		} else {
			[key, val, vis = PROP_VIRTUAL, ref] = items[i];
		}

		createEl(el, null, [
			build(key, null, null, null, type === TYPE_ARRAY ? TYPE_ARRAY : vis),
			type === TYPE_ARRAY ? ' => ' : ': ',
			...(ref ? [createEl('span', {'class': 'tracy-dump-hash'}, ['&' + ref]), ' '] : []),
			tmp = build(val, repository, collapsed, parentIds),
			tmp.lastElementChild.tagName === 'DIV' ? '' : '\n',
		]);
	}
}


function toggleChildren(cont, usedIds) {
	let hashEl, id;

	cont.querySelectorAll(':scope > .tracy-toggle').forEach((el) => {
		hashEl = (el.querySelector('.tracy-dump-hash') || el.previousElementSibling);
		id = hashEl && hashEl.matches('.tracy-dump-hash') ? hashEl.textContent : null;

		if (!usedIds || (id && usedIds[id])) {
			Tracy.Toggle.toggle(el, false);
		} else {
			usedIds[id] = true;
			Tracy.Toggle.toggle(el, true, {usedIds: usedIds});
		}
	});
}


function UnknownEntityException() {}


let Tracy = window.Tracy = window.Tracy || {};
Tracy.Dumper = Tracy.Dumper || Dumper;

function init() {
	Tracy.Dumper.init();
}

if (document.readyState === 'loading') {
	document.addEventListener('DOMContentLoaded', init);
} else {
	init();
}
