/*
 * Mirai. Copyright (C) 2024 Vyn
 * This file is licensed under version 3 of the GNU General Public License (GPL-3.0-only)
 * The license can be found in the LICENSE file or at https://www.gnu.org/licenses/gpl-3.0.txt
 */

#include "UiState.h"
#include "Utils.h"
#include "appwindow.h"
#include "mirai-core/Mirai.h"
#include "mirai-core/TaskItem.h"
#include "mirai-core/TodoMd.h"
#include "slint.h"
#include "slint_sharedvector.h"
#include "slint_string.h"
#include <algorithm>
#include <bits/chrono.h>
#include <cassert>
#include <charconv>
#include <chrono>
#include <ctime>
#include <format>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <memory>
#include <optional>
#include <ostream>
#include <ranges>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>

UiState::UiState(mirai::Mirai *miraiInstance) : miraiInstance_(miraiInstance), view_(miraiInstance)
{
	sources_ = std::make_shared<slint::VectorModel<slint::SharedString>>();
	days_ = std::make_shared<slint::VectorModel<Day>>();
	tags_ = std::make_shared<slint::VectorModel<slint::SharedString>>();

	taskWindow_->global<Backend>().set_sources(sources_);
	taskWindow_->global<Backend>().set_tags(tags_);
	eventWindow_->global<Backend>().set_sources(sources_);
	eventWindow_->global<Backend>().set_tags(tags_);

	view_.update();

	reloadSources();
	reloadTags();
	reloadTasks();

	setupCallbacks();
}

std::optional<Date> stringToDate(const std::string &dateStr)
{
	using std::operator""sv;

	auto dateSplitView =
		dateStr | std::views::split("-"sv) | std::views::transform([](auto v) -> int {
			int i = 0;
			std::from_chars(v.data(), v.data() + v.size(), i);
			return i;
		});
	std::vector<int> dateSplit{dateSplitView.begin(), dateSplitView.end()};
	if (dateSplit.size() != 3) {
		return std::nullopt;
	}

	auto year = dateSplit[0];
	auto month = dateSplit[1];
	auto day = dateSplit[2];
	return Date{.year = year, .month = month, .day = day};
}

void UiState::setupTaskWindowCallbacks()
{
	taskWindow_->on_save([&](SaveTaskData newTaskData) {
		auto source = miraiInstance_->getSourceById(newTaskData.sourceId);
		assert(source);
		auto task = source->getTaskById(newTaskData.id);
		assert(task);
		const Date &date = newTaskData.date;
		const std::string dateStr = SlintDateToStdString(date);
		auto taskData =
			mirai::TodoMdFormat::stringToTask("- [ ] " + std::string(newTaskData.title), dateStr);

		task->setText(taskData.text);
		if (task->day()->getDate() != SlintDateToMiraiDate(date)) {
			auto newDate = source->day(SlintDateToMiraiDate(date));
			newDate->createTask({
				.text = taskData.text,
				.state = task->getState(),
				.tags = taskData.tags,
			});
			auto oldDate = task->day();
			source->deleteTask(*task);
		}
		miraiInstance_->save();
		view_.update();
		reloadTasks();
		taskWindow_->hide();
	});

	taskWindow_->on_create([&](NewTaskData newTaskData) {
		const Date &date = newTaskData.date;
		const std::string dateStr = SlintDateToStdString(date);
		auto source = miraiInstance_->getSourceById(newTaskData.sourceId);
		auto task =
			mirai::TodoMdFormat::stringToTask("- [ ] " + std::string(newTaskData.title), dateStr);

		if (newTaskData.eventId == -1) {
			auto day = source->day(SlintDateToMiraiDate(date));
			assert(day);
			day->createTask(task);
		} else {
			auto event = source->getEventById(newTaskData.eventId);
			assert(event);
			event->createTask(task);
		}

		miraiInstance_->save();
		view_.update();
		reloadTasks();
		taskWindow_->hide();
	});
}

void UiState::setupEventWindowCallbacks()
{
	mainWindow_->global<Backend>().on_open_new_event_form([&]() {
		auto todayDate = std::chrono::year_month_day{
			std::chrono::floor<std::chrono::days>(std::chrono::system_clock::now())
		};
		eventWindow_->set_eventId(-1);
		eventWindow_->set_taskTitle("");
		eventWindow_->set_taskDate({
			.year = static_cast<int>(todayDate.year()),
			// Try to directly use `unsigned`
			.month = static_cast<int>(static_cast<unsigned>(todayDate.month())),
			.day = static_cast<int>(static_cast<unsigned>(todayDate.day())),
		});

		eventWindow_->show();
	});

	mainWindow_->global<Backend>().on_open_edit_event_form([&](int sourceId, int eventId) {
		auto source = miraiInstance_->getSourceById(sourceId);
		assert(source);
		auto event = source->getEventById(eventId);
		assert(event);
		eventWindow_->global<Backend>().set_sources(sources_);
		eventWindow_->global<Backend>().set_tags(tags_);

		eventWindow_->set_sourceId(sourceId);
		eventWindow_->set_eventId(eventId);
		eventWindow_->set_taskTitle(slint::SharedString(event->getText()));
		eventWindow_->set_taskDate(MiraiDateToSlintDate(event->getDate()));
		eventWindow_->set_startsAt(MiraiTimeToSlintTime(event->getStartTime()));
		eventWindow_->set_endsAt(MiraiTimeToSlintTime(event->getEndTime()));
		eventWindow_->show();
	});

	mainWindow_->global<Backend>().on_delete_event_clicked([&](int sourceId, int eventId) {
		auto source = miraiInstance_->getSourceById(sourceId);
		assert(source);
		auto event = source->getEventById(eventId);
		assert(event);
		auto day = event->day();
		assert(day);
		day->deleteEvent(*event);
		miraiInstance_->save();
		view_.update();
		reloadTasks();
	});

	eventWindow_->on_create([&](NewEventParams newEventParams) {
		const Date &date = newEventParams.date;
		const std::string dateStr = SlintDateToStdString(date);
		auto source = miraiInstance_->getSourceById(newEventParams.sourceId);

		auto day = source->day(SlintDateToMiraiDate(date));
		day->createEvent({
			.description = std::string(newEventParams.title),
			.date = SlintDateToMiraiDate(newEventParams.date),
			.startTime = SlintTimeToMiraiTime(newEventParams.startsAt),
			.endTime = SlintTimeToMiraiTime(newEventParams.endsAt),
		});
		miraiInstance_->save();
		view_.update();
		reloadTasks();
		eventWindow_->hide();
	});

	eventWindow_->on_save([&](SaveEventParams newEventParams) {
		const Date &date = newEventParams.date;
		const std::string dateStr = SlintDateToStdString(date);
		auto source = miraiInstance_->getSourceById(newEventParams.sourceId);
		assert(source);
		auto event = source->getEventById(newEventParams.id);
		assert(event);
		event->setText(std::string(newEventParams.title));
		event->setStartTime(SlintTimeToMiraiTime(newEventParams.startsAt));
		event->setEndTime(SlintTimeToMiraiTime(newEventParams.endsAt));

		// TODO we can't change the date of the event for now.

		miraiInstance_->save();
		view_.update();
		reloadTasks();
		eventWindow_->hide();
	});
}

void UiState::setupUtilsCallbacks()
{
	mainWindow_->global<Backend>().on_formatDate([&](const Date &date) {
		std::chrono::year_month_day chronoDate{
			std::chrono::year(date.year),
			std::chrono::month(date.month),
			std::chrono::day(date.day),
		};
		return std::format("{:%d %B}", chronoDate);
	});
}

void UiState::setupCallbacks()
{
	mainWindow_->global<Backend>().on_task_clicked([&](int sourceId, int taskId) {
		auto source = miraiInstance_->getSourceById(sourceId);
		assert(source);
		auto task = source->getTaskById(taskId);
		assert(task);
		task->getState() == mirai::DONE ? task->markAsUndone() : task->markAsDone();
		miraiInstance_->save();
	});

	mainWindow_->global<Backend>().on_source_clicked([&](int index) {
		const auto &source = miraiInstance_->getSourceById(index);
		const auto &sourceName = source->getName();
		if (std::ranges::find(view_.getActiveFilesFilter(), sourceName) ==
			view_.getActiveFilesFilter().end()) {
			view_.addSourceFilter(sourceName);
		} else {
			view_.removeSourceFilter(sourceName);
		}
		view_.update();
		reloadTasks();
	});

	mainWindow_->global<Backend>().on_tag_clicked([&](int index) {
		const std::string &tag = miraiInstance_->getTags().at(index);
		if (std::ranges::find(view_.getActiveTagsFilter(), tag) ==
			view_.getActiveTagsFilter().end()) {
			view_.addTagFilter(tag);
		} else {
			view_.removeTagFilter(tag);
		}
		view_.update();
		reloadTasks();
	});

	mainWindow_->global<Backend>().on_delete_task_clicked([&](int sourceId, int taskId) {
		auto source = miraiInstance_->getSourceById(sourceId);
		assert(source);
		auto task = source->getTaskById(taskId);
		assert(task);
		source->deleteTask(*task);
		miraiInstance_->save();
		view_.update();
		reloadTasks();
	});

	mainWindow_->global<Backend>().on_open_edit_task_form([&](int sourceId, int taskId) {
		auto source = miraiInstance_->getSourceById(sourceId);
		assert(source);
		auto task = source->getTaskById(taskId);
		assert(task);

		taskWindow_->set_taskSourceIndex(sourceId);
		taskWindow_->set_taskId(task->id());
		taskWindow_->set_taskTitle(slint::SharedString(task->getText()));
		taskWindow_->set_taskDate(MiraiDateToSlintDate(task->day()->getDate()));

		taskWindow_->show();
	});

	mainWindow_->global<Backend>().on_open_new_task_form([&](OpenNewTaskFormParams params) {
		taskWindow_->global<Backend>().set_sources(sources_);
		taskWindow_->global<Backend>().set_tags(tags_);

		auto todayDate = std::chrono::year_month_day{
			std::chrono::floor<std::chrono::days>(std::chrono::system_clock::now())
		};
		taskWindow_->set_taskId(-1);
		taskWindow_->set_eventId(params.eventId);
		taskWindow_->set_taskSourceIndex(params.eventSourceId == -1 ? 0 : params.eventSourceId);
		taskWindow_->set_taskTitle("");

		if (params.eventId == -1) {
			taskWindow_->set_taskDate({
				.year = static_cast<int>(todayDate.year()),
				// Try to directly use `unsigned`
				.month = static_cast<int>(static_cast<unsigned>(todayDate.month())),
				.day = static_cast<int>(static_cast<unsigned>(todayDate.day())),
			});
		} else {
			auto source = miraiInstance_->getSourceById(params.eventSourceId);
			assert(source);
			auto event = source->getEventById(params.eventId);
			assert(event);
			taskWindow_->set_taskDate(MiraiDateToSlintDate(event->getDate()));
		}

		taskWindow_->show();
	});

	mainWindow_->global<Backend>().on_toggle_show_completed_tasks([&] {
		view_.hideCompletedTasks(!view_.shouldHideCompletedTasks());
		view_.update();
		reloadTasks();
	});

	mainWindow_->global<Backend>().set_sources(sources_);
	mainWindow_->global<Backend>().set_tags(tags_);
	mainWindow_->global<Backend>().set_visible_tasks(days_);

	setupTaskWindowCallbacks();
	setupEventWindowCallbacks();
	setupUtilsCallbacks();
}

std::shared_ptr<slint::VectorModel<slint::SharedString>>
stdToSlintStringVector(const std::vector<std::string> &stdVector)
{
	auto slintVector = std::make_shared<slint::VectorModel<slint::SharedString>>();
	for (const auto &item : stdVector) {
		slintVector->push_back(slint::SharedString(item));
	}
	return slintVector;
}

void UiState::reloadTasks()
{
	days_->clear();
	if (miraiInstance_->getSources().size() == 0) {
		return;
	}
	auto todayDate = mirai::Date(std::chrono::system_clock::now());
	auto &days = view_;
	auto slintDays = std::make_shared<slint::VectorModel<Day>>();
	for (int dayIndex = 0; dayIndex < days.count(); ++dayIndex) {
		auto &currentDay = view_[dayIndex];
		auto slintEvents = std::make_shared<slint::VectorModel<Event>>();
		auto slintDayTasks = std::make_shared<slint::VectorModel<TaskData>>();
		slintDays->push_back(Day{
			.sourceId = currentDay.day->source()->id(),
			.id = dayIndex,
			.date = MiraiDateToSlintDate(currentDay.day->getDate()),
			.events = slintEvents,
			.tasks = slintDayTasks,
			.isLate = currentDay.day->getDate() < todayDate,
			.isToday = currentDay.day->getDate() == todayDate,
		});
		for (int taskIndex = 0; taskIndex < currentDay.filteredTasks.size(); ++taskIndex) {
			auto &task = currentDay.filteredTasks.at(taskIndex);
			std::vector<slint::SharedString> tags;
			std::transform(
				task->getTags().begin(), task->getTags().end(), std::back_inserter(tags),
				[&](const std::string &tag) {
					return slint::SharedString(tag);
				}
			);
			slintDayTasks->push_back({
				.sourceId = task->source()->id(),
				.id = task->id(),
				.title = slint::SharedString(task->getText()),
				.checked = task->getState() == mirai::DONE,
				.tags = std::make_shared<slint::VectorModel<slint::SharedString>>(tags),
			});
		}
		for (int eventIndex = 0; eventIndex < currentDay.filteredEvents.size(); ++eventIndex) {
			auto &currentEvent = currentDay.filteredEvents.at(eventIndex);
			auto slintTasks = std::make_shared<slint::VectorModel<TaskData>>();
			slintEvents->push_back(Event{
				.sourceId = currentEvent.event->day()->source()->id(),
				.id = currentEvent.event->id(),
				.title = slint::SharedString(currentEvent.event->getText()),
				.startsAt = MiraiTimeToSlintTime(currentEvent.event->getStartTime()),
				.endsAt = MiraiTimeToSlintTime(currentEvent.event->getEndTime()),
				.tasks = slintTasks,
			});
			for (int taskIndex = 0; taskIndex < currentEvent.filteredTasks.size(); ++taskIndex) {
				auto &task = currentEvent.filteredTasks.at(taskIndex);
				std::vector<slint::SharedString> tags;
				std::transform(
					task->getTags().begin(), task->getTags().end(), std::back_inserter(tags),
					[&](const std::string &tag) {
						return slint::SharedString(tag);
					}
				);
				slintTasks->push_back({
					.sourceId = task->source()->id(),
					.id = task->id(),
					.title = slint::SharedString(task->getText()),
					.checked = task->getState() == mirai::DONE,
					.tags = std::make_shared<slint::VectorModel<slint::SharedString>>(tags),
				});
			}
		}
	}
	days_ = slintDays;
	mainWindow_->global<Backend>().set_visible_tasks(days_);
}

void UiState::reloadSources()
{
	sources_->clear();
	for (const auto &source : miraiInstance_->getSources()) {
		sources_->push_back(slint::SharedString(source->getName()));
	}
}

void UiState::reloadTags()
{
	tags_->clear();
	for (const auto &tag : miraiInstance_->getTags()) {
		tags_->push_back(slint::SharedString(tag));
	}
}

void UiState::run()
{
	mainWindow_->run();
}
