//! Minimal direct-DRM scanout: pick the first connected output, allocate
//! two XRGB8888 dumb buffers, run a page-flip loop that lets `render.rs`
//! paint the back buffer while the front is on screen.
//!
//! The splash never atomic-modesets after init — it just keeps the same
//! mode/CRTC pair and rotates between two FBs. When `Card` drops, the fd
//! is closed and the kernel auto-releases DRM master, so the next process
//! (forge, via libseat) can take over cleanly.

use anyhow::{ bail, Context, Result };
use drm::buffer::{ Buffer, DrmFourcc };
use drm::control::
{
	connector, crtc, dumbbuffer::DumbBuffer, framebuffer, Mode, PageFlipFlags,
	Device as ControlDevice,
};
use drm::Device;
use std::fs::{ File, OpenOptions };
use std::os::fd::{ AsFd, AsRawFd, BorrowedFd };
use std::os::unix::fs::OpenOptionsExt;
use std::path::Path;
use std::time::{ Duration, Instant };

/// Owning DRM device wrapper. `Drop` closes the fd, which drops master.
pub struct Card
{
	file: File,
}

impl AsFd for Card
{
	fn as_fd( &self ) -> BorrowedFd<'_> { self.file.as_fd() }
}

impl Device for Card {}
impl ControlDevice for Card {}

impl Card
{
	pub fn open( path: &Path ) -> Result<Self>
	{
		let file = OpenOptions::new()
			.read( true )
			.write( true )
			.custom_flags( libc::O_CLOEXEC )
			.open( path )
			.with_context( || format!( "open {}", path.display() ) )?;
		Ok( Card { file } )
	}

	// No explicit `become_master`: `DRM_IOCTL_SET_MASTER` is `DRM_ROOT_ONLY`
	// in 6.x kernels and returns -EACCES for unprivileged callers. Instead
	// we rely on the kernel's implicit master rule: when a process opens
	// `/dev/dri/cardN` and `dev->master == NULL`, that open transparently
	// becomes master — no ioctl, no cap check. The greeter's forge releases
	// master via `drmDropMaster` on its libseat `PauseSession` (which fires
	// before our `VT_WAITACTIVE` returns), leaving the slot empty for us.
	// If we land here without master anyway (race, or libseat misbehaving),
	// the first `set_crtc` below will fail loudly with EPERM and the splash
	// skips rendering — handled by the same `?` chain that catches every
	// other DRM error.
}

/// Resolved scanout target: connector + crtc + mode + dimensions.
pub struct Output
{
	pub connector: connector::Handle,
	pub crtc:      crtc::Handle,
	pub mode:      Mode,
	pub width:     u32,
	pub height:    u32,
}

/// Walk all connectors, return the first one in `Connected` state along
/// with a valid mode and a CRTC that can drive it. Modes are taken in the
/// order the kernel reports them — the first is typically the EDID
/// preferred mode, which is what every modern display wants.
pub fn pick_output( card: &Card ) -> Result<Output>
{
	let resources = card.resource_handles()
		.context( "drmModeGetResources" )?;

	for &conn_handle in resources.connectors().iter()
	{
		let conn = match card.get_connector( conn_handle, false )
		{
			Ok( c )  => c,
			Err( _ ) => continue,
		};
		if conn.state() != connector::State::Connected { continue; }

		let mode = match conn.modes().get( 0 )
		{
			Some( m ) => *m,
			None      => continue,
		};

		let crtc_handle = match resolve_crtc( card, &conn, &resources )?
		{
			Some( c ) => c,
			None      => continue,
		};

		let ( w, h ) = mode.size();
		return Ok( Output
		{
			connector: conn_handle,
			crtc:      crtc_handle,
			mode,
			width:     w as u32,
			height:    h as u32,
		} );
	}

	bail!( "no connected display" )
}

fn resolve_crtc(
	card:      &Card,
	conn:      &connector::Info,
	resources: &drm::control::ResourceHandles,
) -> Result<Option<crtc::Handle>>
{
	// Prefer the encoder/CRTC already wired up by the kernel for this
	// connector. Falls back to enumerating encoders + possible-CRTCs when
	// no current encoder is set (typical right after VT switch).
	if let Some( enc_handle ) = conn.current_encoder()
	{
		if let Ok( enc ) = card.get_encoder( enc_handle )
		{
			if let Some( c ) = enc.crtc() { return Ok( Some( c ) ); }
		}
	}
	for &enc_handle in conn.encoders().iter()
	{
		let enc = match card.get_encoder( enc_handle )
		{
			Ok( e )  => e,
			Err( _ ) => continue,
		};
		let candidates = resources.filter_crtcs( enc.possible_crtcs() );
		if let Some( &c ) = candidates.first()
		{
			return Ok( Some( c ) );
		}
	}
	Ok( None )
}

/// One scanout buffer: backing dumb buffer + the framebuffer object the
/// kernel exposes to the CRTC. We allocate two of these (front + back) at
/// init and ping-pong between them.
pub struct Surface
{
	pub fb:   framebuffer::Handle,
	pub dumb: DumbBuffer,
}

impl Surface
{
	pub fn create( card: &Card, width: u32, height: u32 ) -> Result<Self>
	{
		let dumb = card
			.create_dumb_buffer( ( width, height ), DrmFourcc::Xrgb8888, 32 )
			.context( "DRM_IOCTL_MODE_CREATE_DUMB" )?;
		let fb = card
			.add_framebuffer( &dumb, 24, 32 )
			.context( "drmModeAddFB" )?;
		Ok( Surface { fb, dumb } )
	}
}

/// Page-flip helper: drives the swap between two `Surface`s and the
/// vblank wait. Owns the two surfaces and the front/back index so the
/// caller only sees a `back_buffer_mut()` slot to paint.
pub struct FlipChain
{
	pub output:  Output,
	surfaces:    [ Surface; 2 ],
	front:       usize,
}

impl FlipChain
{
	pub fn new( card: &Card, output: Output ) -> Result<Self>
	{
		let a = Surface::create( card, output.width, output.height )?;
		let b = Surface::create( card, output.width, output.height )?;
		Ok( FlipChain
		{
			output,
			surfaces: [ a, b ],
			front:    0,
		} )
	}

	/// Initial set: present `surfaces[0]` so the first page-flip already
	/// has a valid front to flip from.
	pub fn arm( &self, card: &Card ) -> Result<()>
	{
		card.set_crtc(
			self.output.crtc,
			Some( self.surfaces[ self.front ].fb ),
			( 0, 0 ),
			&[ self.output.connector ],
			Some( self.output.mode ),
		).context( "drmModeSetCrtc" )?;
		Ok( () )
	}

	/// Render into the back buffer via the supplied closure, then page-flip
	/// to it and block for vsync. The closure receives the mapped bytes and
	/// the buffer pitch (bytes per row, possibly larger than `width × 4`).
	pub fn render_back<F>( &mut self, card: &Card, paint: F ) -> Result<()>
	where
		F: FnOnce( &mut [u8], usize ),
	{
		let back  = 1 - self.front;
		let pitch = self.surfaces[ back ].dumb.pitch() as usize;
		let mut map = card
			.map_dumb_buffer( &mut self.surfaces[ back ].dumb )
			.context( "DRM_IOCTL_MODE_MAP_DUMB" )?;
		paint( map.as_mut(), pitch );
		Ok( () )
	}

	/// Request a page-flip to the back buffer and block until the kernel
	/// reports the flip complete via a vblank event on the DRM fd.
	pub fn flip_and_wait( &mut self, card: &Card, timeout: Duration ) -> Result<()>
	{
		let back = 1 - self.front;
		card.page_flip(
			self.output.crtc,
			self.surfaces[ back ].fb,
			PageFlipFlags::EVENT,
			None,
		).context( "drmModePageFlip" )?;

		wait_flip_event( card, timeout )?;
		self.front = back;
		Ok( () )
	}

}

/// Block on the DRM fd until at least one `PageFlip` event arrives or the
/// timeout elapses. `poll` is used directly rather than `receive_events`'s
/// blocking-read so we have a deadline against the kernel never firing
/// vblank (e.g. CRTC disabled mid-flip).
fn wait_flip_event( card: &Card, timeout: Duration ) -> Result<()>
{
	let raw_fd = card.as_fd().as_raw_fd();
	let deadline = Instant::now() + timeout;

	loop
	{
		let remaining = match deadline.checked_duration_since( Instant::now() )
		{
			Some( d ) => d,
			None      => bail!( "page-flip event timeout" ),
		};
		let ms = remaining.as_millis().min( i32::MAX as u128 ) as i32;

		let mut pfd = libc::pollfd
		{
			fd:      raw_fd,
			events:  libc::POLLIN,
			revents: 0,
		};
		let rc = unsafe { libc::poll( &mut pfd, 1, ms ) };
		if rc < 0
		{
			let e = std::io::Error::last_os_error();
			if e.raw_os_error() == Some( libc::EINTR ) { continue; }
			bail!( "poll(drm_fd): {e}" );
		}
		if rc == 0 { bail!( "page-flip event timeout" ); }

		let events = card.receive_events().context( "drmHandleEvent" )?;
		for ev in events
		{
			if let drm::control::Event::PageFlip( _ ) = ev
			{
				return Ok( () );
			}
		}
		// Spurious wake (other event type) — keep waiting.
	}
}

/// Copy a slice of RGBA8 pre-multiplied bytes (tiny-skia's native layout)
/// into a DRM dumb buffer mapped as XRGB8888 little-endian. Strides may
/// differ — tiny-skia's row stride is `w * 4` but the dumb buffer's pitch
/// can be padded to a power of two.
pub fn blit_rgba_to_xrgb(
	src:        &[u8],
	dst:        &mut [u8],
	w:          u32,
	h:          u32,
	src_stride: usize,
	dst_stride: usize,
)
{
	let row_bytes = ( w as usize ) * 4;
	for y in 0..( h as usize )
	{
		let src_row = &src[ y * src_stride .. y * src_stride + row_bytes ];
		let dst_row = &mut dst[ y * dst_stride .. y * dst_stride + row_bytes ];
		for x in 0..( w as usize )
		{
			let r = src_row[ x * 4 + 0 ];
			let g = src_row[ x * 4 + 1 ];
			let b = src_row[ x * 4 + 2 ];
			dst_row[ x * 4 + 0 ] = b;
			dst_row[ x * 4 + 1 ] = g;
			dst_row[ x * 4 + 2 ] = r;
			dst_row[ x * 4 + 3 ] = 0xFF;
		}
	}
}
