import {EventCollection} from "../time/events.js";
import {JsonParseError, ResponseError} from "./errors.js";

export const EVENT_BOOKING = 'booking';
export const EVENT_REGISTER = 'register';

/**
 * Stores a list of scheduled events which are happening today in a room, for use by the Room Scanner client.
 * Should not store any events after 23:59 today.
 */
export class RSSchedule extends EventCollection {

	/**
	 * Get a friendly name for an event based on its type.
	 * @param {Event} event
	 * @param {boolean} uppercaseFirstChar
	 * @return {string} e.g. "Lesson", "Booking" etc.
	 */
	static toFriendlyName(event, uppercaseFirstChar = false) {
		let eventType = event.type;
		if (eventType === EVENT_REGISTER) {
			eventType = 'lesson'; // Friendlier name.
		}
		if (uppercaseFirstChar) {
			eventType = ucFirst(eventType);
		}

		return eventType;
	}

	/**
	 * Does this event take attendance?
	 * These types of events can be started earlier to allow attendance marking in advance.
	 * @param {RSEvent|NULL} event
	 * @return {boolean}
	 */
	static isAttendanceEvent(event) {
		if (!event) return false;
		if (event.type === EVENT_REGISTER) return true;
		if (RSSchedule.isRoomSession(event)) return true;
		return false;
	}

	/**
	 * Is the supplied Event a Room Session?
	 * These are EVENT_BOOKING types but with extra properties.
	 * @param {Event}
	 * @return {boolean}
	 */
	static isRoomSession(event) {
		if (!event) return false;
		return event.type === EVENT_BOOKING && event.hasOwnProperty('sessionID');
	}

	/**
	 * Get a style describing the RoomSession's occupancy.
	 * @param {RoomSession} event
	 */
	static getOccupancyStyle(event) {
		let attendance = event.sessionOccupancy / event.sessionCapacity;

		if (event.sessionOccupancy <= 0) return 'occupancy-empty';
		if (attendance >= 1) return 'occupancy-full';
		if (attendance >= 0.8) return 'occupancy-sufficient';
		return 'occupancy-sparse';
	}
}

/**
 * @typedef {Event} RSEvent - Room Scanner Event
 * @property {string} name
 * @property {Object|NULL} room
 */

/**
 * @typedef {RSEvent} Booking
 * @property {Object} author
 */

/**
 * @typedef {Booking} RoomSession
 * @property {number} sessionID
 * @property {number} sessionCapacity
 * @property {number} sessionOccupancy
 * @property {boolean} sessionStaff
 * @property {boolean} sessionStudent
 */

/**
 * Represents a countdown timer.
 * An instance can be run any number of times with varying countdown lengths.
 * A specific instance will run a fixed callback (set when initialised) if one of the following happen to the
 * countdown timer:
 * - It reaches 0 (finishes)             - onComplete
 * - It is cancelled prematurely         - onCancel
 * - It is restarted with a new run time - onCancel, then onComplete when 2nd run is complete.
 *
 * @abstract Subclasses must implement how the countdown asynchronously waits for the countdown completion.
 *           Could use e.g. setTimeout.
 */
export class Countdown {

	constructor() {
		this.cancelCb = function(){};
		this.completeCb = function(){};
		this.sessionCb = false;

		this.running = false;
	}

	isRunning() {
		return this.running;
	}

	onComplete(cb) {
		this.completeCb = cb;
		return this;
	}

	onCancel(cb) {
		this.cancelCb = cb;
		return this;
	}

	/**
	 * @param sessionCb Callback to execute only for the current run. Executes when the countdown
	 * stops i.e. if it's cancelled or complete.
	 */
	start(lengthMs, sessionCb = null) {
		// Subclasses should use `lengthMs`.
		if (this.running) {
			return false;
		}

		this.sessionCb = sessionCb;
		this.running = true;
		return true;
	}

	_runSessionCallback() {
		if (this.sessionCb) {
			this.sessionCb();
			this.sessionCb = false;
		}
	}

	cancel() {
		if (!this.running) {
			return false;
		}

		this.running = false;
		this.cancelCb();
		this._runSessionCallback();
		return true;
	}

	_complete() {
		this.running = false;
		this.completeCb();
		this._runSessionCallback();
	}

	restart(lengthMs, cb = null) {
		this.cancel();
		return this.start(lengthMs, cb);
	}
}

/**
 * Runs a callback every second, as well as the usual countdown events.
 * Implemented with setInterval.
 */
export class TickingCountdown extends Countdown {

	constructor() {
		super();
		this.intervalHandle = null;
		this.tickCb = () => {};
	}

	onTick(cb) {
		this.tickCb = cb;
		return this;
	}

	start(lengthMs, currentRunCb = null) {

		if (!super.start(currentRunCb)) {
			return false;
		}

		var secsRemaining = Math.floor(lengthMs / 1000);

		// Note - this Countdown assumes setInterval consistently fires every second.
		var tick = ()=> {

			this.tickCb(secsRemaining);

			if (secsRemaining < 1) {
				clearInterval(this.intervalHandle);
				// Complete the countdown
				this._complete();
			} else {
				secsRemaining -= 1;
			}
		};

		this.intervalHandle = setInterval(tick, 1000); // Run callback every second.
		tick();

		return true;
	}

	cancel() {
		if (!super.cancel()) {
			return false;
		}

		clearInterval(this.intervalHandle);
		return true;
	}
}

/**
 * Simple concrete countdown timer. Implemented with setTimeout.
 */
export class BasicCountdown extends Countdown {

	constructor() {
		super();
		this.timeoutHandle = null;
	}

	start(lengthMs, currentRunCb = null) {
		if (!super.start(lengthMs, currentRunCb)) {
			return false;
		}

		this.timeoutHandle = setTimeout(() => {
			this._complete();
		}, lengthMs);
		return true;
	}

	cancel() {
		if (!super.cancel()) {
			return false;
		}

		clearTimeout(this.timeoutHandle);
		return true;
	}
}

/**
 * Write any errors to localStorage (accessible from page "<site>/errors") and try to
 * POST them to the site server to be stored in ops_audit.
 * @param context
 * @param error
 */
export function logError(context, error) {

	/** Parse errors **/

	let errText, trace = '';

	// Ensure any objects use their `toString` methods rather than `JSON.stringify`' as the latter
	// can miss detail.
	if (error && typeof error === 'object') {

		if (error instanceof Error) {
			trace = error.stack;
			errText = error.toString();

			if (error instanceof JsonParseError || error instanceof ResponseError ) {
				errText += '\nContent:' + JSON.stringify(error.content);
			}

		} else {
			errText = JSON.stringify(error);
		}

	} else {
		errText = '' + error;
	}

	/** Audit errors on main server, if possible **/

	var errorPOST = new FormData();
	errorPOST.append("errorContext", context);
	errorPOST.append("errorBody", `Error: ${errText}. Trace: ${trace}`);

	// Warning - does not rely on `baseUrl` as this may be called from the `window` level.
	//           This should be updated as necessary if the baseUrl path level is altered.
	// This fetch is mostly just an attempt to further broadcast the error. If there is a network issue,
	// then you will have to rely on the "/errors" page as described below.
	fetch('/RoomScanner/ErrorListener', {method: "POST", body: errorPOST, credentials: 'include'});

	/** Send to LocalStorage, viewable on "/errors" page. **/

	const maxErrs = 100;
	// Structure: [{date: string, context: string, details: string}]
	let errs = JSON.parse(localStorage.room_scanner_errors || '[]') || [];


	// Insert new error at the bottom of the array.
	errs.unshift({date: new Date(), context: context, details: errText, trace: trace});

	if (errs.length > maxErrs) {
		// Remove the oldest element
		errs.pop();
	}

	// Save the updated errors list
	localStorage.room_scanner_errors = JSON.stringify(errs);

	/** Log Error to Console **/

	console.error(context, error);
}

export function bindGlobalErrorHandler(rs) {

	// Listen for uncaught global errors, ensuring they are logged to localStorage.room_scanner_errors.
	window.addEventListener('error', function(e) {
		e.preventDefault();

		logError('WINDOW_ERROR_LISTENER', {
			message:  e.message,
			filename: e.filename,
			lineno:   e.lineno
		});
	});
}

/**
 * Check if a longitude and latitude pair is valid.
 */
export function coordsValid(lat, lng) {
	// Parse float first, because according to JS both `null` and `false` are number types 🤦
	// i.e. `isNaN(null) === false` and `isNaN(null) === false`
	lat = parseFloat(lat);
	lng = parseFloat(lng); // `parseFloat` will actually convert non-number types to NaN.

	if (isNaN(lat) || isNaN(lng)) {
		return false;

	// Null island check:
	} else if (lat === 0.0 && lng === 0.0) {
		return false;
	} else {
		return true;
	}
}


