import * as util from "./util.js";
import * as rs from "./roomscanner.js"
import {EventCollection, PAST} from "../time/events.js";
import {EVENT_BOOKING, RSSchedule} from "./util.js";
import {BookingMode, RegistrationMode, InstantStudySessionsMode} from "./mode.js";

/**
 *   Page Elements - See flx_RoomScanner.tpl:
 *
 *   +--------+-------------------------------+
 *   |        ||         HEADLINE            ||
 *   |  ICON  ||        SUB-HEADER           ||
 *   |        |-------------------------------+
 *   +--------+     +---------------------+   |
 *   | +---------+  |                     |   |
 *   | | CAMERA  |  |     INFO PANEL      |   |
 *   | | PREVIEW |  |   (Registration,    |   |
 *   | |         |  |     Activities)     |   |
 *   | +---------+  |                     |   |
 *   | |  SCAN   |  |       [MAP]         |   |
 *   | | MESSAGE |  |   [LOADING MSG]     |   |
 *   | +---------+  +---------------------+   |
 *   | SESSIONINFO (Timer)      OFFLINE MSG   |
 *   +----------------------------------------+
 *
 * See notion page "QR Scanner Self Service Room Mode" for more
 * detail and explanation of page states.
 */

export const ACTIVATE_ANIMATION_LENGTH_MS = 750;
export const FADEOUT_LENGTH_MS  = 480;
export const LOADING_GRACE_MS   = 250;

export const STYLE_SUCCESS = 'success';
export const STYLE_WARNING = 'warning';

/**
 * These represent the states or views the Info Panel can take.
 * @return {string}
 * @constructor
 */
export const INFOPANEL_STATE_ACTIVITIES    = 'INFOPANEL_STATE_ACTIVITIES';
export const INFOPANEL_STATE_REGISTRATION  = 'INFOPANEL_STATE_REGISTRATION';

const DEFAULT_MAX_EVENTS_ON_SCREEN   = 3;
const ICON_ROLL_ANIMATION_LENGTH_MS  = 750;
const ICON_FONT_SIZE_REM             = 3;

export const EVENT_NAME_CHAR_LIMIT        = 35;
export const EVENT_NAME_CHAR_LIMIT_SHORT  = 24; // Used when a room reference is shown alongside the event
const ROOM_NAME_CHAR_LIMIT = 12;

const SCREENSAVER_DEACTIVATE_MINS   = 10;     //- Minutes until we "deactivate" the screen.
const SCREENSAVER_FADEOUT_MINS      = 30;     //- Minutes until we fade the screen. Note, this counter will only start
                                              //  ticking if the screen is deactivated or if we're in register mode.

// Default options when rendering activity grid:
const DEFAULT_ACTIVITY_RENDER_OPTIONS      = {
	highlightCurrentEvent: true, // Highlight background colour for current event.
	urgencyWarning: true,        // Args: TRUE: Tell the user to "hurry!" if they have something going on now.
	                             //       FALSE: Inform them they have an activity right now, with a less severe warning
};

/**
 * Manage UI elements on the scanner page.
 */
export class Page {

	/**
	 * @param {RoomScanner} roomScanner
	 */
	constructor(roomScanner) {

		this.roomScanner = roomScanner;

		this.html = document.querySelector('html');
		this.sessioninfo = document.querySelector('#sessioninfo');
		this.subheader = document.querySelector('#subheader');
		this.pageicon = document.querySelector('#pageicon');
		this.loadingMessage = document.querySelector('#loading-message');
		this.map = document.querySelector('#map');
		this.activities = document.querySelector('#activities');
		this.offlineMessage = document.querySelector('#offline-message');
		this.scanConfirmation = document.querySelector('#scan-confirmation');

		this.registration = document.querySelector('#registration');
		this.registrationName = document.querySelector('#name');
		this.registrationPhoto = document.querySelector('#photo');
		this.registrationMark = document.querySelector('#mark');

		// --- Screen Saver ---

		// Controls screen deactivate and fade out events.
		this._screenSaver = new ScreenSaver(
			this, SCREENSAVER_DEACTIVATE_MINS, SCREENSAVER_FADEOUT_MINS
		);

		// --- Info panel ---
		this.infoPanel = document.querySelector('#info');
		this.infoPanelState = INFOPANEL_STATE_ACTIVITIES;
		this.infoPanelNextState = this.infoPanelState;
		this.infoPanelFadePromise = null;
		this.infoPanelCbs = [];

		// --- Activities ---
		// Store here as these options should be recycled if not deliberately altered.
		this.activityRenderOptions = Object.assign({}, DEFAULT_ACTIVITY_RENDER_OPTIONS);
		this.singleUserActivity = null;
		this._maxEventsOnScreen = null;

		// --- Icon ---
		this.iconRolling = false;
		this.defaultIcon = null;
		this.temporaryIcon = null;

		// --- Page Style ---
		this.pageCss = null;
		this.defaultPageCss = null;

		// --- Misc ---
		this.roomSchedule = false;
		this.userSchedule = false;
		this.loading = false;
		this.loadingPromise = null;
		this.mapData = null;
		this.online = true;
		/**
		 * Is the screen currently activating? if so, return a promise which resolves when the screen
		 * has finished activating.
		 * @type {Promise|boolean}
		 */
		this.activating = false;

		// -- Page Components ---
		// These are created at the end as some use `this` instance as a dependency.
		this.scanMessage = new ScanMessage(
			document.querySelector('#scan-message'),
			document.querySelector('#scan-message').parentElement,
			this.roomScanner.getVocab()
		);

		this.headline = new Headline(document.querySelector('#headline'), this);

		// --- Populate Screen ---
		this._initialiseScreen();
	}

	/**
	 * @return {ScreenSaver}
	 */
	get screenSaver() {
		return this._screenSaver;
	}


	/**
	 * Update the page to reflect current scanner mode.
	 *
	 * Note: The ScannerMode is pushed to the page instead of being pulled (e.g. getMode) to avoid tight
	 * coupling e.g. RoomScanner <-> ScanMessage.
	 *
	 * @param {Mode} mode
	 */
	setScannerMode(mode) {
		this.scanMessage.setScannerMode(mode);
		this.headline.setScannerMode(mode);
	}

	/**
	 * Set a temporary scan message. This will clear when the page countdown hits 0.
	 * @param {string|null} msg
	 * @param {string|null} style STYLE_* e.g. STYLE_SUCCESS.
	 */
	setScanMessage(msg, style = null) {
		if (!msg && !style) { // If neither msg or style are set, return.
			return;
		}
		if (!msg) { // If only STYLE is set, then apply a (temporary) style to the current message.
			msg = this.scanMessage.getMessage();
		}
		this.scanMessage.setTemporaryMessage(msg, style);
	}


	/**
	 * @see this.scanMessage.getRenderLimit()
	 */
	getScanMessageLimit() {
		return this.scanMessage.getRenderLimit();
	}

	fade(f = true) {
		// sto@03/07/19 Disabled as this is going to be replaced with a better screen saver in the future.
		//this.html.style.backgroundColor = f ? '#000' : '#5d5d5d';
		//document.body.classList.toggle('semi-fadeout', f);
	}

	/**
	 * Clear any data shown on screen about the user. This mainly concerns
	 * text data or any HTML data that does not need to have `display: none`
	 * to be made hidden.
	 * Note - the icon state should be handled outside this function so it animates
	 * at the same time as the fade sequence.
	 */
	clearUserData() {
		this.setSingleActivity(false); // Disable "SingleActivity" render.
		this.setActivities(null, false, {});
		this.setRegistrationVisible(false); // Make any register content invisible.
		this.setDataTimeout(-1); // -1 Means hide the timeout html
		this.setPageCss(null);
		this.setScanConfirmationVisible(false);
		this.scanMessage.clearTemporaryMessage();
		this.headline.clearTempAppearance();
	}

	/**
	 * Activate/deactivated the scanner screen.
	 * "Deactivated" is a simplified view of the scanner, with only a centered camera preview
	 * "Activate" is whole the scanner view, which includes activity/register info, screen timeout info etc.
	 * @async
	 * @param newActiveState
	 * @public
	 * @return {Promise}
	 */
	async setActive(newActiveState = true) {

		// If we're transitioning state, not just repeating the same state
		if (this.isActive() !== newActiveState) {

			this.toggleIconRoll();

			document.body.classList.toggle('active', newActiveState);

			// Transition: 'deactivated' --> 'active'
			if (newActiveState) {
				// Alter screen active state via class "active" on body;

				// Wait the activation time length before proceeding:
				this.activating = getDelay(ACTIVATE_ANIMATION_LENGTH_MS);
				await this.activating;

				// The ScanMessage will resize differently depending on the page active state.
				// Force a re-size here to ensure the element is calculated properly for the now 'active' state.
				this.scanMessage.resizeBox();
			}

			this.activating = false;

			// Note - these classes should always be taken off when playing the activate/deactivate animations
			// as the `font-size` transition interferes with the screen transition.
			// These will remain off until the screen is fully active once more.
			this.headline.animated = newActiveState;
			this.subheader.classList.toggle('animated', newActiveState);
		}

		this.fade(false);
	}

	/**
	 * Show a loading message at the bottom of the info-box.
	 * @param {boolean} loading
	 * @param {int} gracePeriodMs Milliseconds before we actually start showing the loading message.
	 *              If `setLoading` is called again with `loading = false` before the grace period has been
	 *              been passed, then the "Loading" message will never be shown at all.
	 */
	setLoading(loading, gracePeriodMs = LOADING_GRACE_MS) {
		this.loading = loading;

		// Wait for any other loading promises to finish.
		if (this.loadingPromise) {
			return;
		}

		if (this.loading) {
			// Schedule the load message to show after the grace period
			getDelay(gracePeriodMs).then(() => {
				this.loadingMessage.classList.toggle('pulse', this.loading);
				// Clear the current promise so new ones can be made.
				this.loadingPromise = null;
			});
		} else {
			// Hide loading messages immediately - grace period not required.
			this.loadingMessage.classList.toggle('pulse', false);
		}
	}

	isActive() {
		return document.body.classList.contains('active');
	}

	getRoom() {
		return this.roomScanner.getRoom();
	}

	/**
	 * Returns a promise while the screen is transitioning from `deactivated` to `active`.
	 * FALSE otherwise.
	 * @return {Promise|false}
	 */
	isActivating() {
		return this.activating;
	}

	setDataTimeout(remainingSecs) {

		if (remainingSecs < 0) {
			this.sessioninfo.innerHTML = '';
		} else {
			var mins = '' + Math.floor(remainingSecs / 60);
			var secs = '' + remainingSecs % 60;
			this.sessioninfo.innerHTML = 'Timeout in ' + mins.padStart(2, '0') + ':' + secs.padStart(2, '0') + '';
		}
	}

	/**
	 * Change the state of the info panel.
	 * Options:
	 * - INFOPANEL_STATE_ACTIVITIES - Show scheduled activities (lessons, room booking sessions) for the day.
	 * - INFOPANEL_STATE_REGISTRATION - Show a register entry (A student's name, their photo, their mark etc).
	 * @param $state See INFOPANEL_STATE_*  (* is a wildcard).
	 */
	_setInfoPanelState(state) {

		if (this.infoPanelState === state) {
			return;
		}

		this.infoPanelState = state;

		if (state === INFOPANEL_STATE_REGISTRATION) {
			var newState = this.registration;
			var oldState = this.activities;
		} else {
			var oldState = this.registration;
			var newState = this.activities;
		}

		oldState.classList.add('hide');
		newState.classList.remove('hide');

		// Hide the map when it has no data:
		if (this.hasMapData()) {
			this.map.classList.add('hide');
		}
	}

	/**
	 * Similar to setActivities, but used for mapping the user's next activity.
	 * This will generate a map preview for the user's next event, if GMaps API access is available.
	 * @param {EventCollection} personSchedule
	 * @param {RSEvent} eventToMap
	 */
	mapUserActivities(personSchedule, eventToMap = null, options = {}) {

		if (!eventToMap) {
			eventToMap = personSchedule.getNextEvent(false, null, new Date());
		}

		if (!eventToMap) {
			return false;
		}

		// If we have valid coordinates for this room and the room of the event,
		// then show a single activity w/ Satellite Map View.
		// Else - Show their full timetable.
		let canShowMap = util.coordsValid(this.roomScanner.getRoom().latitude, this.roomScanner.getRoom().longitude)
			&& util.coordsValid(eventToMap.room.latitude, eventToMap.room.longitude)
			&& this.mapController.isAvailable();

		if (canShowMap) {
			// Show single event w/ Satellite View.
			this.setSingleActivity(eventToMap, true, options);
		} else {
			// Show their full timetable
			this.setActivities(null, personSchedule, options);
		}

		return true;
	}

	/**
	 * Set render options e.g. 'current event' highlighting. This is an instance variable so previous render options
	 * can be (optionally) recycled, by setting `options` as NULL.
	 * The options are mainly recycled for minutely content refreshes.
	 */
	_setActivityRenderOptions(newOptions) {
		// `activityOptions` should be recycled if the param is NULL:
		if (newOptions !== null) {
			this.activityRenderOptions = Object.assign({}, DEFAULT_ACTIVITY_RENDER_OPTIONS, newOptions);
		}
	}

	/**
	 * Get max events which may fit into the activity grid.
	 * This will fade the activity grid if a calculation is needed,
	 * @return {Promise<null|number>}
	 */
	async _getMaxEventsOnScreen() {

		// Determine height of the ActivityGrid, if unset.
		// Only calculate if the screen is active though. If it's not active, but we have a promise which tells us when
		// it will be, then wait on that promise.
		let activationPromise = this.isActivating();
		if (this._maxEventsOnScreen === null && (this.isActive() || activationPromise)) {

			if (activationPromise) {
				this.activities.classList.add('fadeout');
				await activationPromise;
				this.activities.classList.remove('fadeout');
			}

			/** Calculate max number of events which can fit on screen: **/

			// Assume two sections are visible on screen.
			let availableSpace = parseFloat(getComputedStyle(this.infoPanel).height) - (2*this._getSectionMaxHeight());

			// `.max` here to ensure a decent number of events (e.g. 3) are always visible.
			this._maxEventsOnScreen = Math.max(
				Math.floor(availableSpace / this._getEventMaxHeight()),
				DEFAULT_MAX_EVENTS_ON_SCREEN
			)
		}

		return this._maxEventsOnScreen || DEFAULT_MAX_EVENTS_ON_SCREEN;
	}

	/**
	 * Update the HTML for the display of activities/events on the scanner page.
	 *
	 * For both the parameters below, the following are applicable argument values:
	 * - `null`: Recycle the current scheduling when updating the HTML
	 * - `false`: Empties out any scheduling data
	 * - EventCollection: Replaces the current scheduling with this new one
	 *
	 * @param {EventCollection|null|boolean} room
	 * @param {EventCollection|null|boolean} user
	 * @param {Object|null} options See `this.activityOptions`
	 * @async The activity grid may not show its HTML immediately. If we need to calculate how many
	 *        activities can be rendered, and the screen isn't *fully* in its `setActive` state i.e. the animation
	 *        is still playing, then the calculation will be off.
	 *        If so, then leave the activity grid faded-out, waiting for the animation to finish, before
	 *        rendering activity entries.
	 */
	async setActivities(room = null, user = null, options = {}) {

		if (this.singleUserActivity) {
			// Do not try to render activities if we're in the single activity display mode.
			return;
		}

		// Calculate max number of events which can fit on screen.
		// This will pause the render
		let maxEvents = await this._getMaxEventsOnScreen();

		this._setActivityRenderOptions(options);

		// Must be null to preserve data. Use `false` to clear out the entries on screen.
		this.roomSchedule = room === null ? this.roomSchedule : room; // roomSchedule & userSchedule can be `false' here
		this.userSchedule = user === null ? this.userSchedule : user;

		let now = new Date();

		// Stores various flags and data accumulated during the activity screen update.
		let accumulatedActivityData = {
			// If the event that is currently happening right now has been highlighted on screen
			currentEventCaptioned: false,
			personNextEvent: null
		};

		/**
		 * @callback eventCallback
		 * @param {RSEvent} event
		 * @param {number} minsUntilStart
		 * @return {{highlight: boolean, caption: HTMLElement, showRoomRef: boolean}}
		 */

		/**
		 * @param schedule
		 * @param text
		 * @param type
		 * @param numEventsToShow Number of events to render. If there's more than this, then use `filterRelevantEvents`.
		 * @param {eventCallback} eventCb
		 * @return {string}
		 */
		let insertEvents = (schedule, text, type, numEventsToShow, eventCb) => {

			if (!schedule) return '';

			let [filteredEvents, numFutureEventsNotShown]
				= schedule.filterRelevantEvents(numEventsToShow, now);

			let eventHtml = '<div class="section '+type+'">'+text+'</div>';

			if (filteredEvents.length < 1) {
				eventHtml += '<p class="activities-message">No activities available</p>';
				return eventHtml;
			}

			let eventIdx = 0;

			for (let event of filteredEvents) {

				let minsUntilEventStart = Math.ceil((event.start - now) / 60000);

				// Run a callback to determine the render arguments.
				// Note: `minsUntilEventStart` may be negative
				let {highlight, caption, showRoomRef} = eventCb(event, minsUntilEventStart);

				// --- Render the event listing ---
				let css = highlight ? 'highlight' : '';

				// Indicate if a shown event was in the past:
				if (EventCollection.eventStatus(event) === PAST) {
					css += ' past';
				}

				let moreMsg = '';
				// If we're on the last event, and there are more events later in the day (which have been
				// filtered out):
				if (eventIdx+1 === filteredEvents.length && numFutureEventsNotShown > 0) {
					moreMsg = `<span class="row more">and ${numFutureEventsNotShown} more later today</span>`;
				}

				let eventIconChar = null;
				let eventName = event.name;

				eventName = makeNiceString(
					eventName,
					showRoomRef ? EVENT_NAME_CHAR_LIMIT_SHORT : EVENT_NAME_CHAR_LIMIT
				);

				if (event.type === EVENT_BOOKING) {

					eventIconChar = RSSchedule.isRoomSession(event) ? Icon.ELECTRIC_LIGHT_BULB.char : Icon.DOOR.char;

					// Add booking author to caption
					let authorMsg = 'Created by '+event.author.displayName;
					caption.innerText += caption.innerText ? ` (${authorMsg})` : authorMsg;


					if (RSSchedule.isRoomSession(event)) {
						let occupancy = event.sessionOccupancy;
						let capacity = event.sessionCapacity;
						let style = RSSchedule.getOccupancyStyle(event);
						eventName += ` <span class="${style}">(${occupancy}/${capacity})</span>`;
					}
				}


				let eventRoom = this._parseEventRoom(event);
				let roomText = '';

				// Only show roomText if there is sufficient room data
				if (showRoomRef && eventRoom) {
					if (eventRoom.id === this.roomScanner.getRoom().id) {
						roomText = 'this room';
					} else {
						roomText = makeNiceString(event.room.reference, ROOM_NAME_CHAR_LIMIT);
					}
				}


				let startStr = formatDate(event.start, false);
				let endStr = formatDate(event.end, false);
				let roomRefStr = showRoomRef ? ', in ' + ( roomText || 'N/A' ) : '';

				let iconStr = '';
				if (eventIconChar) iconStr = eventIconChar + ' ';

				eventHtml += ` 
					<p class="${css}"> 
						<span class="time">${startStr} → ${endStr}</span>${iconStr + eventName + roomRefStr}
						${caption.innerText ? caption.outerHTML : ''}
						${moreMsg}
					</p>
				`;

				eventIdx++;
			}

			return eventHtml;
		};

		let numEventsPerSection = this.userSchedule ? Math.floor(maxEvents / 2) : maxEvents;

		let roomEventHtml = insertEvents(
			this.roomSchedule,
			'This room today',
			'thisroom',
			numEventsPerSection,
			(event, minsUntilEventStart) => {
				let caption = document.createElement('span');

				let highlight = false;

				let eventType = RSSchedule.toFriendlyName(event, true);

				// If there is a event going on exactly now:
				if (now >= event.start && now < event.end) {

					accumulatedActivityData.currentEventCaptioned = true;
					highlight = this.activityRenderOptions.highlightCurrentEvent;
					caption.innerText = '❖ ' + eventType + ' now in progress';

				// If we're close to the next event, and we've not already highlighted the current one.
				} else if ( now < event.start
							&& minsUntilEventStart <= this.roomScanner.getMaxUpcomingLessonMins()
							&& !accumulatedActivityData.currentEventCaptioned ) {

					caption.innerText = eventType + ' starts in ' + pluralize('minute', minsUntilEventStart);
				}

				// Events happening right now will be captioned. Should we also highlight them too?
				if (caption.innerText && this.activityRenderOptions.highlightCurrentEvent) {
					caption.classList.add('highlight');
				}

				caption.classList.add('row');
				return {highlight: highlight, caption: caption, showRoomRef: false};
			}
		);

		let userEventsHtml = insertEvents(
			this.userSchedule,
			'Your activities today',
			'your',
			numEventsPerSection,
			(event, minsUntilEventStart) => {
				let caption = document.createElement('span');
				let highlight = false;

				let eventRoom = this._parseEventRoom(event);

				// If the event is upcoming or in progress, is NOT taking place in this room and
				// we haven't identified any other event as being "next up":
				if (now < event.end && (!eventRoom || eventRoom.id !== this.roomScanner.getRoom().id)
					&& !accumulatedActivityData.personNextEvent) {

					let eventType = RSSchedule.toFriendlyName(event, true);

					// If the event has already started:
					if (minsUntilEventStart <= 0) {
						caption.className = 'highlight warning';
						// noinspection JSCheckFunctionSignatures
						caption.innerText = this._getEventInProgressMsg(event, eventRoom);
						accumulatedActivityData.personNextEvent = event;

					// Lesson is starting in e.g. ~5-10 mins
					} else if ( minsUntilEventStart <= rs.UPCOMING_LESSON_INDICATOR_HURRY_MINS ) {

						accumulatedActivityData.personNextEvent = event;

						caption.className = 'highlight warning';
						caption.innerText = eventType + ' starts in ' + pluralize('minute', minsUntilEventStart) + '!';

					// Lesson is starting in e.g. ~10-20mins
					} else if (minsUntilEventStart <= this.roomScanner.getMaxUpcomingLessonMins()) {
						accumulatedActivityData.personNextEvent = event;

						caption.className = 'highlight';
						caption.innerText = eventType + ' starts in ' + pluralize('minute', minsUntilEventStart);
					}
					// Else - The event is starting after 20-25mins.
				}

				caption.classList.add('row');
				return {highlight: highlight, caption: caption, showRoomRef: true};
			}
		);

		this.activities.innerHTML = roomEventHtml + userEventsHtml;
	}

	/**
	 *
	 * @param {RSEvent} event
	 * @param {Object|null} eventRoom See ORM/Room
	 * @return {string}
	 * @private
	 */
	_getEventInProgressMsg(event, eventRoom = null) {
		if (eventRoom === null && event.room) {
			eventRoom = event.room;
		}

		if (this.activityRenderOptions.urgencyWarning && eventRoom) {
			return `Hurry to ${eventRoom.reference}!`;
		} else {
			let eventType = RSSchedule.toFriendlyName(event, true);
			let roomStr = eventRoom ? ` (${eventRoom.reference})!` : '!';
			return  '❖ ' + eventType + ' now in progress' + roomStr;
		}
	}

	/**
	 * Event Rooms may be a room object (see ORM/Room) or an integer, representing the room ID.
	 * We know the ID of the current, so if `event.getRoom` is the int ID value of the current room,
	 * then just return a copy of the current room instance.
	 * @param {RSEvent} event
	 * @private
	 * @return {object|null}
	 */
	_parseEventRoom(event) {

		if (typeof event.room === 'object') {
			return event.room;
		} else if (event === this.roomScanner.getRoom().id) {
			return this.roomScanner.getRoom();
		} else {
			return null;
		}
	}

	/**
	 * Set data on the page map. This
	 * @param markerLatLngs
	 */
	setMapData(markerLatLngs = null) {
		this.mapData = markerLatLngs;
		if (markerLatLngs) {
			this.mapController.showMarkers(markerLatLngs);
		}
	}

	hasMapData() {
		return this.mapData;
	}

	/**
	 * Allows a single (user) activity to override the content visible on screen.
	 * @param {RSEvent|NULL|boolean} yourEvent
	 */
	setSingleActivity(yourEvent = null, showMap = false, options = {}) {

		let event = this.singleUserActivity =  yourEvent===null ? this.singleUserActivity : yourEvent;

		if (!event) {
			return;
		}

		this._setActivityRenderOptions(options);

		let eventRoom = this._parseEventRoom(event);

		/** Google Maps A->B Satellite Preview **/

		if (showMap && eventRoom && this.mapController.isAvailable()) {
			let roomLatLng = {"lat": this.roomScanner.getRoom().latitude, "lng": this.roomScanner.getRoom().longitude};
			let regLatLng  = {"lat": eventRoom.latitude, "lng": eventRoom.longitude};

			// Show the map window
			this.map.classList.remove('fadeout', 'hide');
			try {
				this.setMapData([roomLatLng, regLatLng]);
			} catch(err) {
				util.logError('GMAPS_MARKER', err);
				this.map.classList.add('fadeout', 'hide');
			}
		}

		/** Render Individual Activity **/

		let now = new Date();
		let minsUntilRegStart = Math.ceil((this.singleUserActivity.start - now) / 60000);

		let roomRef = this.roomScanner.getRoom().reference === event.room.reference
			? 'this room' : makeNiceString(event.room.reference, ROOM_NAME_CHAR_LIMIT);

		let eventType = RSSchedule.toFriendlyName(event, true);

		let eventName = makeNiceString(event.name, EVENT_NAME_CHAR_LIMIT_SHORT);
		let startStr = event.start.toLocaleTimeString('en-GB', {hour:'numeric', minute:'numeric'});
		let endStr   = event.end.toLocaleTimeString('en-GB', {hour:'numeric', minute:'numeric'});
		let caption = minsUntilRegStart <= 0
			? this._getEventInProgressMsg(event, eventRoom)
			: eventType + ' starts in ' + pluralize('minute', minsUntilRegStart);

		this.activities.innerHTML =
			'<div class="section your">Your current activity</div>'
			+'<p>'
			+'<span class="time">'+startStr+' → '+endStr+'</span>' + eventName + ', in ' +roomRef
			+'<span class="highlight warning row">' + caption + '</span>'
			+'</p>';
	}

	/**
	 * Update page content by fading all the affected page elements in and out.
	 * If the setActive animation is playing, then no fade in/out will occur; HTML changes will be instant.
	 *
	 * @param infoPanelState See INFOPANEL_STATE_*  (* is a wildcard).
	 * @param {function} updateCb
	 * @return {Promise}
	 */
	updatePageContent(infoPanelState = null, updateCb = null) {

		if (infoPanelState) this.infoPanelNextState = infoPanelState;
		// Add callback to the current buffer (array).
		if (updateCb) this.onPageUpdate(updateCb);

		// Run callbacks which will modify HTML on screen.

		const runPageUpdateCbs = () => {
			for (let cb of this.infoPanelCbs) {
				cb();
			}
			this.infoPanelCbs = [];
		};

		// Update page content during activation animation.
		if (this.isActivating()) {
			// Resolve immediately
			return Promise.resolve(runPageUpdateCbs());
		}

		if (this.infoPanelFadePromise) {
			return this.infoPanelFadePromise;
		}

		// Create a new promise and start the fade sequence:
		document.body.classList.add('updating');
		this.map.classList.add('fadeout'); // The map is handled separately

		this.infoPanelFadePromise = getDelay(FADEOUT_LENGTH_MS)
		.then(() => {
			this._setInfoPanelState(this.infoPanelNextState);
			// Hide any scan confirmation indicators. Confirmation should have been visible for long enough now.
			this.setScanConfirmationVisible(false);
		})
		// This delay padding is necessary because `_setInfoPanelState` above will set the `display` css property,
		// which can interrupt animations if the info panel changes.
		.then(() => getDelay(20))
		.then(() => {
			this.infoPanelFadePromise = null;

			runPageUpdateCbs();
		})
		// Ensure the page still unfades correctly even on error.
		// The error will typically be from one of the runCbs.
		.catch(err => util.logError('PAGE_UPDATE_CONTENT', err))
		.then(() => getDelay(200))
		.then(() => {
			document.body.classList.remove('updating');
			// Only show the map again if it has data:
			if (this.hasMapData()) {
				this.map.classList.remove('fadeout');
			}
		});

		return this.infoPanelFadePromise;
	}

	/**
	 * Show a scan confirmation in the corner of the screen. Instantly appears as soon as a QR is read.
	 * @param {boolean} visible
	 */
	setScanConfirmationVisible(visible = true) {
		this.scanConfirmation.classList.toggle('hidden', !visible);
	}

	/**
	 * Schedule some HTML changes to occur with the next page update (screen fade or "activate" transition).
	 * @param {function} htmlAlteringFunc
	 */
	onPageUpdate(htmlAlteringFunc) {
		this.infoPanelCbs.push(htmlAlteringFunc);
	}

	/**
	 * Executed only once. Initialises the screen e.g. the clock element.
	 * @private
	 */
	_initialiseScreen() {

		// --- Setup Page Clock ---
		var dateOptions = { weekday: 'long',    year:   'numeric', month:  'long',    day:'numeric',
		                    hour:    'numeric', minute: 'numeric', second: 'numeric'                  };

		var clockTick = () => {
			var now = new Date();
			this.subheader.innerHTML = now.toLocaleString('en-GB', dateOptions);
		};

		// Update the screen clock every second
		setInterval(() => clockTick(), 1000);
		clockTick();
	}

	/**
	 * Set a temporary headline, welcoming a person. This will clear when the page countdown hits 0.
	 */
	setHeadlineFromPerson(person) {
		this.headline.setFromPerson(person);
	}

	setIcon(icon = null) {

		this.toggleIconRoll();

		this.temporaryIcon = icon;

		var setIconHtml = icon => {
			if (typeof icon === 'string') {
				this.pageicon.innerHTML = icon;
				this.pageicon.style.fontSize = ICON_FONT_SIZE_REM + 'rem';
			} else {
				this.pageicon.innerHTML = icon.char;
				this.pageicon.style.fontSize = (icon.sizeMultiplier * ICON_FONT_SIZE_REM) + 'rem';
			}
		};

		// No icon supplied means use the Static icon
		if (!icon) {
			if (this.defaultIcon) {
				this.pageicon.classList.toggle('clear', true);
				setIconHtml(this.defaultIcon);
			} else {
				// Use the default App logo icon
				this.pageicon.classList.toggle('clear', false);
			}
		} else {
			this.pageicon.classList.toggle('clear', true);
			setIconHtml(icon)
		}
	}

	toggleIconRoll() {

		// Reject new icon roll requests while an existing animation is playing
		if (this.iconRolling) {
			return false;
		}

		this.iconRolling = true;

		// Toggle 'roll-left'
		this.pageicon.classList.toggle('roll-left', !this.pageicon.classList.contains('roll-left'));

		getDelay(ICON_ROLL_ANIMATION_LENGTH_MS).then(() => {
			this.iconRolling = false;
		});
	}

	/**
	 * Update the default icon to use, for the current mode.
	 * @param icon
	 */
	setDefaultIcon(icon = null) {

		// If the default icon is unchanged, do nothing:
		if (this.defaultIcon === icon) {
			return;
		}

		this.defaultIcon = icon;
		if (!this.hasTemporaryIcon()) {
			this.setIcon(); // No args means use the Static icon.
			this.toggleIconRoll();
		}
	}

	/**
	 * @public
	 * @return {boolean}
	 */
	hasTemporaryIcon() {
		return this.temporaryIcon !== null;
	}

	/**
	 * Show or hide all the of registration elements inside the registration info box.
	 * NOTE - This does not change the info panel state from 'Activity' to 'Registration', but instead assigns
	 * an opacity to the content of the student registration elements.
	 * @param vis
	 * @public
	 */
	setRegistrationVisible(vis = true) {
		this.registrationMark.classList.toggle('fadeout', !vis);
		this.registrationPhoto.classList.toggle('fadeout', !vis);
		this.registrationName.classList.toggle('fadeout', !vis);
	}

	/**
	 * Populate the 'Registration' info panel box box (student photo, name, register mark)
	 * @param studentData
	 * @param mark
	 * @param markName
	 * @param markClass
	 */
	setRegistration(studentData, mark, markName, markClass) {
		this.registrationMark.className = '' + markClass;
		this.registrationMark.innerHTML = mark + ' (' + markName + ')';
		this.registrationName.innerHTML = studentData.firstname + ' ' + studentData.surname;
		this.registrationPhoto.style.backgroundImage
			= 'url('+this.roomScanner.getBaseUrl()+'/Photo/'+studentData.reference+')';
	}


	/**
	 * Set overarching page style. Style options:
	 * - STYLE_SUCCESS
	 * - STYLE_WARNING
	 * @param {string|null} style Style constant or NULL to use defaultPageCss instead.
	 */
	setPageCss(style) {
		this.pageCss = style;
		this.html.classList.remove(STYLE_SUCCESS, STYLE_WARNING);

		if (this.pageCss) {
			this.html.classList.add(this.pageCss);
		} else if (this.defaultPageCss) {
			this.html.classList.add(this.defaultPageCss);
		}
	}

	setDefaultPageCss(css) {
		this.defaultPageCss = css;
	}

	/**
	 * Show indicator if we're on/offline.
	 * By default we assume the page is online.
	 */
	setOnline(online) {
		if (online) {
			this.html.classList.remove('offline');
			this.offlineMessage.classList.remove('pulse');
			this.toggleIconRoll();
			this.setDefaultPageCss(null); // No custom page style.
		} else {
			this.html.classList.add('offline');
			this.offlineMessage.classList.add('pulse');
			this.setDefaultPageCss('warning');
			this.toggleIconRoll();
			// Use the default icon. This will show a network failure symbol when in offline mode
			this.setIcon(null);
		}

		this.scanMessage.setOnline(online);

		// Update the page style If we're displaying the default page CSS.
		if (this.pageCss === null) {
			this.setPageCss(null); // NULL - use defaultPageCss.
		}
	}

	/**
	 * @see this.room.getMapController
	 * @return {*}
	 */
	get mapController() {
		return this.roomScanner.getMapController();
	}

	/**
	 * @return {number} Height in Px.
	 * @private
	 */
	 _getEventMaxHeight() {
		let invisibleDummyEvent =
			 '<p class="highlight fadeout" id="dummy-event">'
			+	'<span class="time">00:00</span>Event Name'
			+	'<span class="highlight row">Caption</span>'
			+'</p>';

		this.activities.insertAdjacentHTML('beforeend', invisibleDummyEvent);

		let computedStyles = getComputedStyle(document.getElementById('dummy-event'));

		return parseFloat(computedStyles.height)
			+ parseFloat(computedStyles.marginTop)  
			+ parseFloat(computedStyles.marginTop);
	}

	/**
	 * @return {number} Height in Px.
	 * @private
	 */
	 _getSectionMaxHeight() {
		this.activities.insertAdjacentHTML('beforeend', '<div class="section fadeout" id="dummy-section">Section</div>');
		return parseFloat(getComputedStyle(document.getElementById('dummy-section')).height);
	}
}


const DEFAULT_SCAN_MSG_CHAR_LIMIT = 170;

class ScanMessage {

	/**
	 * @param {Element} messageElem Scan message text element
	 * @param {Element} boxElem     Scan message container box element
	 * @param {Mode}    scannerMode
	 */
	constructor(messageElem, boxElem, vocab) {
		this.messageElem = messageElem;
		this.boxElem = boxElem;
		this.vocab = vocab;
		this.online = true;

		this.temporaryMessage = null;
		this.temporaryStyle = null;
		this.scannerMode = null;

		this._redraw();
	}

	getMessage() {
		return this.messageElem.innerHTML;
	}

	/**
	 * The max number of chars we can fit in the Scan Message, before rendering glitches occur.
	 * @return {number}
	 */
	getRenderLimit() {
		return DEFAULT_SCAN_MSG_CHAR_LIMIT;
	}

	setOnline(on) {
		this.online = on;
		this._redraw();
	}

	/**
	 * @param {string} msg
	 * @param {string} style STYLE_* e.g. STYLE_SUCCESS.
	 */
	setTemporaryMessage(msg, style = null) {
		this.temporaryMessage = msg;
		this.temporaryStyle = style;
		this._redraw();
	}

	clearTemporaryMessage() {
		this.temporaryMessage = null;
		this.temporaryStyle = null;
		this._redraw();
	}

	/**
	 * The message output will be determined by the scan mode.
	 * @param mode
	 */
	setScannerMode(mode) {
		this.scannerMode = mode;
		this._redraw();
	}

	_generateCurrentMsg() {

		let msg = '', style = null;
		let mode = this.scannerMode;

		if (!this.online) {

			style = STYLE_WARNING;
			msg = 'The scanner could not connect to the network. If the problem continues, '
				+ 'please contact a technician';

		} else if (this.temporaryMessage) {

			style = this.temporaryStyle;
			msg = this.temporaryMessage;

		} else if (mode instanceof InstantStudySessionsMode) {

			msg = `Present your QR code to start/end a ${this.vocab.studySupport} session`;

		} else if (mode instanceof RegistrationMode) {

			msg = 'Present your QR code for registration';

		} else if (mode instanceof BookingMode && mode.isRoomSession()) {

			msg = 'Present your QR code to register on this event';

		} else {
			msg = 'Present your QR code';
		}

		return {
			msg: msg,
			style: style
		}
	}

	/**
	 * Resize the container, necessary when its content changes to support animations.
	 */
	resizeBox() {
		// We need to manually set the height to take advantage of the animating transition
		this.boxElem.style.height = this.messageElem.clientHeight + 'px';
	}

	_redraw() {
		const {msg, style} = this._generateCurrentMsg();

		this.messageElem.innerHTML = msg;

		// Clear known styles
		this.boxElem.classList.remove(STYLE_SUCCESS, STYLE_WARNING);

		if (style) {
			this.boxElem.classList.add(style);
		}

		this.resizeBox();
	}
}


class Headline {

	/**
	 * @param {Element} headlineElem Headline <p> element
	 * @param {Page} headlineElem Headline <p> element
	 */
	constructor(headlineElem, page) {
		this._elem = headlineElem;
		this._tempTitle = null;
		this._scannerMode = null;
		this._page = page;
		this._redraw();
	}

	set animated(a) {
		this._elem.classList.toggle('animated', a);
	}

	setScannerMode(mode) {
		this._scannerMode = mode;
		this._redraw();
	}

	setFromPerson(person) {
		this.setTempAppearance(getWelcomeMessage() + ', ' + person.firstname);
	}

	/**
	 * @param {string} title
	 */
	setTempAppearance(title) {
		this._tempTitle = title;
		this._redraw();
	}

	clearTempAppearance() {
		this._tempTitle = null;
		this._redraw();
	}

	_getCurrentAppearance() {

		let title = null;
		let room = this._page.getRoom();

	 	if (this._tempTitle) {

	 		title = this._tempTitle;

		} else if (this._scannerMode instanceof RegistrationMode) {

			title = room.reference + ', ' + this._scannerMode.event.name;

		} else if (this._scannerMode instanceof BookingMode) {

			title = room.reference + ', ' + makeNiceString(this._scannerMode.event.name, EVENT_NAME_CHAR_LIMIT);

		} else {

			title = room.reference + ' - ' + room.name;

		}

		return {title}
	}

	_redraw() {
		const {title} = this._getCurrentAppearance();
		this._elem.innerHTML = title;
	}
}


/**
 * Unicode compatibility notes:
 * Android 5 supports Unicode 6.0 and below
 * Android 6 supports Unicode 7.0 and below
 * Android 7 supports Unicode 8.0 and below
 */
export const Icon = {
	THINKING_FACE:          {char: '🤔',	sizeMultiplier:0.9 }, // U8.0
	QUESTION_MARK:          {char: '❓',	sizeMultiplier:1.0 },
	INFORMATION:            {char: 'ℹ️' ,	sizeMultiplier:1.0 },
	MEMO:                   {char: '📝',	sizeMultiplier:0.7 }, // U6.0
	TICK:                   {char: '✔️',		sizeMultiplier:0.9 },
	CROSS:                  {char: '❌️',	sizeMultiplier:0.8 },
	ALARM_CLOCK:            {char: '⏰️',	sizeMultiplier:0.8 }, // U6.0
	CALENDAR:               {char: '📅',	sizeMultiplier:0.8 }, // U6.0
	THUMBS_UP:              {char: '👍',	sizeMultiplier:0.8 },
	RUNNER:                 {char: '🏃',	sizeMultiplier:1.0 },
	CHEQUERED_FLAG:         {char: '🏁',	sizeMultiplier:0.8 }, // U6.0
	BROKEN_HEART:           {char: '💔',	sizeMultiplier:0.95}, // U6.0
	WAVING_HAND:            {char: '👋',	sizeMultiplier:0.95}, // U6.0
	PENCIL:                 {char: '✏️️',		sizeMultiplier:0.7 },
	PERSON_GESTURING_NO:    {char: '🙅️',	sizeMultiplier:0.9 }, // U6.0
	DOOR:                   {char: '🚪️',	sizeMultiplier:0.9 }, // U6.0
	EYES:                   {char: '👀',	sizeMultiplier:0.9 }, // U6.0
	CLOCK_FACE_FOUR_OCLOCK: {char: '🕓',	sizeMultiplier:0.9 }, // U6.0
	ELECTRICAL_PLUG:        {char: '🔌',	sizeMultiplier:0.9 }, // U6.0
	HOURGLASS:              {char: '⏳',	sizeMultiplier:0.9 }, // U1.1
	GRADUATION_CAP:         {char: '🎓',	sizeMultiplier:0.9 }, // U1.1
	TICKET:                 {char: '🎫',	sizeMultiplier:0.9 }, // U6.0
	SEAT:                   {char: '💺',	sizeMultiplier:0.9 }, // U6.0
	SLIGHTLY_FROWNING_FACE: {char: '🙁',	sizeMultiplier:0.9 }, // U7.0
	NO_ENTRY:               {char: '⛔',	sizeMultiplier:0.9 }, // U7.0
	ELECTRIC_LIGHT_BULB:    {char: '💡',	sizeMultiplier:0.9 }, // U7.0
};

/**
 * @todo RS Implement video feed shifting
 */
export class ScreenSaver {

	/**
	 * @param {Page} page
	 * @param deactivateMins
	 * @param startShiftingMs
	 */
	constructor(page, deactivateMins, startShiftingMs) {

		this._page = page;
		this._enabled = true;

		this._deactivateMs = deactivateMins * 60 * 1000;
		this._startShiftingMs = startShiftingMs * 60 * 1000;

		this.deactivateCountdown = new util.BasicCountdown();
		this.deactivateCountdown.onComplete(() => {
			if (this._enabled) {
				this._page.setActive(false); // Deactivate the screen
			}
		});

		this.startShiftingCountdown = new util.BasicCountdown();
		this.startShiftingCountdown.onComplete(() => {
			//todo RS shifting
		});
	}

	reset() {
		this.deactivateCountdown.restart(this._deactivateMs);
		// Note - the fadeout time starts ticking after we reach the deactivate state.
		this.startShiftingCountdown.restart(this._deactivateMs + this._startShiftingMs);
	}

	get enabled() {
		return this._enabled;
	}

	set enabled(enable) {
		this._enabled = enable;
		if (!enable) {
			this.cancel();
		}
	}

	cancel() {
		this.deactivateCountdown.cancel();
		this.startShiftingCountdown.cancel();
	}
}