///|
/// MateKind classifies the mechanical interface a `MateConnector` describes.
///
/// The kind is CAD metadata. It never changes exported geometry: a connector is
/// a named frame, not a solid feature.
#derive(Debug, Eq)
pub(all) enum MateKind {
  /// Planar contact face. `axis` is the outward normal.
  PlaneFace
  /// Round hole. `axis` points into the bore.
  CylindricalBore
  /// Round shaft. `axis` runs along the shaft, toward the free end.
  CylindricalShaft
  /// Threaded interface. `axis` is the engagement axis.
  Threaded
  /// Extrusion or channel slot. `axis` is the insertion direction.
  ProfileSlot
  /// Reference frame with no contact claim.
  Datum
}

///|
pub extend MateKind with @debug.Debug::{to_repr}

///|
pub extend MateKind with Eq::{not_equal, equal}

///|
/// MateKind::to_string returns a stable lowercase token.
pub fn MateKind::to_string(self : MateKind) -> String {
  match self {
    PlaneFace => "plane-face"
    CylindricalBore => "cylindrical-bore"
    CylindricalShaft => "cylindrical-shaft"
    Threaded => "threaded"
    ProfileSlot => "profile-slot"
    Datum => "datum"
  }
}

///|
/// MateKind::requires_gender reports whether the kind describes geometric
/// engagement that should be declared male or female rather than neutral.
pub fn MateKind::requires_gender(self : MateKind) -> Bool {
  match self {
    CylindricalBore | CylindricalShaft | Threaded => true
    PlaneFace | ProfileSlot | Datum => false
  }
}

///|
/// MateGender describes geometric engagement, not strength or load capacity.
///
/// A male shaft engages a female bore. `Neutral` is correct for datum frames,
/// plane faces, and slots that do not carry an insertion gender.
#derive(Debug, Eq)
pub(all) enum MateGender {
  Neutral
  Male
  Female
}

///|
pub extend MateGender with @debug.Debug::{to_repr}

///|
pub extend MateGender with Eq::{not_equal, equal}

///|
/// MateGender::to_string returns a stable lowercase token.
pub fn MateGender::to_string(self : MateGender) -> String {
  match self {
    Neutral => "neutral"
    Male => "male"
    Female => "female"
  }
}

///|
/// MateError is the typed failure for connector lookup and validation.
#derive(Debug, Eq)
pub(all) enum MateError {
  /// No connector with this name exists on the component.
  UnknownConnector(String)
  /// A connector with this name is already registered on the component.
  DuplicateConnector(String)
  /// The connector is structurally invalid; the message names the reason.
  InvalidConnector(String)
}

///|
pub extend MateError with @debug.Debug::{to_repr}

///|
pub extend MateError with Eq::{not_equal, equal}

///|
/// MateError::message returns a human-readable message.
pub fn MateError::message(self : MateError) -> String {
  match self {
    UnknownConnector(name) => "unknown mate connector: \{name}"
    DuplicateConnector(name) => "duplicate mate connector: \{name}"
    InvalidConnector(reason) => "invalid mate connector: \{reason}"
  }
}

///|
/// MateError::to_string is an alias for `message`.
pub fn MateError::to_string(self : MateError) -> String {
  self.message()
}

///|
/// MateConnector is a named attachment frame (port) on one component.
///
/// `placement` is a full rigid frame in the component-local frame, in
/// millimetres and degrees. `axis` is an independent unit direction in the
/// component-local frame: the approach, insertion, or joint axis. It defaults to
/// +Z but is not required to be the placement's +Z; this matches the URDF
/// `MateConnector`, whose origin (`rpy`) and axis are independent.
///
/// Connector names are stable, unique within a component, and lower-case
/// kebab-case by convention (`shaft`, `bore`, `mount-front`, `bolt-hole-1`).
///
/// Connectors are metadata. Adding one never changes the component's exported
/// STEP geometry, bounding box, layer count, or volume.
#derive(Debug, Eq)
pub(all) struct MateConnector {
  name : String
  placement : Placement
  axis : @cad.Vec3
  kind : MateKind
  gender : MateGender
  diameter_mm : Double?
  depth_mm : Double?
  fit : @cad.Fit
  description : String
}

///|
pub extend MateConnector with @debug.Debug::{to_repr}

///|
pub extend MateConnector with Eq::{not_equal, equal}

///|
/// fit_token returns the stable token for a declared fit.
pub fn fit_token(fit : @cad.Fit) -> String {
  match fit {
    @cad.Fit::Nominal => "nominal"
    @cad.Fit::Close => "close"
    @cad.Fit::Free => "free"
    @cad.Fit::Press => "press"
  }
}

///|
/// normalize_dir returns a unit direction, or the zero vector when `v` has no
/// usable length.
pub fn normalize_dir(v : @cad.Vec3) -> @cad.Vec3 {
  let (x, y, z) = v
  let len = (x * x + y * y + z * z).sqrt()
  if len < 1.0e-12 {
    (0.0, 0.0, 0.0)
  } else {
    (x / len, y / len, z / len)
  }
}

///|
/// dir_dot returns the dot product of two directions.
pub fn dir_dot(a : @cad.Vec3, b : @cad.Vec3) -> Double {
  a.0 * b.0 + a.1 * b.1 + a.2 * b.2
}

///|
/// MateConnector::new builds a connector, normalizing a usable axis.
///
/// Structural problems (empty name, non-finite placement, zero axis, negative
/// diameter) are reported by `validate` and by `Component::with_connector`; the
/// constructor itself is total so it can be used to build test values.
pub fn MateConnector::new(
  name : String,
  placement~ : Placement = Placement::identity(),
  axis~ : @cad.Vec3 = (0.0, 0.0, 1.0),
  kind~ : MateKind = Datum,
  gender~ : MateGender = Neutral,
  diameter_mm? : Double,
  depth_mm? : Double,
  fit~ : @cad.Fit = @cad.Fit::Nominal,
  description~ : String = "",
) -> MateConnector {
  {
    name,
    placement,
    axis: normalize_dir(axis),
    kind,
    gender,
    diameter_mm,
    depth_mm,
    fit,
    description,
  }
}

///|
/// MateConnector::from_mm_deg builds a connector from millimetre translations
/// and degree rotations plus an optional component-local axis.
pub fn MateConnector::from_mm_deg(
  name : String,
  x~ : Double = 0.0,
  y~ : Double = 0.0,
  z~ : Double = 0.0,
  rx_deg~ : Double = 0.0,
  ry_deg~ : Double = 0.0,
  rz_deg~ : Double = 0.0,
  axis~ : @cad.Vec3 = (0.0, 0.0, 1.0),
  kind~ : MateKind = Datum,
  gender~ : MateGender = Neutral,
  diameter_mm? : Double,
  depth_mm? : Double,
  fit~ : @cad.Fit = @cad.Fit::Nominal,
  description~ : String = "",
) -> MateConnector {
  MateConnector::new(
    name,
    placement=Placement::at(x, y, z, rx=rx_deg, ry=ry_deg, rz=rz_deg),
    axis~,
    kind~,
    gender~,
    diameter_mm?,
    depth_mm?,
    fit~,
    description~,
  )
}

///|
/// MateConnector::from_m_rad builds a connector from URDF units: metres and
/// radians. The connector stores millimetres and degrees internally.
pub fn MateConnector::from_m_rad(
  name : String,
  xyz_m : @cad.Vec3,
  rpy_rad : @cad.Vec3,
  axis~ : @cad.Vec3 = (0.0, 0.0, 1.0),
  kind~ : MateKind = Datum,
  gender~ : MateGender = Neutral,
  diameter_mm? : Double,
  depth_mm? : Double,
  fit~ : @cad.Fit = @cad.Fit::Nominal,
  description~ : String = "",
) -> MateConnector {
  let (x, y, z) = xyz_m
  let (roll, pitch, yaw) = rpy_rad
  let to_deg = 180.0 / @math.PI
  MateConnector::new(
    name,
    placement=Placement::at(
      x * 1000.0,
      y * 1000.0,
      z * 1000.0,
      rx=roll * to_deg,
      ry=pitch * to_deg,
      rz=yaw * to_deg,
    ),
    axis~,
    kind~,
    gender~,
    diameter_mm?,
    depth_mm?,
    fit~,
    description~,
  )
}

///|
/// MateConnector::to_m_rad returns `(xyz_m, rpy_rad)` at the URDF boundary.
pub fn MateConnector::to_m_rad(self : MateConnector) -> (@cad.Vec3, @cad.Vec3) {
  let (x, y, z) = self.placement.trans
  let (rx, ry, rz) = self.placement.to_euler()
  let to_rad = @math.PI / 180.0
  ((x * 0.001, y * 0.001, z * 0.001), (rx * to_rad, ry * to_rad, rz * to_rad))
}

///|
/// MateConnector::transformed composes `parent` into the connector frame and
/// rotates its axis as a direction.
///
/// Use this before snapping connectors that belong to components already placed
/// in a common assembly frame.
pub fn MateConnector::transformed(
  self : MateConnector,
  parent : Placement,
) -> MateConnector {
  {
    ..self,
    placement: Placement::compose(parent, self.placement),
    axis: normalize_dir(parent.apply_dir(self.axis)),
  }
}

///|
/// MateConnector::has_finite_placement reports whether every part of the frame
/// is finite.
fn MateConnector::has_finite_placement(self : MateConnector) -> Bool {
  let (x, y, z) = self.placement.trans
  let m = self.placement.rot
  is_finite(x) &&
  is_finite(y) &&
  is_finite(z) &&
  is_finite(m.m00) &&
  is_finite(m.m01) &&
  is_finite(m.m02) &&
  is_finite(m.m10) &&
  is_finite(m.m11) &&
  is_finite(m.m12) &&
  is_finite(m.m20) &&
  is_finite(m.m21) &&
  is_finite(m.m22)
}

///|
/// is_mate_name_valid checks the documented connector naming convention without
/// requiring a full parser: non-empty and free of whitespace.
pub fn is_mate_name_valid(name : String) -> Bool {
  name != "" &&
  !name.contains(" ") &&
  !name.contains("\t") &&
  !name.contains("\n")
}

///|
/// MateConnector::validate checks the structural invariants of one connector.
///
/// It rejects empty or whitespace-bearing names, non-finite frames, zero or
/// non-unit axes, negative diameter/depth, and a kind/gender combination that
/// contradicts the interface (for example a `CylindricalBore` declared male).
pub fn MateConnector::validate(self : MateConnector) -> Result[Unit, MateError] {
  if !is_mate_name_valid(self.name) {
    return Err(
      InvalidConnector(
        "name must be non-empty and contain no whitespace, got \"\{self.name}\"",
      ),
    )
  }
  if !self.has_finite_placement() {
    return Err(InvalidConnector("placement must be finite in \{self.name}"))
  }
  let (ax, ay, az) = self.axis
  if !is_finite(ax) || !is_finite(ay) || !is_finite(az) {
    return Err(InvalidConnector("axis must be finite in \{self.name}"))
  }
  let len = (ax * ax + ay * ay + az * az).sqrt()
  if len < 1.0e-12 {
    return Err(InvalidConnector("axis must be non-zero in \{self.name}"))
  }
  if (len - 1.0).abs() > 1.0e-6 {
    return Err(InvalidConnector("axis must be unit length in \{self.name}"))
  }
  match self.diameter_mm {
    Some(d) =>
      if !is_finite(d) || d < 0.0 {
        return Err(
          InvalidConnector(
            "diameter must be finite and non-negative in \{self.name}",
          ),
        )
      }
    None => ()
  }
  match self.depth_mm {
    Some(d) =>
      if !is_finite(d) || d < 0.0 {
        return Err(
          InvalidConnector(
            "depth must be finite and non-negative in \{self.name}",
          ),
        )
      }
    None => ()
  }
  match (self.kind, self.gender) {
    (CylindricalBore, Male) =>
      return Err(
        InvalidConnector("a cylindrical bore cannot be male in \{self.name}"),
      )
    (CylindricalShaft, Female) =>
      return Err(
        InvalidConnector("a cylindrical shaft cannot be female in \{self.name}"),
      )
    (PlaneFace, Male) | (PlaneFace, Female) =>
      return Err(
        InvalidConnector(
          "a plane face is neutral, not male/female, in \{self.name}",
        ),
      )
    (ProfileSlot, Male) | (ProfileSlot, Female) =>
      return Err(
        InvalidConnector(
          "a profile slot is neutral, not male/female, in \{self.name}",
        ),
      )
    (Datum, Male) | (Datum, Female) =>
      return Err(
        InvalidConnector("a datum is neutral, not male/female, in \{self.name}"),
      )
    _ => ()
  }
  Ok(())
}

///|
/// MateConnector::documentation returns a one-line human-readable record.
pub fn MateConnector::documentation(self : MateConnector) -> String {
  let (x, y, z) = self.placement.trans
  let (rx, ry, rz) = self.placement.to_euler()
  let (ax, ay, az) = self.axis
  let gender = self.gender.to_string()
  let kind = self.kind.to_string()
  let mut text = "\{self.name}: \{kind} \{gender} at (\{x}, \{y}, \{z}) mm, rpy (\{rx}, \{ry}, \{rz}) deg, axis (\{ax}, \{ay}, \{az})"
  match self.diameter_mm {
    Some(d) => text = text + ", diameter \{d} mm"
    None => ()
  }
  match self.depth_mm {
    Some(d) => text = text + ", depth \{d} mm"
    None => ()
  }
  text = text + ", fit \{fit_token(self.fit)}"
  if self.description != "" {
    text = text + " — \{self.description}"
  }
  text
}