//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 v2

import (
	"github.com/pocketbase/pocketbase/tools/types"
	"golang.org/x/net/html"
	"htwkalender/data-manager/model"
	"htwkalender/data-manager/service/date"
	"regexp"
	"strconv"
	"strings"
	"time"
)

// Find the first <table> element in the HTML document
func findFirstTable(node *html.Node) *html.Node {
	if node.Type == html.ElementNode && node.Data == "table" {
		return node
	}
	// Traverse child nodes recursively
	for child := node.FirstChild; child != nil; child = child.NextSibling {
		found := findFirstTable(child)
		if found != nil {
			return found
		}
	}
	return nil
}

// Find the first <span> element with the specified class attribute value
func findFirstSpanWithClass(node *html.Node, classValue string) *html.Node {

	// Check if the current node is a <span> element with the specified class attribute value
	if node.Type == html.ElementNode && node.Data == "span" {
		if hasClassAttribute(node, classValue) {
			return node
		}
	}

	// Traverse child nodes recursively
	for child := node.FirstChild; child != nil; child = child.NextSibling {
		found := findFirstSpanWithClass(child, classValue)
		if found != nil {
			return found
		}
	}
	return nil
}

// Check if the specified element has the specified class attribute value
func hasClassAttribute(node *html.Node, classValue string) bool {
	for _, attr := range node.Attr {
		if attr.Key == "class" && strings.Contains(attr.Val, classValue) {
			return true
		}
	}
	return false
}

// Get Tables with days
func getEventTables(node *html.Node) [][]*html.Node {
	var eventTables [][]*html.Node
	tables := findTables(node)
	// get all tables with events
	for events := range tables {
		rows := findTableRows(tables[events])
		// check that a first row exists
		if len(rows) > 0 {
			rows = rows[1:]
			eventTables = append(eventTables, rows)
		}
	}
	return eventTables
}

// Get Tables with days
func getAllDayLabels(node *html.Node) []string {
	paragraphs := findParagraphs(node)
	var dayArray []string

	for _, p := range paragraphs {
		label := getDayLabel(p)
		if label != "" {
			dayArray = append(dayArray, label)
		}
	}
	return dayArray
}

// Find all <p> elements in the HTML document
func findParagraphs(node *html.Node) []*html.Node {
	var paragraphs []*html.Node

	if node.Type == html.ElementNode && node.Data == "p" {
		paragraphs = append(paragraphs, node)
	}

	for child := node.FirstChild; child != nil; child = child.NextSibling {
		paragraphs = append(paragraphs, findParagraphs(child)...)
	}

	return paragraphs
}

// Find all <tr> elements in <tbody>, excluding the first one
func findTableRows(node *html.Node) []*html.Node {
	var tableRows []*html.Node

	if node.Type == html.ElementNode && node.Data == "tbody" {
		child := node.FirstChild
		for child != nil {
			if child.Type == html.ElementNode && child.Data == "tr" {
				tableRows = append(tableRows, child)
			}
			child = child.NextSibling
		}
	}

	// Traverse child nodes recursively
	for child := node.FirstChild; child != nil; child = child.NextSibling {
		var tableRowElement = findTableRows(child)
		if tableRowElement != nil {
			tableRows = append(tableRows, tableRowElement...)
		}
	}

	// check if tableRows is nil
	if tableRows == nil {
		return []*html.Node{}
	} else {
		return tableRows
	}
}

// Find all <p> elements in the HTML document
func findTables(node *html.Node) []*html.Node {
	var tables []*html.Node

	if node.Type == html.ElementNode && node.Data == "table" {
		tables = append(tables, node)
	}

	for child := node.FirstChild; child != nil; child = child.NextSibling {
		tables = append(tables, findDayTables(child)...)
	}

	return tables
}

// Find all <p> elements in the HTML document
func findDayTables(node *html.Node) []*html.Node {
	var tables []*html.Node

	for child := node.FirstChild; child != nil; child = child.NextSibling {
		tables = append(tables, findDayTables(child)...)
	}

	if node.Type == html.ElementNode && node.Data == "table" && hasClassAttribute(node, "spreadsheet") {
		tables = append(tables, node)
	}

	return tables
}

// Get the text content of the specified node and its descendants
func getDayLabel(node *html.Node) string {

	child := node.FirstChild
	if child != nil {
		if child.Type == html.ElementNode && child.Data == "span" {
			if child.FirstChild != nil {
				return child.FirstChild.Data
			}
		}
	}
	return ""
}

// Find all <td> elements in the current <tr>
func findTableData(node *html.Node) []*html.Node {
	var tableData []*html.Node

	if node.Type == html.ElementNode && node.Data == "tr" {
		child := node.FirstChild
		for child != nil {
			if child.Type == html.ElementNode && child.Data == "td" {
				tableData = append(tableData, child)
			}
			child = child.NextSibling
		}
	}

	return tableData
}

// Get the text content of the specified node and its descendants
func getTextContent(node *html.Node) string {
	var textContent string

	if node.Type == html.TextNode {
		textContent = node.Data
	}

	for child := node.FirstChild; child != nil; child = child.NextSibling {
		textContent += getTextContent(child)
	}

	return textContent
}

func splitEventsByWeek(events []model.Event) []model.Event {
	var newEvents []model.Event

	for _, event := range events {
		weeks := strings.Split(event.Week, ",")
		for _, week := range weeks {
			newEvent := event
			newEvent.Week = strings.TrimSpace(week)
			newEvents = append(newEvents, newEvent)
		}
	}
	return newEvents
}

func splitEventsBySingleWeek(events []model.Event) []model.Event {
	var newEvents []model.Event

	for _, event := range events {
		if strings.Contains(event.Week, "-") {
			weeks := splitWeekRange(event.Week)
			for _, week := range weeks {
				newEvent := event
				newEvent.Week = week
				newEvents = append(newEvents, newEvent)
			}
		} else {
			newEvents = append(newEvents, event)
		}
	}
	return newEvents
}

func splitWeekRange(weekRange string) []string {
	parts := strings.Split(weekRange, "-")
	if len(parts) != 2 {
		return nil // Invalid format
	}

	start, errStart := strconv.Atoi(strings.TrimSpace(parts[0]))
	end, errEnd := strconv.Atoi(strings.TrimSpace(parts[1]))

	if errStart != nil || errEnd != nil {
		return nil // Error converting to integers
	}

	var weeks []string
	for i := start; i <= end; i++ {
		weeks = append(weeks, strconv.Itoa(i))
	}

	return weeks
}

func extractSemesterAndYear(semesterString string) (string, string) {
	winterPattern := "Wintersemester"
	summerPattern := "Sommersemester"

	winterMatch := strings.Contains(semesterString, winterPattern)
	summerMatch := strings.Contains(semesterString, summerPattern)

	semester := ""
	semesterShortcut := ""

	if winterMatch {
		semester = "Wintersemester"
		semesterShortcut = "ws"
	} else if summerMatch {
		semester = "Sommersemester"
		semesterShortcut = "ss"
	} else {
		return "", ""
	}

	yearPattern := `\d{4}`
	combinedPattern := semester + `\s` + yearPattern
	re := regexp.MustCompile(combinedPattern)
	match := re.FindString(semesterString)
	year := ""

	if match != "" {
		reYear := regexp.MustCompile(yearPattern)
		year = reYear.FindString(match)
	}
	return semesterShortcut, year
}

func convertWeeksToDates(events []model.Event, semester string, year string) []model.Event {
	var newEvents []model.Event
	eventYear, _ := strconv.Atoi(year)

	// for each event we need to calculate the start and end date based on the week and the year
	for _, event := range events {
		eventWeek, _ := strconv.Atoi(event.Week)
		eventDay, _ := date.GetDateFromWeekNumber(eventYear, eventWeek, event.Day)
		start := replaceTimeForDate(eventDay, event.Start.Time())
		end := replaceTimeForDate(eventDay, event.End.Time())

		//Check if end is before start
		if end.Before(start) {
			end = end.AddDate(0, 0, 1)
		}

		newEvent := event
		newEvent.Start, _ = types.ParseDateTime(start.In(time.UTC))
		newEvent.End, _ = types.ParseDateTime(end.In(time.UTC))
		newEvent.Semester = semester
		newEvents = append(newEvents, newEvent)
	}
	return newEvents
}

// replaceTimeForDate replaces hour, minute, second, nsec for the selected date
func replaceTimeForDate(date time.Time, replacementTime time.Time) time.Time {
	return time.Date(date.Year(), date.Month(), date.Day(), replacementTime.Hour(), replacementTime.Minute(), replacementTime.Second(), replacementTime.Nanosecond(), date.Location())
}
