// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged.
use super::*;

impl Scene {
    // ── Layout management ─────────────────────────────────────────────────

    /// Rename a paper-space layout.  Updates the Layout object name in the document.
    pub fn rename_layout(&mut self, old_name: &str, new_name: &str) {
        for obj in self.document.objects.values_mut() {
            if let ObjectType::Layout(l) = obj {
                if l.name == old_name {
                    l.name = new_name.to_string();
                    return;
                }
            }
        }
    }

    /// Delete a paper-space layout and all entities owned by it.
    /// Returns `false` if the layout was not found or is "Model".
    pub fn delete_layout(&mut self, name: &str) -> bool {
        if name == "Model" {
            return false;
        }

        let layout_info = self.document.objects.values().find_map(|obj| {
            if let ObjectType::Layout(l) = obj {
                if l.name == name {
                    return Some((l.handle, l.block_record));
                }
            }
            None
        });

        let (layout_handle, block_handle) = match layout_info {
            Some(info) => info,
            None => return false,
        };

        // Remove all entities that belong to this layout's block record.
        let to_remove: Vec<Handle> = self
            .document
            .entities()
            .filter(|e| e.common().owner_handle == block_handle)
            .map(|e| e.common().handle)
            .collect();
        for h in &to_remove {
            self.hatches.remove(h);
            self.meshes.remove(h);
            self.solid_models.remove(h);
            self.document.remove_entity(*h);
        }

        // Remove the Layout object itself.
        self.document.objects.remove(&layout_handle);

        // Drop the layout's entry from the ACAD_LAYOUT dictionary so it does not
        // dangle (and so AutoCAD doesn't try to recover a now-missing layout).
        let dict_handle = self.document.header.acad_layout_dict_handle;
        if let Some(ObjectType::Dictionary(d)) = self.document.objects.get_mut(&dict_handle) {
            d.entries.retain(|(k, _)| k != name);
        }

        // Remove the now-empty paper-space block record.
        let block_name = self
            .document
            .block_records
            .iter()
            .find(|b| b.handle == block_handle)
            .map(|b| b.name.clone());
        if let Some(bn) = block_name {
            self.document.block_records.remove(&bn);
        }

        // Drop any standalone PlotSettings page setup tied to this layout.
        let ps_handles: Vec<Handle> = self
            .document
            .objects
            .iter()
            .filter_map(|(h, o)| match o {
                ObjectType::PlotSettings(ps) if ps.page_name == name => Some(*h),
                _ => None,
            })
            .collect();
        for h in ps_handles {
            self.document.objects.remove(&h);
        }

        // If the deleted layout was active, fall back to Model space.
        if self.current_layout == name {
            self.current_layout = "Model".to_string();
            self.sync_active_space_to_document();
        }

        self.bump_geometry();
        true
    }

    /// Swap the `tab_order` of two paper layouts so they appear in swapped order.
    pub fn swap_layout_order(&mut self, name_a: &str, name_b: &str) {
        let mut order_a: Option<i16> = None;
        let mut order_b: Option<i16> = None;
        for obj in self.document.objects.values() {
            if let ObjectType::Layout(l) = obj {
                if l.name == name_a {
                    order_a = Some(l.tab_order);
                }
                if l.name == name_b {
                    order_b = Some(l.tab_order);
                }
            }
        }
        if let (Some(oa), Some(ob)) = (order_a, order_b) {
            for obj in self.document.objects.values_mut() {
                if let ObjectType::Layout(l) = obj {
                    if l.name == name_a {
                        l.tab_order = ob;
                    } else if l.name == name_b {
                        l.tab_order = oa;
                    }
                }
            }
        }
    }

    /// Rebuild the `pane_grid` layout from the current `model_tiles` rects
    /// (each pane value = its tile index) — used after loading a tiled config
    /// from the VPort table so the panes match the restored tiles.
    pub fn rebuild_panes_from_tiles(&mut self) {
        let items: Vec<(usize, iced::Rectangle)> = self
            .model_tiles
            .borrow()
            .iter()
            .enumerate()
            .map(|(i, t)| (i, t.rect))
            .collect();
        let full = iced::Rectangle {
            x: 0.0,
            y: 0.0,
            width: 1.0,
            height: 1.0,
        };
        let config = if items.is_empty() {
            iced::widget::pane_grid::Configuration::Pane(0)
        } else {
            config_from_rects(&items, full)
        };
        self.model_panes = iced::widget::pane_grid::State::with_configuration(config);
    }

    /// Replace the Model pane layout with a `pane_grid` configuration (VPORTS
    /// presets). Each resulting pane inherits the active tile's camera / style;
    /// pane values are compacted to `0..N` and `model_tiles` rebuilt to match.
    pub fn set_model_panes(&mut self, config: iced::widget::pane_grid::Configuration<usize>) {
        let cam = self.camera.borrow().clone();
        let (mode, grid_on, snap_on) = {
            let tiles = self.model_tiles.borrow();
            let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1));
            tiles
                .get(active)
                .map(|t| (t.render_mode, t.grid_on, t.snap_on))
                .unwrap_or((
                    acadrust::entities::ViewportRenderMode::Wireframe2D,
                    false,
                    false,
                ))
        };
        self.model_panes = iced::widget::pane_grid::State::with_configuration(config);
        let order: Vec<iced::widget::pane_grid::Pane> =
            self.model_panes.iter().map(|(p, _)| *p).collect();
        let mut tiles = Vec::with_capacity(order.len());
        for (i, pane) in order.iter().enumerate() {
            if let Some(v) = self.model_panes.get_mut(*pane) {
                *v = i;
            }
            tiles.push(ModelTile {
                rect: iced::Rectangle {
                    x: 0.0,
                    y: 0.0,
                    width: 1.0,
                    height: 1.0,
                },
                camera: cam.clone(),
                render_mode: mode,
                grid_on,
                snap_on,
            });
        }
        *self.model_tiles.borrow_mut() = tiles;
        self.active_model_tile.set(0);
    }

    /// Split the active Model pane through `pane_grid`, adding a new pane that
    /// inherits the active pane's camera / render-mode / grid. `horizontal`
    /// true → a horizontal divider (panes stacked); false → vertical (panes
    /// side by side). The active pane stays active.
    pub fn split_active_pane(&mut self, horizontal: bool) {
        use iced::widget::pane_grid::Axis;
        let active_idx = self.active_model_tile.get();
        // Keep the active tile's stored camera current before cloning it.
        if let Some(t) = self.model_tiles.borrow_mut().get_mut(active_idx) {
            t.camera = self.camera.borrow().clone();
        }
        let Some(active_pane) = self
            .model_panes
            .iter()
            .find(|(_, &v)| v == active_idx)
            .map(|(p, _)| *p)
        else {
            return;
        };
        let Some(clone) = self.model_tiles.borrow().get(active_idx).cloned() else {
            return;
        };
        let new_val = self.model_tiles.borrow().len();
        self.model_tiles.borrow_mut().push(clone);
        let axis = if horizontal {
            Axis::Horizontal
        } else {
            Axis::Vertical
        };
        if self.model_panes.split(axis, active_pane, new_val).is_none() {
            self.model_tiles.borrow_mut().pop();
            return;
        }
        self.compact_panes(active_pane);
    }

    /// Close the active Model pane (no-op on the last pane); the sibling that
    /// absorbs the space becomes active and its camera goes live.
    pub fn close_active_pane(&mut self) {
        if self.model_panes.len() <= 1 {
            return;
        }
        let active_idx = self.active_model_tile.get();
        if let Some(t) = self.model_tiles.borrow_mut().get_mut(active_idx) {
            t.camera = self.camera.borrow().clone();
        }
        let Some(active_pane) = self
            .model_panes
            .iter()
            .find(|(_, &v)| v == active_idx)
            .map(|(p, _)| *p)
        else {
            return;
        };
        if let Some((_, sibling)) = self.model_panes.close(active_pane) {
            self.compact_panes(sibling);
            let cam = self
                .model_tiles
                .borrow()
                .get(self.active_model_tile.get())
                .map(|t| t.camera.clone());
            if let Some(c) = cam {
                *self.camera.borrow_mut() = c;
            }
        }
    }

    /// Swap two Model panes (by tile index) in the `pane_grid` layout — each
    /// viewport moves to the other's position, keeping its own camera / style.
    pub fn swap_model_panes(&mut self, a: usize, b: usize) {
        if a == b {
            return;
        }
        let pa = self.model_panes.iter().find(|(_, &v)| v == a).map(|(p, _)| *p);
        let pb = self.model_panes.iter().find(|(_, &v)| v == b).map(|(p, _)| *p);
        if let (Some(pa), Some(pb)) = (pa, pb) {
            self.model_panes.swap(pa, pb);
        }
    }

    /// Renumber pane values to a compact `0..N` in layout order and rebuild
    /// `model_tiles` to match (preserving each pane's data), so the pane value
    /// is always a valid `model_tiles` index. `active_pane` becomes the new
    /// active tile.
    fn compact_panes(&mut self, active_pane: iced::widget::pane_grid::Pane) {
        let old = self.model_tiles.borrow().clone();
        let order: Vec<iced::widget::pane_grid::Pane> =
            self.model_panes.iter().map(|(p, _)| *p).collect();
        let fallback = old
            .first()
            .cloned()
            .unwrap_or_else(|| ModelTile {
                rect: iced::Rectangle {
                    x: 0.0,
                    y: 0.0,
                    width: 1.0,
                    height: 1.0,
                },
                camera: self.camera.borrow().clone(),
                render_mode: acadrust::entities::ViewportRenderMode::Wireframe2D,
                grid_on: false,
                snap_on: false,
            });
        let mut new_tiles = Vec::with_capacity(order.len());
        let mut new_active = 0usize;
        for (new_idx, pane) in order.iter().enumerate() {
            let old_idx = *self.model_panes.get(*pane).unwrap_or(&0);
            new_tiles.push(old.get(old_idx).cloned().unwrap_or_else(|| fallback.clone()));
            if let Some(v) = self.model_panes.get_mut(*pane) {
                *v = new_idx;
            }
            if *pane == active_pane {
                new_active = new_idx;
            }
        }
        *self.model_tiles.borrow_mut() = new_tiles;
        self.active_model_tile.set(new_active);
    }

    /// Make Model tile `idx` active, stashing the live camera into the outgoing
    /// tile and loading the incoming one. Returns `true` if the active tile
    /// changed. The caller bumps `camera_generation` and syncs the display.
    pub fn set_active_model_tile(&self, idx: usize) -> bool {
        if self.current_layout != "Model" {
            return false;
        }
        let old = self.active_model_tile.get();
        if idx == old || idx >= self.model_tiles.borrow().len() {
            return false;
        }
        let incoming = {
            let mut tiles = self.model_tiles.borrow_mut();
            if let Some(t) = tiles.get_mut(old) {
                t.camera = self.camera.borrow().clone();
            }
            tiles.get(idx).map(|t| t.camera.clone())
        };
        if let Some(cam) = incoming {
            *self.camera.borrow_mut() = cam;
        }
        self.active_model_tile.set(idx);
        true
    }

    /// Divider bars between Model panes, as pixel rectangles within a
    /// `(vw, vh)` canvas — derived from the `pane_grid` split regions so they
    /// land exactly in the spacing gaps the renderer leaves. Each bar is
    /// `TILE_DIVIDER_PX` thick. Empty outside Model / for a single pane.
    pub fn model_pane_dividers(&self, vw: f32, vh: f32) -> Vec<iced::Rectangle> {
        use iced::widget::pane_grid::Axis;
        if self.current_layout != "Model" || vw < 1.0 || vh < 1.0 {
            return vec![];
        }
        let half = TILE_DIVIDER_PX * 0.5;
        self.model_panes
            .layout()
            .split_regions(TILE_DIVIDER_PX, 0.0, iced::Size::new(vw, vh))
            .values()
            .map(|(axis, region, ratio)| match axis {
                Axis::Vertical => {
                    let x = region.x + ratio * region.width;
                    iced::Rectangle {
                        x: x - half,
                        y: region.y,
                        width: TILE_DIVIDER_PX,
                        height: region.height,
                    }
                }
                Axis::Horizontal => {
                    let y = region.y + ratio * region.height;
                    iced::Rectangle {
                        x: region.x,
                        y: y - half,
                        width: region.width,
                        height: TILE_DIVIDER_PX,
                    }
                }
            })
            .collect()
    }

    /// Top-left pixel corner of Model pane `idx` within a `(vw, vh)` canvas,
    /// derived from the `pane_grid` layout (so it matches the rendered pane).
    pub fn pane_origin_px(&self, idx: usize, vw: f32, vh: f32) -> iced::Point {
        if vw < 1.0 || vh < 1.0 {
            return iced::Point::ORIGIN;
        }
        let regions = self.model_panes.layout().pane_regions(
            TILE_DIVIDER_PX,
            0.0,
            iced::Size::new(vw, vh),
        );
        self.model_panes
            .iter()
            .find(|(_, &v)| v == idx)
            .and_then(|(pane, _)| regions.get(pane))
            .map(|r| iced::Point::new(r.x, r.y))
            .unwrap_or(iced::Point::ORIGIN)
    }

    /// Replace the Model tiled layout with the given normalized rectangles
    /// (each in 0..1). Every tile inherits the current camera; the first
    /// tile becomes active. Used by VPORTS presets and `reset_model_tiles`.
    /// Derive `model_tiles` rects from the `pane_grid` layout (the layout
    /// source of truth) for a canvas of `(canvas_w, canvas_h)` pixels — each
    /// pane's value is its `model_tiles` index. Called before the renderer /
    /// hit-test read tile rects so they track the live pane_grid layout and any
    /// in-flight resize, with the divider gap applied exactly as pane_grid draws
    /// it. No-op outside the Model layout.
    #[allow(dead_code)]
    pub fn sync_tiles_from_panes(&self, canvas_w: f32, canvas_h: f32) {
        if self.current_layout != "Model" || canvas_w < 1.0 || canvas_h < 1.0 {
            return;
        }
        let regions = self.model_panes.layout().pane_regions(
            TILE_DIVIDER_PX,
            0.0,
            iced::Size::new(canvas_w, canvas_h),
        );
        let mut tiles = self.model_tiles.borrow_mut();
        for (pane, &idx) in self.model_panes.iter() {
            if let (Some(r), Some(tile)) = (regions.get(pane), tiles.get_mut(idx)) {
                tile.rect = iced::Rectangle {
                    x: r.x / canvas_w,
                    y: r.y / canvas_h,
                    width: r.width / canvas_w,
                    height: r.height / canvas_h,
                };
            }
        }
    }


    /// Screen-pixel rectangle of the active Model tile within a canvas of
    /// `(vw, vh)`. Full canvas outside the Model layout or for a single
    /// tile. Used to map cursor coordinates into the active tile so pick /
    /// pan / ViewCube work per-pane in a tiled layout.
    /// Canvas bounds + camera for every Model tile whose grid display is on.
    /// Each pane renders its own grid independently of which tile is active or
    /// hovered, so the grid never flickers as the cursor crosses panes. The
    /// active tile uses the live camera (mid-orbit/pan); others use their
    /// stored camera. (#121)
    /// Screen rect + camera for every grid-on sub-view in the current layout —
    /// model tiles in model space, the sheet plus each floating viewport
    /// (clipped to its rectangle) in paper space. Derived from the same
    /// `active_viewports` enumeration the renderer uses, so the grid overlay can
    /// never drift from the views actually on screen (issue #121). The grid

    pub fn active_model_tile_bounds(&self, vw: f32, vh: f32) -> iced::Rectangle {
        if self.current_layout != "Model" {
            return iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
        }
        let tiles = self.model_tiles.borrow();
        let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1));
        match tiles.get(active) {
            Some(t) => iced::Rectangle {
                x: t.rect.x * vw,
                y: t.rect.y * vh,
                width: (t.rect.width * vw).max(1.0),
                height: (t.rect.height * vh).max(1.0),
            },
            None => iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh },
        }
    }
}


/// Reconstruct a `pane_grid` configuration from a set of (tile-index, rect)
/// items covering `region`, by recursively finding a full vertical or
/// horizontal guillotine cut. AutoCAD tiled configs (and anything pane_grid
/// produces) are guillotine layouts, so this round-trips them. A non-guillotine
/// set falls back to chaining panes so nothing is lost.
fn config_from_rects(
    items: &[(usize, iced::Rectangle)],
    region: iced::Rectangle,
) -> iced::widget::pane_grid::Configuration<usize> {
    use iced::widget::pane_grid::{Axis, Configuration as C};
    if items.len() == 1 {
        return C::Pane(items[0].0);
    }
    let eps = 1e-3;
    // Try a vertical cut: a boundary x where every rect is wholly left or right.
    let mut cuts: Vec<f32> = items.iter().map(|(_, r)| r.x + r.width).collect();
    cuts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
    for &x in &cuts {
        if x <= region.x + eps || x >= region.x + region.width - eps {
            continue;
        }
        let split_ok = items
            .iter()
            .all(|(_, r)| r.x + r.width <= x + eps || r.x >= x - eps);
        if !split_ok {
            continue;
        }
        let (a, b): (Vec<_>, Vec<_>) = items
            .iter()
            .cloned()
            .partition(|(_, r)| r.x + r.width <= x + eps);
        if a.is_empty() || b.is_empty() {
            continue;
        }
        let ra = iced::Rectangle {
            width: x - region.x,
            ..region
        };
        let rb = iced::Rectangle {
            x,
            width: region.x + region.width - x,
            ..region
        };
        return C::Split {
            axis: Axis::Vertical,
            ratio: ((x - region.x) / region.width).clamp(0.05, 0.95),
            a: Box::new(config_from_rects(&a, ra)),
            b: Box::new(config_from_rects(&b, rb)),
        };
    }
    // Try a horizontal cut: a boundary y where every rect is wholly above/below.
    let mut cuts: Vec<f32> = items.iter().map(|(_, r)| r.y + r.height).collect();
    cuts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
    for &y in &cuts {
        if y <= region.y + eps || y >= region.y + region.height - eps {
            continue;
        }
        let split_ok = items
            .iter()
            .all(|(_, r)| r.y + r.height <= y + eps || r.y >= y - eps);
        if !split_ok {
            continue;
        }
        let (a, b): (Vec<_>, Vec<_>) = items
            .iter()
            .cloned()
            .partition(|(_, r)| r.y + r.height <= y + eps);
        if a.is_empty() || b.is_empty() {
            continue;
        }
        let ra = iced::Rectangle {
            height: y - region.y,
            ..region
        };
        let rb = iced::Rectangle {
            y,
            height: region.y + region.height - y,
            ..region
        };
        return C::Split {
            axis: Axis::Horizontal,
            ratio: ((y - region.y) / region.height).clamp(0.05, 0.95),
            a: Box::new(config_from_rects(&a, ra)),
            b: Box::new(config_from_rects(&b, rb)),
        };
    }
    // Non-guillotine fallback: peel the first pane off the rest.
    let (first, rest) = items.split_first().unwrap();
    C::Split {
        axis: Axis::Vertical,
        ratio: 0.5,
        a: Box::new(C::Pane(first.0)),
        b: Box::new(config_from_rects(rest, region)),
    }
}
