#pragma once

#include "paint_renderer/compositor.h"
#include "brush.h"
#include "../libs/glm/glm/glm.hpp"
#include "../libs/glm/glm/gtc/matrix_transform.hpp"
#include "../libs/glm/glm/gtx/rotate_vector.hpp"
#include "util.h"

#include <array>
#include <cstddef>
#include <functional>
#include <span>
#include <string_view>
#include <utility>
#include <vector>

namespace pp::panopainter {

void setup_legacy_stroke_dual_shader(bool uses_pattern);

struct LegacyStrokeSampleExecutionRequest {
    std::string_view context;
    glm::vec2 target_size {};
    std::span<const vertex_t> vertices;
    std::span<const pp::paint_renderer::CanvasStrokePoint> sample_points;
    bool copy_stroke_destination = false;
    std::function<void()> bind_destination_texture;
    std::function<void(int, int, int, int, int, int)> copy_framebuffer_to_destination_texture;
    std::function<void()> unbind_destination_texture;
    std::function<void(std::span<const vertex_t>)> upload_brush_vertices;
    std::function<void()> draw_brush_shape;
};

struct LegacyStrokeSampleExecutionResult {
    bool ok = false;
    glm::ivec2 copy_position {};
    glm::ivec2 copy_size {};
    glm::vec4 dirty_bounds {};
};

struct LegacyStrokeSamplePolygonExecutionRequest {
    std::string_view context;
    glm::vec2 target_size {};
    std::span<const vertex_t> polygon_vertices;
    bool copy_stroke_destination = false;
    std::function<void()> bind_destination_texture;
    std::function<void(int, int, int, int, int, int)> copy_framebuffer_to_destination_texture;
    std::function<void()> unbind_destination_texture;
    std::function<void(std::span<const vertex_t>)> upload_brush_vertices;
    std::function<void()> draw_brush_shape;
};

struct LegacyStrokeFaceSamplePolygonExecutionRequest {
    std::string_view context;
    glm::vec2 target_size {};
    std::span<const vertex_t> polygon_vertices;
    int face_index = 0;
    bool copy_stroke_destination = false;
    std::function<void(int)> bind_destination_texture;
    std::function<void(int, int, int, int, int, int, int)> copy_framebuffer_to_destination_texture;
    std::function<void(int)> unbind_destination_texture;
    std::function<void(int, std::span<const vertex_t>)> upload_brush_vertices;
    std::function<void(int)> draw_brush_shape;
};

enum class LegacyCanvasStrokeTextureInput {
    brush_tip,
    stroke_destination,
    pattern,
    mixer,
};

struct LegacyCanvasStrokeTextureBinding {
    LegacyCanvasStrokeTextureInput input = LegacyCanvasStrokeTextureInput::brush_tip;
    int slot = 0;
};

struct LegacyCanvasStrokeTextureInputDispatch {
    std::function<void(int)> activate_texture_unit;
    std::function<void()> bind_brush_tip;
    std::function<void()> unbind_brush_tip;
    std::function<void()> bind_stroke_destination;
    std::function<void()> unbind_stroke_destination;
    std::function<void()> bind_pattern;
    std::function<void()> unbind_pattern;
    std::function<void()> bind_mixer;
    std::function<void()> unbind_mixer;
};

[[nodiscard]] inline LegacyCanvasStrokeTextureInputDispatch make_legacy_canvas_stroke_main_pass_texture_dispatch(
    std::function<void(int)> activate_texture_unit,
    std::function<void()> bind_brush_tip,
    std::function<void()> unbind_brush_tip,
    std::function<void()> bind_pattern,
    std::function<void()> bind_mixer,
    std::function<void()> unbind_mixer)
{
    return LegacyCanvasStrokeTextureInputDispatch {
        .activate_texture_unit = std::move(activate_texture_unit),
        .bind_brush_tip = std::move(bind_brush_tip),
        .unbind_brush_tip = std::move(unbind_brush_tip),
        .bind_pattern = std::move(bind_pattern),
        .bind_mixer = std::move(bind_mixer),
        .unbind_mixer = std::move(unbind_mixer),
    };
}

[[nodiscard]] inline LegacyCanvasStrokeTextureInputDispatch make_legacy_canvas_stroke_brush_tip_texture_dispatch(
    std::function<void(int)> activate_texture_unit,
    std::function<void()> bind_brush_tip,
    std::function<void()> unbind_brush_tip)
{
    return LegacyCanvasStrokeTextureInputDispatch {
        .activate_texture_unit = std::move(activate_texture_unit),
        .bind_brush_tip = std::move(bind_brush_tip),
        .unbind_brush_tip = std::move(unbind_brush_tip),
    };
}

[[nodiscard]] inline LegacyCanvasStrokeTextureInputDispatch make_legacy_canvas_stroke_brush_tip_texture_dispatch(
    std::function<void(int)> activate_texture_unit,
    std::function<void(int)> bind_brush_tip,
    std::function<void(int)> unbind_brush_tip,
    int face_index)
{
    return make_legacy_canvas_stroke_brush_tip_texture_dispatch(
        std::move(activate_texture_unit),
        [bind_brush_tip = std::move(bind_brush_tip), face_index]() {
            bind_brush_tip(face_index);
        },
        [unbind_brush_tip = std::move(unbind_brush_tip), face_index]() {
            unbind_brush_tip(face_index);
        });
}

[[nodiscard]] inline LegacyCanvasStrokeTextureInputDispatch make_legacy_canvas_stroke_destination_texture_dispatch(
    std::function<void(int)> activate_texture_unit,
    std::function<void()> bind_stroke_destination,
    std::function<void()> unbind_stroke_destination)
{
    return LegacyCanvasStrokeTextureInputDispatch {
        .activate_texture_unit = std::move(activate_texture_unit),
        .bind_stroke_destination = std::move(bind_stroke_destination),
        .unbind_stroke_destination = std::move(unbind_stroke_destination),
    };
}

[[nodiscard]] inline LegacyCanvasStrokeTextureInputDispatch make_legacy_canvas_stroke_pad_destination_texture_dispatch(
    std::function<void(int)> activate_texture_unit,
    std::function<void()> bind_stroke_destination,
    std::function<void()> unbind_stroke_destination)
{
    return LegacyCanvasStrokeTextureInputDispatch {
        .activate_texture_unit = std::move(activate_texture_unit),
        .bind_stroke_destination = std::move(bind_stroke_destination),
        .unbind_stroke_destination = std::move(unbind_stroke_destination),
    };
}

[[nodiscard]] inline LegacyCanvasStrokeTextureInputDispatch make_legacy_canvas_stroke_pad_destination_texture_dispatch(
    std::function<void(int)> activate_texture_unit,
    std::function<void(int)> bind_stroke_destination,
    std::function<void(int)> unbind_stroke_destination,
    int face_index)
{
    return make_legacy_canvas_stroke_pad_destination_texture_dispatch(
        std::move(activate_texture_unit),
        [bind_stroke_destination = std::move(bind_stroke_destination), face_index]() {
            bind_stroke_destination(face_index);
        },
        [unbind_stroke_destination = std::move(unbind_stroke_destination), face_index]() {
            unbind_stroke_destination(face_index);
        });
}

struct LegacyCanvasStrokeSamplerDispatch {
    std::function<void(int)> bind_brush_tip_sampler;
    std::function<void()> unbind_brush_tip_sampler;
    std::function<void(int)> bind_stroke_destination_sampler;
    std::function<void()> unbind_stroke_destination_sampler;
    std::function<void(int)> bind_pattern_sampler;
    std::function<void()> unbind_pattern_sampler;
    std::function<void(int)> bind_mixer_sampler;
    std::function<void()> unbind_mixer_sampler;
};

struct LegacyCanvasStrokeMainPassExecutionRequest {
    std::string_view context;
    std::function<void()> bind_samplers;
    std::function<void()> bind_textures;
    std::function<void()> execute_frame_pass;
    std::function<void()> unbind_textures;
    std::function<void()> unbind_samplers;
};

[[nodiscard]] inline LegacyCanvasStrokeMainPassExecutionRequest make_legacy_canvas_stroke_main_pass_execution_request(
    std::string_view context,
    std::function<void()> bind_samplers,
    std::function<void()> bind_textures,
    std::function<void()> execute_frame_pass,
    std::function<void()> unbind_textures,
    std::function<void()> unbind_samplers)
{
    return LegacyCanvasStrokeMainPassExecutionRequest {
        .context = context,
        .bind_samplers = std::move(bind_samplers),
        .bind_textures = std::move(bind_textures),
        .execute_frame_pass = std::move(execute_frame_pass),
        .unbind_textures = std::move(unbind_textures),
        .unbind_samplers = std::move(unbind_samplers),
    };
}

[[nodiscard]] inline LegacyCanvasStrokeSamplerDispatch make_legacy_canvas_stroke_live_pass_sampler_dispatch(
    std::function<void(int)> bind_brush_tip_sampler,
    std::function<void()> unbind_brush_tip_sampler,
    std::function<void(int)> bind_stroke_destination_sampler,
    std::function<void()> unbind_stroke_destination_sampler,
    std::function<void(int)> bind_pattern_sampler,
    std::function<void()> unbind_pattern_sampler,
    std::function<void(int)> bind_mixer_sampler,
    std::function<void()> unbind_mixer_sampler)
{
    return LegacyCanvasStrokeSamplerDispatch {
        .bind_brush_tip_sampler = std::move(bind_brush_tip_sampler),
        .unbind_brush_tip_sampler = std::move(unbind_brush_tip_sampler),
        .bind_stroke_destination_sampler = std::move(bind_stroke_destination_sampler),
        .unbind_stroke_destination_sampler = std::move(unbind_stroke_destination_sampler),
        .bind_pattern_sampler = std::move(bind_pattern_sampler),
        .unbind_pattern_sampler = std::move(unbind_pattern_sampler),
        .bind_mixer_sampler = std::move(bind_mixer_sampler),
        .unbind_mixer_sampler = std::move(unbind_mixer_sampler),
    };
}

[[nodiscard]] inline LegacyCanvasStrokeSamplerDispatch make_legacy_canvas_stroke_sampler_dispatch(
    std::function<void(int)> bind_brush_tip_sampler,
    std::function<void()> unbind_brush_tip_sampler,
    std::function<void(int)> bind_stroke_destination_sampler,
    std::function<void()> unbind_stroke_destination_sampler,
    std::function<void(int)> bind_pattern_sampler,
    std::function<void()> unbind_pattern_sampler,
    std::function<void(int)> bind_mixer_sampler,
    std::function<void()> unbind_mixer_sampler)
{
    return LegacyCanvasStrokeSamplerDispatch {
        .bind_brush_tip_sampler = std::move(bind_brush_tip_sampler),
        .unbind_brush_tip_sampler = std::move(unbind_brush_tip_sampler),
        .bind_stroke_destination_sampler = std::move(bind_stroke_destination_sampler),
        .unbind_stroke_destination_sampler = std::move(unbind_stroke_destination_sampler),
        .bind_pattern_sampler = std::move(bind_pattern_sampler),
        .unbind_pattern_sampler = std::move(unbind_pattern_sampler),
        .bind_mixer_sampler = std::move(bind_mixer_sampler),
        .unbind_mixer_sampler = std::move(unbind_mixer_sampler),
    };
}

[[nodiscard]] inline bool execute_legacy_canvas_stroke_main_pass(
    const LegacyCanvasStrokeMainPassExecutionRequest& request)
{
    if (!request.bind_samplers ||
        !request.bind_textures ||
        !request.execute_frame_pass ||
        !request.unbind_textures ||
        !request.unbind_samplers) {
        return false;
    }

    request.bind_samplers();
    request.bind_textures();
    request.execute_frame_pass();
    request.unbind_textures();
    request.unbind_samplers();
    return true;
}

struct LegacyCanvasStrokeFaceDirtyRequest {
    pp::renderer::Extent2D extent {};
    glm::vec4 previous_accumulated_dirty_box {};
    glm::vec4 previous_pass_dirty_box {};
    glm::vec4 sample_dirty_box {};
    bool include_in_committed_dirty_box = true;
};

struct LegacyCanvasStrokeFaceDirtyResult {
    glm::vec4 accumulated_dirty_box {};
    glm::vec4 pass_dirty_box {};
    bool has_dirty_pixels = false;
    bool committed_dirty = false;
    bool pass_dirty = false;
};

struct LegacyCanvasStrokePadRegionRequest {
    pp::renderer::Extent2D extent {};
    glm::vec4 pass_dirty_box {};
};

struct LegacyCanvasStrokePadRegionResult {
    bool has_pixels = false;
    pp::paint_renderer::CanvasStrokeCopyRegion copy_region {};
    std::array<pp::paint_renderer::CanvasStrokePoint, 6> ndc_quad {};
};

struct LegacyCanvasStrokePadFace {
    int index = 0;
    bool dirty = false;
    glm::vec4 pass_dirty_box {};
};

struct LegacyCanvasStrokePadExecutionRequest {
    std::string_view context;
    pp::renderer::Extent2D extent {};
    std::span<const LegacyCanvasStrokePadFace> faces;
    bool copy_stroke_destination = false;
    std::function<void(std::span<const vertex_t>)> upload_pad_vertices;
    std::function<void(int)> begin_face;
    std::function<void(int)> bind_destination_texture;
    std::function<void(const pp::paint_renderer::CanvasStrokeCopyRegion&)> copy_framebuffer_to_destination_texture;
    std::function<void(int)> unbind_destination_texture;
    std::function<void()> draw_pad;
    std::function<void(int)> finish_face;
    std::function<void(int, const LegacyCanvasStrokePadRegionResult&, std::span<vertex_t>)> execute_face;
};

struct LegacyCanvasStrokePadExecutionResult {
    bool ok = false;
    std::size_t padded_faces = 0;
};

[[nodiscard]] inline LegacyCanvasStrokePadExecutionResult execute_legacy_canvas_stroke_pad_faces(
    const LegacyCanvasStrokePadExecutionRequest& request);

inline void execute_legacy_canvas_stroke_pad_copy_region(
    const pp::paint_renderer::CanvasStrokeCopyRegion& copy_region,
    const std::function<void(int, int, int, int, int, int)>& copy_framebuffer_to_texture_2d)
{
    copy_framebuffer_to_texture_2d(
        copy_region.x,
        copy_region.y,
        copy_region.x,
        copy_region.y,
        copy_region.width,
        copy_region.height);
}

struct LegacyCanvasStrokeMixPassPlane {
    int index = 0;
    bool visible = true;
    bool has_target = false;
    float opacity = 1.0f;
    glm::mat4 mvp { 1.0f };
};

struct LegacyCanvasStrokeMixPassRequest {
    std::string_view context;
    glm::vec2 resolution {};
    std::span<const LegacyCanvasStrokeMixPassPlane> planes;
    std::function<void()> bind_mix_samplers;
    std::function<void()> unbind_mix_samplers;
    std::function<void(int, const glm::mat4&)> setup_plane_shader;
    std::function<void(int)> bind_layer_texture;
    std::function<void(int)> bind_stroke_texture;
    std::function<void(int)> bind_mask_texture;
    std::function<void()> draw_plane;
    std::function<void(int)> unbind_mask_texture;
    std::function<void(int)> unbind_stroke_texture;
    std::function<void(int)> unbind_layer_texture;
};

struct LegacyCanvasStrokeMixPassResult {
    bool ok = false;
    std::size_t composed_planes = 0;
};

struct LegacyCanvasStrokeMixPassSetup {
    std::function<void()> begin;
    std::function<void()> end;
};

[[nodiscard]] inline LegacyCanvasStrokeMixPassSetup make_legacy_canvas_stroke_mix_pass_setup(
    std::function<void()> begin,
    std::function<void()> end)
{
    return LegacyCanvasStrokeMixPassSetup {
        .begin = std::move(begin),
        .end = std::move(end),
    };
}

[[nodiscard]] inline LegacyCanvasStrokeMixPassRequest make_legacy_canvas_stroke_mix_pass_request(
    std::string_view context,
    glm::vec2 resolution,
    std::span<const LegacyCanvasStrokeMixPassPlane> planes,
    std::function<void()> bind_mix_samplers,
    std::function<void()> unbind_mix_samplers,
    std::function<void(int, const glm::mat4&)> setup_plane_shader,
    std::function<void(int)> bind_layer_texture,
    std::function<void(int)> bind_stroke_texture,
    std::function<void(int)> bind_mask_texture,
    std::function<void()> draw_plane,
    std::function<void(int)> unbind_mask_texture,
    std::function<void(int)> unbind_stroke_texture,
    std::function<void(int)> unbind_layer_texture)
{
    return LegacyCanvasStrokeMixPassRequest {
        .context = context,
        .resolution = resolution,
        .planes = planes,
        .bind_mix_samplers = std::move(bind_mix_samplers),
        .unbind_mix_samplers = std::move(unbind_mix_samplers),
        .setup_plane_shader = std::move(setup_plane_shader),
        .bind_layer_texture = std::move(bind_layer_texture),
        .bind_stroke_texture = std::move(bind_stroke_texture),
        .bind_mask_texture = std::move(bind_mask_texture),
        .draw_plane = std::move(draw_plane),
        .unbind_mask_texture = std::move(unbind_mask_texture),
        .unbind_stroke_texture = std::move(unbind_stroke_texture),
        .unbind_layer_texture = std::move(unbind_layer_texture),
    };
}

struct LegacyCanvasStrokeMixPassShell {
    LegacyCanvasStrokeMixPassSetup setup;
    LegacyCanvasStrokeMixPassRequest request;
};

template <typename BeginMixPass, typename EndMixPass>
[[nodiscard]] inline LegacyCanvasStrokeMixPassShell make_legacy_canvas_stroke_mix_pass_shell(
    BeginMixPass&& begin_mix_pass,
    EndMixPass&& end_mix_pass,
    std::string_view context,
    glm::vec2 resolution,
    std::span<const LegacyCanvasStrokeMixPassPlane> planes,
    std::function<void()> bind_mix_samplers,
    std::function<void()> unbind_mix_samplers,
    std::function<void(int, const glm::mat4&)> setup_plane_shader,
    std::function<void(int)> bind_layer_texture,
    std::function<void(int)> bind_stroke_texture,
    std::function<void(int)> bind_mask_texture,
    std::function<void()> draw_plane,
    std::function<void(int)> unbind_mask_texture,
    std::function<void(int)> unbind_stroke_texture,
    std::function<void(int)> unbind_layer_texture)
{
    return LegacyCanvasStrokeMixPassShell {
        .setup = make_legacy_canvas_stroke_mix_pass_setup(
            std::forward<BeginMixPass>(begin_mix_pass),
            std::forward<EndMixPass>(end_mix_pass)),
        .request = make_legacy_canvas_stroke_mix_pass_request(
            context,
            resolution,
            planes,
            std::move(bind_mix_samplers),
            std::move(unbind_mix_samplers),
            std::move(setup_plane_shader),
            std::move(bind_layer_texture),
            std::move(bind_stroke_texture),
            std::move(bind_mask_texture),
            std::move(draw_plane),
            std::move(unbind_mask_texture),
            std::move(unbind_stroke_texture),
            std::move(unbind_layer_texture)),
    };
}

struct LegacyCanvasStrokeDualPassRequest {
    std::string_view context;
    std::function<void()> bind_brush_tip;
    std::function<void()> unbind_brush_tip;
    std::function<void()> setup_dual_shader;
    std::function<void()> execute_frame_pass;
};

struct LegacyCanvasStrokeDualPassResult {
    bool ok = false;
    std::size_t composed_planes = 0;
};

[[nodiscard]] inline LegacyCanvasStrokeMixPassResult execute_legacy_canvas_stroke_mix_pass(
    const LegacyCanvasStrokeMixPassRequest& request);

template <typename Frames, typename Framebuffers, typename BeginFrame, typename PrepareFace, typename ExecuteSample>
std::size_t execute_legacy_canvas_stroke_live_pass_with_face_framebuffers(
    Frames&& frames,
    pp::renderer::Extent2D extent,
    std::span<glm::vec4> accumulated_dirty_boxes,
    std::span<glm::vec4> pass_dirty_boxes,
    std::span<const bool> include_in_committed_dirty_box,
    BeginFrame&& begin_frame,
    PrepareFace&& prepare_face,
    ExecuteSample&& execute_sample,
    Framebuffers& face_framebuffers,
    bool preserve_sample_dirty_as_pass_dirty = false,
    std::span<bool> committed_dirty_faces = {},
    std::span<bool> pass_dirty_faces = {});

template <typename Frames, typename Framebuffers, typename BeginFrame, typename PrepareFace, typename ExecuteSample>
std::size_t execute_legacy_canvas_stroke_dual_pass_frame_callbacks(
    Frames&& frames,
    pp::renderer::Extent2D extent,
    std::span<glm::vec4> accumulated_dirty_boxes,
    std::span<glm::vec4> pass_dirty_boxes,
    std::span<const bool> include_in_committed_dirty_box,
    BeginFrame&& begin_frame,
    PrepareFace&& prepare_face,
    ExecuteSample&& execute_sample,
    Framebuffers& face_framebuffers,
    bool preserve_sample_dirty_as_pass_dirty = false,
    std::span<bool> committed_dirty_faces = {},
    std::span<bool> pass_dirty_faces = {})
{
    return execute_legacy_canvas_stroke_live_pass_with_face_framebuffers(
        std::forward<Frames>(frames),
        extent,
        accumulated_dirty_boxes,
        pass_dirty_boxes,
        include_in_committed_dirty_box,
        std::forward<BeginFrame>(begin_frame),
        std::forward<PrepareFace>(prepare_face),
        std::forward<ExecuteSample>(execute_sample),
        face_framebuffers,
        preserve_sample_dirty_as_pass_dirty,
        committed_dirty_faces,
        pass_dirty_faces);
}

template <typename Frames, typename Framebuffers, typename BeginFrame, typename PrepareFace, typename ExecuteSample>
std::size_t execute_legacy_canvas_stroke_main_pass_frame_callbacks(
    Frames&& frames,
    pp::renderer::Extent2D extent,
    std::span<glm::vec4> accumulated_dirty_boxes,
    std::span<glm::vec4> pass_dirty_boxes,
    std::span<const bool> include_in_committed_dirty_box,
    BeginFrame&& begin_frame,
    PrepareFace&& prepare_face,
    ExecuteSample&& execute_sample,
    Framebuffers& face_framebuffers,
    bool preserve_sample_dirty_as_pass_dirty = false,
    std::span<bool> committed_dirty_faces = {},
    std::span<bool> pass_dirty_faces = {})
{
    return execute_legacy_canvas_stroke_live_pass_with_face_framebuffers(
        std::forward<Frames>(frames),
        extent,
        accumulated_dirty_boxes,
        pass_dirty_boxes,
        include_in_committed_dirty_box,
        std::forward<BeginFrame>(begin_frame),
        std::forward<PrepareFace>(prepare_face),
        std::forward<ExecuteSample>(execute_sample),
        face_framebuffers,
        preserve_sample_dirty_as_pass_dirty,
        committed_dirty_faces,
        pass_dirty_faces);
}

template <typename Faces, typename UploadPadVertices, typename BeginFace, typename BindDestinationTexture, typename CopyFramebufferToDestinationTexture, typename UnbindDestinationTexture, typename DrawPad, typename FinishFace>
std::size_t execute_legacy_canvas_stroke_pad_face_callbacks(
    Faces&& faces,
    pp::renderer::Extent2D extent,
    bool copy_stroke_destination,
    UploadPadVertices&& upload_pad_vertices,
    BeginFace&& begin_face,
    BindDestinationTexture&& bind_destination_texture,
    CopyFramebufferToDestinationTexture&& copy_framebuffer_to_destination_texture,
    UnbindDestinationTexture&& unbind_destination_texture,
    DrawPad&& draw_pad,
    FinishFace&& finish_face)
{
    return execute_legacy_canvas_stroke_pad_faces(
        LegacyCanvasStrokePadExecutionRequest {
            .context = "Canvas::stroke_draw",
            .extent = extent,
            .faces = std::forward<Faces>(faces),
            .copy_stroke_destination = copy_stroke_destination,
            .upload_pad_vertices = std::forward<UploadPadVertices>(upload_pad_vertices),
            .begin_face = std::forward<BeginFace>(begin_face),
            .bind_destination_texture = std::forward<BindDestinationTexture>(bind_destination_texture),
            .copy_framebuffer_to_destination_texture = std::forward<CopyFramebufferToDestinationTexture>(copy_framebuffer_to_destination_texture),
            .unbind_destination_texture = std::forward<UnbindDestinationTexture>(unbind_destination_texture),
            .draw_pad = std::forward<DrawPad>(draw_pad),
            .finish_face = std::forward<FinishFace>(finish_face),
        }).padded_faces;
}

[[nodiscard]] inline LegacyCanvasStrokeDualPassResult execute_legacy_canvas_stroke_dual_pass(
    const LegacyCanvasStrokeDualPassRequest& request)
{
    LegacyCanvasStrokeDualPassResult result;
    if (!request.setup_dual_shader ||
        !request.bind_brush_tip ||
        !request.unbind_brush_tip ||
        !request.execute_frame_pass) {
        return result;
    }

    request.setup_dual_shader();
    request.bind_brush_tip();
    request.execute_frame_pass();
    request.unbind_brush_tip();
    result.ok = true;
    return result;
}

inline void setup_legacy_canvas_stroke_dual_shader(bool uses_pattern)
{
    setup_legacy_stroke_dual_shader(uses_pattern);
}

template <std::size_t PlaneCount, typename HasTarget>
[[nodiscard]] inline std::array<LegacyCanvasStrokeMixPassPlane, PlaneCount>
plan_legacy_canvas_stroke_mix_pass_planes(
    bool visible,
    float opacity,
    const glm::mat4& mvp_base,
    const std::array<glm::mat4, PlaneCount>& plane_transforms,
    HasTarget&& has_target,
    const glm::mat4& plane_offset = glm::translate(glm::vec3(0.0f, 0.0f, -1.0f)))
{
    std::array<LegacyCanvasStrokeMixPassPlane, PlaneCount> planes {};
    for (std::size_t plane_index = 0; plane_index < PlaneCount; ++plane_index) {
        planes[plane_index] = LegacyCanvasStrokeMixPassPlane {
            .index = static_cast<int>(plane_index),
            .visible = visible,
            .has_target = static_cast<bool>(has_target(static_cast<int>(plane_index))),
            .opacity = opacity,
            .mvp = mvp_base * plane_transforms[plane_index] * plane_offset,
        };
    }
    return planes;
}

template <typename BeginMixPass, typename EndMixPass>
[[nodiscard]] inline LegacyCanvasStrokeMixPassResult
execute_legacy_canvas_stroke_mix_pass_with_setup(
    BeginMixPass&& begin_mix_pass,
    EndMixPass&& end_mix_pass,
    const LegacyCanvasStrokeMixPassRequest& request)
{
    begin_mix_pass();
    const auto result = execute_legacy_canvas_stroke_mix_pass(request);
    end_mix_pass();
    return result;
}

template <typename BeginMixPass, typename EndMixPass>
[[nodiscard]] inline LegacyCanvasStrokeMixPassResult
execute_legacy_canvas_stroke_mix_pass_shell(
    BeginMixPass&& begin_mix_pass,
    EndMixPass&& end_mix_pass,
    const LegacyCanvasStrokeMixPassRequest& request)
{
    return execute_legacy_canvas_stroke_mix_pass_with_setup(
        std::forward<BeginMixPass>(begin_mix_pass),
        std::forward<EndMixPass>(end_mix_pass),
        request);
}

struct LegacyCanvasStrokeComputeRequest {
    StrokeSample previous_sample {};
    std::span<const StrokeSample> samples;
    float zoom = 1.0f;
    glm::vec2 mixer_size {};
    glm::mat4 model_view { 1.0f };
};

template <typename ProjectStroke, typename MakeFrame>
[[nodiscard]] auto plan_legacy_canvas_stroke_frames(
    const LegacyCanvasStrokeComputeRequest& request,
    ProjectStroke&& project_stroke,
    MakeFrame&& make_frame)
{
    using StrokeFrame = decltype(make_frame(
        std::declval<glm::vec4>(),
        std::declval<glm::vec4>(),
        0.0f,
        0.0f,
        project_stroke(std::declval<std::array<vertex_t, 4>&>(), false, request.model_view)));

    std::vector<StrokeFrame> frames;
    StrokeSample prev = request.previous_sample;
    std::array<vertex_t, 4> brush_quad = {
        vertex_t{ {0, 0, 1, 1}, {0, 0}, {0, 0} },
        vertex_t{ {0, 0, 1, 1}, {0, 1}, {0, 1} },
        vertex_t{ {0, 0, 1, 1}, {1, 1}, {1, 1} },
        vertex_t{ {0, 0, 1, 1}, {1, 0}, {1, 0} },
    };

    for (const auto& sample : request.samples) {
        if (!sample.valid()) {
            continue;
        }

        const glm::vec2 mixer_dx(prev.size * 0.5f, 0);
        const glm::vec2 mixer_dy(0, prev.size * 0.5f);
        const glm::vec2 mixer_offsets[4] = {
            -mixer_dx - mixer_dy,
            -mixer_dx + mixer_dy,
            +mixer_dx + mixer_dy,
            +mixer_dx - mixer_dy,
        };

        const glm::vec2 brush_dx(sample.size * 0.5f, 0);
        const glm::vec2 brush_dy(0, sample.size * 0.5f);
        const glm::vec2 brush_offsets[4] = {
            -brush_dx - brush_dy,
            -brush_dx + brush_dy,
            +brush_dx + brush_dy,
            +brush_dx - brush_dy,
        };

        glm::vec2 mixer_bb_min(request.mixer_size);
        glm::vec2 mixer_bb_max(0, 0);
        for (int i = 0; i < 4; ++i) {
            const auto mixer_point =
                xy(prev.pos) + request.zoom * sample.scale * mixer_offsets[i] * glm::orientate2(-sample.angle);
            mixer_bb_min = glm::max(glm::vec2(0, 0), glm::min(mixer_bb_min, mixer_point));
            mixer_bb_max = glm::min(request.mixer_size, glm::max(mixer_bb_max, mixer_point));

            if (sample.pos.z == 0.0f) {
                brush_quad[i].pos = glm::vec4(
                    xy(sample.pos) +
                        request.zoom * sample.scale * brush_offsets[i] * glm::orientate2(-sample.angle) -
                        glm::vec2(0, 1),
                    1,
                    1);
            } else {
                const auto inverse_view = glm::inverse(glm::lookAt({ 0, 0, 0 }, sample.pos, { 0, 1, 0 }));
                const glm::vec3 offset_3d = inverse_view * glm::vec4(brush_offsets[i], 0, 1);
                brush_quad[i].pos = glm::vec4(sample.pos + offset_3d, 1.0f);
            }
            brush_quad[i].uvs2 = mixer_point / request.mixer_size;
        }

        const auto mixer_rect =
            glm::vec4(glm::floor(mixer_bb_min), glm::ceil(mixer_bb_max - mixer_bb_min)) / request.zoom;
        auto shapes = sample.pos.z == 0.0f
            ? project_stroke(brush_quad, false, request.model_view)
            : project_stroke(brush_quad, true, glm::lookAt({ 0, 0, 0 }, sample.pos, { 0, 1, 0 }));
        frames.push_back(make_frame(mixer_rect, glm::vec4(sample.col, 1), sample.flow, sample.opacity, std::move(shapes)));

        prev = sample;
    }

    return frames;
}

template <typename Frames, typename BeginFrame, typename ExecuteFace>
std::size_t execute_legacy_canvas_stroke_frame_faces(
    Frames&& frames,
    BeginFrame&& begin_frame,
    ExecuteFace&& execute_face)
{
    std::size_t executed_faces = 0;
    for (auto& frame : frames) {
        begin_frame(frame);
        for (int face_index = 0; face_index < 6; ++face_index) {
            auto& vertices = frame.shapes[face_index];
            if (vertices.size() < 3) {
                continue;
            }
            execute_face(frame, face_index, vertices);
            ++executed_faces;
        }
    }
    return executed_faces;
}

template <typename BindTextureInput, std::size_t BindingCount>
requires std::invocable<BindTextureInput&, LegacyCanvasStrokeTextureInput, int>
inline void bind_legacy_canvas_stroke_texture_inputs(
    const std::array<LegacyCanvasStrokeTextureBinding, BindingCount>& bindings,
    BindTextureInput&& bind_texture_input)
{
    for (const auto& binding : bindings) {
        bind_texture_input(binding.input, binding.slot);
    }
}

template <typename UnbindTextureInput, std::size_t BindingCount>
requires std::invocable<UnbindTextureInput&, LegacyCanvasStrokeTextureInput, int>
inline void unbind_legacy_canvas_stroke_texture_inputs(
    const std::array<LegacyCanvasStrokeTextureBinding, BindingCount>& bindings,
    UnbindTextureInput&& unbind_texture_input)
{
    for (const auto& binding : bindings) {
        unbind_texture_input(binding.input, binding.slot);
    }
}

inline void bind_legacy_canvas_stroke_texture_input(
    LegacyCanvasStrokeTextureInput input,
    const LegacyCanvasStrokeTextureInputDispatch& dispatch)
{
    switch (input) {
    case LegacyCanvasStrokeTextureInput::brush_tip:
        if (dispatch.bind_brush_tip) {
            dispatch.bind_brush_tip();
        }
        break;
    case LegacyCanvasStrokeTextureInput::stroke_destination:
        if (dispatch.bind_stroke_destination) {
            dispatch.bind_stroke_destination();
        }
        break;
    case LegacyCanvasStrokeTextureInput::pattern:
        if (dispatch.bind_pattern) {
            dispatch.bind_pattern();
        }
        break;
    case LegacyCanvasStrokeTextureInput::mixer:
        if (dispatch.bind_mixer) {
            dispatch.bind_mixer();
        }
        break;
    }
}

inline void unbind_legacy_canvas_stroke_texture_input(
    LegacyCanvasStrokeTextureInput input,
    const LegacyCanvasStrokeTextureInputDispatch& dispatch)
{
    switch (input) {
    case LegacyCanvasStrokeTextureInput::brush_tip:
        if (dispatch.unbind_brush_tip) {
            dispatch.unbind_brush_tip();
        }
        break;
    case LegacyCanvasStrokeTextureInput::stroke_destination:
        if (dispatch.unbind_stroke_destination) {
            dispatch.unbind_stroke_destination();
        }
        break;
    case LegacyCanvasStrokeTextureInput::pattern:
        if (dispatch.unbind_pattern) {
            dispatch.unbind_pattern();
        }
        break;
    case LegacyCanvasStrokeTextureInput::mixer:
        if (dispatch.unbind_mixer) {
            dispatch.unbind_mixer();
        }
        break;
    }
}

inline void bind_legacy_canvas_stroke_sampler_input(
    LegacyCanvasStrokeTextureInput input,
    int slot,
    const LegacyCanvasStrokeSamplerDispatch& dispatch)
{
    switch (input) {
    case LegacyCanvasStrokeTextureInput::brush_tip:
        if (dispatch.bind_brush_tip_sampler) {
            dispatch.bind_brush_tip_sampler(slot);
        }
        break;
    case LegacyCanvasStrokeTextureInput::stroke_destination:
        if (dispatch.bind_stroke_destination_sampler) {
            dispatch.bind_stroke_destination_sampler(slot);
        }
        break;
    case LegacyCanvasStrokeTextureInput::pattern:
        if (dispatch.bind_pattern_sampler) {
            dispatch.bind_pattern_sampler(slot);
        }
        break;
    case LegacyCanvasStrokeTextureInput::mixer:
        if (dispatch.bind_mixer_sampler) {
            dispatch.bind_mixer_sampler(slot);
        }
        break;
    }
}

inline void unbind_legacy_canvas_stroke_sampler_input(
    LegacyCanvasStrokeTextureInput input,
    const LegacyCanvasStrokeSamplerDispatch& dispatch)
{
    switch (input) {
    case LegacyCanvasStrokeTextureInput::brush_tip:
        if (dispatch.unbind_brush_tip_sampler) {
            dispatch.unbind_brush_tip_sampler();
        }
        break;
    case LegacyCanvasStrokeTextureInput::stroke_destination:
        if (dispatch.unbind_stroke_destination_sampler) {
            dispatch.unbind_stroke_destination_sampler();
        }
        break;
    case LegacyCanvasStrokeTextureInput::pattern:
        if (dispatch.unbind_pattern_sampler) {
            dispatch.unbind_pattern_sampler();
        }
        break;
    case LegacyCanvasStrokeTextureInput::mixer:
        if (dispatch.unbind_mixer_sampler) {
            dispatch.unbind_mixer_sampler();
        }
        break;
    }
}

template <std::size_t BindingCount>
inline void bind_legacy_canvas_stroke_texture_inputs(
    const std::array<LegacyCanvasStrokeTextureBinding, BindingCount>& bindings,
    const LegacyCanvasStrokeTextureInputDispatch& dispatch)
{
    for (const auto& binding : bindings) {
        if (dispatch.activate_texture_unit) {
            dispatch.activate_texture_unit(binding.slot);
        }
        bind_legacy_canvas_stroke_texture_input(binding.input, dispatch);
    }
}

template <std::size_t BindingCount>
inline void unbind_legacy_canvas_stroke_texture_inputs(
    const std::array<LegacyCanvasStrokeTextureBinding, BindingCount>& bindings,
    const LegacyCanvasStrokeTextureInputDispatch& dispatch)
{
    for (const auto& binding : bindings) {
        if (dispatch.activate_texture_unit) {
            dispatch.activate_texture_unit(binding.slot);
        }
        unbind_legacy_canvas_stroke_texture_input(binding.input, dispatch);
    }
}

template <std::size_t BindingCount>
inline void bind_legacy_canvas_stroke_sampler_inputs(
    const std::array<LegacyCanvasStrokeTextureBinding, BindingCount>& bindings,
    const LegacyCanvasStrokeSamplerDispatch& dispatch)
{
    for (const auto& binding : bindings) {
        bind_legacy_canvas_stroke_sampler_input(binding.input, binding.slot, dispatch);
    }
}

template <std::size_t BindingCount>
inline void unbind_legacy_canvas_stroke_sampler_inputs(
    const std::array<LegacyCanvasStrokeTextureBinding, BindingCount>& bindings,
    const LegacyCanvasStrokeSamplerDispatch& dispatch)
{
    for (const auto& binding : bindings) {
        unbind_legacy_canvas_stroke_sampler_input(binding.input, dispatch);
    }
}

template <typename Frames, typename BeginFrame, typename BeginFace, typename ExecuteSample, typename FinishFace>
std::size_t execute_legacy_canvas_stroke_frame_samples(
    Frames&& frames,
    BeginFrame&& begin_frame,
    BeginFace&& begin_face,
    ExecuteSample&& execute_sample,
    FinishFace&& finish_face)
{
    std::size_t executed_faces = 0;
    for (auto& frame : frames) {
        begin_frame(frame);
        for (int face_index = 0; face_index < 6; ++face_index) {
            auto& vertices = frame.shapes[face_index];
            if (vertices.size() < 3) {
                continue;
            }

            begin_face(frame, face_index, vertices);
            auto sample_dirty_box = execute_sample(frame, face_index, vertices);
            finish_face(frame, face_index, sample_dirty_box);
            ++executed_faces;
        }
    }
    return executed_faces;
}

template <
    typename Frames,
    typename BeginFrame,
    typename BeginFace,
    typename ExecuteSample,
    typename PrepareDirtyRequest,
    typename FinishFace>
std::size_t execute_legacy_canvas_stroke_frame_samples_with_dirty_tracking(
    Frames&& frames,
    pp::renderer::Extent2D extent,
    std::span<glm::vec4> accumulated_dirty_boxes,
    std::span<glm::vec4> pass_dirty_boxes,
    std::span<const bool> include_in_committed_dirty_box,
    BeginFrame&& begin_frame,
    BeginFace&& begin_face,
    ExecuteSample&& execute_sample,
    PrepareDirtyRequest&& prepare_dirty_request,
    FinishFace&& finish_face,
    std::span<bool> committed_dirty_faces = {},
    std::span<bool> pass_dirty_faces = {})
{
    std::size_t executed_faces = 0;
    for (auto& frame : frames) {
        begin_frame(frame);
        for (int face_index = 0; face_index < 6; ++face_index) {
            auto& vertices = frame.shapes[face_index];
            if (vertices.size() < 3) {
                continue;
            }

            const glm::vec4 previous_accumulated_dirty_box =
                face_index < accumulated_dirty_boxes.size()
                ? accumulated_dirty_boxes[face_index]
                : glm::vec4(0.0f);
            const glm::vec4 previous_pass_dirty_box =
                face_index < pass_dirty_boxes.size()
                ? pass_dirty_boxes[face_index]
                : glm::vec4(0.0f);
            const bool include_dirty_box =
                face_index < include_in_committed_dirty_box.size()
                ? include_in_committed_dirty_box[face_index]
                : true;

            glm::vec4* accumulated_dirty_box =
                face_index < accumulated_dirty_boxes.size()
                ? &accumulated_dirty_boxes[face_index]
                : nullptr;
            glm::vec4* pass_dirty_box =
                face_index < pass_dirty_boxes.size()
                ? &pass_dirty_boxes[face_index]
                : nullptr;
            bool* committed_dirty =
                face_index < committed_dirty_faces.size()
                ? &committed_dirty_faces[face_index]
                : nullptr;
            bool* pass_dirty =
                face_index < pass_dirty_faces.size()
                ? &pass_dirty_faces[face_index]
                : nullptr;

            execute_legacy_canvas_stroke_face_sample(
                LegacyCanvasStrokeFaceDirtyRequest {
                    .extent = extent,
                    .previous_accumulated_dirty_box = previous_accumulated_dirty_box,
                    .previous_pass_dirty_box = previous_pass_dirty_box,
                    .include_in_committed_dirty_box = include_dirty_box,
                },
                [&] {
                    return execute_sample(frame, face_index, vertices);
                },
                [&] {
                    begin_face(frame, face_index, vertices);
                },
                [&](auto& request) {
                    prepare_dirty_request(frame, face_index, vertices, request);
                },
                [&](glm::vec4 sample_dirty_box) {
                    finish_face(frame, face_index, vertices, sample_dirty_box);
                },
                accumulated_dirty_box,
                pass_dirty_box,
                committed_dirty,
                pass_dirty);
            ++executed_faces;
        }
    }
    return executed_faces;
}

template <typename Frames, typename BeginFrame, typename BeginFace, typename ExecuteSample, typename FinishFace>
std::size_t execute_legacy_canvas_stroke_live_pass_with_dirty_tracking(
    Frames&& frames,
    pp::renderer::Extent2D extent,
    std::span<glm::vec4> accumulated_dirty_boxes,
    std::span<glm::vec4> pass_dirty_boxes,
    std::span<const bool> include_in_committed_dirty_box,
    BeginFrame&& begin_frame,
    BeginFace&& begin_face,
    ExecuteSample&& execute_sample,
    FinishFace&& finish_face,
    bool preserve_sample_dirty_as_pass_dirty = false,
    std::span<bool> committed_dirty_faces = {},
    std::span<bool> pass_dirty_faces = {})
{
    return execute_legacy_canvas_stroke_frame_samples_with_dirty_tracking(
        std::forward<Frames>(frames),
        extent,
        accumulated_dirty_boxes,
        pass_dirty_boxes,
        include_in_committed_dirty_box,
        std::forward<BeginFrame>(begin_frame),
        std::forward<BeginFace>(begin_face),
        std::forward<ExecuteSample>(execute_sample),
        [&](auto&, int, auto&, auto& request) {
            if (preserve_sample_dirty_as_pass_dirty) {
                request.previous_pass_dirty_box = request.sample_dirty_box;
            }
        },
        [&](auto& frame, int face_index, auto& vertices, glm::vec4 sample_dirty_box) {
            finish_face(frame, face_index, vertices, sample_dirty_box);
        },
        committed_dirty_faces,
        pass_dirty_faces);
}

template <typename Frames, typename Framebuffers, typename BeginFrame, typename PrepareFace, typename ExecuteSample>
std::size_t execute_legacy_canvas_stroke_live_pass_with_face_framebuffers(
    Frames&& frames,
    pp::renderer::Extent2D extent,
    std::span<glm::vec4> accumulated_dirty_boxes,
    std::span<glm::vec4> pass_dirty_boxes,
    std::span<const bool> include_in_committed_dirty_box,
    BeginFrame&& begin_frame,
    PrepareFace&& prepare_face,
    ExecuteSample&& execute_sample,
    Framebuffers& face_framebuffers,
    bool preserve_sample_dirty_as_pass_dirty,
    std::span<bool> committed_dirty_faces,
    std::span<bool> pass_dirty_faces)
{
    return execute_legacy_canvas_stroke_live_pass_with_dirty_tracking(
        std::forward<Frames>(frames),
        extent,
        accumulated_dirty_boxes,
        pass_dirty_boxes,
        include_in_committed_dirty_box,
        std::forward<BeginFrame>(begin_frame),
        [&](auto& frame, int face_index, auto& vertices) {
            prepare_face(frame, face_index, vertices);
            face_framebuffers[face_index].bindFramebuffer();
        },
        std::forward<ExecuteSample>(execute_sample),
        [&](auto&, int face_index, auto&, glm::vec4) {
            face_framebuffers[face_index].unbindFramebuffer();
        },
        preserve_sample_dirty_as_pass_dirty,
        committed_dirty_faces,
        pass_dirty_faces);
}

template <
    typename SetupCompositeShader,
    typename BindCompositeSamplers,
    typename BindCompositeTextures,
    typename DrawComposite,
    typename UnbindCompositeTextures>
void execute_legacy_canvas_stroke_temporary_composite(
    SetupCompositeShader&& setup_composite_shader,
    BindCompositeSamplers&& bind_composite_samplers,
    BindCompositeTextures&& bind_composite_textures,
    DrawComposite&& draw_composite,
    UnbindCompositeTextures&& unbind_composite_textures)
{
    setup_composite_shader();
    bind_composite_samplers();
    bind_composite_textures();
    draw_composite();
    unbind_composite_textures();
}

[[nodiscard]] inline pp::paint_renderer::CanvasStrokeBox legacy_canvas_stroke_box(glm::vec4 box) noexcept
{
    return pp::paint_renderer::CanvasStrokeBox {
        .min_x = box.x,
        .min_y = box.y,
        .max_x = box.z,
        .max_y = box.w,
    };
}

[[nodiscard]] inline glm::vec4 legacy_canvas_stroke_glm_box(
    pp::paint_renderer::CanvasStrokeBox box) noexcept
{
    return glm::vec4(box.min_x, box.min_y, box.max_x, box.max_y);
}

[[nodiscard]] inline LegacyCanvasStrokePadRegionResult plan_legacy_canvas_stroke_pad_region(
    const LegacyCanvasStrokePadRegionRequest& request) noexcept
{
    const auto plan = pp::paint_renderer::plan_canvas_stroke_pad_region(
        pp::paint_renderer::CanvasStrokePadRegionRequest {
            .extent = request.extent,
            .pass_dirty_box = legacy_canvas_stroke_box(request.pass_dirty_box),
        });

    return LegacyCanvasStrokePadRegionResult {
        .has_pixels = plan.has_pixels,
        .copy_region = plan.copy_region,
        .ndc_quad = plan.ndc_quad,
    };
}

[[nodiscard]] inline LegacyCanvasStrokeFaceDirtyResult plan_legacy_canvas_stroke_face_dirty_update(
    const LegacyCanvasStrokeFaceDirtyRequest& request) noexcept
{
    const auto plan = pp::paint_renderer::plan_canvas_stroke_face_dirty_update(
        pp::paint_renderer::CanvasStrokeFaceDirtyUpdateRequest {
            .extent = request.extent,
            .previous_accumulated_dirty_box =
                legacy_canvas_stroke_box(request.previous_accumulated_dirty_box),
            .previous_pass_dirty_box = legacy_canvas_stroke_box(request.previous_pass_dirty_box),
            .sample_dirty_box = legacy_canvas_stroke_box(request.sample_dirty_box),
            .include_in_committed_dirty_box = request.include_in_committed_dirty_box,
        });

    return LegacyCanvasStrokeFaceDirtyResult {
        .accumulated_dirty_box = legacy_canvas_stroke_glm_box(plan.accumulated_dirty_box),
        .pass_dirty_box = legacy_canvas_stroke_glm_box(plan.pass_dirty_box),
        .has_dirty_pixels = plan.has_dirty_pixels,
        .committed_dirty = plan.committed_dirty,
        .pass_dirty = plan.pass_dirty,
    };
}

template <std::size_t FaceCount>
[[nodiscard]] inline std::array<LegacyCanvasStrokePadFace, FaceCount> make_legacy_canvas_stroke_pad_faces(
    const std::array<bool, FaceCount>& dirty_faces,
    const std::array<glm::vec4, FaceCount>& pass_dirty_boxes) noexcept
{
    std::array<LegacyCanvasStrokePadFace, FaceCount> faces {};
    for (std::size_t face_index = 0; face_index < FaceCount; ++face_index) {
        faces[face_index].index = static_cast<int>(face_index);
        faces[face_index].dirty = dirty_faces[face_index];
        faces[face_index].pass_dirty_box = pass_dirty_boxes[face_index];
    }
    return faces;
}

[[nodiscard]] inline LegacyCanvasStrokeFaceDirtyResult apply_legacy_canvas_stroke_face_dirty_update(
    const LegacyCanvasStrokeFaceDirtyRequest& request,
    glm::vec4& accumulated_dirty_box,
    glm::vec4* pass_dirty_box = nullptr,
    bool* committed_dirty = nullptr,
    bool* pass_dirty = nullptr) noexcept
{
    const auto result = plan_legacy_canvas_stroke_face_dirty_update(request);
    accumulated_dirty_box = result.accumulated_dirty_box;
    if (pass_dirty_box) {
        *pass_dirty_box = result.pass_dirty_box;
    }
    if (committed_dirty) {
        *committed_dirty = result.committed_dirty;
    }
    if (pass_dirty) {
        *pass_dirty = result.pass_dirty;
    }
    return result;
}

template <typename ExecuteSample, typename BeginFace, typename PrepareDirtyRequest, typename FinishFace>
[[nodiscard]] inline LegacyCanvasStrokeFaceDirtyResult execute_legacy_canvas_stroke_face_sample(
    const LegacyCanvasStrokeFaceDirtyRequest& dirty_request,
    ExecuteSample&& execute_sample,
    BeginFace&& begin_face,
    PrepareDirtyRequest&& prepare_dirty_request,
    FinishFace&& finish_face,
    glm::vec4* accumulated_dirty_box = nullptr,
    glm::vec4* pass_dirty_box = nullptr,
    bool* committed_dirty = nullptr,
    bool* pass_dirty = nullptr) noexcept(noexcept(execute_sample()))
{
    LegacyCanvasStrokeFaceDirtyResult result;
    begin_face();

    auto request = dirty_request;
    request.sample_dirty_box = execute_sample();
    prepare_dirty_request(request);

    finish_face(request.sample_dirty_box);

    glm::vec4 accumulated = request.previous_accumulated_dirty_box;
    result = apply_legacy_canvas_stroke_face_dirty_update(
        request,
        accumulated,
        pass_dirty_box,
        committed_dirty,
        pass_dirty);
    result.accumulated_dirty_box = accumulated;
    if (accumulated_dirty_box) {
        *accumulated_dirty_box = accumulated;
    }
    return result;
}

[[nodiscard]] inline LegacyCanvasStrokePadExecutionResult execute_legacy_canvas_stroke_pad_faces(
    const LegacyCanvasStrokePadExecutionRequest& request)
{
    LegacyCanvasStrokePadExecutionResult result;
    if (request.extent.width == 0U ||
        request.extent.height == 0U ||
        (!request.execute_face &&
            (!request.upload_pad_vertices ||
                !request.begin_face ||
                !request.draw_pad ||
                !request.finish_face))) {
        return result;
    }

    for (const auto& face : request.faces) {
        if (!face.dirty) {
            continue;
        }

        const auto pad_region = plan_legacy_canvas_stroke_pad_region(
            LegacyCanvasStrokePadRegionRequest {
                .extent = request.extent,
                .pass_dirty_box = face.pass_dirty_box,
            });
        if (!pad_region.has_pixels) {
            continue;
        }

        std::array<vertex_t, 6> pad_quad = {
            vertex_t({ pad_region.ndc_quad[0].x, pad_region.ndc_quad[0].y }),
            vertex_t({ pad_region.ndc_quad[1].x, pad_region.ndc_quad[1].y }),
            vertex_t({ pad_region.ndc_quad[2].x, pad_region.ndc_quad[2].y }),
            vertex_t({ pad_region.ndc_quad[3].x, pad_region.ndc_quad[3].y }),
            vertex_t({ pad_region.ndc_quad[4].x, pad_region.ndc_quad[4].y }),
            vertex_t({ pad_region.ndc_quad[5].x, pad_region.ndc_quad[5].y }),
        };

        if (request.execute_face) {
            request.execute_face(face.index, pad_region, pad_quad);
        } else {
            if (!request.upload_pad_vertices ||
                !request.begin_face ||
                !request.draw_pad ||
                !request.finish_face) {
                return result;
            }
            if (request.copy_stroke_destination &&
                (!request.bind_destination_texture ||
                    !request.copy_framebuffer_to_destination_texture ||
                    !request.unbind_destination_texture)) {
                return result;
            }

            request.upload_pad_vertices(pad_quad);
            request.begin_face(face.index);
            if (request.copy_stroke_destination) {
                request.bind_destination_texture(face.index);
                request.copy_framebuffer_to_destination_texture(pad_region.copy_region);
            }
            request.draw_pad();
            if (request.copy_stroke_destination) {
                request.unbind_destination_texture(face.index);
            }
            request.finish_face(face.index);
        }
        ++result.padded_faces;
    }

    result.ok = true;
    return result;
}

[[nodiscard]] inline LegacyStrokeSampleExecutionResult execute_legacy_canvas_stroke_sample(
    const LegacyStrokeSampleExecutionRequest& request)
{
    LegacyStrokeSampleExecutionResult result;
    if (request.target_size.x <= 0.0f ||
        request.target_size.y <= 0.0f ||
        request.vertices.empty() ||
        !request.upload_brush_vertices ||
        !request.draw_brush_shape) {
        return result;
    }

    if (request.copy_stroke_destination) {
        if (!request.bind_destination_texture ||
            !request.copy_framebuffer_to_destination_texture ||
            !request.unbind_destination_texture) {
            return result;
        }
        request.bind_destination_texture();
    }

    const auto sample_bounds = pp::paint_renderer::plan_canvas_stroke_sample_bounds(
        pp::paint_renderer::CanvasStrokeSampleBoundsRequest {
            .extent = pp::renderer::Extent2D {
                .width = static_cast<std::uint32_t>(request.target_size.x),
                .height = static_cast<std::uint32_t>(request.target_size.y),
            },
            .vertices = request.sample_points,
        });
    if (!sample_bounds.has_pixels) {
        if (request.copy_stroke_destination) {
            request.unbind_destination_texture();
        }
        return result;
    }

    result.copy_position = glm::ivec2(sample_bounds.copy_region.x, sample_bounds.copy_region.y);
    result.copy_size = glm::ivec2(sample_bounds.copy_region.width, sample_bounds.copy_region.height);
    result.dirty_bounds = glm::vec4(
        sample_bounds.dirty_bounds.min_x,
        sample_bounds.dirty_bounds.min_y,
        sample_bounds.dirty_bounds.max_x,
        sample_bounds.dirty_bounds.max_y);

    if (request.copy_stroke_destination) {
        request.copy_framebuffer_to_destination_texture(
            result.copy_position.x,
            result.copy_position.y,
            result.copy_position.x,
            result.copy_position.y,
            result.copy_size.x,
            result.copy_size.y);
    }

    if (request.vertices.size() == 4) {
        std::array<vertex_t, 6> rect {
            request.vertices[0],
            request.vertices[1],
            request.vertices[2],
            request.vertices[0],
            request.vertices[2],
            request.vertices[3],
        };
        request.upload_brush_vertices(rect);
    } else {
        request.upload_brush_vertices(request.vertices);
    }

    request.draw_brush_shape();

    if (request.copy_stroke_destination) {
        request.unbind_destination_texture();
    }

    result.ok = true;
    return result;
}

[[nodiscard]] inline LegacyStrokeSampleExecutionResult execute_legacy_canvas_stroke_sample_polygon(
    const LegacyStrokeSamplePolygonExecutionRequest& request)
{
    std::vector<vertex_t> sample_vertices(
        request.polygon_vertices.begin(),
        request.polygon_vertices.end());
    if (sample_vertices.size() != 3 && sample_vertices.size() != 4) {
        sample_vertices = triangulate_simple(sample_vertices);
    }

    std::vector<pp::paint_renderer::CanvasStrokePoint> sample_points;
    sample_points.reserve(sample_vertices.size());
    for (const auto& vertex : sample_vertices) {
        sample_points.push_back(pp::paint_renderer::CanvasStrokePoint {
            .x = vertex.pos.x,
            .y = vertex.pos.y,
        });
    }

    return execute_legacy_canvas_stroke_sample(
        LegacyStrokeSampleExecutionRequest {
            .context = request.context,
            .target_size = request.target_size,
            .vertices = sample_vertices,
            .sample_points = sample_points,
            .copy_stroke_destination = request.copy_stroke_destination,
            .bind_destination_texture = request.bind_destination_texture,
            .copy_framebuffer_to_destination_texture =
                request.copy_framebuffer_to_destination_texture,
            .unbind_destination_texture = request.unbind_destination_texture,
            .upload_brush_vertices = request.upload_brush_vertices,
            .draw_brush_shape = request.draw_brush_shape,
        });
}

[[nodiscard]] inline LegacyStrokeSampleExecutionResult execute_legacy_canvas_stroke_face_sample_polygon(
    const LegacyStrokeFaceSamplePolygonExecutionRequest& request)
{
    if (!request.upload_brush_vertices || !request.draw_brush_shape) {
        return {};
    }

    if (request.copy_stroke_destination &&
        (!request.bind_destination_texture ||
            !request.copy_framebuffer_to_destination_texture ||
            !request.unbind_destination_texture)) {
        return {};
    }

    return execute_legacy_canvas_stroke_sample_polygon(
        LegacyStrokeSamplePolygonExecutionRequest {
            .context = request.context,
            .target_size = request.target_size,
            .polygon_vertices = request.polygon_vertices,
            .copy_stroke_destination = request.copy_stroke_destination,
            .bind_destination_texture = [&] {
                request.bind_destination_texture(request.face_index);
            },
            .copy_framebuffer_to_destination_texture =
                [&](int src_x, int src_y, int dst_x, int dst_y, int width, int height) {
                    request.copy_framebuffer_to_destination_texture(
                        request.face_index,
                        src_x,
                        src_y,
                        dst_x,
                        dst_y,
                        width,
                        height);
                },
            .unbind_destination_texture = [&] {
                request.unbind_destination_texture(request.face_index);
            },
            .upload_brush_vertices = [&](std::span<const vertex_t> vertices) {
                request.upload_brush_vertices(request.face_index, vertices);
            },
            .draw_brush_shape = [&] {
                request.draw_brush_shape(request.face_index);
            },
        });
}

template <std::size_t BindingCount>
[[nodiscard]] inline LegacyStrokeSampleExecutionResult execute_legacy_canvas_stroke_face_sample_polygon(
    const LegacyStrokeFaceSamplePolygonExecutionRequest& request,
    const std::array<LegacyCanvasStrokeTextureBinding, BindingCount>& destination_texture_bindings,
    const LegacyCanvasStrokeTextureInputDispatch& destination_texture_dispatch)
{
    return execute_legacy_canvas_stroke_face_sample_polygon(
        LegacyStrokeFaceSamplePolygonExecutionRequest {
            .context = request.context,
            .target_size = request.target_size,
            .polygon_vertices = request.polygon_vertices,
            .face_index = request.face_index,
            .copy_stroke_destination = request.copy_stroke_destination,
            .bind_destination_texture = [&](int) {
                bind_legacy_canvas_stroke_texture_inputs(
                    destination_texture_bindings,
                    destination_texture_dispatch);
            },
            .copy_framebuffer_to_destination_texture =
                request.copy_framebuffer_to_destination_texture,
            .unbind_destination_texture = [&](int) {
                unbind_legacy_canvas_stroke_texture_inputs(
                    destination_texture_bindings,
                    destination_texture_dispatch);
            },
            .upload_brush_vertices = request.upload_brush_vertices,
            .draw_brush_shape = request.draw_brush_shape,
        });
}

[[nodiscard]] inline LegacyCanvasStrokeMixPassResult execute_legacy_canvas_stroke_mix_pass(
    const LegacyCanvasStrokeMixPassRequest& request)
{
    LegacyCanvasStrokeMixPassResult result;
    if (request.resolution.x <= 0.0f ||
        request.resolution.y <= 0.0f ||
        !request.setup_plane_shader ||
        !request.bind_layer_texture ||
        !request.bind_stroke_texture ||
        !request.bind_mask_texture ||
        !request.draw_plane ||
        !request.unbind_mask_texture ||
        !request.unbind_stroke_texture ||
        !request.unbind_layer_texture) {
        return result;
    }

    if (request.bind_mix_samplers) {
        request.bind_mix_samplers();
    }

    for (const auto& plane : request.planes) {
        if (!plane.visible || plane.opacity <= 0.0f || !plane.has_target) {
            continue;
        }

        request.setup_plane_shader(plane.index, plane.mvp);
        request.bind_layer_texture(plane.index);
        request.bind_stroke_texture(plane.index);
        request.bind_mask_texture(plane.index);
        request.draw_plane();
        request.unbind_mask_texture(plane.index);
        request.unbind_stroke_texture(plane.index);
        request.unbind_layer_texture(plane.index);
        ++result.composed_planes;
    }

    if (request.unbind_mix_samplers) {
        request.unbind_mix_samplers();
    }

    result.ok = true;
    return result;
}

} // namespace pp::panopainter
