use std::error::Error;

use halley_core::{
    bearings::{Bearing, bearings_for_visible_nodes},
    field::{NodeId, Vec2},
    viewport::Viewport,
};
use smithay::{
    backend::renderer::{
        Color32F, Texture,
        gles::{GlesFrame, Uniform},
    },
    utils::{Buffer, Physical, Rectangle, Transform},
};

use crate::compositor::root::Halley;
use crate::overlay::overlay_fill_and_text_colors;
use crate::render::app_icon::{ensure_app_icon_resources_for_node_ids, node_app_icon_entry};
use crate::render::draw_primitives::draw_rect;
use crate::render::state::NodeAppIconCacheEntry;
use crate::render::{node_app_icon_fallback_glyph, node_app_icon_texture_allowed};
use crate::text::{draw_ui_text, prime_ui_text, ui_text_size};

#[derive(Clone, Debug)]
pub(crate) struct BearingChipLayout {
    pub(crate) node_id: NodeId,
    pub(crate) chip_rect: Rectangle<i32, Physical>,
    pub(crate) icon_rect: Option<Rectangle<i32, Physical>>,
    pub(crate) label: String,
    pub(crate) distance_text: Option<String>,
    pub(crate) distance_rect: Option<Rectangle<i32, Physical>>,
    pub(crate) distance_pos: Option<(i32, i32)>,
    pub(crate) alpha: f32,
}

const CHIP_PAD_X: i32 = 10;
const CHIP_PAD_Y: i32 = 7;
const EDGE_PAD: i32 = 16;
const LABEL_SCALE: i32 = 2;
const META_SCALE: i32 = 2;
const ICON_SIZE: i32 = 16;
const ICON_TEXT_GAP: i32 = 6;
const DISTANCE_GAP: i32 = 4;
const META_PAD_X: i32 = 7;
const META_PAD_Y: i32 = 4;
const GROUP_GAP: i32 = 10;
const MAX_LABEL_CHARS: usize = 24;
const MIN_DISTANCE_ALPHA: f32 = 0.34;
const DISTANCE_HIDE_MULTIPLIER: f32 = 1.5;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum BearingLane {
    NW,
    N,
    NE,
    W,
    E,
    SW,
    S,
    SE,
}

impl BearingLane {
    fn all() -> [Self; 8] {
        [
            Self::NW,
            Self::N,
            Self::NE,
            Self::W,
            Self::E,
            Self::SW,
            Self::S,
            Self::SE,
        ]
    }

    fn from_bearing(bearing: Bearing) -> Self {
        match bearing {
            Bearing::NW => Self::NW,
            Bearing::N => Self::N,
            Bearing::NE => Self::NE,
            Bearing::W => Self::W,
            Bearing::E => Self::E,
            Bearing::SW => Self::SW,
            Bearing::S => Self::S,
            Bearing::SE => Self::SE,
        }
    }

    fn uses_horizontal_axis(self) -> bool {
        matches!(self, Self::N | Self::S)
    }
}

#[derive(Clone, Copy, Debug)]
struct BearingSize {
    chip_w: i32,
    chip_h: i32,
    show_icon: bool,
    distance_rect_w: i32,
    distance_rect_h: i32,
    distance_block_h: i32,
}

impl BearingSize {
    fn total_height(self) -> i32 {
        self.distance_block_h + self.chip_h
    }

    fn primary_extent(self, lane: BearingLane) -> i32 {
        if lane.uses_horizontal_axis() {
            self.chip_w
        } else {
            self.total_height()
        }
    }
}

#[derive(Clone, Debug)]
struct BearingCandidate {
    node_id: NodeId,
    lane: BearingLane,
    projected: f32,
    distance: f32,
    label: String,
    size: BearingSize,
}

#[derive(Clone, Debug)]
struct BearingGroup {
    lane: BearingLane,
    projected: f32,
    node_id: NodeId,
    label: String,
    distance: f32,
    size: BearingSize,
    alpha: f32,
}

pub(crate) fn ensure_bearing_icon_resources(
    renderer: &mut smithay::backend::renderer::gles::GlesRenderer,
    st: &mut Halley,
    monitor: &str,
) -> Result<(), Box<dyn Error>> {
    if !st.runtime.tuning.bearings.show_icons
        || !node_app_icon_texture_allowed(st.runtime.tuning.node_show_app_icons, false)
    {
        return Ok(());
    }

    let viewport = bearings_viewport_for_monitor(st, monitor);
    let node_ids = bearings_for_visible_nodes(&st.model.field, &viewport)
        .into_iter()
        .filter_map(|(id, _)| {
            let node = st.model.field.node(id)?;
            (node.kind == halley_core::field::NodeKind::Surface
                && st
                    .model
                    .monitor_state
                    .node_monitor
                    .get(&id)
                    .map(String::as_str)
                    == Some(monitor)
                && !node_intersects_bearings_view(st, monitor, id))
            .then_some(id)
        })
        .collect::<Vec<_>>();
    ensure_app_icon_resources_for_node_ids(renderer, st, node_ids.into_iter())
}

pub(crate) fn collect_bearing_layouts(
    st: &Halley,
    screen_w: i32,
    screen_h: i32,
    monitor: &str,
    ui_mix: f32,
) -> Vec<BearingChipLayout> {
    if ui_mix <= 0.002 {
        return Vec::new();
    }

    let viewport = bearings_viewport_for_monitor(st, monitor);
    let mut candidates = Vec::new();
    for (id, bearing) in bearings_for_visible_nodes(&st.model.field, &viewport) {
        let Some(node) = st.model.field.node(id) else {
            continue;
        };
        if node.kind != halley_core::field::NodeKind::Surface {
            continue;
        }
        if st
            .model
            .monitor_state
            .node_monitor
            .get(&id)
            .map(String::as_str)
            != Some(monitor)
        {
            continue;
        }
        if node_intersects_bearings_view(st, monitor, id) {
            continue;
        }

        let lane = BearingLane::from_bearing(bearing);
        let distance = offscreen_distance_from_monitor_edge(st, monitor, id).unwrap_or(0.0);
        let label = bearing_label(st, id, node.label.as_str());
        let projected = projected_anchor_for_lane(st, monitor, id, lane, screen_w, screen_h);
        let distance_text = st
            .runtime
            .tuning
            .bearings
            .show_distance
            .then(|| format!("{:.0}px", distance.round()));
        let size = bearing_size(
            st,
            label.as_str(),
            st.runtime.tuning.bearings.show_icons,
            distance_text.as_deref(),
        );

        candidates.push(BearingCandidate {
            node_id: id,
            lane,
            projected,
            distance,
            label,
            size,
        });
    }

    let mut layouts = Vec::new();
    for lane in BearingLane::all() {
        let mut lane_candidates = candidates
            .iter()
            .filter(|candidate| candidate.lane == lane)
            .cloned()
            .collect::<Vec<_>>();
        if lane_candidates.is_empty() {
            continue;
        }
        lane_candidates.sort_by(|a, b| {
            a.projected
                .partial_cmp(&b.projected)
                .unwrap_or(std::cmp::Ordering::Equal)
                .then(a.node_id.as_u64().cmp(&b.node_id.as_u64()))
        });

        let groups = group_lane_candidates(st, lane_candidates, ui_mix)
            .into_iter()
            .filter(|group| group.alpha > 0.002)
            .collect::<Vec<_>>();
        if groups.is_empty() {
            continue;
        }
        layouts.extend(layout_lane_groups(st, lane, groups, screen_w, screen_h));
    }

    let (_, chip_text_color) = overlay_fill_and_text_colors(&st.runtime.tuning);
    for layout in &layouts {
        prime_ui_text(st, layout.label.as_str(), LABEL_SCALE, chip_text_color);
        if let Some(distance_text) = layout.distance_text.as_ref() {
            prime_ui_text(st, distance_text, META_SCALE, chip_text_color);
        }
    }

    layouts
}

pub(crate) fn bearing_hit_test(
    st: &Halley,
    screen_w: i32,
    screen_h: i32,
    monitor: &str,
    sx: f32,
    sy: f32,
) -> Option<NodeId> {
    let ui_mix = st
        .ui
        .render_state
        .bearings_mix
        .get(monitor)
        .copied()
        .unwrap_or_else(|| {
            if st.ui.render_state.bearings_visible() {
                1.0
            } else {
                0.0
            }
        });
    if ui_mix <= 0.05 {
        return None;
    }
    let px = sx.round() as i32;
    let py = sy.round() as i32;
    collect_bearing_layouts(st, screen_w, screen_h, monitor, ui_mix)
        .into_iter()
        .find(|layout| rect_contains(layout.chip_rect, px, py))
        .map(|layout| layout.node_id)
}

pub(crate) fn draw_bearings(
    frame: &mut GlesFrame<'_, '_>,
    st: &Halley,
    damage: Rectangle<i32, Physical>,
    layouts: &[BearingChipLayout],
) -> Result<(), Box<dyn Error>> {
    let rounded = st.runtime.tuning.window_border_radius_px() > 0;
    let (chip_fill_color, chip_text_color) = overlay_fill_and_text_colors(&st.runtime.tuning);
    for layout in layouts {
        if layout.alpha <= 0.002 {
            continue;
        }

        draw_shader_label(
            frame,
            st,
            rounded,
            layout.chip_rect.loc.x,
            layout.chip_rect.loc.y,
            layout.chip_rect.size.w,
            layout.chip_rect.size.h,
            11.0,
            0.0,
            layout.alpha,
            Color32F::new(
                chip_fill_color.r(),
                chip_fill_color.g(),
                chip_fill_color.b(),
                0.0,
            ),
            Color32F::new(
                chip_fill_color.r(),
                chip_fill_color.g(),
                chip_fill_color.b(),
                0.92 * layout.alpha,
            ),
            damage,
        )?;

        if let Some(distance_rect) = layout.distance_rect
            && let Some((distance_x, distance_y)) = layout.distance_pos
            && let Some(distance_text) = layout.distance_text.as_ref()
        {
            draw_shader_label(
                frame,
                st,
                rounded,
                distance_rect.loc.x,
                distance_rect.loc.y,
                distance_rect.size.w,
                distance_rect.size.h,
                8.0,
                0.0,
                layout.alpha,
                Color32F::new(
                    chip_fill_color.r(),
                    chip_fill_color.g(),
                    chip_fill_color.b(),
                    0.0,
                ),
                Color32F::new(
                    chip_fill_color.r(),
                    chip_fill_color.g(),
                    chip_fill_color.b(),
                    0.88 * layout.alpha,
                ),
                damage,
            )?;
            draw_ui_text(
                frame,
                st,
                distance_x,
                distance_y,
                distance_text,
                META_SCALE,
                Color32F::new(
                    chip_text_color.r(),
                    chip_text_color.g(),
                    chip_text_color.b(),
                    layout.alpha * 0.96,
                ),
                damage,
            )?;
        }

        let text_x = layout.chip_rect.loc.x
            + CHIP_PAD_X
            + if layout.icon_rect.is_some() {
                ICON_SIZE + ICON_TEXT_GAP
            } else {
                0
            };
        let (_, label_h) = ui_text_size(st, layout.label.as_str(), LABEL_SCALE);
        let text_y = layout.chip_rect.loc.y + (layout.chip_rect.size.h - label_h) / 2;
        draw_ui_text(
            frame,
            st,
            text_x,
            text_y,
            layout.label.as_str(),
            LABEL_SCALE,
            Color32F::new(
                chip_text_color.r(),
                chip_text_color.g(),
                chip_text_color.b(),
                layout.alpha,
            ),
            damage,
        )?;

        if let Some(icon_rect) = layout.icon_rect {
            draw_bearing_icon(
                frame,
                st,
                layout.node_id,
                icon_rect,
                damage,
                layout.alpha,
                chip_text_color,
            )?;
        }
    }

    Ok(())
}

fn group_lane_candidates(
    st: &Halley,
    candidates: Vec<BearingCandidate>,
    ui_mix: f32,
) -> Vec<BearingGroup> {
    let mut groups = Vec::new();
    let mut current: Vec<BearingCandidate> = Vec::new();

    for candidate in candidates {
        if let Some(previous) = current.last() {
            let min_gap = crowding_threshold(previous.lane, previous.size, candidate.size);
            if candidate.projected - previous.projected > min_gap {
                groups.push(finalize_group(st, std::mem::take(&mut current), ui_mix));
            }
        }
        current.push(candidate);
    }

    if !current.is_empty() {
        groups.push(finalize_group(st, current, ui_mix));
    }

    groups
}

fn finalize_group(st: &Halley, members: Vec<BearingCandidate>, ui_mix: f32) -> BearingGroup {
    let member_count = members.len();
    let lane = members[0].lane;
    let nearest = members
        .iter()
        .min_by(|a, b| {
            a.distance
                .partial_cmp(&b.distance)
                .unwrap_or(std::cmp::Ordering::Equal)
                .then(a.node_id.as_u64().cmp(&b.node_id.as_u64()))
        })
        .expect("bearing group should not be empty");
    let projected = members
        .iter()
        .map(|candidate| candidate.projected)
        .sum::<f32>()
        / member_count as f32;
    let label = if member_count == 1 {
        nearest.label.clone()
    } else {
        format!("{member_count} nodes")
    };
    let distance = nearest.distance;
    let distance_text = st
        .runtime
        .tuning
        .bearings
        .show_distance
        .then(|| format!("{:.0}px", distance.round()));
    let size = bearing_size(
        st,
        label.as_str(),
        member_count == 1 && st.runtime.tuning.bearings.show_icons,
        distance_text.as_deref(),
    );
    let distance_fade = bearing_distance_alpha(st.runtime.tuning.bearings.fade_distance, distance);

    BearingGroup {
        lane,
        projected,
        node_id: nearest.node_id,
        label,
        distance,
        size,
        alpha: (ui_mix * distance_fade).clamp(0.0, 1.0),
    }
}

fn bearing_distance_alpha(fade_distance: f32, distance: f32) -> f32 {
    if fade_distance <= f32::EPSILON {
        return 1.0;
    }

    let fade_end = fade_distance * DISTANCE_HIDE_MULTIPLIER;
    if distance <= fade_distance {
        let t = (distance / fade_distance).clamp(0.0, 1.0);
        return MIN_DISTANCE_ALPHA + (1.0 - t) * (1.0 - MIN_DISTANCE_ALPHA);
    }
    if distance >= fade_end {
        return 0.0;
    }

    let tail_t = ((distance - fade_distance) / (fade_end - fade_distance)).clamp(0.0, 1.0);
    (MIN_DISTANCE_ALPHA * (1.0 - tail_t)).clamp(0.0, 1.0)
}

fn layout_lane_groups(
    st: &Halley,
    lane: BearingLane,
    groups: Vec<BearingGroup>,
    screen_w: i32,
    screen_h: i32,
) -> Vec<BearingChipLayout> {
    let mut groups = groups;
    if groups.is_empty() {
        return Vec::new();
    }

    groups.sort_by(|a, b| {
        a.projected
            .partial_cmp(&b.projected)
            .unwrap_or(std::cmp::Ordering::Equal)
            .then(a.node_id.as_u64().cmp(&b.node_id.as_u64()))
    });

    let mut centers = groups
        .iter()
        .map(|group| {
            let (min_center, max_center) = lane_center_bounds(lane, group.size, screen_w, screen_h);
            group.projected.clamp(min_center, max_center)
        })
        .collect::<Vec<_>>();

    for index in 1..centers.len() {
        let min_sep = crowding_threshold(lane, groups[index - 1].size, groups[index].size);
        let min_center = centers[index - 1] + min_sep;
        if centers[index] < min_center {
            centers[index] = min_center;
        }
    }
    for index in (0..centers.len().saturating_sub(1)).rev() {
        let (_, max_center) = lane_center_bounds(lane, groups[index].size, screen_w, screen_h);
        centers[index] = centers[index].min(max_center);
        let max_prev = centers[index + 1]
            - crowding_threshold(lane, groups[index].size, groups[index + 1].size);
        if centers[index] > max_prev {
            centers[index] = max_prev;
        }
    }
    for index in 1..centers.len() {
        let min_sep = crowding_threshold(lane, groups[index - 1].size, groups[index].size);
        let min_center = centers[index - 1] + min_sep;
        if centers[index] < min_center {
            centers[index] = min_center;
        }
    }

    groups
        .into_iter()
        .zip(centers)
        .map(|(group, center)| build_layout_from_group(st, group, center, screen_w, screen_h))
        .collect()
}

fn build_layout_from_group(
    st: &Halley,
    group: BearingGroup,
    center: f32,
    screen_w: i32,
    screen_h: i32,
) -> BearingChipLayout {
    let chip_w = group.size.chip_w;
    let chip_h = group.size.chip_h;
    let total_h = group.size.total_height();
    let total_top = (center.round() as i32) - total_h / 2;
    let chip_y_vertical = total_top + group.size.distance_block_h;
    let distance_text = st
        .runtime
        .tuning
        .bearings
        .show_distance
        .then(|| format!("{:.0}px", group.distance.round()));

    let (chip_x, chip_y) = match group.lane {
        BearingLane::N => (
            (center.round() as i32) - chip_w / 2,
            EDGE_PAD + group.size.distance_block_h,
        ),
        BearingLane::S => (
            (center.round() as i32) - chip_w / 2,
            screen_h - EDGE_PAD - chip_h,
        ),
        BearingLane::W => (EDGE_PAD, chip_y_vertical),
        BearingLane::E => (screen_w - EDGE_PAD - chip_w, chip_y_vertical),
        BearingLane::NW => (EDGE_PAD, chip_y_vertical),
        BearingLane::NE => (screen_w - EDGE_PAD - chip_w, chip_y_vertical),
        BearingLane::SW => (EDGE_PAD, chip_y_vertical),
        BearingLane::SE => (screen_w - EDGE_PAD - chip_w, chip_y_vertical),
    };

    let icon_rect = group.size.show_icon.then(|| {
        Rectangle::new(
            (chip_x + CHIP_PAD_X, chip_y + (chip_h - ICON_SIZE) / 2).into(),
            (ICON_SIZE, ICON_SIZE).into(),
        )
    });
    let distance_rect = distance_text.as_ref().map(|_| {
        Rectangle::new(
            (
                chip_x + ((chip_w - group.size.distance_rect_w) / 2),
                chip_y - DISTANCE_GAP - group.size.distance_rect_h,
            )
                .into(),
            (group.size.distance_rect_w, group.size.distance_rect_h).into(),
        )
    });
    let distance_pos = distance_text
        .as_ref()
        .zip(distance_rect)
        .map(|(text, rect)| {
            let (_, text_h) = ui_text_size(st, text, META_SCALE);
            (
                rect.loc.x + META_PAD_X,
                rect.loc.y + (rect.size.h - text_h) / 2,
            )
        });

    BearingChipLayout {
        node_id: group.node_id,
        chip_rect: Rectangle::new((chip_x, chip_y).into(), (chip_w, chip_h).into()),
        icon_rect,
        label: group.label,
        distance_text,
        distance_rect,
        distance_pos,
        alpha: group.alpha,
    }
}

fn bearing_size(
    st: &Halley,
    label: &str,
    show_icon: bool,
    distance_text: Option<&str>,
) -> BearingSize {
    let (label_w, label_h) = ui_text_size(st, label, LABEL_SCALE);
    let icon_gap = if show_icon {
        ICON_SIZE + ICON_TEXT_GAP
    } else {
        0
    };
    let chip_w = (CHIP_PAD_X * 2 + icon_gap + label_w).max(44);
    let chip_h = (CHIP_PAD_Y * 2 + label_h.max(if show_icon { ICON_SIZE } else { 0 })).max(24);
    let (distance_rect_w, distance_rect_h, distance_block_h) = distance_text
        .map(|text| {
            let (text_w, text_h) = ui_text_size(st, text, META_SCALE);
            let rect_w = text_w + META_PAD_X * 2;
            let rect_h = text_h + META_PAD_Y * 2;
            (rect_w, rect_h, rect_h + DISTANCE_GAP)
        })
        .unwrap_or((0, 0, 0));

    BearingSize {
        chip_w,
        chip_h,
        show_icon,
        distance_rect_w,
        distance_rect_h,
        distance_block_h,
    }
}

fn crowding_threshold(lane: BearingLane, left: BearingSize, right: BearingSize) -> f32 {
    ((left.primary_extent(lane) + right.primary_extent(lane)) as f32 * 0.5) + GROUP_GAP as f32
}

fn lane_center_bounds(
    lane: BearingLane,
    size: BearingSize,
    screen_w: i32,
    screen_h: i32,
) -> (f32, f32) {
    if lane.uses_horizontal_axis() {
        let half = size.chip_w as f32 * 0.5;
        (
            EDGE_PAD as f32 + half,
            screen_w as f32 - EDGE_PAD as f32 - half,
        )
    } else {
        let half = size.total_height() as f32 * 0.5;
        (
            EDGE_PAD as f32 + half,
            screen_h as f32 - EDGE_PAD as f32 - half,
        )
    }
}

fn bearings_viewport_for_monitor(st: &Halley, monitor: &str) -> Viewport {
    let (center, size) = monitor_view_center_size(st, monitor);
    Viewport::new(center, size)
}

fn monitor_view_center_size(st: &Halley, monitor: &str) -> (Vec2, Vec2) {
    if st.model.monitor_state.current_monitor == monitor {
        (st.model.viewport.center, st.model.zoom_ref_size)
    } else {
        st.model
            .monitor_state
            .monitors
            .get(monitor)
            .map(|space| (space.viewport.center, space.zoom_ref_size))
            .unwrap_or((st.model.viewport.center, st.model.zoom_ref_size))
    }
}

fn node_intersects_bearings_view(st: &Halley, monitor: &str, node_id: NodeId) -> bool {
    let Some(node) = st.model.field.node(node_id) else {
        return false;
    };
    let ext = bearings_collision_extents(st, node);
    let (center, size) = monitor_view_center_size(st, monitor);
    let min_x = center.x - size.x * 0.5;
    let max_x = center.x + size.x * 0.5;
    let min_y = center.y - size.y * 0.5;
    let max_y = center.y + size.y * 0.5;

    let node_min_x = node.pos.x - ext.left;
    let node_max_x = node.pos.x + ext.right;
    let node_min_y = node.pos.y - ext.top;
    let node_max_y = node.pos.y + ext.bottom;

    node_max_x > min_x && node_min_x < max_x && node_max_y > min_y && node_min_y < max_y
}

fn projected_anchor_for_lane(
    st: &Halley,
    monitor: &str,
    node_id: NodeId,
    lane: BearingLane,
    screen_w: i32,
    screen_h: i32,
) -> f32 {
    let Some(node) = st.model.field.node(node_id) else {
        return if lane.uses_horizontal_axis() {
            screen_w as f32 * 0.5
        } else {
            screen_h as f32 * 0.5
        };
    };
    let (center, size) = monitor_view_center_size(st, monitor);
    let dx = node.pos.x - center.x;
    let dy = node.pos.y - center.y;
    let half_w = size.x * 0.5;
    let half_h = size.y * 0.5;
    let tx = if dx.abs() <= f32::EPSILON {
        f32::INFINITY
    } else {
        half_w / dx.abs()
    };
    let ty = if dy.abs() <= f32::EPSILON {
        f32::INFINITY
    } else {
        half_h / dy.abs()
    };
    let t = tx.min(ty);
    let edge_x = center.x + dx * t;
    let edge_y = center.y + dy * t;

    let scalar = if lane.uses_horizontal_axis() {
        project_horizontal_edge_scalar(center.x, half_w, size.x, edge_x, screen_w)
    } else {
        project_vertical_edge_scalar(center.y, half_h, size.y, edge_y, screen_h)
    };

    scalar.clamp(
        EDGE_PAD as f32,
        if lane.uses_horizontal_axis() {
            screen_w as f32 - EDGE_PAD as f32
        } else {
            screen_h as f32 - EDGE_PAD as f32
        },
    )
}

fn project_horizontal_edge_scalar(
    center_x: f32,
    half_w: f32,
    size_x: f32,
    edge_x: f32,
    screen_w: i32,
) -> f32 {
    ((edge_x - (center_x - half_w)) / size_x.max(1.0)) * screen_w as f32
}

fn project_vertical_edge_scalar(
    center_y: f32,
    half_h: f32,
    size_y: f32,
    edge_y: f32,
    screen_h: i32,
) -> f32 {
    ((edge_y - (center_y - half_h)) / size_y.max(1.0)) * screen_h as f32
}

fn bearing_label(st: &Halley, node_id: NodeId, title: &str) -> String {
    let base = if !title.trim().is_empty() {
        title.trim().to_string()
    } else if let Some(app_id) = st.model.node_app_ids.get(&node_id) {
        app_id.clone()
    } else {
        format!("Node {}", node_id.as_u64())
    };
    truncate_label(base.as_str())
}

fn offscreen_distance_from_monitor_edge(
    st: &Halley,
    monitor: &str,
    node_id: NodeId,
) -> Option<f32> {
    let node = st.model.field.node(node_id)?;
    let ext = bearings_collision_extents(st, node);
    let (center, size) = monitor_view_center_size(st, monitor);
    let min_x = center.x - size.x * 0.5;
    let max_x = center.x + size.x * 0.5;
    let min_y = center.y - size.y * 0.5;
    let max_y = center.y + size.y * 0.5;

    let node_min_x = node.pos.x - ext.left;
    let node_max_x = node.pos.x + ext.right;
    let node_min_y = node.pos.y - ext.top;
    let node_max_y = node.pos.y + ext.bottom;

    let overflow_x = if node_max_x <= min_x {
        min_x - node_max_x
    } else if node_min_x >= max_x {
        node_min_x - max_x
    } else {
        0.0
    };
    let overflow_y = if node_max_y <= min_y {
        min_y - node_max_y
    } else if node_min_y >= max_y {
        node_min_y - max_y
    } else {
        0.0
    };

    Some((overflow_x * overflow_x + overflow_y * overflow_y).sqrt())
}

fn bearings_collision_extents(
    st: &Halley,
    node: &halley_core::field::Node,
) -> crate::compositor::overlap::system::CollisionExtents {
    match node.state {
        halley_core::field::NodeState::Node | halley_core::field::NodeState::Core => {
            st.collision_extents_for_node(node)
        }
        halley_core::field::NodeState::Active | halley_core::field::NodeState::Drifting => {
            st.surface_window_collision_extents(node)
        }
    }
}

fn truncate_label(label: &str) -> String {
    let mut out = String::new();
    let mut count = 0usize;
    for ch in label.chars() {
        if count >= MAX_LABEL_CHARS {
            break;
        }
        out.push(ch);
        count += 1;
    }
    if label.chars().count() > MAX_LABEL_CHARS {
        out.push_str("...");
    }
    out
}

fn rect_contains(rect: Rectangle<i32, Physical>, x: i32, y: i32) -> bool {
    x >= rect.loc.x
        && x < rect.loc.x + rect.size.w
        && y >= rect.loc.y
        && y < rect.loc.y + rect.size.h
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn vertical_projection_decreases_when_edge_moves_up() {
        let upper = project_vertical_edge_scalar(0.0, 50.0, 100.0, -25.0, 1000);
        let lower = project_vertical_edge_scalar(0.0, 50.0, 100.0, 25.0, 1000);

        assert!(upper < lower, "upper={upper} lower={lower}");
        assert_eq!(upper, 250.0);
        assert_eq!(lower, 750.0);
    }

    #[test]
    fn distance_alpha_reaches_zero_at_hide_multiplier() {
        let fade_distance = 1200.0;

        assert_eq!(bearing_distance_alpha(fade_distance, 0.0), 1.0);
        assert!(
            (bearing_distance_alpha(fade_distance, fade_distance) - MIN_DISTANCE_ALPHA).abs()
                < 1e-6
        );
        assert!(bearing_distance_alpha(fade_distance, fade_distance * 1.25) > 0.0);
        assert_eq!(
            bearing_distance_alpha(fade_distance, fade_distance * DISTANCE_HIDE_MULTIPLIER),
            0.0
        );
        assert_eq!(
            bearing_distance_alpha(fade_distance, fade_distance * 2.0),
            0.0
        );
    }
}

fn draw_bearing_icon(
    frame: &mut GlesFrame<'_, '_>,
    st: &Halley,
    node_id: NodeId,
    rect: Rectangle<i32, Physical>,
    damage: Rectangle<i32, Physical>,
    alpha: f32,
    chip_text_color: Color32F,
) -> Result<(), Box<dyn Error>> {
    if node_app_icon_texture_allowed(st.runtime.tuning.node_show_app_icons, false)
        && let Some(NodeAppIconCacheEntry::Ready(icon)) = node_app_icon_entry(st, node_id)
    {
        let src = Rectangle::<f64, Buffer>::new(
            (0.0, 0.0).into(),
            (icon.width as f64, icon.height as f64).into(),
        );
        frame.render_texture_from_to(
            &icon.texture,
            src,
            rect,
            &[damage],
            &[],
            Transform::Normal,
            alpha,
            None,
            &[],
        )?;
    } else {
        draw_rect(
            frame,
            rect.loc.x,
            rect.loc.y,
            rect.size.w,
            rect.size.h,
            Color32F::new(0.22, 0.82, 0.92, alpha * 0.30),
            damage,
        )?;
        let glyph = node_app_icon_fallback_glyph(
            st.model.node_app_ids.get(&node_id).map(String::as_str),
            st.model
                .field
                .node(node_id)
                .map(|node| node.label.as_str())
                .unwrap_or("?"),
        )
        .to_string();
        let (text_w, text_h) = ui_text_size(st, &glyph, 2);
        draw_ui_text(
            frame,
            st,
            rect.loc.x + (rect.size.w - text_w) / 2,
            rect.loc.y + (rect.size.h - text_h) / 2,
            &glyph,
            2,
            Color32F::new(
                chip_text_color.r(),
                chip_text_color.g(),
                chip_text_color.b(),
                alpha,
            ),
            damage,
        )?;
    }
    Ok(())
}

fn draw_shader_label(
    frame: &mut GlesFrame<'_, '_>,
    st: &Halley,
    rounded: bool,
    x: i32,
    y: i32,
    w: i32,
    h: i32,
    corner_radius: f32,
    border_px: f32,
    alpha: f32,
    border_color: Color32F,
    fill_color: Color32F,
    damage: Rectangle<i32, Physical>,
) -> Result<(), Box<dyn Error>> {
    let Some(texture) = st.ui.render_state.gpu.node_circle_texture.as_ref() else {
        return Ok(());
    };
    let Some(program) = st.ui.render_state.ui_rect_program(rounded) else {
        return Ok(());
    };

    let dest = Rectangle::<i32, Physical>::new((x, y).into(), (w.max(1), h.max(1)).into());
    let tex_size = texture.size();
    let src = Rectangle::<f64, Buffer>::new(
        (0.0, 0.0).into(),
        (tex_size.w as f64, tex_size.h as f64).into(),
    );
    let uniforms = [
        Uniform::new(
            "node_color",
            (
                border_color.r(),
                border_color.g(),
                border_color.b(),
                border_color.a(),
            ),
        ),
        Uniform::new(
            "fill_color",
            (
                fill_color.r(),
                fill_color.g(),
                fill_color.b(),
                fill_color.a(),
            ),
        ),
        Uniform::new("rect_size", (w as f32, h as f32)),
        Uniform::new(
            "inner_rect_size",
            (
                (w as f32 - border_px * 2.0).max(1.0),
                (h as f32 - border_px * 2.0).max(1.0),
            ),
        ),
        Uniform::new(
            "inner_rect_offset",
            (border_px.max(0.0), border_px.max(0.0)),
        ),
        Uniform::new("corner_radius", corner_radius),
        Uniform::new("inner_corner_radius", (corner_radius - border_px).max(0.0)),
        Uniform::new("border_px", border_px),
    ];

    frame.render_texture_from_to(
        texture,
        src,
        dest,
        &[damage],
        &[],
        Transform::Normal,
        alpha.clamp(0.0, 1.0),
        Some(program),
        &uniforms,
    )?;

    Ok(())
}
