// Copyright 2026 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

use super::cmd::{DepthFill, LayerFill, LayerFillAttrs, PaintFill, PaintFillAttrs, RenderCmd};
use super::depth::{DepthSegment, DepthState};
use crate::coarse::depth;
use crate::filter::context::FilterContext;
use crate::kurbo::{Affine, Vec2};
use crate::peniko::{BlendMode, Extend, ImageQuality, ImageSampler};
use crate::record::RecordedFill;
use crate::util::Span;
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use vello_common::encode::{EncodedImage, EncodedPaint};
use vello_common::filter::FilterLayerPlacement;
use vello_common::geometry::RectU16;
use vello_common::mask::Mask;
use vello_common::paint::{ImageSource, IndexedPaint, Paint};
use vello_common::pixmap::Pixmap;
use vello_common::record::{LayerClip, LayerProps, Node, RecordedLayer, RecordedLayerKind};
use vello_common::strip::{Strip, visit_strip_fill_segments};
use vello_common::tile::Tile;
use vello_common::util::{Clear, RectExt, RetainVec, VecPool};

/// State for a single row of strips.
#[derive(Debug, Default)]
pub(crate) struct RowState {
    /// Normal render commands rendered in back-to-front with depth buffer read.
    pub(crate) render_cmds: Vec<RenderCmd>,
    /// Opaque fill commands rendered front-to-back with depth buffer read and write.
    pub(crate) depth_cmds: Vec<DepthFill>,
    /// State of the depth buffer for this row.
    depth: DepthState,
    /// Current layer depth.
    pub(super) layer_depth: usize,
    layer_stack: Vec<RowLayerState>,
}

/// Each recorded layer by itself already stores a bounding box.
/// However, we want to take this one step further and track a horizontal bounding
/// box of each row band individually. This allows us to save a lot of work when clearing buffers
/// during fine rasterization.
///
/// For example, let's say that we are rendering a triangle with the points
/// (0, 0), (500, 0) and (250, 250). The bounding box would be (0, 0) and (500, 500). However,
/// the triangle gets much more narrow as we advance through the rows. If we just chose
/// the coarse bounding box, we would always end up clearing a whole width of 500 pixels during
/// fine rasterization. By tracking the bounding box per row, we can reduce the work to the
/// absolute minimum necessary for each row.
#[derive(Debug)]
struct RowLayerState {
    /// The index in the command stream where the `PushBuf` command of the corresponding
    /// layer lives.
    push_cmd_idx: usize,
    /// The horizontal span of the layer.
    span: Option<Span>,
}

impl RowState {
    pub(super) fn new() -> Self {
        Self::default()
    }

    fn clear(&mut self) {
        self.render_cmds.clear();
        self.depth_cmds.clear();
        self.depth.reset();
        self.layer_depth = 0;
        self.layer_stack.clear();
    }

    #[inline]
    pub(super) fn push_cmd(&mut self, cmd: RenderCmd) {
        match cmd {
            RenderCmd::PaintFill(cmd) => self.include_current_span(cmd.span),
            RenderCmd::LayerFill(cmd) => self.include_current_span(cmd.span),
            RenderCmd::PushBuf(_) | RenderCmd::PopBuf => {}
        }

        self.render_cmds.push(cmd);
    }

    #[inline]
    pub(super) fn push_buf(&mut self) {
        let push_cmd_idx = self.render_cmds.len();
        self.render_cmds.push(RenderCmd::PushBuf(None));
        self.layer_stack.push(RowLayerState {
            push_cmd_idx,
            span: None,
        });
        self.layer_depth += 1;
    }

    #[inline]
    pub(super) fn pop_buf(&mut self) {
        let layer = self.layer_stack.pop().unwrap();
        match &mut self.render_cmds[layer.push_cmd_idx] {
            RenderCmd::PushBuf(push_span) => *push_span = layer.span,
            _ => unreachable!("layer stack must point to a PushBuf command"),
        }

        self.render_cmds.push(RenderCmd::PopBuf);

        self.layer_depth -= 1;
    }

    #[inline]
    pub(super) fn push_depth_fill(&mut self, cmd: DepthFill, draw_id: u32) {
        let span = cmd.span();
        self.depth.include_span(span, draw_id);
        self.depth_cmds.push(cmd);
    }

    #[inline]
    pub(crate) fn can_skip_depth(&self, span: Span, draw_id: u32) -> bool {
        self.depth.can_skip(span, draw_id)
    }

    #[inline]
    fn include_current_span(&mut self, span: Span) {
        if let Some(layer) = self.layer_stack.last_mut() {
            match &mut layer.span {
                Some(layer_span) => layer_span.extend(span),
                None => layer.span = Some(span),
            }
        }
    }
}

impl Clear for RowState {
    fn clear(&mut self) {
        Self::clear(self);
    }
}

fn debug_assert_tile_aligned(point: (u16, u16), description: &str) {
    debug_assert_eq!(
        point.0 % Tile::WIDTH,
        0,
        "{description} must be tile-width aligned",
    );
    debug_assert_eq!(
        point.1 % Tile::HEIGHT,
        0,
        "{description} must be tile-height aligned",
    );
}

/// A bucketer that groups commands into strip-row-sized buckets.
#[derive(Debug)]
pub(crate) struct CommandBucketer {
    /// The viewport of the root/filter layer we are currently bucketing.
    viewport: RectU16,
    /// The currently active stack of clip bboxes (from layer clips), always anchored at the
    /// (0, 0) origin, regardless of the viewport, tracked for two reasons:
    /// - So we can clamp fill commands to the bbox and avoid unnecessary rendering work.
    /// - In case we have a filter layer with a clip, it all happens in three steps:
    ///   1) We first push a new intermediate layer with the clip.
    ///   2) We composite the whole filter layer **cropped to the active clip bbox**. Note that
    ///      the filter layer itself is _not_ affected by the clip bbox, as filter layers should
    ///      always be fully rendered before applying clipping. However, by limiting the composition
    ///      to the coarse clip bbox we might be able to save a lot of work if only a small part
    ///      of the filter layer is visible.
    ///   3) Pop the intermediate layer, which will take care of doing the fine-grained clipping
    ///      (e.g. applying anti-aliasing from the clip path).
    pub(super) clip_bboxes: Vec<RectU16>,
    /// The actual render commands for each strip row.
    ///
    /// Since this is essentially a 2D-array, we use [`RetainVec`] to preserve inner allocations
    /// upon resetting.
    pub(super) rows: RetainVec<RowState>,
    pub(crate) paint_fill_attrs: Vec<PaintFillAttrs>,
    pub(crate) layer_fill_attrs: Vec<LayerFillAttrs>,
    pub(crate) filter_paints: Vec<EncodedPaint>,
    /// Keeping track of currently active layers to enable lazy layer pushing.
    pub(super) active_layers: Vec<ActiveLayer>,
    /// A vector pool for keeping track of occupied rows in a layer.
    occupied_rows_pool: VecPool<usize>,
    /// Scratch space used when replaying clip strips while popping clipped layers.
    occupied_rows_bool_scratch: Vec<bool>,
    /// A counter to assign monotonically increasing IDs to draws to enable depth buffer rendering.
    pub(super) next_draw_id: u32,
}

impl CommandBucketer {
    pub(crate) fn from_wh(width: u16, height: u16) -> Self {
        Self::new(RectU16::new(0, 0, width, height))
    }

    pub(crate) fn new(mut viewport: RectU16) -> Self {
        // It's _very_ important that we snap to tile coordinates. Fine rasterization assumes
        // that the width is a multiple of the tile width, so if that's not the case bad things
        // could happen!
        viewport = viewport.snap_to_tile_coordinates();
        let clip_bbox = RectU16::new(0, 0, viewport.width(), viewport.height());
        // Note: `clip_bbox` is already snapped to tile coordinates because `viewport` is, so no
        // need to `div_ceil` here.
        let num_rows = usize::from(clip_bbox.height() / Tile::HEIGHT);

        Self {
            viewport,
            clip_bboxes: vec![clip_bbox],
            rows: RetainVec::with_len(num_rows, RowState::new),
            paint_fill_attrs: Vec::new(),
            layer_fill_attrs: Vec::new(),
            filter_paints: Vec::new(),
            active_layers: Vec::new(),
            occupied_rows_pool: VecPool::default(),
            occupied_rows_bool_scratch: vec![false; num_rows],
            // It is important to start at 1, because the depth buffer uses 0 for "no entries yet".
            next_draw_id: 1,
        }
    }

    fn bbox_span(bbox: RectU16) -> Span {
        // Bbox might be empty vertically but not horizontally. In this case,
        // it should still be considered a zero-sized span, though.
        // TODO: Discard empty layers in an earlier stage.
        if bbox.is_empty() {
            Span::new(bbox.x0, 0)
        } else {
            Span::new(bbox.x0, bbox.x1 - bbox.x0)
        }
    }

    pub(crate) fn rows(&self) -> &[RowState] {
        self.rows.as_slice()
    }

    pub(crate) fn width(&self) -> u16 {
        self.clip_bboxes[0].width()
    }

    fn viewport_origin(&self) -> (u16, u16) {
        (self.viewport.x0, self.viewport.y0)
    }

    pub(crate) fn reset(&mut self, mut viewport: RectU16) {
        // See comments in `CommandBucketer::reset`.
        viewport = viewport.snap_to_tile_coordinates();
        let clip_bbox = RectU16::new(0, 0, viewport.width(), viewport.height());
        let num_rows = usize::from(viewport.height() / Tile::HEIGHT);

        self.rows.clear();
        self.rows.resize_with(num_rows, RowState::new);
        self.paint_fill_attrs.clear();
        self.layer_fill_attrs.clear();
        self.filter_paints.clear();
        for layer in self.active_layers.drain(..) {
            self.occupied_rows_pool.submit(layer.occupied_rows);
        }
        self.occupied_rows_bool_scratch.clear();
        self.occupied_rows_bool_scratch.resize(num_rows, false);
        self.next_draw_id = 1;
        self.viewport = viewport;
        self.clip_bboxes.truncate(1);
        self.clip_bboxes[0] = clip_bbox;
    }

    fn next_draw_id(&mut self) -> u32 {
        let draw_id = self.next_draw_id;
        self.next_draw_id += 1;

        draw_id
    }

    #[inline(always)]
    pub(super) fn ensure_row_layers(&mut self, row_idx: usize) {
        let layer_depth = self.rows[row_idx].layer_depth;
        if layer_depth == self.active_layers.len() {
            return;
        }

        for layer_idx in layer_depth..self.active_layers.len() {
            self.rows[row_idx].push_buf();
            self.active_layers[layer_idx].occupied_rows.push(row_idx);
        }
    }

    pub(crate) fn bucket_commands(
        &mut self,
        nodes: &[Node],
        draws: &[RecordedFill],
        layers: &[RecordedLayer],
        strips: &[Strip],
        encoded_paints: &[EncodedPaint],
        filter_ctx: &FilterContext,
    ) {
        // When rendering filter layers, we always anchor them so that the top-left of the bounding
        // box lands at (0, 0), even if the bounding box's top-left is for example at (200, 200).
        // This is to ensure that the pixmap is as small as possible. Otherwise, if we for example
        // had a filter layer that draws something small in the bottom right but nowhere else, we
        // would still need to allocate a full viewport-sized pixmap!
        // Therefore, we need to keep track of this offset so that for example paints know that
        // they should actually be sampled at (200, 200) instead of (0, 0).

        debug_assert_tile_aligned(self.viewport_origin(), "viewport origin");

        for node in nodes {
            for RecordedFill {
                thread_idx,
                strip_range,
                paint,
                blend_mode,
                mask,
            } in node.draws_in(draws)
            {
                let draw_id = self.next_draw_id();
                let attrs = PaintFillAttrs {
                    paint: paint.clone(),
                    blend_mode: *blend_mode,
                    mask: mask.clone(),
                    draw_id,
                    thread_idx: *thread_idx,
                    origin: self.viewport_origin(),
                };
                self.generate_fill(&strips[strip_range.clone()], &attrs, encoded_paints);
            }

            if let Some(id) = node.layer {
                let layer = &layers[id as usize];
                let props = &layer.props;

                match &layer.kind {
                    RecordedLayerKind::Regular => {
                        // Regular layers are inlined and bucketed into the same command stream.
                        self.push_layer(props);
                        // TODO: Avoid recursion to prevent stack overflows for deeply nested
                        // layers.
                        self.bucket_commands(
                            &layer.nodes,
                            draws,
                            layers,
                            strips,
                            encoded_paints,
                            filter_ctx,
                        );
                        self.pop_layer(strips);
                    }
                    RecordedLayerKind::Filter { placement, .. } => {
                        let needs_layer = props.blend_mode != BlendMode::default()
                            || props.opacity != 1.0
                            || props.mask.is_some()
                            || props.clip_path.is_some();

                        if needs_layer {
                            self.push_layer(props);
                        }

                        // Note: At this point, we've already rasterized all dependent
                        // filter layers, so this should never fail.
                        if let Some(pixmap) = filter_ctx.filter_layer(id as usize) {
                            self.generate_filter_layer_fill(
                                pixmap,
                                *placement,
                                encoded_paints.len(),
                            );
                        }

                        if needs_layer {
                            self.pop_layer(strips);
                        }
                    }
                }
            }
        }
    }

    pub(crate) fn push_layer(&mut self, props: &LayerProps) {
        let parent_bbox = *self.clip_bboxes.last().unwrap();
        let bbox = props
            .clip_path
            .as_ref()
            .map(|clip| {
                // Make sure to translate the clip path from viewport-space to local space, since
                // `clip_bboxes` uses this coordinate system.
                let clip_bbox = clip.bbox.relative_to_origin(self.viewport_origin());
                clip_bbox.intersect(parent_bbox)
            })
            .unwrap_or(parent_bbox);

        if props.clip_path.is_some() {
            self.clip_bboxes.push(bbox);
        }

        self.active_layers.push(ActiveLayer {
            // TODO: Masks are currently probably broken if they are used inside of a filter layer,
            // since they aren't shifted and also only work if the mask has the same dimensions as
            // our allocated filter layer.
            mask: props.mask.clone(),
            blend_mode: props.blend_mode,
            opacity: props.opacity,
            clip: props.clip_path.clone(),
            span: Self::bbox_span(bbox),
            occupied_rows: self.occupied_rows_pool.take(),
        });

        // If the blend mode is destructive, we need to eagerly push to all rows in the clip bbox,
        // since even areas where we didn't draw anything need to be blended with the destructive
        // blend mode.
        if props.blend_mode.is_destructive() && !bbox.is_empty() {
            let row_start = usize::from(bbox.y0 / Tile::HEIGHT);
            let row_end = usize::from(bbox.y1.div_ceil(Tile::HEIGHT)).min(self.rows.len());
            for row_idx in row_start..row_end {
                self.ensure_row_layers(row_idx);
            }
        }
    }

    pub(crate) fn pop_layer(&mut self, strips: &[Strip]) {
        let mut layer = self.active_layers.pop().unwrap();
        let opacity = layer.opacity;
        let blend_mode = layer.blend_mode;

        // Two cases that need to be distinguished.
        //
        // If no clip was associated, we simply iterate over all rows that
        // were lazily associated with some rendered contents and emit a `LayerFill`
        // instructions across the whole width of the bounding box of the layer.
        //
        // If there _was_ a clip, things get trickier because the `LayerFill` commands
        // need to be generated on a more fine-grained basis, and might in certain cases
        // also require anti-aliasing.

        if let Some(clip) = layer.clip {
            let attrs_idx = self.layer_fill_attrs.len() as u32;
            let draw_id = self.next_draw_id();

            self.layer_fill_attrs.push(LayerFillAttrs {
                blend_mode,
                opacity,
                mask: layer.mask.clone(),
                draw_id,
                thread_idx: clip.thread_idx,
            });
            self.clip_bboxes.pop();

            // Note: The clip strips themselves are still in viewport space, not in local space.
            // They will be converted to local space when calling `generate`.
            let clip_strips = &strips[clip.strip_range];

            // We need to make sure that we only emit `LayerFill` commands for rows that actually
            // have been pushed into. If we don't have a destructive blend mode and only parts of
            // the layer were painted, it can happen that the clip path itself covers a row
            // that actually doesn't have an associated `PushBuf` command.
            // For such rows, we don't want to emit any layer fill commands, because there is
            // nothing to fill into in the first place!

            // `occupied_rows` stores the indices of all rows that have been marked, but they
            // are not sorted or anything. Therefore, we convert it into an indexable array
            // such that in the `generate` closure, we can easily check whether the row is included
            // or now.
            for &row_idx in &layer.occupied_rows {
                self.occupied_rows_bool_scratch[row_idx] = true;
            }

            // Only generate layer fill commands if it actually lies within a row that has
            // been touched by the contents of the layer.
            self.generate(
                clip_strips,
                |bucketer, fill| {
                    if bucketer.occupied_rows_bool_scratch[fill.row_idx] {
                        bucketer.rows[fill.row_idx].push_cmd(RenderCmd::LayerFill(LayerFill::new(
                            fill.span, None, attrs_idx,
                        )));
                    }
                },
                |bucketer, fill| {
                    if bucketer.occupied_rows_bool_scratch[fill.row_idx] {
                        bucketer.rows[fill.row_idx].push_cmd(RenderCmd::LayerFill(LayerFill::new(
                            fill.span,
                            Some(fill.alpha_idx),
                            attrs_idx,
                        )));
                    }
                },
            );

            for row_idx in layer.occupied_rows.drain(..) {
                self.rows[row_idx].pop_buf();
                // Make sure to reset it so that by the end, the vector is all `false` again.
                self.occupied_rows_bool_scratch[row_idx] = false;
            }

            self.occupied_rows_pool.submit(layer.occupied_rows);
        } else {
            let attrs_idx = self.layer_fill_attrs.len() as u32;
            let draw_id = self.next_draw_id();

            self.layer_fill_attrs.push(LayerFillAttrs {
                blend_mode,
                opacity,
                mask: layer.mask.clone(),
                draw_id,
                thread_idx: 0,
            });

            for row_idx in layer.occupied_rows.drain(..) {
                let row = &mut self.rows[row_idx];

                // TODO: Instead of always pushing the full layer bbox across all rows, it
                // would be nice to instead only emit the per-row bounding box.
                row.push_cmd(RenderCmd::LayerFill(LayerFill::new(
                    layer.span, None, attrs_idx,
                )));
                row.pop_buf();
            }

            self.occupied_rows_pool.submit(layer.occupied_rows);
        }
    }

    pub(crate) fn generate_filter_layer_fill(
        &mut self,
        pixmap: Arc<Pixmap>,
        placement: FilterLayerPlacement,
        static_paint_count: usize,
    ) {
        let origin = self.viewport_origin();
        let dest_bbox = placement.dest_bbox;
        let src_sample_shift = placement.src_origin();

        // Here, we want to determine the absolute transform that needs to be applied to the filter
        // image for it to be placed correctly. On the one hand, we position it such that it starts
        // at the top-left of `dest_bbox`, which is the intended location the filter layer should
        // be composited into. On the other hand, we optionally apply a correction to ensure we
        // are sampling from the right location of the filter pixmap (see
        // `FilterLayerPlacement::new` for a more detailed description of how/what this offset
        // represents).
        let src_offset = (
            i32::from(src_sample_shift.0) - i32::from(dest_bbox.x0),
            i32::from(src_sample_shift.1) - i32::from(dest_bbox.y0),
        );

        // Now that we have determined the image transform, we next need to determine which
        // strip rows in the bucketer should actually be emitted. Keep in mind that regardless
        // of what `viewport_origin` is, when actually rendering we always shift the origin to
        // (0, 0). (Note: We did _not_ do this for computing `src_offset` because the shift for the
        // paint itself will be applied later when resolving the indexed paint)
        let dest_bbox = dest_bbox.relative_to_origin(origin);
        let clip_bbox = *self.clip_bboxes.last().unwrap();
        // As noted in [`CommandBucketer::clip_bboxes`], we only need to composite the parts
        // of the filter layer that actually lie within clip bounding box.
        // `clip_bbox` already is in viewport-local coordinates, so we don't need to shift it
        // like we did for `dest_bbox`.
        let clipped_dest_bbox = dest_bbox.intersect(clip_bbox);
        if clipped_dest_bbox.is_empty() {
            return;
        }

        let draw_id = self.next_draw_id();
        let span = Self::bbox_span(clipped_dest_bbox);
        let paint_idx = static_paint_count + self.filter_paints.len();
        self.filter_paints.push(EncodedPaint::Image(EncodedImage {
            source: ImageSource::Pixmap(pixmap),
            sampler: ImageSampler {
                x_extend: Extend::Pad,
                y_extend: Extend::Pad,
                quality: ImageQuality::Low,
                alpha: 1.0,
            },
            may_have_transparency: true,
            transform: Affine::translate((f64::from(src_offset.0), f64::from(src_offset.1))),
            x_advance: Vec2::new(1.0, 0.0),
            y_advance: Vec2::new(0.0, 1.0),
            tint: None,
        }));
        let attrs_idx = self.paint_fill_attrs.len() as u32;
        self.paint_fill_attrs.push(PaintFillAttrs {
            paint: Paint::Indexed(IndexedPaint::new(paint_idx)),
            blend_mode: BlendMode::default(),
            mask: None,
            draw_id,
            thread_idx: 0,
            origin,
        });
        let row_start = usize::from(clipped_dest_bbox.y0 / Tile::HEIGHT);
        let row_end = usize::from(clipped_dest_bbox.y1.div_ceil(Tile::HEIGHT));
        for row_idx in row_start..row_end {
            self.push_fill(GeneratedFill { row_idx, span }, attrs_idx, None);
        }
    }

    pub(crate) fn generate_fill(
        &mut self,
        strip_buf: &[Strip],
        attrs: &PaintFillAttrs,
        encoded_paints: &[EncodedPaint],
    ) {
        if strip_buf.is_empty() {
            return;
        }

        debug_assert_ne!(attrs.draw_id, 0, "fill draw IDs should start at 1");

        let attrs_idx = self.paint_fill_attrs.len() as u32;
        self.paint_fill_attrs.push(attrs.clone());

        let draw_id =
            // While in certain cases it _might_ be okay to use depth culling while inside of
            // a layer, it can get very finicky with blend modes etc., so we just outright
            // reject those for now.
            (self.active_layers.is_empty()
                && attrs.blend_mode == BlendMode::default()
                && attrs.mask.is_none()
                && !attrs.paint.may_have_transparency(encoded_paints))
                .then_some(attrs.draw_id);

        self.generate(
            strip_buf,
            |bucketer, fill| {
                // `push_fill` already calls `ensure_row_layers` so no need to call it twice.
                bucketer.push_fill(fill, attrs_idx, draw_id);
            },
            |bucketer, fill| {
                let row_idx = fill.row_idx;
                bucketer.ensure_row_layers(row_idx);
                bucketer.rows[row_idx].push_cmd(RenderCmd::PaintFill(PaintFill::new(
                    fill.span,
                    Some(fill.alpha_idx),
                    attrs_idx,
                )));
            },
        );
    }

    pub(crate) fn generate<F, A>(
        &mut self,
        strip_buf: &[Strip],
        mut fill_cmd: F,
        mut alpha_fill_cmd: A,
    ) where
        F: FnMut(&mut Self, GeneratedFill),
        A: FnMut(&mut Self, GeneratedAlphaFill),
    {
        if strip_buf.is_empty() {
            return;
        }

        let clip_bbox = *self.clip_bboxes.last().unwrap();

        // TODO: Don't emit layers with empty clip bboxes (and non-destructive blend modes)
        // in the first place during recordings.
        if clip_bbox.is_empty() {
            return;
        }

        // Note: Those will always be aligned to tile coordinates.
        let clip_x0 = clip_bbox.x0;
        let clip_x1 = clip_bbox.x1;

        debug_assert_tile_aligned((clip_x0, clip_bbox.y0), "clip start");
        debug_assert_tile_aligned((clip_x1, clip_bbox.y1), "clip end");

        let origin = self.viewport_origin();
        debug_assert_tile_aligned(origin, "viewport origin");

        // Note: the viewport of a filter layer is based on the bounds of its rendered contents.
        // Therefore, those are always guaranteed to be within the viewport rect. However, this does not
        // apply to any clip paths associated with the filter layer. Including those in the filter
        // layer bbox could unnecessarily balloon the size of the pixmap we allocate if it is
        // very large, since those don't actually contribute any visible output. However, this does
        // mean that this method might be called with strips that do not lie within the viewport.
        // Therefore, we need to make sure to clip those appropriately.

        let origin_tile_x = origin.0 / Tile::WIDTH;
        let origin_tile_y = origin.1 / Tile::HEIGHT;
        let clip_scene_y0 = origin.1.saturating_add(clip_bbox.y0);
        let clip_scene_y1 = origin.1.saturating_add(clip_bbox.y1);
        // Convert to scene coordinates.
        let clip_scene_x0 = origin.0.saturating_add(clip_x0);
        let clip_scene_x1 = origin.0.saturating_add(clip_x1);

        // Clip bounding box in tile units.
        let tile_bounds = RectU16::new(
            clip_scene_x0 / Tile::WIDTH,
            clip_scene_y0 / Tile::HEIGHT,
            clip_scene_x1 / Tile::WIDTH,
            clip_scene_y1 / Tile::HEIGHT,
        );

        visit_strip_fill_segments(
            strip_buf,
            tile_bounds,
            self,
            |bucketer, segment| {
                let row_idx = usize::from(segment.tile_y - origin_tile_y);
                let x0 = (segment.tile_x0 - origin_tile_x) * Tile::WIDTH;
                let x1 = (segment.tile_x1 - origin_tile_x) * Tile::WIDTH;
                alpha_fill_cmd(
                    bucketer,
                    GeneratedAlphaFill {
                        row_idx,
                        span: Span::new(x0, x1 - x0),
                        alpha_idx: segment.alpha_idx,
                    },
                );
            },
            |bucketer, segment| {
                let row_idx = usize::from(segment.tile_y - origin_tile_y);
                let x0 = (segment.tile_x0 - origin_tile_x) * Tile::WIDTH;
                let x1 = (segment.tile_x1 - origin_tile_x) * Tile::WIDTH;
                fill_cmd(
                    bucketer,
                    GeneratedFill {
                        row_idx,
                        span: Span::new(x0, x1 - x0),
                    },
                );
            },
        );
    }

    /// Note: If depth-culling should be disabled, pass `None` to `draw_id`.
    fn push_fill(&mut self, fill: GeneratedFill, attrs_idx: u32, draw_id: Option<u32>) {
        self.ensure_row_layers(fill.row_idx);
        let row = &mut self.rows[fill.row_idx];
        let draw_id = draw_id.filter(|_| row.layer_depth == 0);

        let Some(draw_id) = draw_id else {
            // If depth-culling is disabled, we can just push it as a single contiguous command.
            row.push_cmd(RenderCmd::PaintFill(PaintFill::new(
                fill.span, None, attrs_idx,
            )));

            return;
        };

        depth::split_opaque_span(fill.span, |segment| match segment {
            DepthSegment::Regular(span) => {
                row.push_cmd(RenderCmd::PaintFill(PaintFill::new(span, None, attrs_idx)));
            }
            DepthSegment::Opaque(bucket_range) => {
                row.push_depth_fill(DepthFill::new(bucket_range, attrs_idx), draw_id);
            }
        });
    }
}

/// Metadata about the currently active layer.
#[derive(Debug, Clone)]
pub(crate) struct ActiveLayer {
    pub(crate) mask: Option<Mask>,
    pub(crate) blend_mode: BlendMode,
    pub(crate) opacity: f32,
    pub(crate) clip: Option<LayerClip>,
    pub(crate) span: Span,
    /// Which rows have been drawn into and thus contain lazily-allocated `PushBuf` instructions.
    pub(crate) occupied_rows: Vec<usize>,
}

/// A generic fill to allow using `generate_fill` to create either paint fills or blend fills.
#[derive(Debug, Clone, Copy)]
pub(crate) struct GeneratedFill {
    pub(crate) row_idx: usize,
    pub(crate) span: Span,
}

/// A generic alpha fill to allow using `generate_fill` to create either paint fills or blend fills.
#[derive(Debug, Clone, Copy)]
pub(crate) struct GeneratedAlphaFill {
    pub(crate) row_idx: usize,
    pub(crate) span: Span,
    pub(crate) alpha_idx: u32,
}

#[cfg(test)]
mod tests {
    use super::LayerClip;
    use crate::coarse::CommandBucketer;
    use crate::coarse::cmd::{PaintFillAttrs, RenderCmd};
    use crate::coarse::depth::{BucketRange, DEPTH_BUCKET_WIDTH};
    use vello_common::color::palette::css::{BLUE, RED};
    use vello_common::color::{AlphaColor, Srgb};
    use vello_common::geometry::RectU16;
    use vello_common::paint::{Paint, PremulColor};
    use vello_common::peniko::{BlendMode, Compose, Mix};
    use vello_common::record::LayerProps;
    use vello_common::strip::Strip;
    use vello_common::tile::Tile;

    fn color(alpha: AlphaColor<Srgb>) -> PremulColor {
        PremulColor::from_alpha_color(alpha)
    }

    fn fill_attrs(paint: Paint) -> PaintFillAttrs {
        PaintFillAttrs {
            paint,
            blend_mode: BlendMode::default(),
            mask: None,
            draw_id: 1,
            thread_idx: 0,
            origin: (0, 0),
        }
    }

    fn layer_props() -> LayerProps {
        LayerProps {
            blend_mode: BlendMode::default(),
            opacity: 1.0,
            mask: None,
            clip_path: None,
        }
    }

    fn clipped_layer_props(bbox: RectU16) -> LayerProps {
        LayerProps {
            blend_mode: BlendMode::default(),
            opacity: 1.0,
            mask: None,
            clip_path: Some(LayerClip {
                strip_range: 0..0,
                thread_idx: 0,
                bbox,
            }),
        }
    }

    fn destructive_clipped_layer_props(bbox: RectU16) -> LayerProps {
        LayerProps {
            blend_mode: BlendMode::new(Mix::Normal, Compose::Clear),
            ..clipped_layer_props(bbox)
        }
    }

    fn clipped_layer_props_with_strips(
        bbox: RectU16,
        strip_range: core::ops::Range<usize>,
    ) -> LayerProps {
        LayerProps {
            blend_mode: BlendMode::default(),
            opacity: 1.0,
            mask: None,
            clip_path: Some(LayerClip {
                strip_range,
                thread_idx: 0,
                bbox,
            }),
        }
    }

    fn count_layer_fills(cmds: &[RenderCmd]) -> usize {
        cmds.iter()
            .filter(|cmd| matches!(cmd, RenderCmd::LayerFill(_)))
            .count()
    }

    #[test]
    fn opaque_fill_inside_layer_does_not_use_depth_write() {
        let mut bucketer = CommandBucketer::from_wh(DEPTH_BUCKET_WIDTH, 4);
        let strips = [
            Strip::new(0, 0, 0, false),
            Strip::new(DEPTH_BUCKET_WIDTH, 0, 0, true),
        ];

        bucketer.push_layer(&layer_props());
        bucketer.generate_fill(&strips, &fill_attrs(Paint::Solid(color(RED))), &[]);

        let row = &bucketer.rows()[0];
        assert_eq!(row.depth_cmds.len(), 0);
        assert_eq!(row.render_cmds.len(), 2);
        assert!(matches!(row.render_cmds[0], RenderCmd::PushBuf(_)));
        assert!(
            matches!(row.render_cmds[1], RenderCmd::PaintFill(cmd) if cmd.span.pixel_x() == 0 && cmd.span.pixel_width() == DEPTH_BUCKET_WIDTH)
        );
    }

    #[test]
    fn alpha_fill_is_clipped_to_active_layer_bbox() {
        let mut bucketer = CommandBucketer::from_wh(8, 4);
        let strips = [Strip::new(0, 0, 0, false), Strip::new(12, 0, 48, false)];

        bucketer.push_layer(&clipped_layer_props(RectU16::new(4, 0, 8, 4)));
        bucketer.generate_fill(&strips, &fill_attrs(Paint::Solid(color(RED))), &[]);

        let row = &bucketer.rows()[0];
        assert_eq!(row.render_cmds.len(), 2);
        assert!(matches!(row.render_cmds[0], RenderCmd::PushBuf(_)));
        assert!(matches!(
            row.render_cmds[1],
            RenderCmd::PaintFill(cmd)
                if cmd.span.pixel_x() == 4
                    && cmd.span.pixel_width() == 4
                    && cmd.alpha_idx() == Some(u32::from(4 * Tile::HEIGHT))
        ));
    }

    #[test]
    fn disjoint_nested_clip_bounds_do_not_emit_commands() {
        let mut bucketer = CommandBucketer::from_wh(16, 4);
        let strips = [Strip::new(0, 0, 0, false), Strip::new(16, 0, 0, true)];

        bucketer.push_layer(&clipped_layer_props(RectU16::new(0, 0, 4, 4)));
        bucketer.push_layer(&clipped_layer_props(RectU16::new(8, 0, 12, 4)));
        bucketer.generate_fill(&strips, &fill_attrs(Paint::Solid(color(RED))), &[]);

        assert!(bucketer.rows().iter().all(|row| row.render_cmds.is_empty()));
    }

    #[test]
    fn empty_destructive_clip_does_not_push_rows() {
        let mut bucketer = CommandBucketer::from_wh(16, 4);

        bucketer.push_layer(&clipped_layer_props(RectU16::new(0, 0, 4, 4)));
        bucketer.push_layer(&destructive_clipped_layer_props(RectU16::new(8, 0, 12, 4)));

        assert!(bucketer.rows().iter().all(|row| row.render_cmds.is_empty()));
    }

    #[test]
    fn opaque_fill_uses_depth_write_when_possible() {
        let end = DEPTH_BUCKET_WIDTH * 2 + 4;
        let mut bucketer = CommandBucketer::from_wh(end, 4);
        let strips = [Strip::new(4, 0, 0, false), Strip::new(end, 0, 0, true)];

        bucketer.generate_fill(&strips, &fill_attrs(Paint::Solid(color(RED))), &[]);

        let row = &bucketer.rows()[0];
        assert_eq!(row.depth_cmds.len(), 1);
        assert_eq!(row.depth_cmds[0].bucket_range(), BucketRange::new(1, 2));
        assert_eq!(row.render_cmds.len(), 2);
        assert!(
            matches!(row.render_cmds[0], RenderCmd::PaintFill(cmd) if cmd.span.pixel_x() == 4 && cmd.span.pixel_width() == DEPTH_BUCKET_WIDTH - 4)
        );
        assert!(
            matches!(row.render_cmds[1], RenderCmd::PaintFill(cmd) if cmd.span.pixel_x() == DEPTH_BUCKET_WIDTH * 2 && cmd.span.pixel_width() == 4)
        );
    }

    #[test]
    fn non_opaque_fill_uses_regular_commands() {
        let mut bucketer = CommandBucketer::from_wh(DEPTH_BUCKET_WIDTH, 4);
        let strips = [
            Strip::new(0, 0, 0, false),
            Strip::new(DEPTH_BUCKET_WIDTH, 0, 0, true),
        ];

        bucketer.generate_fill(
            &strips,
            &fill_attrs(Paint::Solid(color(BLUE.with_alpha(0.5)))),
            &[],
        );

        let row = &bucketer.rows()[0];
        assert_eq!(row.depth_cmds.len(), 0);
        assert_eq!(row.render_cmds.len(), 1);
        assert!(
            matches!(row.render_cmds[0], RenderCmd::PaintFill(cmd) if cmd.span.pixel_x() == 0 && cmd.span.pixel_width() == DEPTH_BUCKET_WIDTH)
        );
    }

    #[test]
    fn clips_fills_correctly_inside_nonzero_origin_viewport() {
        // Viewport spans scene (32, 32) to (96, 96). Local space is 64x64, the origin at (32, 32).
        let mut bucketer = CommandBucketer::new(RectU16::new(32, 32, 96, 96));
        // Clip bbox in scene coordinates: (40, 32)..(72, 96) => local (8, 0)..(40, 64).
        bucketer.push_layer(&clipped_layer_props(RectU16::new(40, 32, 72, 96)));

        // A 32px-wide alpha strip at scene (40, 32) => local (8, 0), fully inside the clip.
        let strips = [
            Strip::new(40, 32, 0, false),
            Strip::new(72, 32, 32 * u32::from(Tile::HEIGHT), false),
        ];
        bucketer.generate_fill(&strips, &fill_attrs(Paint::Solid(color(RED))), &[]);

        let row = &bucketer.rows()[0];

        assert!(matches!(row.render_cmds[0], RenderCmd::PushBuf(_)));
        // The fill inside should not have been clipped.
        assert!(matches!(
            row.render_cmds[1],
            RenderCmd::PaintFill(cmd)
                if cmd.span.pixel_x() == 8
                    && cmd.span.pixel_width() == 32
                    && cmd.alpha_idx() == Some(0)
        ));
    }

    #[test]
    fn culls_clip_strips_above_viewport_origin() {
        // Viewport spans scene (0, 32) to (64, 96). Local space is 64x64, the origin at (0, 32).
        let mut bucketer = CommandBucketer::new(RectU16::new(0, 32, 64, 96));

        let alpha = u32::from(Tile::HEIGHT);
        let strips = [
            // Content: 16px alpha strip at scene (0, 32) => local row 0.
            Strip::new(0, 32, 0, false),
            Strip::new(16, 32, 16 * alpha, false),
            // Clip strip above the viewport origin at scene y = 0, covering nothing visible.
            Strip::new(0, 0, 16 * alpha, false),
            Strip::new(16, 0, 32 * alpha, false),
            // Clip strip at scene y = 32 => local row 0: the real coverage.
            Strip::new(0, 32, 32 * alpha, false),
            Strip::new(16, 32, 48 * alpha, false),
        ];

        // Clip bbox in scene coordinates: (0, 0)..(16, 40) => local (0, 0)..(16, 8).
        bucketer.push_layer(&clipped_layer_props_with_strips(
            RectU16::new(0, 0, 16, 40),
            2..6,
        ));
        bucketer.generate_fill(&strips[0..2], &fill_attrs(Paint::Solid(color(RED))), &[]);
        bucketer.pop_layer(&strips);

        let row = &bucketer.rows()[0];
        assert_eq!(
            count_layer_fills(&row.render_cmds),
            1,
            "row 0 must be composited exactly once, got {:?}",
            row.render_cmds
        );
    }
}
