///|
fn bn(value : Double) -> String {
  @bpmn_layout.svg_number(value).unwrap()
}

///|
fn BpmnPaint::event_symbol(
  self : BpmnPaint,
  kind : @bpmn.EventKind,
  filled : Bool,
  stroke : String,
) -> Array[@writer.Node] {
  let fill = if filled { stroke } else { self.paint.color("bg") }
  match kind {
    Message =>
      [
        self.rect("1", "4", "22", "16", stroke, fill~),
        self.path(
          "M1 4 L12 13 L23 4",
          if filled {
            self.paint.color("bg")
          } else {
            stroke
          },
        ),
      ]
    Timer => {
      let parts = [
        self.circle("12", "12", "10.5", stroke),
        self.path("M12 5 V12 L17 15", stroke),
      ]
      for
        (x1, y1, x2, y2) in [
          (12, 2, 12, 4),
          (12, 20, 12, 22),
          (2, 12, 4, 12),
          (20, 12, 22, 12),
          (5, 5, 6, 6),
          (18, 18, 19, 19),
          (5, 19, 6, 18),
          (18, 6, 19, 5),
        ] {
        parts.push(self.path("M\{x1} \{y1} L\{x2} \{y2}", stroke, width="1"))
      }
      parts
    }
    Conditional =>
      [
        self.rect("4", "1", "16", "22", stroke, fill~),
        self.path(
          "M7 6 H17 M7 10 H17 M7 14 H17 M7 18 H17",
          if filled {
            self.paint.color("bg")
          } else {
            stroke
          },
          width="1",
        ),
      ]
    Link => [self.path("M1 8 H13 V2 L23 12 L13 22 V16 H1 Z", stroke, fill~)]
    Signal => [self.path("M12 1 L23 22 H1 Z", stroke, fill~)]
    Error => [self.path("M15 1 L4 13 H11 L8 23 L21 9 H14 Z", stroke, fill~)]
    Escalation => [self.path("M12 1 L23 22 L12 16 L1 22 Z", stroke, fill~)]
    Cancel =>
      [
        self.path(
          "M4 1 L12 9 L20 1 L23 4 L15 12 L23 20 L20 23 L12 15 L4 23 L1 20 L9 12 L1 4 Z",
          stroke,
          fill~,
        ),
      ]
    Compensation =>
      [self.path("M12 2 L1 12 L12 22 Z M23 2 L12 12 L23 22 Z", stroke, fill~)]
    Terminate => [self.circle("12", "12", "10", stroke, fill=stroke)]
  }
}

///|
fn BpmnPaint::event_contents(
  self : BpmnPaint,
  markers : @bpmn.Markers,
  filled : Bool,
  stroke : String,
) -> Array[@writer.Node] {
  if markers.events.length() > 1 && markers.parallel_multiple {
    [self.path("M12 2 V22 M2 12 H22", stroke, width="3")]
  } else if markers.events.length() > 1 {
    [
      self.path(
        "M12 1 L23 9 L19 22 H5 L1 9 Z",
        stroke,
        fill=if filled { stroke } else { self.paint.color("bg") },
      ),
    ]
  } else if markers.events.length() == 1 {
    self.event_symbol(markers.events[0].kind, filled, stroke)
  } else {
    []
  }
}

///|
fn multiple_participants(value : String) -> Bool {
  let digits = value.trim().trim_start(chars="+").trim_start(chars="0")
  digits.length() > 0 &&
  digits.iter().all(c => c >= '0' && c <= '9') &&
  (digits.length() > 1 || digits != "1")
}

///|
fn BpmnPaint::task_symbol(
  self : BpmnPaint,
  kind : @bpmn.NodeKind,
  stroke : String,
) -> Array[@writer.Node] {
  match kind {
    UserTask =>
      [
        self.circle("12", "5", "4", stroke),
        self.path(
          "M4 23 V18 Q4 12 12 12 Q20 12 20 18 V23 M8 16 V23 M16 16 V23", stroke,
        ),
      ]
    ServiceTask =>
      [
        self.path(
          "M9 1 H15 L16 5 L19 7 L23 7 L24 13 L20 15 L19 18 L19 22 L13 24 L10 20 L7 19 L3 20 L1 14 L5 11 L5 8 L4 4 Z",
          stroke,
          fill=self.paint.color("bg"),
        ),
        self.circle("12", "12", "4", stroke),
      ]
    SendTask => self.event_symbol(Message, true, stroke)
    ReceiveTask => self.event_symbol(Message, false, stroke)
    ManualTask =>
      [
        self.path(
          "M2 15 L5 12 L8 14 V3 Q8 0 11 3 V11 V2 Q13 -1 15 2 V11 V4 Q17 1 19 4 V12 V7 Q22 5 23 8 V16 Q22 22 17 24 H10 Q7 22 2 17 Z",
          stroke,
          fill=self.paint.color("bg"),
        ),
      ]
    BusinessRuleTask =>
      [
        self.rect("1", "2", "22", "20", stroke),
        self.path("M1 8 H23 M8 8 V22 M1 14 H23", stroke),
        self.rect("1", "2", "22", "6", stroke, fill=stroke),
      ]
    ScriptTask =>
      [
        self.path(
          "M5 1 H21 Q17 4 20 7 H7 V19 Q6 23 1 23 Q5 20 3 17 H17 Q19 20 16 23 H1 M8 10 H16 M8 14 H16",
          stroke,
        ),
      ]
    _ => []
  }
}

///|
fn BpmnPaint::loop_symbol(
  self : BpmnPaint,
  kind : @bpmn.LoopKind,
  stroke : String,
) -> Array[@writer.Node] {
  match kind {
    None => []
    Standard =>
      [self.path("M18 6 A9 9 0 1 0 21 16 M13 5 H20 V12", stroke, width="2")]
    Sequential => [self.path("M3 5 H21 M3 12 H21 M3 19 H21", stroke, width="2")]
    Parallel => [self.path("M5 3 V21 M12 3 V21 M19 3 V21", stroke, width="2")]
  }
}

///|
fn BpmnPaint::event_border(
  self : BpmnPaint,
  node : @bpmn.Node,
  stroke : String,
) -> Array[@writer.Node] {
  let dash = if !node.markers.interrupting { Some("4 3") } else { None }
  let parts = [
    self.circle(
      "50",
      "50",
      "50",
      stroke,
      fill=self.paint.color("bg"),
      width=if node.kind == EndEvent { "4" } else { "1.5" },
      dash?,
    ),
  ]
  if node.kind == IntermediateCatchEvent ||
    node.kind == IntermediateThrowEvent ||
    node.kind == BoundaryEvent {
    parts.push(self.circle("50", "50", "40", stroke, dash?))
  }
  parts
}

///|
fn BpmnPaint::outline(
  self : BpmnPaint,
  shape : @bpmn_layout.Shape,
) -> @writer.Node {
  let node = shape.node
  let stroke = self.paint.color(
    "\{@bpmn_layout.family(node.kind).name()}-stroke",
  )
  let fill = self.paint.color("\{@bpmn_layout.family(node.kind).name()}-fill")
  let mut normalize = "scale(0.01 0.0125)"
  let parts = match node.kind {
    StartEvent
    | EndEvent
    | IntermediateCatchEvent
    | IntermediateThrowEvent
    | BoundaryEvent => {
      normalize = "scale(0.01 0.01)"
      self.event_border(node, stroke)
    }
    ExclusiveGateway
    | InclusiveGateway
    | ParallelGateway
    | ComplexGateway
    | EventBasedGateway => {
      normalize = "scale(0.01 0.01)"
      [
        self.path(
          "M50 0 L100 50 L50 100 L0 50 Z",
          stroke,
          fill=self.paint.color("bg"),
        ),
      ]
    }
    Participant | Lane =>
      [self.rect("0", "0", "100", "80", stroke, fill=self.paint.color("bg"))]
    Group =>
      [self.rect("0", "0", "100", "80", stroke, radius="10", dash="4 3 1 3")]
    TextAnnotation => [self.path("M12 0 H0 V80 H12", stroke)]
    DataObject | DataObjectReference | DataInput | DataOutput =>
      [
        self.path(
          "M0 0 H75 L100 20 V80 H0 Z M75 0 V20 H100",
          stroke,
          fill=self.paint.color("bg"),
        ),
      ]
    DataStore | DataStoreReference =>
      [
        self.path(
          "M0 12 A50 12 0 0 1 100 12 V68 A50 12 0 0 1 0 68 Z M0 12 A50 12 0 0 0 100 12 M0 18 A50 12 0 0 0 100 18 M0 24 A50 12 0 0 0 100 24",
          stroke,
          fill=self.paint.color("bg"),
        ),
      ]
    Message =>
      [
        self.rect("0", "0", "100", "80", stroke, fill=self.paint.color("bg")),
        self.path("M0 0 L50 45 L100 0", stroke),
      ]
    Transaction =>
      [
        self.rect("0", "0", "100", "80", stroke, fill~, radius="10"),
        self.rect("4", "4", "92", "72", stroke, radius="7"),
      ]
    _ =>
      [
        self.rect(
          "0",
          "0",
          "100",
          "80",
          stroke,
          fill~,
          radius="10",
          weight=if node.kind == CallActivity { "4" } else { "1.5" },
          dash?=if node.markers.event_subprocess { Some("4 3") } else { None },
        ),
      ]
  }
  if @bpmn_layout.activity(node.kind) {
    parts.insert(
      0,
      self.rect(
        "0",
        "0",
        "100",
        "80",
        "none",
        fill=self.paint.color("bg"),
        radius="10",
      ),
    )
  }
  // Stroke widths are user units even when the normalized symbol is resized.
  let normalized = parts.map(part => {
    match part {
      Element(name, attrs, children) =>
        self.element(
          name,
          attrs=attrs
            .iter()
            .concat([("vector-effect", "non-scaling-stroke")].iter())
            .to_array(),
          children~,
        )
      _ => part
    }
  })
  let r = shape.bounds
  self.element(
    "g",
    attrs=[
      (
        "transform",
        "translate(\{r.x.token} \{r.y.token}) scale(\{r.width.token} \{r.height.token})",
      ),
      ("aria-hidden", "true"),
    ],
    children=[
      self.element("g", attrs=[("transform", normalize)], children=normalized),
    ],
  )
}

///|
fn BpmnPaint::markers(
  self : BpmnPaint,
  shape : @bpmn_layout.Shape,
  starts : ArrayView[@bpmn.Node],
) -> Array[@writer.Node] {
  let node = shape.node
  let r = shape.box
  let stroke = self.paint.color(
    "\{@bpmn_layout.family(node.kind).name()}-stroke",
  )
  let parts = []
  fn place(
    x : Double,
    y : Double,
    scale : Double,
    icons : ArrayView[@writer.Node],
  ) -> Unit {
    parts.push(
      self.element(
        "g",
        attrs=[
          ("transform", "translate(\{bn(x)} \{bn(y)}) scale(\{bn(scale)})"),
        ],
        children=icons,
      ),
    )
  }
  if @bpmn_layout.activity(node.kind) {
    let kind = if node.kind == CallActivity {
      node.markers.called_kind.unwrap_or(Task)
    } else {
      node.kind
    }
    place(10.0, 8.0, 0.8, self.task_symbol(kind, stroke))
    if kind == ReceiveTask && node.markers.instantiate {
      parts.push(self.circle("20", "18", "13", stroke))
    }
    let icons : Array[Array[@writer.Node]] = []
    if (
        node.kind == SubProcess ||
        node.kind == AdHocSubProcess ||
        node.kind == Transaction ||
        (
          node.kind == CallActivity &&
          node.markers.called_kind == Some(SubProcess)
        )
      ) &&
      !node.expanded.unwrap_or(false) {
      icons.push([
        self.rect("2", "2", "20", "20", stroke, fill=self.paint.color("bg")),
        self.path("M6 12 H18 M12 6 V18", stroke),
      ])
    }
    if node.markers.compensation {
      icons.push(self.event_symbol(Compensation, false, stroke))
    }
    if node.markers.loop_kind != None {
      icons.push(self.loop_symbol(node.markers.loop_kind, stroke))
    }
    if node.kind == AdHocSubProcess {
      icons.push([self.path("M2 14 Q7 4 12 14 Q17 24 22 14", stroke, width="2")])
    }
    for i, icon in icons {
      place(
        r.width / 2.0 - icons.length().to_double() * 10.0 + i.to_double() * 20.0,
        r.height - 19.0,
        0.65,
        icon,
      )
    }
    if node.markers.event_subprocess && !node.expanded.unwrap_or(false) {
      if starts.length() == 1 {
        let start = starts[0]
        let badge = @bpmn.Node::new("badge", StartEvent, markers=start.markers)
        let border = self.event_border(badge, stroke)
        parts.push(
          self.element(
            "g",
            attrs=[("transform", "translate(5 5) scale(0.24)")],
            children=border,
          ),
        )
        place(
          10.0,
          10.0,
          0.55,
          self.event_contents(start.markers, false, stroke),
        )
      }
    }
  }
  if node.kind == StartEvent ||
    node.kind == EndEvent ||
    node.kind == IntermediateCatchEvent ||
    node.kind == IntermediateThrowEvent ||
    node.kind == BoundaryEvent {
    let filled = node.kind == EndEvent || node.kind == IntermediateThrowEvent
    let size = Double::min(r.width, r.height) * 0.55
    let symbols = self.event_contents(node.markers, filled, stroke)
    place((r.width - size) / 2.0, (r.height - size) / 2.0, size / 24.0, symbols)
  }
  if node.kind == ExclusiveGateway ||
    node.kind == InclusiveGateway ||
    node.kind == ParallelGateway ||
    node.kind == ComplexGateway ||
    node.kind == EventBasedGateway {
    let symbols = match node.kind {
      ExclusiveGateway =>
        if node.geometry.bind(g => g.marker_visible).unwrap_or(true) {
          [self.path("M3 3 L21 21 M21 3 L3 21", stroke, width="3")]
        } else {
          []
        }
      InclusiveGateway => [self.circle("12", "12", "10", stroke, width="2.5")]
      ParallelGateway => [self.path("M12 1 V23 M1 12 H23", stroke, width="3")]
      ComplexGateway =>
        [
          self.path(
            "M12 1 V23 M1 12 H23 M4 4 L20 20 M4 20 L20 4",
            stroke,
            width="2.5",
          ),
        ]
      _ => {
        let parallel = bpmn_detail(node, "eventGatewayType") == Some("Parallel")
        let symbols = [self.circle("12", "12", "11", stroke)]
        if !node.markers.instantiate {
          symbols.push(self.circle("12", "12", "8.5", stroke, width="1"))
        }
        if parallel {
          symbols.push(self.path("M12 5 V19 M5 12 H19", stroke, width="2"))
        } else {
          symbols.push(self.path("M12 5 L19 10 L16 18 H8 L5 10 Z", stroke))
        }
        symbols
      }
    }
    let size = Double::min(r.width, r.height) * 0.55
    place((r.width - size) / 2.0, (r.height - size) / 2.0, size / 24.0, symbols)
  }
  if node.kind == Participant || node.kind == Lane {
    if node.geometry.bind(g => g.horizontal).unwrap_or(true) {
      parts.push(self.path("M30 0 V\{bn(r.height)}", stroke))
    } else {
      parts.push(self.path("M0 30 H\{bn(r.width)}", stroke))
    }
    if node.markers.details
      .iter()
      .any(p => {
        p.0 == "participantMultiplicity/@maximum" && multiple_participants(p.1)
      }) {
      place(
        r.width / 2.0 - 8.0,
        r.height - 18.0,
        0.65,
        self.loop_symbol(Parallel, stroke),
      )
    }
  }
  if node.kind == DataInput || node.kind == DataOutput {
    place(6.0, 6.0, 0.6, [
      self.path(
        "M2 8 H12 V2 L23 12 L12 22 V16 H2 Z",
        stroke,
        fill=if node.kind == DataOutput {
          stroke
        } else {
          self.paint.color("bg")
        },
      ),
    ])
  }
  if node.markers.collection {
    place(
      r.width / 2.0 - 8.0,
      r.height - 17.0,
      0.6,
      self.loop_symbol(Parallel, stroke),
    )
  }
  [
    self.element(
      "g",
      attrs=[
        (
          "transform",
          "translate(\{shape.bounds.x.token} \{shape.bounds.y.token})",
        ),
        ("class", "bpmn-markers"),
      ],
      children=parts,
    ),
  ]
}