/// <reference path="listeners.ts" />

const oneDay = 86400000;

const daysOfMonth = [31,28,31,30,31,30,31,31,30,31,30,31];
//https://www.timeanddate.com/sun/russia/saint-peterburg
const dayLengthMax = 1341;// The longest day has daylight of 18:51 and civil daylight of 3:30 which adds up to 22:21, which are 1341 minutes
const dayLengthMin = 468; // The shortest day has daylight of 6:53 and civil daylight of 1:55 which adds up to  7:48, which are  468 minutes
const seasons = [
	'winter',
	'winter',
	'winter',
	'spring',
	'summer',
	'summer',
	'summer',
	'summer',
	'summer',
	'autumn',
	'winter',
	'winter'
];

const holidays = {
	winter : {
		start: [1,1],
		end: [1,15],
		school: true,
		work: false
	},
	spring : {
		start: [3,20],
		end: [3,26],
		school: true,
		work: false
	},
	summer : {
		start: [6,1],
		end: [8,31],
		school: true,
		work: false
	},
	autumn : {
		start: [11,4],
		end: [11,11],
		school: true,
		work: false
	},
}

const enum EDayVacationState{
	
	/**
	 * No holidays today.
	 */
	DEFAULT,

	/**
	 * Holidays for school students (such as summer vacations; also includes everyone elses holidays).
	 */
	SCHOOL_HOLIDAY,

	/**
	 * Holidays for everyone (such as Christmas).
	 */
	HOLIDAY
}

/**
 * Only ever roll time forwards by increasing minutes. This is the only way skills, stats, etc. get updated.
 * Increasing day and hour only excute some function. Avoid if possible. These fields are for internatl use only.
 * Increasing month or year won't execute events.
 */
class GameTime{

	_now = undefined;

	get minutes(){
		return this._now.getUTCMinutes();
	}
	set minutes(v:number){
		if(typeof v !== "number"){
			console.error("Time Increase by invalid value",v);
			return;
		}

		if(Number.isNaN(v)){
			console.error('Criticial: Time set to NaN! Aborting!');
			return;
		}
		v = Math.ceil(v);

		let minutes = this._now.getUTCMinutes();
		let timeToAdd = v - minutes;

		if(timeToAdd == 0)
			return;
		this.execute_every_timeUpdate(timeToAdd);

		let currentMinutesIn15MinutesInterval = minutes % 15;
		let minutesToCompleteCurrent15MinutesInterval = 15 - currentMinutesIn15MinutesInterval;

		if(minutesToCompleteCurrent15MinutesInterval > timeToAdd){
			this._now.setUTCMinutes(minutes + timeToAdd);
			this.execute_every_timeUpdate_post();
			return;
		}

		let currentIterationTimeToAdd = minutesToCompleteCurrent15MinutesInterval;

		while(timeToAdd > 0){
			let timeToSet = this._now.getTime() + currentIterationTimeToAdd * 60000;
			this._now = new Date(timeToSet);
			if(timeToSet % 900000 == 0)
				this.execute_every_15_minutes();
			if(this.minutes == 0){
				this.execute_every_1_hour();
				if(this.hour == 0){
					this.execute_every_1_day();
					if(this.day == 1){
						this.execute_every_1_month();
						if(this.month == 1){
							this.execute_every_1_year();
						}
					}
				}
			}


			timeToAdd -= currentIterationTimeToAdd;
			currentIterationTimeToAdd = Math.min(15,timeToAdd);
		}

		//this._now.setUTCMinutes(minutes % 60);

		this.execute_every_timeUpdate_post();
	}
	get hour(){return this._now.getUTCHours();}
	set hour(v){
		const oneDay = 86400000;
		const now = this.now;
		let targetTime = new Date(now);
		targetTime.setUTCHours(v,0);
		let millisecondDifference = targetTime.getTime() - now.getTime();
		if(millisecondDifference < 0)
			millisecondDifference += 86400000;
		const minuteDifference = Math.floor(millisecondDifference / 60000);
		this.minutes += minuteDifference;
	}

	get seconds(){
		return 0;
	}

	_startDate = null;
	get startDate():Date{return this._startDate;}

	get year(){return this._now.getUTCFullYear();}
	set year(v){this._now.setUTCFullYear(v);}
	get month(){return this._now.getUTCMonth()+1;}
	/*set month(v){
		this._now.setUTCMonth(v-1);
	}*/

	/**
	 * The day-part of the current date. Example: 29.02.2018 would have a day of 29.
	 */
	get day(){return this._now.getUTCDate();}
	/*set day(v){
		let daysToAdd = v - this.day;
		let month = this.month;

		for(let i = 0; i < daysToAdd; i++){
			this._now.setUTCDate(this._now.getUTCDate()+1);
			if(this.month != month){
				this.execute_every_1_month();
				if(this.month == 1)
					this.execute_every_1_year();
				month = this.month;
			}
		}

		/*while(v > daysOfMonth[this.month + 1]){
			this.execute_every_1_month();
			v-= daysOfMonth[this.month + 1];
			this.month += 1;

		}
		this._now.setUTCDate(v);*/
	//}

	//
	
	/**
	 * From 1 (Monday) to 7 (Sunday)
	 * @readonly
	 * @type {number}
	 */
	get weekday(){
		return this._now.getUTCDay() || 7;
	}

	
	/**
	 * Days since the game started. 1 is the first day. Check `daystartS` if you want to have the switch between days at sleeptime.
	 * @readonly
	 * @type {number}
	 */
	get daystart():number{
		return this.#daystartWithOffset();
	}

	/**
	 * Days since the game started. 1 is the first day. Offset so that the switch from one day to the next is at a time when the PC is most likely asleep.
	 * @readonly
	 * @type {number}
	 */
	get daystartS():number{
		const _4hours30minutes = 16200000;
		return this.#daystartWithOffset(_4hours30minutes);
	}

	#daystartWithOffset(offset:number=0):number{
		return Math.floor(Math.abs((this.now.getTime() - this.startDate.getTime() - offset) / oneDay)) + 1;
	}

	daystartNextAtNthOfMonth(n: number){
		const now = this.now;
		let nextDate = new Date(now);
		if(n <= now.getUTCDate())
			nextDate.setUTCMonth(now.getUTCMonth() + 1);
		nextDate.setUTCDate(n);
		const timeDifference = nextDate.getTime() - now.getTime();
		const daysDifference = Math.floor(timeDifference / 86400000);

		return daysDifference + this.daystart;
	}

	daystartNextAtWeekday(weekday: number){
		const currentWeekday = this.weekday;
		if(weekday > currentWeekday)
			return weekday - currentWeekday + this.daystart;
		return 7 + weekday - currentWeekday + this.daystart;
	}

	
	/**
	 * @param {number} daystart
	 * @returns {*}
	 */
	daystartToDate(daystart: number){
		return new Date(this._startDate.getTime() + (daystart-1)*86400000);
	}

	// Years that have passed since the game started. Starts with 1 in the first year.
	get yearstart(){
		const today = this.today;
		const startDate = this.startDate;
		let startDateInCurrentYear = clone(startDate);
		startDateInCurrentYear.setUTCFullYear(today.getUTCFullYear());

		if(startDateInCurrentYear > today)
			//It's early in the year
			return today.getUTCFullYear() - startDate.getUTCFullYear();
		return today.getUTCFullYear() - startDate.getUTCFullYear() + 1;
	}

	initTime(yearOrDate: number | Date,month=undefined,day=undefined,hour=undefined,minute=undefined){
		if(yearOrDate instanceof Date){
			this._startDate = clone(yearOrDate);
			this._now = clone(yearOrDate);
			this._startDate.setUTCHours(0,0,0,0);
		}
		else if(typeof yearOrDate == "number" &&
				typeof month == "number" &&
				typeof day == "number" &&
				typeof hour == "number" &&
				typeof minute == "number"){
			this._startDate = new Date(Date.UTC(yearOrDate,month-1,day));
			this._now = new Date(Date.UTC(yearOrDate,month-1,day,hour,minute));
		}else{
			console.error('Arguments for time.initDate not supported:',yearOrDate,month,day,hour,minute);
		}
		console.log("$time.initTime (startDate,now)",this._startDate,this._now);

	}

	get isNight(){
		const dayStage = this.dayStage;
    	return !(dayStage >= 1 && dayStage <= 3);
	}

	get dayDistanceToMiddleOfYear(){
		let dayOfYear = this.dayOfYear;
		let distanceToMiddleOfYear = 0;
		if(dayOfYear >= 354)
			distanceToMiddleOfYear = 536 - dayOfYear; // 183 (half a year) + 353 (winter equinox) - current day
		else if(dayOfYear >= 171)
			distanceToMiddleOfYear = dayOfYear - 171;
		else
			distanceToMiddleOfYear = 171-dayOfYear;
		return distanceToMiddleOfYear;
	}

	get dayLength(){
		return (dayLengthMax-(dayLengthMax-dayLengthMin)*this.dayDistanceToMiddleOfYear/182);
	}

	get dayOfYear(){
		let days = 0;
		for(let i=0; i < this.month-1; i++){
			days += daysOfMonth[i];
		}
		days += this.day;
		return days;
	}

	get dawnDuskLength(){
		return 120;
	}

	get dayStage(){
		/*1 - dawn
		2 - midday
		3 - sunset
		4 - the beginning of the night
		5 - night
		6 - the end of the night*/
		let minutesSinceMidnight = this.minutesSinceMidnight;
		let dayStages = this.dayStagesStartEnd;
		for (const [key, startEnd] of Object.entries(dayStages)) {
			if(startEnd[0] < startEnd[1]){
				if(minutesSinceMidnight >= startEnd[0] && minutesSinceMidnight < startEnd[1]){
					return parseInt(key);
				}
			}else{
				if(minutesSinceMidnight >= startEnd[0] || minutesSinceMidnight < startEnd[1]){
					return parseInt(key);
				}
			}
		}
		console.warn('dayStage calculation failed, returning default value');
		return 2;
	}

	// Return the start and end times of each stage of the current day in minutes since midnight
	get dayStagesStartEnd(){
		let DayLength = this.dayLength;
		let ddl = this.dawnDuskLength;
		let half_ddl = ddl/2;
		let sunZenithMinutes = this.sunZenithMinutes;

		let dawnStart		= sunZenithMinutes - DayLength / 2;
		let dawnEnd			= dawnStart + half_ddl;
		let earlyDawnStart	= dawnStart - half_ddl;
		let earlyDawnEnd	= dawnStart;

		let duskEnd			= sunZenithMinutes + DayLength / 2;
		let duskStart		= duskEnd - half_ddl;
		let lateDuskStart	= duskEnd;
		let lateDuskEnd		= lateDuskStart + half_ddl;

		let middayStart		= dawnEnd;
		let middayEnd		= duskStart;

		let nightStart		= lateDuskEnd;
		let nightEnd		= earlyDawnStart;

		return {
			"1": [dawnStart,dawnEnd],
			"2": [middayStart,middayEnd],
			"3": [duskStart,duskEnd],
			"4": [lateDuskStart,lateDuskEnd],
			"5": [nightStart,nightEnd],
			"6": [earlyDawnStart,earlyDawnEnd],
		};
	}

	get endTime(){
		return new Date(9999,11,31);
	}

	get isWinter(){
		return (seasons[this.month - 1] == 'winter');
	}

	get minutesSinceMidnight() {
		return this.hour * 60 + this.minutes;
	}

	
	/**
	 * No need to clone this value.
	 * @readonly
	 * @type {Date}
	 */
	get now():Date{
		return clone(this._now);
	}

	
	/**
	 * Description placeholder
	 * @date 10/3/2023 - 1:06:21 PM
	 *
	 * @param {number} offset
	 * @returns {Date}
	 */
	nowWithMinutesOffset(offset: number){
		let now = this.now;
		now.setUTCMinutes(now.getUTCMinutes() + offset);
		return now;
	}

	get tomorrow(){
		var tomorrow = this.now;
		tomorrow.setUTCDate(tomorrow.getUTCDate()+1);
		tomorrow.setUTCHours(0,0,0,0);
		return tomorrow;
	}

	secondsSinceDate(date: { getTime: () => number; }){
		if(!(date instanceof Date))
			return Infinity;
		return Math.floor((this.now.getTime() - date.getTime()) / 1000);
	}

	
	/**
	 * Returns whether a given Date is in the future.
	 * @date 10/3/2023 - 1:10:48 PM
	 *
	 * @param {Date} date
	 * @returns {boolean}
	 */
	isFuture(date: Date){
		try{
			return (this.secondsSinceDate(date) < 0);
		}catch(e){
			console.error("ERROR in $time.isFuture",date);
			return false;
		}
	}

	get today():Date{
		let now = clone(this.now);
		now.setUTCHours(0,0,0,0);
		return now;
	}

	dayOfIndex(index: number){
		let today = this.today;
		let currentIndex = this.daystart;
		let diff = index - currentIndex;
		today.setUTCDate(today.getUTCDate() + diff);
		return today;
	}

	dayOfIndexS(index: number):Date{
		const _4hours30minutes = 16200000;
		let today = this.today;
		let currentIndex = this.daystartS;
		let diff = index - currentIndex;
		today.setUTCDate(today.getUTCDate() + diff);
		today.setTime(today.getTime()+_4hours30minutes);
		return today;
	}

	/**
	 * Returns a day that is in the future by `offset` (for example, offset 1 means tomorrow).
	 * Days switch at migdnight. Usually you want them to change when the player sleeps instead. Use `dayWithOffsetS()` in this case.
	 * @param {number} offset
	 * @returns {Date}
	 */
	dayWithOffset(offset: number):Date{
		return this.dayOfIndex(this.daystart + offset);
	}

	
	/**
	 * Returns a day that is in the future by `offset` (for example, offset 1 means tomorrow).
	 * Days switch at a time when the player is usually asleep (4:30).
	 * @param {number} offset
	 * @returns {Date}
	 */
	dayWithOffsetS(offset: number):Date{
		return this.dayOfIndexS(this.daystartS + offset);
	}

	time(hour: number,minute: number,flags={}){
		let currentTime = this.now;
		currentTime.setUTCHours(hour,minute);

		return currentTime;
	}

	//Minutes since midnight at which the sun is at his zenith
	//It's set to 13:00
	get sunZenithMinutes(){
		return 780;
	}

	get sunTime(){
		let CurTimeSun = this.hour*60 + this.minutes;
		//CurTimeSun (minuts) <0 - before midday; >0 - after midday
		CurTimeSun = CurTimeSun-this.sunZenithMinutes

		return CurTimeSun;
	}

	advanceTo(hour: number,minute: number,advanceToNextDay=false){
		let minuteDifference = (hour * 60 + minute) - (this.hour * 60 + this.minutes);
		if(minuteDifference > 0){
			this.minutes += minuteDifference;
		}else if(advanceToNextDay){
			this.minutes += 1440 + minuteDifference;
		}
		console.log("Time: advanceTo",hour,minute,advanceToNextDay,minuteDifference,this.now);
	}

	ageOfDate(dayOrDate:number|Date,month=undefined,year=undefined){
		let day = dayOrDate;

		if(dayOrDate instanceof Date){
			day = dayOrDate.getUTCDate();
			month=dayOrDate.getUTCMonth() + 1;
			year = dayOrDate.getUTCFullYear();
		}

		let yearDifference = this.year - year;
		if(month > this.month || (month == this.month && day > this.day))
			return yearDifference - 1;
		return yearDifference;
	}

	//29th Feb is not possible
	dateOfAge(age: number){
		let year = this.year - age;
		let randomMonth = rand(1,12);
		let randomDay = rand(1,daysOfMonth[randomMonth-1]);
		if(randomMonth > this.month || (randomMonth == this.month && randomDay > this.day))
			year -= 1;
		let date = new Date(Date.UTC(year,randomMonth-1,randomDay));
		console.log('Generated Date:',date);
		return date;
	}

	//Year -1: Current year, -2: Year with the next occurance of the date (either this our next)
	daysDifference(day: number,month: number,year=-1){
		const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds
		let currentDate = this.today;
		let return_positive = false;
		if(year == -1)
			year = this.year;
		else if(year == -2){
			year = this.year;
			return_positive = true;
		}
		let testDate = new Date(Date.UTC(year, month-1, day));
		let diffDays = Math.round((testDate.getTime() - currentDate.getTime()) / oneDay);
		if(diffDays < 0 && return_positive)
			return this.daysDifference(day,month,this.year+1);
		return diffDays;
	}

	dayStartToDateArray(d: number){
		let currentDayStart = this.daystart;
		let dayStartDiff = d - currentDayStart;
		let currentDate = this.today;
		currentDate.setDate(currentDate.getDate() + dayStartDiff);
		return [currentDate.getDate(),currentDate.getMonth(),currentDate.getFullYear()];
	}

	
	/**
	 * How many minutes is the time in the past. Negativ if it is in the future.
	 * @date 9/10/2023 - 7:43:26 PM
	 *
	 * @param {number} hour
	 * @param {number} minute
	 * @returns {number}
	 */
	timeDifference(hour: number,minute: number){
		return (this.hour - hour) * 60 + (this.minutes - minute);
	}

	// execute every n time-units

	execute_every_15_minutes(){
		console.log("Time: 15-Minutes-Tick",this.hour,this.minutes);
		State.variables.pc.execute_every_15_minutes();
	}

	execute_every_1_hour(){
		console.log("Time: 1-Hour-Tick",this.hour,this.minutes);
		State.variables.pc.execute_every_1_hour();
	}

	execute_every_1_day(){
		console.log("Time: 1-Day-Tick",this.daystart,this.hour,this.minutes);
		State.variables.pc.execute_every_1_day();
		State.variables.finances.execute_every_1_day();
	}

	execute_every_1_month(){
		console.log("Time: 1-Month-Tick",this.month,this.day,this.hour,this.minutes);
	}

	execute_every_1_year(){
		console.log("Time: 1-Year-Tick",this.year,this.month,this.day,this.hour,this.minutes);
	}

	execute_every_timeUpdate(timeToAdd: number){
		for(const listener of Object.values(setup.listeners_timeUpdate))
			listener(timeToAdd);
		State.variables.pc.execute_every_timeUpdate(timeToAdd);
	}

	//executed whenever the time changes after everything else has been calculated
	execute_every_timeUpdate_post(){
		/*let missedEvents = this.getPastTimedEvents(undefined,true);
		for(let missedEventIndex in missedEvents){
			let missedEvent = missedEvents[missedEventIndex];
			switch(missedEvent.key){
				case 'date':
					$.wiki('<<datingMissed `'+JSON.stringify(missedEvent)+'`>>');
					break;
				case 'work':
					const jobs = State.variables.jobs;
					jobs.workMissed(missedEvent.data.jobId,missedEvent.endTime);
				default:
					if(missedEvent.data?.type){
						switch(missedEvent.data.type){
							case 'questEvent':
								let questId = missedEvent.data.questId;
								let eventId = missedEvent.data.eventId;
								let repeat = missedEvent.data.repeat;
								State.variables.q.eventExecute(questId,eventId);
								switch(repeat){
									case 'daily':
										this.rescheduleTimedEventByKey(missedEvent.key,'day',1);
										break;
								}
								break;
						}
					}
			}
			missedEvents[missedEventIndex].handled = true;
		}*/
	}


	get minutesTimestamp(){
		//  Zero minutesTimestamp will be at New Years Eve of 2015
		//return this.minutesSince(2015,1,1,0,0);
		let yearDifference = this.year - 2015;
		let dayDifference  = this.day - 1;
		let hourDifference = this.hour;
		let minutesDifference=this.minutes;

		let days = yearDifference * 365;
		for(let i=0 ; i < this.month - 1;i++){
			days += daysOfMonth[i];
		}
		days += dayDifference;

		let minutes = (days * 24 + hourDifference) * 60 + minutesDifference;
		return minutes;

	}

	// Output
	get dateTimeString(){
		if(!this._now)
			return '';
		let year = (''+this.year).padStart(4,'0');
		let month = (''+this.month).padStart(2,'0');
		let day = (''+this.day).padStart(2,'0');
		let hour = (''+this.hour).padStart(2,'0');
		let minutes = (''+this.minutes).padStart(2,'0');
		let date = day+'.'+month+'.'+year;
		let time = hour+':'+minutes;
		return date+' '+time;
	}

	// ----- special_events -----
	// Special Events are one day long, such has your graduation
	/*_specialEvents = [

	]

	addEvent(key,day,month,year){
		this._specialEvents.push({
			key: key,
			day: day,
			month: month,
			year: year
		});
	}

	currentSpecialEvents(){
		let result = [];
		for(let specialEvent of this._specialEvents){
			if(specialEvent.day == this.day && specialEvent.month == this.month && specialEvent.year == this.year)
				result.push(specialEvent.key);
		}
		return result;
	}*/

	// ----- timed events -----
	// Timed events have a start date and time, duration and meta-informationen, such as a scheduled date
	_timedEvents:any = {_system:{nextId:0},events:{}}

	addTimedEvent(key: string,day: number,month: number,year: number,hour: number,minute: number,duration: number,data={}){
		let startTime = new Date(Date.UTC(year, month - 1, day, hour, minute, 0));
		return this.addTimedEventByDate(key,startTime,duration,data);
	}

	addTimedEventByDate(key: any,startTime: Date,duration: number,data={}){
		let endTime = new Date(startTime.getTime() + duration*60000);
		let timedEvent = {
			key: key,
			startTime: startTime,
			endTime: endTime,
			duration : duration,
			data: data
		};

		let id = ''+this._timedEvents._system.nextId++;

		//this._timedEvents.push(timedEvents);
		this._timedEvents.events[id] = timedEvent;
		console.log('Timed event added',id,timedEvent);
		return id;
	}

	getCurrentTimedEvents(key=undefined){
		let result = [];
		let now = this.now;
		for(let timedEvent of Object.values(this._timedEvents.events)){
			if(key === undefined || timedEvent['key'] == key){
				if(now.getTime() >= timedEvent['startTime'].getTime() && now.getTime() <= timedEvent['endTime'].getTime())
					result.push(timedEvent);
			}
		}
		return result;
	}

	getPastTimedEvents(key=undefined,ignoreHandled = false){
		let result = [];
		let now = this.now;
		for(let timedEvent of Object.values(this._timedEvents.events)){
			if(ignoreHandled && timedEvent['handled'])
				continue;
			if(key === undefined || timedEvent['key'] == key){
				if(now.getTime() > timedEvent['endTime'].getTime())
					result.push(timedEvent);
			}
		}
		return result;
	}

	rescheduleTimedEventById(id: string | number,dateTime: { getTime: () => number; }){
		if(!this._timedEvents.events[id])
			return false;
		const duration = this._timedEvents.events[id].duration;
		this._timedEvents.events[id].startTime = dateTime;
		this._timedEvents.events[id].endTime= new Date(dateTime.getTime() + duration*60000);
		return true;
	}

	rescheduleTimedEventByKey(key: any,offsetMode='day',offsetCount=1){
		for(let timedEvent of Object.values(this._timedEvents.events)){
			if(timedEvent['key'] == key){
				switch(offsetMode){
					case 'day':
						timedEvent['startTime'].setUTCDate(timedEvent['startTime'].getUTCDate() + offsetCount);
						timedEvent['endTime'].setUTCDate(timedEvent['endTime'].getUTCDate() + offsetCount);
						break;
					case 'month':
						timedEvent['startTime'].setUTCMonth(timedEvent['startTime'].getUTCMonth() + offsetCount);
						timedEvent['endTime'].setUTCMonth(timedEvent['endTime'].getUTCMonth() + offsetCount);
						break;
					case 'year':
						timedEvent['startTime'].setUTCFullYear(timedEvent['startTime'].getUTCFullYear() + offsetCount);
						timedEvent['endTime'].setUTCFullYear(timedEvent['endTime'].getUTCFullYear() + offsetCount);
						break;
				}
			}
			console.log("Timed event rescheduled:",timedEvent);
		}
	}

	unscheduleTimedEventsByKeyAndData(key: any,data: ArrayLike<unknown> | { [s: string]: unknown; }){
		this._timedEvents = Object.values(this._timedEvents.events).filter((e)=>{
			if(e['key'] != key)
				return true;
			for (const [data_key, data_value] of Object.entries(data)) {
				if(data_value != e['data'][data_key])
					return true;
			}
			return false;
		});
	}

	unscheduleTimedEventById(id: string | number){
		return (delete this._timedEvents.events[id]);
	}

	// ----- holidays ----

	//Returns an array of the indizes of the current holidays
	
	currentHolidays(includeSchoolHolidays=true,includeWorkHolidays=true){
		return this.holidaysOfDate(this.now,includeSchoolHolidays,includeWorkHolidays);
	}

	holidaysOfDate(date: Date,includeSchoolHolidays=true,includeWorkHolidays=true){

		date = new Date(date.getTime());
		date.setUTCHours(0,0,0,0);

		let h = holidays;
		let result = [];
		for (const [holiday_key, holiday_value] of Object.entries(h)) {
			let startDate = new Date(date.getTime());
			startDate.setUTCMonth(holiday_value.start[0]-1,holiday_value.start[1]);
			let endDate = new Date(date.getTime());
			endDate.setUTCMonth(holiday_value.end[0]-1,holiday_value.end[1]);

			if(date.getTime() >= startDate.getTime() && date.getTime() <= endDate.getTime()){
				if(
					(includeSchoolHolidays && holiday_value.school) ||
					(includeWorkHolidays   && holiday_value.work)
				)
					result.push(holiday_key);
			}
		}
		return result;
	}

	daysOfHolidayLeft(holidayId: string | number){
		let holiday = holidays[holidayId];
		return this.daysDifference(holiday.end[1],holiday.end[0],-2) + 1; //+1 since the end of the holidays is counted as well
	}

	get isSchoolDay(){
		if(this.weekday >= 6)
			return false;
		return !this.isSchoolHoliday;
	}

	get isSchoolHoliday(){
		return this.dateIsSchoolHoliday(this.now);
	}

	
	/**
	 * @param {Date} date
	 * @returns {boolean}
	 */
	dateIsSchoolHoliday(date: Date):boolean{
		let holidayIds = this.holidaysOfDate(date);
		for(let holidayId of holidayIds){
			let holidayData = holidays[holidayId];
			if(holidayData.school)
				return true;
		}
		return false;
	}

	dateIsWorkHoliday(date: Date):boolean{
		return (this.holidaysOfDate(date,false,true).length > 0);
	}

	
	/*
	_timetableConditionMet(condition: string | string[],dateTime=undefined){
		if(!condition)
			return true;

		dateTime ??= this.now;

		const weekDay = dateTime.getUTCDay();
		const isWeekend = (weekDay == 0 || weekDay == 6);

		switch(condition){
			case 'always':			return true;
			case 'else':			return false;
			case 'noSchoolHoliday':	return !this.dateIsSchoolHoliday(dateTime);
			case 'schoolHoliday':	return this.dateIsSchoolHoliday(dateTime);
			case 'weekday':			return !isWeekend;
			case 'weekend':			return isWeekend;
		}
		
		const todaysAbbrev = ['mo','tu','we','th','fr','sa','su'][weekDay];
		if(condition.indexOf(todaysAbbrev) == -1)
			return false;
		return true;
	}

	_timetableGetSchedules(timetable,dateTime=undefined){
		let todaysSchedules = [];
		dateTime ??= this.now;

		for(const [scheduleDay,daySchedules] of Object.entries(timetable)){
			if(!this._timetableConditionMet(scheduleDay))
				continue;

			if(Array.isArray(daySchedules))
				todaysSchedules = todaysSchedules.concat(daySchedules);
			else
				todaysSchedules = todaysSchedules.concat(this._timetableGetSchedules(daySchedules));
		}
		
		if(todaysSchedules.length == 0 && ('else' in timetable)){
			if(Array.isArray(timetable.else))
				todaysSchedules = timetable.else;
			else
				todaysSchedules = this._timetableGetSchedules(timetable.else);
		}

		todaysSchedules = todaysSchedules.filter(schedule => this._timetableConditionMet(schedule.condition));

		todaysSchedules.sort((a,b)=>(a.start[0]*100+a.start[1]) - (b.start[0]*100+b.start[1]));
		return todaysSchedules;
	}*/


	#timetableEntriesByDay<T>(timetable: Timetable<T>,day:Date, defaultBegin:number[] = [0,0]):{val:T,begin:number[]}[]{
		let entries:{val:T,begin:number[]}[] = [];
		for(const entry of Object.values(timetable)){
			if(this.#timespanIdentifierIsDay(entry.time,day)){
				if(entry.result)
					entries.push({begin:entry.time.begin ?? defaultBegin, val: entry.result});
				else if(entry.either)
					entries.push({begin:entry.time.begin ?? defaultBegin, val: entry.either.random()});
				else if(entry.subtable){
					entries.push(...(this.#timetableEntriesByDay<T>(entry.subtable,day,entry.time.begin ?? defaultBegin)));
				}
			}
		}
		return entries.toSorted((a,b)=>a.begin[0]*60+a.begin[1]-b.begin[0]*60-b.begin[1]);
	}

	timetableLookup<T>(timetable: Timetable<T>,dateTime:Date=undefined):null|T{
		return this.timetableLookupMultiple<T>(timetable,dateTime,1)[0].val;
	}

	timetableLookupMultiple<T>(timetable: Timetable<T>,dateTime:Date=undefined,n:number=2):{val:T,beginDate:Date}[]{
		let entries:{val:T,beginDate:Date}[] = [];
		
		dateTime ??= this.now;
		const timetableEntriesByDay:{val:T,begin:number[]}[] = this.#timetableEntriesByDay<T>(timetable,dateTime);

		const currentTimeMinutes = dateTime.getUTCHours() * 60 + dateTime.getUTCMinutes();

		let lookedAtTheActivityWeAreCurrentlyAt = false;

		for(const timetableEntry of timetableEntriesByDay.toReversed()){
			let timeMatch = false;
			const entryMinuts = timetableEntry.begin[0] * 60 + timetableEntry.begin[1];
			if(entryMinuts >= currentTimeMinutes)
				timeMatch = true;
			if(!lookedAtTheActivityWeAreCurrentlyAt && entryMinuts <= currentTimeMinutes){
				lookedAtTheActivityWeAreCurrentlyAt = true;
				timeMatch = true;	
			}
			if(timeMatch){
				let beginTime = new Date(dateTime.getTime());
				beginTime.setUTCHours(timetableEntry.begin[0],timetableEntry.begin[1],0,0);
				entries.unshift({
					val: timetableEntry.val,
					beginDate: beginTime
				});
				if(entries.length > n)
					entries.pop();
			}
		}

		if(entries.length < n){
			const missingElementCount = n - entries.length;
			let tomorrow = new Date(dateTime.getTime() + oneDay);
			tomorrow.setUTCHours(0,0,0,0);
			let elementsFromTomorrow = this.timetableLookupMultiple(timetable,tomorrow,missingElementCount);
			entries.push(...elementsFromTomorrow);
		}

		return entries;

	}

	//#region TimespanIdentifier

		timespanIdentifierMatches(ti:TimespanIdentifier,time:Date=undefined):boolean{
			time ??= this.now;
			if(!this.#timespanIdentifierIsDay(ti,time))
				return false;

			const start = ti.begin ?? [0,0];
			const end = ti.end ?? [0,0];

			if(!this.#timespanIdentifierIsInTime(start,end,time))
				return false;

			switch (this.#timespanIdentifierInMultidayTimeSlot(start,end,time)) {
				case -1:
					const yesterday = new Date(time.getTime() - 86400000);
					return this.#timespanIdentifierIsDay(ti,yesterday);
				case 1:
					const tomorrow = new Date(time.getTime() + 86400000);
					return this.#timespanIdentifierIsDay(ti,tomorrow);
			}

			return true;
		}

		#timespanIdentifierIsInTime(start:number[],end:number[],time:Date):boolean{
			let openMinutes = start[0] * 60 + start[1];
			let closeMinutes= end[0]* 60 + end[1];

			let minutesSinceMidnight = time.getUTCHours()*60+time.getUTCMinutes();

			//Default case: the place isn't open over midnight
			if(openMinutes <= closeMinutes)
				return (minutesSinceMidnight >= openMinutes && minutesSinceMidnight < closeMinutes);

			if(minutesSinceMidnight >= openMinutes)
				return true;
			if(minutesSinceMidnight < closeMinutes)
				return true;
		}

		#timespanIdentifierIsDay(ti:TimespanIdentifier,time:Date):boolean{
			if(typeof ti.holiday == "number"){
				/*if(this.dateIsSchoolHoliday(time))
					return ti.holiday;*/
				switch(ti.holiday){
					case EDayVacationState.DEFAULT:
						return (!this.dateIsSchoolHoliday(time) && !this.dateIsWorkHoliday(time));
					case EDayVacationState.SCHOOL_HOLIDAY:
						return this.dateIsSchoolHoliday(time);
					case EDayVacationState.HOLIDAY:
						return (this.dateIsWorkHoliday(time) || this.dateIsSchoolHoliday(time));
				}
			}
			
			const weekday = time.getUTCDay() || 7;
			if(ti.daysBL){
				if(this.#timespanIdentifierDaysParse(ti.daysBL).includes(weekday))
					return false;
			}

			if(ti.days){
				if(!this.#timespanIdentifierDaysParse(ti.days).includes(weekday))
					return false;
			}

			return true;
		}

		

		/**
		 * -1 if early in the morning, 1 if at night, 0 otherwise.
		 * @param start 
		 * @param end 
		 * @param time 
		 * @returns 
		 */
		#timespanIdentifierInMultidayTimeSlot(start:number[],end:number[],time:Date):number{
			let openMinutes = start[0] * 60 + start[1];
			let closeMinutes= end[0]* 60 + end[1];

			let minutesSinceMidnight = time.getUTCHours()*60+time.getUTCMinutes();

			//Default case: the place isn't open over midnight
			if(openMinutes <= closeMinutes)
				return 0;

			if(minutesSinceMidnight >= openMinutes)
				return 1;
			if(minutesSinceMidnight < closeMinutes)
				return -1;
			return 0;
		}

		#timespanIdentifierDaysParse(days:string[]):number[]{
			let daysOnWhitelist:number[] = [];
			const dayArray = ['','MO','TU','WE','TH','FR','SA','SU'];
			for(const dayWL of days){
				const index = dayArray.indexOf(dayWL);
				if(index > 0){
					daysOnWhitelist.pushUnique(index);
					continue;
				}
				if(dayWL == 'work'){
					daysOnWhitelist.pushUnique(1,2,3,4,5);
					continue;
				}
			}
			return daysOnWhitelist;
		}

		timespanIdentifierToString(ti:TimespanIdentifier):string{
			let dayCalc = ((x: number) => ((x+7) % 7) || 7);

			let result:string[] = [];

			let allWeekDays = [1,2,3,4,5,6,7];

			let dayWhitelist = [];
			if(ti.days)
				dayWhitelist = this.#timespanIdentifierDaysParse(ti.days);
			else
				dayWhitelist = clone(allWeekDays);

			let dayBlacklist = [];
			if(ti.daysBL)
				dayBlacklist = this.#timespanIdentifierDaysParse(ti.daysBL);
			
			let days = dayWhitelist;
			for(const dayOnBL of dayBlacklist)
				days.delete(dayOnBL);

			if(days.length == 7)
				result.push(`everyday`);
			else if(days.length == 6){
				const missingDay = 28 - days.reduce((previous,current)=>previous+current,0);
				result.push(`on all days, except on ${setup.weekDayIntToString(missingDay)}s`);
			}else if(days.length == 5){
				let missingDays = allWeekDays.filter(x => !days.includes(x));
				missingDays.sort();
				if(missingDays[1] == missingDays[0] + 1){
					const firstDayOfInterval = dayCalc(missingDays[1] + 1);
					const lastDayOfInterval = dayCalc(missingDays[1] + 5);
					result.push(`from ${setup.weekDayIntToString(firstDayOfInterval)} to ${setup.weekDayIntToString(lastDayOfInterval)}`);
				}else if(missingDays[1] == 7 && missingDays[0] == 1){
					const firstDayOfInterval = 2;
					const lastDayOfInterval = 6;
					result.push(`from ${setup.weekDayIntToString(firstDayOfInterval)} to ${setup.weekDayIntToString(lastDayOfInterval)}`);
				}else{
					result.push(`on all days, except on ${setup.weekDayIntToString(missingDays[0])}s and ${setup.weekDayIntToString(missingDays[1])}s`);
				}
			}else if(days.length == 4){
				let missingDays = allWeekDays.filter(x => !days.includes(x));
				missingDays.sort();
				days.sort();
				if(missingDays.isConsecutive()){
					const firstDayOfInterval = dayCalc(missingDays.last() + 1);
					const lastDayOfInterval = dayCalc(missingDays.first() - 1);
					result.push(`from ${setup.weekDayIntToString(firstDayOfInterval)} to ${setup.weekDayIntToString(lastDayOfInterval)}`);
				}else if(days.isConsecutive()){
					const firstDayOfInterval = days.first();
					const lastDayOfInterval = days.last();
					result.push(`from ${setup.weekDayIntToString(firstDayOfInterval)} to ${setup.weekDayIntToString(lastDayOfInterval)}`);
				}else{
					let daysString = 'on ';
					for(const day of days){
						if(day == days.last())
							daysString += ` and ${setup.weekDayIntToString(day)}s`;
						else if(day == days.first())
							daysString += `${setup.weekDayIntToString(day)}s`;
						else
							daysString += `, ${setup.weekDayIntToString(day)}s`;
					}
					result.push(daysString);
				}
			}else if(days.length == 1){
				result.push(`on ${setup.weekDayIntToString(days[0])}s`);
			}else{
				let daysString = 'on ';
				for(const day of days){
					if(day == days.last())
						daysString += ` and ${setup.weekDayIntToString(day)}s`;
					else if(day == days.first())
						daysString += `${setup.weekDayIntToString(day)}s`;
					else
						daysString += `, ${setup.weekDayIntToString(day)}s`;
				}
				result.push(daysString);
			}


			if(ti.begin || ti.end){
				const start = ti.begin ?? [0,0];
				const end = ti.end ?? [0,0];
				let timeResult = '';
				timeResult += 'from '+start[0].toString().padStart(2,"0") +':'+start[1].toString().padStart(2,"0");
				timeResult += ' to ' +end[0].toString().padStart(2,"0")+':'+end[1].toString().padStart(2,"0");
				result.push(timeResult);
			}



			return result.join(', ');
		}

	//#endregion

	//#endregion

	//#region ExpiringVar
		isExpired<T>(v:ExpiringVar<T>,dateTime:Date=undefined):boolean{
			if(!v)
				return true;
			dateTime ??= this.now;
			return (v.til.getTime() <= dateTime.getTime());
		}
	//#endregion

	//#region System

		_init(gameTime: { [x: string]: any; }){
			Object.keys(gameTime).forEach(function (pn) {
				this[pn] = clone(gameTime[pn]);
			}, this);

			return this;
		}

		clone = function () {
			return (new setup.GameTime())._init(this);
		};

		toJSON = function () {
			var ownData = {};
			Object.keys(this).forEach(function (pn) {
				if(typeof this[pn] !== "function")
					ownData[pn] = clone(this[pn]);
			}, this);
			return JSON.reviveWrapper('(new setup.GameTime())._init($ReviveData$)', ownData);
		};

	
}

setup.GameTime = GameTime;

setup.durationToString = function(duration: number){
	return Math.floor(duration/60).toString()+':'+(duration%60).toString().padStart(2, '0');
}

setup.weekDayIntToString ??= function(day:number){
	switch(day){
		case 1: return 'Monday';
		case 2: return 'Tuesday';
		case 3: return 'Wednesday';
		case 4: return 'Thursday';
		case 5: return 'Friday';
		case 6: return 'Saturday';
		case 7: return 'Sunday';
		default: return '???';}
}
