//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
//Copyright (C) 2024 HTWKalender support@htwkalender.de

//This program is free software: you can redistribute it and/or modify
//it under the terms of the GNU Affero General Public License as published by
//the Free Software Foundation, either version 3 of the License, or
//(at your option) any later version.

//This program is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//GNU Affero General Public License for more details.

//You should have received a copy of the GNU Affero General Public License
//along with this program.  If not, see <https://www.gnu.org/licenses/>.

package ical

import (
	ics "github.com/arran4/golang-ical"
	"htwkalender/ical/model"
	"htwkalender/ical/service/functions"
	clock "htwkalender/ical/service/functions/time"
	"htwkalender/ical/service/names"
	"time"

	_ "time/tzdata"
)

func GenerateIcalFeed(events model.Events, mapping map[string]model.FeedCollection, userAgent string) *ics.Calendar {

	cal := ics.NewCalendarFor("HTWK Kalender")
	setDefaultIcalParams(cal)

	europeTime, _ := time.LoadLocation("Europe/Berlin")
	internalClock := clock.RealClock{}

	for _, event := range events {
		mapEntry, mappingFound := mapping[event.UUID]

		var eventHash = functions.HashString(time.Time(event.Start).String() + time.Time(event.End).String() + event.Course + event.Name + event.Rooms)

		icalEvent := ics.NewEvent(eventHash + "@htwkalender.de")

		icalEvent.SetDtStampTime(internalClock.Now().Local().In(europeTime))
		icalEvent.SetStartAt(time.Time(event.Start).Local().In(europeTime))
		icalEvent.SetEndAt(time.Time(event.End).Local().In(europeTime))

		if mappingFound {
			addPropertyIfNotEmpty(icalEvent, ics.ComponentPropertySummary, replaceNameIfUserDefined(&event, mapEntry))
			addAlarmIfSpecified(icalEvent, event, mapEntry, internalClock)
		} else {
			addPropertyIfNotEmpty(icalEvent, ics.ComponentPropertySummary, event.Name)
		}

		addUserAgentSpecificDescription(icalEvent, event, userAgent)
		addPropertyIfNotEmpty(icalEvent, ics.ComponentPropertyLocation, event.Rooms)

		cal.AddVEvent(icalEvent)

	}
	return cal
}

func setDefaultIcalParams(cal *ics.Calendar) {
	cal.SetMethod(ics.MethodPublish)
	cal.SetProductId("-//HTWK Kalender//htwkalender.de//DE")
	cal.SetTzid("Europe/Berlin")
	cal.SetXWRCalName("HTWK Kalender")
	cal.SetXWRTimezone("Europe/Berlin")
	cal.SetVersion("2.0")
	cal.SetCalscale("GREGORIAN")

	vTimeZone := ics.NewTimezone("Europe/Berlin")

	vTimeZone.Components = []ics.Component{
		&ics.Daylight{
			ComponentBase: ics.ComponentBase{
				Properties: []ics.IANAProperty{
					{
						BaseProperty: ics.BaseProperty{
							IANAToken: string(ics.PropertyDtstart),
							Value:     "19700329T020000",
						},
					},
					{
						BaseProperty: ics.BaseProperty{
							IANAToken: string(ics.PropertyRrule),
							Value:     "FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU",
						},
					},
					{
						BaseProperty: ics.BaseProperty{
							IANAToken: string(ics.PropertyTzname),
							Value:     "CEST",
						},
					},
					{
						BaseProperty: ics.BaseProperty{
							IANAToken: string(ics.PropertyTzoffsetfrom),
							Value:     "+0100",
						},
					},
					{
						BaseProperty: ics.BaseProperty{
							IANAToken: string(ics.PropertyTzoffsetto),
							Value:     "+0200",
						},
					},
				},
			},
		},
		&ics.Standard{
			ComponentBase: ics.ComponentBase{
				Properties: []ics.IANAProperty{
					{
						BaseProperty: ics.BaseProperty{
							IANAToken: string(ics.PropertyDtstart),
							Value:     "19701025T030000",
						},
					},
					{
						BaseProperty: ics.BaseProperty{
							IANAToken: string(ics.PropertyRrule),
							Value:     "FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU",
						},
					},
					{
						BaseProperty: ics.BaseProperty{
							IANAToken: string(ics.PropertyTzname),
							Value:     "CET",
						},
					},
					{
						BaseProperty: ics.BaseProperty{
							IANAToken: string(ics.PropertyTzoffsetfrom),
							Value:     "+0200",
						},
					},
					{
						BaseProperty: ics.BaseProperty{
							IANAToken: string(ics.PropertyTzoffsetto),
							Value:     "+0100",
						},
					},
				},
			},
		},
	}

	cal.AddVTimezone(vTimeZone)
}

// AddPropertyIfNotEmpty adds a property to the component if the value is not empty
// or contains only whitespaces
func addPropertyIfNotEmpty(component *ics.VEvent, key ics.ComponentProperty, value string) {
	if !functions.OnlyWhitespace(value) {
		component.AddProperty(key, value)
	}
}

// if reminder is specified in the configuration for this event, an alarm will be added to the event
func addAlarmIfSpecified(icalEvent *ics.VEvent, event model.Event, mapping model.FeedCollection, clock clock.Clock) {
	// if event.Start > now
	// then add alarm
	if time.Time(event.Start).Local().After(clock.Now().Local()) && mapping.Reminder {
		alarm := icalEvent.AddAlarm()
		alarm.AddProperty(ics.ComponentPropertyTrigger, "-P0DT0H15M0S")
		alarm.AddProperty(ics.ComponentPropertyAction, "DISPLAY")
		alarm.AddProperty(ics.ComponentPropertyDescription, "Next course: "+replaceNameIfUserDefined(&event, mapping)+" in "+event.Rooms)
	}
}

// replaceNameIfUserDefined replaces the name of the event with the user defined name if it is not empty
// all contained template strings will be replaced with the corresponding values from the event
func replaceNameIfUserDefined(event *model.Event, mapping model.FeedCollection) string {
	if !functions.OnlyWhitespace(mapping.UserDefinedName) {
		return names.ReplaceTemplateSubStrings(mapping.UserDefinedName, *event)
	}
	return event.Name
}
