/++

	OpenD could use automatic mixin to child class...

	Extensions: color. exrule? trash day - if holiday occurred that week, move it forward a day

	Standards: categories

	UI idea for rrule: show a mini two year block with the day highlighted
		-> also just let user click on a bunch of days so they can make a list

	Want ability to add special info to a single item of a recurring event

	Can use inotify to reload ui when sqlite db changes (or a trigger on postgres?)

	https://datatracker.ietf.org/doc/html/rfc5545
	https://icalendar.org/
+/
module arsd.calendar;

import arsd.core;

import std.datetime;

/++
	History:
		Added July 3, 2024
+/
SimplifiedUtcTimestamp parseTimestampString(string when, SysTime relativeTo) /*pure*/ {
	import std.string;

	int parsingWhat;
	int bufferedNumber = int.max;

	int secondsCount;

	void addSeconds(string word, int bufferedNumber, int multiplier) {
		if(parsingWhat == 0)
			parsingWhat = 1;
		if(parsingWhat != 1)
			throw ArsdException!"unusable timestamp string"("you said 'at' but gave a relative time", when);
		if(bufferedNumber == int.max)
			throw ArsdException!"unusable timestamp string"("no number before unit", when, word);
		secondsCount += bufferedNumber * multiplier;
		bufferedNumber = int.max;
	}

	foreach(word; when.split(" ")) {
		word = strip(word).toLower().replace(",", "");
		if(word == "in")
			parsingWhat = 1;
		else if(word == "at")
			parsingWhat = 2;
		else if(word == "and") {
			// intentionally blank
		} else if(word.indexOf(":") != -1) {
			if(secondsCount != 0)
				throw ArsdException!"unusable timestamp string"("cannot mix time styles", when, word);

			if(parsingWhat == 0)
				parsingWhat = 2; // assume absolute time when this comes in

			bool wasPm;

			if(word.length > 2 && word[$-2 .. $] == "pm") {
				word = word[0 .. $-2];
				wasPm = true;
			} else if(word.length > 2 && word[$-2 .. $] == "am") {
				word = word[0 .. $-2];
			}

			// FIXME: what about midnight?
			int multiplier = 3600;
			foreach(part; word.split(":")) {
				import std.conv;
				secondsCount += multiplier * to!int(part);
				multiplier /= 60;
			}

			if(wasPm)
				secondsCount += 12 * 3600;
		} else if(word.isNumeric()) {
			import std.conv;
			bufferedNumber = to!int(word);
		} else if(word == "seconds" || word == "second") {
			addSeconds(word, bufferedNumber, 1);
		} else if(word == "minutes" || word == "minute") {
			addSeconds(word, bufferedNumber, 60);
		} else if(word == "hours" || word == "hour") {
			addSeconds(word, bufferedNumber, 60 * 60);
		} else
			throw ArsdException!"unusable timestamp string"("i dont know what this word means", when, word);
	}

	if(parsingWhat == 0)
		throw ArsdException!"unusable timestamp string"("couldn't figure out what to do with this input", when);

	else if(parsingWhat == 1) // relative time
		return SimplifiedUtcTimestamp((relativeTo + seconds(secondsCount)).stdTime);
	else if(parsingWhat == 2) { // absolute time (assuming it is today in our time zone)
		auto today = relativeTo;
		today.hour = 0;
		today.minute = 0;
		today.second = 0;
		return SimplifiedUtcTimestamp((today + seconds(secondsCount)).stdTime);
	} else
		assert(0);
}

unittest {
	auto testTime = SysTime(DateTime(Date(2024, 07, 03), TimeOfDay(10, 0, 0)), UTC());
	void test(string what, string expected) {
		auto result = parseTimestampString(what, testTime).toString;
		assert(result == expected, result);
	}

	test("in 5 minutes", "2024-07-03T10:05:00Z");
	test("in 5 minutes and 5 seconds", "2024-07-03T10:05:05Z");
	test("in 5 minutes, 45 seconds", "2024-07-03T10:05:45Z");
	test("at 5:44", "2024-07-03T05:44:00Z");
	test("at 5:44pm", "2024-07-03T17:44:00Z");
}

version(none)
void main() {
	auto e = new CalendarEvent(
		start: DateTime(2024, 4, 22),
		end: Date(2024, 04, 22),
	);
}

class Calendar {
	CalendarEvent[] events;
}

/++

+/
class CalendarEvent {
	DateWithOptionalTime start;
	DateWithOptionalTime end;

	Recurrence recurrence;

	int color;
	string title; // summary
	string details;

	string uid;

	this(DateWithOptionalTime start, DateWithOptionalTime end, Recurrence recurrence = Recurrence.none) {
		this.start = start;
		this.end = end;
		this.recurrence = recurrence;
	}
}

/++

+/
struct DateWithOptionalTime {
	string tzlocation;
	DateTime dt;
	bool hadTime;

	@implicit
	this(DateTime dt) {
		this.dt = dt;
		this.hadTime = true;
	}

	@implicit
	this(Date d) {
		this.dt = DateTime(d, TimeOfDay.init);
		this.hadTime = false;
	}

	this(in char[] s) {
		// FIXME
	}
}

/++

+/
struct Recurrence {
	static Recurrence none() {
		return Recurrence.init;
	}
}

/+

enum FREQ {

}

struct RRULE {
	FREQ freq;
	int interval;
	int count;
	DAY wkst;

	// these can be negative too indicating the xth from the last...
	DAYSET byday; // ubyte bitmask... except it can also have numbers atached wtf

	// so like `BYDAY=-2MO` means second-to-last monday

	MONTHDAYSET byMonthDay; // uint bitmask
	HOURSET byHour; // uint bitmask
	MONTHDSET byMonth; // ushort bitmask

	WEEKSET byWeekNo; // ulong bitmask

	int BYSETPOS;
}

+/

struct ICalParser {
	// if the following line starts with whitespace, remove the cr/lf/ and that ONE ws char, then add to the previous line
	// it is supposed to support this even if it is in the middle of a utf-8 sequence
	//      contentline   = name *(";" param ) ":" value CRLF
	// you're supposed to split lines longer than 75 octets when generating.

	void feedEntireFile(in ubyte[] data) {
		feed(data);
		feed(null);
	}
	void feedEntireFile(in char[] data) {
		feed(data);
		feed(null);
	}

	/++
		Feed it some data you have ready.

		Feed it an empty array or `null` to indicate end of input.
	+/
	void feed(in char[] data) {
		feed(cast(const(ubyte)[]) data);
	}

	/// ditto
	void feed(in ubyte[] data) {
		const(ubyte)[] toProcess;
		if(unprocessedData.length) {
			unprocessedData ~= data;
			toProcess = unprocessedData;
		} else {
			toProcess = data;
		}

		auto eol = toProcess.indexOf("\n");
		if(eol == -1) {
			unprocessedData = cast(ubyte[]) toProcess;
		} else {
			// if it is \r\n, remove the \r FIXME
			// if it is \r\n<space>, need to concat
			// if it is \r\n\t, also need to concat
			processLine(toProcess[0 .. eol]);
		}
	}

	/// ditto
	void feed(typeof(null)) {
		feed(cast(const(ubyte)[]) null);
	}

	private ubyte[] unprocessedData;

	private void processLine(in ubyte[] line) {

	}
}

immutable monthNames = [
	"",
	"January",
	"February",
	"March",
	"April",
	"May",
	"June",
	"July",
	"August",
	"September",
	"October",
	"November",
	"December"
];