///|
fn render_paint_stroke_radius(paint : Paint) -> Float {
  let width : Float = if paint.stroke_width <= 0.0 {
    1.0
  } else {
    paint.stroke_width
  }
  width / 2.0
}

///|
fn point_draw_bounds(point : Point, paint : Paint) -> Rect {
  let radius = render_paint_stroke_radius(paint)
  Rect::from_xywh(
    point.x - radius,
    point.y - radius,
    radius * 2.0,
    radius * 2.0,
  )
}

///|
fn points_draw_bounds(points : Array[Point], paint : Paint) -> Rect? {
  match Rect::from_points(points) {
    None => None
    Some(bounds) => {
      let radius = render_paint_stroke_radius(paint)
      Some(bounds.outset(radius, radius))
    }
  }
}

///|
fn line_draw_bounds(start : Point, end : Point, paint : Paint) -> Rect? {
  points_draw_bounds([start, end], paint)
}

///|
let estimated_render_text_advance_per_byte : Float = 8.0

///|
let estimated_render_text_ascent : Float = 12.0

///|
let estimated_render_text_descent : Float = 4.0

///|
fn point_mode_draw_bounds(
  points : Array[Point],
  mode : PointMode,
  paint : Paint,
) -> Rect? {
  match mode {
    Points => points_draw_bounds(points, paint)
    Lines => {
      let used : Array[Point] = []
      for i = 0; i + 1 < points.length(); i = i + 2 {
        used.push(points[i])
        used.push(points[i + 1])
      }
      points_draw_bounds(used, paint)
    }
    Polygon =>
      if points.length() < 2 {
        None
      } else {
        points_draw_bounds(points, paint)
      }
  }
}

///|
fn text_utf8_draw_bounds(
  byte_count : Int,
  origin : Point,
  is_right_to_left? : Bool = false,
) -> Rect? {
  if byte_count <= 0 {
    None
  } else {
    let width = Float::from_int(byte_count) *
      estimated_render_text_advance_per_byte
    let left = if is_right_to_left { origin.x - width } else { origin.x }
    let right = if is_right_to_left { origin.x } else { origin.x + width }
    Some(
      Rect::new(
        left,
        origin.y - estimated_render_text_ascent,
        right,
        origin.y + estimated_render_text_descent,
      ).sorted(),
    )
  }
}

///|
fn text_run_draw_bounds(
  text : Bytes,
  descriptor : TextRunDescriptor,
  origin : Point,
) -> Rect? {
  let range = descriptor.range.clamp_to_text(text)
  text_utf8_draw_bounds(
    range.length(),
    origin,
    is_right_to_left=descriptor.is_right_to_left(),
  )
}

///|
fn shaped_glyph_run_draw_bounds(
  glyph_run : ShapedGlyphRunDescriptor,
  origin : Point,
) -> Rect? {
  if !glyph_run.is_valid() {
    None
  } else {
    let advance = glyph_run.shaped_run.advance
    let right = origin.x + advance.x
    let bottom = origin.y + advance.y
    Some(Rect::new(origin.x, origin.y, right, bottom).sorted())
  }
}

///|
fn image_draw_bounds(image : ImageDescriptor, top_left : Point) -> Rect? {
  match image.dimensions() {
    None => None
    Some(dimensions) =>
      Some(
        Rect::from_xywh(
          top_left.x,
          top_left.y,
          Float::from_int(dimensions.width),
          Float::from_int(dimensions.height),
        ),
      )
  }
}

///|
fn RenderCommand::draw_bounds(self : RenderCommand) -> Rect? {
  match self {
    DrawCanvasPoint(point, paint) => Some(point_draw_bounds(point, paint))
    DrawCanvasLine(start, end, paint) => line_draw_bounds(start, end, paint)
    DrawCanvasPoints(points, mode, paint) =>
      point_mode_draw_bounds(points, mode, paint)
    DrawCanvasRect(rect, _) => Some(rect.sorted())
    DrawCanvasOval(oval, _) => Some(oval.sorted())
    DrawCanvasCircle(center, radius, _) =>
      if radius <= 0.0 {
        None
      } else {
        Some(
          Rect::from_xywh(
            center.x - radius,
            center.y - radius,
            radius * 2.0,
            radius * 2.0,
          ),
        )
      }
    DrawCanvasArc(oval, _, _, _, paint) => {
      let radius = render_paint_stroke_radius(paint)
      Some(oval.sorted().outset(radius, radius))
    }
    DrawCanvasRoundRect(rect, _, _, _) => Some(rect.sorted())
    DrawCanvasRRect(rrect, _) => Some(rrect.bounds())
    DrawCanvasDRRect(outer, _, _) => Some(outer.bounds())
    DrawCanvasRectShader(rect, _, _) => Some(rect.sorted())
    DrawCanvasRoundRectShader(rect, _, _, _, _) => Some(rect.sorted())
    DrawCanvasPath(path, _) => path.bounds()
    DrawCanvasPathShader(path, _, _) => path.bounds()
    DrawCanvasImage(image, top_left) => image_draw_bounds(image, top_left)
    DrawCanvasImageRect(_, _, dst, _, _) => Some(dst.sorted())
    DrawCanvasTextUtf8(text, origin, _) =>
      text_utf8_draw_bounds(text.length(), origin)
    DrawCanvasTextRunUtf8(text, descriptor, _, origin, _) =>
      text_run_draw_bounds(text, descriptor, origin)
    DrawCanvasShapedGlyphRunUtf8(_, glyph_run, origin, _) =>
      shaped_glyph_run_draw_bounds(glyph_run, origin)
    _ => None
  }
}

///|
fn include_render_command_bounds(bounds : Rect?, next : Rect) -> Rect? {
  if next.is_empty() {
    bounds
  } else {
    match bounds {
      None => Some(next)
      Some(bounds) => Some(bounds.join(next))
    }
  }
}

///|
priv struct RenderCommandBoundsState {
  matrix : Matrix
  clip : Rect
}

///|
fn RenderCommandBoundsState::new(
  pass_bounds : Rect,
) -> RenderCommandBoundsState {
  { matrix: Matrix::identity(), clip: pass_bounds }
}

///|
fn render_command_update_matrix(
  command : RenderCommand,
  current_matrix : Matrix,
  matrix_stack : Array[Matrix],
) -> Matrix {
  match command {
    SaveCanvas | SaveCanvasLayer(_, _, _, _, _) => {
      matrix_stack.push(current_matrix)
      current_matrix
    }
    RestoreCanvas =>
      match matrix_stack.pop() {
        None => Matrix::identity()
        Some(matrix) => matrix
      }
    TranslateCanvas(delta) => current_matrix.post_translate(delta.x, delta.y)
    ScaleCanvas(scale) => current_matrix.post_scale(scale.width, scale.height)
    RotateCanvas(degrees) => current_matrix.post_rotate_degrees(degrees)
    SkewCanvas(skew) => current_matrix.post_skew(skew.x, skew.y)
    ConcatCanvas(matrix) => current_matrix.post_concat(matrix)
    ResetCanvasMatrix => Matrix::identity()
    _ => current_matrix
  }
}

///|
fn render_command_clip_bounds(
  rect : Rect,
  current_matrix : Matrix,
  pass_bounds : Rect,
) -> Rect? {
  current_matrix.map_rect(rect).intersect(pass_bounds)
}

///|
fn render_command_update_clip(
  command : RenderCommand,
  current_matrix : Matrix,
  current_clip : Rect,
  pass_bounds : Rect,
  clip_stack : Array[Rect],
) -> Rect {
  match command {
    SaveCanvas | SaveCanvasLayer(_, _, _, _, _) => {
      clip_stack.push(current_clip)
      current_clip
    }
    RestoreCanvas =>
      match clip_stack.pop() {
        None => pass_bounds
        Some(clip) => clip
      }
    ResetCanvasMatrix => current_clip
    ClipCanvasRect(rect, Intersect) =>
      match render_command_clip_bounds(rect, current_matrix, pass_bounds) {
        None => Rect::empty()
        Some(clip) =>
          match current_clip.intersect(clip) {
            None => Rect::empty()
            Some(clip) => clip
          }
      }
    ClipCanvasRRect(rrect, Intersect) =>
      match
        render_command_clip_bounds(rrect.bounds(), current_matrix, pass_bounds) {
        None => Rect::empty()
        Some(clip) =>
          match current_clip.intersect(clip) {
            None => Rect::empty()
            Some(clip) => clip
          }
      }
    ClipCanvasPath(path, Intersect) =>
      match path.bounds() {
        None => Rect::empty()
        Some(bounds) =>
          match
            render_command_clip_bounds(bounds, current_matrix, pass_bounds) {
            None => Rect::empty()
            Some(clip) =>
              match current_clip.intersect(clip) {
                None => Rect::empty()
                Some(clip) => clip
              }
          }
      }
    ClipCanvasRect(_, Difference)
    | ClipCanvasRRect(_, Difference)
    | ClipCanvasPath(_, Difference) => current_clip
    _ => current_clip
  }
}

///|
fn render_command_transformed_bounds(
  command : RenderCommand,
  current_matrix : Matrix,
  current_clip : Rect,
) -> Rect? {
  match command {
    ClearCanvas(_) => Some(current_clip)
    DrawCanvasColor(_, _) | DrawCanvasPaint(_) => Some(current_clip)
    DrawCanvasPaintShader(_, _) => Some(current_clip)
    command =>
      match command.draw_bounds() {
        None => None
        Some(draw_bounds) =>
          current_matrix.map_rect(draw_bounds).intersect(current_clip)
      }
  }
}

///|
pub fn RenderCommandList::touched_bounds(self : RenderCommandList) -> Rect? {
  let pass_bounds = self.pass.bounds.to_rect()
  if pass_bounds.is_empty() {
    None
  } else {
    let initial_bounds : Rect? = None
    let matrix_stack : Array[Matrix] = []
    let clip_stack : Array[Rect] = []
    for bounds = initial_bounds, state = RenderCommandBoundsState::new(
            pass_bounds,
          ), i = 0
        i < self.commands.length()
        i = i + 1 {
      let command = self.commands[i]
      let next = render_command_transformed_bounds(
        command,
        state.matrix,
        state.clip,
      )
      let bounds = match next {
        None => bounds
        Some(next) => include_render_command_bounds(bounds, next)
      }
      let matrix = render_command_update_matrix(
        command,
        state.matrix,
        matrix_stack,
      )
      let clip = render_command_update_clip(
        command,
        state.matrix,
        state.clip,
        pass_bounds,
        clip_stack,
      )
      continue bounds, { matrix, clip }, i + 1
    } nobreak {
      bounds
    }
  }
}