//! Layer Manager — floating window.

use crate::app::Message;
use crate::ui::properties::{lw_options, LinetypeItem, LwItem};
use crate::ui::ROW_H;
use acadrust::tables::layer::Layer as DocLayer;
use acadrust::tables::Table;
use acadrust::types::aci_table::aci_to_rgb;
use acadrust::types::{Color as AcadColor, LineWeight};
use acadrust::Handle;
use iced::widget::{
    button, column, combo_box, container, mouse_area, row, scrollable, text, text_input, tooltip,
};
use iced::Padding;
use iced::{Background, Border, Color, Element, Fill, Length, Theme};
use crate::t;
use std::borrow::Cow;

// ── Per-viewport column descriptor ───────────────────────────────────────────

#[derive(Clone, Debug)]
pub struct VpCol {
    pub handle: Handle,
    pub label: String,
}

/// Sortable column in the Layer Manager table. Clicking a header sorts by
/// that column; clicking the active header again flips the direction (#133).
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum LayerSortCol {
    Name,
    On,
    Freeze,
    Lock,
    Plot,
    Color,
    Linetype,
    Lineweight,
    Transparency,
}

// ── Row-height-derived constants ─────────────────────────────────────────
/// SVG icon size inside a layer-table cell.
const ICON_SZ: f32 = ROW_H * 0.62; // ≈16 px at ROW_H=26
/// Font size for cell text.
const FONT_SZ: f32 = ROW_H * 0.42; // ≈11 px at ROW_H=26
/// Vertical padding for combo_box / text_input so their total height = ROW_H.
const COMBO_PAD_V: f32 = (ROW_H - FONT_SZ * 1.3 - 2.0) / 2.0;
const LINETYPE_MENU_W: f32 = 220.0;
/// Widget id for the layer-table scrollable, so a freshly created layer can be
/// scrolled into view after it is added (#271).
pub const LAYER_TABLE_SCROLL_ID: &str = "layer-manager-table-scroll";

fn muted_style(theme: &Theme) -> iced::widget::text::Style {
    iced::widget::text::Style {
        color: Some(theme.palette().background.base.text.scale_alpha(0.68)),
    }
}

fn table_input_style(
    theme: &Theme,
    status: iced::widget::text_input::Status,
) -> iced::widget::text_input::Style {
    let palette = theme.palette();
    let border = match status {
        iced::widget::text_input::Status::Focused { .. } => palette.primary.base.color,
        _ => palette.background.neutral.color,
    };
    iced::widget::text_input::Style {
        background: Background::Color(palette.background.base.color),
        border: Border {
            color: border,
            width: 1.0,
            radius: 2.0.into(),
        },
        icon: palette.background.base.text,
        placeholder: palette.background.base.text.scale_alpha(0.48),
        value: palette.background.base.text,
        selection: palette.primary.base.color.scale_alpha(0.5),
    }
}

// ── Layer data ────────────────────────────────────────────────────────────

#[derive(Clone, Debug)]
pub struct Layer {
    pub name: String,
    pub visible: bool,
    pub frozen: bool,
    pub locked: bool,
    pub plottable: bool,
    pub color: AcadColor,
    pub linetype: String,
    pub lineweight: LineWeight,
    pub transparency: i32,
    /// Freeze state per-viewport, indexed parallel to LayerPanel::vp_cols.
    pub vp_frozen: Vec<bool>,
}

impl Layer {
    pub fn new(name: &str, color: AcadColor) -> Self {
        Self {
            name: name.to_string(),
            visible: true,
            frozen: false,
            locked: false,
            plottable: true,
            color,
            linetype: "Continuous".to_string(),
            lineweight: LineWeight::Default,
            transparency: 0,
            vp_frozen: vec![],
        }
    }
}

// ── Panel state ───────────────────────────────────────────────────────────

#[derive(Clone)]
pub struct LayerPanel {
    pub layers: Vec<Layer>,
    #[allow(dead_code)]
    pub visible: bool,
    /// Anchor row: the last row clicked. Drives the editable combos and the
    /// Shift-range origin. Its layer is always part of `selected_multi`.
    pub selected: Option<usize>,
    /// All selected rows (Ctrl/Shift extend it). Bulk property changes and
    /// deletion act on every row here. Empty ⇔ nothing selected.
    pub selected_multi: Vec<usize>,
    pub editing: Option<usize>,
    pub edit_buf: String,
    pub current_layer: String,
    pub linetype_items: Vec<LinetypeItem>,
    pub color_picker_row: Option<usize>,
    pub color_full_palette: bool,
    pub linetype_combo: combo_box::State<LinetypeItem>,
    pub lw_combo: combo_box::State<LwItem>,
    /// Per-viewport columns (only populated when in a paper layout with viewports).
    pub vp_cols: Vec<VpCol>,
    /// Active sort column, or `None` for document order.
    pub sort_col: Option<LayerSortCol>,
    /// Sort direction; `true` = ascending.
    pub sort_asc: bool,
    /// Live name filter from the search box; empty shows every layer (#343).
    pub filter: String,
}

impl Default for LayerPanel {
    fn default() -> Self {
        Self {
            visible: false,
            layers: vec![Layer::new("0", AcadColor::Index(7))],
            selected: None,
            selected_multi: Vec::new(),
            editing: None,
            edit_buf: String::new(),
            current_layer: "0".to_string(),
            linetype_items: vec![LinetypeItem {
                name: "Continuous".into(),
                art: String::new(),
            }],
            color_picker_row: None,
            color_full_palette: false,
            linetype_combo: combo_box::State::new(vec![LinetypeItem {
                name: "Continuous".into(),
                art: String::new(),
            }]),
            lw_combo: combo_box::State::new(lw_options()),
            vp_cols: vec![],
            // Default to alphabetical (Name) order in both the Layer Manager
            // table and the ribbon's quick layer dropdown (#270). A header
            // click still re-sorts by any other column.
            sort_col: Some(LayerSortCol::Name),
            sort_asc: true,
            filter: String::new(),
        }
    }
}

impl LayerPanel {
    /// Sync layers + update per-viewport freeze columns.
    /// `vp_info`: list of (vp_handle, vp_label, frozen_layer_handles) from scene.
    pub fn sync_with_viewports(
        &mut self,
        doc_layers: &Table<DocLayer>,
        vp_info: Vec<(Handle, String, Vec<Handle>)>,
    ) {
        // The rebuild below re-indexes rows, so capture the selection by name
        // first and re-resolve it after (indices alone would go stale).
        let anchor_name = self
            .selected
            .and_then(|i| self.layers.get(i))
            .map(|l| l.name.clone());
        let multi_names: Vec<String> = self
            .selected_multi
            .iter()
            .filter_map(|&i| self.layers.get(i).map(|l| l.name.clone()))
            .collect();

        self.vp_cols = vp_info
            .iter()
            .map(|(h, label, _)| VpCol {
                handle: *h,
                label: label.clone(),
            })
            .collect();

        self.layers = doc_layers
            .iter()
            .map(|l| {
                let layer_handle = l.handle;
                let vp_frozen = vp_info
                    .iter()
                    .map(|(_, _, frozen_handles)| frozen_handles.contains(&layer_handle))
                    .collect();
                Layer {
                    name: l.name.clone(),
                    visible: !l.flags.off,
                    frozen: l.flags.frozen,
                    locked: l.flags.locked,
                    plottable: l.is_plottable,
                    color: l.color,
                    linetype: if l.line_type.is_empty() {
                        "Continuous".to_string()
                    } else {
                        l.line_type.clone()
                    },
                    lineweight: l.line_weight,
                    transparency: (l.transparency.as_percent() * 100.0).round() as i32,
                    vp_frozen,
                }
            })
            .collect();

        // Re-resolve the selection against the rebuilt rows by name.
        self.selected = anchor_name
            .and_then(|n| self.layers.iter().position(|l| l.name == n));
        self.selected_multi = multi_names
            .iter()
            .filter_map(|n| self.layers.iter().position(|l| l.name == *n))
            .collect();

        self.apply_sort();
    }

    /// Set/flip the sort column from a header click, then re-sort.
    pub fn sort_by(&mut self, col: LayerSortCol) {
        if self.sort_col == Some(col) {
            self.sort_asc = !self.sort_asc;
        } else {
            self.sort_col = Some(col);
            self.sort_asc = true;
        }
        self.apply_sort();
    }

    pub fn refresh_sort(&mut self) {
        self.apply_sort();
    }

    /// Reorder `self.layers` by the active sort column, preserving the current
    /// selection by name. No-op in document order (`sort_col == None`).
    fn apply_sort(&mut self) {
        let Some(col) = self.sort_col else {
            return;
        };
        let asc = self.sort_asc;
        let sel_name = self
            .selected
            .and_then(|i| self.layers.get(i))
            .map(|l| l.name.clone());
        // Sort reorders rows, so the multi-selection (stored as indices) must
        // be re-resolved by name afterward or it would point at the wrong rows.
        let multi_names: Vec<String> = self
            .selected_multi
            .iter()
            .filter_map(|&i| self.layers.get(i).map(|l| l.name.clone()))
            .collect();

        use std::cmp::Ordering;
        self.layers.sort_by(|a, b| {
            let ord = match col {
                LayerSortCol::Name => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
                LayerSortCol::On => a.visible.cmp(&b.visible),
                LayerSortCol::Freeze => a.frozen.cmp(&b.frozen),
                LayerSortCol::Lock => a.locked.cmp(&b.locked),
                LayerSortCol::Plot => a.plottable.cmp(&b.plottable),
                LayerSortCol::Color => color_sort_key(a.color).cmp(&color_sort_key(b.color)),
                LayerSortCol::Linetype => {
                    a.linetype.to_lowercase().cmp(&b.linetype.to_lowercase())
                }
                LayerSortCol::Lineweight => a.lineweight.value().cmp(&b.lineweight.value()),
                LayerSortCol::Transparency => a.transparency.cmp(&b.transparency),
            };
            // Stable tie-break by name so equal keys keep a predictable order.
            let ord = ord.then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
            if asc {
                ord
            } else {
                match ord {
                    Ordering::Less => Ordering::Greater,
                    Ordering::Greater => Ordering::Less,
                    Ordering::Equal => Ordering::Equal,
                }
            }
        });

        if let Some(n) = sel_name {
            self.selected = self.layers.iter().position(|l| l.name == n);
        }
        self.selected_multi = multi_names
            .iter()
            .filter_map(|n| self.layers.iter().position(|l| l.name == *n))
            .collect();
    }

    pub fn sync_linetypes(&mut self, items: Vec<LinetypeItem>) {
        self.linetype_combo = combo_box::State::new(items.clone());
        self.linetype_items = items;
    }

    /// Render the layer panel as the full content of its own OS window.
    pub fn view_window(
        &self,
        name_col_w: f32,
        sizing: crate::ui::modal::ModalSizing,
    ) -> Element<'_, Message> {
        self.view_content(name_col_w, sizing)
    }

    fn view_content(
        &self,
        name_col_w: f32,
        sizing: crate::ui::modal::ModalSizing,
    ) -> Element<'_, Message> {
        let has_sel = self.selected.is_some();
        let sel_is_zero = self
            .selected
            .map(|i| self.layers.get(i).map(|l| l.name == "0").unwrap_or(false))
            .unwrap_or(false);
        let can_set_current = self
            .selected
            .and_then(|i| self.layers.get(i))
            .is_some_and(|layer| layer.name != self.current_layer);

        // ── Toolbar ───────────────────────────────────────────────────────
        let toolbar = container(
            row![
                toolbar_btn(crate::ui::icons::PLUS, t!("New"), Message::LayerNew),
                toolbar_btn_cond(
                    crate::ui::icons::TRASH,
                    t!("Delete"),
                    Message::LayerDelete,
                    has_sel && !sel_is_zero,
                ),
                toolbar_btn_cond(
                    crate::ui::icons::CHECK,
                    t!("Set Current"),
                    Message::LayerSetCurrent,
                    can_set_current,
                ),
                iced::widget::Space::new().width(sizing.width),
                // Search box: filters rows by name as the user types (#343).
                text_input(t!("Search…").as_ref(), &self.filter)
                    .on_input(Message::LayerManagerFilterChanged)
                    .size(FONT_SZ)
                    .padding([3, 6])
                    .width(Length::Fixed(180.0))
                    .style(table_input_style),
            ]
            .spacing(2)
            .align_y(iced::Center),
        )
        .style(|theme: &Theme| container::Style {
            background: Some(Background::Color(
                theme.palette().background.weak.color
            )),
            ..Default::default()
        })
        .width(sizing.width)
        .padding([4, 8]);

        // ── Column header ─────────────────────────────────────────────────
        let sc = self.sort_col;
        let sa = self.sort_asc;
        let mut header_row = row![
            text(t!("Status")).size(10).style(muted_style).width(50),
            sortable_header(t!("Name"), LayerSortCol::Name, Length::Fixed(name_col_w), sc, sa),
            // Draggable divider: adjusts the Name column width (#359).
            iced::widget::mouse_area(
                container(iced::widget::Space::new().width(2).height(14)).style(
                    |theme: &Theme| container::Style {
                        background: Some(Background::Color(
                            theme.palette().background.neutral.color
                        )),
                        ..Default::default()
                    },
                ),
            )
            .on_press(Message::LayerNameColGrab)
            .interaction(iced::mouse::Interaction::ResizingHorizontally),
            sortable_header(t!("On"), LayerSortCol::On, Length::Fixed(COL_ICON), sc, sa),
            sortable_header(t!("Freeze"), LayerSortCol::Freeze, Length::Fixed(COL_ICON), sc, sa),
            sortable_header(t!("Lock"), LayerSortCol::Lock, Length::Fixed(COL_ICON), sc, sa),
            sortable_header(t!("Plot"), LayerSortCol::Plot, Length::Fixed(COL_ICON), sc, sa),
            sortable_header(t!("Color"), LayerSortCol::Color, Length::Fixed(COL_COLOR), sc, sa),
            sortable_header(t!("Linetype"), LayerSortCol::Linetype, Length::Fixed(COL_LT), sc, sa),
            sortable_header(t!("Lineweight"), LayerSortCol::Lineweight, Length::Fixed(COL_LW), sc, sa),
            sortable_header(
                t!("Transparency"),
                LayerSortCol::Transparency,
                Length::Fixed(COL_TRANS),
                sc,
                sa
            ),
        ]
        .spacing(4)
        .width(sizing.width)
        .align_y(iced::Center);

        for vp in &self.vp_cols {
            header_row = header_row.push(
                text(vp.label.as_str())
                    .size(10)
                    .style(muted_style)
                    .width(Length::Fixed(COL_ICON)),
            );
        }

        let col_header = container(header_row)
            .style(|theme: &Theme| {
                let palette = theme.palette();
                container::Style {
                background: Some(Background::Color(palette.background.weak.color)),
                border: Border {
                    color: palette.background.neutral.color,
                    width: 1.0,
                    radius: 0.0.into(),
                },
                ..Default::default()
                }
            })
            .padding([4, 8])
            .width(sizing.width);

        // ── Layer rows ────────────────────────────────────────────────────
        let mut rows_col = column![].spacing(0);
        let filter = self.filter.to_lowercase();
        for (i, layer) in self.layers.iter().enumerate() {
            if !filter.is_empty() && !layer.name.to_lowercase().contains(&filter) {
                continue;
            }
            // Highlight every selected row; show the editable combos only on the
            // anchor (a single shared combo state can't drive several rows).
            let is_anchor = self.selected == Some(i);
            let is_sel = is_anchor || self.selected_multi.contains(&i);
            let is_current = layer.name == self.current_layer;
            let is_editing = self.editing == Some(i);
            let color_open = self.color_picker_row == Some(i);

            let (ltc, lwc) = if is_anchor {
                (Some(&self.linetype_combo), Some(&self.lw_combo))
            } else {
                (None, None)
            };

            rows_col = rows_col.push(layer_row(
                i,
                layer,
                is_sel,
                is_current,
                is_editing,
                &self.edit_buf,
                color_open,
                ltc,
                lwc,
                &self.vp_cols,
                name_col_w,
            ));

        }

        let table = scrollable(rows_col)
            .id(iced::advanced::widget::Id::new(LAYER_TABLE_SCROLL_ID))
            .height(sizing.height.min(240.0));

        // ── Full-window frame ─────────────────────────────────────────────
        container(column![toolbar, col_header, table].spacing(0))
            .style(|theme: &Theme| container::Style {
                background: Some(Background::Color(
                    theme.palette().background.base.color
                )),
                ..Default::default()
            })
            .width(sizing.width)
            .height(sizing.height)
            .into()
    }
}

// ── Sorting helpers ─────────────────────────────────────────────────────────

fn layer_cell_button_style(
    theme: &Theme,
    status: button::Status,
    is_selected: bool,
    index: usize,
) -> button::Style {
    let palette = theme.palette();
    let highlighted = matches!(status, button::Status::Hovered);
    let pair = if highlighted {
        palette.background.strong
    } else if is_selected {
        palette.primary.weak
    } else if index % 2 == 0 {
        palette.background.base
    } else {
        palette.background.weak
    };
    button::Style {
        background: highlighted.then_some(Background::Color(pair.color)),
        text_color: pair.text,
        ..Default::default()
    }
}

fn layer_header_button_style(theme: &Theme, status: button::Status) -> button::Style {
    let palette = theme.palette();
    let highlighted = matches!(
        status,
        button::Status::Hovered | button::Status::Pressed
    );
    let pair = if highlighted {
        palette.background.strong
    } else {
        palette.background.weak
    };
    button::Style {
        background: highlighted.then_some(Background::Color(pair.color)),
        text_color: pair.text,
        ..Default::default()
    }
}

/// Packed RGB key for ordering colours deterministically by hue-ish bytes.
fn color_sort_key(c: AcadColor) -> u32 {
    let c = iced_color_from_acad(&c);
    let q = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u32;
    (q(c.r) << 16) | (q(c.g) << 8) | q(c.b)
}

/// A clickable column header that sorts the table by `col`. Shows an up/down
/// SVG arrow when it is the active sort column (#133).
fn sortable_header<'a>(
    label: Cow<'static, str>,
    col: LayerSortCol,
    width: Length,
    active: Option<LayerSortCol>,
    asc: bool,
) -> Element<'a, Message> {
    let mut content = row![text(label).size(10).style(muted_style)]
        .spacing(3)
        .align_y(iced::Center);
    if active == Some(col) {
        content = content.push(if asc {
            crate::ui::icons::themed_arrow_up(8.0)
        } else {
            crate::ui::icons::themed_arrow_down(8.0)
        });
    }
    button(content)
        .on_press(Message::LayerSort(col))
        .style(layer_header_button_style)
        .padding(Padding {
            top: 0.0,
            bottom: 0.0,
            left: 2.0,
            right: 2.0,
        })
        .width(width)
        .into()
}

// ── Toolbar buttons ───────────────────────────────────────────────────────

fn toolbar_btn<'a>(icon: &'static [u8], label: Cow<'static, str>, msg: Message) -> Element<'a, Message> {
    button(
        row![
            crate::ui::icons::themed(icon, 12.0),
            text(label).size(11),
        ]
        .spacing(5)
        .align_y(iced::Center),
    )
    .on_press(msg)
        .style(|theme: &Theme, status| {
            let palette = theme.palette();
            let pair = match status {
                button::Status::Hovered | button::Status::Pressed => {
                    palette.background.strong
                }
                _ => palette.background.weak,
            };
            button::Style {
            background: Some(Background::Color(pair.color)),
            border: Border {
                radius: 3.0.into(),
                color: palette.background.neutral.color,
                width: 1.0,
            },
            text_color: pair.text,
            ..Default::default()
            }
        })
        .padding([4, 10])
        .into()
}

fn toolbar_btn_cond<'a>(
    icon: &'static [u8],
    label: Cow<'static, str>,
    msg: Message,
    enabled: bool,
) -> Element<'a, Message> {
    let mut b = button(
        row![
            if enabled {
                crate::ui::icons::themed(icon, 12.0)
            } else {
                crate::ui::icons::themed_disabled(icon, 12.0)
            },
            if enabled {
                text(label).size(11)
            } else {
                text(label).size(11).style(|theme: &Theme| iced::widget::text::Style {
                    color: Some(
                        theme.palette().background.base.text.scale_alpha(0.42)
                    ),
                })
            },
        ]
        .spacing(5)
        .align_y(iced::Center),
    )
    .style(move |theme: &Theme, status| {
        let palette = theme.palette();
        let pair = match status {
            button::Status::Hovered if enabled => palette.background.strong,
            _ => palette.background.weak,
        };
        button::Style {
        background: Some(Background::Color(pair.color)),
        border: Border {
            radius: 3.0.into(),
            color: palette.background.neutral.color,
            width: 1.0,
        },
        text_color: if enabled {
            pair.text
        } else {
            pair.text.scale_alpha(0.42)
        },
        ..Default::default()
        }
    })
    .padding([4, 10]);
    if enabled {
        b = b.on_press(msg);
    }
    b.into()
}

// ── Layer row ─────────────────────────────────────────────────────────────

#[allow(clippy::too_many_arguments)]
/// Hover popup showing a layer's full name when the cell truncates it.
fn name_tip<'a>(name: &'a str) -> Element<'a, Message> {
    container(text(name).size(FONT_SZ))
        .padding(Padding {
            top: 3.0,
            bottom: 3.0,
            left: 7.0,
            right: 7.0,
        })
        .style(|theme: &Theme| {
            let palette = theme.palette();
            container::Style {
            background: Some(Background::Color(palette.background.strong.color)),
            border: Border {
                color: palette.background.neutral.color,
                width: 1.0,
                radius: 3.0.into(),
            },
            text_color: Some(palette.background.strong.text),
            ..Default::default()
            }
        })
        .into()
}

fn layer_row<'a>(
    index: usize,
    layer: &'a Layer,
    is_selected: bool,
    is_current: bool,
    is_editing: bool,
    edit_buf: &'a str,
    color_picker_open: bool,
    lt_combo: Option<&'a combo_box::State<LinetypeItem>>,
    lw_combo_state: Option<&'a combo_box::State<LwItem>>,
    vp_cols: &'a [VpCol],
    name_col_w: f32,
) -> Element<'a, Message> {
    let svg_btn = |bytes: &'static [u8], on_press: Message| -> Element<'a, Message> {
        button(crate::ui::icons::semantic(bytes, ICON_SZ))
        .on_press(on_press)
        .style(move |theme: &Theme, status| {
            layer_cell_button_style(theme, status, is_selected, index)
        })
        .padding(Padding {
            top: COMBO_PAD_V,
            bottom: COMBO_PAD_V,
            left: 4.0,
            right: 4.0,
        })
        .height(Length::Fixed(ROW_H))
        .into()
    };

    let vis_svg = crate::ui::icons::layer_visible(layer.visible);
    let frz_svg = crate::ui::icons::layer_freeze(layer.frozen);
    let lck_svg = crate::ui::icons::layer_lock(layer.locked);
    let plot_icon: Element<'_, Message> = if layer.plottable {
        crate::ui::icons::themed(crate::ui::icons::PRINT, ICON_SZ)
    } else {
        row![
            crate::ui::icons::themed(crate::ui::icons::PRINT, ICON_SZ),
            crate::ui::icons::themed_danger(crate::ui::icons::CLOSE, ICON_SZ * 0.65),
        ]
        .spacing(0)
        .align_y(iced::Center)
        .into()
    };

    let plot_btn: Element<'_, Message> = button(plot_icon)
        .on_press(Message::LayerTogglePlot(index))
        .style(move |theme: &Theme, status| {
            layer_cell_button_style(theme, status, is_selected, index)
        })
        .padding(Padding {
            top: COMBO_PAD_V,
            bottom: COMBO_PAD_V,
            left: 4.0,
            right: 4.0,
        })
        .height(Length::Fixed(ROW_H))
        .into();

    let status_dot: Element<'_, Message> = if is_current {
        crate::ui::icons::themed_success(crate::ui::icons::CHECK, 13.0)
    } else {
        crate::ui::icons::themed_secondary(crate::ui::icons::DOT, 9.0)
    };

    // Name cell
    let name_cell: Element<'_, Message> = if is_editing {
        text_input("", edit_buf)
            .on_input(Message::LayerRenameEdit)
            .on_submit(Message::LayerRenameCommit)
            .size(FONT_SZ)
            .padding(Padding {
                top: COMBO_PAD_V,
                bottom: COMBO_PAD_V,
                left: 4.0,
                right: 4.0,
            })
            .style(table_input_style)
            .width(Length::Fixed(name_col_w))
            .into()
    } else {
        // ~6 px per glyph at the 10 px row font — track the column width.
        let name_budget = ((name_col_w / 6.0) as usize).max(8);
        let name_btn = button(
            text(crate::ui::text_util::elide(&layer.name, name_budget))
                .size(FONT_SZ),
        )
        .on_press(Message::LayerRenameStart(index))
        .style(move |theme: &Theme, status| {
            layer_cell_button_style(theme, status, is_selected, index)
        })
        .padding(Padding {
            top: COMBO_PAD_V,
            bottom: COMBO_PAD_V,
            left: 4.0,
            right: 4.0,
        })
        .height(Length::Fixed(ROW_H))
        .width(Length::Fixed(name_col_w));
        // When the name is truncated, reveal the full text on hover so the
        // user can still read it without widening the column.
        if layer.name.chars().count() > name_budget {
            tooltip(name_btn, name_tip(&layer.name), tooltip::Position::FollowCursor).into()
        } else {
            name_btn.into()
        }
    };

    // Color cell — looks like a combo_box input; click opens swatch dropdown below row.
    // Shared colour selector. Layers carry a concrete colour (no ByLayer /
    // ByBlock); true colours stay RGB instead of being collapsed to ACI 7.
    let color_cell: Element<'_, Message> = container(crate::ui::color_select::color_selector(
        layer.color,
        color_picker_open,
        crate::ui::color_select::ColorExtras {
            by_layer: false,
            by_block: false,
            ..Default::default()
        },
        Message::LayerColorSet,
        Message::LayerColorPickerToggle(index),
        Message::OpenColorWindow(
            crate::app::ColorPickTarget::Layer(index),
            layer.color,
        ),
    ))
    .width(Length::Fixed(COL_COLOR))
    .into();

    // Linetype cell — uses LinetypeItem (with ASCII art) same as Properties panel
    let cur_lt_item = LinetypeItem {
        name: layer.linetype.clone(),
        art: String::new(), // art comes from combo state items; just match by name
    };
    let lt_cell: Element<'_, Message> = if let Some(state) = lt_combo {
        crate::ui::wide_menu::wide_menu(
            combo_box(
                state,
                t!("linetype").as_ref(),
                Some(&cur_lt_item),
                |item: LinetypeItem| Message::LayerLinetypeSet(item.name),
            )
            .size(FONT_SZ)
            .padding(Padding {
                top: COMBO_PAD_V,
                bottom: COMBO_PAD_V,
                left: 4.0,
                right: 4.0,
            })
            .width(Length::Fixed(COL_LT))
            .input_style(combo_input_style),
            LINETYPE_MENU_W,
        )
    } else {
        text(layer.linetype.as_str())
            .size(FONT_SZ)
            .style(muted_style)
            .width(Length::Fixed(COL_LT))
            .into()
    };

    // Lineweight cell
    let cur_lw_item = LwItem(layer.lineweight);
    let lw_cell: Element<'_, Message> = if let Some(state) = lw_combo_state {
        combo_box(state, t!("lineweight").as_ref(), Some(&cur_lw_item), |item: LwItem| {
            Message::LayerLineweightSet(item.0)
        })
        .size(FONT_SZ)
        .padding(Padding {
            top: COMBO_PAD_V,
            bottom: COMBO_PAD_V,
            left: 4.0,
            right: 4.0,
        })
        .width(Length::Fixed(COL_LW))
        .input_style(combo_input_style)
        .into()
    } else {
        text(cur_lw_item.to_string())
            .size(FONT_SZ)
            .style(muted_style)
            .width(Length::Fixed(COL_LW))
            .into()
    };

    // Transparency cell
    let trans_str = layer.transparency.to_string();
    let trans_cell = text_input("0", &trans_str)
        .on_input(move |s| Message::LayerTransparencyEdit(index, s))
        .size(FONT_SZ)
        .padding(Padding {
            top: COMBO_PAD_V,
            bottom: COMBO_PAD_V,
            left: 4.0,
            right: 4.0,
        })
        .style(|theme: &Theme, status| {
            let mut style = table_input_style(theme, status);
            style.background = iced::Background::Color(Color::TRANSPARENT);
            style
        })
        .width(Length::Fixed(COL_TRANS));

    let mut row_content = row![
        container(status_dot)
            .width(50)
            .align_x(iced::Center)
            .align_y(iced::Center),
        name_cell,
        iced::widget::Space::new().width(2),
        container(svg_btn(vis_svg, Message::LayerToggleVisible(index)))
            .width(Length::Fixed(COL_ICON))
            .align_x(iced::Center),
        container(svg_btn(frz_svg, Message::LayerToggleFreeze(index)))
            .width(Length::Fixed(COL_ICON))
            .align_x(iced::Center),
        container(svg_btn(lck_svg, Message::LayerToggleLock(index)))
            .width(Length::Fixed(COL_ICON))
            .align_x(iced::Center),
        container(plot_btn)
            .width(Length::Fixed(COL_ICON))
            .align_x(iced::Center),
        color_cell,
        lt_cell,
        lw_cell,
        trans_cell,
    ]
    .spacing(4)
    .width(Fill)
    .align_y(iced::Center);

    // Per-viewport freeze columns
    for (vp_idx, _vp_col) in vp_cols.iter().enumerate() {
        let is_vp_frozen = layer.vp_frozen.get(vp_idx).copied().unwrap_or(false);
        let vp_frz_svg = crate::ui::icons::layer_freeze(is_vp_frozen);
        row_content = row_content.push(
            container(svg_btn(
                vp_frz_svg,
                Message::LayerToggleVpFreeze(index, vp_idx),
            ))
            .width(Length::Fixed(COL_ICON))
            .align_x(iced::Center),
        );
    }

    mouse_area(
        container(row_content)
            .style(move |theme: &Theme| {
                let palette = theme.palette();
                let pair = if is_selected {
                    palette.primary.weak
                } else if index % 2 == 0 {
                    palette.background.base
                } else {
                    palette.background.weak
                };
                container::Style {
                background: Some(Background::Color(pair.color)),
                text_color: Some(pair.text),
                ..Default::default()
                }
            })
            .padding(Padding {
                top: 0.0,
                bottom: 0.0,
                left: 8.0,
                right: 8.0,
            })
            .height(Length::Fixed(ROW_H))
            .width(Fill),
    )
    .on_press(Message::LayerSelect(index))
    .into()
}

// ── Combo style ────────────────────────────────────────────────────────────

fn combo_input_style(
    theme: &Theme,
    status: iced::widget::text_input::Status,
) -> iced::widget::text_input::Style {
    table_input_style(theme, status)
}

// ── Display helpers ───────────────────────────────────────────────────────

pub fn iced_color_from_acad(c: &AcadColor) -> Color {
    match c {
        AcadColor::Index(i) => {
            let (r, g, b) = aci_to_rgb(*i).unwrap_or((200, 200, 200));
            Color::from_rgb(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0)
        }
        AcadColor::Rgb { r, g, b } => {
            Color::from_rgb(*r as f32 / 255.0, *g as f32 / 255.0, *b as f32 / 255.0)
        }
        _ => Color::WHITE,
    }
}

// ── Column widths ─────────────────────────────────────────────────────────

// Name column width is user-adjustable via the header divider drag (#359);
// the app passes the current width into `view_window`.
const COL_ICON: f32 = 44.0;
const COL_COLOR: f32 = 90.0;
const COL_LT: f32 = 110.0;
const COL_LW: f32 = 90.0;
const COL_TRANS: f32 = 80.0;
