use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::time::Instant;

use mlt_core::GeometryType;
use mlt_core::geo_types::{Geometry, Polygon};
use mlt_core::geojson::{Feature, FeatureCollection};
use ratatui::layout::{Constraint, Rect};
use ratatui::widgets::TableState;
use rstar::{PointDistance as _, RTree};
use usize_cast::IntoUsize as _;

use crate::ls::{FileAlgorithm, FileSortColumn, LsRow};
use crate::ui::mbt::MbtilesState;
use crate::ui::{
    GeometryIndexEntry, auto_expand, coord_f64, group_by_layer, is_entry_visible, load_fc,
    multi_part_count,
};

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ViewMode {
    FileBrowser,
    LayerOverview,
    /// Interactive world map viewer for .mbtiles files.
    MbtilesMap,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResizeHandle {
    LeftRight,
    FeaturesProperties,
    PropertiesGeometry,
    FileBrowserLeftRight,
    FileBrowserPreviewFilter,
    FileBrowserFilterInfo,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TreeItem {
    All,
    Layer(usize),
    Feature {
        layer: usize,
        feat: usize,
    },
    SubFeature {
        layer: usize,
        feat: usize,
        part: usize,
    },
}

impl TreeItem {
    pub(crate) fn layer_feat_part(&self) -> Option<(usize, usize, Option<usize>)> {
        match self {
            Self::Feature { layer, feat } => Some((*layer, *feat, None)),
            Self::SubFeature { layer, feat, part } => Some((*layer, *feat, Some(*part))),
            _ => None,
        }
    }
}

#[derive(Clone, PartialEq, Eq)]
pub struct HoveredInfo {
    pub(crate) tree_idx: usize,
    pub(crate) layer: usize,
    pub(crate) feat: usize,
    pub(crate) part: Option<usize>,
}

impl HoveredInfo {
    pub fn new(tree_idx: usize, layer: usize, feat: usize, part: Option<usize>) -> Self {
        Self {
            tree_idx,
            layer,
            feat,
            part,
        }
    }
}

pub struct LayerGroup {
    pub(crate) name: String,
    pub(crate) extent: u32,
    pub(crate) feature_indices: Vec<usize>,
}

impl LayerGroup {
    pub fn new(name: String, extent: u32, feature_indices: Vec<usize>) -> Self {
        Self {
            name,
            extent,
            feature_indices,
        }
    }
}

pub type PreviewValue = (PathBuf, Result<(FeatureCollection, u32), ()>);

pub struct App {
    pub(crate) mode: ViewMode,
    pub(crate) files: Vec<LsRow>,
    pub(crate) selected_file_index: usize,
    pub(crate) file_list_state: TableState,
    pub(crate) analysis_rx: Option<mpsc::Receiver<Vec<LsRow>>>,
    /// Base path for file browser; used when drawing to show relative paths.
    pub(crate) file_browser_base: Option<PathBuf>,
    file_sort: Option<(FileSortColumn, bool)>,
    pub(crate) file_table_area: Option<Rect>,
    pub(crate) file_table_widths: Option<[Constraint; 6]>,
    pub(crate) current_file: Option<PathBuf>,
    pub(crate) fc: FeatureCollection,
    pub(crate) layer_groups: Vec<LayerGroup>,
    pub(crate) tree_items: Vec<TreeItem>,
    pub(crate) selected_index: usize,
    pub(crate) hovered: Option<HoveredInfo>,
    expanded_layers: Vec<bool>,
    pub(crate) expanded_features: HashSet<(usize, usize)>,
    last_scroll_time: Instant,
    scroll_speed: usize,
    pub(crate) needs_redraw: bool,
    cached_bounds: Option<(f64, f64, f64, f64)>,
    cached_bounds_key: usize,
    pub(crate) left_pct: u16,
    pub(crate) features_pct: u16,
    pub(crate) resizing: Option<ResizeHandle>,
    pub(crate) properties_scroll: u16,
    pub(crate) tree_scroll: u16,
    pub(crate) tree_inner_height: usize,
    pub(crate) last_properties_key: Option<(usize, usize)>,
    pub(crate) properties_pct: u16,
    geometry_index: Option<RTree<GeometryIndexEntry>>,
    pub(crate) file_left_pct: u16,
    pub(crate) file_preview_pct: u16,
    pub(crate) file_filter_pct: u16,
    pub(crate) ext_filters: HashSet<String>,
    pub(crate) geom_filters: HashSet<GeometryType>,
    pub(crate) algo_filters: HashSet<FileAlgorithm>,
    pub(crate) filter_scroll: u16,
    pub(crate) filtered_file_indices: Vec<usize>,
    pub(crate) file_table_inner_height: usize,
    pub(crate) show_help: bool,
    pub(crate) help_scroll: u16,
    pub(crate) error_popup: Option<(String, String)>,
    pub(crate) file_info_scroll: u16,
    pub(crate) preview_tile_path: Option<PathBuf>,
    pub(crate) preview_fc: Option<FeatureCollection>,
    pub(crate) preview_extent: u32,
    pub(crate) preview_rx: Option<mpsc::Receiver<PreviewValue>>,
    pub(crate) preview_load_requested: Option<PathBuf>,
    /// State for the `MbtilesMap` mode (Some only when mode == `MbtilesMap`).
    pub(crate) mbt_state: Option<Box<MbtilesState>>,
}

impl Default for App {
    fn default() -> Self {
        Self {
            mode: ViewMode::FileBrowser,
            files: Vec::new(),
            selected_file_index: 0,
            file_list_state: TableState::default(),
            analysis_rx: None,
            file_browser_base: None,
            file_sort: None,
            file_table_area: None,
            file_table_widths: None,
            current_file: None,
            fc: FeatureCollection {
                features: Vec::new(),
                ty: "FeatureCollection".into(),
            },
            layer_groups: Vec::new(),
            tree_items: Vec::new(),
            selected_index: 0,
            hovered: None,
            expanded_layers: Vec::new(),
            expanded_features: HashSet::new(),
            last_scroll_time: Instant::now(),
            scroll_speed: 1,
            needs_redraw: true,
            cached_bounds: None,
            cached_bounds_key: usize::MAX,
            left_pct: 30,
            features_pct: 50,
            resizing: None,
            properties_scroll: 0,
            last_properties_key: None,
            tree_scroll: 0,
            tree_inner_height: 0,
            properties_pct: 50,
            geometry_index: None,
            file_left_pct: 70,
            file_preview_pct: 33,
            file_filter_pct: 33,
            ext_filters: HashSet::new(),
            geom_filters: HashSet::new(),
            algo_filters: HashSet::new(),
            filter_scroll: 0,
            filtered_file_indices: Vec::new(),
            file_table_inner_height: 10,
            show_help: false,
            help_scroll: 0,
            error_popup: None,
            file_info_scroll: 0,
            preview_tile_path: None,
            preview_fc: None,
            preview_extent: 4096,
            preview_rx: None,
            preview_load_requested: None,
            mbt_state: None,
        }
    }
}

impl App {
    pub(crate) fn new_file_browser(
        files: Vec<LsRow>,
        analysis_rx: Option<mpsc::Receiver<Vec<LsRow>>>,
        base_path: PathBuf,
    ) -> Self {
        let mut file_list_state = TableState::default();
        file_list_state.select(Some(0));
        let filtered_file_indices = (0..files.len()).collect();
        Self {
            files,
            file_list_state,
            analysis_rx,
            file_browser_base: Some(base_path),
            filtered_file_indices,
            ..Self::default()
        }
    }

    pub(crate) fn new_mbtiles(mbt_state: MbtilesState, path: PathBuf) -> Self {
        Self {
            mode: ViewMode::MbtilesMap,
            current_file: Some(path),
            mbt_state: Some(Box::new(mbt_state)),
            ..Self::default()
        }
    }

    pub(crate) fn new_single_file(fc: FeatureCollection, path: Option<PathBuf>) -> Self {
        let layer_groups = group_by_layer(&fc);
        let expanded_layers = auto_expand(&layer_groups);
        let mut app = Self {
            mode: ViewMode::LayerOverview,
            current_file: path,
            expanded_layers,
            layer_groups,
            fc,
            ..Self::default()
        };
        app.build_geometry_index();
        app.build_tree_items();
        app
    }

    pub(crate) fn data_loaded(&self) -> bool {
        self.analysis_rx.is_none()
            && !self
                .files
                .iter()
                .any(|r| matches!(r, LsRow::Loading { .. }))
    }

    pub(crate) fn open_help(&mut self) {
        self.show_help = true;
        self.help_scroll = 0;
        self.invalidate();
    }

    pub(crate) fn handle_file_header_click(&mut self, col: FileSortColumn) {
        if !self.data_loaded() {
            return;
        }
        let prev = self.get_selected_file().map(|r| r.path().to_path_buf());
        let asc = !matches!(self.file_sort, Some((c, a)) if c == col && a);
        self.file_sort = Some((col, asc));
        self.files.sort_by(|a, b| file_cmp(a, b, col, asc));
        self.rebuild_filtered_files();
        if let Some(path) = prev
            && let Some(pos) = self
                .filtered_file_indices
                .iter()
                .position(|&i| self.files[i].path() == path.as_path())
        {
            self.selected_file_index = pos;
            self.file_list_state.select(Some(pos));
        }
    }

    fn load_file(&mut self, path: &Path) -> anyhow::Result<()> {
        self.fc = load_fc(path)?;
        self.layer_groups = group_by_layer(&self.fc);
        self.current_file = Some(path.to_path_buf());
        self.mode = ViewMode::LayerOverview;
        self.expanded_layers = auto_expand(&self.layer_groups);
        self.expanded_features.clear();
        self.build_geometry_index();
        self.build_tree_items();
        self.selected_index = 0;
        self.invalidate_bounds();
        Ok(())
    }

    pub(crate) fn global_idx(&self, layer: usize, feat: usize) -> usize {
        self.layer_groups[layer].feature_indices[feat]
    }

    pub(crate) fn feature(&self, layer: usize, feat: usize) -> &Feature {
        &self.fc.features[self.global_idx(layer, feat)]
    }

    pub(crate) fn extent(&self) -> u32 {
        self.layer_groups.first().map_or(4096, |g| g.extent)
    }

    pub(crate) fn selected_item(&self) -> &TreeItem {
        &self.tree_items[self.selected_index]
    }

    fn build_tree_items(&mut self) {
        self.tree_items.clear();
        self.tree_items.push(TreeItem::All);
        for (li, group) in self.layer_groups.iter().enumerate() {
            self.tree_items.push(TreeItem::Layer(li));
            if !self.expanded_layers.get(li).copied().unwrap_or(false) {
                continue;
            }
            for (fi, &gi) in group.feature_indices.iter().enumerate() {
                self.tree_items.push(TreeItem::Feature {
                    layer: li,
                    feat: fi,
                });
                if self.expanded_features.contains(&(li, fi)) {
                    for part in 0..multi_part_count(&self.fc.features[gi].geometry) {
                        self.tree_items.push(TreeItem::SubFeature {
                            layer: li,
                            feat: fi,
                            part,
                        });
                    }
                }
            }
        }
    }

    fn build_geometry_index(&mut self) {
        let mut entries: Vec<GeometryIndexEntry> = Vec::new();
        for (li, group) in self.layer_groups.iter().enumerate() {
            for (fi, &gi) in group.feature_indices.iter().enumerate() {
                let geom = &self.fc.features[gi].geometry;
                let n = multi_part_count(geom);
                let parts: Vec<Option<usize>> = if n == 0 {
                    vec![None]
                } else {
                    (0..n).map(Some).collect()
                };
                for part in parts {
                    let vertices = geometry_vertices(geom, part);
                    if !vertices.is_empty() {
                        entries.push(GeometryIndexEntry {
                            layer: li,
                            feat: fi,
                            part,
                            vertices,
                        });
                    }
                }
            }
        }
        self.geometry_index = (!entries.is_empty()).then(|| RTree::bulk_load(entries));
    }

    pub(crate) fn scroll_step(&mut self) -> usize {
        let now = Instant::now();
        let elapsed = now.duration_since(self.last_scroll_time).as_millis();
        self.last_scroll_time = now;
        self.scroll_speed = match elapsed {
            0..50 => (self.scroll_speed + 1).min(20),
            50..120 => self.scroll_speed.max(2),
            _ => 1,
        };
        self.scroll_speed
    }

    pub(crate) fn move_up_by(&mut self, n: usize) {
        self.move_by(n, false);
    }

    pub(crate) fn move_down_by(&mut self, n: usize) {
        self.move_by(n, true);
    }

    fn move_by(&mut self, n: usize, down: bool) {
        match self.mode {
            ViewMode::FileBrowser => {
                let prev = self.selected_file_index;
                let max = self.filtered_file_indices.len().saturating_sub(1);
                self.selected_file_index = if down {
                    self.selected_file_index.saturating_add(n).min(max)
                } else {
                    self.selected_file_index.saturating_sub(n).min(max)
                };
                self.file_list_state.select(Some(self.selected_file_index));
                if self.selected_file_index != prev {
                    self.invalidate_bounds();
                }
            }
            ViewMode::LayerOverview => {
                let prev = self.selected_index;
                let max = self.tree_items.len().saturating_sub(1);
                self.selected_index = if down {
                    self.selected_index.saturating_add(n).min(max)
                } else {
                    self.selected_index.saturating_sub(n)
                };
                if self.selected_index != prev {
                    self.scroll_selected_into_view(self.tree_inner_height);
                    self.invalidate_bounds();
                }
            }
            ViewMode::MbtilesMap => {}
        }
    }

    pub(crate) fn move_to_start(&mut self) {
        self.move_up_by(usize::MAX);
    }

    pub(crate) fn move_to_end(&mut self) {
        self.move_down_by(usize::MAX);
    }

    pub(crate) fn page_size(&self) -> usize {
        match self.mode {
            ViewMode::FileBrowser => self.file_table_inner_height,
            ViewMode::LayerOverview | ViewMode::MbtilesMap => self.tree_inner_height,
        }
    }

    pub(crate) fn handle_enter(&mut self) {
        match self.mode {
            ViewMode::FileBrowser => {
                let path = self.get_selected_file().map(|r| r.path().to_path_buf());
                let (path_str, is_error, error_msg) = self
                    .get_selected_file()
                    .map(|r| {
                        let s = r.path().display().to_string();
                        match r {
                            LsRow::Error { error, .. } => (s, true, error.clone()),
                            _ => (s, false, String::new()),
                        }
                    })
                    .unwrap_or_default();
                if let Some(path) = path {
                    if is_error {
                        self.error_popup = Some((path_str, error_msg));
                        self.invalidate();
                    } else if let Err(e) = self.load_file(&path) {
                        self.error_popup = Some((path_str, e.to_string()));
                        self.invalidate();
                    }
                }
            }
            ViewMode::LayerOverview => match self.tree_items.get(self.selected_index) {
                Some(TreeItem::Layer(li)) => {
                    let li = *li;
                    if li < self.expanded_layers.len() {
                        self.expanded_layers[li] = !self.expanded_layers[li];
                        self.build_tree_items();
                        self.invalidate();
                    }
                }
                Some(TreeItem::Feature { layer, feat }) => {
                    let key = (*layer, *feat);
                    if multi_part_count(&self.feature(key.0, key.1).geometry) > 0 {
                        if !self.expanded_features.remove(&key) {
                            self.expanded_features.insert(key);
                        }
                        self.build_tree_items();
                        self.invalidate();
                    }
                }
                _ => {}
            },
            ViewMode::MbtilesMap => {}
        }
    }

    pub(crate) fn handle_plus(&mut self) {
        if self.mode != ViewMode::LayerOverview {
            return;
        }
        match self.tree_items.get(self.selected_index) {
            Some(TreeItem::Layer(li)) => {
                let li = *li;
                if li < self.expanded_layers.len() && !self.expanded_layers[li] {
                    self.expanded_layers[li] = true;
                    self.build_tree_items();
                    self.invalidate();
                }
            }
            Some(TreeItem::Feature { layer, feat }) => {
                let key = (*layer, *feat);
                if multi_part_count(&self.feature(key.0, key.1).geometry) > 0
                    && !self.expanded_features.contains(&key)
                {
                    self.expanded_features.insert(key);
                    self.build_tree_items();
                    self.invalidate();
                }
            }
            _ => {}
        }
    }

    pub(crate) fn handle_minus(&mut self) {
        if self.mode != ViewMode::LayerOverview {
            return;
        }
        match self.tree_items.get(self.selected_index).cloned() {
            Some(TreeItem::Layer(li))
                if li < self.expanded_layers.len() && self.expanded_layers[li] =>
            {
                self.expanded_layers[li] = false;
                self.rebuild_and_clamp();
            }
            Some(TreeItem::Feature { layer, feat }) => {
                if self.expanded_features.remove(&(layer, feat)) {
                    self.rebuild_and_clamp();
                } else if layer < self.expanded_layers.len() && self.expanded_layers[layer] {
                    self.expanded_layers[layer] = false;
                    self.rebuild_and_select(|it| matches!(it, TreeItem::Layer(l) if *l == layer));
                }
            }
            Some(TreeItem::SubFeature { layer, feat, .. }) => {
                self.expanded_features.remove(&(layer, feat));
                self.rebuild_and_select(|it| {
                    matches!(it, TreeItem::Feature { layer: l, feat: f } if *l == layer && *f == feat)
                });
            }
            _ => {}
        }
    }

    pub(crate) fn handle_star(&mut self) {
        if self.mode != ViewMode::LayerOverview {
            return;
        }
        let new = !self.expanded_layers.iter().all(|&e| e);
        self.expanded_layers.fill(new);
        self.rebuild_and_clamp();
    }

    pub(crate) fn handle_escape(&mut self) -> bool {
        match self.mode {
            ViewMode::FileBrowser | ViewMode::MbtilesMap => true,
            ViewMode::LayerOverview if self.files.is_empty() => true,
            ViewMode::LayerOverview => {
                self.mode = ViewMode::FileBrowser;
                self.invalidate_bounds();
                false
            }
        }
    }

    pub(crate) fn handle_left_arrow(&mut self) {
        if self.mode != ViewMode::LayerOverview {
            return;
        }
        let Some(item) = self.tree_items.get(self.selected_index).cloned() else {
            return;
        };
        let target = match item {
            TreeItem::SubFeature { layer, feat, .. } => self.tree_items.iter().position(|t| {
                matches!(t, TreeItem::Feature { layer: l, feat: f } if *l == layer && *f == feat)
            }),
            TreeItem::Feature { layer, .. } => self
                .tree_items
                .iter()
                .position(|t| matches!(t, TreeItem::Layer(l) if *l == layer)),
            TreeItem::Layer(_) => Some(0),
            TreeItem::All => {
                if !self.files.is_empty() {
                    self.mode = ViewMode::FileBrowser;
                }
                return;
            }
        };
        if let Some(idx) = target
            && idx != self.selected_index
        {
            self.selected_index = idx;
            self.invalidate_bounds();
        }
    }

    fn rebuild_and_clamp(&mut self) {
        self.build_tree_items();
        self.selected_index = self
            .selected_index
            .min(self.tree_items.len().saturating_sub(1));
        self.invalidate_bounds();
    }

    fn rebuild_and_select(&mut self, pred: impl Fn(&TreeItem) -> bool) {
        self.build_tree_items();
        if let Some(idx) = self.tree_items.iter().position(pred) {
            self.selected_index = idx;
        }
        self.invalidate_bounds();
    }

    pub(crate) fn invalidate(&mut self) {
        self.needs_redraw = true;
    }

    pub(crate) fn invalidate_bounds(&mut self) {
        self.cached_bounds = None;
        self.invalidate();
    }

    pub(crate) fn selected_file_real_index(&self) -> Option<usize> {
        self.filtered_file_indices
            .get(self.selected_file_index)
            .copied()
    }

    pub(crate) fn get_selected_file(&self) -> Option<&LsRow> {
        self.selected_file_real_index()
            .and_then(|i| self.files.get(i))
    }

    pub(crate) fn rebuild_filtered_files(&mut self) {
        let prev = self.selected_file_real_index();
        let has_filters = !self.ext_filters.is_empty()
            || !self.geom_filters.is_empty()
            || !self.algo_filters.is_empty();
        self.filtered_file_indices = (0..self.files.len())
            .filter(|&i| {
                if !has_filters {
                    return true;
                }
                let ext = self.files[i]
                    .path()
                    .extension()
                    .and_then(|e| e.to_str())
                    .map(str::to_lowercase)
                    .unwrap_or_default();
                if !self.ext_filters.is_empty()
                    && !self.ext_filters.iter().any(|f| f.as_str() == ext)
                {
                    return false;
                }
                match &self.files[i] {
                    LsRow::Info { info, .. } => {
                        self.geom_filters
                            .iter()
                            .all(|g| info.geometries.contains(g))
                            && self
                                .algo_filters
                                .iter()
                                .all(|a| info.algorithms.contains(a))
                    }
                    _ => true,
                }
            })
            .collect();
        let pos = prev
            .and_then(|ri| self.filtered_file_indices.iter().position(|&i| i == ri))
            .unwrap_or(0);
        self.selected_file_index = pos;
        self.file_list_state.select(Some(pos));
        self.invalidate();
    }

    pub(crate) fn get_bounds(&mut self) -> (f64, f64, f64, f64) {
        if self.cached_bounds_key != self.selected_index || self.cached_bounds.is_none() {
            self.cached_bounds = Some(self.calculate_bounds());
            self.cached_bounds_key = self.selected_index;
        }
        self.cached_bounds.unwrap()
    }

    pub fn calculate_bounds(&self) -> (f64, f64, f64, f64) {
        let sel = self.selected_item();
        let ext = self.extent();
        let (mut x0, mut y0) = (f64::INFINITY, f64::INFINITY);
        let (mut x1, mut y1) = (f64::NEG_INFINITY, f64::NEG_INFINITY);

        let mut update = |v: &[f64; 2]| {
            x0 = x0.min(v[0]);
            y0 = y0.min(v[1]);
            x1 = x1.max(v[0]);
            y1 = y1.max(v[1]);
        };

        let geoms: Vec<&Geometry<i32>> = match sel {
            TreeItem::All => self.fc.features.iter().map(|f| &f.geometry).collect(),
            TreeItem::Layer(l) => self.layer_groups[*l]
                .feature_indices
                .iter()
                .map(|&gi| &self.fc.features[gi].geometry)
                .collect(),
            TreeItem::Feature { layer, feat } | TreeItem::SubFeature { layer, feat, .. } => {
                vec![&self.feature(*layer, *feat).geometry]
            }
        };
        for geom in geoms {
            for v in geometry_vertices(geom, None) {
                update(&v);
            }
        }

        x0 = x0.min(0.0);
        y0 = y0.min(0.0);
        x1 = x1.max(f64::from(ext));
        y1 = y1.max(f64::from(ext));
        let px = (x1 - x0) * 0.1;
        let py = (y1 - y0) * 0.1;
        (x0 - px, y0 - py, x1 + px, y1 + py)
    }

    pub(crate) fn find_hovered_feature(&mut self, cx: f64, cy: f64, bounds: (f64, f64, f64, f64)) {
        let sel = self.selected_item().clone();
        let threshold = (bounds.2 - bounds.0).max(bounds.3 - bounds.1) * 0.02;
        let thresh_sq = threshold * threshold;
        let early_exit = thresh_sq * 0.01;
        let pt = [cx, cy];

        let best = if let Some(ref tree) = self.geometry_index {
            let mut best: Option<(f64, usize, usize, Option<usize>)> = None;
            for e in tree.nearest_neighbor_iter(pt) {
                let d = e.distance_2(&pt);
                if d > thresh_sq {
                    break;
                }
                if !is_entry_visible(e.layer, e.feat, &sel) {
                    continue;
                }
                if best.is_none_or(|(bd, ..)| d < bd) {
                    best = Some((d, e.layer, e.feat, e.part));
                    if d < early_exit {
                        break;
                    }
                }
            }
            best
        } else {
            None
        };

        self.hovered = best.and_then(|(_, l, f, p)| {
            self.find_tree_idx_for_feature(l, f, p)
                .map(|idx| HoveredInfo::new(idx, l, f, p))
        });
    }

    fn find_tree_idx_for_feature(
        &self,
        layer: usize,
        feat: usize,
        part: Option<usize>,
    ) -> Option<usize> {
        for (idx, item) in self.tree_items.iter().enumerate() {
            match item {
                TreeItem::Layer(li)
                    if *li == layer
                        && !self.expanded_layers.get(layer).copied().unwrap_or(false) =>
                {
                    return Some(idx);
                }
                TreeItem::Feature { layer: l, feat: f }
                    if *l == layer
                        && *f == feat
                        && (part.is_none() || !self.expanded_features.contains(&(layer, feat))) =>
                {
                    return Some(idx);
                }
                TreeItem::SubFeature {
                    layer: l,
                    feat: f,
                    part: p,
                } if *l == layer && *f == feat && part == Some(*p) => {
                    return Some(idx);
                }
                _ => {}
            }
        }
        None
    }

    fn ensure_layer_expanded(&mut self, layer: usize) {
        if layer < self.expanded_layers.len() && !self.expanded_layers[layer] {
            self.expanded_layers[layer] = true;
            self.build_tree_items();
        }
    }

    fn select_and_scroll(
        &mut self,
        layer: usize,
        feat: Option<usize>,
        part: Option<usize>,
        tree_height: u16,
    ) {
        let inner = tree_height.saturating_sub(2).into_usize();
        self.ensure_layer_expanded(layer);
        if let Some(f) = feat {
            if multi_part_count(&self.feature(layer, f).geometry) > 0 {
                self.expanded_features.insert((layer, f));
                self.build_tree_items();
            }
            if let Some(idx) = self.find_tree_idx_for_feature(layer, f, part) {
                self.selected_index = idx;
                self.scroll_selected_into_view(inner);
            }
        } else if let Some(idx) = self
            .tree_items
            .iter()
            .position(|it| matches!(it, TreeItem::Layer(l) if *l == layer))
        {
            self.selected_index = idx;
            self.scroll_selected_into_view(inner);
        }
        self.invalidate_bounds();
    }

    pub(crate) fn scroll_selected_into_view(&mut self, inner_height: usize) {
        let idx = self.selected_index;
        if idx < self.tree_scroll.into_usize() {
            self.tree_scroll = u16::try_from(idx).unwrap_or(0);
        } else if inner_height > 0 && idx >= self.tree_scroll.into_usize() + inner_height {
            self.tree_scroll =
                u16::try_from(idx.saturating_sub(inner_height.saturating_sub(1))).unwrap_or(0);
        }
    }

    pub(crate) fn handle_feature_click(
        &mut self,
        layer: usize,
        feat: usize,
        part: Option<usize>,
        tree_height: u16,
    ) {
        match self.selected_item().clone() {
            TreeItem::All => self.select_and_scroll(layer, None, None, tree_height),
            TreeItem::Layer(_) => {
                self.select_and_scroll(layer, Some(feat), None, tree_height);
            }
            _ => self.select_and_scroll(layer, Some(feat), part, tree_height),
        }
    }
}

fn error_size(row: &LsRow) -> usize {
    match row {
        LsRow::Error { size: Some(s), .. } => *s,
        _ => 0,
    }
}

fn file_cmp(a: &LsRow, b: &LsRow, col: FileSortColumn, asc: bool) -> std::cmp::Ordering {
    use std::cmp::Ordering;
    let ord = match (a, b) {
        (LsRow::Info { info: ai, .. }, LsRow::Info { info: bi, .. }) => match col {
            FileSortColumn::File => ai.path.cmp(&bi.path),
            FileSortColumn::Size => ai.size.cmp(&bi.size),
            FileSortColumn::EncPct => match (ai.encoding_pct, bi.encoding_pct) {
                (Some(a), Some(b)) => a.total_cmp(&b),
                (None, None) => Ordering::Equal,
                (None, Some(_)) => Ordering::Greater,
                (Some(_), None) => Ordering::Less,
            },
            FileSortColumn::Layers => ai.layers.cmp(&bi.layers),
            FileSortColumn::Features => ai.features.cmp(&bi.features),
        },
        (LsRow::Info { .. }, _) => Ordering::Less,
        (_, LsRow::Info { .. }) => Ordering::Greater,
        (LsRow::Error { .. }, LsRow::Error { .. }) => match col {
            FileSortColumn::Size => error_size(a).cmp(&error_size(b)),
            _ => a.path().cmp(b.path()),
        },
        _ => a.path().cmp(b.path()),
    };
    if asc { ord } else { ord.reverse() }
}

fn poly_verts(poly: &Polygon<i32>) -> Vec<[f64; 2]> {
    poly.exterior()
        .0
        .iter()
        .copied()
        .chain(poly.interiors().iter().flat_map(|r| r.0.iter().copied()))
        .map(coord_f64)
        .collect()
}

fn geometry_vertices(geom: &Geometry<i32>, part: Option<usize>) -> Vec<[f64; 2]> {
    match (geom, part) {
        (Geometry::<i32>::Point(p), None) => vec![coord_f64(p.0)],
        (Geometry::<i32>::LineString(ls), None) => ls.0.iter().copied().map(coord_f64).collect(),
        (Geometry::<i32>::MultiPoint(mp), None) => mp.iter().map(|p| coord_f64(p.0)).collect(),
        (Geometry::<i32>::Polygon(poly), None) => poly_verts(poly),
        (Geometry::<i32>::MultiLineString(mls), None) => mls
            .iter()
            .flat_map(|ls| ls.0.iter().copied().map(coord_f64))
            .collect(),
        (Geometry::<i32>::MultiPolygon(mpoly), None) => mpoly.iter().flat_map(poly_verts).collect(),
        (Geometry::<i32>::MultiPoint(mp), Some(p)) => {
            mp.0.get(p)
                .map(|pt| vec![coord_f64(pt.0)])
                .unwrap_or_default()
        }
        (Geometry::<i32>::MultiLineString(mls), Some(p)) => mls
            .0
            .get(p)
            .map(|ls| ls.0.iter().copied().map(coord_f64).collect())
            .unwrap_or_default(),
        (Geometry::<i32>::MultiPolygon(mpoly), Some(p)) => {
            mpoly.0.get(p).map(poly_verts).unwrap_or_default()
        }
        _ => Vec::new(),
    }
}
