//! Expression evaluation functions

use crate::ast::*;
use crate::errors::PikruError;
use crate::types::{Angle, EvalValue, Length as Inches, OffsetIn, Point};

use super::context::RenderContext;
use super::types::*;

// From implementations for EvalValue
impl From<f64> for EvalValue {
    fn from(value: f64) -> Self {
        EvalValue::Scalar(value)
    }
}

impl From<f32> for EvalValue {
    fn from(value: f32) -> Self {
        EvalValue::Scalar(value as f64)
    }
}

// From implementations for Length (Inches)
impl From<f64> for Inches {
    fn from(value: f64) -> Self {
        Inches::inches(value)
    }
}

impl From<f32> for Inches {
    fn from(value: f32) -> Self {
        Inches::inches(value as f64)
    }
}

/// Get the nth vertex of a rendered object (line, spline, etc.)
/// cref: pikchr "Nth vertex of object" syntax
/// For line/spline objects, returns the nth waypoint (1-indexed)
/// Falls back to start/end/center for objects without waypoints
fn get_nth_vertex(obj: &RenderedObject, nth: &Nth) -> PointIn {
    // Try to get waypoints from the object
    if let Some(waypoints) = obj.waypoints() {
        let len = waypoints.len();
        if len == 0 {
            return obj.center();
        }

        let index = match nth {
            Nth::First(_) | Nth::Ordinal(1, _, _) => 0,
            Nth::Last(_) | Nth::Previous(_) => len - 1,
            Nth::Ordinal(n, _, _) => {
                // Pikchr uses 1-based indexing
                let idx = (*n as usize).saturating_sub(1);
                idx.min(len - 1)
            }
        };

        crate::log::debug!(
            nth = ?nth,
            waypoints_len = len,
            index = index,
            vertex_x = waypoints[index].x.raw(),
            vertex_y = waypoints[index].y.raw(),
            "get_nth_vertex"
        );

        waypoints[index]
    } else {
        // Fallback for non-line objects
        match nth {
            Nth::First(_) | Nth::Ordinal(1, _, _) => obj.start(),
            Nth::Last(_) | Nth::Previous(_) => obj.end(),
            Nth::Ordinal(_, _, _) => obj.center(),
        }
    }
}

pub fn eval_expr(ctx: &RenderContext, expr: &Expr) -> Result<Value, PikruError> {
    match expr {
        Expr::Number(n) => {
            // cref: pik_expr (pikchr.c) - bare numbers are unitless scalars
            // When used in length contexts (assignments to boxwid, etc.), they're
            // interpreted as inches. But in expressions like "2*dw", the 2 is a scalar
            // multiplier, so Scalar * Length = Length works correctly.
            if !n.is_finite() {
                return Err(PikruError::Generic(
                    "Invalid numeric literal: not finite".to_string(),
                ));
            }
            Ok(Value::Scalar(*n))
        }
        Expr::Variable(name) => {
            // cref: pik_get_var (pikchr.c:6625) - falls back to color lookup
            if let Some(val) = ctx.variables.get(name) {
                Ok(Value::from(*val))
            } else {
                // Try parsing as a color name (always succeeds, returns Raw if unknown)
                let color = name.parse::<crate::types::Color>().unwrap();
                let rgb_str = color.to_rgb_string();
                if let Some(rgb) = rgb_str
                    .strip_prefix("rgb(")
                    .and_then(|s| s.strip_suffix(')'))
                {
                    let parts: Vec<&str> = rgb.split(',').collect();
                    if parts.len() == 3
                        && let (Ok(r), Ok(g), Ok(b)) = (
                            parts[0].trim().parse::<u32>(),
                            parts[1].trim().parse::<u32>(),
                            parts[2].trim().parse::<u32>(),
                        )
                    {
                        let color_val = (r << 16) | (g << 8) | b;
                        return Ok(Value::from(EvalValue::Color(color_val)));
                    }
                }
                Err(PikruError::Generic(format!("Undefined variable: {}", name)))
            }
        }
        Expr::BuiltinVar(b) => {
            let key = match b {
                BuiltinVar::Fill => "fill",
                BuiltinVar::Color => "color",
                BuiltinVar::Thickness => "thickness",
            };
            ctx.variables
                .get(key)
                .copied()
                .map(Value::from)
                .ok_or_else(|| PikruError::Generic(format!("Undefined builtin: {}", key)))
        }
        Expr::BinaryOp(lhs, op, rhs) => {
            let l = eval_expr(ctx, lhs)?;
            let r = eval_expr(ctx, rhs)?;
            use Value::*;
            let result = match (l, r, op) {
                // Length + Length = Length (typed op)
                (Len(a), Len(b), BinaryOp::Add) => Len(a + b),
                // Length - Length = Length (typed op)
                (Len(a), Len(b), BinaryOp::Sub) => Len(a - b),
                // Length * Length = Scalar (area-like, unitless)
                (Len(a), Len(b), BinaryOp::Mul) => Scalar(a.raw() * b.raw()),
                // Length / Length = Scalar (ratio, via checked_div)
                (Len(a), Len(b), BinaryOp::Div) => a
                    .checked_div(b)
                    .map(|s| Scalar(s.raw()))
                    .ok_or_else(|| PikruError::Generic("Division by zero".to_string()))?,
                // Length + Scalar: treat scalar as length (C compatibility)
                (Len(a), Scalar(b), BinaryOp::Add) => Len(a + Inches::inches(b)),
                (Len(a), Scalar(b), BinaryOp::Sub) => Len(a - Inches::inches(b)),
                // Length * Scalar = Length (scaling, typed op)
                (Len(a), Scalar(b), BinaryOp::Mul) => Len(a * b),
                // Length / Scalar = Length (typed op)
                (Len(a), Scalar(b), BinaryOp::Div) => {
                    if b == 0.0 {
                        return Err(PikruError::Generic("Division by zero".to_string()));
                    }
                    Len(a / b)
                }
                // Scalar + Length: treat scalar as length (C compatibility)
                (Scalar(a), Len(b), BinaryOp::Add) => Len(Inches::inches(a) + b),
                (Scalar(a), Len(b), BinaryOp::Sub) => Len(Inches::inches(a) - b),
                // Scalar * Length = Length (scaling, typed op)
                (Scalar(a), Len(b), BinaryOp::Mul) => Len(crate::types::Scalar(a) * b),
                // Scalar / Length = Scalar (inverse scaling)
                (Scalar(a), Len(b), BinaryOp::Div) => {
                    if b.raw() == 0.0 {
                        return Err(PikruError::Generic("Division by zero".to_string()));
                    }
                    Scalar(a / b.raw())
                }
                // Scalar ops (unitless)
                (Scalar(a), Scalar(b), BinaryOp::Add) => Scalar(a + b),
                (Scalar(a), Scalar(b), BinaryOp::Sub) => Scalar(a - b),
                (Scalar(a), Scalar(b), BinaryOp::Mul) => Scalar(a * b),
                (Scalar(a), Scalar(b), BinaryOp::Div) => {
                    if b == 0.0 {
                        return Err(PikruError::Generic("Division by zero".to_string()));
                    }
                    Scalar(a / b)
                }
                // Colors can't participate in mathematical operations
                (Color(_), _, _) | (_, Color(_), _) => {
                    return Err(PikruError::Generic(
                        "Cannot perform math operations on colors".to_string(),
                    ));
                }
            };
            // Validate result is finite (catches overflow to infinity)
            validate_value(result)
        }
        Expr::UnaryOp(op, e) => {
            let v = eval_expr(ctx, e)?;
            Ok(match (op, v) {
                (UnaryOp::Neg, Value::Len(l)) => Value::Len(-l), // typed Neg op
                (UnaryOp::Pos, Value::Len(l)) => Value::Len(l),
                (UnaryOp::Neg, Value::Scalar(s)) => Value::Scalar(-s),
                (UnaryOp::Pos, Value::Scalar(s)) => Value::Scalar(s),
                (_, Value::Color(_)) => {
                    return Err(PikruError::Generic(
                        "Cannot perform unary operations on colors".to_string(),
                    ));
                }
            })
        }
        Expr::ParenExpr(e) => eval_expr(ctx, e),
        Expr::FuncCall(fc) => {
            let args: Result<Vec<Value>, _> = fc.args.iter().map(|a| eval_expr(ctx, a)).collect();
            let args = args?;
            use Value::*;
            let result = match fc.func {
                Function::Abs => match args[0] {
                    Len(l) => Len(l.abs()), // typed abs
                    Scalar(s) => Scalar(s.abs()),
                    Color(_) => {
                        return Err(PikruError::Generic(
                            "Cannot take abs() of a color".to_string(),
                        ));
                    }
                },
                Function::Cos => {
                    let v = match args[0] {
                        Len(l) => l.raw(),
                        Scalar(s) => s,
                        Color(_) => {
                            return Err(PikruError::Generic(
                                "Cannot take cos() of a color".to_string(),
                            ));
                        }
                    };
                    Scalar(v.to_radians().cos())
                }
                Function::Sin => {
                    let v = match args[0] {
                        Len(l) => l.raw(),
                        Scalar(s) => s,
                        Color(_) => {
                            return Err(PikruError::Generic(
                                "Cannot take sin() of a color".to_string(),
                            ));
                        }
                    };
                    Scalar(v.to_radians().sin())
                }
                Function::Int => match args[0] {
                    Len(l) => Len(Inches::inches(l.raw().trunc())),
                    Scalar(s) => Scalar(s.trunc()),
                    Color(_) => {
                        return Err(PikruError::Generic(
                            "Cannot take int() of a color".to_string(),
                        ));
                    }
                },
                Function::Sqrt => match args[0] {
                    Len(l) if l.raw() < 0.0 => {
                        return Err(PikruError::Generic("sqrt of negative".to_string()));
                    }
                    Len(l) => Len(Inches::inches(l.raw().sqrt())),
                    Scalar(s) if s < 0.0 => {
                        return Err(PikruError::Generic("sqrt of negative".to_string()));
                    }
                    Scalar(s) => Scalar(s.sqrt()),
                    Color(_) => {
                        return Err(PikruError::Generic(
                            "Cannot take sqrt() of a color".to_string(),
                        ));
                    }
                },
                Function::Max => {
                    let a = match args[0] {
                        Len(l) => l.raw(),
                        Scalar(s) => s,
                        Color(_) => {
                            return Err(PikruError::Generic(
                                "Cannot take max() of a color".to_string(),
                            ));
                        }
                    };
                    let b = match args[1] {
                        Len(l) => l.raw(),
                        Scalar(s) => s,
                        Color(_) => {
                            return Err(PikruError::Generic(
                                "Cannot take max() of a color".to_string(),
                            ));
                        }
                    };
                    Scalar(a.max(b))
                }
                Function::Min => {
                    let a = match args[0] {
                        Len(l) => l.raw(),
                        Scalar(s) => s,
                        Color(_) => {
                            return Err(PikruError::Generic(
                                "Cannot take min() of a color".to_string(),
                            ));
                        }
                    };
                    let b = match args[1] {
                        Len(l) => l.raw(),
                        Scalar(s) => s,
                        Color(_) => {
                            return Err(PikruError::Generic(
                                "Cannot take min() of a color".to_string(),
                            ));
                        }
                    };
                    Scalar(a.min(b))
                }
            };
            validate_value(result)
        }
        Expr::DistCall(p1, p2) => {
            let a = eval_position(ctx, p1)?;
            let b = eval_position(ctx, p2)?;
            // Use typed subtraction: Point - Point = Offset
            let offset = b - a;
            let dist = Inches::inches((offset.dx.raw().powi(2) + offset.dy.raw().powi(2)).sqrt());
            Ok(Value::Len(dist))
        }
        Expr::ObjectProp(obj, prop_ref) => {
            let r = resolve_object(ctx, obj).ok_or_else(|| {
                PikruError::Generic("Unknown object in property lookup".to_string())
            })?;
            match prop_ref {
                PropertyRef::Num(prop) => {
                    let val = match prop {
                        NumProperty::Width => r.width(),
                        NumProperty::Height => r.height(),
                        NumProperty::Radius | NumProperty::Diameter => {
                            r.width().min(r.height()) / 2.0
                        }
                        NumProperty::Thickness => r.style().stroke_width,
                    };
                    Ok(Value::Len(val))
                }
                PropertyRef::Dash(prop) => {
                    let val = match prop {
                        DashProperty::Dashed => {
                            r.style().dashed.unwrap_or(Inches::ZERO)
                        }
                        DashProperty::Dotted => {
                            r.style().dotted.unwrap_or(Inches::ZERO)
                        }
                    };
                    Ok(Value::Len(val))
                }
                PropertyRef::Color(prop) => {
                    let color_str = match prop {
                        ColorProperty::Color => &r.style().stroke,
                        ColorProperty::Fill => &r.style().fill,
                    };
                    let color = color_str.parse::<crate::types::Color>().unwrap();
                    let rgb = color.to_u32();
                    Ok(Value::Scalar(rgb as f64))
                }
            }
        }
        Expr::ObjectCoord(obj, coord) => {
            let r = resolve_object(ctx, obj)
                .ok_or_else(|| PikruError::Generic("Unknown object in coord lookup".to_string()))?;
            Ok(Value::Len(match coord {
                Coord::X => r.center().x,
                Coord::Y => r.center().y,
            }))
        }
        Expr::ObjectEdgeCoord(obj, edge, coord) => {
            let r = resolve_object(ctx, obj).ok_or_else(|| {
                PikruError::Generic("Unknown object in edge coord lookup".to_string())
            })?;
            let pt = get_edge_point(r, edge);
            Ok(Value::Len(match coord {
                Coord::X => pt.x,
                Coord::Y => pt.y,
            }))
        }
        Expr::VertexCoord(nth, obj, coord) => {
            let r = resolve_object(ctx, obj).ok_or_else(|| {
                PikruError::Generic("Unknown object in vertex coord lookup".to_string())
            })?;
            let target = get_nth_vertex(r, nth);
            Ok(Value::Len(match coord {
                Coord::X => target.x,
                Coord::Y => target.y,
            }))
        }
        Expr::PlaceName(name) => Err(PikruError::Generic(format!(
            "Unsupported place name in expression: {}",
            name
        ))),
    }
}

pub fn eval_len(ctx: &RenderContext, expr: &Expr) -> Result<Inches, PikruError> {
    match eval_expr(ctx, expr)? {
        Value::Len(l) => Ok(l),
        Value::Scalar(s) => Ok(Inches(s)), // treat scalar as inches for len contexts
        Value::Color(_) => Err(PikruError::Generic(
            "Cannot use a color as a length".to_string(),
        )),
    }
}

pub fn eval_scalar(ctx: &RenderContext, expr: &Expr) -> Result<f64, PikruError> {
    match eval_expr(ctx, expr)? {
        Value::Scalar(s) => Ok(s),
        Value::Len(l) => Ok(l.0),
        Value::Color(_) => Err(PikruError::Generic(
            "Cannot use a color as a scalar".to_string(),
        )),
    }
}

pub fn eval_rvalue(ctx: &RenderContext, rvalue: &RValue) -> Result<EvalValue, PikruError> {
    match rvalue {
        RValue::Expr(e) => {
            crate::log::debug!("eval_rvalue: RValue::Expr({:?})", e);
            let value = eval_expr(ctx, e)?;
            let eval_val = EvalValue::from(value);
            crate::log::debug!("eval_rvalue: converted to {:?}", eval_val);
            Ok(eval_val)
        }
        RValue::PlaceName(name) => {
            crate::log::debug!("eval_rvalue: RValue::PlaceName({})", name);
            // Try to parse as a color name
            let color = name.parse::<crate::types::Color>().unwrap();
            let rgb_str = color.to_rgb_string();
            crate::log::debug!("eval_rvalue: parsed color {} -> {}", name, rgb_str);
            if let Some(rgb) = rgb_str
                .strip_prefix("rgb(")
                .and_then(|s| s.strip_suffix(')'))
            {
                let parts: Vec<&str> = rgb.split(',').collect();
                if parts.len() == 3
                    && let (Ok(r), Ok(g), Ok(b)) = (
                        parts[0].trim().parse::<u32>(),
                        parts[1].trim().parse::<u32>(),
                        parts[2].trim().parse::<u32>(),
                    )
                {
                    let color_val = (r << 16) | (g << 8) | b;
                    crate::log::debug!("eval_rvalue: returning Color({})", color_val);
                    return Ok(EvalValue::Color(color_val));
                }
            }
            crate::log::debug!("eval_rvalue: failed to parse color, returning Scalar(0.0)");
            Ok(EvalValue::Scalar(0.0))
        }
    }
}

pub fn eval_position(ctx: &RenderContext, pos: &Position) -> Result<PointIn, PikruError> {
    match pos {
        Position::Coords(x, y) => {
            let px = eval_len(ctx, x)?;
            let py = eval_len(ctx, y)?;
            Ok(Point::new(px, py))
        }
        Position::Place(place) => {
            let result = eval_place(ctx, place)?;
            crate::log::debug!(
                ?place,
                result_x = result.x.0,
                result_y = result.y.0,
                "Position::Place"
            );
            Ok(result)
        }
        Position::PlaceOffset(place, op, dx, dy) => {
            let base = eval_place(ctx, place)?;
            let dx_val = eval_len(ctx, dx)?;
            let dy_val = eval_len(ctx, dy)?;
            let offset = OffsetIn::new(dx_val, dy_val);
            match op {
                BinaryOp::Add => Ok(base + offset),
                BinaryOp::Sub => Ok(base - offset),
                _ => Ok(base),
            }
        }
        Position::Between(factor, pos1, pos2) => {
            let f = eval_scalar(ctx, factor)?;
            let p1 = eval_position(ctx, pos1)?;
            let p2 = eval_position(ctx, pos2)?;
            // Interpolate: p1 + (p2 - p1) * f
            let result = p1 + (p2 - p1) * f;
            crate::log::debug!(
                f = f,
                p1_x = p1.x.0,
                p1_y = p1.y.0,
                p2_x = p2.x.0,
                p2_y = p2.y.0,
                result_x = result.x.0,
                result_y = result.y.0,
                "Position::Between calculation"
            );
            Ok(result)
        }
        Position::Bracket(factor, pos1, pos2) => {
            // Same as between: p1 + (p2 - p1) * f
            let f = eval_scalar(ctx, factor)?;
            let p1 = eval_position(ctx, pos1)?;
            let p2 = eval_position(ctx, pos2)?;
            Ok(p1 + (p2 - p1) * f)
        }
        Position::AboveBelow(dist, ab, base_pos) => {
            let d = eval_len(ctx, dist)?;
            let base = eval_position(ctx, base_pos)?;
            // Y-up: Above = +Y, Below = -Y
            let offset = match ab {
                AboveBelow::Above => OffsetIn::new(Inches::ZERO, d),
                AboveBelow::Below => OffsetIn::new(Inches::ZERO, -d),
            };
            Ok(base + offset)
        }
        Position::LeftRightOf(dist, lr, base_pos) => {
            let d = eval_len(ctx, dist)?;
            let base = eval_position(ctx, base_pos)?;
            let offset = match lr {
                LeftRight::Left => OffsetIn::new(-d, Inches::ZERO),
                LeftRight::Right => OffsetIn::new(d, Inches::ZERO),
            };
            Ok(base + offset)
        }
        Position::EdgePointOf(dist, edge, base_pos) => {
            let d = eval_len(ctx, dist)?;
            let base = eval_position(ctx, base_pos)?;
            // Calculate offset based on edge direction
            let dir = edge.to_unit_vec();
            Ok(base + dir * d)
        }
        Position::Heading(dist, heading, base_pos) => {
            let d = eval_len(ctx, dist)?;
            let base = eval_position(ctx, base_pos)?;
            let angle = match heading {
                HeadingDir::EdgePoint(ep) => ep.to_angle(),
                HeadingDir::Expr(e) => Angle::degrees(eval_scalar(ctx, e).unwrap_or(0.0)),
            };
            // C pikchr uses: pt.x += dist*sin(r); pt.y += dist*cos(r);
            // We use the same Y-up convention internally, flip happens in to_svg().
            let rad = angle.to_radians();
            Ok(Point::new(
                base.x + Inches(d.0 * rad.sin()),
                base.y + Inches(d.0 * rad.cos()),
            ))
        }
        Position::Tuple(pos1, pos2) => {
            // Extract x from pos1, y from pos2
            let p1 = eval_position(ctx, pos1)?;
            let p2 = eval_position(ctx, pos2)?;
            Ok(Point::new(p1.x, p2.y))
        }
    }
}

pub fn endpoint_object_from_position(
    ctx: &RenderContext,
    pos: &Position,
) -> Option<EndpointObject> {
    // cref: pik_position_from_place (pikchr.c) - only sets ppObj for direct object/edge references
    // Calculated positions (between, above/below, bracket) do NOT set pFrom/pTo for autochop
    match pos {
        Position::Place(place) => endpoint_object_from_place(ctx, place),
        // Extract underlying Place from offset positions (e.g., C0.ne + (0.05,0))
        // These still reference an object for autochop purposes
        Position::PlaceOffset(place, _, _, _) => endpoint_object_from_place(ctx, place),
        // cref: PL_BETWEEN in pik_position_from_place - does NOT set *ppObj
        // For "between A and B", no object attachment (calculated position)
        Position::Between(_, _, _) => None,
        // cref: Other calculated positions also don't set object attachment
        Position::AboveBelow(_, _, _) => None,
        Position::Bracket(_, _, _) => None,
        _ => None,
    }
}

fn endpoint_object_from_place(ctx: &RenderContext, place: &Place) -> Option<EndpointObject> {
    match place {
        // Only return object for center references (e.g., `C0`, `last box`)
        // cref: pik_last_ref_object (pikchr.c) - only returns object if point == ptAt (center)
        // Edge references like `C0.ne` or `.ne of C0` do NOT trigger autochop
        Place::Object(obj) => {
            // cref: pik_position_from_place (pikchr.c)
            // When referencing objects inside sublists (dotted names like Ptr.A),
            // C pikchr does NOT trigger implicit autochop (both endpoints present).
            // However, explicit `chop` attribute DOES work for dotted names.
            // We mark dotted names so the autochop logic can differentiate.
            if let Object::Named(name) = obj
                && !name.path.is_empty()
            {
                // Object is inside a sublist (e.g., Ptr.A) - mark as dotted name
                crate::log::debug!(
                    ?name,
                    "endpoint_object_from_place: dotted name (explicit chop works, implicit autochop disabled)"
                );
                return resolve_object(ctx, obj).map(EndpointObject::from_rendered_dotted);
            }
            resolve_object(ctx, obj).map(EndpointObject::from_rendered)
        }
        // Edge/vertex references do NOT set object attachment for autochop
        // cref: pik_last_ref_object checks ptAt == pPt, which fails for edge points
        Place::ObjectEdge(_, _) | Place::EdgePointOf(_, _) | Place::Vertex(_, _) => None,
    }
}

fn eval_place(ctx: &RenderContext, place: &Place) -> Result<PointIn, PikruError> {
    match place {
        Place::Object(obj) => {
            if let Some(rendered) = resolve_object(ctx, obj) {
                Ok(rendered.center())
            } else {
                // Check if it's a named position (e.g., `OUT: 6.3in right of previous.e`)
                if let Object::Named(name) = obj
                    && let ObjectNameBase::PlaceName(n) = &name.base
                    && let Some(pos) = ctx.get_named_position(n)
                {
                    crate::log::debug!(
                        name = %n,
                        x = pos.x.raw(),
                        y = pos.y.raw(),
                        "eval_place: found named position"
                    );
                    return Ok(pos);
                }
                Ok(ctx.position)
            }
        }
        Place::ObjectEdge(obj, edge) => {
            if let Some(rendered) = resolve_object(ctx, obj) {
                let edge_point = get_edge_point(rendered, edge);
                crate::log::debug!(
                    ?edge,
                    center_x = rendered.center().x.raw(),
                    center_y = rendered.center().y.raw(),
                    edge_x = edge_point.x.raw(),
                    edge_y = edge_point.y.raw(),
                    "eval_place: ObjectEdge"
                );
                Ok(edge_point)
            } else {
                Ok(ctx.position)
            }
        }
        Place::EdgePointOf(edge, obj) => {
            if let Some(rendered) = resolve_object(ctx, obj) {
                Ok(get_edge_point(rendered, edge))
            } else {
                Ok(ctx.position)
            }
        }
        Place::Vertex(nth, obj) => {
            if let Some(rendered) = resolve_object(ctx, obj) {
                Ok(get_nth_vertex(rendered, nth))
            } else {
                Ok(ctx.position)
            }
        }
    }
}

#[allow(unused_variables)]
pub fn resolve_object<'a>(ctx: &'a RenderContext, obj: &Object) -> Option<&'a RenderedObject> {
    match obj {
        Object::Named(name) => {
            // First resolve the base object
            let base_obj = match &name.base {
                ObjectNameBase::PlaceName(n) => ctx.get_object(n),
                ObjectNameBase::This => ctx.current_object.as_ref().or_else(|| ctx.last_object()),
            }?;

            // Then follow the path through sublists (e.g., Main.A -> Main's child A)
            if name.path.is_empty() {
                Some(base_obj)
            } else {
                resolve_path_in_object(base_obj, &name.path)
            }
        }
        Object::Nth(nth) => resolve_nth(ctx, nth),
        Object::NthOf(nth, container) => {
            // Scoped ordinal: "1st box of A" - look up nth within container's children
            let container_obj = resolve_object(ctx, container)?;
            let children = container_obj.children()?;
            resolve_nth_in_children(children, nth)
        }
    }
}

/// Resolve an nth reference against the current context
fn resolve_nth<'a>(ctx: &'a RenderContext, nth: &Nth) -> Option<&'a RenderedObject> {
    match nth {
        Nth::Last(class) => {
            let oc = class.as_ref().and_then(nth_class_to_class_name);
            ctx.get_last_object(oc)
        }
        Nth::First(class) => {
            let oc = class.as_ref().and_then(nth_class_to_class_name);
            let obj = ctx.get_nth_object(1, oc);
            if let Some(_o) = obj {
                crate::log::debug!(
                    name = ?_o.name,
                    class = ?_o.class(),
                    start_x = _o.start().x.0,
                    start_y = _o.start().y.0,
                    "resolve_object Nth::First"
                );
            }
            obj
        }
        Nth::Ordinal(n, modifier, class) => {
            let oc = class.as_ref().and_then(nth_class_to_class_name);
            match modifier {
                NthModifier::Last | NthModifier::Previous => {
                    // "3rd last box" / "3rd previous box" - count from end
                    ctx.get_nth_last_object(*n as usize, oc)
                }
                NthModifier::None => {
                    // "3rd box" - count from start
                    ctx.get_nth_object(*n as usize, oc)
                }
            }
        }
        Nth::Previous(class) => {
            // "previous" or "previous box" - most recently completed object
            let oc = class.as_ref().and_then(nth_class_to_class_name);
            if oc.is_some() {
                // "previous box" = "last box" (per pikchr spec, previous ≡ last)
                ctx.get_last_object(oc)
            } else {
                ctx.object_list.last()
            }
        }
    }
}

/// Resolve an nth reference within a container's children
fn resolve_nth_in_children<'a>(children: &'a [RenderedObject], nth: &Nth) -> Option<&'a RenderedObject> {
    match nth {
        Nth::First(class) | Nth::Last(class) | Nth::Previous(class) => {
            let oc = class.as_ref().and_then(nth_class_to_class_name);
            let matching: Vec<&RenderedObject> = if let Some(cn) = oc {
                children.iter().filter(|c| c.class() == cn).collect()
            } else {
                children.iter().collect()
            };
            match nth {
                Nth::First(_) => matching.first().copied(),
                _ => matching.last().copied(), // last and previous are equivalent
            }
        }
        Nth::Ordinal(n, modifier, class) => {
            let oc = class.as_ref().and_then(nth_class_to_class_name);
            let matching: Vec<&RenderedObject> = if let Some(cn) = oc {
                children.iter().filter(|c| c.class() == cn).collect()
            } else {
                children.iter().collect()
            };
            match modifier {
                NthModifier::Last | NthModifier::Previous => {
                    // Count from end
                    let idx = matching.len().checked_sub(*n as usize)?;
                    matching.get(idx).copied()
                }
                NthModifier::None => {
                    // Count from start (1-based)
                    matching.get((*n as usize).checked_sub(1)?).copied()
                }
            }
        }
    }
}

/// Resolve a path within an object's children (e.g., ["A"] finds child named "A")
fn resolve_path_in_object<'a>(
    obj: &'a RenderedObject,
    path: &[String],
) -> Option<&'a RenderedObject> {
    if path.is_empty() {
        return Some(obj);
    }

    let (next_name, remaining) = path.split_first().unwrap();
    let children = obj.children()?;
    let child = children
        .iter()
        .find(|child| child.name.as_deref() == Some(next_name.as_str()))?;

    crate::log::debug!(
        parent_name = ?obj.name,
        child_name = next_name,
        child_center_x = child.center().x.raw(),
        child_center_y = child.center().y.raw(),
        child_start_x = child.start().x.raw(),
        child_start_y = child.start().y.raw(),
        "resolve_path_in_object: found child"
    );

    resolve_path_in_object(child, remaining)
}

fn nth_class_to_class_name(nc: &NthClass) -> Option<ClassName> {
    match nc {
        NthClass::ClassName(cn) => Some(*cn),
        NthClass::Sublist => Some(ClassName::Sublist),
    }
}

// cref: pik_set_at (pikchr.c:6195-6199) - converts Start/End to compass points
#[allow(clippy::let_and_return)] // We want the binding for debug logging
fn get_edge_point(obj: &RenderedObject, edge: &EdgePoint) -> PointIn {
    use crate::ast::Direction;
    use crate::render::shapes::ShapeEnum;

    // For lines, .start refers to actual waypoint (ptEnter in C)
    // For open lines, .end also refers to waypoint (ptExit)
    // For CLOSED lines (with "close" attribute), .end is computed from
    // bounding box like block objects
    // cref: pik_place_of_elem (pikchr.c:4118-4122)
    // cref: pikchr.c:4398-4404 - "For 'closed' lines, the .end is one of the
    //       .e, .s, .w, or .n points of the bounding box, as with block objects"
    // Note: .start is still the first waypoint even for closed lines (ptEnter is not changed)
    let is_closed = obj.style().close_path;

    match (&obj.shape, edge) {
        // .start is always from waypoints for lines (closed or not)
        (ShapeEnum::Line(line), EdgePoint::Start) => {
            return line.waypoints.first().copied().unwrap_or(obj.center());
        }
        // .end uses waypoints for OPEN lines only
        (ShapeEnum::Line(line), EdgePoint::End) if !is_closed => {
            return line.waypoints.last().copied().unwrap_or(obj.center());
        }
        (ShapeEnum::Arc(arc), EdgePoint::Start) => {
            return arc.start;
        }
        (ShapeEnum::Arc(arc), EdgePoint::End) => {
            return arc.end;
        }
        // Splines: .start always from waypoints
        (ShapeEnum::Spline(spline), EdgePoint::Start) => {
            return spline.waypoints.first().copied().unwrap_or(obj.center());
        }
        // Splines: .end uses waypoints for OPEN splines only
        (ShapeEnum::Spline(spline), EdgePoint::End) if !is_closed => {
            return spline.waypoints.last().copied().unwrap_or(obj.center());
        }
        _ => {}
    }
    // For closed lines/splines with .end, fall through to use bbox-based edge points

    // Convert Start/End to compass points based on object's stored direction
    // (for non-line objects)
    let resolved_edge = match edge {
        EdgePoint::Start => {
            // Start is at the entry edge (opposite of object's direction)
            match obj.direction {
                Direction::Right => EdgePoint::West,
                Direction::Down => EdgePoint::North,
                Direction::Left => EdgePoint::East,
                Direction::Up => EdgePoint::South,
            }
        }
        EdgePoint::End => {
            // End is at the exit edge (same as object's direction)
            match obj.direction {
                Direction::Right => EdgePoint::East,
                Direction::Down => EdgePoint::South,
                Direction::Left => EdgePoint::West,
                Direction::Up => EdgePoint::North,
            }
        }
        other => *other,
    };

    let result = match resolved_edge {
        EdgePoint::Center | EdgePoint::C => obj.center(),
        _ => obj.edge_point(resolved_edge.to_unit_vec()),
    };

    crate::log::debug!(
        ?edge,
        ?resolved_edge,
        result_x = result.x.raw(),
        result_y = result.y.raw(),
        "get_edge_point"
    );

    result
}

// cref: pik_get_color_from_name
pub fn eval_color(ctx: &RenderContext, rvalue: &RValue) -> String {
    match rvalue {
        // Color name like "Red", "blue", "lightgray"
        RValue::PlaceName(name) => name.parse::<crate::types::Color>().unwrap().to_string(),
        // Expression - could be a variable like $featurecolor or a hex literal
        RValue::Expr(expr) => match expr {
            Expr::Variable(name) => {
                // Look up variable in context
                if let Some(val) = ctx.variables.get(name) {
                    match val {
                        EvalValue::Color(c) => format!("#{:06x}", c),
                        // Scalar or Length could be a hex color value (e.g., 0xfedbce)
                        EvalValue::Scalar(s) => format!("#{:06x}", *s as u32),
                        EvalValue::Length(l) => format!("#{:06x}", l.raw() as u32),
                    }
                } else {
                    // Undefined variable - fall back to parsing as color name
                    name.parse::<crate::types::Color>().unwrap().to_string()
                }
            }
            Expr::Number(n) => {
                // Numeric literal like 0xfedbce
                format!("#{:06x}", *n as u32)
            }
            _ => "black".to_string(),
        },
    }
}

/// Helper to extract a length from an EvalValue, with fallback
/// Accepts both Length and Scalar values - scalars are interpreted as inches
pub fn get_length(ctx: &RenderContext, name: &str, default: f64) -> f64 {
    ctx.variables
        .get(name)
        .map(|v| match v {
            EvalValue::Length(l) => l.raw(),
            EvalValue::Scalar(s) => *s,
            EvalValue::Color(_) => default,
        })
        .unwrap_or(default)
}

/// Helper to extract a scalar from an EvalValue, with fallback
pub fn get_scalar(ctx: &RenderContext, name: &str, default: f64) -> f64 {
    ctx.variables
        .get(name)
        .map(|v| v.as_scalar())
        .unwrap_or(default)
}

/// Validate that a Value is finite (not NaN or infinity from overflow)
fn validate_value(v: Value) -> Result<Value, PikruError> {
    match v {
        Value::Len(l) if !l.is_finite() => Err(PikruError::Generic(
            "Arithmetic overflow (result is infinite or NaN)".to_string(),
        )),
        Value::Scalar(s) if !s.is_finite() => Err(PikruError::Generic(
            "Arithmetic overflow (result is infinite or NaN)".to_string(),
        )),
        _ => Ok(v),
    }
}
