use std::cell::{Ref, RefCell, RefMut};
use std::cmp::Ordering;
use std::rc::{Rc, Weak};
use std::sync::atomic::{self, AtomicU64};

use smithay::utils::{Logical, Point, Rectangle};
use smithay::wayland::shell::xdg::ToplevelSurface;

use crate::drawing::ForgeElement;
use crate::windows::surface::InputSurfaceKind;
use crate::windows::{self, InputSurface, Output, Window, WlSurface};

const DEFAULT_LAYOUT: Layout = Layout {
    primary: None,
    secondary: None,
    id: LayoutId(0),
};

static NEXT_LAYOUT_ID: AtomicU64 = AtomicU64::new(1);

#[derive(Debug, Default)]
pub struct Layouts {
    pub focus: Option<Weak<RefCell<Window>>>,
    parent_layouts: Vec<LayoutId>,
    transactions: Vec<Transaction>,
    active_layout: Option<usize>,
    layouts: Vec<Layout>,

    /// Desktop-mode stacking order. Front of the vec (index 0) is the
    /// topmost layout. Only used when rendering/hit-testing in desktop
    /// mode; mobile keeps the "active-layout only" model.
    stacking: Vec<LayoutId>,
}

impl Layouts {
    pub fn active(&self) -> &Layout {
        self.active_layout
            .and_then(|i| self.layouts.get(i))
            .unwrap_or(&DEFAULT_LAYOUT)
    }

    pub fn pending_active(&self) -> Layout {
        let mut layout = self.active().clone();

        for transaction in &self.transactions {
            match transaction {
                Transaction::Active(Some(index)) => {
                    if let Some(new) = self.layouts.get(*index) {
                        layout = new.clone();
                    }
                }
                Transaction::Active(None) => layout = DEFAULT_LAYOUT,
            }
        }

        layout
    }

    pub fn create(&mut self, output: &Output, primary: Rc<RefCell<Window>>) {
        let active = self.active();
        if active.id != DEFAULT_LAYOUT.id {
            self.parent_layouts.push(active.id);
        }

        // Floating windows manage their own rectangle — don't overwrite it
        // with the tiled primary rect.
        {
            let mut p = primary.borrow_mut();
            if !p.floating {
                let rectangle = output.primary_rectangle(false);
                p.set_dimensions(output.scale(), rectangle);
            }
        }

        let layout = Layout::new(primary);
        let id = layout.id;
        self.layouts.push(layout);

        // New layouts appear on top of the desktop stack.
        self.stacking.retain(|i| *i != id);
        self.stacking.insert(0, id);

        let position = LayoutPosition::new(self.layouts.len() - 1, false);
        self.set_active(output, Some(position), false);
    }

    /// Move `id` to the front (topmost) of the desktop stacking order.
    pub fn raise_to_top(&mut self, id: LayoutId) {
        if self.stacking.first().copied() == Some(id) {
            return;
        }
        self.stacking.retain(|i| *i != id);
        self.stacking.insert(0, id);
    }

    /// Find the layout id that contains a window owning `surface`.
    pub fn layout_id_for(&self, surface: &WlSurface) -> Option<LayoutId> {
        for layout in &self.layouts {
            for w in layout.windows() {
                if w.borrow().owns_surface(surface) {
                    return Some(layout.id);
                }
            }
        }
        None
    }

    /// Iterator over layouts in desktop stacking order (topmost first).
    pub fn stacking_layouts(&self) -> impl Iterator<Item = &Layout> {
        self.stacking
            .iter()
            .filter_map(|id| self.layouts.iter().find(|l| l.id == *id))
    }

    pub fn set_active(
        &mut self,
        output: &Output,
        layout_position: Option<LayoutPosition>,
        clear_parents: bool,
    ) {
        let layout_index = layout_position.map(|p| p.index);

        self.focus = None;
        if let Some(pos) = layout_position {
            let layout = self.layouts.get(pos.index).unwrap_or(&DEFAULT_LAYOUT);
            let window = if pos.secondary {
                &layout.secondary
            } else {
                &layout.primary
            };

            if let Some(window) = window {
                self.focus = Some(Rc::downgrade(window));
                window.borrow_mut().enter(output);
            }

            layout.resize(output);
        }

        if clear_parents {
            self.parent_layouts.clear();
        }

        self.add_transaction(Transaction::Active(layout_index));
    }

    pub fn cycle_active(&mut self, output: &Output, mut n: isize) {
        let count = self.layouts.len();
        if count == 0 {
            return;
        }

        let active_index = match self.active_layout {
            Some(i) => i,
            None => {
                n -= n.signum();
                0
            }
        };

        let target = (active_index as isize + n).rem_euclid(count as isize) as usize;
        self.set_active(output, Some(LayoutPosition::new(target, false)), true);
    }

    pub fn reap(&self, output: &Output, surface: &ToplevelSurface) {
        windows::start_transaction();

        for layout in &self.layouts {
            let primary = layout.primary.as_deref().map(RefCell::borrow_mut);
            let secondary = layout.secondary.as_deref().map(RefCell::borrow_mut);

            let growing = if primary.as_ref().is_some_and(|w| &w.surface == surface) {
                secondary
            } else if secondary.as_ref().is_some_and(|w| &w.surface == surface) {
                primary
            } else {
                continue;
            };

            if let Some(mut window) = growing {
                let rectangle = output.primary_rectangle(false);
                window.set_dimensions(output.scale(), rectangle);
            }

            break;
        }
    }

    pub fn apply_transaction(&mut self, output: &Output) -> bool {
        let applied = !self.transactions.is_empty();

        for i in 0..self.transactions.len() {
            match self.transactions[i] {
                Transaction::Active(layout) => self.apply_active_transaction(output, layout),
            }
        }
        self.transactions.clear();

        let mut clear_active = false;
        let mut index = 0;
        let mut removed_ids: Vec<LayoutId> = Vec::new();

        self.layouts.retain_mut(|layout| {
            if let Some(secondary) = layout.secondary.as_ref() {
                let mut s = secondary.borrow_mut();
                if s.alive() {
                    s.apply_transaction();
                } else {
                    drop(s);
                    layout.secondary = None;
                }
            }

            if let Some(primary) = layout.primary.as_ref() {
                let mut p = primary.borrow_mut();
                if p.alive() {
                    p.apply_transaction();
                } else {
                    drop(p);
                    layout.primary = layout.secondary.take();
                }
            }

            let retain = layout.primary.is_some() || layout.secondary.is_some();
            if !retain {
                removed_ids.push(layout.id);
            }

            match Some(index).cmp(&self.active_layout) {
                Ordering::Less if !retain => {
                    self.active_layout = self.active_layout.and_then(|a| a.checked_sub(1));
                }
                Ordering::Equal if !retain => clear_active = true,
                _ => (),
            }

            index += 1;
            retain
        });

        self.stacking.retain(|id| !removed_ids.contains(id));

        if clear_active {
            let parent = self.parent_layouts.pop();
            self.active_layout = self.layouts.iter().position(|l| Some(l.id) == parent);

            if self.active_layout.is_none() {
                self.parent_layouts.clear();
            }
        }

        applied || clear_active
    }

    fn apply_active_transaction(&mut self, output: &Output, layout: Option<usize>) {
        if layout == self.active_layout {
            return;
        }

        self.send_active_leave(output);
        self.active_layout = layout;
    }

    fn add_transaction(&mut self, transaction: Transaction) {
        windows::start_transaction();
        self.transactions.push(transaction);
    }

    fn send_active_leave(&mut self, output: &Output) {
        self.with_visible_mut(|w| w.leave(output));
    }

    pub fn with_visible<F: FnMut(&Window)>(&self, mut fun: F) {
        for window in self.active().windows() {
            fun(&window.borrow());
        }
    }

    pub fn with_visible_mut<F: FnMut(&mut Window)>(&mut self, mut fun: F) {
        for window in self.active().windows() {
            fun(&mut window.borrow_mut());
        }
    }

    /// Emit textures for all visible windows.
    ///
    /// Mobile: only the active layout's windows. SSDs are drawn by the
    /// no-op `ssd_render` path (nothing on mobile).
    ///
    /// Desktop: topmost-first, and each window's SSD is emitted
    /// immediately before its own texture so the (SSD+window) bundle
    /// sits as one z-layer above any lower-z window. Without this
    /// interleaving a background window's SSD can overlap a topmost
    /// window's content where the bar's y-range crosses into it.
    pub fn textures_and_ssd(
        &mut self,
        textures: &mut Vec<ForgeElement>,
        canvas: &crate::output::Canvas,
        renderer: &mut smithay::backend::renderer::gles::GlesRenderer,
        text_renderer: &mut crate::text::TextRenderer,
        csd_apps: &[forge_ipc::AppIdMatcher],
        pointer_pos: Option<Point<f64, Logical>>,
    ) {
        let scale = canvas.scale();
        let desktop = !canvas.is_mobile();

        if desktop {
            // Snapshot (rect, title, maximized, suppress_ssd) per window so
            // SSD rendering doesn't keep a RefCell borrow while the window
            // textures run. `suppress_ssd` follows the protocol-driven
            // `decoration` policy when the client bound xdg-decoration, and
            // otherwise falls back to a config-driven app-id allowlist
            // (`forge.toml -> decoration.csd_apps`) — clients that bind
            // xdg-decoration and request ClientSide opt out the standard
            // way; clients that just paint their own bar without binding
            // the protocol can still be muted by adding their `app_id`
            // regex to the list.
            let items: Vec<(
                Rectangle<i32, Logical>,
                String,
                bool,
                bool,
                std::rc::Rc<std::cell::RefCell<Window>>,
            )> = self
                .stacking_layouts()
                .flat_map(|l| l.windows().cloned())
                .filter(|w| !w.borrow().minimized)
                .map(|w| {
                    let (rect, title, max, suppress_ssd) = {
                        let b = w.borrow();
                        (
                            b.bounds(scale),
                            b.title(),
                            b.is_maximized(),
                            b.suppress_ssd(csd_apps),
                        )
                    };
                    (rect, title, max, suppress_ssd, w)
                })
                .collect();

            let round_program = crate::rounded::program(renderer);
            for (rect, title, maximized, suppress_ssd, w) in &items {
                if !*suppress_ssd {
                    let mut win = w.borrow_mut();
                    let ssd = win
                        .ssd
                        .get_or_insert_with(|| Box::new(crate::ssd::SsdDecoration::new()));
                    ssd.render(
                        textures,
                        canvas,
                        renderer,
                        text_renderer,
                        title,
                        *rect,
                        *maximized,
                        pointer_pos,
                    );
                }
                let round = (!*suppress_ssd)
                    .then(|| round_program.clone())
                    .flatten()
                    .map(|program| crate::rounded::Rounding {
                        program,
                        radius: crate::ssd::SsdDecoration::bar_radius(),
                    });
                w.borrow().textures_rounded(textures, scale, round.as_ref());
            }
            return;
        }

        // Mobile: no SSD, just the active layout's windows.
        let layout = self.active();
        if let Some(secondary) = layout.secondary().map(|w| w.borrow()) {
            if !secondary.minimized {
                secondary.textures(textures, scale, None, None);
            }
        }
        if let Some(primary) = layout.primary().map(|w| w.borrow()) {
            if !primary.minimized {
                primary.textures(textures, scale, None, None);
            }
        }
    }

    /// Paint a single Wayland toplevel by its `wl_surface` (SSD bar then
    /// the window's own textures). Returns `true` if a matching window
    /// was found and painted. Used by the z-ordered desktop render path.
    pub fn paint_one(
        &mut self,
        wl: &WlSurface,
        textures: &mut Vec<ForgeElement>,
        canvas: &crate::output::Canvas,
        renderer: &mut smithay::backend::renderer::gles::GlesRenderer,
        text_renderer: &mut crate::text::TextRenderer,
        csd_apps: &[forge_ipc::AppIdMatcher],
        pointer_pos: Option<Point<f64, Logical>>,
    ) -> bool {
        let scale = canvas.scale();
        let desktop = !canvas.is_mobile();
        if !desktop {
            return false;
        }

        let Some(window) = self.find_window(wl).cloned() else {
            return false;
        };
        let (rect, title, max, suppress_ssd) = {
            let b = window.borrow();
            if b.minimized {
                return false;
            }
            (
                b.bounds(scale),
                b.title(),
                b.is_maximized(),
                b.suppress_ssd(csd_apps),
            )
        };
        if !suppress_ssd {
            let mut win = window.borrow_mut();
            let ssd = win
                .ssd
                .get_or_insert_with(|| Box::new(crate::ssd::SsdDecoration::new()));
            ssd.render(
                textures,
                canvas,
                renderer,
                text_renderer,
                &title,
                rect,
                max,
                pointer_pos,
            );
        }
        let round = (!suppress_ssd)
            .then(|| crate::rounded::program(renderer))
            .flatten()
            .map(|program| crate::rounded::Rounding {
                program,
                radius: crate::ssd::SsdDecoration::bar_radius(),
            });
        window
            .borrow()
            .textures_rounded(textures, scale, round.as_ref());
        true
    }

    /// Like [`Self::paint_one`], but for an extend head: paints only when
    /// the window's global rect overlaps `clip` (the head's region of the
    /// global space), translated into the head's local coordinates and
    /// rasterised at the head canvas' scale.
    pub fn paint_one_translated(
        &mut self,
        wl: &WlSurface,
        textures: &mut Vec<ForgeElement>,
        canvas: &crate::output::Canvas,
        clip: Rectangle<i32, Logical>,
        renderer: &mut smithay::backend::renderer::gles::GlesRenderer,
        text_renderer: &mut crate::text::TextRenderer,
        csd_apps: &[forge_ipc::AppIdMatcher],
        pointer_pos: Option<Point<f64, Logical>>,
    ) -> bool {
        let scale = canvas.scale();
        let Some(window) = self.find_window(wl).cloned() else {
            return false;
        };
        let (rect, title, max, suppress_ssd) = {
            let b = window.borrow();
            if b.minimized {
                return false;
            }
            (
                b.bounds(scale),
                b.title(),
                b.is_maximized(),
                b.suppress_ssd(csd_apps),
            )
        };
        if !rect.overlaps(clip) {
            return false;
        }
        let origin = clip.loc;
        let local = Rectangle::new(rect.loc - origin, rect.size);
        let local_pointer = pointer_pos.map(|p| p - origin.to_f64());
        if !suppress_ssd {
            let mut win = window.borrow_mut();
            let ssd = win
                .ssd
                .get_or_insert_with(|| Box::new(crate::ssd::SsdDecoration::new()));
            ssd.render(
                textures,
                canvas,
                renderer,
                text_renderer,
                &title,
                local,
                max,
                local_pointer,
            );
        }
        let round = (!suppress_ssd)
            .then(|| crate::rounded::program(renderer))
            .flatten()
            .map(|program| crate::rounded::Rounding {
                program,
                radius: crate::ssd::SsdDecoration::bar_radius(),
            });
        window
            .borrow()
            .textures_with_bounds(textures, scale, None, local.loc, None, round.as_ref());
        true
    }

    pub fn paint_remaining(
        &mut self,
        skip: &std::collections::HashSet<u32>,
        textures: &mut Vec<ForgeElement>,
        canvas: &crate::output::Canvas,
        renderer: &mut smithay::backend::renderer::gles::GlesRenderer,
        text_renderer: &mut crate::text::TextRenderer,
        csd_apps: &[forge_ipc::AppIdMatcher],
        pointer_pos: Option<Point<f64, Logical>>,
    ) {
        use crate::windows::surface::Surface as _;
        use smithay::reexports::wayland_server::Resource as _;
        let wls: Vec<WlSurface> = self
            .layouts
            .iter()
            .flat_map(|l| l.windows())
            .filter_map(|w| {
                let s = w.borrow().surface.surface().clone();
                (!skip.contains(&s.id().protocol_id())).then_some(s)
            })
            .collect();
        for wl in &wls {
            self.paint_one(
                wl,
                textures,
                canvas,
                renderer,
                text_renderer,
                csd_apps,
                pointer_pos,
            );
        }
    }

    pub fn windows(&self) -> impl Iterator<Item = Ref<'_, Window>> {
        self.layouts
            .iter()
            .flat_map(|l| l.windows())
            .map(|w| w.borrow())
    }

    pub fn windows_mut(&mut self) -> impl Iterator<Item = RefMut<'_, Window>> {
        self.layouts
            .iter()
            .flat_map(|l| l.windows())
            .map(|w| w.borrow_mut())
    }

    pub fn position(&self, surface: &WlSurface) -> Option<LayoutPosition> {
        for (i, layout) in self.layouts.iter().enumerate() {
            if layout
                .primary
                .as_ref()
                .is_some_and(|w| w.borrow().owns_surface(surface))
            {
                return Some(LayoutPosition::new(i, false));
            }
            if layout
                .secondary
                .as_ref()
                .is_some_and(|w| w.borrow().owns_surface(surface))
            {
                return Some(LayoutPosition::new(i, true));
            }
        }
        None
    }

    pub fn find_window(&self, surface: &WlSurface) -> Option<&Rc<RefCell<Window>>> {
        self.layouts
            .iter()
            .flat_map(|l| l.windows())
            .find(|w| w.borrow().surface() == surface)
    }

    pub fn touch_surface_at(
        &mut self,
        output_scale: f64,
        position: Point<f64, Logical>,
        desktop: bool,
    ) -> Option<InputSurface> {
        if desktop {
            // Topmost first — stacking_layouts() iterates front-to-back.
            let ids: Vec<LayoutId> = self.stacking.clone();
            for id in ids {
                let Some(layout) = self.layouts.iter().find(|l| l.id == id) else {
                    continue;
                };
                for window in layout.windows() {
                    let window_ref = window.borrow();
                    if window_ref.minimized {
                        continue;
                    }
                    if let Some(mut surface) = window_ref.surface_at(output_scale, position) {
                        let app_id = window_ref.app_id.clone();
                        let weak = Rc::downgrade(window);
                        surface.toplevel = Some(InputSurfaceKind::Layout((weak, app_id)));
                        return Some(surface);
                    }
                }
            }
            return None;
        }

        let active = self.active_layout.and_then(|i| self.layouts.get(i))?;

        for window in active.windows() {
            let window_ref = window.borrow();
            if window_ref.minimized {
                continue;
            }
            if let Some(mut surface) = window_ref.surface_at(output_scale, position) {
                let app_id = window_ref.app_id.clone();
                let weak = Rc::downgrade(window);
                surface.toplevel = Some(InputSurfaceKind::Layout((weak, app_id)));
                return Some(surface);
            }
        }

        None
    }

    pub fn layouts(&self) -> &[Layout] {
        &self.layouts
    }

    pub fn is_empty(&self) -> bool {
        self.layouts.is_empty()
    }

    pub fn len(&self) -> usize {
        self.layouts.len()
    }

    /// Get the current active workspace index (0-based).
    pub fn active_index(&self) -> Option<usize> {
        self.active_layout
    }

    /// Switch to a specific workspace by index.
    pub fn switch_to(&mut self, output: &Output, index: usize) {
        if index < self.layouts.len() && self.active_layout != Some(index) {
            self.set_active(output, Some(LayoutPosition::new(index, false)), true);
        }
    }

}

#[derive(Clone, Debug)]
pub struct Layout {
    primary: Option<Rc<RefCell<Window>>>,
    secondary: Option<Rc<RefCell<Window>>>,
    id: LayoutId,
}

impl Layout {
    fn new(primary: Rc<RefCell<Window>>) -> Self {
        let id = LayoutId(NEXT_LAYOUT_ID.fetch_add(1, atomic::Ordering::Relaxed));
        Self {
            id,
            primary: Some(primary),
            secondary: None,
        }
    }

    pub fn primary(&self) -> Option<&Rc<RefCell<Window>>> {
        self.primary.as_ref()
    }

    pub fn secondary(&self) -> Option<&Rc<RefCell<Window>>> {
        self.secondary.as_ref()
    }

    pub fn windows(&self) -> impl Iterator<Item = &Rc<RefCell<Window>>> {
        self.primary.iter().chain(&self.secondary)
    }

    pub fn window_count(&self) -> usize {
        self.primary.is_some() as usize + self.secondary.is_some() as usize
    }

    pub fn resize(&self, output: &Output) {
        let primary = self.primary.as_deref().map(RefCell::borrow_mut);
        let secondary = self.secondary.as_deref().map(RefCell::borrow_mut);

        if let Some(mut p) = primary {
            if !p.floating {
                let secondary_alive = secondary.as_ref().is_some_and(|w| w.alive());
                let rectangle = output.primary_rectangle(secondary_alive);
                p.set_dimensions(output.scale(), rectangle);
            }
        }

        if let Some(mut s) = secondary {
            if !s.floating {
                let rectangle = output.secondary_rectangle();
                s.set_dimensions(output.scale(), rectangle);
            }
        }
    }
}

#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
pub struct LayoutId(u64);

#[derive(Debug)]
enum Transaction {
    Active(Option<usize>),
}

#[derive(Copy, Clone, Debug)]
pub struct LayoutPosition {
    pub index: usize,
    pub secondary: bool,
}

impl LayoutPosition {
    pub fn new(index: usize, secondary: bool) -> Self {
        Self { index, secondary }
    }
}
