// Copyright 2023 The Grim Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use egui::epaint::RectShape;
use egui::{
	Align, Context, CornerRadius, CursorIcon, LayerId, Layout, Modifiers, Order, ResizeDirection,
	Stroke, StrokeKind, UiBuilder, ViewportCommand,
};
use lazy_static::lazy_static;
use std::sync::atomic::{AtomicBool, Ordering};

use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{ARROWS_IN, ARROWS_OUT, CARET_DOWN, MOON, SUN, X};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::ContentContainer;
use crate::gui::views::{Content, KeyboardContent, Modal, TitlePanel, View};

lazy_static! {
	/// State to check if platform Back button was pressed.
	static ref BACK_BUTTON_PRESSED: AtomicBool = AtomicBool::new(false);
}

/// Implements ui entry point and contains platform-specific callbacks.
pub struct App<Platform> {
	/// Handles platform-specific functionality.
	pub platform: Platform,

	/// Main content.
	content: Content,

	/// Last window resize direction.
	resize_direction: Option<ResizeDirection>,
	/// Flag to check if it's first draw.
	first_draw: bool,
	/// Last status-bar icon state pushed to the platform (Android).
	status_bar_white: Option<bool>,
}

impl<Platform: PlatformCallbacks> App<Platform> {
	pub fn new(platform: Platform) -> Self {
		Self {
			platform,
			content: Content::default(),
			resize_direction: None,
			first_draw: true,
			status_bar_white: None,
		}
	}

	/// Called of first content draw.
	fn on_first_draw(&mut self, ctx: &Context) {
		// Set platform context.
		if View::is_desktop() {
			self.platform.set_context(ctx);
		}
		// Setup visuals.
		crate::setup_fonts(ctx);
		crate::setup_visuals(ctx);
	}

	/// Draw application content.
	pub fn ui(&mut self, ctx: &Context) {
		if self.first_draw {
			self.on_first_draw(ctx);
			self.first_draw = false;
		}

		// Heartbeat for "is the app on-screen": stamp this frame, and keep a light
		// ~2s repaint cadence so the stamp stays fresh while visible. When the app
		// is backgrounded eframe stops calling this, the stamp goes stale, and
		// background workers (the @name re-verify sweep) pause until we're back.
		crate::mark_frame();
		ctx.request_repaint_after(std::time::Duration::from_secs(2));

		// Keep the Android status-bar icons readable against the in-app theme
		// (the app draws edge-to-edge over the status bar). Only on change.
		let white_icons = crate::gui::theme::status_bar_white_icons();
		if self.status_bar_white != Some(white_icons) {
			self.platform.set_status_bar_white_icons(white_icons);
			self.status_bar_white = Some(white_icons);
		}

		// Handle Esc keyboard key event and platform Back button key event.
		let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
		if back_pressed
			|| ctx.input_mut(|i| {
				i.consume_key(Modifiers::NONE, egui::Key::Escape)
					|| i.consume_key(Modifiers::NONE, egui::Key::BrowserBack)
			}) {
			// Pass event to content.
			self.content.on_back(ctx, &self.platform);
			if back_pressed {
				BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed);
			}
			// Request repaint to update previous content.
			ctx.request_repaint();
		}

		// Handle Close event on desktop.
		if View::is_desktop() && ctx.input(|i| i.viewport().close_requested()) {
			if !self.content.exit_allowed {
				ctx.send_viewport_cmd(ViewportCommand::CancelClose);
				Content::show_exit_modal();
			} else {
				let (w, h) = View::window_size(ctx);
				AppConfig::save_window_size(w, h);
				ctx.input(|i| {
					if let Some(rect) = i.viewport().outer_rect {
						AppConfig::save_window_pos(rect.left(), rect.top());
					}
				});
			}
		}

		// Show main content.
		egui::CentralPanel::default()
			.frame(egui::Frame {
				..Default::default()
			})
			.show(ctx, |ui| {
				if View::is_desktop() {
					let is_fullscreen =
						ui.ctx().input(|i| i.viewport().fullscreen.unwrap_or(false));
					let os = egui::os::OperatingSystem::from_target_os();
					match os {
						egui::os::OperatingSystem::Mac => {
							self.window_title_ui(ui, is_fullscreen);
							ui.add_space(-1.0);
							Self::title_panel_bg(ui, true);
							self.content.ui(ui, &self.platform);
						}
						egui::os::OperatingSystem::Windows => {
							Self::title_panel_bg(ui, false);
							self.content.ui(ui, &self.platform);
						}
						_ => {
							self.custom_frame_ui(ui, is_fullscreen);
						}
					}
				} else {
					Self::title_panel_bg(ui, false);
					self.content.ui(ui, &self.platform);
				}
			});

		// Check if desktop window was focused after requested attention.
		if self.platform.user_attention_required()
			&& ctx.input(|i| i.viewport().focused.unwrap_or(true))
		{
			self.platform.clear_user_attention();
		}

		// Show modal or keyboard window above opened Modal.
		if Modal::opened().is_some() {
			ctx.move_to_top(LayerId::new(Order::Middle, egui::Id::new(Modal::WINDOW_ID)));
			let keyboard_showing = if let Some(l) = ctx.top_layer_id() {
				l.id == egui::Id::new(KeyboardContent::WINDOW_ID)
			} else {
				false
			};
			if keyboard_showing {
				ctx.move_to_top(LayerId::new(
					Order::Middle,
					egui::Id::new(KeyboardContent::WINDOW_ID),
				));
			}
		}
		// Reset keyboard state for newly opened modal.
		if Modal::first_draw() {
			KeyboardContent::reset_window_state();
		}
	}

	/// Draw custom desktop window frame content.
	fn custom_frame_ui(&mut self, ui: &mut egui::Ui, is_fullscreen: bool) {
		// Paint the window area inside the frame margin with the theme
		// background first: surface gaps are otherwise transparent, which X11
		// without a compositor renders as black (strip under the sidebar in
		// light/yellow themes). The margin ring itself must STAY transparent —
		// painting it gives the window a visible border under a compositor.
		let fill_rect = if is_fullscreen {
			ui.max_rect()
		} else {
			ui.max_rect().shrink(Content::WINDOW_FRAME_MARGIN)
		};
		let fill_rounding = if is_fullscreen {
			CornerRadius::ZERO
		} else {
			CornerRadius {
				nw: 8,
				ne: 8,
				sw: 0,
				se: 0,
			}
		};
		ui.painter()
			.rect_filled(fill_rect, fill_rounding, Colors::fill());
		let content_bg_rect = {
			let mut r = ui.max_rect();
			if !is_fullscreen {
				r = r.shrink(Content::WINDOW_FRAME_MARGIN);
			}
			r.min.y += Content::WINDOW_TITLE_HEIGHT + TitlePanel::HEIGHT;
			r
		};
		let content_bg = RectShape::new(
			content_bg_rect,
			CornerRadius::ZERO,
			Colors::fill_lite(),
			View::default_stroke(),
			StrokeKind::Outside,
		);
		// Draw content background.
		ui.painter().add(content_bg);

		let mut content_rect = ui.max_rect();
		if !is_fullscreen {
			content_rect = content_rect.shrink(Content::WINDOW_FRAME_MARGIN);
		}
		// Draw window content.
		ui.scope_builder(UiBuilder::new().max_rect(content_rect), |ui| {
			// Draw window title.
			self.window_title_ui(ui, is_fullscreen);
			ui.add_space(-1.0);

			// Draw title panel background.
			Self::title_panel_bg(ui, true);

			let content_rect = {
				let mut rect = ui.max_rect();
				rect.min.y += Content::WINDOW_TITLE_HEIGHT;
				rect
			};
			let mut content_ui =
				ui.new_child(UiBuilder::new().max_rect(content_rect).layout(*ui.layout()));
			// Draw main content.
			self.content.ui(&mut content_ui, &self.platform);
		});

		// Setup resize areas.
		if !is_fullscreen {
			self.resize_area_ui(ui, ResizeDirection::North);
			self.resize_area_ui(ui, ResizeDirection::East);
			self.resize_area_ui(ui, ResizeDirection::South);
			self.resize_area_ui(ui, ResizeDirection::West);
			self.resize_area_ui(ui, ResizeDirection::NorthWest);
			self.resize_area_ui(ui, ResizeDirection::NorthEast);
			self.resize_area_ui(ui, ResizeDirection::SouthEast);
			self.resize_area_ui(ui, ResizeDirection::SouthWest);
		}
	}

	/// Draw title panel background.
	fn title_panel_bg(ui: &mut egui::Ui, window_title: bool) {
		let title_rect = {
			let mut rect = ui.max_rect();
			if window_title {
				rect.min.y += Content::WINDOW_TITLE_HEIGHT - 0.5;
			}
			rect.max.y = rect.min.y + View::get_top_inset() + TitlePanel::HEIGHT;
			rect
		};
		let title_bg = RectShape::filled(title_rect, CornerRadius::ZERO, Colors::yellow());
		ui.painter().add(title_bg);
	}

	/// Draw custom window title content.
	fn window_title_ui(&self, ui: &mut egui::Ui, is_fullscreen: bool) {
		let title_rect = {
			let mut rect = ui.max_rect();
			rect.max.y = rect.min.y + Content::WINDOW_TITLE_HEIGHT;
			rect
		};

		let title_bg_rect = {
			let mut r = title_rect.clone();
			r.max.y += TitlePanel::HEIGHT - 1.0;
			r
		};
		let is_mac = egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Mac;
		let window_title_bg = RectShape::new(
			title_bg_rect,
			if is_fullscreen || is_mac {
				CornerRadius::ZERO
			} else {
				CornerRadius {
					nw: 8.0 as u8,
					ne: 8.0 as u8,
					sw: 0.0 as u8,
					se: 0.0 as u8,
				}
			},
			Colors::yellow_dark(),
			Stroke::new(1.0, Colors::STROKE),
			StrokeKind::Outside,
		);
		// Draw title background.
		ui.painter().add(window_title_bg);

		let painter = ui.painter();

		let interact_rect = {
			let mut rect = title_rect.clone();
			rect.max.x -= 128.0;
			rect.min.x += 85.0;
			if !is_fullscreen {
				rect.min.y += Content::WINDOW_FRAME_MARGIN;
			}
			rect
		};
		let title_resp = ui.interact(
			interact_rect,
			egui::Id::new("window_title"),
			egui::Sense::drag(),
		);
		// Interact with the window title (drag to move window):
		if !is_fullscreen && title_resp.drag_started_by(egui::PointerButton::Primary) {
			ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag);
		}

		// Paint the title. Centering on the full rect runs the tail of the
		// string under the right-side window buttons at narrow widths (the
		// "Build NNN" digits collide with the minimize caret at 390px), so
		// when the centered galley would reach the button cluster, center it
		// in the free span between the theme toggle and the buttons instead,
		// eliding if even that span is too tight.
		let title_text = format!("Goblin ツ · Build {}", crate::BUILD);
		let title_font = egui::FontId::proportional(15.0);
		let title_ink = Colors::title(true);
		const BUTTONS_LEFT_INSET: f32 = 60.0; // theme toggle
		const BUTTONS_RIGHT_INSET: f32 = 168.0; // minimize + fullscreen + close
		let free_left = title_rect.min.x + BUTTONS_LEFT_INSET;
		let free_right = title_rect.max.x - BUTTONS_RIGHT_INSET;
		let mut galley = painter.layout_no_wrap(title_text.clone(), title_font.clone(), title_ink);
		let mut center_x = title_rect.center().x;
		if center_x + galley.size().x / 2.0 > free_right {
			center_x = (free_left + free_right) / 2.0;
			if galley.size().x > free_right - free_left {
				let mut job =
					egui::text::LayoutJob::simple_singleline(title_text, title_font, title_ink);
				job.wrap =
					egui::text::TextWrapping::truncate_at_width((free_right - free_left).max(0.0));
				galley = painter.layout_job(job);
			}
		}
		painter.galley(
			egui::pos2(
				center_x - galley.size().x / 2.0,
				title_rect.center().y - galley.size().y / 2.0,
			),
			galley,
			title_ink,
		);

		ui.scope_builder(UiBuilder::new().max_rect(title_rect), |ui| {
			ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
				// Draw button to close window.
				View::title_button_small(ui, X, |_| {
					if Modal::opened().is_none() || Modal::opened_closeable() {
						Content::show_exit_modal();
					}
				});

				// Draw fullscreen button.
				let fullscreen_icon = if is_fullscreen { ARROWS_IN } else { ARROWS_OUT };
				View::title_button_small(ui, fullscreen_icon, |ui| {
					ui.ctx()
						.send_viewport_cmd(ViewportCommand::Fullscreen(!is_fullscreen));
				});

				// Draw button to minimize window.
				View::title_button_small(ui, CARET_DOWN, |ui| {
					ui.ctx().send_viewport_cmd(ViewportCommand::Minimized(true));
				});

				// Draw application icon.
				let layout_size = ui.available_size();
				ui.allocate_ui_with_layout(
					layout_size,
					Layout::left_to_right(Align::Center),
					|ui| {
						// Draw button to minimize window.
						let use_dark = AppConfig::dark_theme().unwrap_or(false);
						let theme_icon = if use_dark { SUN } else { MOON };
						View::title_button_small(ui, theme_icon, |ui| {
							AppConfig::set_dark_theme(!use_dark);
							crate::setup_visuals(ui.ctx());
						});
					},
				);
			});
		});
	}

	/// Setup window resize area.
	fn resize_area_ui(&mut self, ui: &egui::Ui, direction: ResizeDirection) {
		let mut rect = ui.max_rect();

		// Setup area id, cursor and area rect based on direction.
		let (id, cursor, rect) = match direction {
			ResizeDirection::North => ("n", CursorIcon::ResizeNorth, {
				rect.min.x += Content::WINDOW_FRAME_MARGIN * 2.0;
				rect.max.y = rect.min.y + Content::WINDOW_FRAME_MARGIN;
				rect.max.x -= Content::WINDOW_FRAME_MARGIN * 2.0;
				rect
			}),
			ResizeDirection::East => ("e", CursorIcon::ResizeEast, {
				rect.min.y += Content::WINDOW_FRAME_MARGIN * 2.0;
				rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN;
				rect.max.y -= Content::WINDOW_FRAME_MARGIN * 2.0;
				rect
			}),
			ResizeDirection::South => ("s", CursorIcon::ResizeSouth, {
				rect.min.x += Content::WINDOW_FRAME_MARGIN * 2.0;
				rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN;
				rect.max.x -= Content::WINDOW_FRAME_MARGIN * 2.0;
				rect
			}),
			ResizeDirection::West => ("w", CursorIcon::ResizeWest, {
				rect.min.y += Content::WINDOW_FRAME_MARGIN * 2.0;
				rect.max.x = rect.min.x + Content::WINDOW_FRAME_MARGIN;
				rect.max.y -= Content::WINDOW_FRAME_MARGIN * 2.0;
				rect
			}),
			ResizeDirection::NorthWest => ("nw", CursorIcon::ResizeNorthWest, {
				rect.max.y = rect.min.y + Content::WINDOW_FRAME_MARGIN * 2.0;
				rect.max.x = rect.max.y + Content::WINDOW_FRAME_MARGIN * 2.0;
				rect
			}),
			ResizeDirection::NorthEast => ("ne", CursorIcon::ResizeNorthEast, {
				rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN * 2.0;
				rect.max.y = Content::WINDOW_FRAME_MARGIN * 2.0;
				rect
			}),
			ResizeDirection::SouthEast => ("se", CursorIcon::ResizeSouthEast, {
				rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN * 2.0;
				rect.min.x = rect.max.x - Content::WINDOW_FRAME_MARGIN * 2.0;
				rect
			}),
			ResizeDirection::SouthWest => ("sw", CursorIcon::ResizeSouthWest, {
				rect.min.y = rect.max.y - Content::WINDOW_FRAME_MARGIN * 2.0;
				rect.max.x = rect.min.x + Content::WINDOW_FRAME_MARGIN * 2.0;
				rect
			}),
		};

		// Setup resize area.
		let id = egui::Id::new("window_resize").with(id);
		let sense = egui::Sense::drag();
		let area_resp = ui.interact(rect, id, sense).on_hover_cursor(cursor);
		if area_resp.dragged() {
			if self.resize_direction.is_none() {
				self.resize_direction = Some(direction.clone());
				ui.ctx()
					.send_viewport_cmd(ViewportCommand::BeginResize(direction));
			}
		}
		if area_resp.drag_stopped() {
			self.resize_direction = None;
		}
	}
}

/// To draw with egui`s eframe (for wgpu, glow backends and wasm target).
impl<Platform: PlatformCallbacks> eframe::App for App<Platform> {
	fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) {
		ctx.plugin_or_default::<egui_async::EguiAsyncPlugin>();
		self.ui(ctx);
	}

	fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
		let os = egui::os::OperatingSystem::from_target_os();
		let is_win = os == egui::os::OperatingSystem::Windows;
		let is_mac = os == egui::os::OperatingSystem::Mac;
		if !View::is_desktop() || is_win || is_mac {
			return Colors::fill_lite().to_normalized_gamma_f32();
		}
		Colors::TRANSPARENT.to_normalized_gamma_f32()
	}
}

#[allow(dead_code)]
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
#[unsafe(no_mangle)]
/// Handle Back key code event from Android.
pub extern "C" fn Java_mw_gri_android_MainActivity_onBack(
	_env: jni::JNIEnv,
	_class: jni::objects::JObject,
	_activity: jni::objects::JObject,
) {
	BACK_BUTTON_PRESSED.store(true, Ordering::Relaxed);
}
