///|
fn gradient_sample_point(
  local_x : Double,
  local_y : Double,
  bbox : BoundingBox,
  units : GradientUnits,
  gradient_transform : Transform,
) -> (Double, Double) {
  let (x, y) = match units {
    ObjectBoundingBox => {
      let width = bbox.width()
      let height = bbox.height()
      (
        if width > 0.0 {
          (local_x - bbox.min_x) / width
        } else {
          0.0
        },
        if height > 0.0 {
          (local_y - bbox.min_y) / height
        } else {
          0.0
        },
      )
    }
    UserSpaceOnUse => (local_x, local_y)
  }
  gradient_transform.inverse().apply(x, y)
}

///|
fn apply_spread_inline(t : Double, spread : SpreadMethod) -> Double {
  match spread {
    Pad => max(0.0, min(1.0, t))
    Repeat => t - t.floor()
    Reflect => {
      let period = t - (t / 2.0).floor() * 2.0
      if period <= 1.0 {
        period
      } else {
        2.0 - period
      }
    }
  }
}

///|
fn sample_linear_gradient(
  gradient : LinearGradient,
  local_x : Double,
  local_y : Double,
  bbox : BoundingBox,
) -> Color {
  let (x, y) = gradient_sample_point(
    local_x,
    local_y,
    bbox,
    gradient.units,
    gradient.transform,
  )
  let dx = gradient.x2 - gradient.x1
  let dy = gradient.y2 - gradient.y1
  let length_squared = dx * dx + dy * dy
  let t = if length_squared > 0.000000000001 {
    ((x - gradient.x1) * dx + (y - gradient.y1) * dy) / length_squared
  } else {
    0.0
  }
  gradient.color_at(t)
}

///|
fn sample_radial_gradient(
  gradient : RadialGradient,
  local_x : Double,
  local_y : Double,
  bbox : BoundingBox,
) -> Color {
  let (x, y) = gradient_sample_point(
    local_x,
    local_y,
    bbox,
    gradient.units,
    gradient.transform,
  )
  let mut fx = gradient.fx
  let mut fy = gradient.fy
  let focus_dx = fx - gradient.cx
  let focus_dy = fy - gradient.cy
  let focus_distance = (focus_dx * focus_dx + focus_dy * focus_dy).sqrt()
  if gradient.r > 0.0 && focus_distance >= gradient.r {
    let scale = gradient.r * 0.999999 / focus_distance
    fx = gradient.cx + focus_dx * scale
    fy = gradient.cy + focus_dy * scale
  }
  let qx = x - fx
  let qy = y - fy
  let dx = gradient.cx - fx
  let dy = gradient.cy - fy
  let a = dx * dx + dy * dy - gradient.r * gradient.r
  let b = -2.0 * (qx * dx + qy * dy)
  let c = qx * qx + qy * qy
  let t = if gradient.r <= 0.0 {
    0.0
  } else if a.abs() <= 0.000000000001 {
    if b.abs() <= 0.000000000001 {
      0.0
    } else {
      -c / b
    }
  } else {
    let discriminant = b * b - 4.0 * a * c
    if discriminant <= 0.0 {
      0.0
    } else {
      let root = discriminant.sqrt()
      max((-b + root) / (2.0 * a), (-b - root) / (2.0 * a))
    }
  }
  let t = apply_spread_inline(t, gradient.spread_method)
  interpolate_gradient_color(t, gradient.stops)
}

///|
fn make_paint_server_setter(
  paint : ResolvedPaint,
  shape : Shape,
  transform : Transform,
  ctx : RenderState,
  resources : RenderResources,
  opacity : Double,
  sampling : ImageSampling,
) -> ColorSink? {
  let bbox = get_shape_bounds(shape)
  match paint {
    LinearGrad(gradient) => {
      let inverse = transform.inverse()
      Some({
        set: (x, y, coverage_color) => {
          if x < 0 || x >= ctx.width || y < 0 || y >= ctx.height {
            return
          }
          let (local_x, local_y) = inverse.apply(
            x.to_double() + 0.5,
            y.to_double() + 0.5,
          )
          ctx.setter.pixel(
            x,
            y,
            apply_coverage(
              apply_opacity(
                sample_linear_gradient(gradient, local_x, local_y, bbox),
                opacity,
              ),
              coverage_color.a * 257,
            ),
          )
        },
      })
    }
    RadialGrad(gradient) => {
      let inverse = transform.inverse()
      Some({
        set: (x, y, coverage_color) => {
          if x < 0 || x >= ctx.width || y < 0 || y >= ctx.height {
            return
          }
          let (local_x, local_y) = inverse.apply(
            x.to_double() + 0.5,
            y.to_double() + 0.5,
          )
          ctx.setter.pixel(
            x,
            y,
            apply_coverage(
              apply_opacity(
                sample_radial_gradient(gradient, local_x, local_y, bbox),
                opacity,
              ),
              coverage_color.a * 257,
            ),
          )
        },
      })
    }
    Pattern(pattern) =>
      make_pattern_setter(
        pattern, bbox, transform, ctx, resources, opacity, sampling,
      )
    _ => None
  }
}

///|