use rand::Rng;
use rfd::AsyncFileDialog;
use std::fs;
use std::io::Read;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Receiver, SyncSender};
use std::thread;
use std::time::{Duration, Instant};
use winit::application::ApplicationHandler;
use winit::dpi::{PhysicalPosition, PhysicalSize};
use winit::event::{ButtonSource, ElementState, MouseButton, StartCause, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::window::WindowId;

use crate::dirs;
use crate::event::{AppEvent, AppEventScheduler};
use crate::persist;
use crate::preferences;
use crate::render::{RenderError, RenderState};
use crate::ui::{UiCommand, UiInstance};
use crate::window::Window;
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Preferences};
use crate::wrapper::{DesktopWrapper, MmapResourceStorage, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};

pub(crate) struct App {
	render_state: Option<RenderState>,
	wgpu_context: WgpuContext,
	window: Option<Window>,
	window_scale: f64,
	window_size: PhysicalSize<u32>,
	window_maximized: bool,
	window_fullscreen: bool,
	window_pending_drag: bool,
	pointer_position: PhysicalPosition<f64>,
	pointer_lock_position: Option<PhysicalPosition<f64>>,
	ui_scale: f64,
	app_event_receiver: Receiver<AppEvent>,
	app_event_scheduler: AppEventScheduler,
	desktop_wrapper: DesktopWrapper,
	ui: UiInstance,
	ui_frame_received: bool,
	start_render_sender: SyncSender<()>,
	web_communication_initialized: bool,
	web_communication_startup_buffer: Vec<Vec<u8>>,
	preferences: Preferences,
	launch_documents: Option<Vec<PathBuf>>,
	startup_time: Option<Instant>,
	exiting: Arc<AtomicBool>,
	exit_reason: ExitReason,
}

impl App {
	pub(crate) fn init() {
		Window::init();
	}

	pub(crate) fn new(
		ui: UiInstance,
		wgpu_context: WgpuContext,
		app_event_receiver: Receiver<AppEvent>,
		app_event_scheduler: AppEventScheduler,
		preferences: Preferences,
		launch_documents: Vec<PathBuf>,
	) -> Self {
		let ctrlc_app_event_scheduler = app_event_scheduler.clone();
		ctrlc::set_handler(move || {
			tracing::info!("Termination signal received, exiting...");
			ctrlc_app_event_scheduler.schedule(AppEvent::Exit);
		})
		.expect("Error setting Ctrl-C handler");

		let exiting = Arc::new(AtomicBool::new(false));

		let rendering_app_event_scheduler = app_event_scheduler.clone();
		let (start_render_sender, start_render_receiver) = std::sync::mpsc::sync_channel(1);
		let exiting_clone = exiting.clone();
		std::thread::spawn(move || {
			let runtime = tokio::runtime::Runtime::new().unwrap();
			loop {
				let result = runtime.block_on(DesktopWrapper::execute_node_graph());
				rendering_app_event_scheduler.schedule(AppEvent::NodeGraphExecutionResult(result));
				let _ = start_render_receiver.recv_timeout(Duration::from_millis(10));
				if exiting_clone.load(Ordering::Relaxed) {
					break;
				}
			}
		});

		let resource_storage = MmapResourceStorage::new(dirs::app_resources_dir()).expect("Failed to initialize on-disk resource storage");

		// Wake the winit event loop when an editor future completes.
		let wake_scheduler = app_event_scheduler.clone();
		let wake = Arc::new(move || {
			wake_scheduler.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::Wake));
		});
		let desktop_wrapper = DesktopWrapper::new(rand::rng().random(), Arc::new(resource_storage), dirs::app_autosave_documents_dir(), wgpu_context.clone(), wake);

		Self {
			render_state: None,
			wgpu_context,
			window: None,
			window_scale: 1.,
			window_size: PhysicalSize { width: 0, height: 0 },
			window_maximized: false,
			window_fullscreen: false,
			window_pending_drag: false,
			pointer_position: Default::default(),
			pointer_lock_position: Default::default(),
			ui_scale: 1.,
			app_event_receiver,
			app_event_scheduler,
			desktop_wrapper,
			ui,
			ui_frame_received: false,
			start_render_sender,
			web_communication_initialized: false,
			web_communication_startup_buffer: Vec::new(),
			preferences,
			launch_documents: Some(launch_documents),
			startup_time: None,
			exiting,
			exit_reason: ExitReason::Shutdown,
		}
	}

	pub(crate) fn run(mut self, event_loop: EventLoop) -> ExitReason {
		event_loop.run_app(&mut self).unwrap();
		self.exit_reason
	}

	fn exit(&mut self, reason: Option<ExitReason>) {
		if self.exiting.swap(true, Ordering::Relaxed) {
			return;
		}
		let _ = self.start_render_sender.send(());
		if let Some(reason) = reason {
			self.exit_reason = reason;
		}
		self.app_event_scheduler.schedule(AppEvent::Exit);
	}

	fn resize(&mut self) {
		let Some(window) = &self.window else {
			tracing::error!("Resize failed due to missing window");
			return;
		};

		let maximized = window.is_maximized();
		if maximized != self.window_maximized {
			self.window_maximized = maximized;
			self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::UpdateMaximized { maximized }));
		}

		let fullscreen = window.is_fullscreen();
		if fullscreen != self.window_fullscreen {
			self.window_fullscreen = fullscreen;
			self.app_event_scheduler
				.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::UpdateFullscreen { fullscreen }));
		}

		let size = window.surface_size();
		let scale = window.scale_factor() * self.ui_scale;
		let is_new_size = size != self.window_size;
		let is_new_scale = scale != self.window_scale;

		if !is_new_size && !is_new_scale {
			return;
		}

		if is_new_size {
			self.ui.send(UiCommand::Resized {
				width: size.width,
				height: size.height,
			});
		}
		if is_new_scale {
			self.ui.send(UiCommand::ScaleChanged(scale));
		}

		self.ui.send(UiCommand::Refresh);

		if let Some(render_state) = &mut self.render_state {
			render_state.resize(size.width, size.height);
		}

		window.request_redraw();

		self.window_size = size;
		self.window_scale = scale;
	}

	fn handle_desktop_frontend_message(&mut self, message: DesktopFrontendMessage, responses: &mut Vec<DesktopWrapperMessage>) {
		match message {
			DesktopFrontendMessage::ToWeb(messages) => {
				let Some(bytes) = serialize_frontend_messages(messages) else {
					tracing::error!("Failed to serialize frontend messages");
					return;
				};
				self.send_or_queue_web_message(bytes);
			}
			DesktopFrontendMessage::OpenFileDialog { title, filters, multiple, context } => {
				let app_event_scheduler = self.app_event_scheduler.clone();
				let _ = thread::spawn(move || {
					let mut dialog = AsyncFileDialog::new().set_title(title);
					for filter in filters {
						dialog = dialog.add_filter(filter.name, &filter.extensions);
					}

					let handles = if multiple {
						futures::executor::block_on(dialog.pick_files()).unwrap_or_default()
					} else {
						futures::executor::block_on(dialog.pick_file()).into_iter().collect()
					};

					for handle in handles {
						let path = handle.path().to_path_buf();
						match fs::read(&path) {
							Ok(content) => {
								let message = DesktopWrapperMessage::FileDialogResult { path, content, context };
								app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
							}
							Err(e) => tracing::error!("Failed to read file {}: {}", path.display(), e),
						}
					}
				});
			}
			DesktopFrontendMessage::SaveFileDialog {
				title,
				default_filename,
				default_folder,
				filters,
				context,
			} => {
				let app_event_scheduler = self.app_event_scheduler.clone();
				let _ = thread::spawn(move || {
					let mut dialog = AsyncFileDialog::new().set_title(title).set_file_name(default_filename);
					if let Some(folder) = default_folder {
						dialog = dialog.set_directory(folder);
					}
					for filter in filters {
						dialog = dialog.add_filter(filter.name, &filter.extensions);
					}

					let show_dialog = async move { dialog.save_file().await.map(|f| f.path().to_path_buf()) };

					if let Some(path) = futures::executor::block_on(show_dialog) {
						let message = DesktopWrapperMessage::SaveFileDialogResult { path, context };
						app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
					}
				});
			}
			DesktopFrontendMessage::WriteFile { path, content } => {
				if let Err(e) = fs::write(&path, content) {
					tracing::error!("Failed to write file {}: {}", path.display(), e);
				}
			}
			DesktopFrontendMessage::OpenUrl(url) => {
				let _ = thread::spawn(move || {
					if let Err(e) = open::that(&url) {
						tracing::error!("Failed to open URL: {}: {}", url, e);
					}
				});
			}
			DesktopFrontendMessage::UpdateViewportPhysicalBounds { x, y, width, height } => {
				if let Some(render_state) = &mut self.render_state
					&& let Some(window) = &self.window
				{
					let window_size = window.surface_size();

					let viewport_offset_x = x / window_size.width as f64;
					let viewport_offset_y = y / window_size.height as f64;
					render_state.set_viewport_offset([viewport_offset_x as f32, viewport_offset_y as f32]);

					let viewport_scale_x = if width != 0. { window_size.width as f64 / width } else { 1. };
					let viewport_scale_y = if height != 0. { window_size.height as f64 / height } else { 1. };
					render_state.set_viewport_scale([viewport_scale_x as f32, viewport_scale_y as f32]);
				}
			}
			DesktopFrontendMessage::UpdateUIScale { scale } => {
				self.ui_scale = scale;
				self.resize();
			}
			DesktopFrontendMessage::UpdateOverlays(scene) => {
				if let Some(render_state) = &mut self.render_state {
					render_state.set_overlays_scene(scene);
				}
				if let Some(window) = &self.window {
					window.request_redraw();
				}
			}
			DesktopFrontendMessage::PersistenceWriteState { state } => {
				persist::write_state(state);
			}
			DesktopFrontendMessage::PersistenceReadState => {
				responses.push(DesktopWrapperMessage::LoadPersistedState { state: persist::read_state() });
			}
			DesktopFrontendMessage::PersistenceReadDocument { id } => {
				if let Some(document) = persist::read_document_content(&id) {
					responses.push(DesktopWrapperMessage::LoadDocumentContent { id, document });
				} else {
					tracing::error!("Failed to read document content for {id:?}");
				}
			}
			DesktopFrontendMessage::PersistenceWriteDocument { id, document_serialized_content } => {
				persist::write_document_content(id, document_serialized_content);
			}
			DesktopFrontendMessage::PersistenceDeleteDocument { id } => {
				persist::delete_document(&id);
			}
			DesktopFrontendMessage::PersistenceWritePreferences { preferences } => {
				preferences::write(preferences);
			}
			DesktopFrontendMessage::PersistenceLoadPreferences => {
				let preferences = preferences::read();
				let message = DesktopWrapperMessage::LoadPreferences { preferences };
				responses.push(message);
			}
			DesktopFrontendMessage::OpenLaunchDocuments => {
				let Some(launch_documents) = std::mem::take(&mut self.launch_documents) else {
					tracing::error!("OpenLaunchDocuments should only be sent once");
					return;
				};
				self.app_event_scheduler.schedule(AppEvent::OpenFiles(launch_documents));
			}
			DesktopFrontendMessage::UpdateMenu { entries } => {
				if let Some(window) = &self.window {
					window.update_menu(entries);
				}
			}
			DesktopFrontendMessage::ClipboardRead => {
				if let Some(window) = &self.window {
					let content = window.clipboard_read();
					let message = DesktopWrapperMessage::ClipboardReadResult { content };
					self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
				}
			}
			DesktopFrontendMessage::ClipboardWrite { content } => {
				if let Some(window) = &mut self.window {
					window.clipboard_write(content);
				}
			}
			DesktopFrontendMessage::PointerLock => {
				self.pointer_lock_position = Some(self.pointer_position);
				if let Some(window) = &self.window {
					window.start_pointer_lock();
				}
			}
			DesktopFrontendMessage::WindowClose => {
				self.app_event_scheduler.schedule(AppEvent::Exit);
			}
			DesktopFrontendMessage::WindowMinimize => {
				if let Some(window) = &self.window {
					window.minimize();
				}
			}
			DesktopFrontendMessage::WindowMaximize => {
				if let Some(window) = &self.window {
					window.toggle_maximize();
				}
			}
			DesktopFrontendMessage::WindowFullscreen => {
				if let Some(window) = &mut self.window {
					window.toggle_fullscreen();
				}
			}
			DesktopFrontendMessage::WindowDrag => {
				self.window_pending_drag = true;
			}
			DesktopFrontendMessage::WindowFocus => {
				if let Some(window) = &self.window {
					window.focus();
				}
			}
			DesktopFrontendMessage::WindowHide => {
				if let Some(window) = &self.window {
					window.hide();
				}
			}
			DesktopFrontendMessage::WindowHideOthers => {
				if let Some(window) = &self.window {
					window.hide_others();
				}
			}
			DesktopFrontendMessage::WindowShowAll => {
				if let Some(window) = &self.window {
					window.show_all();
				}
			}
			DesktopFrontendMessage::Restart => {
				self.exit(Some(ExitReason::Restart));
			}
			DesktopFrontendMessage::LoadThirdPartyLicenses => {
				let compressed = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/third-party-licenses.txt.xz"));
				let mut reader = lzma_rust2::XzReader::new(compressed.as_slice(), false);
				let mut text = String::new();
				if let Err(e) = reader.read_to_string(&mut text) {
					tracing::error!("Failed to decompress third-party licenses: {e}");
					return;
				}

				let message = DesktopWrapperMessage::LoadThirdPartyLicenses { text };
				responses.push(message);
			}
		}
	}

	fn handle_desktop_frontend_messages(&mut self, messages: Vec<DesktopFrontendMessage>) {
		let mut responses = Vec::new();
		for message in messages {
			self.handle_desktop_frontend_message(message, &mut responses);
		}
		for message in responses {
			self.dispatch_desktop_wrapper_message(message);
		}
	}

	fn dispatch_desktop_wrapper_message(&mut self, message: DesktopWrapperMessage) {
		let responses = self.desktop_wrapper.dispatch(message);
		self.handle_desktop_frontend_messages(responses);
	}

	fn send_or_queue_web_message(&mut self, message: Vec<u8>) {
		if self.web_communication_initialized {
			self.ui.send(UiCommand::Message(message));
		} else {
			self.web_communication_startup_buffer.push(message);
		}
	}

	fn user_event(&mut self, event_loop: &dyn ActiveEventLoop, event: AppEvent) {
		match event {
			AppEvent::WebCommunicationInitialized => {
				self.web_communication_initialized = true;
				for message in self.web_communication_startup_buffer.drain(..) {
					self.ui.send(UiCommand::Message(message));
				}
			}
			AppEvent::DesktopWrapperMessage(message) => self.dispatch_desktop_wrapper_message(message),
			AppEvent::NodeGraphExecutionResult(result) => match result {
				NodeGraphExecutionResult::HasRun(texture) => {
					self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::PollNodeGraphEvaluation);
					if let Some(texture) = texture
						&& let Some(render_state) = self.render_state.as_mut()
						&& let Some(window) = self.window.as_ref()
					{
						render_state.bind_viewport_texture(texture);
						window.request_redraw();
					}
				}
				NodeGraphExecutionResult::NotRun => {}
			},
			AppEvent::UiUpdate(texture) => {
				if let Some(render_state) = self.render_state.as_mut() {
					render_state.bind_ui_texture(texture);
				}
				if let Some(window) = &self.window {
					window.request_redraw();
				}
				if !self.ui_frame_received {
					self.ui_frame_received = true;
				}
			}
			AppEvent::CursorChange(cursor) => {
				if let Some(window) = &mut self.window {
					window.set_cursor(event_loop, cursor);
				}
			}
			AppEvent::Exit => {
				tracing::info!("Exiting main event loop");
				event_loop.exit();
			}
			AppEvent::UiCrashed => {
				tracing::error!("UI process crashed, exiting.");
				self.exit(Some(ExitReason::Shutdown));
			}
			AppEvent::OpenFiles(paths) => {
				// Accumulate launch documents until OpenLaunchDocuments message is received
				if let Some(launch_documents) = &mut self.launch_documents {
					launch_documents.extend(paths);
					return;
				}

				if paths.is_empty() {
					return;
				}
				let app_event_scheduler = self.app_event_scheduler.clone();
				let _ = thread::spawn(move || {
					for path in paths {
						tracing::info!("Opening file: {}", path.display());
						if let Ok(content) = fs::read(&path) {
							let message = DesktopWrapperMessage::OpenFile { path, content };
							app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
						} else {
							tracing::error!("Failed to read file: {}", path.display());
						}
					}
				});
			}
			#[cfg(target_os = "macos")]
			AppEvent::MenuEvent { id } => {
				self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::MenuEvent { id });
			}
		}
	}
}
impl ApplicationHandler for App {
	fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
		let window = Window::new(event_loop, self.app_event_scheduler.clone());
		self.window = Some(window);

		#[cfg(not(target_os = "macos"))]
		let present_mode = None;
		#[cfg(target_os = "macos")]
		let present_mode = if !self.preferences.vsync { Some(wgpu::PresentMode::Immediate) } else { None };

		let render_state = RenderState::new(self.window.as_ref().unwrap(), self.wgpu_context.clone(), present_mode);
		self.render_state = Some(render_state);

		if let Some(window) = &self.window.as_ref() {
			window.show();
		}

		self.resize();

		self.startup_time = Some(Instant::now());
	}

	fn proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) {
		while let Ok(event) = self.app_event_receiver.try_recv() {
			self.user_event(event_loop, event);
		}
	}

	fn window_event(&mut self, _event_loop: &dyn ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
		// Handle pointer lock release
		if let Some(pointer_lock_position) = self.pointer_lock_position
			&& let WindowEvent::PointerButton {
				state: ElementState::Released,
				button: ButtonSource::Mouse(MouseButton::Left),
				..
			} = event
		{
			self.pointer_lock_position = None;
			if let Some(window) = &self.window {
				window.end_pointer_lock();
			}
			self.ui.send(UiCommand::Input(WindowEvent::PointerMoved {
				device_id: None,
				position: pointer_lock_position,
				primary: true,
				source: winit::event::PointerSource::Mouse,
			}));
		}

		self.ui.send(UiCommand::Input(event.clone()));

		match event {
			WindowEvent::CloseRequested => {
				self.app_event_scheduler.schedule(AppEvent::Exit);
			}
			WindowEvent::SurfaceResized(_) | WindowEvent::ScaleFactorChanged { .. } => {
				self.resize();
			}
			WindowEvent::RedrawRequested => {
				#[cfg(target_os = "macos")]
				self.resize();

				let Some(render_state) = &mut self.render_state else { return };
				if let Some(window) = &self.window {
					if !window.can_render() {
						return;
					}

					match render_state.render(window) {
						Ok(_) => {}
						Err(RenderError::OutdatedUITextureError) => {
							self.ui.send(UiCommand::Refresh);
						}
						Err(RenderError::SurfaceLost) => {
							tracing::warn!("lost surface");
						}
						Err(other) => tracing::error!("Render error: {:?}", other),
					}
					let _ = self.start_render_sender.try_send(());
				}

				if !self.ui_frame_received
					&& !self.preferences.disable_ui_acceleration
					&& self.web_communication_initialized
					&& let Some(startup_time) = self.startup_time
					&& startup_time.elapsed() > Duration::from_secs(3)
				{
					tracing::error!("UI acceleration not working, exiting.");
					self.exit(Some(ExitReason::UiAccelerationFailure));
				}
			}
			WindowEvent::DragDropped { paths, .. } => {
				for path in paths {
					match fs::read(&path) {
						Ok(content) => {
							let message = DesktopWrapperMessage::ImportFile { path, content };
							self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
						}
						Err(e) => {
							tracing::error!("Failed to read dropped file {}: {}", path.display(), e);
							return;
						}
					};
				}
			}

			// Forward and Back buttons are not supported by CEF and thus need to be directly forwarded the editor
			WindowEvent::PointerButton {
				button: ButtonSource::Mouse(button),
				state: ElementState::Pressed,
				..
			} => {
				let mouse_keys = match button {
					MouseButton::Back => Some(MouseKeys::BACK),
					MouseButton::Forward => Some(MouseKeys::FORWARD),
					_ => None,
				};
				if let Some(mouse_keys) = mouse_keys {
					let message = DesktopWrapperMessage::Input(InputMessage::PointerDown {
						editor_mouse_state: MouseState { mouse_keys, ..Default::default() },
						modifier_keys: Default::default(),
					});
					self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));

					let message = DesktopWrapperMessage::Input(InputMessage::PointerUp {
						editor_mouse_state: Default::default(),
						modifier_keys: Default::default(),
					});
					self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
				}
			}

			WindowEvent::PointerMoved { position, .. } | WindowEvent::PointerLeft { position: Some(position), .. } | WindowEvent::PointerEntered { position, .. }
				if self.pointer_lock_position.is_none() =>
			{
				self.pointer_position = position;

				if self.window_pending_drag {
					self.window_pending_drag = false;
					if let Some(window) = &self.window {
						window.start_drag();
					}
				}
			}

			WindowEvent::PointerButton {
				button: ButtonSource::Mouse(MouseButton::Left),
				state: ElementState::Released,
				..
			} => {
				self.window_pending_drag = false;
			}

			_ => {}
		}
	}

	fn device_event(&mut self, _event_loop: &dyn ActiveEventLoop, _device_id: Option<winit::event::DeviceId>, event: winit::event::DeviceEvent) {
		if self.pointer_lock_position.is_some()
			&& let winit::event::DeviceEvent::PointerMotion { delta: (x, y) } = event
		{
			let message = DesktopWrapperMessage::PointerLockMove { x, y };
			self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
		}
	}

	fn new_events(&mut self, _event_loop: &dyn ActiveEventLoop, cause: winit::event::StartCause) {
		if let StartCause::ResumeTimeReached { .. } = cause
			&& let Some(window) = &self.window
		{
			window.request_redraw();
		}
	}

	fn about_to_wait(&mut self, event_loop: &dyn ActiveEventLoop) {
		event_loop.set_control_flow(ControlFlow::WaitUntil(Instant::now() + Duration::from_millis(10)));
	}
}

pub(crate) enum ExitReason {
	Shutdown,
	Restart,
	UiAccelerationFailure,
}
