///|
fn polygon(box : Rect, values : ArrayView[(Double, Double)]) -> Array[Point] {
  values.map(p => Point::{
    x: box.x + p.0 * box.width,
    y: box.y + p.1 * box.height,
  })
}

///|
fn arc(
  points : Array[Point],
  x : Double,
  y : Double,
  rx : Double,
  ry : Double,
  start : Double,
  end : Double,
  steps : Int32,
) -> Unit {
  for i in 0..<=steps {
    let t = start + (end - start) * i.to_double() / steps.to_double()
    points.push({ x: x + rx * @math.cos(t), y: y + ry * @math.sin(t), })
  }
}

///|
/// Each painted outer boundary and its edge attachment use this same vector.
/// Curves use fixed sampled segments, so path bytes and clipping agree on all targets.
pub fn outline(shape : String, box : Rect) -> ReadOnlyArray[Point] {
  let r = box
  let x = r.x
  let y = r.y
  let w = r.width
  let h = r.height
  let circle = [
    "circle", "doublecircle", "sm-circ", "fr-circ", "f-circ", "cross-circ", "anchor",
  ].contains(shape)
  let points : Array[Point] = []
  if circle || shape == "ellipse" {
    arc(
      points,
      x + w / 2.0,
      y + h / 2.0,
      w / 2.0,
      h / 2.0,
      0.0,
      2.0 * @math.PI,
      64,
    )
    return ReadOnlyArray::from_array(points)
  }
  if shape == "stadium" ||
    shape == "rounded" ||
    shape == "state" ||
    shape == "composite" {
    let rx = if shape == "stadium" {
      Double::min(h / 2.0, w / 2.0)
    } else {
      Double::min(12.0, w / 4.0)
    }
    let ry = Double::min(rx, h / 2.0)
    arc(points, x + w - rx, y + ry, rx, ry, -@math.PI / 2.0, 0.0, 8)
    arc(points, x + w - rx, y + h - ry, rx, ry, 0.0, @math.PI / 2.0, 8)
    arc(points, x + rx, y + h - ry, rx, ry, @math.PI / 2.0, @math.PI, 8)
    arc(points, x + rx, y + ry, rx, ry, @math.PI, 1.5 * @math.PI, 8)
    return ReadOnlyArray::from_array(points)
  }
  if shape == "cylinder" || shape == "lin-cyl" {
    arc(
      points,
      x + w / 2.0,
      y + h * 0.15,
      w / 2.0,
      h * 0.15,
      @math.PI,
      2.0 * @math.PI,
      24,
    )
    arc(points, x + w / 2.0, y + h * 0.85, w / 2.0, h * 0.15, 0.0, @math.PI, 24)
    return ReadOnlyArray::from_array(points)
  }
  if shape == "h-cyl" {
    arc(
      points,
      x + w * 0.12,
      y + h / 2.0,
      w * 0.12,
      h / 2.0,
      @math.PI / 2.0,
      1.5 * @math.PI,
      24,
    )
    arc(
      points,
      x + w * 0.88,
      y + h / 2.0,
      w * 0.12,
      h / 2.0,
      -@math.PI / 2.0,
      @math.PI / 2.0,
      24,
    )
    return ReadOnlyArray::from_array(points)
  }
  if shape == "delay" {
    points.push({ x, y, })
    arc(
      points,
      x + w - h / 2.0,
      y + h / 2.0,
      h / 2.0,
      h / 2.0,
      -@math.PI / 2.0,
      @math.PI / 2.0,
      24,
    )
    points.push({ x, y: y + h, })
    return ReadOnlyArray::from_array(points)
  }
  if ["doc", "docs", "lin-doc", "tag-doc"].contains(shape) {
    points.push({ x, y, })
    points.push({ x: x + w, y, })
    points.push({ x: x + w, y: y + h * 0.88, })
    for i in 0..<=32 {
      let t = i.to_double() / 32.0
      points.push({
        x: x + w * (1.0 - t),
        y: y + h * (0.88 - 0.12 * @math.sin(t * 2.0 * @math.PI)),
      })
    }
    return ReadOnlyArray::from_array(points)
  }
  if shape == "flag" {
    for i in 0..<=32 {
      let t = i.to_double() / 32.0
      points.push({
        x: x + w * t,
        y: y + h * (0.12 + 0.1 * @math.sin(t * 2.0 * @math.PI)),
      })
    }
    for i in 0..<=32 {
      let t = i.to_double() / 32.0
      points.push({
        x: x + w * (1.0 - t),
        y: y + h * (0.88 - 0.1 * @math.sin(t * 2.0 * @math.PI)),
      })
    }
    return ReadOnlyArray::from_array(points)
  }
  if shape == "bow-rect" {
    points.push({ x, y, })
    points.push({ x: x + w, y, })
    for i in 0..<=24 {
      let t = i.to_double() / 24.0
      points.push({
        x: x + w - w * 0.12 * @math.sin(t * @math.PI),
        y: y + h * t,
      })
    }
    points.push({ x, y: y + h, })
    for i in 0..<=24 {
      let t = i.to_double() / 24.0
      points.push({
        x: x + w * 0.12 * @math.sin(t * @math.PI),
        y: y + h * (1.0 - t),
      })
    }
    return ReadOnlyArray::from_array(points)
  }
  if shape == "cloud" {
    for i in 0..<96 {
      let t = 2.0 * @math.PI * i.to_double() / 96.0
      let ripple = 0.87 + 0.13 * @math.cos(8.0 * t)
      points.push({
        x: x + w / 2.0 + w / 2.0 * ripple * @math.cos(t),
        y: y + h / 2.0 + h / 2.0 * ripple * @math.sin(t),
      })
    }
    return ReadOnlyArray::from_array(points)
  }
  if shape == "bang" {
    for i in 0..<24 {
      let t = 2.0 * @math.PI * i.to_double() / 24.0
      let scale = if i % 2 == 0 { 1.0 } else { 0.72 }
      points.push({
        x: x + w / 2.0 + w / 2.0 * scale * @math.cos(t),
        y: y + h / 2.0 + h / 2.0 * scale * @math.sin(t),
      })
    }
    return ReadOnlyArray::from_array(points)
  }
  if shape == "person" {
    return outline("rounded", {
      x,
      y: y + h * 0.34,
      width: w,
      height: h * 0.66,
    })
  }
  if shape == "curv-trap" {
    points.push({ x: x + w * 0.15, y, })
    points.push({ x: x + w * 0.85, y, })
    arc(
      points,
      x + w * 0.85,
      y + h / 2.0,
      w * 0.15,
      h / 2.0,
      -@math.PI / 2.0,
      @math.PI / 2.0,
      16,
    )
    points.push({ x: x + w * 0.15, y: y + h, })
    points.push({ x, y: y + h / 2.0, })
    return ReadOnlyArray::from_array(points)
  }
  if shape == "braces" {
    let both = outline("brace", box).iter().to_array()
    both.append(outline("brace-r", box))
    return ReadOnlyArray::from_array(both)
  }
  let values : Array[(Double, Double)] = match shape {
    "choice" | "diamond" => [(0.5, 0.0), (1.0, 0.5), (0.5, 1.0), (0.0, 0.5)]
    "hexagon" =>
      [(0.2, 0.0), (0.8, 0.0), (1.0, 0.5), (0.8, 1.0), (0.2, 1.0), (0.0, 0.5)]
    "lean-r" => [(0.15, 0.0), (1.0, 0.0), (0.85, 1.0), (0.0, 1.0)]
    "lean-l" => [(0.0, 0.0), (0.85, 0.0), (1.0, 1.0), (0.15, 1.0)]
    "trap-b" => [(0.2, 0.0), (0.8, 0.0), (1.0, 1.0), (0.0, 1.0)]
    "trap-t" | "bucket" => [(0.0, 0.0), (1.0, 0.0), (0.8, 1.0), (0.2, 1.0)]
    "folder" =>
      [
        (0.0, 0.0),
        (0.35, 0.0),
        (0.45, 0.15),
        (1.0, 0.15),
        (1.0, 1.0),
        (0.0, 1.0),
      ]
    "note" => [(0.0, 0.0), (0.85, 0.0), (1.0, 0.2), (1.0, 1.0), (0.0, 1.0)]
    "brace" =>
      [
        (0.18, 0.0),
        (0.08, 0.05),
        (0.07, 0.38),
        (0.0, 0.5),
        (0.07, 0.62),
        (0.08, 0.95),
        (0.18, 1.0),
        (0.18, 0.97),
        (0.1, 0.93),
        (0.1, 0.6),
        (0.03, 0.5),
        (0.1, 0.4),
        (0.1, 0.07),
        (0.18, 0.03),
      ]
    "brace-r" =>
      [
        (0.82, 0.0),
        (0.92, 0.05),
        (0.93, 0.38),
        (1.0, 0.5),
        (0.93, 0.62),
        (0.92, 0.95),
        (0.82, 1.0),
        (0.82, 0.97),
        (0.9, 0.93),
        (0.9, 0.6),
        (0.97, 0.5),
        (0.9, 0.4),
        (0.9, 0.07),
        (0.82, 0.03),
      ]
    "braces" =>
      [
        (0.0, 0.5),
        (0.06, 0.4),
        (0.06, 0.06),
        (0.16, 0.0),
        (0.84, 0.0),
        (0.94, 0.06),
        (0.94, 0.4),
        (1.0, 0.5),
        (0.94, 0.6),
        (0.94, 0.94),
        (0.84, 1.0),
        (0.16, 1.0),
        (0.06, 0.94),
        (0.06, 0.6),
      ]
    "odd" => [(0.2, 0.0), (1.0, 0.0), (1.0, 1.0), (0.2, 1.0), (0.0, 0.5)]
    "notch-rect" =>
      [(0.16, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.2)]
    "hourglass" =>
      [(0.0, 0.0), (1.0, 0.0), (0.55, 0.5), (1.0, 1.0), (0.0, 1.0), (0.45, 0.5)]
    "sl-rect" => [(0.0, 0.2), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]
    "flip-tri" => [(0.0, 0.0), (1.0, 0.0), (0.5, 1.0)]
    "tri" => [(0.5, 0.0), (1.0, 1.0), (0.0, 1.0)]
    "notch-pent" =>
      [(0.1, 0.0), (0.9, 0.0), (1.0, 0.2), (1.0, 1.0), (0.0, 1.0), (0.0, 0.2)]
    "bolt" =>
      [
        (0.55, 0.0),
        (0.0, 0.6),
        (0.4, 0.6),
        (0.3, 1.0),
        (1.0, 0.35),
        (0.6, 0.35),
      ]
    _ => [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]
  }
  ReadOnlyArray::from_array(polygon(box, values))
}

///|
fn segment_distance(p : Point, a : Point, b : Point) -> Double {
  let dx = b.x - a.x
  let dy = b.y - a.y
  let length = dx * dx + dy * dy
  let t = if length == 0.0 {
    0.0
  } else {
    Double::max(
      0.0,
      Double::min(1.0, ((p.x - a.x) * dx + (p.y - a.y) * dy) / length),
    )
  }
  let x = p.x - a.x - t * dx
  let y = p.y - a.y - t * dy
  (x * x + y * y).sqrt()
}

///|
fn on_shape(p : Point, shape : Shape) -> Bool {
  if shape.shape == "datastore" {
    let r = shape.outline_box
    return segment_distance(p, { x: r.x, y: r.y, }, {
        x: r.x + r.width,
        y: r.y,
      }) <=
      0.1 ||
      segment_distance(p, { x: r.x, y: r.y + r.height, }, {
        x: r.x + r.width,
        y: r.y + r.height,
      }) <=
      0.1
  }

  if shape.shape == "braces" {
    return on_shape(p, { ..shape, shape: "brace", }) ||
      on_shape(p, { ..shape, shape: "brace-r", })
  }

  let boundary = outline(shape.shape, shape.outline_box)
  for i, a in boundary {
    if segment_distance(p, a, boundary[(i + 1) % boundary.length()]) <= 0.1 {
      return true
    }
  }
  false
}

///|
fn port(shape : Shape, side : Int32, fraction : Double) -> Point {
  let box = shape.outline_box
  if shape.shape == "datastore" {
    match side {
      0 => return { x: box.x + box.width * (0.55 + 0.4 * fraction), y: box.y, }
      2 =>
        return {
          x: box.x + box.width * (0.05 + 0.4 * fraction),
          y: box.y + box.height,
        }
      _ => ()
    }
  }
  let boundary = outline(shape.shape, box)
  let horizontal = side == 0 || side == 2
  let coordinate = if horizontal {
    box.y + box.height * fraction
  } else {
    box.x + box.width * fraction
  }
  let hits : Array[Double] = []
  for i, a in boundary {
    let b = boundary[(i + 1) % boundary.length()]
    let aa = if horizontal { a.y } else { a.x }
    let bb = if horizontal { b.y } else { b.x }
    if aa == bb {
      if (coordinate - aa).abs() < 0.0001 {
        hits.push(if horizontal { a.x } else { a.y })
        hits.push(if horizontal { b.x } else { b.y })
      }
      continue
    }
    let t = (coordinate - aa) / (bb - aa)
    if t >= 0.0 && t <= 1.0 {
      hits.push(
        if horizontal {
          a.x + (b.x - a.x) * t
        } else {
          a.y + (b.y - a.y) * t
        },
      )
    }
  }
  hits.sort_by((a, b) => a.compare(b))
  let v = if side == 0 || side == 1 { hits.last() } else { hits.get(0) }
  if v == None {
    let desired : Point = match side {
      0 => { x: box.x + box.width, y: coordinate, }
      1 => { x: coordinate, y: box.y + box.height, }
      2 => { x: box.x, y: coordinate, }
      _ => { x: coordinate, y: box.y, }
    }
    let mut closest = boundary[0]
    let mut distance = 1000000000000000000.0
    for p in boundary {
      let d = (p.x - desired.x) * (p.x - desired.x) +
        (p.y - desired.y) * (p.y - desired.y)
      if d < distance {
        closest = p
        distance = d
      }
    }
    return closest
  }
  let v = v.unwrap()
  if horizontal {
    { x: v, y: coordinate, }
  } else {
    { x: coordinate, y: v, }
  }
}