//! Drawing objects and their attributes.
//
// Ported in parts from xfig src/u_redraw.c and src/f_save.c (object type
// order) (xfig licence, see LICENSE-xfig).

use rustfig_geom::{arc_from_three_points, flatten_round_rect, Affine, Point, Rect};
use serde::{Deserialize, Serialize};
use std::sync::Arc as Shared;

pub use figbase::image::RasterImage;
pub use figbase::style::{CapStyle, JoinStyle, Justify, LineKind};

/// Stable identity of an object within a document session.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize)]
pub struct ObjectId(pub u64);

/// A Fig colour number: -1 default, 0–31 standard, 32–543 user colours.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ColorRef(pub i32);

impl ColorRef {
    pub const DEFAULT: ColorRef = ColorRef(-1);
    pub const BLACK: ColorRef = ColorRef(0);
    pub const WHITE: ColorRef = ColorRef(7);

    pub fn is_default_or_black(self) -> bool {
        self.0 <= 0
    }
}

impl Default for ColorRef {
    fn default() -> Self {
        ColorRef::BLACK
    }
}

/// Graphic attributes shared by ellipses, polylines, splines and arcs.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Style {
    pub line: LineKind,
    /// Line thickness in 1/80 inch.
    pub thickness: i32,
    pub pen_color: ColorRef,
    pub fill_color: ColorRef,
    /// 0–999, larger is further back.
    pub depth: i32,
    pub pen_style: i32,
    /// -1 no fill, 0–40 shades/tints, 41–62 patterns.
    pub area_fill: i32,
    /// Dash length / dot gap in 1/80 inch.
    pub style_val: f64,
    pub cap: CapStyle,
    pub join: JoinStyle,
}

impl Default for Style {
    fn default() -> Self {
        Style {
            line: LineKind::Solid,
            thickness: 1,
            pen_color: ColorRef::BLACK,
            fill_color: ColorRef::WHITE,
            depth: 50,
            pen_style: -1,
            area_fill: -1,
            style_val: 0.0,
            cap: CapStyle::Butt,
            join: JoinStyle::Miter,
        }
    }
}

impl Style {
    pub fn is_filled(&self) -> bool {
        self.area_fill >= 0
    }
}

/// Maximum object depth.
pub const MAX_DEPTH: i32 = 999;

/// An arrowhead specification.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Arrow {
    /// 0 stick, 1 triangle, 2 indented, 3 pointed, 4+ extended shapes.
    pub kind: i32,
    /// 0 hollow (white), 1 filled with pen colour.
    pub style: i32,
    /// Outline thickness in 1/80 inch.
    pub thickness: f64,
    /// Width in Fig units.
    pub width: f64,
    /// Length in Fig units.
    pub height: f64,
}

impl Default for Arrow {
    fn default() -> Self {
        Arrow {
            kind: 1,
            style: 1,
            thickness: 1.0,
            width: 60.0,
            height: 120.0,
        }
    }
}

/// Ellipse sub-types (how it was entered).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EllipseKind {
    EllipseRadii,
    EllipseDiameter,
    CircleRadius,
    CircleDiameter,
}

impl EllipseKind {
    pub fn from_fig(v: i32) -> EllipseKind {
        match v {
            2 => EllipseKind::EllipseDiameter,
            3 => EllipseKind::CircleRadius,
            4 => EllipseKind::CircleDiameter,
            _ => EllipseKind::EllipseRadii,
        }
    }
    pub fn to_fig(self) -> i32 {
        match self {
            EllipseKind::EllipseRadii => 1,
            EllipseKind::EllipseDiameter => 2,
            EllipseKind::CircleRadius => 3,
            EllipseKind::CircleDiameter => 4,
        }
    }
    pub fn is_circle(self) -> bool {
        matches!(self, EllipseKind::CircleRadius | EllipseKind::CircleDiameter)
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Ellipse {
    pub kind: EllipseKind,
    pub style: Style,
    pub direction: i32,
    /// Angle of the x axis, radians counter-clockwise.
    pub angle: f64,
    pub center: Point,
    pub radii: Point,
    pub start: Point,
    pub end: Point,
}

/// Polyline sub-types.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PolyKind {
    Polyline,
    Box,
    Polygon,
    ArcBox,
    Picture,
}

impl PolyKind {
    pub fn from_fig(v: i32) -> PolyKind {
        match v {
            2 => PolyKind::Box,
            3 => PolyKind::Polygon,
            4 => PolyKind::ArcBox,
            5 => PolyKind::Picture,
            _ => PolyKind::Polyline,
        }
    }
    pub fn to_fig(self) -> i32 {
        match self {
            PolyKind::Polyline => 1,
            PolyKind::Box => 2,
            PolyKind::Polygon => 3,
            PolyKind::ArcBox => 4,
            PolyKind::Picture => 5,
        }
    }
    pub fn is_closed(self) -> bool {
        !matches!(self, PolyKind::Polyline)
    }
    /// Box-like kinds whose points form an axis-aligned rectangle.
    pub fn is_rect(self) -> bool {
        matches!(self, PolyKind::Box | PolyKind::ArcBox | PolyKind::Picture)
    }
}

/// An imported picture inside a picture polyline.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Picture {
    pub flipped: bool,
    pub file: String,
    /// Decoded pixels, if the file could be loaded. Not saved.
    #[serde(skip)]
    pub image: Option<Shared<RasterImage>>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Polyline {
    pub kind: PolyKind,
    pub style: Style,
    /// Corner radius of arc-boxes in 1/80 inch; -1 for lines that have none.
    pub radius: i32,
    pub forward_arrow: Option<Arrow>,
    pub backward_arrow: Option<Arrow>,
    /// Vertices. Closed kinds do not repeat the first point.
    pub points: Vec<Point>,
    pub picture: Option<Picture>,
}

impl Polyline {
    pub fn new(kind: PolyKind, style: Style, points: Vec<Point>) -> Polyline {
        Polyline {
            kind,
            style,
            // xfig: -1 for everything but arc-boxes (`u_create.c`).
            radius: -1,
            forward_arrow: None,
            backward_arrow: None,
            points,
            picture: None,
        }
    }

    /// A box from two opposite corners.
    pub fn rect(style: Style, a: Point, b: Point) -> Polyline {
        let r = Rect::new(a, b);
        Polyline::new(PolyKind::Box, style, r.corners().to_vec())
    }
}

/// Spline sub-types (the curve model of each point lives in `shapes`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SplineKind {
    Approximated,
    Interpolated,
    X,
}

impl SplineKind {
    pub fn default_shape(self) -> f64 {
        match self {
            SplineKind::Approximated => 1.0,
            SplineKind::Interpolated => -0.5,
            SplineKind::X => 0.5,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Spline {
    pub kind: SplineKind,
    pub closed: bool,
    pub style: Style,
    pub forward_arrow: Option<Arrow>,
    pub backward_arrow: Option<Arrow>,
    pub points: Vec<Point>,
    /// One shape factor per point, in [-1, 1].
    pub shapes: Vec<f64>,
}

impl Spline {
    pub fn new(kind: SplineKind, closed: bool, style: Style, points: Vec<Point>) -> Spline {
        let n = points.len();
        let mut shapes = vec![kind.default_shape(); n];
        if !closed {
            if let Some(s) = shapes.first_mut() {
                *s = 0.0;
            }
            if let Some(s) = shapes.last_mut() {
                *s = 0.0;
            }
        }
        Spline {
            kind,
            closed,
            style,
            forward_arrow: None,
            backward_arrow: None,
            points,
            shapes,
        }
    }

    pub fn fig_subtype(&self) -> i32 {
        let base = match self.kind {
            SplineKind::Approximated => 0,
            SplineKind::Interpolated => 2,
            SplineKind::X => 4,
        };
        base + i32::from(self.closed)
    }
}

/// Text flag bits.
pub mod text_flags {
    pub const RIGID: i32 = 1;
    pub const SPECIAL: i32 = 2;
    pub const POSTSCRIPT: i32 = 4;
    pub const HIDDEN: i32 = 8;
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Text {
    pub justify: Justify,
    pub color: ColorRef,
    pub depth: i32,
    pub pen_style: i32,
    /// Font number; meaning depends on the PostScript flag.
    pub font: i32,
    /// Size in points.
    pub size: f64,
    /// Radians, counter-clockwise.
    pub angle: f64,
    pub flags: i32,
    /// Height in Fig units (ascent of the string).
    pub height: f64,
    /// Length in Fig units (advance width of the longest line).
    pub length: f64,
    /// Baseline anchor; meaning depends on `justify`.
    pub origin: Point,
    pub text: String,
    /// An installed font (F7), `Family Style` as [`local_font_name`] gives
    /// it, drawn instead of the Fig font `font`, which is its nearest Fig
    /// font ([`nearest_ps_font`]). Fig 3.2 keeps it as a comment of the text
    /// ([`local_font_comment`]); xfig and fig2dev ignore that.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub local_font: Option<String>,
}

/// The prefix of the text comment that carries [`Text::local_font`].
pub const LOCAL_FONT_PREFIX: &str = "rustfig-font:";

/// The comment line (without the `# `) for a local font.
pub fn local_font_comment(name: &str) -> String {
    format!("{LOCAL_FONT_PREFIX} {}", name.replace('\n', " "))
}

/// The local font a comment line names, if it is such a line.
pub fn parse_local_font_comment(line: &str) -> Option<&str> {
    let name = line.trim().strip_prefix(LOCAL_FONT_PREFIX)?.trim();
    (!name.is_empty()).then_some(name)
}

/// The generic kind of a local font, for choosing its nearest Fig font.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FontKind {
    Serif,
    Sans,
    Mono,
}

/// The PostScript font number nearest to a local font: serif is Times (0),
/// sans Helvetica (16), mono Courier (12); bold adds 2 and italic 1 (xfig's
/// order Roman, Italic, Bold, Bold Italic).
pub fn nearest_ps_font(kind: FontKind, bold: bool, italic: bool) -> i32 {
    let base = match kind {
        FontKind::Serif => 0,
        FontKind::Sans => 16,
        FontKind::Mono => 12,
    };
    base + if bold { 2 } else { 0 } + i32::from(italic)
}

/// The comments of `o` as the Fig file keeps them: a text's local font
/// comes first.
pub fn comments_with_local_font(o: &Object) -> Vec<String> {
    let mut v = o.comments.clone();
    if let ObjectKind::Text(Text {
        local_font: Some(n), ..
    }) = &o.kind
    {
        v.insert(0, local_font_comment(n));
    }
    v
}

impl Text {
    pub fn is_postscript(&self) -> bool {
        self.flags & text_flags::POSTSCRIPT != 0
    }
    pub fn is_hidden(&self) -> bool {
        self.flags & text_flags::HIDDEN != 0
    }
    pub fn is_special(&self) -> bool {
        self.flags & text_flags::SPECIAL != 0
    }
    pub fn is_rigid(&self) -> bool {
        self.flags & text_flags::RIGID != 0
    }

    /// The four corners of the text box (baseline-left first), from the
    /// stored `length` and `height`.
    pub fn corners(&self) -> [Point; 4] {
        let l = if self.length.is_finite() {
            self.length.max(0.0)
        } else {
            0.0
        };
        let h = if self.height.is_finite() {
            self.height.max(0.0)
        } else {
            0.0
        };
        let x0 = match self.justify {
            Justify::Left => 0.0,
            Justify::Center => -l / 2.0,
            Justify::Right => -l,
        };
        let rot = Affine::rotate(if self.angle.is_finite() { self.angle } else { 0.0 });
        let lines = self.text.lines().count().max(1) as f64;
        let line_gap = self.size.max(0.0) * rustfig_geom::FIG_PER_POINT * 1.2;
        let descent = h * 0.25 + (lines - 1.0) * line_gap;
        [
            Point::new(x0, descent),
            Point::new(x0 + l, descent),
            Point::new(x0 + l, -h),
            Point::new(x0, -h),
        ]
        .map(|p| self.origin + rot.apply_vec(p))
    }
}

/// Circular arc through three points.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Arc {
    /// Pie wedge (closed) instead of an open arc.
    pub pie: bool,
    pub style: Style,
    /// Counter-clockwise on screen (Fig direction 1).
    pub ccw: bool,
    pub forward_arrow: Option<Arrow>,
    pub backward_arrow: Option<Arrow>,
    pub center: Point,
    pub points: [Point; 3],
}

impl Arc {
    /// Build an arc through three points; `None` if they are collinear.
    pub fn through(style: Style, p1: Point, p2: Point, p3: Point, pie: bool) -> Option<Arc> {
        let g = arc_from_three_points(p1, p2, p3)?;
        Some(Arc {
            pie,
            style,
            ccw: g.counter_clockwise(),
            forward_arrow: None,
            backward_arrow: None,
            center: g.center,
            points: [p1, p2, p3],
        })
    }

    /// Recompute centre and direction from the three points.
    pub fn refit(&mut self) {
        if let Some(g) = arc_from_three_points(self.points[0], self.points[1], self.points[2]) {
            self.center = g.center;
            self.ccw = g.counter_clockwise();
        }
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct Compound {
    pub objects: Vec<Object>,
}

/// The kinds of drawing objects.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ObjectKind {
    Ellipse(Ellipse),
    Polyline(Polyline),
    Spline(Spline),
    Text(Text),
    Arc(Arc),
    Compound(Compound),
}

/// A drawing object with its identity and attached comments.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Object {
    #[serde(default)]
    pub id: ObjectId,
    #[serde(default)]
    pub comments: Vec<String>,
    #[serde(flatten)]
    pub kind: ObjectKind,
}

impl AsRef<Object> for Object {
    fn as_ref(&self) -> &Object {
        self
    }
}

impl Object {
    /// A text's local font (F7), if it has one.
    pub fn local_font(&self) -> Option<&str> {
        match &self.kind {
            ObjectKind::Text(t) => t.local_font.as_deref(),
            _ => None,
        }
    }

    /// Move a `rustfig-font:` comment line of a text into
    /// [`Text::local_font`] (what the Fig reader does); other comments stay.
    pub fn take_local_font(mut self) -> Object {
        if let ObjectKind::Text(t) = &mut self.kind {
            if let Some(pos) = self.comments.iter().position(|c| parse_local_font_comment(c).is_some()) {
                let line = self.comments.remove(pos);
                t.local_font = parse_local_font_comment(&line).map(str::to_string);
            }
        }
        self
    }

    pub fn new(kind: ObjectKind) -> Object {
        Object {
            id: ObjectId(0),
            comments: Vec::new(),
            kind,
        }
    }

    /// Position of the object's type in xfig's object lists: within a depth
    /// xfig draws arcs, compounds, ellipses, lines, splines, then texts
    /// (one list per type; `u_redraw.c`), and saves them in that order
    /// (`f_save.c`).
    pub fn xfig_type_rank(&self) -> u8 {
        match &self.kind {
            ObjectKind::Arc(_) => 0,
            ObjectKind::Compound(_) => 1,
            ObjectKind::Ellipse(_) => 2,
            ObjectKind::Polyline(_) => 3,
            ObjectKind::Spline(_) => 4,
            ObjectKind::Text(_) => 5,
        }
    }

    /// Short type name ("polyline", "box", "text", ...).
    pub fn type_name(&self) -> &'static str {
        match &self.kind {
            ObjectKind::Ellipse(e) if e.kind.is_circle() => "circle",
            ObjectKind::Ellipse(_) => "ellipse",
            ObjectKind::Polyline(p) => match p.kind {
                PolyKind::Polyline => "polyline",
                PolyKind::Box => "box",
                PolyKind::Polygon => "polygon",
                PolyKind::ArcBox => "arc-box",
                PolyKind::Picture => "picture",
            },
            ObjectKind::Spline(s) if s.closed => "closed spline",
            ObjectKind::Spline(_) => "spline",
            ObjectKind::Text(_) => "text",
            ObjectKind::Arc(_) => "arc",
            ObjectKind::Compound(_) => "compound",
        }
    }

    pub fn style(&self) -> Option<&Style> {
        match &self.kind {
            ObjectKind::Ellipse(e) => Some(&e.style),
            ObjectKind::Polyline(p) => Some(&p.style),
            ObjectKind::Spline(s) => Some(&s.style),
            ObjectKind::Arc(a) => Some(&a.style),
            ObjectKind::Text(_) | ObjectKind::Compound(_) => None,
        }
    }

    pub fn style_mut(&mut self) -> Option<&mut Style> {
        match &mut self.kind {
            ObjectKind::Ellipse(e) => Some(&mut e.style),
            ObjectKind::Polyline(p) => Some(&mut p.style),
            ObjectKind::Spline(s) => Some(&mut s.style),
            ObjectKind::Arc(a) => Some(&mut a.style),
            ObjectKind::Text(_) | ObjectKind::Compound(_) => None,
        }
    }

    /// Depth (the minimum depth of the children for compounds).
    pub fn depth(&self) -> i32 {
        match &self.kind {
            ObjectKind::Text(t) => t.depth,
            ObjectKind::Compound(c) => c.objects.iter().map(Object::depth).min().unwrap_or(0),
            _ => self.style().map(|s| s.depth).unwrap_or(0),
        }
    }

    /// Set the depth (shifting all children of a compound by the same amount).
    pub fn set_depth(&mut self, depth: i32) {
        let depth = depth.clamp(0, MAX_DEPTH);
        match &mut self.kind {
            ObjectKind::Text(t) => t.depth = depth,
            ObjectKind::Compound(c) => {
                let cur = c.objects.iter().map(Object::depth).min().unwrap_or(0);
                let delta = depth - cur;
                for o in &mut c.objects {
                    let d = o.depth() + delta;
                    o.set_depth(d);
                }
            }
            _ => {
                if let Some(s) = self.style_mut() {
                    s.depth = depth;
                }
            }
        }
    }

    /// The arrowheads (forward, backward) of objects that can have them.
    pub fn arrows(&self) -> Option<(Option<Arrow>, Option<Arrow>)> {
        match &self.kind {
            ObjectKind::Polyline(p) if p.kind == PolyKind::Polyline => Some((p.forward_arrow, p.backward_arrow)),
            ObjectKind::Spline(s) if !s.closed => Some((s.forward_arrow, s.backward_arrow)),
            ObjectKind::Arc(a) if !a.pie => Some((a.forward_arrow, a.backward_arrow)),
            _ => None,
        }
    }

    pub fn arrows_mut(&mut self) -> Option<(&mut Option<Arrow>, &mut Option<Arrow>)> {
        match &mut self.kind {
            ObjectKind::Polyline(p) if p.kind == PolyKind::Polyline => {
                Some((&mut p.forward_arrow, &mut p.backward_arrow))
            }
            ObjectKind::Spline(s) if !s.closed => Some((&mut s.forward_arrow, &mut s.backward_arrow)),
            ObjectKind::Arc(a) if !a.pie => Some((&mut a.forward_arrow, &mut a.backward_arrow)),
            _ => None,
        }
    }

    /// Editable vertices, if the object has any.
    pub fn points(&self) -> Option<&[Point]> {
        match &self.kind {
            ObjectKind::Polyline(p) => Some(&p.points),
            ObjectKind::Spline(s) => Some(&s.points),
            ObjectKind::Arc(a) => Some(&a.points),
            _ => None,
        }
    }

    /// A cheap box around the object, line width included: a superset for
    /// pre-filtering picks, not the figure's bounds (those are fig2dev's,
    /// `rustfig_format::bridge::object_bounds`).
    pub fn approx_bbox(&self) -> Rect {
        match &self.kind {
            ObjectKind::Compound(c) => c.objects.iter().fold(Rect::EMPTY, |r, o| r.union(&o.approx_bbox())),
            ObjectKind::Text(t) => Rect::from_points(t.corners()),
            _ => {
                let mut r = Rect::EMPTY;
                for p in crate::flatten_object(self) {
                    for q in p.pts {
                        r.add_point(q);
                    }
                }
                let w = self
                    .style()
                    .map(|s| rustfig_geom::stroke_width(f64::from(s.thickness)) / 2.0)
                    .unwrap_or(0.0);
                r.expand(w)
            }
        }
    }

    /// Bounding box of the geometry only (no line width): what alignment uses.
    pub fn geometry_bbox(&self) -> Rect {
        match &self.kind {
            ObjectKind::Compound(c) => c.objects.iter().fold(Rect::EMPTY, |r, o| r.union(&o.geometry_bbox())),
            ObjectKind::Text(t) => Rect::from_points(t.corners()),
            _ => {
                let mut r = Rect::EMPTY;
                for p in crate::flatten_object(self) {
                    for q in p.pts {
                        r.add_point(q);
                    }
                }
                r
            }
        }
    }

    pub fn translate(&mut self, dx: f64, dy: f64) {
        self.transform(&Affine::translate(dx, dy), false);
    }

    /// Apply an affine transform to the geometry. `scale_text` also scales
    /// font sizes of non-rigid texts.
    pub fn transform(&mut self, t: &Affine, scale_text: bool) {
        let f = |p: &mut Point| *p = t.apply(*p);
        match &mut self.kind {
            ObjectKind::Ellipse(e) if similarity_angle(t).is_some() => {
                // xfig's `flip_ellipse`, `rotate_ellipse` and scale: the radii
                // scale, the angle turns (flip negates it and flips the
                // direction); circles keep their kind (W22-23).
                let Some((s, phi, mirror)) = similarity_angle(t) else {
                    return;
                };
                e.center = t.apply(e.center);
                f(&mut e.start);
                f(&mut e.end);
                e.radii = Point::new(e.radii.x * s, e.radii.y * s);
                if mirror {
                    e.direction ^= 1;
                    e.angle = -e.angle;
                } else if phi != 0.0 {
                    let a = (e.angle - phi).rem_euclid(std::f64::consts::TAU);
                    e.angle = (a * 1024.0).round() / 1024.0;
                }
            }
            ObjectKind::Ellipse(e) => {
                let (rx, ry, angle) = transform_ellipse(t, e.radii.x, e.radii.y, e.angle);
                e.center = t.apply(e.center);
                f(&mut e.start);
                f(&mut e.end);
                e.radii = Point::new(rx, ry);
                e.angle = angle;
                if e.kind.is_circle() && (rx - ry).abs() > 0.5 {
                    e.kind = EllipseKind::EllipseRadii;
                }
                if e.kind.is_circle() {
                    e.angle = 0.0;
                }
            }
            ObjectKind::Polyline(p) => {
                if p.kind == PolyKind::ArcBox && !preserves_axes(t) {
                    let r = Rect::from_points(p.points.iter().copied());
                    p.points = flatten_round_rect(&r, p.radius.max(0) as f64 * rustfig_geom::FIG_PER_STYLE_UNIT);
                    p.kind = PolyKind::Polygon;
                }
                p.points.iter_mut().for_each(f);
                if p.kind.is_rect() {
                    if preserves_axes(t) {
                        // xfig turns and mirrors the points in place (`flip_line`,
                        // `rotate_line`): the first point stays the first (W22-23).
                        if p.kind == PolyKind::Picture {
                            if let Some(pic) = &mut p.picture {
                                if t.is_mirroring() {
                                    pic.flipped = !pic.flipped;
                                }
                            }
                        }
                    } else if p.kind == PolyKind::Box {
                        p.kind = PolyKind::Polygon;
                    }
                }
            }
            ObjectKind::Spline(s) => s.points.iter_mut().for_each(f),
            ObjectKind::Arc(a) => {
                a.points.iter_mut().for_each(f);
                a.center = t.apply(a.center);
                a.refit();
            }
            ObjectKind::Text(tx) => {
                tx.origin = t.apply(tx.origin);
                let dir = Point::new(tx.angle.cos(), -tx.angle.sin());
                let nd = t.apply_vec(dir);
                let mut angle = (-nd.y).atan2(nd.x);
                if t.is_mirroring() {
                    // Keep text readable: mirror the anchor, not the glyphs.
                    let readable = -tx.angle;
                    if (angle - readable).cos() < 0.0 {
                        angle += std::f64::consts::PI;
                        tx.justify = tx.justify.swapped();
                    }
                }
                if angle.abs() < 1e-9 {
                    angle = 0.0;
                }
                tx.angle = angle.rem_euclid(std::f64::consts::TAU);
                if tx.angle > std::f64::consts::TAU - 1e-9 {
                    tx.angle = 0.0;
                }
                let s = t.mean_scale();
                if scale_text && !tx.is_rigid() && s.is_finite() && s > 0.0 {
                    tx.size = (tx.size * s).clamp(1.0, 2000.0);
                    tx.height *= s;
                    tx.length *= s;
                }
            }
            ObjectKind::Compound(c) => {
                for o in &mut c.objects {
                    o.transform(t, scale_text);
                }
            }
        }
    }

    /// Distance from `p` to the object's outline; 0 inside filled shapes
    /// and inside text boxes.
    pub fn distance(&self, p: Point) -> f64 {
        match &self.kind {
            ObjectKind::Compound(c) => c.objects.iter().map(|o| o.distance(p)).fold(f64::INFINITY, f64::min),
            ObjectKind::Text(t) => {
                let c = t.corners();
                if rustfig_geom::point_in_polygon(p, &c) {
                    0.0
                } else {
                    rustfig_geom::dist_to_polyline(p, &c, true)
                }
            }
            ObjectKind::Polyline(pl) if pl.kind == PolyKind::Picture => {
                if rustfig_geom::point_in_polygon(p, &pl.points) {
                    0.0
                } else {
                    rustfig_geom::dist_to_polyline(p, &pl.points, true)
                }
            }
            _ => {
                let filled = self.style().map(|s| s.is_filled()).unwrap_or(false);
                let mut best = f64::INFINITY;
                for fp in crate::flatten_object(self) {
                    if filled && fp.closed && rustfig_geom::point_in_polygon(p, &fp.pts) {
                        return 0.0;
                    }
                    best = best.min(rustfig_geom::dist_to_polyline(p, &fp.pts, fp.closed));
                }
                best
            }
        }
    }
}

/// For a rotation, uniform scale or axis flip (what xfig's modes produce):
/// the scale factor, the visual turn (clockwise, y down) and whether it
/// mirrors. `None` for shears and skews.
fn similarity_angle(t: &Affine) -> Option<(f64, f64, bool)> {
    let eps = 1e-9;
    let sc = (t.a * t.a + t.b * t.b).sqrt();
    if !sc.is_finite() || sc < eps {
        return None;
    }
    let tol = 1e-6 * sc;
    if (t.a - t.d).abs() < tol && (t.b + t.c).abs() < tol {
        let phi = t.b.atan2(t.a);
        return Some((sc, if phi.abs() < eps { 0.0 } else { phi }, false));
    }
    if t.b.abs() < tol && t.c.abs() < tol && (t.a + t.d).abs() < tol {
        return Some((sc, 0.0, true));
    }
    None
}

/// True for transforms that keep axis-aligned rectangles axis-aligned.
fn preserves_axes(t: &Affine) -> bool {
    (t.b.abs() < 1e-9 && t.c.abs() < 1e-9) || (t.a.abs() < 1e-9 && t.d.abs() < 1e-9)
}

/// Transform an ellipse given by radii and angle; returns new radii and angle
/// (via the singular value decomposition of the 2x2 linear part).
fn transform_ellipse(t: &Affine, rx: f64, ry: f64, angle: f64) -> (f64, f64, f64) {
    // M = L * R(angle) * diag(rx, ry), columns are the transformed semi-axes.
    let r = Affine::rotate(angle);
    let u = t.apply_vec(r.apply_vec(Point::new(rx, 0.0)));
    let v = t.apply_vec(r.apply_vec(Point::new(0.0, ry)));
    let (a, b, c, d) = (u.x, v.x, u.y, v.y);
    // SVD of [a b; c d].
    let e = (a + d) / 2.0;
    let f = (a - d) / 2.0;
    let g = (c + b) / 2.0;
    let h = (c - b) / 2.0;
    let q = (e * e + h * h).sqrt();
    let w = (f * f + g * g).sqrt();
    let sx = q + w;
    let sy = (q - w).abs();
    let a1 = g.atan2(f);
    let a2 = h.atan2(e);
    let theta = (a2 - a1) / 2.0;
    let phi = (a2 + a1) / 2.0;
    let _ = theta;
    // `phi` is the rotation of the major axis in y-down maths coordinates.
    let new_angle = -phi;
    if !(sx.is_finite() && sy.is_finite() && new_angle.is_finite()) {
        return (rx, ry, angle);
    }
    let na = new_angle.rem_euclid(std::f64::consts::PI);
    (sx, sy, if na.abs() < 1e-9 { 0.0 } else { na })
}

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

    fn ellipse(rx: f64, ry: f64, angle: f64) -> Object {
        Object::new(ObjectKind::Ellipse(Ellipse {
            kind: EllipseKind::EllipseRadii,
            style: Style::default(),
            direction: 1,
            angle,
            center: Point::new(100.0, 100.0),
            radii: Point::new(rx, ry),
            start: Point::new(100.0, 100.0),
            end: Point::new(100.0 + rx, 100.0 + ry),
        }))
    }

    #[test]
    fn ellipse_scale_and_rotate() {
        let mut e = ellipse(200.0, 100.0, 0.0);
        e.transform(&Affine::scale(2.0, 1.0), false);
        let ObjectKind::Ellipse(el) = &e.kind else { panic!() };
        assert!((el.radii.x - 400.0).abs() < 1e-6 && (el.radii.y - 100.0).abs() < 1e-6);
        assert!(el.angle.abs() < 1e-9);

        let mut e = ellipse(200.0, 100.0, 0.0);
        let before = e.geometry_bbox();
        e.transform(
            &Affine::about(Affine::rotate(std::f64::consts::FRAC_PI_2), Point::new(100.0, 100.0)),
            false,
        );
        let after = e.geometry_bbox();
        assert!((before.width() - after.height()).abs() < 2.0);
        assert!((before.height() - after.width()).abs() < 2.0);
    }

    #[test]
    fn box_rotated_becomes_polygon() {
        let mut o = Object::new(ObjectKind::Polyline(Polyline::rect(
            Style::default(),
            Point::new(0.0, 0.0),
            Point::new(100.0, 50.0),
        )));
        o.transform(&Affine::rotate(std::f64::consts::FRAC_PI_2), false);
        let ObjectKind::Polyline(p) = &o.kind else { panic!() };
        assert_eq!(p.kind, PolyKind::Box);
        o.transform(&Affine::rotate(0.3), false);
        let ObjectKind::Polyline(p) = &o.kind else { panic!() };
        assert_eq!(p.kind, PolyKind::Polygon);
    }

    #[test]
    fn text_flip_keeps_readable() {
        let mut o = Object::new(ObjectKind::Text(Text {
            justify: Justify::Left,
            color: ColorRef::BLACK,
            depth: 50,
            pen_style: -1,
            font: 0,
            size: 12.0,
            angle: 0.0,
            flags: text_flags::POSTSCRIPT,
            height: 100.0,
            length: 300.0,
            origin: Point::new(100.0, 100.0),
            text: "hi".into(),
            local_font: None,
        }));
        o.transform(&Affine::scale(-1.0, 1.0), false);
        let ObjectKind::Text(t) = &o.kind else { panic!() };
        assert_eq!(t.justify, Justify::Right);
        assert!(t.angle.abs() < 1e-9);
        assert_eq!(t.origin, Point::new(-100.0, 100.0));
        o.transform(&Affine::scale(1.0, -1.0), false);
        let ObjectKind::Text(t) = &o.kind else { panic!() };
        assert_eq!(t.justify, Justify::Right);
        assert!(t.angle.abs() < 1e-9);
    }

    #[test]
    fn serde_roundtrip() {
        let o = ellipse(10.0, 20.0, 0.5);
        let s = serde_json::to_string(&o).unwrap();
        let back: Object = serde_json::from_str(&s).unwrap();
        assert_eq!(o, back);
    }

    #[test]
    fn depth_of_compound() {
        let mut a = ellipse(1.0, 1.0, 0.0);
        a.set_depth(30);
        let mut b = ellipse(1.0, 1.0, 0.0);
        b.set_depth(60);
        let mut c = Object::new(ObjectKind::Compound(Compound { objects: vec![a, b] }));
        assert_eq!(c.depth(), 30);
        c.set_depth(10);
        let ObjectKind::Compound(cc) = &c.kind else { panic!() };
        assert_eq!(cc.objects[0].depth(), 10);
        assert_eq!(cc.objects[1].depth(), 40);
    }
}
