//! Splash binary the daemon execs in place of the user's session command.
//!
//! Flow:
//! 1. Open the user's VT (`/dev/tty<XDG_VTNR>`).
//! 2. Connect to `LOGINMANAGER_SESSION_SOCK` and write `0x01`. This is the
//!    "ready" signal the daemon waits on before issuing `Seat.SwitchTo`.
//! 3. `ioctl(VT_WAITACTIVE)` on the tty fd. The kernel parks us until our
//!    VT is the foreground — solves the chicken/egg the option-B fix
//!    couldn't on its own (DRM master requires being the active VT, but
//!    being the active VT was gated on signalling "painted", which needed
//!    master). With `VT_WAITACTIVE` the splash signals first, *then* gets
//!    woken with master already available.
//! 4. Open `/dev/dri/card0`, take DRM master, set a CRTC and run a
//!    page-flipped fade-in + pulse on the brand logo for ~800 ms.
//! 5. Drop everything and `execv` the real session command (which is
//!    `crustace-session` → `forge` → `crustace`). On exec, our DRM fd
//!    closes, the kernel releases master, and forge's libseat session
//!    picks it back up immediately.
//!
//! Every step after step 2 is best-effort: if anything fails (missing
//! logo, no display, master refused, kernel without VT_WAITACTIVE, …) the
//! binary falls through to the `execv` so the user always gets their
//! session, just without the splash.

mod drm;
mod render;

use anyhow::{ Context, Result };
use std::ffi::CString;
use std::io::Write;
use std::os::unix::net::UnixStream;
use std::path::{ Path, PathBuf };
use std::time::{ Duration, Instant };

const FB_BG_R: u8 = 0x10;
const FB_BG_G: u8 = 0x10;
const FB_BG_B: u8 = 0x10;

fn main() -> !
{
	tracing_subscriber::fmt().with_target( false ).init();

	let args: Vec<String> = std::env::args().collect();
	if args.len() < 3
	{
		tracing::error!(
			"usage: loginmanager-splash <logo.svg> <real-session-command>"
		);
		std::process::exit( 2 );
	}
	let logo_path    = PathBuf::from( &args[ 1 ] );
	let real_session = PathBuf::from( &args[ 2 ] );

	// Signal-ready + VT_WAITACTIVE happen regardless; only the actual
	// rendering is allowed to fail-soft.
	signal_and_wait_active();

	if let Err( e ) = run_splash( &logo_path )
	{
		tracing::warn!( "splash skipped: {e:#}" );
	}

	exec_real_session( &real_session );
}

/// Hand-off with the daemon. The splash has no privileged ops of its own:
/// it just sends a byte to say "I'm up", then blocks on a read until the
/// daemon writes back an ack. The daemon's ack means the daemon has done
/// the VT switch (and confirmed it via `VT_WAITACTIVE` on `/dev/tty0`, a
/// privilege the splash doesn't have), so by the time `read` returns we
/// are the foreground console and the previous master has dropped — the
/// next `open(/dev/dri/card0)` becomes implicit master without any ioctl.
///
/// If the daemon closes the socket without sending the ack (read returns
/// 0 bytes), we proceed anyway — the splash will likely fail at `set_crtc`
/// and fall through to `execv` of the real session.
fn signal_and_wait_active()
{
	let sock_path = std::env::var( "LOGINMANAGER_SESSION_SOCK" ).ok();

	let Some( p ) = sock_path else
	{
		tracing::warn!( "splash: LOGINMANAGER_SESSION_SOCK not set, skipping handshake" );
		return;
	};

	let mut stream = match UnixStream::connect( &p )
	{
		Ok( s ) => s,
		Err( e ) =>
		{
			tracing::warn!( "splash: connect {p}: {e}" );
			return;
		}
	};

	if let Err( e ) = stream.write_all( &[ 1u8 ] )
	{
		tracing::warn!( "splash: write ready byte: {e}" );
		return;
	}
	tracing::info!( "splash: signalled ready on {p}, waiting for daemon ack" );

	let mut ack = [ 0u8; 1 ];
	match std::io::Read::read_exact( &mut stream, &mut ack )
	{
		Ok( () ) if ack[0] == 1 =>
		{
			tracing::info!( "splash: got daemon ack, VT is ours" );
		}
		Ok( () ) =>
		{
			tracing::warn!( "splash: unexpected ack byte {:#x}, proceeding anyway", ack[0] );
		}
		Err( e ) =>
		{
			tracing::warn!( "splash: no ack from daemon ({e}), proceeding anyway" );
		}
	}
}

/// Open DRM, take master, decode the SVG, drive ~800 ms of fade-in + pulse.
fn run_splash( logo_path: &Path ) -> Result<()>
{
	let card_path = Path::new( "/dev/dri/card0" );
	let card      = drm::Card::open( card_path )?;

	let output = drm::pick_output( &card )?;
	tracing::info!(
		"splash: scanout {}x{} on connector {:?}",
		output.width, output.height, output.connector,
	);

	let fb_w = output.width;
	let fb_h = output.height;

	let mut chain = drm::FlipChain::new( &card, output )?;
	chain.arm( &card )?;

	// Decode logo at ~25 % of the smaller screen dimension so it reads
	// well on phones and laptops without bespoke variants.
	let max_edge = ( fb_w.min( fb_h ) / 4 ).max( 96 );
	let logo     = render::decode_svg( logo_path, max_edge )?;

	// Cache the staging pixmap across frames — at 1920×1080 each frame is
	// ~8 MiB, allocating per-frame at 60 fps wastes 384 MiB per second.
	let mut tmp = resvg::tiny_skia::Pixmap::new( fb_w, fb_h )
		.context( "tiny_skia::Pixmap::new (staging)" )?;

	let timeline = render::Timeline::default_short();
	let start    = Instant::now();
	let bg       = resvg::tiny_skia::Color::from_rgba8( FB_BG_R, FB_BG_G, FB_BG_B, 0xFF );

	while start.elapsed() < timeline.duration
	{
		let ( alpha, scale ) = timeline.at( start.elapsed() );
		render::paint_pixmap( &mut tmp.as_mut(), bg, &logo, alpha, scale );

		let src = tmp.data();
		chain.render_back( &card, |dst, pitch|
		{
			drm::blit_rgba_to_xrgb(
				src, dst,
				fb_w, fb_h,
				( fb_w as usize ) * 4, pitch,
			);
		} )?;
		chain.flip_and_wait( &card, Duration::from_millis( 500 ) )?;
	}

	// Leave the last frame on the CRTC. When we `execv` the next process
	// the DRM fd closes, master drops, but the framebuffer stays scanned
	// out until forge issues its own SetCrtc — so the user sees the brand
	// mark frozen for the few hundred ms forge needs to come up, instead
	// of black.
	Ok( () )
}

/// `execv` the real session command, replacing this process. The argv we
/// hand it is just `[basename]`, matching what the daemon used to pass
/// directly via its own `execl`.
fn exec_real_session( real_session: &Path ) -> !
{
	let cmd  = CString::new( real_session.as_os_str().as_encoded_bytes() )
		.expect( "real session path contains NUL" );
	let arg0 = CString::new(
		real_session
			.file_name()
			.and_then( |s| s.to_str() )
			.unwrap_or( "session" ),
	).expect( "session basename contains NUL" );

	unsafe { libc::execl( cmd.as_ptr(), arg0.as_ptr(), std::ptr::null::<libc::c_char>() ) };

	let e = std::io::Error::last_os_error();
	tracing::error!( "execv {}: {e}", real_session.display() );
	std::process::exit( 127 );
}
