/*
This file is part of »dali«.

Copyright 2025 'kcf' <fenris@folksprak.org>

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

»dali« 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 Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
 */


namespace _dali.widgets
{
	
	/**
	 */
	type type_get_entries = (
		(
			from_pit : lib_plankton.pit.type_pit,
			to_pit : lib_plankton.pit.type_pit,
			calendar_ids : Array<_dali.type_calendar_id>
		)
		=>
		Promise<Array<_dali.type_event_object_extended>>
	);
	
	
	/**
	 */
	export class class_widget_weekview
		implements lib_plankton.zoo_widget.interface_widget
	{
		
		/**
		 * [dependency]
		 */
		private get_entries : type_get_entries;
		
		
		/**
		 * [hook]
		 */
		private action_select_event : (
			(
				event_key : _dali.type_event_key
			)
			=>
			void
		);
		
		
		/**
		 * [hook]
		 */
		private action_select_day : (
			(
				date : lib_plankton.pit.type_date
			)
			=>
			void
		);
		
		
		/**
		 * [setting]
		 */
		private show_begin_time : boolean;
		
		
		/**
		 * [state]
		 */
		private vertical : boolean;
		
		
		/**
		 * [state]
		 */
		private year : int;
		
		
		/**
		 * [state]
		 */
		private week : int;
		
		
		/**
		 * [state]
		 */
		private count : int;
		
		
		/**
		 * [state]
		 */
		private event_map : lib_plankton.map.type_map<
			_dali.type_event_key,
			{
				element : HTMLElement;
				hash : string;
			}
		>;
		
		
		/**
		 * [state]
		 */
		private container : (null | Element);
		
		
		/**
		 */
		public constructor(
			get_entries : type_get_entries,
			{
				"action_select_day": action_select_day = ((date) => {}),
				"action_select_event": action_select_event = ((event_key) => {}),
				"show_begin_time": show_begin_time = true,
				"vertical": vertical = false,
				"initial_year": initial_year = null,
				"initial_week": initial_week = null,
				"initial_count": initial_count = 5,
			}
			:
			{
				action_select_event ?: (
					(
						event_key : _dali.type_event_key
					)
					=>
					void
				);
				action_select_day ?: (
					(
						date : lib_plankton.pit.type_date
					)
					=>
					void
				);
				show_begin_time ?: boolean;
				vertical ?: boolean;
				initial_year ?: (null | int);
				initial_week ?: (null | int);
				initial_count ?: int;
			}
			=
			{}
		)
		{
			// dependencies
			this.get_entries = get_entries;
			
			// hooks
			this.action_select_day = action_select_day;
			this.action_select_event = action_select_event;
			
			// settings
			this.show_begin_time = show_begin_time;
			
			// state
			// const ywd_now : lib_plankton.pit.type_ywd = lib_plankton.pit.to_ywd(lib_plankton.pit.now());
			const ywd_begin : lib_plankton.pit.type_ywd = lib_plankton.call.convey(
				lib_plankton.pit.now(),
				[
					x => lib_plankton.pit.shift_week(x, -1),
					lib_plankton.pit.to_ywd,
				]
			);
			
			this.vertical = vertical;
			this.year = (
				initial_year
				??
				ywd_begin.year
			);
			this.week = (
				initial_week
				??
				ywd_begin.week
			);
			this.count = initial_count;
			this.event_map = lib_plankton.map.hashmap.implementation_map(
				lib_plankton.map.hashmap.make<
					_dali.type_event_key,
					{
						element : HTMLElement;
						hash : string;
					}
				>(
					event_key => event_key
				)
			);
			this.container = null;
		}
		
		
		/**
		 * some kind of checksum for comparing entries
		 * @todo base64 encode?
		 * @todo sha256 hash?
		 */
		private static entry_hash(
			entry : _dali.type_event_object_extended
		)
		: string
		{
			return lib_plankton.call.convey(
				{
					"calendar_id": entry.calendar_id,
					"calendar_name": entry.calendar_name,
					"hue": Math.floor(entry.hue * 0xFFFF),
					"access_level": entry.access_level,
					"event_object": entry.event_object,
				},
				[
					x => lib_plankton.json.encode(x),
				]
			);
		}
		
		
		/**
		 */
		private static event_generate_tooltip(
			calendar_name : string,
			event_object : _dali.type_event_object
		)
		: string
		{
			return (
				lib_plankton.string.coin(
					"[{{calendar_name}}] {{event_name}}\n",
					{
						"calendar_name": calendar_name,
						"event_name": event_object.name,
					}
				)
				+
				"--\n"
				+
				(
					(event_object.begin.time !== null)
					?
					lib_plankton.string.coin(
						"{{label}}: {{value}}\n",
						{
							"label": lib_plankton.translate.get("event.when"),
							"value": lib_plankton.pit.timespan_format(
								event_object.begin,
								event_object.end,
								{
									"timezone_indicator": lib_plankton.translate.get("common.timezone_indicator"),
									"adjust_to_ce": true,
									"show_timezone": true,
									"omit_date": true,
								}
							),
						}
					)
					:
					""
				)
				+
				(
					(event_object.location !== null)
					?
					(
						lib_plankton.string.coin(
							"{{label}}: {{value}}\n",
							{
								"label": lib_plankton.translate.get("event.location"),
								"value": event_object.location,
							}
						)
					)
					:
					""
				)
				+
				(
					(event_object.link !== null)
					?
					(
						lib_plankton.string.coin(
							"{{label}}: {{value}}\n",
							{
								"label": lib_plankton.translate.get("event.link"),
								"value": event_object.link,
							}
						)
					)
					:
					""
				)
				/*
				+
				(
					(event_object.description !== null)
					?
					(
						"--\n"
						+
						lib_plankton.string.coin(
							"{{description}}\n",
							{
								"description": event_object.description,
							}
						)
					)
					:
					""
				)
				 */
			);
		}
		
		
		/**
		 */
		private async get_entries_wrapped(
			{
				"calendar_ids": calendar_ids = null,
				"timezone_shift": timezone_shift = 0,
			}
			:
			{
				calendar_ids ?: (null | Array<_dali.type_calendar_id>);
				timezone_shift ?: int;
			}
			=
			{
			}
		)
		: Promise<Array<_dali.type_event_object_extended>>
		{
			const entries = await this.get_entries(
				lib_plankton.pit.from_ywd(
					{
						"year": this.year,
						"week": this.week,
						"day": 1,
					},
					{
						"timezone_shift": timezone_shift,
					}
				),
				lib_plankton.pit.from_ywd(
					{
						"year": this.year,
						"week": (this.week + this.count),
						"day": 1,
					},
					{
						"timezone_shift": timezone_shift,
					}
				),
				calendar_ids
			);
			entries.sort(
				(entry1, entry2) => {
					const b1 : lib_plankton.pit.type_pit = lib_plankton.pit.from_datetime(entry1.event_object.begin);
					const b2 : lib_plankton.pit.type_pit = lib_plankton.pit.from_datetime(entry2.event_object.begin);
					return ((b1 <= b2) ? -1 : +1);
				}
			);
			return entries;
		}
		
		
		/**
		 */
		private async entry_insert(
			entry : _dali.type_event_object_extended
		)
		: Promise<(null | HTMLElement)>
		{
			const selector : string = lib_plankton.string.coin(
				".weekview-cell[rel=\"{{rel}}\"] > .weekview-events",
				{
					"rel": lib_plankton.pit.date_format(entry.event_object.begin.date),
				}
			);
			const dom_cell = this.container.querySelector(selector);
			if (dom_cell === null)
			{
				lib_plankton.log.debug(
					"dali.widget.weekview.entry_insert.out_of_scope",
					{
						"entry": entry,
					}
				);
				return null;
			}
			else
			{
				const dom_entry = await _dali.helpers.element_from_template(
					"widget-weekview",
					"tableview-cell-entry",
					{
						"color": _dali.helpers.event_color(entry.hue),
						"content": lib_plankton.string.coin(
							(
								(
									this.show_begin_time
									&&
									(entry.event_object.begin.time === null)
								)
								?
								"{{name}}"
								:
								"{{begin}} | {{name}}"
							),
							{
								"begin": (
									(entry.event_object.begin.time === null)
									?
									null
									:
									lib_plankton.pit.time_format(
										entry.event_object.begin.time,
										{"show_seconds": false}
									)
								),
								"name": entry.event_object.name,
							}
						),
						"rel": entry.key,
						"additional_classes": lib_plankton.string.coin(
							" access_level-{{access_level}}",
							{
								"access_level": _dali.access_level_encode(entry.access_level),
							}
						),
					}
				);
				
				// listener
				dom_entry.addEventListener(
					"click",
					(event) => {
						const rel : string = dom_entry.getAttribute("rel");
						const event_key : _dali.type_event_key = rel;
						this.action_select_event(
							event_key
						);
					}
				);
				
				// emplace
				dom_cell.appendChild(dom_entry);
				
				return dom_entry;
			}
		}
		
		
		/**
		 */
		private async entry_add(
			entry : _dali.type_event_object_extended
		)
		: Promise<void>
		{
			const dom_entry : (null | HTMLElement) = await this.entry_insert(entry);
			if (dom_entry === null)
			{
				// do nothing
			}
			else
			{
				this.event_map.set(
					entry.key,
					{
						"element": dom_entry,
						"hash": class_widget_weekview.entry_hash(entry),
					}
				);
			}
		}
		
		
		/**
		 */
		private async entry_update(
			key : _dali.type_event_key,
			entry : _dali.type_event_object_extended
		)
		: Promise<void>
		{
			if (! this.event_map.has(key))
			{
				lib_plankton.log.warning(
					"dali.widget.weekview.entry_update.event_missing",
					{
						"key": key,
					}
				);
			}
			else
			{
				const value = this.event_map.get(key);
				const hash_old : string = value.hash;
				const hash_new : string = class_widget_weekview.entry_hash(entry);
				if (hash_old === hash_new)
				{
					// do nothing
					lib_plankton.log.debug(
						"dali.widget.weekview.entry_update.nothing_to_update",
						{
							"key": key,
							"entry": entry,
							"element": value.element,
						}
					);
				}
				else
				{
					const dom_entry_old : HTMLElement = value.element;
					dom_entry_old.remove();
					const dom_entry_new : (null | HTMLElement) = await this.entry_insert(entry);
					if (dom_entry_new === null)
					{
						// do nothing
					}
					else
					{
						this.event_map.set(
							entry.key,
							{
								"element": dom_entry_new,
								"hash": hash_new,
							}
						);
					}
				}
			}
		}
		
		
		/**
		 */
		private async entry_remove(
			key : _dali.type_event_key
		)
		: Promise<void>
		{
			if (! this.event_map.has(key))
			{
				// do nothing
				lib_plankton.log.warning(
					"dali.widget.weekview.entry_remove.not_in_map",
					{
						"key": key,
						"pairs": lib_plankton.map.dump(this.event_map),
					}
				);
			}
			else
			{
				const value = this.event_map.get(
					key
				);
				this.event_map.delete(
					key
				);
				value.element.remove();
			}
		}
		

		/**
		 */
		public async update_entries(
		)
		: Promise<void>
		{
			const entries : Array<_dali.type_event_object_extended> = await this.get_entries_wrapped(
			);
			
			const contrast = lib_plankton.list.contrast<
				any,
				_dali.type_event_object_extended
			>(
				lib_plankton.map.dump(this.event_map),
				pair => pair.key,
				entries,
				event => event.key
			);
			await Promise.all(
				[]
				// remove
				.concat(
					contrast.only_left.map(
						({"key": key, "left": left}) => this.entry_remove(key)
					)
				)
				// update
				.concat(
					contrast.both.map(
						({"key": key, "left": left, "right": right}) => this.entry_update(key, right)
					)
				)
				// add
				.concat(
					contrast.only_right.map(
						({"key": key, "right": right}) => this.entry_add(right)
					)
				)
			);
		}
		
		
		/**
		 */
		private async update_table(
		)
		: Promise<void>
		{
			/**
			 * @todo avoid?
			 */
			lib_plankton.map.clear(this.event_map);
			const context : Element = this.container;
			// structure
			{
				type type_row_data = Array<
					{
						head : int;
						pits : Array<lib_plankton.pit.type_pit>;
					}
				>;
				
				/**
				 * @todo als Variable?
				 */
				const timezone_shift : int = 0;
				const now_pit : lib_plankton.pit.type_pit = lib_plankton.pit.now();
				const today_begin_pit : lib_plankton.pit.type_pit = lib_plankton.pit.trunc_day(now_pit);
				const today_end_pit : lib_plankton.pit.type_pit = lib_plankton.pit.shift_day(today_begin_pit, 1);
				const day_names : Array<string> = [
					lib_plankton.translate.get("common.weekday.monday"),
					lib_plankton.translate.get("common.weekday.tuesday"),
					lib_plankton.translate.get("common.weekday.wednesday"),
					lib_plankton.translate.get("common.weekday.thursday"),
					lib_plankton.translate.get("common.weekday.friday"),
					lib_plankton.translate.get("common.weekday.saturday"),
					lib_plankton.translate.get("common.weekday.sunday"),
				];
				const row_data_horizontal : type_row_data = (
					lib_plankton.list.sequence(this.count)
					.map(
						offset => {
							const week : int = (this.week + offset);
							const week_adjusted : int = lib_plankton.call.convey(
								this.week,
								[
									x => lib_plankton.pit.from_ywd({"year": this.year, "week": x, "day": 2}),
									x => lib_plankton.pit.shift_week(x, offset),
									lib_plankton.pit.to_ywd,
									x => x.week
								]
							);
							return {
								"head": week_adjusted,
								"pits": (
									lib_plankton.list.sequence(7)
									.map(
										day => lib_plankton.pit.from_ywd(
											{
												"year": this.year,
												"week": week,
												"day": (1 + day),
											},
											{
												"timezone_shift": timezone_shift,
											}
										)
									)
								),
							};
						}
					)
				);
				const row_data_vertical : type_row_data = (
					lib_plankton.list.sequence(7)
					.map(
						day_of_week => {
							return {
								"head": day_of_week,
								"pits": (
									lib_plankton.list.sequence(this.count)
									.map(
										offset => {
											const week : int = (this.week + offset);
											return lib_plankton.pit.from_ywd(
												{
													"year": this.year,
													"week": week,
													"day": (1 + day_of_week),
												},
												{
													"timezone_shift": timezone_shift,
												}
											);
										}
									)
								)
							};
						}
					)
				);
				const row_data : type_row_data = (
					(! this.vertical)
					?
					row_data_horizontal
					:
					row_data_vertical
				);
				// head
				{
					const dom_tr = document.createElement("tr");
					{
						if (! this.vertical)
						{
							// anchor
							{
								const dom_th = document.createElement("th");
								dom_th.classList.add("weekview-cell");
								dom_tr.appendChild(dom_th);
							}
							// days
							{
								day_names.forEach(
									(day_name) => {
										const dom_th = document.createElement("th");
										dom_th.classList.add("weekview-cell");
										dom_th.classList.add("weekview-cell-week");
										dom_th.textContent = day_name;
										dom_tr.appendChild(dom_th);
									}
								);
							}
						}
						else
						{
							// anchor
							{
								const dom_th = document.createElement("th");
								dom_th.classList.add("weekview-cell");
								dom_tr.appendChild(dom_th);
							}
							// days
							{
								lib_plankton.list.sequence(this.count).forEach(
									(offset) => {
										const dom_th = document.createElement("th");
										dom_th.classList.add("weekview-cell");
										dom_th.classList.add("weekview-cell-day");
										const week : int = lib_plankton.call.convey(
											this.week,
											[
												x => lib_plankton.pit.from_ywd({"year": this.year, "week": x, "day": 2}),
												x => lib_plankton.pit.shift_week(x, offset),
												lib_plankton.pit.to_ywd,
												x => x.week
											]
										);
										dom_th.textContent = week.toFixed(0).padStart(2, "0");
										dom_tr.appendChild(dom_th);
									}
								);
							}
						}
					}
					context.querySelector(".weekview-table thead").innerHTML = "";
					context.querySelector(".weekview-table thead").appendChild(dom_tr);
				}
				// body
				{
					context.querySelector(".weekview-table tbody").innerHTML = (
						await _dali.helpers.promise_row<string>(
							row_data
							.map(
								(row, index) => async () => _dali.helpers.template_coin(
									"widget-weekview",
									"tableview-row",
									{
										"week": (
											(! this.vertical)
											?
											row.head.toFixed(0).padStart(2, "0")
											:
											day_names[index]
										),
										"cells": (
											await _dali.helpers.promise_row<string>(
												row.pits
												.map(
													(day_pit) => async () => {
														const is_today : boolean = lib_plankton.pit.is_between(
															day_pit,
															today_begin_pit,
															today_end_pit
														);
														const day_as_date : lib_plankton.pit.type_date = lib_plankton.pit.to_datetime_ce(day_pit).date;
														const day_as_ywd : lib_plankton.pit.type_ywd = lib_plankton.pit.to_ywd(day_pit);
														return _dali.helpers.template_coin(
															"widget-weekview",
															"tableview-cell",
															{
																"extra_classes": (
																	[""]
																	.concat(is_today ? ["weekview-cell-today"] : [])
																	.join(" ")
																),
																"day": (
																	_dali.conf.get().misc.weekview_cell_day_format
																	.replace(new RegExp("Y", "g"), day_as_date.year.toFixed(0).padStart(4, "0"))
																	.replace(new RegExp("m", "g"), day_as_date.month.toFixed(0).padStart(2, "0"))
																	.replace(new RegExp("b", "g"), _dali.helpers.month_name(day_as_date.month))
																	.replace(new RegExp("d", "g"), day_as_date.day.toFixed(0).padStart(2, "0"))
																	.replace(new RegExp("W", "g"), day_as_ywd.week.toFixed(0).padStart(2, "0"))
																	.replace(new RegExp("w", "g"), day_as_ywd.day.toFixed(0).padStart(1, "0"))
																),
																"rel": lib_plankton.string.coin(
																	"{{year}}-{{month}}-{{day}}",
																	{
																		"year": day_as_date.year.toFixed(0).padStart(4, "0"),
																		"month": day_as_date.month.toFixed(0).padStart(2, "0"),
																		"day": day_as_date.day.toFixed(0).padStart(2, "0"),
																	}
																),
																"entries": ""
															}
														);
													}
												)
											)
										).join(""),
									}
								)
							)
						)
					).join("");
				}
			}
			// listeners
			{
				context.querySelectorAll(".weekview-cell-regular").forEach(
					(element) => {
						element.addEventListener(
							"click",
							(event) => {
								if (
									(! (element === event.target))
									&&
									(! (event.target as HTMLElement).classList.contains("weekview-day"))
								)
								{
									// do nothing
								}
								else
								{
									const rel : string = element.getAttribute("rel");
									const parts : Array<string> = rel.split("-");
									const date : lib_plankton.pit.type_date = {
										"year": parseInt(parts[0]),
										"month": parseInt(parts[1]),
										"day": parseInt(parts[2]),
									};
									this.action_select_day(date);
								}
							}
						);
					}
				);
			}
		}
		
		
		/**
		 */
		public toggle_visibility(
			calendar_id : _dali.type_calendar_id,
			{
				"mode": mode = null,
			}
			:
			{
				mode ?: (null | boolean);
			}
			=
			{
			}
		)
		: void
		{
			this.container.querySelectorAll(".weekview-event_entry").forEach(
				(element) => {
					const rel : string = element.getAttribute("rel");
					const parts : Array<string> = rel.split("/");
					const calendar_id_ : _dali.type_calendar_id = parseInt(parts[0]);
					if (! (calendar_id === calendar_id_))
					{
						// do nothing
					}
					else
					{
						element.classList.toggle(
							"weekview-cell-hidden",
							((mode !== null) ? (! mode) : undefined)
						);
					}
				}
			);
		}
		
		
		/**
		 * [implementation]
		 */
		public async load(
			target_element : Element
		)
		: Promise<void>
		{
			target_element.innerHTML = await _dali.helpers.template_coin(
				"widget-weekview",
				"main",
				{
				}
			);
			let input_year : _dali.widgets.class_widget_special_number_input;
			let input_week : _dali.widgets.class_widget_special_number_input;
			// control:year
			{
				input_year = new _dali.widgets.class_widget_special_number_input(
					{
						"label": lib_plankton.translate.get("widget.weekview.controls.year"),
						"minimum": 1900,
						"maximum": 2500,
						"initial_value": this.year,
						"action_change": async (year_raw) => {
							// model
							const number_of_weeks : int = await _dali.number_of_weeks(year_raw);
							if (this.week > number_of_weeks)
							{
								this.year = year_raw;
								this.week = number_of_weeks;
							}
							else
							{
								this.year = year_raw;
								// this.week = this.week;
							}
							
							// view
							input_week.set_value(this.week);
							await this.update_table();
							await this.update_entries();
						},
					}
				);
				await input_year.load(target_element.querySelector(".weekview-controls"));
			}
			// control:week
			{
				input_week = new _dali.widgets.class_widget_special_number_input(
					{
						"label": lib_plankton.translate.get("widget.weekview.controls.week"),
						"initial_value": this.week,
						"action_change": async (week_raw) => {
							// model
							if (week_raw < 1)
							{
								this.year = (this.year - 1);
								this.week = await _dali.number_of_weeks(this.year);
							}
							else
							{
								if (week_raw > await _dali.number_of_weeks(this.year))
								{
									this.year = (this.year + 1);
									this.week = 1;
								}
								else
								{
									// this.year = this.year;
									this.week = week_raw;
								}
							}
							
							// view
							input_year.set_value(this.year);
							input_week.set_value(this.week);
							await this.update_table();
							await this.update_entries();
						},
					}
				);
				await input_week.load(target_element.querySelector(".weekview-controls"));
			}
			// control:count
			/*
			{
				const widget : lib_plankton.zoo_widget.interface_widget = new _dali.widgets.class_widget_special_number_input(
					{
						"label": lib_plankton.translate.get("widget.weekview.controls.count"),
						"minimum": 2,
						"maximum": 6,
						"initial_value": this.count,
						"action_change": async (value) => {
							this.count = value;
							await this.update_table();
							await this.update_entries();
						},
					}
				);
				await widget.load(target_element.querySelector(".weekview-controls"));
			}
			 */
			// control:vertical
			{
				const dom_control = await _dali.helpers.element_from_template(
					"widget-weekview",
					"control-vertical",
					{
						"label": lib_plankton.translate.get("widget.weekview.controls.vertical"),
					}
				);
				const dom_input : HTMLInputElement = (dom_control.querySelector("input") as HTMLInputElement);
				dom_input.addEventListener(
					"change",
					async () => {
						event.preventDefault();
						const value : boolean = dom_input.checked;
						this.vertical = value;
						await this.update_table();
						await this.update_entries();
					}
				);
				dom_input.checked = this.vertical;
				target_element.querySelector(".weekview-controls").appendChild(dom_control);
			}
			
			this.container = target_element.querySelector(".weekview");
			
			await this.update_table();
			await this.update_entries();
			
			return Promise.resolve<void>(undefined);
		}
			
	}
	
}
