use cef::args::Args;
use cef::sys::{CEF_API_VERSION_LAST, cef_log_severity_t, cef_thread_id_t};
use cef::{
	App, Browser, BrowserSettings, CefString, Client, DictionaryValue, ImplBrowser, ImplBrowserHost, ImplCommandLine, ImplRequestContext, LogSeverity, RequestContextSettings, SchemeHandlerFactory,
	Settings, Task, ThreadId, WindowInfo, api_hash, browser_host_create_browser_sync, execute_process, post_task,
};
use std::cell::RefCell;
use std::marker::PhantomData;
use std::path::Path;
use std::sync::mpsc::Sender;

use crate::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME, WINDOWLESS_FRAME_RATE};
use crate::delegate::BrowserDelegate;
use crate::dirs::TempDir;
use crate::frames::FrameStreamer;
use crate::input::{self, InputEvent};
use crate::internal::task::ClosureTask;
use crate::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderProcessAppImpl, SchemeHandlerFactoryImpl};
use crate::ipc::{MessageType, SendMessage};
use crate::view::ViewInfoUpdate;

thread_local! {
	static CONTEXT: RefCell<Option<BrowserContext>> = const { RefCell::new(None) };
}

pub(crate) struct CefContext {
	_not_send: PhantomData<*const ()>, // impl !Send for CefContext
}

impl CefContext {
	pub(crate) fn create(delegate: BrowserDelegate, frames: FrameStreamer, view_info_sender: Sender<ViewInfoUpdate>, accelerated_paint: bool) -> Result<Self, InitError> {
		let args = bootstrap(false);
		#[cfg(target_os = "macos")]
		crate::platform::mac::install_application();

		let instance_dir = TempDir::new().map_err(|e| InitError::InstanceDirectoryCreationFailed(e.to_string()))?;
		initialize(&args, instance_dir.as_ref(), accelerated_paint)?;

		let (created_tx, created_rx) = std::sync::mpsc::channel();
		let install_browser = move || {
			let result = create_browser(delegate, frames, view_info_sender, instance_dir, accelerated_paint).map(|context| CONTEXT.with(|b| *b.borrow_mut() = Some(context)));
			let _ = created_tx.send(result);
		};
		#[cfg(target_os = "macos")]
		install_browser();
		#[cfg(not(target_os = "macos"))]
		run_on_ui_thread(install_browser);

		created_rx.recv().unwrap_or(Err(InitError::BrowserCreationFailed))?;
		Ok(Self { _not_send: PhantomData })
	}

	#[cfg(not(target_os = "macos"))]
	pub(crate) fn run<R: Send + 'static>(self, control: impl FnOnce(CefContextHandle) -> R + Send + 'static) -> R {
		let result = control(CefContextHandle);
		let (dropped_sender, dropped_receiver) = std::sync::mpsc::channel();
		run_on_ui_thread(move || {
			drop(CONTEXT.take());
			let _ = dropped_sender.send(());
		});
		let _ = dropped_receiver.recv();
		cef::shutdown();
		result
	}

	#[cfg(target_os = "macos")]
	pub(crate) fn run<R: Send + 'static>(self, control: impl FnOnce(CefContextHandle) -> R + Send + 'static) -> R {
		let (result_sender, result_receiver) = std::sync::mpsc::channel();
		let control_thread = std::thread::Builder::new()
			.name("cef-host-control".to_string())
			.spawn(move || {
				let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| control(CefContextHandle)));
				with_context(|context| {
					if let Some(host) = context.browser.host() {
						host.close_browser(1);
					}
				});
				run_on_ui_thread(cef::quit_message_loop);
				match result {
					Ok(result) => {
						let _ = result_sender.send(result);
					}
					Err(panic) => std::panic::resume_unwind(panic),
				}
			})
			.expect("Failed to spawn the CEF control thread");
		cef::run_message_loop();
		drop(CONTEXT.take());
		cef::shutdown();
		if let Err(panic) = control_thread.join() {
			std::panic::resume_unwind(panic);
		}
		result_receiver.recv().expect("The CEF control thread ended without a result")
	}
}

pub(crate) fn execute_helper_process() -> std::process::ExitCode {
	let args = bootstrap(true);
	assert_eq!(args.as_cmd_line().unwrap().has_switch(Some(&"type".into())), 1, "Not a CEF helper process");
	let mut app = RenderProcessAppImpl::app();
	let code = execute_process(Some(args.as_main_args()), Some(&mut app), std::ptr::null_mut());
	std::process::ExitCode::from(code as u8)
}

fn bootstrap(helper: bool) -> Args {
	#[cfg(target_os = "macos")]
	{
		let loader = cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), helper);
		assert!(loader.load());
		// LibraryLoader unloads the framework on drop
		std::mem::forget(loader);
	}
	#[cfg(not(target_os = "macos"))]
	let _ = helper;

	let _ = api_hash(CEF_API_VERSION_LAST, 0);
	Args::new()
}

fn initialize(args: &Args, instance_dir: &Path, accelerated_paint: bool) -> Result<(), InitError> {
	let mut app = App::new(BrowserProcessAppImpl::new(accelerated_paint));
	if cef::initialize(Some(args.as_main_args()), Some(&platform_settings(instance_dir)?), Some(&mut app), std::ptr::null_mut()) != 1 {
		return Err(InitError::InitializationFailureCode(cef::get_exit_code() as u32));
	}
	Ok(())
}

fn platform_settings(instance_dir: &Path) -> Result<Settings, InitError> {
	let log_severity = match std::env::var("GRAPHITE_BROWSER_LOG").unwrap_or_default().to_lowercase().as_str() {
		"debug" => cef_log_severity_t::LOGSEVERITY_VERBOSE,
		"info" => cef_log_severity_t::LOGSEVERITY_INFO,
		"warn" => cef_log_severity_t::LOGSEVERITY_WARNING,
		"error" => cef_log_severity_t::LOGSEVERITY_ERROR,
		"none" => cef_log_severity_t::LOGSEVERITY_DISABLE,
		_ => cef_log_severity_t::LOGSEVERITY_FATAL,
	};

	let Some(root_cache_path) = instance_dir.to_str().map(CefString::from) else {
		return Err(InitError::PathResolutionFailed(format!("non-UTF-8 instance directory path: {}", instance_dir.display())));
	};
	let base = Settings {
		windowless_rendering_enabled: 1,
		root_cache_path,
		cache_path: "".into(),
		disable_signal_handlers: 1,
		log_severity: LogSeverity::from(log_severity),
		..Default::default()
	};

	#[cfg(target_os = "macos")]
	{
		let exe = std::env::current_exe().map_err(|e| InitError::PathResolutionFailed(format!("cannot get current exe path: {e}")))?;
		let app_root = exe
			.parent()
			.and_then(|p| p.parent())
			.and_then(|p| p.parent())
			.ok_or_else(|| InitError::PathResolutionFailed(format!("executable is not inside an app bundle: {}", exe.display())))?;
		let Some(main_bundle_path) = app_root.to_str().map(CefString::from) else {
			return Err(InitError::PathResolutionFailed(format!("invalid app bundle path: {}", app_root.display())));
		};
		Ok(Settings {
			main_bundle_path,
			multi_threaded_message_loop: 0,
			external_message_pump: 0,
			no_sandbox: 1, // GPU helper crashes when running with sandbox
			..base
		})
	}

	#[cfg(not(target_os = "macos"))]
	Ok(Settings {
		multi_threaded_message_loop: 1,
		#[cfg(target_os = "linux")]
		no_sandbox: 1,
		..base
	})
}

fn create_browser(delegate: BrowserDelegate, frames: FrameStreamer, view_info_sender: Sender<ViewInfoUpdate>, instance_dir: TempDir, accelerated_paint: bool) -> Result<BrowserContext, InitError> {
	#[cfg(not(feature = "accelerated_paint"))]
	let _ = accelerated_paint;
	let mut client = Client::new(BrowserProcessClientImpl::new(&delegate, frames));

	let window_info = WindowInfo {
		windowless_rendering_enabled: 1,
		#[cfg(feature = "accelerated_paint")]
		shared_texture_enabled: accelerated_paint as i32,
		..Default::default()
	};

	let settings = BrowserSettings {
		windowless_frame_rate: WINDOWLESS_FRAME_RATE,
		background_color: 0x0,
		..Default::default()
	};

	let Some(mut incognito_request_context) = cef::request_context_create_context(
		Some(&RequestContextSettings {
			persist_session_cookies: 0,
			cache_path: "".into(),
			..Default::default()
		}),
		Option::<&mut cef::RequestContextHandler>::None,
	) else {
		return Err(InitError::RequestContextCreationFailed);
	};

	let mut scheme_handler_factory = SchemeHandlerFactory::new(SchemeHandlerFactoryImpl::new(delegate.clone()));
	incognito_request_context.clear_scheme_handler_factories();
	if incognito_request_context.register_scheme_handler_factory(Some(&RESOURCE_SCHEME.into()), Some(&RESOURCE_DOMAIN.into()), Some(&mut scheme_handler_factory)) != 1 {
		return Err(InitError::SchemeHandlerRegistrationFailed);
	}

	let url = format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/");
	browser_host_create_browser_sync(
		Some(&window_info),
		Some(&mut client),
		Some(&url.as_str().into()),
		Some(&settings),
		Option::<&mut DictionaryValue>::None,
		Some(&mut incognito_request_context),
	)
	.map(|browser| BrowserContext {
		delegate,
		browser,
		view_info_sender,
		_instance_dir: instance_dir,
	})
	.ok_or_else(|| {
		tracing::error!("Failed to create browser");
		InitError::BrowserCreationFailed
	})
}

#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)]
pub(crate) enum InitError {
	#[error("Failed to create the instance directory: {0}")]
	InstanceDirectoryCreationFailed(String),
	#[error("Initialization failed with code: {0}")]
	InitializationFailureCode(u32),
	#[error("Browser creation failed")]
	BrowserCreationFailed,
	#[error("Request context creation failed")]
	RequestContextCreationFailed,
	#[error("Failed to resolve a required path: {0}")]
	PathResolutionFailed(String),
	#[error("Scheme handler registration failed")]
	SchemeHandlerRegistrationFailed,
}

#[derive(Clone)]
pub(crate) struct CefContextHandle;

impl CefContextHandle {
	pub(crate) fn apply_input(&self, events: Vec<InputEvent>) {
		with_context(move |context| {
			for event in &events {
				input::apply(&context.browser, event);
			}
		});
	}

	pub(crate) fn update_view_info(&self, update: ViewInfoUpdate) {
		with_context(move |context| context.update_view_info(update));
	}

	pub(crate) fn refresh_view_info(&self) {
		with_context(|context| context.refresh_view_info());
	}

	pub(crate) fn send_web_message(&self, message: Vec<u8>) {
		with_context(move |context| context.send_web_message(message));
	}
}

struct BrowserContext {
	delegate: BrowserDelegate,
	browser: Browser,
	view_info_sender: Sender<ViewInfoUpdate>,
	_instance_dir: TempDir,
}

impl BrowserContext {
	fn update_view_info(&self, update: ViewInfoUpdate) {
		let _ = self.view_info_sender.send(update);
	}

	fn refresh_view_info(&self) {
		let view_info = self.delegate.view_info();
		let Some(host) = self.browser.host() else {
			tracing::error!("Browser host is not available, cannot refresh view info");
			return;
		};
		host.set_zoom_level(view_info.zoom());
		host.was_resized();

		// Fix for CEF not updating the view after resize
		// TODO: remove once https://github.com/chromiumembedded/cef/issues/3822 is fixed
		host.invalidate(cef::PaintElementType::default());
	}

	fn send_web_message(&self, message: Vec<u8>) {
		self.send_message(MessageType::SendToJS, &message);
	}
}

impl Drop for BrowserContext {
	fn drop(&mut self) {
		tracing::debug!("Shutting down CEF");
		if let Some(host) = self.browser.host() {
			host.close_browser(1);
		} else {
			tracing::error!("Browser host is not available, cannot close browser");
		}
	}
}

impl SendMessage for BrowserContext {
	fn send_message(&self, message_type: MessageType, message: &[u8]) {
		let Some(frame) = self.browser.main_frame() else {
			tracing::error!("Main frame is not available, cannot send message");
			return;
		};
		frame.send_message(message_type, message);
	}
}

fn run_on_ui_thread<F>(closure: F)
where
	F: FnOnce() + Send + 'static,
{
	let closure_task = ClosureTask::new(closure);
	let mut task = Task::new(closure_task);
	if post_task(ThreadId::from(cef_thread_id_t::TID_UI), Some(&mut task)) != 1 {
		tracing::error!("Failed to post a task to the CEF UI thread");
	}
}

fn with_context<F>(closure: F)
where
	F: FnOnce(&mut BrowserContext) + Send + 'static,
{
	run_on_ui_thread(move || {
		CONTEXT.with(|b| {
			if let Some(context) = b.borrow_mut().as_mut() {
				closure(context);
			}
		});
	});
}
