// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
use std::time::{Instant, Duration};

use egui::{CentralPanel, ScrollArea, Ui, Context, Frame, Margin, Vec2};
use winit::{window::Window, event::{Event, WindowEvent}};

use super::{Gui, style::BACKGROUND};

use crate::app::{App, config::action::{Action, ActionEvent, ActionState}};

pub struct GuiState {
	last_repaint: Instant,
	should_exit: bool,
	prev_scroll_offset: Vec2,
}

#[must_use = "Should call `GuiState:after_update` with this to prevent the GUI from freezing from inactivity if the user scrolls by dragging and releasing."]
pub struct GuiUpdateResult(Option<Vec2>);

impl GuiState {
	pub fn new() -> GuiState {
		GuiState {
			last_repaint: Instant::now(),
			should_exit: false,
			prev_scroll_offset: Vec2::ZERO,
		}
	}

	pub fn enable(&mut self, gui: &mut Gui) {
		self.should_exit = false;
		self.should_repaint();
		gui.do_extra_pass();
	}

	pub fn action(&mut self, action: ActionEvent) {
		if matches!(action, ActionEvent::Action(Action::GuiLeave, ActionState::Pressed)) {
			self.should_exit = true;
		}
	}

	pub fn event(&mut self, app: &mut App, event: &Event<()>) {
		match event {
			Event::WindowEvent { event, .. } => {
				/*
				 * Don't repaint when a RedrawRequested is emitted, as those redraw
				 * requests can come from the below call to request_redraw, rather
				 * than any actual need to redraw.
				 */
				if (app.gui.window_event(&app.window, event).repaint && *event != WindowEvent::RedrawRequested) || matches!(event, WindowEvent::MouseInput { .. }) {
					self.should_repaint();
					app.window.request_redraw();
				}
			}
			Event::AboutToWait => {
				// Waits for egui animations to finish
				if self.last_repaint.elapsed() < Duration::from_millis(500) {
					app.window.request_redraw();
				}
			}
			_ => ()
		}
	}

	pub fn update(gui: &mut Gui, id: &str, window: &Window, mut add_contents: impl FnMut(&Context, &mut Ui)) -> GuiUpdateResult {
		let mut res = None;
		gui.update(window, |ctx| {
			// Same as the default frame but without the margins, so `ui.available_width` can return the complete width
			let frame = Frame::none().fill(BACKGROUND);

			CentralPanel::default().frame(frame).show(ctx, |ui| {
				const MAX_WIDTH: f32 = 1000.0;
				const MIN_MARGINS: f32 = 60.0;
				const WIDTH_THRESHOLD: f32 = MAX_WIDTH + MIN_MARGINS * 2.0;

				let width = ui.available_width();

				/*
				 * Margins start at `MIN_MARGINS` and stay constant until they
				 * increase when the content width reaches its maximum of
				 * `MAX_WIDTH`.
				 */
				let margins = if width < WIDTH_THRESHOLD { MIN_MARGINS } else { (width - MAX_WIDTH) / 2.0 };

				let response = ScrollArea::vertical().id_salt(id).show(ui, |ui| {
					Frame::none().outer_margin(Margin::symmetric(margins, 0.0)).show(ui, |ui| {
						ui.vertical_centered(|ui| {
							// Added through h1
							//ui.add_space(60.0);
							add_contents(ctx, ui);
							ui.add_space(60.0);
						});
					});
				});

				res = Some(response.state.offset);
			});
		});
		GuiUpdateResult(res)
	}

	/**
	 * Can't do this logic in the `update` method as adding the self reference to
	 * that causes borrow checker issues in the code that uses this which would
	 * be a huge hassle to resolve.
	 */
	pub fn after_update(&mut self, res: GuiUpdateResult) {
		if let Some(offset) = res.0 && offset != self.prev_scroll_offset {
			self.should_repaint();
			self.prev_scroll_offset = offset;
		}
	}

	pub fn should_repaint(&mut self) {
		self.last_repaint = Instant::now();
	}

	pub fn should_exit(&mut self) -> bool {
		let ret = self.should_exit;
		self.should_exit = false;
		ret
	}

	pub fn exit(&mut self) {
		self.should_exit = true;
	}
}
