//! Edge routing: Manhattan ASCII paths with 0–1 elbow.
//!
//! v0 keeps it simple: exit perpendicular to `fromSide`, run to the
//! target's column/row, then enter perpendicular to `toSide`. No
//! overlap avoidance and no smoothing — `>50` densely-crossing edges
//! are accepted as illegible per PLAN.md.

use std::collections::HashMap;

use ratatui::buffer::Buffer;
use ratatui::style::Color;

use crate::canvas::{Canvas, Edge, EdgeEnd, Node, Side};
use crate::layout::{node_bbox_in_cells, CellRect, Viewport};

use super::theme::Theme;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Point {
    x: i32,
    y: i32,
}

pub fn render_edges(canvas: &Canvas, vp: &Viewport, theme: &Theme, buf: &mut Buffer) {
    let by_id: HashMap<&str, &Node> = canvas.nodes.iter().map(|n| (n.id(), n)).collect();
    for edge in &canvas.edges {
        let Some(from) = by_id.get(edge.from_node.as_str()) else {
            continue;
        };
        let Some(to) = by_id.get(edge.to_node.as_str()) else {
            continue;
        };
        let Some(from_rect) = node_bbox_in_cells(from, vp) else {
            continue;
        };
        let Some(to_rect) = node_bbox_in_cells(to, vp) else {
            continue;
        };
        draw_edge(edge, from_rect, to_rect, theme, buf);
    }
}

fn side_anchor(rect: CellRect, side: Side) -> Point {
    let mx = rect.x + rect.w as i32 / 2;
    let my = rect.y + rect.h as i32 / 2;
    match side {
        Side::Top => Point {
            x: mx,
            y: rect.y - 1,
        },
        Side::Bottom => Point {
            x: mx,
            y: rect.y + rect.h as i32,
        },
        Side::Left => Point {
            x: rect.x - 1,
            y: my,
        },
        Side::Right => Point {
            x: rect.x + rect.w as i32,
            y: my,
        },
    }
}

fn pick_side(from: CellRect, to: CellRect) -> (Side, Side) {
    let from_cx = from.x + from.w as i32 / 2;
    let from_cy = from.y + from.h as i32 / 2;
    let to_cx = to.x + to.w as i32 / 2;
    let to_cy = to.y + to.h as i32 / 2;
    let dx = to_cx - from_cx;
    let dy = to_cy - from_cy;
    if dx.abs() >= dy.abs() {
        if dx >= 0 {
            (Side::Right, Side::Left)
        } else {
            (Side::Left, Side::Right)
        }
    } else if dy >= 0 {
        (Side::Bottom, Side::Top)
    } else {
        (Side::Top, Side::Bottom)
    }
}

fn draw_edge(edge: &Edge, from: CellRect, to: CellRect, theme: &Theme, buf: &mut Buffer) {
    let (from_side, to_side) = match (edge.from_side, edge.to_side) {
        (Some(a), Some(b)) => (a, b),
        _ => pick_side(from, to),
    };
    let a = side_anchor(from, from_side);
    let b = side_anchor(to, to_side);

    let color = theme.map_color(edge.color.as_deref(), theme.border);
    let style = theme.style(color);

    let path = route(a, from_side, b);
    paint_path(&path, buf, style);
    if !matches!(edge.to_end, Some(EdgeEnd::None)) {
        paint_arrow(b, to_side, buf, style);
    }
    if let Some(label) = &edge.label {
        paint_label(&path, label, buf, style);
    }
}

/// Build a Manhattan path from `a` to `b` with at most one elbow.
/// Direction of the first segment is dictated by `from_side`.
fn route(a: Point, from_side: Side, b: Point) -> Vec<Point> {
    let mut pts = vec![a];
    let elbow = match from_side {
        Side::Left | Side::Right => Point { x: b.x, y: a.y },
        Side::Top | Side::Bottom => Point { x: a.x, y: b.y },
    };
    if elbow != a {
        pts.push(elbow);
    }
    if b != elbow {
        pts.push(b);
    }
    pts
}

fn paint_path(pts: &[Point], buf: &mut Buffer, style: ratatui::style::Style) {
    for window in pts.windows(2) {
        let (p, q) = (window[0], window[1]);
        if p.x == q.x {
            let (y0, y1) = if p.y <= q.y { (p.y, q.y) } else { (q.y, p.y) };
            for y in y0..=y1 {
                set_cell(buf, p.x, y, '│', style);
            }
        } else if p.y == q.y {
            let (x0, x1) = if p.x <= q.x { (p.x, q.x) } else { (q.x, p.x) };
            for x in x0..=x1 {
                set_cell(buf, x, p.y, '─', style);
            }
        }
    }
    if pts.len() == 3 {
        let elbow = pts[1];
        let prev = pts[0];
        let next = pts[2];
        let ch = corner_char(prev, elbow, next);
        set_cell(buf, elbow.x, elbow.y, ch, style);
    }
}

fn corner_char(prev: Point, elbow: Point, next: Point) -> char {
    // Determine where prev and next sit relative to the elbow.
    let from_left = prev.x < elbow.x;
    let from_right = prev.x > elbow.x;
    let from_above = prev.y < elbow.y;
    let from_below = prev.y > elbow.y;
    let to_left = next.x < elbow.x;
    let to_right = next.x > elbow.x;
    let to_above = next.y < elbow.y;
    let to_below = next.y > elbow.y;

    match (
        from_left || to_left,
        from_right || to_right,
        from_above || to_above,
        from_below || to_below,
    ) {
        (true, false, false, true) => '┐',
        (false, true, false, true) => '┌',
        (true, false, true, false) => '┘',
        (false, true, true, false) => '└',
        _ => '┼',
    }
}

fn paint_arrow(at: Point, to_side: Side, buf: &mut Buffer, style: ratatui::style::Style) {
    let ch = match to_side {
        Side::Left => '▶',
        Side::Right => '◀',
        Side::Top => '▼',
        Side::Bottom => '▲',
    };
    set_cell(buf, at.x, at.y, ch, style);
}

fn paint_label(pts: &[Point], label: &str, buf: &mut Buffer, style: ratatui::style::Style) {
    let Some(seg) = longest_segment(pts) else {
        return;
    };
    let (a, b) = seg;
    let mid = Point {
        x: (a.x + b.x) / 2,
        y: (a.y + b.y) / 2,
    };
    let max_len = ((b.x - a.x).abs() + (b.y - a.y).abs()).max(1) as usize;
    let truncated = truncate_with_ellipsis(label, max_len);
    let half = truncated.chars().count() as i32 / 2;
    let (sx, sy) = if a.x == b.x {
        // vertical segment — write horizontally centered next to elbow
        (mid.x + 1, mid.y)
    } else {
        (mid.x - half, mid.y)
    };
    for (i, ch) in truncated.chars().enumerate() {
        set_cell(buf, sx + i as i32, sy, ch, style);
    }
}

fn longest_segment(pts: &[Point]) -> Option<(Point, Point)> {
    pts.windows(2)
        .map(|w| (w[0], w[1]))
        .max_by_key(|(a, b)| (a.x - b.x).abs() + (a.y - b.y).abs())
}

fn truncate_with_ellipsis(s: &str, max: usize) -> String {
    let count = s.chars().count();
    if count <= max {
        return s.to_string();
    }
    if max <= 1 {
        return "…".to_string();
    }
    let take = max - 1;
    let mut out: String = s.chars().take(take).collect();
    out.push('…');
    out
}

fn set_cell(buf: &mut Buffer, x: i32, y: i32, ch: char, style: ratatui::style::Style) {
    if x < 0 || y < 0 {
        return;
    }
    let x = x as u16;
    let y = y as u16;
    let area = buf.area;
    if x < area.x || y < area.y || x >= area.x + area.width || y >= area.y + area.height {
        return;
    }
    let cell = &mut buf[(x, y)];
    cell.set_char(ch);
    cell.set_style(style);
    let _ = Color::Reset; // silence unused import warning if compiler ever objects
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::canvas::{Canvas, Edge, TextNode};
    use crate::layout::Viewport;
    use crate::render::render_canvas;
    use ratatui::layout::Rect;

    fn buffer_to_string(buf: &Buffer) -> String {
        let mut out = String::new();
        for y in 0..buf.area.height {
            for x in 0..buf.area.width {
                out.push_str(buf[(x, y)].symbol());
            }
            out.push('\n');
        }
        out
    }

    #[test]
    fn truncate_label_keeps_ellipsis() {
        assert_eq!(truncate_with_ellipsis("hello", 5), "hello");
        assert_eq!(truncate_with_ellipsis("hello world", 5), "hell…");
        assert_eq!(truncate_with_ellipsis("xy", 1), "…");
    }

    #[test]
    fn renders_edge_between_two_nodes() {
        let canvas = Canvas {
            nodes: vec![
                Node::Text(TextNode {
                    id: "a".into(),
                    x: 0,
                    y: 0,
                    width: 80,
                    height: 48,
                    text: "A".into(),
                    color: None,
                }),
                Node::Text(TextNode {
                    id: "b".into(),
                    x: 200,
                    y: 0,
                    width: 80,
                    height: 48,
                    text: "B".into(),
                    color: None,
                }),
            ],
            edges: vec![Edge {
                id: "e".into(),
                from_node: "a".into(),
                from_side: Some(Side::Right),
                from_end: None,
                to_node: "b".into(),
                to_side: Some(Side::Left),
                to_end: None,
                label: Some("ok".into()),
                color: None,
            }],
        };
        let vp = Viewport::new(40, 6);
        let theme = Theme::with_colors(false);
        let area = Rect::new(0, 0, 40, 6);
        let mut buf = Buffer::empty(area);
        render_canvas(&canvas, &vp, &theme, &mut buf);
        crate::render::edges::render_edges(&canvas, &vp, &theme, &mut buf);

        let s = buffer_to_string(&buf);
        assert!(s.contains('─'), "horizontal edge missing: {s}");
        assert!(s.contains('▶'), "arrow head missing: {s}");
        assert!(s.contains("ok"), "label missing: {s}");
    }
}
