<template>
	<Subtitle value="Reports" />
	<Subtitle :value="patient_info.name" />
	<div class="filter card mb-3 shadow" :class="{ 'list-wide': !selection }">
		<ul class="list-group list-group-flush">
			<li class="list-group-item">
				<div class="input-group">
					<span class="input-group-text">🔎</span>
					<input type="text" class="form-control" placeholder="Filter" v-model="x_query" />
					<button v-if="x_query" class="btn btn-outline-secondary" @click="x_query = ''">❌</button>
				</div>
			</li>
			<li class="list-group-item d-flex justify-content-between align-items-center">
				<div class="btn-group">
					<button v-for="report in reports" class="btn" :class="{ 'btn-primary': report.enabled, 'btn-outline-primary': !report.enabled }" @click="enable(report)">{{report.name}}<input type="checkbox" class="form-check-input" :checked="report.enabled" @click.stop="report.enabled = !report.enabled" /></button>
				</div>
				<DateRangePicker range="Range" direction="-1" v-model:date="date_end" v-model:date_end="date_begin" />
			</li>
		</ul>
	</div>
	<div class="row">
		<div class="selector col-12" :class="{ 'col-xl-4': selection }">
			<div class="card mb-3 shadow">
				<div class="card-header"><template v-if="resultset.length > 0"><template v-if="resultset.length > rs_filtered.length">{{rs_filtered.length}} of </template>{{resultset.length}}</template><template v-else-if="is_loading">Loading</template><template v-else>No</template> record{{resultset.length == 1 ? '' : 's'}}</div>
				<ul class="scroller list-group list-group-flush" :class="{ 'list-skinny': selection }" ref="scroller">
					<li v-for="item in rs_filtered" :key="item" class="record list-group-item" :class="{ 'active': (selection) && (selection.id == item.id) }" @click="selection = item"><span class="badge emblem" :class="[item.emblem]" /> <span class="datetime date">{{datestring(item.time)}}</span> <span class="datetime time">{{timestring(item.time)}}</span> • <span class="title"><span v-for="title in item.title">{{title}}</span></span><template v-if="item.snippets"><div v-for="snippet in item.snippets" class="snippet" v-html="snippet" /></template></li>
					<li class="bottom list-group-item" ref="bottom"><button v-if="date_next" class="btn btn-outline-primary" :disabled="is_loading" @click="date_begin = date_next"><template v-if="is_loading">Loading</template><template v-else>Load</template> back to {{datestring(date_next)}}<template v-if="is_loading">…</template></button></li>
				</ul>
			</div>
		</div>
		<div v-if="selection" class="col-12 col-xl-8">
			<div class="card mb-3 shadow">
				<div class="card-header d-flex justify-content-between align-items-center">
					<span>{{selection.title.join(' - ')}}</span>
					<span class="close" @click="selection = null">❌</span>
				</div>
				<div class="detail card-body" v-html="selection.highlight || selection.detail" />
			</div>
		</div>
	</div>
</template>

<style>
	ul.scroller span.highlight, div.detail span.highlight {
		background-color: #ff0;
	}
	li.record.active span.highlight {
		color: #000;
	}
</style>

<style scoped>
	div.filter.list-wide {
		position: sticky;
		top: 3.65rem;
		z-index: 2;
	}
	div.filter input.form-check-input {
		position: absolute;
		top: 0;
		right: 0;
		margin-top: 0;
	}
	div.selector {
		position: sticky;
		top: 1.15rem;
		z-index: 1;
	}
	ul.scroller.list-skinny {
		max-height: 25vh;
		overflow-y: auto;
	}
	li.record {
		cursor: default;
		border-top: 1px solid #dee2e6;
		padding: 0.25rem 0.75rem;
	}
	li.record:nth-child(even) {
		background-color: rgba(0, 0, 0, 0.05);
	}
	li.record.active {
		color: #fff;
		background-color: #0d6efd;
	}
	li.bottom button {
		width: 100%;
	}
	span.badge.emblem:empty {
		display: inline-block;
		font-family: monospace;
	}
	span.badge.emblem-notes {
		background-color: var(--bs-purple);
	}
	span.badge.emblem-notes::after {
		content: 'N';
	}
	span.badge.emblem-labs {
		background-color: var(--bs-pink);
	}
	span.badge.emblem-labs::after {
		content: 'L';
	}
	span.badge.emblem-microbiology {
		background-color: var(--bs-orange);
	}
	span.badge.emblem-microbiology::after {
		content: 'M';
	}
	span.badge.emblem-bloodbank {
		background-color: var(--bs-red);
	}
	span.badge.emblem-bloodbank::after {
		content: 'B';
	}
	span.badge.emblem-pathology {
		background-color: var(--bs-yellow);
	}
	span.badge.emblem-pathology::after {
		content: 'P';
	}
	span.badge.emblem-radiology {
		background-color: var(--bs-green);
	}
	span.badge.emblem-radiology::after {
		content: 'R';
	}
	span.datetime, span.title span:first-child {
		font-weight: bold;
	}
	span.title span:not(:first-child)::before {
		content: ' - ';
	}
	ul.scroller.list-skinny span.datetime.time {
		display: none;
	}
	div.snippet {
		overflow: hidden;
		white-space: nowrap;
		text-overflow: ellipsis;
	}
	div.snippet::before, div.snippet::after {
		content: '…';
	}
	span.close {
		cursor: default;
	}
	div.detail {
		font-family: monospace;
		white-space: pre-wrap;
	}
	@media (min-width: 1200px) {
		div.selector {
			position: static;
		}
		ul.scroller.list-skinny {
			max-height: 75vh;
		}
		div.detail {
			max-height: 75vh;
			overflow-y: auto;
		}
	}
</style>

<script>
	import { flow, uniq, debounce, strftime_vista, strptime_vista } from './util.mjs';

	import Subtitle from './Subtitle.vue';
	import DateRangePicker from './DateRangePicker.vue';

	const SZ_WINDOW = 100;
	const SZ_RANGE = 40000;

	function f_parse_columns(item) {
		var res = [], line, brk, sub;
		for(var i = 0; i < item.length; ++i) {
			brk = (line = item[i]).indexOf('^');
			if(brk >= 0) {
				if(res[sub = line.substring(0, brk)]) res[sub].push(line.substring(brk + 1));
				else res[sub] = [line.substring(brk + 1)];
			}
		}
		for(var k in res) if(res[k]) res[k] = res[k].join('\n');
		return res;
	}

	const create_reports = () => [
		{
			name: 'Notes',
			rpt_id: 'OR_PN:PROGRESS NOTES~TIUPRG;ORDV04;15;',
			map: flow(f_parse_columns, function(x) {
				var time = new Date(x[3]);
				return {
					time,
					id: 'OR_PN:' + time.getTime() + ':' + x[2],
					emblem: 'emblem-notes',
					title: [x[4], x[5], '#' + x[2]],
					detail: escape_html(x[6])
				};
			}),
			loader: reportloader_chunk,
			enabled: true
		},
		{
			name: 'Labs',
			rpt_id: 'OR_OV_R:LAB OVERVIEW (COLLECTED SPECIMENS)~OV;ORDV02C;32;',
			map: flow(f_parse_columns, function(x) {
				var time = new Date(x[2]);
				return {
					time,
					id: 'OR_OV_R:' + time.getTime() + ':' + x[12],
					emblem: 'emblem-labs',
					title: [x[3], x[6], x[8], x[10], '#' + x[12]],
					detail: escape_html(x[15])
				};
			}),
			loader: reportloader_alpha
		},
		{
			name: 'Microbiology',
			rpt_id: 'OR_MIC:MICROBIOLOGY~MI;ORDV05;38;',
			map: flow(f_parse_columns, function(x) {
				var time = new Date(x[2]);
				return {
					time,
					id: 'OR_MIC:' + time.getTime() + ':' + x[6],
					emblem: 'emblem-microbiology',
					title: [x[3], x[4], x[5], '#' + x[6]],
					detail: escape_html(x[7])
				};
			}),
			loader: reportloader_chunk
		},
		{
			name: 'Blood Bank',
			rpt_id: '2:BLOOD BANK REPORT~;;0',
			singleton: true,
			map(x) {
				var now = new Date();
				return {
					time: new Date(now.getFullYear(), now.getMonth(), now.getDate()),
					id: 'BB',
					emblem: 'emblem-bloodbank',
					title: ['BLOOD BANK'],
					detail: escape_html(x.join('\n'))
				};
			},
			loader: reportloader_full
		},
		{
			name: 'Pathology',
			rpt_id: 'OR_APR:ANATOMIC PATHOLOGY~SP;ORDV02A;0;',
			map: flow(f_parse_columns, function(x) {
				var time = new Date(x[2]);
				return {
					time,
					id: 'OR_APR:' + time.getTime() + ':' + x[4],
					emblem: 'emblem-pathology',
					title: [x[3], '#' + x[4]],
					detail: escape_html(x[5])
				};
			}),
			loader: reportloader_full
		},
		{
			name: 'Radiology',
			rpt_id: 'OR_R18:IMAGING~RIM;ORDV08;0;',
			map: flow(f_parse_columns, function(x) {
				var time = new Date(x[2]);
				return {
					time,
					id: 'OR_R18:' + time.getTime() + ':' + x[9],
					emblem: 'emblem-radiology',
					title: [x[3], x[4], x[5], '#' + x[9]],
					detail: escape_html(x[6])
				};
			}),
			loader: reportloader_chunk
		},
	];

	function data_limit(data, dt_alpha, dt_omega) {
		for(var i = 0, x, none = true; i < data.length; ++i) if(((x = data[i]).time <= dt_omega) || (!x.time) || (isNaN(x.time))) {
			data = data.slice(i);
			none = false;
			break;
		}
		if(none) return [];
		for(var i = data.length - 1, x, none = true; i >= 0; --i) if(((x = data[i]).time >= dt_alpha) || (!x.time) || (isNaN(x.time))) return i < data.length - 1 ? data.slice(0, i + 1) : data;
		return [];
	}

	function data_endtime(data) {
		for(var i = data.length - 1; i >= 0; --i) if((data[i].time) && (!isNaN(data[i].time))) return data[i].time;
	}

	function data_interval(data) {
		data = data.slice(data.length - SZ_WINDOW - 1).filter(x => (x.time) && (!isNaN(x.time)));
		return data.length > 1 ? (data[0].time - data[data.length - 1].time)/(data.length - 1)*SZ_WINDOW : 86400000*SZ_WINDOW;
	}

	function reportloader_full(dfn, rpt_id, fn_map, omega) {
		var cachekey = dfn + ';' + rpt_id, dt_omega = strptime_vista(omega), data = null, dt_end;
		async function fn(client, alpha) {
			var dt_alpha = strptime_vista(alpha);
			if(!data) {
				data = (reportloader_full[cachekey] || (reportloader_full[cachekey] = await client.ORWRP_REPORT_TEXT(dfn, rpt_id, '', SZ_RANGE, '', -1, -1))).map(fn_map).sort((a, b) => b.time - a.time), dt_end = null;
				dt_end = data_endtime(data);
			}
			var res = alpha !== undefined ? data_limit(data, dt_alpha, dt_omega) : [];
			if((data.length > 0) && ((dt_alpha > dt_end) || (alpha === undefined)) && ((res.length < 1) || (res[res.length - 1] !== data[data.length - 1]))) res.next = strftime_vista(res.dt_next = dt_end);	// lookahead
			return res;
		}
		fn.omega = omega;
		return fn;
	}

	function reportloader_alpha(dfn, rpt_id, fn_map, omega) {
		var dt_omega = strptime_vista(omega), cursor = Math.floor(strftime_vista(new Date())) + 0.235959999, interval = 86400000*365*2, data = [], dt_end, hasmore = true;
		async function fn(client, alpha) {
			var dt_alpha = strptime_vista(alpha);
			if(alpha !== undefined) {
				if((hasmore) && (cursor >= alpha)) {
					while(cursor >= alpha) cursor = Math.floor(strftime_vista(new Date(strptime_vista(cursor) - interval)));
					data = (await client.ORWRP_REPORT_TEXT(dfn, rpt_id, '', SZ_RANGE, '', cursor, -1)).map(fn_map).sort((a, b) => b.time - a.time);
					dt_end = data_endtime(data);
				}
				var res = data_limit(data, dt_alpha, dt_omega);
			} else var res = [];
			if((data.length > 0) && ((dt_alpha > dt_end) || (alpha === undefined)) && ((res.length < 1) || (res[res.length - 1] !== data[data.length - 1]))) res.next = strftime_vista(res.dt_next = dt_end);	// lookahead
			else if(hasmore) {
				var count = data.length;
				while(interval <= 86400000*365*8) {
					cursor = Math.floor(strftime_vista(new Date(strptime_vista(cursor) - interval)));
					data = (await client.ORWRP_REPORT_TEXT(dfn, rpt_id, '', SZ_RANGE, '', cursor, -1)).map(fn_map).sort((a, b) => b.time - a.time);
					if(data.length > count) {
						res.next = strftime_vista(res.dt_next = dt_end = data_endtime(data));
						return res;
					} else interval *= 2;
				}
				data = (await client.ORWRP_REPORT_TEXT(dfn, rpt_id, '', SZ_RANGE, '', -1, -1)).map(fn_map).sort((a, b) => b.time - a.time);
				cursor = Math.floor(res.next = strftime_vista(res.dt_next = dt_end = data_endtime(data)));
				hasmore = false;
			}
			return res;
		}
		fn.omega = omega;
		return fn;
	}

	function reportloader_omega(dfn, rpt_id, fn_map, omega) {
		var dt_omega = strptime_vista(omega), cursor = Math.floor(omega) + 0.235959999, data = [], idmap = {};
		async function fn(client, alpha) {
			var dt_alpha = strptime_vista(alpha), batch;
			if(cursor >= alpha) {
				batch = (await client.ORWRP_REPORT_TEXT(dfn, rpt_id, '', SZ_RANGE, '', Math.floor(alpha).toFixed(9), cursor.toFixed(9))).map(fn_map).sort((a, b) => b.time - a.time);
				cursor = strftime_vista(new Date(strptime_vista(Math.floor(alpha)) - 86400000)) + 0.235959999;
				batch = batch.filter(x => idmap[x.id] ? console.warn('Duplicate record', x) : true);
				batch.reduce((acc, val) => (acc[val.id] = val, acc), idmap);
				Array.prototype.push.apply(data, batch);
			}
			var dt_cursor = strptime_vista(cursor), interval = data_interval(data);	// lookahead
			while(((batch = (await client.ORWRP_REPORT_TEXT_LONGCACHE(dfn, rpt_id, '', SZ_RANGE, '', Math.floor(strftime_vista(new Date(dt_cursor - interval))).toFixed(9), cursor.toFixed(9))).map(fn_map)).length < SZ_WINDOW) && (interval <= 86400000*SZ_RANGE)) interval *= 2;
			var res = alpha !== undefined ? data_limit(data, dt_alpha, dt_omega) : [];
			res.next = strftime_vista(res.dt_next = new Date(dt_cursor - interval));
			return res;
		}
		fn.omega = omega;
		return fn;
	}

	function reportloader_chunk(dfn, rpt_id, fn_map, omega) {
		var dt_omega = strptime_vista(omega), cursor = Math.floor(omega) + 0.235959999, data = [], idmap = {}, hasmore = true;
		if(rpt_id.endsWith(';')) rpt_id += SZ_WINDOW;
		async function fn(client, alpha) {
			if(alpha !== undefined) {
				var dt_alpha = strptime_vista(alpha), batch;
				while((hasmore) && (cursor >= alpha)) {
					batch = (await client.ORWRP_REPORT_TEXT(dfn, rpt_id, '', SZ_RANGE, '', -1, cursor.toFixed(9))).map(fn_map).sort((a, b) => b.time - a.time);
					cursor = data_endtime(batch); if(cursor) cursor = strftime_vista(cursor);
					hasmore = (cursor) && (batch.length >= SZ_WINDOW);
					batch = batch.filter(x => idmap[x.id] ? console.warn('Duplicate record', x) : true);
					batch.reduce((acc, val) => (acc[val.id] = val, acc), idmap);
					Array.prototype.push.apply(data, batch);
				}
				var res = data_limit(data, dt_alpha, dt_omega);
			} else var res = [];
			if(hasmore) {	// lookahead
				var batch = (await client.ORWRP_REPORT_TEXT_LONGCACHE(dfn, rpt_id, '', SZ_RANGE, '', -1, cursor.toFixed(9))).map(fn_map).sort((a, b) => b.time - a.time);
				res.dt_next = data_endtime(batch);
				if(res.dt_next) res.next = strftime_vista(res.dt_next);
			}
			if((!res.dt_next) && (cursor) && (data.length > 0) && ((res.length < 1) || (res[res.length - 1] !== data[data.length - 1]))) res.dt_next = strptime_vista(res.next = cursor);
			return res;
		}
		fn.omega = omega;
		return fn;
	}

	const escape_div = document.createElement('div');
	function escape_html(s) {
		escape_div.textContent = s;
		return escape_div.innerHTML;
	}

	function snippets(text, regex, replacement) {
		var res = [], context = new RegExp('(?:\\S+\\s+){0,3}\\S*(' + regex.source + ')\\S*(?:\\s+\\S+){0,3}', regex.flags), match;
		if(context.global) while((match = context.exec(text)) !== null) res.push(match[0].replace(regex, replacement).replace(/\s+/g, ' ').replace(/([\W_])\1{2,}/g, '$1$1'));
		else if((match = context.exec(text)) !== null) res.push(match[0].replace(regex, replacement).replace(/\s+/g, ' ').replace(/([\W_])\1{2,}/g, '$1$1'));
		return uniq(res);
	}

	export default {
		components: {
			Subtitle, DateRangePicker
		},
		props: {
			client: Object,
			sensitive: Boolean,
			patient_dfn: String,
			patient_info: Object
		},
		data() {
			var now = new Date();
			return {
				dfn: null,
				is_loading: false,
				date_end: now,
				date_begin: now,
				date_next: null,
				query: '',
				x_query: '',
				reports: create_reports(),
				loaders: {},
				resultsets: {},
				selection: null,
				observer_scroller: null,
				observer_viewport: null
			};
		},
		computed: {
			resultset() {
				return this.reports.map((x, i) => x.enabled ? this.resultsets[i] : null).filter(x => x).reduce((acc, val) => (Array.prototype.push.apply(acc, val), acc), []).sort((a, b) => b.time - a.time);
			},
			rs_filtered() {
				var query = this.query.replace(/^\s+|\s+$/g, '');
				if(query.length > 0) {
					if(query.startsWith('"')) {
						query = query.substring(1, query.length - ((query.length > 1) && (query.endsWith('"')) ? 1 : 0));
						if(query.length > 0) {
							query = new RegExp(query.replace(/\s+/g, ' ').split(' ').map(x => x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('\\s*'), 'gims');
							return this.resultset.filter(x => (x.detail) && (query.test(x.detail))).map(x => Object.assign({ snippets: snippets(x.detail, query, '<span class="highlight">$&</span>'), highlight: x.detail.replace(query, '<span class="highlight">$&</span>') }, x));
						}
					} else if(query.startsWith('/')) {
						if(query.length > 1) {
							var m = /^\/(.*)\/([a-z]*)$/.exec(query);
							query = m ? new RegExp(m[1], m[2]) : new RegExp(query.substring(1), 'gims');
							return this.resultset.filter(x => (x.detail) && (query.test(x.detail))).map(x => Object.assign({ snippets: snippets(x.detail, query, '<span class="highlight">$&</span>'), highlight: x.detail.replace(query, '<span class="highlight">$&</span>') }, x));
						}
					} else {
						query = new RegExp(query.replace(/\s+/g, ' ').split(' ').map(x => '\\b' + x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('\\S*(?:\\s+\\S+){0,5}\\s+'), 'gims');
						return this.resultset.filter(x => (x.detail) && (query.test(x.detail))).map(x => Object.assign({ snippets: snippets(x.detail, query, '<span class="highlight">$&</span>'), highlight: x.detail.replace(query, '<span class="highlight">$&</span>') }, x));
					}
				}
				return this.resultset;
			}
		},
		watch: {
			rs_filtered(value) {
				if((value) && (this.selection)) {
					var id = this.selection.id;
					for(var i = 0; i < value.length; ++i) if(value[i].id == id) return this.selection = value[i];
					this.selection = null;
				}
			}
		},
		methods: {
			strftime_vista,
			datestring(date) {
				return date.toLocaleDateString('sv-SE');
			},
			timestring(date) {
				return date.toLocaleTimeString('en-GB').substring(0, 5);
			},
			enable(report) {
				if(!report.enabled) {
					var reports = this.reports;
					for(var i = reports.length - 1; i >= 0; --i) reports[i].enabled = false;
					report.enabled = true;
				}
			},
			async loader_setup() {
				try {
					this.is_loading = true;
					if((this.client) && (this.patient_dfn)) {
						if(this.dfn != this.patient_dfn) {
							this.dfn = this.patient_dfn;
							this.loaders = {};
							this.resultsets = {};
						}
						var dfn = this.patient_dfn, loaders = this.loaders, resultsets = this.resultsets, reports = this.reports, report, omega = strftime_vista(this.date_end), alpha = this.date_begin != this.date_end ? strftime_vista(this.date_begin) : undefined, next = [];
						for(var i = 0; i < reports.length; ++i) if((report = reports[i]).enabled) {
							if((!loaders[i]) || (loaders[i].omega != omega)) loaders[i] = report.loader(dfn, report.rpt_id, report.map, omega);
							resultsets[i] = await loaders[i](this.client, alpha);
							if(resultsets[i].next) next.push(resultsets[i].next);
						}
						if(next.length > 0) {
							this.date_next = strptime_vista(Math.floor(Math.max(...next)));
							if(!alpha) this.date_begin = this.date_next;
						}
					} else {
						this.dfn = null;
						this.loaders = {};
						this.resultsets = {};
					}
				} catch(ex) {
					console.warn(ex);
				} finally {
					this.is_loading = false;
				}
			}
		},
		created() {
			this.$watch(
				() => (this.client, this.patient_dfn, this.reports.map(x => x.enabled), this.date_begin, this.date_end, {}),
				debounce(() => this.loader_setup(), 500),
				{ immediate: true }
			);
			this.$watch(
				() => this.x_query,
				debounce(value => this.query = value, 500),
				{ immediate: true }
			);
		},
		mounted() {
			this.observer_scroller = new IntersectionObserver(() => { if((this.date_next) && (!this.query.replace(/^\s+|\s+$/g, '')) && (this.selection)) this.date_begin = this.date_next; }, { root: this.$refs.scroller, rootMargin: '25%' });
			this.observer_scroller.observe(this.$refs.bottom);
			this.observer_viewport = new IntersectionObserver(() => { if((this.date_next) && (!this.query.replace(/^\s+|\s+$/g, '')) && (!this.selection)) this.date_begin = this.date_next; }, { rootMargin: '25%' });
			this.observer_viewport.observe(this.$refs.bottom);
		},
		destroyed() {
			if(this.observer_viewport) this.observer_viewport.disconnect();
			if(this.observer_scroller) this.observer_scroller.disconnect();
		}
	};
</script>
