text: /* Magic Mirror
   * Calendar Util Methods
   *
   * By Michael Teeuw https://michaelteeuw.nl
   * MIT Licensed.
   */
  
  /**
   * @external Moment
   */
  const moment = require("moment");
  const path = require("path");
  const zoneTable = require(path.join(__dirname, "windowsZones.json"));
  const Log = require("../../js/logger.js");
  
  const CalendarUtils = {
  	/**
  	 * Calculate the time correction, either dst/std or full day in cases where
  	 * utc time is day before plus offset
  	 *
  	 * @param {object} event the event which needs adjustement
  	 * @param {Date} date the date on which this event happens
  	 * @returns {number} the necessary adjustment in hours
  	 */
  	calculateTimezoneAdjustment: function (event, date) {
  		let adjustHours = 0;
  		// if a timezone was specified
  		if (!event.start.tz) {
  			Log.debug(" if no tz, guess based on now");
  			event.start.tz = moment.tz.guess();
  		}
  		Log.debug("initial tz=" + event.start.tz);
  
  		// if there is a start date specified
  		if (event.start.tz) {
  			// if this is a windows timezone
  			if (event.start.tz.includes(" ")) {
  				// use the lookup table to get theIANA name as moment and date don't know MS timezones
  				let tz = CalendarUtils.getIanaTZFromMS(event.start.tz);
  				Log.debug("corrected TZ=" + tz);
  				// watch out for unregistered windows timezone names
  				// if we had a successful lookup
  				if (tz) {
  					// change the timezone to the IANA name
  					event.start.tz = tz;
  					// Log.debug("corrected timezone="+event.start.tz)
  				}
  			}
  			Log.debug("corrected tz=" + event.start.tz);
  			let current_offset = 0; // offset  from TZ string or calculated
  			let mm = 0; // date with tz or offset
  			let start_offset = 0; // utc offset of created with tz
  			// if there is still an offset, lookup failed, use it
  			if (event.start.tz.startsWith("(")) {
  				const regex = /[+|-]\d*:\d*/;
  				const start_offsetString = event.start.tz.match(regex).toString().split(":");
  				let start_offset = parseInt(start_offsetString[0]);
  				start_offset *= event.start.tz[1] === "-" ? -1 : 1;
  				adjustHours = start_offset;
  				Log.debug("defined offset=" + start_offset + " hours");
  				current_offset = start_offset;
  				event.start.tz = "";
  				Log.debug("ical offset=" + current_offset + " date=" + date);
  				mm = moment(date);
  				let x = parseInt(moment(new Date()).utcOffset());
  				Log.debug("net mins=" + (current_offset * 60 - x));
  
  				mm = mm.add(x - current_offset * 60, "minutes");
  				adjustHours = (current_offset * 60 - x) / 60;
  				event.start = mm.toDate();
  				Log.debug("adjusted date=" + event.start);
  			} else {
  				// get the start time in that timezone
  				let es = moment(event.start);
  				// check for start date prior to start of daylight changing date
  				if (es.format("YYYY") < 2007) {
  					es.set("year", 2013); // if so, use a closer date
  				}
  				Log.debug("start date/time=" + es.toDate());
  				start_offset = moment.tz(es, event.start.tz).utcOffset();
  				Log.debug("start offset=" + start_offset);
  
  				Log.debug("start date/time w tz =" + moment.tz(moment(event.start), event.start.tz).toDate());
  
  				// get the specified date in that timezone
  				mm = moment.tz(moment(date), event.start.tz);
  				Log.debug("event date=" + mm.toDate());
  				current_offset = mm.utcOffset();
  			}
  			Log.debug("event offset=" + current_offset + " hour=" + mm.format("H") + " event date=" + mm.toDate());
  
  			// if the offset is greater than 0, east of london
  			if (current_offset !== start_offset) {
  				// big offset
  				Log.debug("offset");
  				let h = parseInt(mm.format("H"));
  				// check if the event time is less than the offset
  				if (h > 0 && h < Math.abs(current_offset) / 60) {
  					// if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
  					// we need to fix that
  					adjustHours = 24;
  					// Log.debug("adjusting date")
  				}
  				//-300 > -240
  				//if (Math.abs(current_offset) > Math.abs(start_offset)){
  				if (current_offset > start_offset) {
  					adjustHours -= 1;
  					Log.debug("adjust down 1 hour dst change");
  					//} else if (Math.abs(current_offset) < Math.abs(start_offset)) {
  				} else if (current_offset < start_offset) {
  					adjustHours += 1;
  					Log.debug("adjust up 1 hour dst change");
  				}
  			}
  		}
  		Log.debug("adjustHours=" + adjustHours);
  		return adjustHours;
  	},
  
  	/**
  	 * Filter the events from ical according to the given config
  	 *
  	 * @param {object} data the calendar data from ical
  	 * @param {object} config The configuration object
  	 * @returns {string[]} the filtered events
  	 */
  	filterEvents: function (data, config) {
  		const newEvents = [];
  
  		// limitFunction doesn't do much limiting, see comment re: the dates
  		// array in rrule section below as to why we need to do the filtering
  		// ourselves
  		const limitFunction = function (date, i) {
  			return true;
  		};
  
  		const eventDate = function (event, time) {
  			return CalendarUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
  		};
  
  		Log.debug("There are " + Object.entries(data).length + " calendar entries.");
  		Object.entries(data).forEach(([key, event]) => {
  			Log.debug("Processing entry...");
  			const now = new Date();
  			const today = moment().startOf("day").toDate();
  			const future = moment().startOf("day").add(config.maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
  			let past = today;
  
  			if (config.includePastEvents) {
  				past = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate();
  			}
  
  			// FIXME: Ugly fix to solve the facebook birthday issue.
  			// Otherwise, the recurring events only show the birthday for next year.
  			let isFacebookBirthday = false;
  			if (typeof event.uid !== "undefined") {
  				if (event.uid.indexOf("@facebook.com") !== -1) {
  					isFacebookBirthday = true;
  				}
  			}
  
  			if (event.type === "VEVENT") {
  				Log.debug("\nEvent: " + JSON.stringify(event));
  				let startDate = eventDate(event, "start");
  				let endDate;
  
  				if (typeof event.end !== "undefined") {
  					endDate = eventDate(event, "end");
  				} else if (typeof event.duration !== "undefined") {
  					endDate = startDate.clone().add(moment.duration(event.duration));
  				} else {
  					if (!isFacebookBirthday) {
  						// make copy of start date, separate storage area
  						endDate = moment(startDate.format("x"), "x");
  					} else {
  						endDate = moment(startDate).add(1, "days");
  					}
  				}
  
  				Log.debug("startDate (local): " + startDate.toDate());
  				Log.debug("endDate (local): " + endDate.toDate());
  
  				// Calculate the duration of the event for use with recurring events.
  				let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
  				Log.debug("duration: " + duration);
  
  				// FIXME: Since the parsed json object from node-ical comes with time information
  				// this check could be removed (?)
  				if (event.start.length === 8) {
  					startDate = startDate.startOf("day");
  				}
  
  				const title = CalendarUtils.getTitleFromEvent(event);
  				Log.debug("title: " + title);
  
  				let excluded = false,
  					dateFilter = null;
  
  				for (let f in config.excludedEvents) {
  					let filter = config.excludedEvents[f],
  						testTitle = title.toLowerCase(),
  						until = null,
  						useRegex = false,
  						regexFlags = "g";
  
  					if (filter instanceof Object) {
  						if (typeof filter.until !== "undefined") {
  							until = filter.until;
  						}
  
  						if (typeof filter.regex !== "undefined") {
  							useRegex = filter.regex;
  						}
  
  						// If additional advanced filtering is added in, this section
  						// must remain last as we overwrite the filter object with the
  						// filterBy string
  						if (filter.caseSensitive) {
  							filter = filter.filterBy;
  							testTitle = title;
  						} else if (useRegex) {
  							filter = filter.filterBy;
  							testTitle = title;
  							regexFlags += "i";
  						} else {
  							filter = filter.filterBy.toLowerCase();
  						}
  					} else {
  						filter = filter.toLowerCase();
  					}
  
  					if (CalendarUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
  						if (until) {
  							dateFilter = until;
  						} else {
  							excluded = true;
  						}
  						break;
  					}
  				}
  
  				if (excluded) {
  					return;
  				}
  
  				const location = event.location || false;
  				const geo = event.geo || false;
  				const description = event.description || false;
  
  				if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
  					const rule = event.rrule;
  					let addedEvents = 0;
  
  					const pastMoment = moment(past);
  					const futureMoment = moment(future);
  
  					// can cause problems with e.g. birthdays before 1900
  					if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) {
  						rule.origOptions.dtstart.setYear(1900);
  						rule.options.dtstart.setYear(1900);
  					}
  
  					// For recurring events, get the set of start dates that fall within the range
  					// of dates we're looking for.
  					// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
  					let pastLocal = 0;
  					let futureLocal = 0;
  					if (CalendarUtils.isFullDayEvent(event)) {
  						Log.debug("fullday");
  						// if full day event, only use the date part of the ranges
  						pastLocal = pastMoment.toDate();
  						futureLocal = futureMoment.toDate();
  
  						Log.debug("pastLocal: " + pastLocal);
  						Log.debug("futureLocal: " + futureLocal);
  					} else {
  						// if we want past events
  						if (config.includePastEvents) {
  							// use the calculated past time for the between from
  							pastLocal = pastMoment.toDate();
  						} else {
  							// otherwise use NOW.. cause we shouldn't use any before now
  							pastLocal = moment().toDate(); //now
  						}
  						futureLocal = futureMoment.toDate(); // future
  					}
  					Log.debug("Search for recurring events between: " + pastLocal + " and " + futureLocal);
  					const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
  					Log.debug("Title: " + event.summary + ", with dates: " + JSON.stringify(dates));
  					// The "dates" array contains the set of dates within our desired date range range that are valid
  					// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
  					// had its date changed from outside the range to inside the range.  For the time being,
  					// we'll handle this by adding *all* recurrence entries into the set of dates that we check,
  					// because the logic below will filter out any recurrences that don't actually belong within
  					// our display range.
  					// Would be great if there was a better way to handle this.
  					Log.debug("event.recurrences: " + event.recurrences);
  					if (event.recurrences !== undefined) {
  						for (let r in event.recurrences) {
  							// Only add dates that weren't already in the range we added from the rrule so that
  							// we don"t double-add those events.
  							if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) {
  								dates.push(new Date(r));
  							}
  						}
  					}
  					// Loop through the set of date entries to see which recurrences should be added to our event list.
  					for (let d in dates) {
  						let date = dates[d];
  						// Remove the time information of each date by using its substring, using the following method:
  						// .toISOString().substring(0,10).
  						// since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ
  						// (see https://momentjs.com/docs/#/displaying/as-iso-string/).
  						const dateKey = date.toISOString().substring(0, 10);
  						let curEvent = event;
  						let showRecurrence = true;
  
  						// Get the offset of today where we are processing
  						// This will be the correction, we need to apply.
  						let nowOffset = new Date().getTimezoneOffset();
  						// For full day events, the time might be off from RRULE/Luxon problem
  						// Get time zone offset of the rule calculated event
  						let dateoffset = date.getTimezoneOffset();
  
  						// Reduce the time by the following offset.
  						Log.debug(" recurring date is " + date + " offset is " + dateoffset);
  
  						let dh = moment(date).format("HH");
  						Log.debug(" recurring date is " + date + " offset is " + dateoffset / 60 + " Hour is " + dh);
  
  						if (CalendarUtils.isFullDayEvent(event)) {
  							Log.debug("Fullday");
  							// If the offset is negative (east of GMT), where the problem is
  							if (dateoffset < 0) {
  								// Remove the offset, independently of the comparison between the date hour and the offset,
  								// since in the case that *date houre < offset*, the *new Date* command will handle this by
  								// representing the day before.
  
  								// Reduce the time by the offset:
  								// Apply the correction to the date/time to get it UTC relative
  								date = new Date(date.getTime() - Math.abs(nowOffset) * 60000);
  								// the duration was calculated way back at the top before we could correct the start time..
  								// fix it for this event entry
  								//duration = 24 * 60 * 60 * 1000;
  								Log.debug("new recurring date1 is " + date);
  							} else {
  								// if the timezones are the same, correct date if needed
  								if (event.start.tz === moment.tz.guess()) {
  									// if the date hour is less than the offset
  									if (24 - dh < Math.abs(dateoffset / 60)) {
  										// apply the correction to the date/time back to right day
  										date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
  										// the duration was calculated way back at the top before we could correct the start time..
  										// fix it for this event entry
  										//duration = 24 * 60 * 60 * 1000;
  										Log.debug("new recurring date2 is " + date);
  									}
  								}
  							}
  						} else {
  							// not full day, but luxon can still screw up the date on the rule processing
  							// we need to correct the date to get back to the right event for
  							if (dateoffset < 0) {
  								// if the date hour is less than the offset
  								if (dh < Math.abs(dateoffset / 60)) {
  									// Reduce the time by the offset:
  									// Apply the correction to the date/time to get it UTC relative
  									date = new Date(date.getTime() - Math.abs(nowOffset) * 60000);
  									// the duration was calculated way back at the top before we could correct the start time..
  									// fix it for this event entry
  									//duration = 24 * 60 * 60 * 1000;
  									Log.debug("new recurring date1 is " + date);
  								}
  							} else {
  								// if the timezones are the same, correct date if needed
  								if (event.start.tz === moment.tz.guess()) {
  									// if the date hour is less than the offset
  									if (24 - dh < Math.abs(dateoffset / 60)) {
  										// apply the correction to the date/time back to right day
  										date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
  										// the duration was calculated way back at the top before we could correct the start time..
  										// fix it for this event entry
  										//duration = 24 * 60 * 60 * 1000;
  										Log.debug("new recurring date2 is " + date);
  									}
  								}
  							}
  						}
  						startDate = moment(date);
  						Log.debug("Corrected startDate (local): " + startDate.toDate());
  
  						let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, date);
  
  						// For each date that we're checking, it's possible that there is a recurrence override for that one day.
  						if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
  							// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
  							curEvent = curEvent.recurrences[dateKey];
  							startDate = moment(curEvent.start);
  							duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
  						}
  						// If there's no recurrence override, check for an exception date.  Exception dates represent exceptions to the rule.
  						else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) {
  							// This date is an exception date, which means we should skip it in the recurrence pattern.
  							showRecurrence = false;
  						}
  						Log.debug("duration: " + duration);
  
  						endDate = moment(parseInt(startDate.format("x")) + duration, "x");
  						if (startDate.format("x") === endDate.format("x")) {
  							endDate = endDate.endOf("day");
  						}
  
  						const recurrenceTitle = CalendarUtils.getTitleFromEvent(curEvent);
  
  						// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
  						// it to the event list.
  						if (endDate.isBefore(past) || startDate.isAfter(future)) {
  							showRecurrence = false;
  						}
  
  						if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) {
  							showRecurrence = false;
  						}
  
  						if (showRecurrence === true) {
  							Log.debug("saving event: " + description);
  							addedEvents++;
  							newEvents.push({
  								title: recurrenceTitle,
  								startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
  								endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
  								fullDayEvent: CalendarUtils.isFullDayEvent(event),
  								recurringEvent: true,
  								class: event.class,
  								firstYear: event.start.getFullYear(),
  								location: location,
  								geo: geo,
  								description: description
  							});
  						}
  					}
  					// End recurring event parsing.
  				} else {
  					// Single event.
  					const fullDayEvent = isFacebookBirthday ? true : CalendarUtils.isFullDayEvent(event);
  					// Log.debug("full day event")
  
  					if (config.includePastEvents) {
  						// Past event is too far in the past, so skip.
  						if (endDate < past) {
  							return;
  						}
  					} else {
  						// It's not a fullday event, and it is in the past, so skip.
  						if (!fullDayEvent && endDate < new Date()) {
  							return;
  						}
  
  						// It's a fullday event, and it is before today, So skip.
  						if (fullDayEvent && endDate <= today) {
  							return;
  						}
  					}
  
  					// It exceeds the maximumNumberOfDays limit, so skip.
  					if (startDate > future) {
  						return;
  					}
  
  					if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) {
  						return;
  					}
  
  					// Adjust start date so multiple day events will be displayed as happening today even though they started some days ago already
  					if (fullDayEvent && startDate <= today) {
  						startDate = moment(today);
  					}
  					// if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00)
  					if (fullDayEvent && startDate.format("x") === endDate.format("x")) {
  						endDate = endDate.endOf("day");
  					}
  					// get correction for date saving and dst change between now and then
  					let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, startDate.toDate());
  					// Every thing is good. Add it to the list.
  					newEvents.push({
  						title: title,
  						startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
  						endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
  						fullDayEvent: fullDayEvent,
  						class: event.class,
  						location: location,
  						geo: geo,
  						description: description
  					});
  				}
  			}
  		});
  
  		newEvents.sort(function (a, b) {
  			return a.startDate - b.startDate;
  		});
  
  		// include up to maximumEntries current or upcoming events
  		// If past events should be included, include all past events
  		const now = moment();
  		let entries = 0;
  		let events = [];
  		for (let ne of newEvents) {
  			if (moment(ne.endDate, "x").isBefore(now)) {
  				if (config.includePastEvents) events.push(ne);
  				continue;
  			}
  			entries++;
  			// If max events has been saved, skip the rest
  			if (entries > config.maximumEntries) break;
  			events.push(ne);
  		}
  
  		return events;
  	},
  
  	/**
  	 * Lookup iana tz from windows
  	 *
  	 * @param {string} msTZName the timezone name to lookup
  	 * @returns {string|null} the iana name or null of none is found
  	 */
  	getIanaTZFromMS: function (msTZName) {
  		// Get hash entry
  		const he = zoneTable[msTZName];
  		// If found return iana name, else null
  		return he ? he.iana[0] : null;
  	},
  
  	/**
  	 * Gets the title from the event.
  	 *
  	 * @param {object} event The event object to check.
  	 * @returns {string} The title of the event, or "Event" if no title is found.
  	 */
  	getTitleFromEvent: function (event) {
  		let title = "Event";
  		if (event.summary) {
  			title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary;
  		} else if (event.description) {
  			title = event.description;
  		}
  
  		return title;
  	},
  
  	/**
  	 * Checks if an event is a fullday event.
  	 *
  	 * @param {object} event The event object to check.
  	 * @returns {boolean} True if the event is a fullday event, false otherwise
  	 */
  	isFullDayEvent: function (event) {
  		if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
  			return true;
  		}
  
  		const start = event.start || 0;
  		const startDate = new Date(start);
  		const end = event.end || 0;
  		if ((end - start) % (24 * 60 * 60 * 1000) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) {
  			// Is 24 hours, and starts on the middle of the night.
  			return true;
  		}
  
  		return false;
  	},
  
  	/**
  	 * Determines if the user defined time filter should apply
  	 *
  	 * @param {Date} now Date object using previously created object for consistency
  	 * @param {Moment} endDate Moment object representing the event end date
  	 * @param {string} filter The time to subtract from the end date to determine if an event should be shown
  	 * @returns {boolean} True if the event should be filtered out, false otherwise
  	 */
  	timeFilterApplies: function (now, endDate, filter) {
  		if (filter) {
  			const until = filter.split(" "),
  				value = parseInt(until[0]),
  				increment = until[1].slice(-1) === "s" ? until[1] : until[1] + "s", // Massage the data for moment js
  				filterUntil = moment(endDate.format()).subtract(value, increment);
  
  			return now < filterUntil.format("x");
  		}
  
  		return false;
  	},
  
  	/**
  	 * Determines if the user defined title filter should apply
  	 *
  	 * @param {string} title the title of the event
  	 * @param {string} filter the string to look for, can be a regex also
  	 * @param {boolean} useRegex true if a regex should be used, otherwise it just looks for the filter as a string
  	 * @param {string} regexFlags flags that should be applied to the regex
  	 * @returns {boolean} True if the title should be filtered out, false otherwise
  	 */
  	titleFilterApplies: function (title, filter, useRegex, regexFlags) {
  		if (useRegex) {
  			// Assume if leading slash, there is also trailing slash
  			if (filter[0] === "/") {
  				// Strip leading and trailing slashes
  				filter = filter.substr(1).slice(0, -1);
  			}
  
  			filter = new RegExp(filter, regexFlags);
  
  			return filter.test(title);
  		} else {
  			return title.includes(filter);
  		}
  	}
  };
  
  if (typeof module !== "undefined") {
  	module.exports = CalendarUtils;
  }
  