///|
fn valid_point(point : HitPoint) -> Bool {
  finite(point.screen_x) &&
  finite(point.screen_y) &&
  finite(point.data_x) &&
  finite(point.data_y)
}

///|
fn valid_info(info : HitInfo) -> Bool {
  valid_point(info.point) &&
  finite(info.value) &&
  (match info.interval {
    Some((start, end)) => finite(start) && finite(end)
    None => true
  }) &&
  (match info.fraction {
    Some(value) => finite(value)
    None => true
  })
}

///|
fn valid_rect(rect : (Double, Double, Double, Double)) -> Bool {
  let (left, top, width, height) = rect
  finite(left) &&
  finite(top) &&
  finite(width) &&
  finite(height) &&
  width > 0.0 &&
  height > 0.0 &&
  finite(left + width) &&
  finite(top + height)
}

///|
fn rect_contains(
  rect : (Double, Double, Double, Double),
  x : Double,
  y : Double,
) -> Bool {
  let (left, top, width, height) = rect
  valid_rect(rect) &&
  x >= left &&
  y >= top &&
  x <= left + width &&
  y <= top + height
}

///|
// An overlay belongs to its panel; it must not mask another overlapping axes.
fn Drawing::excluded(
  self : Drawing,
  panel_index : Int32,
  x : Double,
  y : Double,
) -> Bool {
  for (panel, rectangle) in self.exclusions {
    if panel == panel_index && rect_contains(rectangle, x, y) {
      return true
    }
  }
  false
}

///|
fn valid_shape(shape : HitShape) -> Bool {
  match shape {
    Point => true
    Rectangle(left, top, width, height) =>
      valid_rect((left, top, width, height))
    Sector(cx, cy, radius, start, sweep) =>
      finite(cx) &&
      finite(cy) &&
      finite(radius) &&
      radius > 0.0 &&
      finite(start) &&
      finite(sweep) &&
      sweep > 0.0
  }
}

///|
fn degrees_normalized(angle : Double) -> Double {
  let wrapped = angle % 360.0
  if wrapped < 0.0 {
    wrapped + 360.0
  } else {
    wrapped
  }
}

///|
fn shape_contains(shape : HitShape, x : Double, y : Double) -> Bool {
  match shape {
    Point => false
    Rectangle(left, top, width, height) =>
      rect_contains((left, top, width, height), x, y)
    Sector(cx, cy, radius, start, sweep) => {
      let dx = x - cx
      let dy = cy - y
      if !finite(dx) || !finite(dy) || hit_distance(dx, dy) > radius {
        return false
      }
      // The center belongs to every positive sweep and no angle is needed.
      if (dx == 0.0 && dy == 0.0) || sweep >= 360.0 {
        return true
      }
      let angle = @math.atan2(dy, dx) * 180.0 / @math.PI
      degrees_normalized(degrees_normalized(angle) - degrees_normalized(start)) <=
      sweep
    }
  }
}

///|
fn Drawing::target_hit(
  self : Drawing,
  x : Double,
  y : Double,
  radius : Double,
  scale : Double,
  mode : HitMode,
) -> HitPoint? {
  if self.targets.is_empty() {
    return self.nearest(x, y, radius, scale)
  }
  if self.width <= 0 ||
    self.height <= 0 ||
    !finite(x) ||
    !finite(y) ||
    x < 0.0 ||
    y < 0.0 ||
    x > self.width.to_double() ||
    y > self.height.to_double() ||
    !finite(radius) ||
    radius < 0.0 ||
    !finite(scale) ||
    scale <= 0.0 {
    return None
  }
  // Areas have priority over point anchors; the last painted area wins.
  for index = self.targets.length() - 1; index >= 0; index = index - 1 {
    let target = self.targets[index]
    if valid_info(target.info) &&
      !self.excluded(target.info.point.panel_index, x, y) &&
      valid_shape(target.shape) &&
      rect_contains(target.bounds, x, y) &&
      shape_contains(target.shape, x, y) {
      return Some(target.info.point)
    }
  }
  let mut best : HitPoint? = None
  let mut best_distance = radius
  for target in self.targets {
    if self.excluded(target.info.point.panel_index, x, y) ||
      target.shape != Point ||
      (mode == XNearest && target.info.kind == Line) ||
      !valid_info(target.info) ||
      !rect_contains(target.bounds, x, y) ||
      !rect_contains(
        target.bounds,
        target.info.point.screen_x,
        target.info.point.screen_y,
      ) {
      continue
    }
    let point = target.info.point
    let dx = (point.screen_x - x) * scale
    let dy = (point.screen_y - y) * scale
    if !finite(dx) || !finite(dy) {
      continue
    }
    let distance = hit_distance(dx, dy)
    if distance <= best_distance && (best is None || distance < best_distance) {
      best = Some(point)
      best_distance = distance
    }
  }
  if best is Some(_) || mode == Nearest {
    return best
  }
  let mut best_dx = 0.0
  let mut best_dy = 0.0
  for target in self.targets {
    if self.excluded(target.info.point.panel_index, x, y) ||
      target.info.kind != Line ||
      target.shape != Point ||
      !valid_info(target.info) ||
      !rect_contains(target.bounds, x, y) ||
      !rect_contains(
        target.bounds,
        target.info.point.screen_x,
        target.info.point.screen_y,
      ) {
      continue
    }
    let point = target.info.point
    let dx = (point.screen_x - x).abs()
    let dy = (point.screen_y - y).abs()
    if !finite(dx) || !finite(dy) {
      continue
    }
    if best is None || dx < best_dx || (dx == best_dx && dy < best_dy) {
      best = Some(point)
      best_dx = dx
      best_dy = dy
    }
  }
  best
}