///|
priv struct GradientSegment2D {
  start_offset : Double
  end_offset : Double
  sample_start_offset : Double
  sample_end_offset : Double
  color_start : @render.Color
  color_end : @render.Color
}

///|
pub fn tessellate_gradient_rect(
  command : GradientRectDrawCommand2D,
) -> Array[ColoredTriangle2D] {
  let triangles : Array[ColoredTriangle2D] = []
  let dx = command.end[X] - command.start[X]
  let dy = command.end[Y] - command.start[Y]
  let length_squared = dx * dx + dy * dy
  guard command.rect.size[X] > 0.0 &&
    command.rect.size[Y] > 0.0 &&
    length_squared > 0.0 &&
    command.stops.length() >= 2 else {
    return triangles
  }
  let rectangle = [
    @math.Vec2(0.0, 0.0),
    @math.Vec2(1.0, 0.0),
    @math.Vec2(1.0, 1.0),
    @math.Vec2(0.0, 1.0),
  ]
  let shape = if maximum_radius(command.radii) <= 0.0 {
    rectangle
  } else {
    rounded_perimeter(
      { position: @math.Vec2(0.0, 0.0), size: command.rect.size },
      command.radii,
      command.output_scale,
    ).map(fn(point) {
      @math.Vec2(
        point[X] / command.rect.size[X],
        point[Y] / command.rect.size[Y],
      )
    })
  }
  let mut minimum_projection = 1.0e300
  let mut maximum_projection = -1.0e300
  for point in shape {
    let projection = (
        (point[X] - command.start[X]) * dx + (point[Y] - command.start[Y]) * dy
      ) /
      length_squared
    minimum_projection = @cmp.minimum(minimum_projection, projection)
    maximum_projection = @cmp.maximum(maximum_projection, projection)
  }
  let segments : Array[GradientSegment2D] = []
  let first = command.stops[0]
  if minimum_projection < first.offset {
    segments.push({
      start_offset: minimum_projection,
      end_offset: @cmp.minimum(first.offset, maximum_projection),
      sample_start_offset: minimum_projection,
      sample_end_offset: minimum_projection,
      color_start: first.color,
      color_end: first.color,
    })
  }
  for index in 0..<(command.stops.length() - 1) {
    let current = command.stops[index]
    let next = command.stops[index + 1]
    if next.offset > current.offset &&
      next.offset > minimum_projection &&
      current.offset < maximum_projection {
      segments.push({
        start_offset: @cmp.maximum(current.offset, minimum_projection),
        end_offset: @cmp.minimum(next.offset, maximum_projection),
        sample_start_offset: current.offset,
        sample_end_offset: next.offset,
        color_start: current.color,
        color_end: next.color,
      })
    }
  }
  let last = command.stops[command.stops.length() - 1]
  if maximum_projection > last.offset {
    segments.push({
      start_offset: @cmp.maximum(last.offset, minimum_projection),
      end_offset: maximum_projection,
      sample_start_offset: maximum_projection,
      sample_end_offset: maximum_projection,
      color_start: last.color,
      color_end: last.color,
    })
  }
  for segment in segments {
    if segment.end_offset <= segment.start_offset {
      continue
    }
    let mut polygon = shape.copy()
    for boundary in [(segment.start_offset, true), (segment.end_offset, false)] {
      if polygon.is_empty() {
        break
      }
      let clipped : Array[@math.Vec2] = []
      let mut previous = polygon[polygon.length() - 1]
      let mut previous_projection = (
          (previous[X] - command.start[X]) * dx +
          (previous[Y] - command.start[Y]) * dy
        ) /
        length_squared
      let mut previous_inside = if boundary.1 {
        previous_projection >= boundary.0
      } else {
        previous_projection <= boundary.0
      }
      for current in polygon {
        let current_projection = (
            (current[X] - command.start[X]) * dx +
            (current[Y] - command.start[Y]) * dy
          ) /
          length_squared
        let current_inside = if boundary.1 {
          current_projection >= boundary.0
        } else {
          current_projection <= boundary.0
        }
        if current_inside != previous_inside {
          let amount = (boundary.0 - previous_projection) /
            (current_projection - previous_projection)
          clipped.push(
            @math.Vec2(
              previous[X] + (current[X] - previous[X]) * amount,
              previous[Y] + (current[Y] - previous[Y]) * amount,
            ),
          )
        }
        if current_inside {
          clipped.push(current)
        }
        previous = current
        previous_projection = current_projection
        previous_inside = current_inside
      }
      polygon = clipped
    }
    if polygon.length() < 3 {
      continue
    }
    let vertices : Array[ColoredVertex2D] = []
    for point in polygon {
      let projection = (
          (point[X] - command.start[X]) * dx +
          (point[Y] - command.start[Y]) * dy
        ) /
        length_squared
      let amount = if segment.sample_start_offset == segment.sample_end_offset {
        0.0
      } else {
        (projection - segment.sample_start_offset) /
        (segment.sample_end_offset - segment.sample_start_offset)
      }
      let (local_x, local_y) = command.transform.apply_to_point(
        point[X] * command.rect.size[X],
        point[Y] * command.rect.size[Y],
      )
      vertices.push({
        position: @math.Vec2(
          command.rect.position[X] + local_x,
          command.rect.position[Y] + local_y,
        ),
        color: {
          r: (segment.color_start.r.to_double() +
          (segment.color_end.r.to_double() - segment.color_start.r.to_double()) *
          amount)
          .round()
          .to_uint(),
          g: (segment.color_start.g.to_double() +
          (segment.color_end.g.to_double() - segment.color_start.g.to_double()) *
          amount)
          .round()
          .to_uint(),
          b: (segment.color_start.b.to_double() +
          (segment.color_end.b.to_double() - segment.color_start.b.to_double()) *
          amount)
          .round()
          .to_uint(),
          a: segment.color_start.a +
          (segment.color_end.a - segment.color_start.a) * amount,
        },
      })
    }
    for index in 1..<(vertices.length() - 1) {
      triangles.push({
        a: vertices[0],
        b: vertices[index],
        c: vertices[index + 1],
      })
    }
  }
  triangles
}