//! Layout transactions and the resize pipeline on `Windows`. A transaction
//! batches configure churn (scale, mode, orientation, output layout changes)
//! and holds presentation until clients ack or a deadline passes; the resize
//! passes re-fit tiled and floating windows against the new work area.

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

use super::*;

const MAX_LOCKED_TRANSACTION_MILLIS: u64 = 100;
const MAX_TRANSACTION_MILLIS: u64 = 1000;

static TRANSACTION_START: AtomicU64 = AtomicU64::new(0);

pub fn start_transaction() {
    if TRANSACTION_START.load(Ordering::Relaxed) != 0 {
        return;
    }

    let now = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
    TRANSACTION_START.store(now, Ordering::Relaxed);
}

#[derive(Debug)]
pub(super) struct Transaction {
    pub(super) view: Option<View>,
}

impl Transaction {
    fn new() -> Self {
        Self { view: None }
    }
}

impl Windows {
    pub(super) fn start_transaction(&mut self) -> &mut Transaction {
        start_transaction();
        self.transaction.get_or_insert(Transaction::new())
    }

    pub fn update_transaction(&mut self) -> Option<Duration> {
        let start = TRANSACTION_START.load(Ordering::Relaxed);
        if start == 0 {
            return None;
        }

        let elapsed = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64 - start;
        let timeout = self.transaction_timeout();

        if elapsed <= timeout {
            let lock_ready = match self.pending_view() {
                View::Lock(Some(window)) => window.transaction_done(),
                View::Lock(None) => false,
                _ => true,
            };

            let finished = lock_ready
                && self.layouts.windows().all(|w| w.transaction_done())
                && self.layers.iter().all(Window::transaction_done);

            if !finished {
                return Some(Duration::from_millis(timeout - elapsed));
            }
        } else {
            for mut window in self.layouts.windows_mut().filter(|w| !w.transaction_done()) {
                window.set_ignore_transactions();
            }
            for window in self.layers.iter_mut().filter(|w| !w.transaction_done()) {
                window.set_ignore_transactions();
            }
        }

        TRANSACTION_START.store(0, Ordering::Relaxed);

        let old_layout_count = self.layouts.active().window_count();
        let old_layer_count = self.layers.len();

        if let Some(view) = self.transaction.take().and_then(|t| t.view) {
            self.dirty = true;
            self.view = view;
        }

        self.dirty |= self.layouts.apply_transaction(&self.output);

        self.layers.apply_transaction();

        if let View::Lock(Some(window)) = &mut self.view {
            window.apply_transaction();
        }

        let old_canvas = mem::replace(&mut self.canvas, *self.output.canvas());
        self.dirty |= old_canvas.orientation() != self.canvas.orientation()
            || old_canvas.scale() != self.canvas.scale();

        if self.layouts.is_empty() && matches!(self.view, View::Fullscreen(_)) {
            self.view = View::Workspace;
        }

        self.dirty |= old_layout_count != self.layouts.active().window_count()
            || old_layer_count != self.layers.len();

        None
    }

    fn transaction_timeout(&self) -> u64 {
        match self.pending_view() {
            View::Lock(_) => MAX_LOCKED_TRANSACTION_MILLIS,
            _ => MAX_TRANSACTION_MILLIS,
        }
    }

    /// Re-run auto placement of the extend screens: they keep marching from
    /// the primary's right edge after its logical size moved (a scale, mode
    /// or orientation change would otherwise open a pointer-blocking gap —
    /// or an overlap — between the regions). Config-pinned positions stay.
    fn reflow_extra_outputs(&mut self) {
        let mut next_x = self.output.logical_rect().size.w;
        for o in &mut self.extra_outputs {
            if o.location_pinned() {
                let r = o.logical_rect();
                next_x = next_x.max(r.loc.x + r.size.w);
                continue;
            }
            o.set_location((next_x, 0).into());
            next_x += o.logical_rect().size.w;
        }
    }

    pub fn resize_all(&mut self) {
        self.reflow_extra_outputs();

        let available_fullscreen = self.output.available_fullscreen();
        let output_size = self.output.size();
        let scale = self.output.scale();

        let fullscreen_window = match self.pending_view_mut() {
            View::Fullscreen(window) => {
                let mut w = window.borrow_mut();
                w.set_dimensions(scale, available_fullscreen);
                Some(w.surface().clone())
            },
            View::Lock(Some(window)) => {
                window.set_dimensions(scale, Rectangle::from_size(output_size));
                None
            },
            _ => None,
        };
        let fullscreen_window = fullscreen_window.as_ref();

        // While mirror is forced for the session lock, the extra outputs are
        // transiently gone and the desktop is hidden behind the lock surface.
        // Clamping stored positions against the primary-only topology would
        // permanently pile the external screens' windows onto the primary, so
        // the position-mutating passes are skipped; the resize_all after
        // unlock re-fits against the restored topology. This must hold for
        // EVERY caller (apply_output_scale from the mirror rebuild included),
        // which is why the guard lives here and not at the call sites.
        let keep_positions = crate::output::mirror_forced();

        let avail = self.output.available();
        for layout in self.layouts.layouts() {
            if layout.windows().any(|w| Some(w.borrow().surface()) == fullscreen_window) {
                continue;
            }
            layout.resize(&self.output);

            if keep_positions {
                continue;
            }
            for window_rc in layout.windows() {
                let mut w = window_rc.borrow_mut();
                if !w.floating {
                    continue;
                }
                let bar_h = self.bar_h_for(&w);
                let area = self.owner_area(w.output_rectangle());
                w.clamp_to_available(scale, area, bar_h);
            }
        }

        if !keep_positions {
            let screens = self.output_rects();
            self.x11_windows.clamp_to_available(&screens, avail);
        }

        for window in self.layers.iter_mut() {
            let fullscreened =
                window.surface.layer() == Layer::Overlay && fullscreen_window.is_some();
            window.update_dimensions(&mut self.output, fullscreened);
        }

        self.refresh_output_overlaps();
    }

    pub fn resize_visible(&mut self) {
        let available_fullscreen = self.output.available_fullscreen();
        let scale = self.output.scale();
        let size = self.output.size();

        match self.pending_view_mut() {
            View::Fullscreen(window) => {
                window.borrow_mut().set_dimensions(scale, available_fullscreen);

                for window in self.layers.overlay_mut() {
                    window.update_dimensions(&mut self.output, true);
                }
            },
            View::Lock(Some(window)) => {
                window.set_dimensions(scale, Rectangle::from_size(size));
            },
            _ => {
                self.layouts.pending_active().resize(&self.output);

                for window in self.layers.iter_mut() {
                    window.update_dimensions(&mut self.output, false);
                }
            },
        }
    }
}
