// SPDX-License-Identifier: AGPL-3.0-or-later

use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::Duration;

use parking_lot::{Condvar, Mutex};

use fluxer_encoder_ring::d3d11::D3D11SharedHandle;
use fluxer_encoder_ring::{
    D3D11KeyedMutexBackend, EncoderFrameRate, EncoderInputRing, EncoderReady, RING_SIZE, RingError,
    TextureFormat,
};

#[cfg(target_os = "windows")]
use fluxer_encoder_ring::{EncoderDims, FillReservation, NvencD3D11Handoff};

#[cfg(target_os = "windows")]
use windows::Win32::Graphics::Direct3D11::{ID3D11DeviceContext, ID3D11Texture2D};

pub const READY_WAIT_TIMEOUT_CAP: Duration = Duration::from_millis(17);

const EVICTIONS_PER_RESERVE_MAX: u64 = 2;

#[cfg(target_os = "windows")]
const NVENC_BITRATE_BPS_DEFAULT: u32 = 8_000_000;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EncoderAttachError {
    AlreadyAttached,
    InvalidDimensions { width: u32, height: u32 },
    RingInitFailed,
    NotAttached,
    BlitFailed,
    SlotUnavailable,
    DeviceUnavailable,
}

impl std::fmt::Display for EncoderAttachError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::AlreadyAttached => write!(f, "encoder ring already attached"),
            Self::InvalidDimensions { width, height } => {
                write!(f, "encoder ring invalid dimensions {width}x{height}")
            }
            Self::RingInitFailed => write!(f, "encoder ring initialise failed"),
            Self::NotAttached => write!(f, "encoder ring not attached"),
            Self::BlitFailed => write!(f, "blit into ring slot failed"),
            Self::SlotUnavailable => write!(f, "no ring slot texture available"),
            Self::DeviceUnavailable => write!(f, "ring backend device unavailable"),
        }
    }
}

impl std::error::Error for EncoderAttachError {}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct EncoderAttachStats {
    pub frames_submitted: u64,
    pub frames_dropped: u64,
    pub ring_full_events: u64,
    pub failed_blits: u64,
}

pub struct EncoderAttachment {
    ring: Mutex<EncoderInputRing<D3D11KeyedMutexBackend>>,
    ready_condvar: Condvar,
    attached: AtomicBool,
    frames_submitted: AtomicU64,
    frames_dropped: AtomicU64,
    ring_full_events: AtomicU64,
    failed_blits: AtomicU64,
    width: u32,
    height: u32,
    frame_rate: EncoderFrameRate,
    #[cfg(target_os = "windows")]
    context: ID3D11DeviceContext,
    #[cfg(target_os = "windows")]
    nvenc: Mutex<Option<NvencD3D11Handoff>>,
}

impl EncoderAttachment {
    pub fn try_new(width: u32, height: u32) -> Result<Arc<Self>, EncoderAttachError> {
        Self::try_new_with_frame_rate(width, height, EncoderFrameRate::default())
    }

    pub fn try_new_with_frame_rate(
        width: u32,
        height: u32,
        frame_rate: EncoderFrameRate,
    ) -> Result<Arc<Self>, EncoderAttachError> {
        if width == 0 || height == 0 {
            return Err(EncoderAttachError::InvalidDimensions { width, height });
        }
        assert!(frame_rate.numerator > 0, "frame rate numerator positive");
        assert!(
            frame_rate.denominator > 0,
            "frame rate denominator positive"
        );
        let backend =
            D3D11KeyedMutexBackend::new().map_err(|_| EncoderAttachError::RingInitFailed)?;
        let mut ring = EncoderInputRing::new(backend);
        ring.initialise(width, height, TextureFormat::Nv12)
            .map_err(|_| EncoderAttachError::RingInitFailed)?;
        #[cfg(target_os = "windows")]
        let context = ring
            .backend_mut()
            .context()
            .ok_or(EncoderAttachError::DeviceUnavailable)?;
        #[cfg(target_os = "windows")]
        let nvenc = init_nvenc_pre_registered(&mut ring, width, height, frame_rate);
        let attachment = Self {
            ring: Mutex::new(ring),
            ready_condvar: Condvar::new(),
            attached: AtomicBool::new(true),
            frames_submitted: AtomicU64::new(0),
            frames_dropped: AtomicU64::new(0),
            ring_full_events: AtomicU64::new(0),
            failed_blits: AtomicU64::new(0),
            width,
            height,
            frame_rate,
            #[cfg(target_os = "windows")]
            context,
            #[cfg(target_os = "windows")]
            nvenc: Mutex::new(nvenc),
        };
        assert!(
            attachment.attached.load(Ordering::Acquire),
            "attachment is attached"
        );
        assert!(attachment.width > 0, "attachment width positive");
        assert!(
            attachment.frame_rate.numerator > 0,
            "attachment fps positive"
        );
        Ok(Arc::new(attachment))
    }

    pub fn width(&self) -> u32 {
        let w = self.width;
        assert!(w > 0, "attachment width positive");
        assert!(self.height > 0, "attachment height positive");
        w
    }

    pub fn height(&self) -> u32 {
        let h = self.height;
        assert!(h > 0, "attachment height positive");
        assert!(self.width > 0, "attachment width positive");
        h
    }

    pub fn frame_rate(&self) -> EncoderFrameRate {
        let rate = self.frame_rate;
        assert!(rate.numerator > 0, "attachment fps numerator positive");
        assert!(rate.denominator > 0, "attachment fps denominator positive");
        rate
    }

    pub fn is_attached(&self) -> bool {
        let a = self.attached.load(Ordering::Acquire);
        assert!(self.width > 0, "width intact while reading attached");
        assert!(self.height > 0, "height intact while reading attached");
        a
    }

    pub fn detach(&self) {
        self.attached.store(false, Ordering::Release);
        self.ready_condvar.notify_all();
        assert!(
            !self.attached.load(Ordering::Acquire),
            "detach flag cleared"
        );
        assert!(self.width > 0, "attachment intact across detach");
    }

    pub fn capacity(&self) -> usize {
        let cap = RING_SIZE;
        assert!(cap > 0, "ring capacity positive");
        assert_eq!(cap, 8, "ring capacity matches RING_SIZE");
        cap
    }

    pub fn note_ring_full(&self) {
        self.ring_full_events.fetch_add(1, Ordering::Relaxed);
    }

    #[cfg(target_os = "windows")]
    pub fn nvenc_pre_registered(&self) -> bool {
        let registered = self.nvenc.lock().is_some();
        assert!(self.width > 0, "width intact while reading nvenc state");
        assert!(self.height > 0, "height intact while reading nvenc state");
        registered
    }

    pub fn stats(&self) -> EncoderAttachStats {
        let stats = EncoderAttachStats {
            frames_submitted: self.frames_submitted.load(Ordering::Relaxed),
            frames_dropped: self.frames_dropped.load(Ordering::Relaxed),
            ring_full_events: self.ring_full_events.load(Ordering::Relaxed),
            failed_blits: self.failed_blits.load(Ordering::Relaxed),
        };
        assert!(
            stats.frames_submitted <= u64::MAX / 2,
            "submitted plausible"
        );
        assert!(stats.frames_dropped <= u64::MAX / 2, "dropped plausible");
        stats
    }

    fn account_evictions(&self, evicted_delta: u64) {
        assert!(
            evicted_delta <= EVICTIONS_PER_RESERVE_MAX,
            "evictions bounded per reserve"
        );
        assert!(self.width > 0, "attachment intact while accounting");
        if evicted_delta == 0 {
            return;
        }
        self.frames_dropped
            .fetch_add(evicted_delta, Ordering::Relaxed);
        self.ring_full_events
            .fetch_add(evicted_delta, Ordering::Relaxed);
    }

    pub fn submit_notify(&self) -> Result<(), EncoderAttachError> {
        if !self.attached.load(Ordering::Acquire) {
            return Err(EncoderAttachError::NotAttached);
        }
        let mut ring = self.ring.lock();
        let pre_dropped = ring.dropped_count();
        let result: Result<(), RingError> = ring.submit_skip_oldest(|_handle| {});
        let evicted_delta = ring.dropped_count().saturating_sub(pre_dropped);
        drop(ring);
        self.account_evictions(evicted_delta);
        match result {
            Ok(()) => {
                self.frames_submitted.fetch_add(1, Ordering::Relaxed);
                self.ready_condvar.notify_one();
                Ok(())
            }
            Err(RingError::FullDropped { .. }) => Ok(()),
            Err(_) => Err(EncoderAttachError::RingInitFailed),
        }
    }

    pub fn wait_next_ready(&self, timeout: Duration) -> Option<EncoderReady<D3D11SharedHandle>> {
        assert!(self.width > 0, "attachment width positive");
        assert!(self.height > 0, "attachment height positive");
        if !self.attached.load(Ordering::Acquire) {
            return None;
        }
        let capped_timeout = timeout.min(READY_WAIT_TIMEOUT_CAP);
        let mut ring = self.ring.lock();
        if let Some(ready) = ring.poll_next_ready() {
            return Some(ready);
        }
        let _ = self.ready_condvar.wait_for(&mut ring, capped_timeout);
        if !self.attached.load(Ordering::Acquire) {
            return None;
        }
        ring.poll_next_ready()
    }

    #[cfg(target_os = "windows")]
    fn cancel_reservation(&self, reservation: FillReservation<D3D11SharedHandle>) {
        assert!(
            (reservation.slot_index() as usize) < RING_SIZE,
            "cancelled slot within ring"
        );
        assert!(self.width > 0, "attachment intact while cancelling");
        let mut ring = self.ring.lock();
        let _ = ring.cancel(reservation);
    }

    #[cfg(target_os = "windows")]
    pub fn submit_capture_frame_with_blit(
        &self,
        capture_texture: &ID3D11Texture2D,
        capture_width: u32,
        capture_height: u32,
    ) -> Result<(), EncoderAttachError> {
        assert!(capture_width > 0, "capture width positive");
        assert!(capture_height > 0, "capture height positive");
        assert!(self.width > 0, "attachment width positive");
        assert!(self.height > 0, "attachment height positive");
        if !self.attached.load(Ordering::Acquire) {
            return Err(EncoderAttachError::NotAttached);
        }
        let mut ring = self.ring.lock();
        let pre_dropped = ring.dropped_count();
        let reserved = ring.reserve_skip_oldest();
        let evicted_delta = ring.dropped_count().saturating_sub(pre_dropped);
        let dest_texture = match &reserved {
            Ok(reservation) => ring
                .backend_mut()
                .texture_for_slot(reservation.slot_index()),
            Err(_) => None,
        };
        drop(ring);
        self.account_evictions(evicted_delta);
        let reservation = match reserved {
            Ok(reservation) => reservation,
            Err(RingError::FullDropped { .. }) => return Ok(()),
            Err(_) => return Err(EncoderAttachError::RingInitFailed),
        };
        let Some(dest_texture) = dest_texture else {
            self.cancel_reservation(reservation);
            self.failed_blits.fetch_add(1, Ordering::Relaxed);
            return Err(EncoderAttachError::BlitFailed);
        };
        unsafe {
            self.context.CopyResource(&dest_texture, capture_texture);
        }
        let mut ring = self.ring.lock();
        match ring.commit(reservation) {
            Ok(sequence) => {
                drop(ring);
                assert!(sequence > 0, "committed sequence positive");
                self.frames_submitted.fetch_add(1, Ordering::Relaxed);
                self.ready_condvar.notify_one();
                Ok(())
            }
            Err(_) => Err(EncoderAttachError::RingInitFailed),
        }
    }
}

#[cfg(target_os = "windows")]
fn collect_ring_slot_handles(
    ring: &mut EncoderInputRing<D3D11KeyedMutexBackend>,
) -> Option<Vec<D3D11SharedHandle>> {
    assert_eq!(
        ring.free_count(),
        RING_SIZE,
        "collect requires a fresh ring"
    );
    let mut reservations: Vec<FillReservation<D3D11SharedHandle>> = Vec::with_capacity(RING_SIZE);
    let mut handles: Vec<D3D11SharedHandle> = Vec::with_capacity(RING_SIZE);
    for _ in 0..RING_SIZE {
        let Ok(reservation) = ring.reserve() else {
            break;
        };
        handles.push(reservation.handle.clone());
        reservations.push(reservation);
    }
    let mut all_cancelled = true;
    for reservation in reservations.drain(..) {
        if ring.cancel(reservation).is_err() {
            all_cancelled = false;
        }
    }
    if !all_cancelled {
        return None;
    }
    assert_eq!(ring.free_count(), RING_SIZE, "all reservations returned");
    assert!(handles.len() <= RING_SIZE, "collected handles bounded");
    if handles.len() == RING_SIZE {
        Some(handles)
    } else {
        None
    }
}

#[cfg(target_os = "windows")]
fn init_nvenc_pre_registered(
    ring: &mut EncoderInputRing<D3D11KeyedMutexBackend>,
    width: u32,
    height: u32,
    frame_rate: EncoderFrameRate,
) -> Option<NvencD3D11Handoff> {
    assert!(width > 0, "nvenc init width positive");
    assert!(height > 0, "nvenc init height positive");
    assert!(frame_rate.numerator > 0, "nvenc frame rate positive");
    let device = ring.backend_mut().device()?;
    let handles = collect_ring_slot_handles(ring)?;
    assert_eq!(handles.len(), RING_SIZE, "pre-register covers whole ring");
    let dims = EncoderDims::new(width, height);
    let mut handoff =
        NvencD3D11Handoff::new_with_frame_rate(device, dims, NVENC_BITRATE_BPS_DEFAULT, frame_rate)
            .ok()?;
    handoff.pre_register_slots(&handles, dims).ok()?;
    Some(handoff)
}

impl Drop for EncoderAttachment {
    fn drop(&mut self) {
        self.attached.store(false, Ordering::Release);
        self.ready_condvar.notify_all();
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn rejects_zero_dimensions() {
        let err = EncoderAttachment::try_new(0, 720).err();
        assert!(matches!(
            err,
            Some(EncoderAttachError::InvalidDimensions { .. })
        ));
        let err2 = EncoderAttachment::try_new(640, 0).err();
        assert!(matches!(
            err2,
            Some(EncoderAttachError::InvalidDimensions { .. })
        ));
    }

    #[test]
    fn ready_wait_timeout_cap_is_one_frame_interval() {
        assert!(READY_WAIT_TIMEOUT_CAP >= Duration::from_millis(1));
        assert!(READY_WAIT_TIMEOUT_CAP <= Duration::from_millis(33));
    }

    #[cfg(not(target_os = "windows"))]
    #[test]
    fn off_windows_init_fails() {
        let err = EncoderAttachment::try_new(640, 480).err();
        assert!(matches!(err, Some(EncoderAttachError::RingInitFailed)));
    }

    #[cfg(target_os = "windows")]
    #[test]
    fn windows_attach_then_detach_smoke() {
        let attach = match EncoderAttachment::try_new(640, 480) {
            Ok(a) => a,
            Err(_) => return,
        };
        assert!(attach.is_attached());
        assert_eq!(attach.width(), 640);
        assert_eq!(attach.height(), 480);
        assert_eq!(attach.capacity(), 8);
        let stats = attach.stats();
        assert_eq!(stats.frames_submitted, 0, "fresh attach has 0 submissions");
        assert_eq!(stats.failed_blits, 0, "fresh attach has 0 failed blits");
        attach.detach();
        assert!(!attach.is_attached());
    }

    #[cfg(target_os = "windows")]
    #[test]
    fn windows_submit_notify_records_stats() {
        let attach = match EncoderAttachment::try_new(64, 64) {
            Ok(a) => a,
            Err(_) => return,
        };
        for _ in 0..3 {
            attach.submit_notify().expect("notify ok");
        }
        let stats = attach.stats();
        assert_eq!(stats.frames_submitted, 3);
        assert_eq!(stats.frames_dropped, 0);
        assert_eq!(stats.ring_full_events, 0);
    }

    #[cfg(target_os = "windows")]
    #[test]
    fn windows_submit_rejected_when_detached() {
        let attach = match EncoderAttachment::try_new(64, 64) {
            Ok(a) => a,
            Err(_) => return,
        };
        attach.detach();
        let err = attach.submit_notify().err();
        assert!(matches!(err, Some(EncoderAttachError::NotAttached)));
    }

    #[cfg(target_os = "windows")]
    #[test]
    fn windows_wait_next_ready_returns_committed_frame() {
        let attach = match EncoderAttachment::try_new(64, 64) {
            Ok(a) => a,
            Err(_) => return,
        };
        attach.submit_notify().expect("notify ok");
        let ready = attach.wait_next_ready(Duration::from_millis(5));
        let ready = ready.expect("committed frame becomes ready");
        assert_eq!(ready.sequence, 1);
        assert_eq!(ready.duplicate_count, 0);
    }

    #[cfg(target_os = "windows")]
    #[test]
    fn windows_wait_next_ready_empty_ring_times_out() {
        let attach = match EncoderAttachment::try_new(64, 64) {
            Ok(a) => a,
            Err(_) => return,
        };
        let ready = attach.wait_next_ready(Duration::from_millis(2));
        assert!(ready.is_none());
        assert!(attach.is_attached());
    }

    #[cfg(target_os = "windows")]
    #[test]
    fn windows_wait_next_ready_after_detach_returns_none() {
        let attach = match EncoderAttachment::try_new(64, 64) {
            Ok(a) => a,
            Err(_) => return,
        };
        attach.detach();
        let ready = attach.wait_next_ready(Duration::from_millis(2));
        assert!(ready.is_none());
        assert!(!attach.is_attached());
    }

    #[cfg(target_os = "windows")]
    #[test]
    fn windows_ring_usable_after_nvenc_pre_register_attempt() {
        let attach = match EncoderAttachment::try_new(64, 64) {
            Ok(a) => a,
            Err(_) => return,
        };
        let _ = attach.nvenc_pre_registered();
        attach.submit_notify().expect("notify ok");
        let stats = attach.stats();
        assert_eq!(stats.frames_submitted, 1);
        assert_eq!(stats.frames_dropped, 0);
    }
}
