///|
fn Sketch::valid_point(self : Sketch, id : PointId) -> Bool {
  physical_equal(self.owner, id.owner) &&
  id.index >= 0 &&
  id.index < self.points.length()
}

///|
fn Sketch::valid_line(self : Sketch, id : LineId) -> Bool {
  physical_equal(self.owner, id.owner) &&
  id.index >= 0 &&
  id.index < self.lines.length()
}

///|
fn Sketch::valid_circle(self : Sketch, id : CircleId) -> Bool {
  physical_equal(self.owner, id.owner) &&
  id.index >= 0 &&
  id.index < self.circles.length()
}

///|
fn Sketch::constraint_slot(self : Sketch, id : ConstraintId) -> Int32? {
  if !physical_equal(self.owner, id.owner) || id.index < 0 {
    return None
  }
  for slot, stored in self.constraint_ids {
    if stored.index == id.index {
      return Some(slot)
    }
  }
  None
}

///|
fn bounded(x : Double) -> Bool {
  finite(x) && x.abs() <= 1.0e100
}

///|
fn Sketch::constraint_has_foreign_handle(self : Sketch, c : Constraint) -> Bool {
  let point = (p : PointId) => physical_equal(self.owner, p.owner)
  let line = (l : LineId) => physical_equal(self.owner, l.owner)
  let circle = (c : CircleId) => physical_equal(self.owner, c.owner)
  let owned = match c {
    Fix(p, _, _) => point(p)
    Coincident(a, b)
    | Distance(a, b, _)
    | DistanceX(a, b, _)
    | DistanceY(a, b, _) => point(a) && point(b)
    Symmetric(a, b, center) => point(a) && point(b) && point(center)
    Mirror(a, b, axis) => point(a) && point(b) && line(axis)
    Horizontal(l) | Vertical(l) => line(l)
    Parallel(a, b)
    | Perpendicular(a, b)
    | EqualLength(a, b)
    | LengthRatio(a, b, _)
    | Angle(a, b, _) => line(a) && line(b)
    Midpoint(p, l) | PointOnLine(p, l) | DistanceToLine(p, l, _) =>
      point(p) && line(l)
    Radius(c, _) => circle(c)
    EqualRadius(a, b)
    | Concentric(a, b)
    | ExternalTangent(a, b)
    | InternalTangent(a, b) => circle(a) && circle(b)
    PointOnCircle(p, c) => point(p) && circle(c)
    Tangent(l, c) => line(l) && circle(c)
  }
  !owned
}

///|
fn Sketch::validate(self : Sketch) -> Result[Unit, SketchError] {
  if self.points.length() * 2 + self.circles.length() > 256 ||
    self.constraints.length() > 512 ||
    self.lines.length() > 512 {
    return Err(ResourceLimit)
  }
  for i, p in self.points {
    if !bounded(p.x) || !bounded(p.y) {
      return Err(
        InvalidGeometry(
          "Point at index \{i}: coordinates must be finite and within +/-1e100",
        ),
      )
    }
  }
  for i, l in self.lines {
    if !physical_equal(self.owner, l.a.owner) ||
      !physical_equal(self.owner, l.b.owner) {
      return Err(ForeignHandle)
    }
    if !self.valid_point(l.a) || !self.valid_point(l.b) {
      return Err(
        InvalidGeometry("Line at index \{i}: references a missing point"),
      )
    }
    let a = self.points[l.a.index]
    let b = self.points[l.b.index]
    if a.x == b.x && a.y == b.y {
      return Err(DegenerateLine(self.line_id(i)))
    }
  }
  for i, c in self.circles {
    if !physical_equal(self.owner, c.center.owner) {
      return Err(ForeignHandle)
    }
    if !self.valid_point(c.center) {
      return Err(
        InvalidGeometry("Circle at index \{i}: references a missing center"),
      )
    }
    if !bounded(c.radius) || c.radius <= 0.0 {
      return Err(InvalidRadius(self.circle_id(i)))
    }
  }
  for i, c in self.constraints {
    if !self.enabled[i] {
      continue
    }
    if self.constraint_has_foreign_handle(c) {
      return Err(ForeignHandle)
    }
    let ok = match c {
      Fix(p, x, y) => self.valid_point(p) && bounded(x) && bounded(y)
      Coincident(a, b) => self.valid_point(a) && self.valid_point(b)
      Symmetric(a, b, center) =>
        self.valid_point(a) && self.valid_point(b) && self.valid_point(center)
      Mirror(a, b, axis) =>
        self.valid_point(a) && self.valid_point(b) && self.valid_line(axis)
      Horizontal(l) | Vertical(l) => self.valid_line(l)
      Distance(a, b, d) =>
        self.valid_point(a) && self.valid_point(b) && bounded(d) && d >= 0.0
      DistanceX(a, b, d) | DistanceY(a, b, d) =>
        self.valid_point(a) && self.valid_point(b) && bounded(d)
      Parallel(a, b) | Perpendicular(a, b) | EqualLength(a, b) =>
        self.valid_line(a) && self.valid_line(b)
      LengthRatio(a, b, ratio) =>
        self.valid_line(a) &&
        self.valid_line(b) &&
        finite(ratio) &&
        ratio > 0.0 &&
        ratio <= 1.0e12
      Angle(a, b, t) =>
        self.valid_line(a) &&
        self.valid_line(b) &&
        finite(t) &&
        t.abs() <= 2.0 * @math.PI
      Midpoint(p, l) | PointOnLine(p, l) =>
        self.valid_point(p) && self.valid_line(l)
      DistanceToLine(p, l, d) =>
        self.valid_point(p) && self.valid_line(l) && bounded(d)
      Radius(c, r) => self.valid_circle(c) && bounded(r) && r > 0.0
      EqualRadius(a, b)
      | Concentric(a, b)
      | ExternalTangent(a, b)
      | InternalTangent(a, b) => self.valid_circle(a) && self.valid_circle(b)
      PointOnCircle(p, c) => self.valid_point(p) && self.valid_circle(c)
      Tangent(l, c) => self.valid_line(l) && self.valid_circle(c)
    }
    if !ok {
      return Err(InvalidConstraint(self.constraint_ids[i]))
    }
  }
  Ok(())
}

///|
priv struct Frame {
  x : Double
  y : Double
  scale : Double
}

///|
/// Translation and a uniform length scale keep typical coordinates near unity.
fn Sketch::frame(self : Sketch) -> Frame {
  let origin = self.points.get(0).unwrap_or({ x: 0.0, y: 0.0, })
  let mut scale = 0.0
  for p in self.points {
    scale = dmax(scale, dmax((p.x - origin.x).abs(), (p.y - origin.y).abs()))
  }
  for c in self.circles {
    scale = dmax(scale, c.radius)
  }
  for i, c in self.constraints {
    if !self.enabled[i] {
      continue
    }
    match c {
      Fix(_, x, y) => {
        scale = dmax(scale, (x - origin.x).abs())
        scale = dmax(scale, (y - origin.y).abs())
      }
      Distance(_, _, d)
      | DistanceX(_, _, d)
      | DistanceY(_, _, d)
      | DistanceToLine(_, _, d)
      | Radius(_, d) => scale = dmax(scale, d.abs())
      _ => ()
    }
  }
  { x: origin.x, y: origin.y, scale: if scale > 0.0 { scale } else { 1.0 }, }
}

///|
fn Sketch::parameters(self : Sketch, frame : Frame) -> Array[Double] {
  let out = []
  for p in self.points {
    out.push((p.x - frame.x) / frame.scale)
    out.push((p.y - frame.y) / frame.scale)
  }
  for c in self.circles {
    out.push(c.radius / frame.scale)
  }
  out
}