<script>
	import { tick } from 'svelte';
	import { uniq, groupByArray, quantile_sorted, debounce, escapeHTML, escapeRegExp, strHashHSL, datetime_datestr, datetime_timestr, isInViewport, filter_pattern, filter_test, filter_mark, filter_snippets_lines, inherit } from '$lib/util.js';
	import { get_api_measurements, get_api_labs } from '$lib/backend.js';

	export let data;

	let query = '', pattern = null, selection = null, component_items = decorate_measurements(data.measurements).concat(decorate_labs(data.labs));
	let debounced_pattern = debounce((/*query*/) => (pattern = query ? filter_pattern(escapeHTML(query)) : null), 200);

	$: debounced_pattern(query);	// argument `query` is for reactivity hinting only

	function decorate_measurements(xs) {
		return xs.map(x => (x = Object.assign({ _content: escapeHTML(Object.values(x).join('\x00')), _ts: new Date(x.time) }, x), x.timestr = datetime_timestr(x._ts), x));
	}

	function decorate_labs(xs) {
		return components(xs.map(x => Object.assign({ _content: escapeHTML(Object.values(x).join('\x00')), _ts: new Date(x.time_collected) }, x)));
	}

	function components(reports) {
		const res = [], re_lab_test = /^(?<name>\w[^\r\n]{26})(?<value>[^\r\n]{8}) (?:(?<flag>\w[ \*])|  )  (?<unit>[^\r\n]{10}) (?<range>[^\r\n]{16}) \[(?<site>\d+)\]$/gm;
		reports.forEach(function(x) {
			let datestr = datetime_datestr(x._ts), timestr = datetime_timestr(x._ts), m, r;
			while(m = re_lab_test.exec(x.body)) {
				m = m.groups;
				for(let k in m) if(m[k]) m[k] = m[k].replace(/^\s+|\s+$/g, '');
				if((r = m.range) && (r.includes(' - '))) {
					r = r.split(' - ');
					m.rangeL = r[0];
					m.rangeH = r[1];
				}
				m.datestr = datestr;
				m.timestr = timestr;
				m.report = x;
				Object.assign(m, x);
				delete m.body;
				delete m._content;
				res.push(m);
			}
		});
		return res;
	}

	function calculate(items) {
		var snapshots = [], results = [], history, update, item;
		groupByArray(items, x => x._ts).map(group => group.values.reduce(((acc, x) => (acc.values[x.name] = x, acc)), { key: group.key, values: {}})).sort((a, b) => (a.key > b.key) - (a.key < b.key)).forEach(function(group) {
			snapshots.push({ key: group.key, values: history = Object.assign(snapshots.length > 0 ? inherit(snapshots[snapshots.length - 1].values) : inherit(calc_constants), update = group.values) });
			history['Time'] = update['Time'] = { time: group.key, value: group.key };
			for(var i = 0; i < calc_functions.length; ++i) {
				var calculation = calc_functions[i], deps = calculation.deps;
				for(var j = deps.length - 1, satisfied = true, updated = false; j >= 0; --j) if(!history[deps[j]]) { satisfied = false; break; }
				else if(update[deps[j]]) updated = true;
				if((satisfied) && (updated)) {
					item = calculation.calc(...calculation.deps.map(x => history[x].value), history[calculation.name] && history[calculation.name].value);
					if((item !== undefined) && (item !== null) && (item === item) && (item != 'NaN')) {	// item === item if not NaN
						results.push(history[calculation.name] = update[calculation.name] = Object.assign({ _ts: group.key, value: item }, calculation));
						if((calculation.hasOwnProperty('rangeL')) && (item < calculation.rangeL)) update[calculation.name].flag = 'L';
						else if((calculation.hasOwnProperty('rangeH')) && (item > calculation.rangeH)) update[calculation.name].flag = 'H';
					}
				}
			}
		});
		return results;
	}
	const calc_constants = {
		DOB: { _ts: null, value: new Date(data.facesheet_data.dob) },
		Sex: { _ts: null, value: data.facesheet_data.sex }
	};
	const calc_functions = [
		{ name: 'Age', unit: 'yr', deps: ['Time', 'DOB'], calc(Time, DOB, prev) { var x = Math.floor((Time - DOB.getTime())/3.15576e10); return x != prev ? x : undefined; } },
		{ name: 'BMI', unit: 'kg/m²', rangeL: 18.5, rangeH: 24.9, range: '18.5 - 24.9', deps: ['Ht', 'Wt'], calc: (Ht, Wt) => (10000*Wt/(Ht*Ht)).toPrecision(3) },
		{ name: 'BSA', unit: 'm²', deps: ['Ht', 'Wt'], calc: (Ht, Wt) => (0.007184*Math.pow(Ht, 0.725)*Math.pow(Wt, 0.425)).toPrecision(3) },
		{ name: 'CrCl', unit: 'mL/min', deps: ['Age', 'Sex', 'Wt', 'CREATININE'], calc: (Age, Sex, Wt, CREATININE) => (((140 - Age) * Wt)/(72*CREATININE)*(Sex == 'MALE' ? 1 : 0.85)).toPrecision(4) },
		{ name: 'RETICYLOCYTE#', unit: 'K/cmm', rangeL: 50, rangeH: 100, range: '50 - 100', deps: ['RBC', 'RETICULOCYTES'], calc: (RBC, RETICULOCYTES) => (10*RBC*RETICULOCYTES).toPrecision(3) }
	];

	$: component_calculated = component_items.concat(calculate(component_items));
	$: component_groups = groupByArray(component_calculated, x => new Date(x._ts.getFullYear(), x._ts.getMonth(), x._ts.getDate())).map(function(group) {
		group = group.values.reduce(((acc, x) => ((acc.values[x.name] || (acc.values[x.name] = [])).push(x), acc)), { key: group.key, datestr: datetime_datestr(group.key), datestr_year: group.key.getFullYear(), values: {}});
		for(var k in group.values) if(group.values.hasOwnProperty(k)) {
			var items = group.values[k].sort((a, b) => a._ts - b._ts);
			var strings = items.map(item => item.timestr + ' • ' + item.value + (item.unit ? ' ' + item.unit : '') + (item.flag ? ' [' + item.flag + ']' : '')/* + (item.comment && item.comment.indexOf('\n') < 0 ? ' • ' + item.comment : '')*/);
			var flags = uniq(items.map(item => item.flag).filter(x => x).map(x => x.charAt(0)));
			//var comments = uniq(items.map(item => item.comment).filter(x => x && x.indexOf('\n') >= 0));
			var numbers = uniq(items.map(item => item.value).filter(x => isNumeric(x)));
			var min = Math.min.apply(null, numbers);
			var max = Math.max.apply(null, numbers);
			group.values[k] = {
				time: group.key,
				name: k,
				unit: items[0].unit,
				range: items[0].range,
				value: numbers.length > 1 ? min + ' - ' + max : numbers.length == 1 ? numbers[0] : items.length == 1 ? items[0].value : 'MULTIPLE', min: min, max: max,
				flag: flags.length > 1 ? '*' : flags.length == 1 ? flags[0] : null,
				reports: items.map(item => item.report).filter(x => x),
				comment: (strings.join('\n') /*+ '\n\n' + comments.join('\n\n')*/).replace(/^\s+|\s+$/g, '')
			};
		}
		return group;
	}).sort((a, b) => (a.key > b.key) - (a.key < b.key));
	$: component_names = uniq(component_calculated.map(x => x.name));
	$: component_stats = statistics(component_calculated);

	function isNumeric(n) {
		return !isNaN(parseFloat(n)) && isFinite(n);
	}
	function statistics(resultset) {
		var res = {}, group, item;
		for(var i = resultset.length - 1; i >= 0; --i) {
			item = resultset[i];
			if(isNumeric(item.value)) {
				if(res[item.name]) res[item.name].push(+item.value);
				else res[item.name] = [+item.value];
			}
		}
		for(var k in res) if(res.hasOwnProperty(k)) {
			item = res[k].sort((a, b) => a - b);
			item = res[k] = {
				n: item.length,
				q25: quantile_sorted(item, 0.25),
				q50: quantile_sorted(item, 0.50),
				q75: quantile_sorted(item, 0.75)
			}
			item.range = item.q25 != item.q75 ? ('IQR: ' + item.q25 + ' - ' + item.q75) : ('Median: ' + item.q50);
		}
		return res;
	}
	function tooltip(item) {
		if(item) {
			var res = [], stat;
			if(item.range) res.push('Ref: ' + item.range + ' ' + item.unit + (item.flag ? ' [' + item.flag + ']' : ''));
			if(stat = component_stats[item.name]) res.push(stat.range + (item.range ? ' ' + item.unit : '') + (isNaN(parseFloat(item.value)) ? '' : item.value < stat.q25 ? ' [L]' : item.value > stat.q75 ? ' [H]' : ''));
			if(item.comment) {
				if(res.length > 0) res.push('');
				res.push(item.comment);
			}
			return res.join('\n');
		}
	};
	function abnormal_ref(item) {
		return (item) && (item.flag);
	};
	function abnormal_ref_low(item) {
		return (item) && (item.flag) && (item.flag.indexOf('L') >= 0);
	};
	function abnormal_ref_high(item) {
		return (item) && (item.flag) && (item.flag.indexOf('H') >= 0);
	};
	function abnormal_iqr(item) {
		var stat;
		if((item) && (stat = component_stats[item.name]) && (stat.n > 2)) {
			if((item.hasOwnProperty('min')) && (item.hasOwnProperty('max'))) return (item.min < stat.q25) || (item.max > stat.q75);
			else if(isNumeric(item.value)) return (item.value < stat.q25) || (item.value > stat.q75);
		}
	};
	function abnormal_iqr_low(item) {
		var stat;
		if((item) && (stat = component_stats[item.name]) && (stat.n > 2)) {
			if((item.hasOwnProperty('min')) && (item.hasOwnProperty('max'))) return item.min < stat.q25;
			else if(isNumeric(item.value)) return item.value < stat.q25;
		}
	};
	function abnormal_iqr_high(item) {
		var stat;
		if((item) && (stat = component_stats[item.name]) && (stat.n > 2)) {
			if((item.hasOwnProperty('min')) && (item.hasOwnProperty('max'))) return item.max > stat.q75;
			else if(isNumeric(item.value)) return item.value > stat.q75;
		}
	};

	function abstract(body) {
		let re = /^(\w.*?)[ \t]{2,}\S.*?[ \t]{2,}/gm, m, res = [];
		while(m = re.exec(body)) res.push(m[1]);
		re = /^\*[ ]+((?:.+?) REPORT) =>/gm;
		while(m = re.exec(body)) res.push(m[1]);
		return res;
	}

	async function loadmore(evt, factor = 1.5) {
		if(loadmore.loading) return;
		loadmore.loading = true;
		try {
			const next_offset = data.offset + (loadmore.limit = (factor*loadmore.limit)|0);
			const measurements = await get_api_measurements({ mrn: data.mrn, omega: 'T-' + (data.offset + 1), alpha: 'T-' + next_offset });
			Array.prototype.push.apply(component_items, decorate_measurements(measurements));
			const labs = await get_api_labs({ mrn: data.mrn, omega: 'T-' + (data.offset + 1), alpha: 'T-' + next_offset });
			Array.prototype.push.apply(component_items, decorate_labs(labs));
			component_items = component_items;	// reactivity hint
			data.offset = next_offset;
		} finally {
			loadmore.loading = false;
		}
	}
	loadmore.loading = false;
	loadmore.limit = 30;

	let header;
	(async function loadinit(target = 16, requests = 4) {
		await tick();
		for(let i = 0; (i < requests) && (component_groups.length < target); ++i) await loadmore();
		await tick();
		if((header) && (component_groups)) header.children[header.children.length - 1].scrollIntoView({ block: 'nearest', inline: 'end' });
	})();

	/*
	const observer = new IntersectionObserver((entries) => { if((!query) && (entries[0].isIntersecting)) loadmore(null); }, { root: null, rootMargin: '0px', threshold: 0.5 });
	let bottom = null;
	$: {
		observer.disconnect();
		if(bottom) observer.observe(bottom);
	}

	let reportlist;
	async function scroll(selection) {
		if(selection) {
			await tick();
			const el = reportlist.querySelector('.active');
			if((el) && (!isInViewport(el, true))) el.scrollIntoView({ block: 'center' });
		} else {
			const items = reportlist.children;
			for(let i = 0, el; i < items.length; ++i) if(isInViewport(el = items[i])) {
				await tick();
				el.scrollIntoView({ block: 'start' });
				break;
			}
		}
	}
	$: if(reportlist) scroll(selection);
	*/
</script>

<svelte:head>
	<title>Flowsheet</title>
</svelte:head>

{#if (selection) && (selection.length > 0)}
	<div class="halfpane rightpane">
		{#each selection as row, idx}
			<nav class="navbar bg-body-secondary">
				<div class="container-fluid">
					<span class="navbar-brand">{datetime_datestr(row._ts)}@{datetime_timestr(row._ts)} {row.accession} {row.specimen}</span>
					{#if idx == 0}<button type="button" class="btn btn-outline-light" on:click={() => selection = null}>❌</button>{/if}
				</div>
			</nav>
			<div class="container-fluid report">{@html pattern ? filter_mark(pattern, escapeHTML(row.body)) : escapeHTML(row.body)}</div>
		{/each}
	</div>
{/if}
<div class={(selection) && (selection.length > 0) ? 'halfpane leftpane' : ''}>
	<div style="font-family: monospace;" role="region" tabindex="0">
		<table class="table-sticky table-data">
			<thead>
				<tr bind:this={header}>
					<th class="corner">
						<input type="text" class="form-control" placeholder="Filter..." bind:value={query}>
						{#if !loadmore.loading}<span class="badge rounded-pill bg-primary position-absolute top-0 start-100 translate-middle-x" style="cursor: pointer;" on:click={loadmore}>More</span>{/if}
					</th>
					{#each component_groups as group, idx}<th class="date" class:first={ (idx == 0) || (group.key != component_groups[idx - 1].key) }><div class="year">{group.datestr.substring(0, 4)}</div><div class="monthdate">{group.datestr.substring(5)}</div>{#if false}<div class="hourminute daily">{datetime_timestr(group.values[0]._ts)}</div>{/if}</th>{/each}
				</tr>
			</thead>
			{#if pattern}
				<tbody>
					{#each component_names as name}
						{#if filter_test(pattern, name)}
							<tr class="match">
								<th>{@html pattern ? filter_mark(pattern, escapeHTML(name)) : escapeHTML(name)}</th>
								{#each component_groups as group, idx}
									<td class:first={ (idx == 0) || (group.key != component_groups[idx - 1].key) } class:abnormal-ref={abnormal_ref(group.values[name])} class:abnormal-ref-low={abnormal_ref_low(group.values[name])} class:abnormal-ref-high={abnormal_ref_high(group.values[name])} class:abnormal-iqr={abnormal_iqr(group.values[name])} class:abnormal-iqr-low={abnormal_iqr_low(group.values[name])} class:abnormal-iqr-high={abnormal_iqr_high(group.values[name])} title={tooltip(group.values[name])} on:click={() => selection = (group.values[name]) && (group.values[name].reports)}>{group.values[name] ? group.values[name].value : ''}</td>
								{/each}
							</tr>
						{/if}
					{/each}
				</tbody>
				<tbody>
					{#each component_names as name}
						{#if !filter_test(pattern, name)}
							<tr>
								<th>{name}</th>
								{#each component_groups as group, idx}
									<td class:first={ (idx == 0) || (group.key != component_groups[idx - 1].key) } class:abnormal-ref={abnormal_ref(group.values[name])} class:abnormal-ref-low={abnormal_ref_low(group.values[name])} class:abnormal-ref-high={abnormal_ref_high(group.values[name])} class:abnormal-iqr={abnormal_iqr(group.values[name])} class:abnormal-iqr-low={abnormal_iqr_low(group.values[name])} class:abnormal-iqr-high={abnormal_iqr_high(group.values[name])} title={tooltip(group.values[name])} on:click={() => selection = (group.values[name]) && (group.values[name].reports)}>{group.values[name] ? group.values[name].value : ''}</td>
								{/each}
							</tr>
						{/if}
					{/each}
				</tbody>
			{:else}
				<tbody>
					{#each component_names as name}
						<tr>
							<th>{name}</th>
							{#each component_groups as group, idx}
								<td class:first={ (idx == 0) || (group.key != component_groups[idx - 1].key) } class:abnormal-ref={abnormal_ref(group.values[name])} class:abnormal-ref-low={abnormal_ref_low(group.values[name])} class:abnormal-ref-high={abnormal_ref_high(group.values[name])} class:abnormal-iqr={abnormal_iqr(group.values[name])} class:abnormal-iqr-low={abnormal_iqr_low(group.values[name])} class:abnormal-iqr-high={abnormal_iqr_high(group.values[name])} title={tooltip(group.values[name])} on:click={() => selection = (group.values[name]) && (group.values[name].reports)}>{group.values[name] ? group.values[name].value : ''}</td>
							{/each}
						</tr>
					{/each}
				</tbody>
			{/if}
		</table>
	</div>
</div>

<style>
	:global(th mark) {
		padding: 0;
		font-weight: bold;
	}
	@media (prefers-reduced-motion: no-preference) {
		:root {
			scroll-behavior: auto;
		}
	}
	[role="region"][tabindex] {
		max-height: calc(100vh - 4.5rem);
		overflow: auto;
	}
	table {
		width: 100%;
	}
	table tr.match th:first-child {
		background-color: #c6def1;
	}
	table th, table td {
		padding: 0 0.5rem;
		white-space: nowrap;
		text-align: center;
	}
	table.table-sticky thead {
		position: sticky;
		z-index: 1020;
		top: 0;
		background-color: #fff;
	}
	table.table-sticky th:first-child {
		position: sticky;
		z-index: 1010;
		left: 0;
		background-color: #fff;
	}
	table.table-sticky thead th.corner {
		padding: 0;
	}
	.navbar {
		position: sticky;
		z-index: 1020;
		top: 3.5rem;
	}
	.leftpane {
		display: none;
	}
	li.active {
		scroll-margin-top: 3.5rem;
	}
	div.singleline {
		white-space: nowrap;
		overflow: hidden;
		text-overflow: ellipsis;
	}
	.abstract {
		font-size: 0.8em;
	}
	.snippets {
		font-family: monospace;
	}
	.report {
		font-family: monospace;
		white-space: pre-wrap;
	}

	table.table-sticky {
		border: 2px solid #dee2e6;
	}
	table.table-sticky th:first-child {
		border-right: 2px solid #dee2e6;
	}
	table.table-sticky tbody {
		border-top: 2px solid #dee2e6;
	}
	table.table-sticky tbody tr {
		border-top: 1px dashed #dee2e6;
	}
	table.table-sticky tbody tr:hover {
		border: 1px solid #6c757d;
	}
	td:nth-of-type(odd) {
		background-color: rgba(0, 0, 0, 0.05);
	}
	table.table-sticky tbody th, table.table-sticky th.name {
		cursor: default;
		font-weight: bold;
		text-align: center;
		padding-left: 1rem;
		padding-right: 1rem;
	}
	table.table-sticky th.filtered {
		background-color: #6c757d;
		color: #fff;
	}
	table.table-sticky th.date {
		cursor: default;
		font-size: 80%;
		font-weight: normal;
		text-align: center;
	}
	table.table-sticky th.date .monthdate {
		font-size: 125%;
		font-weight: bold;
	}
	table.table-sticky th.date .hourminute.daily {
		display: none;
	}
	table.table-sticky tbody td {
		padding: 0 0.5rem;
		max-width: 12rem;
		overflow: hidden;
		text-overflow: ellipsis;
	}
	.first {
		border-left: 1px solid #dee2e6;
	}
	.year, .monthdate {
		visibility: hidden;
	}
	.first .year, .first .monthdate {
		visibility: visible;
	}

	table.table-data .abnormal-ref::after {
		content:  ' *';
	}
	table.table-data .abnormal-ref-low::after {
		content:  ' L';
	}
	table.table-data .abnormal-ref-high::after {
		content:  ' H';
	}
	table.table-data .abnormal-iqr, table.table-data .abnormal-iqr-low.abnormal-iqr-high {
		color: #f39a27;
	}
	table.table-data .abnormal-iqr-low {
		color: #976ed7;
	}
	table.table-data .abnormal-iqr-high {
		color: #c23b23;
	}
	table.table-data .abnormal-ref, table.table-data .abnormal-iqr {
		background-color: #fbffde;
	}
	table.table-data .abnormal-ref-low.abnormal-iqr-low, table.table-data .abnormal-ref-high.abnormal-iqr-high {
		font-weight: bold;
		background-color: #ffd1d1;
	}

	@media screen and (min-width: 720px) {
		.halfpane {
			position: absolute;
			top: 3.5rem;
			bottom: 0;
			width: 50%;
			overflow: auto;
		}
		.leftpane {
			display: block;
			left: 0;
			z-index: -1;
		}
		.leftpane [role="region"][tabindex] {
			max-height: calc(100vh - 3.5rem);
			direction: rtl;
		}
		.leftpane [role="region"][tabindex] > * {
			direction: ltr;
		}
		.rightpane {
			right: 0;
			box-shadow: var(--bs-box-shadow);
		}
		.halfpane .navbar {
			top: 0;
		}
	}
</style>
