
export const PAST    = 'PAST';
export const FUTURE  = 'FUTURE';
export const PRESENT = 'PRESENT';

/**
 * @typedef {Object} Event
 * @property {number} id
 * @property {string} type
 * @property {Date}   start
 * @property {Date}   end
 */

/**
 * EventCollection
 */
export class EventCollection {

	/**
	 * @param {(Event[]|Object[])} data   Note: data passed into this array will be mutated as *a copy is not made*.
	 * @param {boolean} isJsonArr If this is populated from JSON-decoded data. If so, we must convert date properties
	 *                  to suitable `Date` instances.
	 */
	constructor(data, isJsonArr = true) {

		if (isJsonArr) {
			for (let ev of data) {
				ev.start = parseDate(ev.start);
				ev.end = parseDate(ev.end);
			}
		}

		// Any other types e.g. null, undefined, false should be interpreted as an empty EventCollection.
		if (!Array.isArray(data)) {
			data = [];
		}

		/**
		 * @type {Event[]}
		 * @private
		 */
		this._data = data;

		/**
		 * @type {Object.<string, Event[]>}
		 * @private
		 */
		this._byType = {};


		for(let event of data) {
			if (!Array.isArray(this._byType[event.type])) {
				this._byType[event.type] = [];
			}

			this._byType[event.type].push(event);
		}
	}

	getData() {
		return this._data;
	}

	/**
	 * If there is an activity going on right now, return it.
	 * If there is nothing going on right now, return the next activity or NULL if there are no
	 * upcoming activities.
	 * @param {boolean} currentOnly
	 * @param {?string} type
	 * @param {Date} fromTime
	 * @return {Event}
	 */
	getNextEvent(currentOnly = false, type = null, fromTime = new Date()) {

		let idx = this._getNextEventIndex(fromTime, type, currentOnly);

		return idx > -1 ? this._data[idx] : null;
	}

	/**
	 * NB: For back-to-back events, this will always prioritise the *current* event even if the next one is within
	 * the time window.
	 * @param {number} timeWindowMs
	 * @param {?string} type
	 * @param {Date} fromTime
	 * @return {*}
	 */
	getNextEventWithinTime(timeWindowMs, type = null, fromTime = new Date()) {

		// Event will either be upcoming or going on right now
		let event = this.getNextEvent(false, type, fromTime);

		// If there is an upcoming/current event
		if (event) {
			// `msUntilLesson` will be negative if `.start` has already been passed.
			let msUntilLesson = event.start - fromTime;
			// If an event is coming up soon
			if (msUntilLesson <= timeWindowMs) {
				return event;
			}
		}

		return null;
	}

	_getNextEventIndex(fromTime, type, currentOnly) {

		let eventArr = [];

		if (!type) {
			eventArr = this._data;
		} else if (this._byType.hasOwnProperty(type)) {
			eventArr = this._byType[type];
		}

		// registerData should order its registers earliest -> latest
		for (let i = 0; i < eventArr.length; i++) {
			// Get the first upcoming register
			if (fromTime < eventArr[i].end && (!currentOnly || fromTime >= eventArr[i].start)) {
				return i;
			}
		}

		return -1;
	}

	clone() {
		var simpleCopy = deepClone(this.getData());
		for (var idx in simpleCopy) {
			simpleCopy[idx].start = new Date(simpleCopy[idx].start);
			simpleCopy[idx].end = new Date(simpleCopy[idx].end);
		}
		return new this.constructor(simpleCopy);
	}

	get length() {
		return this._data.length;
	}

	/**
	 * @param {EventCollection} otherSchedule
	 */
	equals(otherSchedule) {
		return JSON.stringify(this._data) === JSON.stringify(otherSchedule.getData());
	}

	/**
	 * @param {EventCollection} collection
	 */
	intersect(collection) {
		if (this.length < collection.length) {
			smallerSet = this;
			largerSet = collection;
		}  else {
			smallerSet = collection;
			largerSet = this;
		}

		let events = [];
		for (let event of smallerSet.getData()) {
			if (largerSet.has(event)) {
				events.push(event);
			}
		}

		return new EventCollection(events, false);
	}

	/**
	 * Does this event exist in this EventCollection?
	 * @param {Event} event
	 */
	has(event) {
		if (!this._byType.hasOwnProperty(event.type)) {
			return false;
		}

		for (let typeEvent of this._byType[event.type]) {
			if (eventsEqual(typeEvent, event)) {
				return true;
			}

			// If we've gone past all applicable events.
			if (typeEvent.start > event.start) {
				return false;
			}
		}
	}

	/**
	 * If there are any events in this schedule.
	 * @return {boolean}
	 */
	hasValue() {
		return this._data.length > 0;
	}

	/**
	 *
	 * @param rawCount
	 * @param relevantTime
	 * @return {Array} Index 0: Filtered event set. Index 1:
	 */
	filterRelevantEvents(rawCount, relevantTime = new Date()) {

		var numEvents = this._data.length;
		var numToShow = Math.max(rawCount, 1);

		if (numToShow >= numEvents) {
			return [this._data, 0];
		}

		var currIdx = this._getNextEventIndex(relevantTime, false);
		if (currIdx < 0) {
			currIdx = numEvents-1;
		}

		var includePrevEvent = numToShow >= 3;

		// If the next event is the very first.
		if (currIdx === 0) {
			// Return the first {numToShow} events.
			return [this._data.slice(0, numToShow), numEvents - numToShow];

		// If the next event is close to the very end of the day.
		} else if ((numEvents + (includePrevEvent ? 1 : 0) - (currIdx + numToShow)) < 1 ) {
			// Return the last {numToShow} remaining events
			return [this._data.slice(-numToShow), 0];

		// If we only want to see 1 or 2 events
		} else if (!includePrevEvent) {
			// Return this event and maybe the next
			return [this._data.slice(currIdx, currIdx + numToShow), numEvents - (currIdx + numToShow)];

		// If we want to see the previous event and {numToShow-1} which follow after it.
		// This should only be done if there are 3 or more events to show.
		} else {
			// Return the previous event and all those which follow within the {numToShow} limit
			return [this._data.slice(currIdx - 1, currIdx + numToShow - 1), numEvents + 1 - (currIdx + numToShow)];
		}
	}

	/**
	 * @param {Event} event
	 * @return {string}
	 */
	static eventStatus(event) {
		let now = new Date();
		if (now >= event.end) {
			return PAST;
		} else if (now < event.start) {
			return FUTURE;
		} else {
			return PRESENT;
		}
	}
}

/**
 *
 * @param {Event} ev1
 * @param {Event} ev2
 * @return {*}
 */
function eventsEqual(ev1, ev2) {
	return ev1.id === ev2.id
		&& ev1.type === ev2.type
		&& ev1.start.getTime() === ev2.start.getTime()
		&& ev1.end.getTime() === ev2.end.getTime();
}