#pragma once

#include "app.h"
#include "app_core/document_animation.h"
#include "legacy_canvas_stroke_composite_services.h"
#include "legacy_canvas_stroke_erase_services.h"
#include "shader.h"

#include <array>
#include <functional>
#include <utility>

namespace pp::panopainter {

struct LegacyCanvasDrawMergeCheckerboardUniforms {
    glm::mat4 mvp { 1.0f };
    bool colorize = false;
};

struct LegacyCanvasDrawMergeTextureUniforms {
    glm::mat4 mvp { 1.0f };
    int texture_slot = 0;
};

struct LegacyCanvasDrawMergeAtlasUniforms {
    glm::mat4 mvp { 1.0f };
    int texture_slot = 0;
    glm::vec2 offset {};
    glm::vec2 size { 1.0f };
};

struct LegacyCanvasDrawMergeEquirectUniforms {
    glm::mat4 mvp { 1.0f };
    int texture_slot = 0;
};

struct LegacyCanvasDrawMergeTextureMaskUniforms {
    int texture_slot = 0;
    glm::vec2 pattern_offset {};
};

struct LegacyCanvasDrawMergeTextureAlphaUniforms {
    glm::mat4 mvp { 1.0f };
    int texture_slot = 0;
    float alpha = 1.0f;
    int highlight = 0;
};

struct LegacyCanvasDrawMergeTextureBlendUniforms {
    glm::mat4 mvp { 1.0f };
    int texture_slot = 0;
    int destination_texture_slot = 2;
    bool use_destination_texture = false;
    int blend_mode = 0;
    float alpha = 1.0f;
};

struct LegacyCanvasDrawMergeTextureColorizeUniforms {
    glm::mat4 mvp { 1.0f };
    int texture_slot = 0;
    glm::vec4 color { 1.0f };
};

struct LegacyCanvasDrawMergeShaderExecution {
    std::function<void(kShader)> use_shader;
    std::function<void(kShaderUniform, int)> set_int;
    std::function<void(kShaderUniform, float)> set_float;
    std::function<void(kShaderUniform, const glm::vec2&)> set_vec2;
    std::function<void(kShaderUniform, const glm::vec4&)> set_vec4;
    std::function<void(kShaderUniform, const glm::mat4&)> set_mat4;
};

inline void setup_legacy_canvas_draw_merge_checkerboard_shader(
    const LegacyCanvasDrawMergeCheckerboardUniforms& uniforms);

struct LegacyCanvasDrawMergeLayerBlendUniforms {
    LegacyCanvasDrawMergeTextureBlendUniforms shader;
    bool copy_destination = false;
};

struct LegacyCanvasDrawMergeLayerBlendExecution {
    std::function<void()> unbind_merge_framebuffer;
    std::function<void()> bind_samplers;
    std::function<void()> bind_merge_texture;
    std::function<void()> bind_destination_texture;
    std::function<void()> copy_destination_framebuffer;
    std::function<void()> draw;
    std::function<void()> unbind_destination_texture;
    std::function<void()> unbind_merge_texture;
};

struct LegacyCanvasDrawMergeLayerTextureExecution {
    std::function<void()> bind_sampler;
    std::function<void()> bind_layer_texture;
    std::function<void()> draw;
    std::function<void()> unbind_layer_texture;
};

struct LegacyCanvasDrawMergeLayerCompositeExecution {
    std::function<void()> execute_temporary_erase;
    std::function<void()> execute_temporary_paint;
    std::function<void()> execute_layer_texture;
    std::function<void()> execute_layer_blend;
};

struct LegacyCanvasDrawMergeLayerPlaneExecution {
    std::function<void()> bind_blender_framebuffer;
    std::function<void()> clear_blender_framebuffer;
    std::function<void()> unbind_blender_framebuffer;
    std::function<void()> prepare_temporary_erase;
    std::function<void(int, float)> draw_temporary_erase_frame;
    std::function<void()> cleanup_temporary_erase;
    std::function<void()> prepare_temporary_paint;
    std::function<void(int, float)> draw_temporary_paint_frame;
    std::function<void()> cleanup_temporary_paint;
    std::function<void()> prepare_layer_texture;
    std::function<void(int, float)> draw_layer_texture_frame;
    std::function<void()> cleanup_layer_texture;
    std::function<void()> draw_blend;
};

struct LegacyCanvasDrawMergeLayerPathExecution {
    std::function<void()> bind_blender_framebuffer;
    std::function<void()> clear_blender_framebuffer;
    std::function<void()> unbind_blender_framebuffer;
    std::function<void()> prepare_temporary_erase;
    std::function<void()> cleanup_temporary_erase;
    std::function<void()> prepare_temporary_paint;
    std::function<void()> cleanup_temporary_paint;
    std::function<void()> prepare_layer_texture;
    std::function<void()> cleanup_layer_texture;
    std::function<void()> draw_blend;
    std::function<void()> draw_debug_outline;
    std::function<void(int, float)> draw_frame;
};

struct LegacyCanvasDrawMergeLayerPathGlUniforms {
    LegacyStrokeEraseUniforms temporary_erase;
    LegacyStrokeCompositeUniforms temporary_paint;
    LegacyCanvasDrawMergeTextureAlphaUniforms layer_texture;
    LegacyCanvasDrawMergeLayerBlendUniforms blend;
    bool use_nearest_sampler = false;
    bool use_dual_texture = false;
};

struct LegacyCanvasDrawMergeLayerPathGlExecution {
    std::function<void()> bind_blender_framebuffer;
    std::function<void()> clear_blender_framebuffer;
    std::function<void()> unbind_blender_framebuffer;
    std::function<void(int)> bind_sampler;
    std::function<void(int)> bind_nearest_sampler;
    std::function<void(int)> bind_stencil_sampler;
    std::function<void(int)> set_active_texture_unit;
    std::function<void()> bind_temporary_texture;
    std::function<void()> unbind_temporary_texture;
    std::function<void()> bind_smask_texture;
    std::function<void()> unbind_smask_texture;
    std::function<void()> bind_temporary_dual_texture;
    std::function<void()> unbind_temporary_dual_texture;
    std::function<void()> bind_pattern_texture;
    std::function<void()> draw_face;
    std::function<void()> bind_blender_texture;
    std::function<void()> unbind_blender_texture;
    std::function<void()> bind_destination_texture;
    std::function<void()> unbind_destination_texture;
    std::function<void()> copy_destination_framebuffer;
    std::function<void()> draw_debug_outline;
    std::function<void(int, float)> draw_frame;
};

struct LegacyCanvasDrawLayerVisit {
    size_t layer_index = 0;
    int plane_index = 0;
    int z = 0;
    int first_frame = 0;
    int last_frame = 0;
};

struct LegacyCanvasDrawMergeTemporaryCompositeExecution {
    std::function<void()> setup;
    std::function<void()> bind_samplers;
    std::function<void()> bind_textures;
    std::function<void()> draw;
    std::function<void()> unbind_textures;
};

struct LegacyCanvasDrawMergeBackgroundSetupUniforms {
    bool draw_merged = false;
    bool use_blend = false;
};

struct LegacyCanvasDrawMergeBackgroundSetupExecution {
    std::function<void()> disable_blend;
    std::function<void(int)> draw_checkerboard_plane;
};

template <
    typename Setup,
    typename BindSamplers,
    typename BindTextures,
    typename Draw,
    typename UnbindTextures>
[[nodiscard]] inline LegacyCanvasDrawMergeTemporaryCompositeExecution make_legacy_canvas_draw_merge_temporary_erase_composite(
    Setup&& setup,
    BindSamplers&& bind_samplers,
    BindTextures&& bind_textures,
    Draw&& draw,
    UnbindTextures&& unbind_textures)
{
    return LegacyCanvasDrawMergeTemporaryCompositeExecution {
        .setup = std::forward<Setup>(setup),
        .bind_samplers = std::forward<BindSamplers>(bind_samplers),
        .bind_textures = std::forward<BindTextures>(bind_textures),
        .draw = std::forward<Draw>(draw),
        .unbind_textures = std::forward<UnbindTextures>(unbind_textures),
    };
}

template <
    typename Setup,
    typename BindSamplers,
    typename BindTextures,
    typename Draw,
    typename UnbindTextures>
[[nodiscard]] inline LegacyCanvasDrawMergeTemporaryCompositeExecution make_legacy_canvas_draw_merge_temporary_paint_composite(
    Setup&& setup,
    BindSamplers&& bind_samplers,
    BindTextures&& bind_textures,
    Draw&& draw,
    UnbindTextures&& unbind_textures)
{
    return LegacyCanvasDrawMergeTemporaryCompositeExecution {
        .setup = std::forward<Setup>(setup),
        .bind_samplers = std::forward<BindSamplers>(bind_samplers),
        .bind_textures = std::forward<BindTextures>(bind_textures),
        .draw = std::forward<Draw>(draw),
        .unbind_textures = std::forward<UnbindTextures>(unbind_textures),
    };
}

struct LegacyCanvasDrawMergePlaneSetupUniforms {
    LegacyCanvasDrawMergeCheckerboardUniforms checkerboard;
    bool use_blend = false;
    bool draw_checkerboard = false;
};

struct LegacyCanvasDrawMergePlaneSetupExecution {
    std::function<void()> clear_plane;
    std::function<void()> disable_blend;
    std::function<void()> enable_blend;
    std::function<void()> draw;
};

struct LegacyCanvasDrawMergeFinalPlaneCompositeUniforms {
    LegacyCanvasDrawMergeCheckerboardUniforms checkerboard;
    LegacyCanvasDrawMergeTextureUniforms texture;
    bool draw_checkerboard = false;
};

struct LegacyCanvasDrawMergeFinalPlaneCompositeExecution {
    std::function<void()> bind_merged_texture_copy_target;
    std::function<void()> copy_merged_framebuffer;
    std::function<void()> enable_blend;
    std::function<void()> draw;
    std::function<void()> bind_sampler;
    std::function<void()> bind_merged_texture;
    std::function<void()> unbind_merged_texture;
};

struct LegacyCanvasDrawMergeCacheToScreenCompositeUniforms {
    LegacyCanvasDrawMergeCheckerboardUniforms checkerboard;
    LegacyCanvasDrawMergeTextureUniforms texture;
};

struct LegacyCanvasDrawMergeCacheToScreenCompositeExecution {
    std::function<void()> enable_blend;
    std::function<void(const LegacyCanvasDrawMergeCheckerboardUniforms&, int)> draw_checkerboard_plane;
    std::function<void()> bind_sampler;
    std::function<void()> bind_cache_texture;
    std::function<void()> draw_cache_texture;
    std::function<void()> unbind_cache_texture;
};

struct LegacyCanvasDrawMergeDisplayResolveUniforms {
    LegacyCanvasDrawMergeTextureUniforms texture;
};

struct LegacyCanvasDrawMergeDisplayResolveExecution {
    std::function<void()> unbind_resolve_framebuffer;
    std::function<void()> clear_color_buffer;
    std::function<void()> apply_viewport;
    std::function<void()> bind_sampler;
    std::function<void()> bind_resolve_texture;
    std::function<void()> draw;
    std::function<void()> unbind_resolve_texture;
};

struct LegacyCanvasDrawMergePostDrawExecution {
    std::function<void()> draw_mask_free;
    std::function<void()> draw_mask_line;
    std::function<void()> draw_smask_faces;
    std::function<void()> draw_grid_modes;
    std::function<void()> draw_heightmap;
    std::function<void()> draw_current_modes;
};

template <typename GridT>
[[nodiscard]] inline auto make_legacy_canvas_draw_merge_heightmap_draw(
    GridT* grid,
    const glm::mat4& proj,
    const glm::mat4& camera)
{
    return [grid, proj, camera] {
        grid->draw_heightmap(proj, camera, false);
    };
}

template <typename ModesT>
[[nodiscard]] inline auto make_legacy_canvas_draw_merge_current_modes_draw(
    ModesT* modes,
    const glm::mat4& ortho_proj,
    const glm::mat4& proj,
    const glm::mat4& camera)
{
    return [modes, ortho_proj, proj, camera] {
        for (auto& mode : *modes)
            mode->on_Draw(ortho_proj, proj, camera);
    };
}

template <typename ModesT>
[[nodiscard]] inline auto make_legacy_canvas_draw_merge_grid_modes_draw(
    ModesT* modes,
    const glm::mat4& ortho_proj,
    const glm::mat4& proj,
    const glm::mat4& camera)
{
    return [modes, ortho_proj, proj, camera] {
        for (auto& mode : *modes)
            mode->on_Draw(ortho_proj, proj, camera);
    };
}

template <typename PlaneTransform, typename DrawPlane>
[[nodiscard]] inline auto make_legacy_canvas_draw_merge_background_checkerboard_plane(
    glm::mat4 proj,
    glm::mat4 camera,
    float layer_scale,
    PlaneTransform plane_transform,
    DrawPlane draw_plane)
{
    return [proj, camera, layer_scale, plane_transform, draw_plane](int plane_index) {
        auto checkerboard_uniforms = LegacyCanvasDrawMergeCheckerboardUniforms {
            .mvp = proj * camera *
                glm::scale(glm::vec3(layer_scale)) *
                plane_transform[plane_index] *
                glm::translate(glm::vec3(0, 0, -1.f)),
            .colorize = false,
        };

        setup_legacy_canvas_draw_merge_checkerboard_shader(checkerboard_uniforms);
        draw_plane();
    };
}

struct LegacyCanvasDrawMergeSmaskFacesExecution {
    std::function<void()> set_active_texture_unit;
    std::function<void()> enable_blend;
    std::function<void(int plane_index)> bind_face_texture;
    std::function<void()> draw_face;
    std::function<void(int plane_index)> unbind_face_texture;
};

[[nodiscard]] inline LegacyCanvasDrawMergeShaderExecution legacy_shader_manager_draw_merge_execution() noexcept
{
    return {
        .use_shader = [](kShader shader) { ShaderManager::use(shader); },
        .set_int = [](kShaderUniform uniform, int value) { ShaderManager::u_int(uniform, value); },
        .set_float = [](kShaderUniform uniform, float value) { ShaderManager::u_float(uniform, value); },
        .set_vec2 = [](kShaderUniform uniform, const glm::vec2& value) { ShaderManager::u_vec2(uniform, value); },
        .set_vec4 = [](kShaderUniform uniform, const glm::vec4& value) { ShaderManager::u_vec4(uniform, value); },
        .set_mat4 = [](kShaderUniform uniform, const glm::mat4& value) { ShaderManager::u_mat4(uniform, value); },
    };
}

inline void apply_legacy_canvas_draw_merge_mvp(
    const glm::mat4& mvp,
    const LegacyCanvasDrawMergeShaderExecution& execution) noexcept
{
    execution.set_mat4(kShaderUniform::MVP, mvp);
}

inline void apply_legacy_canvas_draw_merge_mvp(const glm::mat4& mvp)
{
    apply_legacy_canvas_draw_merge_mvp(mvp, legacy_shader_manager_draw_merge_execution());
}

inline void setup_legacy_canvas_draw_merge_checkerboard_shader(
    const LegacyCanvasDrawMergeCheckerboardUniforms& uniforms,
    const LegacyCanvasDrawMergeShaderExecution& execution) noexcept
{
    execution.use_shader(kShader::Checkerboard);
    execution.set_int(kShaderUniform::Colorize, uniforms.colorize);
    execution.set_mat4(kShaderUniform::MVP, uniforms.mvp);
}

inline void setup_legacy_canvas_draw_merge_checkerboard_shader(
    const LegacyCanvasDrawMergeCheckerboardUniforms& uniforms)
{
    setup_legacy_canvas_draw_merge_checkerboard_shader(uniforms, legacy_shader_manager_draw_merge_execution());
}

inline void setup_legacy_canvas_draw_merge_texture_shader(
    const LegacyCanvasDrawMergeTextureUniforms& uniforms,
    const LegacyCanvasDrawMergeShaderExecution& execution) noexcept
{
    execution.use_shader(kShader::Texture);
    execution.set_int(kShaderUniform::Tex, uniforms.texture_slot);
    execution.set_mat4(kShaderUniform::MVP, uniforms.mvp);
}

inline void setup_legacy_canvas_draw_merge_texture_shader(
    const LegacyCanvasDrawMergeTextureUniforms& uniforms)
{
    setup_legacy_canvas_draw_merge_texture_shader(uniforms, legacy_shader_manager_draw_merge_execution());
}

inline void setup_legacy_canvas_draw_merge_atlas_shader(
    const LegacyCanvasDrawMergeAtlasUniforms& uniforms,
    const LegacyCanvasDrawMergeShaderExecution& execution) noexcept
{
    execution.use_shader(kShader::Atlas);
    execution.set_vec2(kShaderUniform::Tof, uniforms.offset);
    execution.set_vec2(kShaderUniform::Tsz, uniforms.size);
    execution.set_int(kShaderUniform::Tex, uniforms.texture_slot);
    execution.set_mat4(kShaderUniform::MVP, uniforms.mvp);
}

inline void setup_legacy_canvas_draw_merge_atlas_shader(
    const LegacyCanvasDrawMergeAtlasUniforms& uniforms)
{
    setup_legacy_canvas_draw_merge_atlas_shader(uniforms, legacy_shader_manager_draw_merge_execution());
}

inline void setup_legacy_canvas_draw_merge_equirect_shader(
    const LegacyCanvasDrawMergeEquirectUniforms& uniforms,
    const LegacyCanvasDrawMergeShaderExecution& execution) noexcept
{
    execution.use_shader(kShader::Equirect);
    execution.set_mat4(kShaderUniform::MVP, uniforms.mvp);
    execution.set_int(kShaderUniform::Tex, uniforms.texture_slot);
}

inline void setup_legacy_canvas_draw_merge_equirect_shader(
    const LegacyCanvasDrawMergeEquirectUniforms& uniforms)
{
    setup_legacy_canvas_draw_merge_equirect_shader(uniforms, legacy_shader_manager_draw_merge_execution());
}

inline void setup_legacy_canvas_draw_merge_texture_mask_shader(
    const LegacyCanvasDrawMergeTextureMaskUniforms& uniforms,
    const LegacyCanvasDrawMergeShaderExecution& execution) noexcept
{
    execution.use_shader(kShader::TextureMask);
    execution.set_int(kShaderUniform::Tex, uniforms.texture_slot);
    execution.set_vec2(kShaderUniform::PatternOffset, uniforms.pattern_offset);
}

inline void setup_legacy_canvas_draw_merge_texture_mask_shader(
    const LegacyCanvasDrawMergeTextureMaskUniforms& uniforms)
{
    setup_legacy_canvas_draw_merge_texture_mask_shader(uniforms, legacy_shader_manager_draw_merge_execution());
}

inline void setup_legacy_canvas_draw_merge_texture_alpha_shader(
    const LegacyCanvasDrawMergeTextureAlphaUniforms& uniforms,
    const LegacyCanvasDrawMergeShaderExecution& execution) noexcept
{
    execution.use_shader(kShader::TextureAlpha);
    execution.set_int(kShaderUniform::Tex, uniforms.texture_slot);
    execution.set_float(kShaderUniform::Alpha, uniforms.alpha);
    execution.set_int(kShaderUniform::Highlight, uniforms.highlight);
    execution.set_mat4(kShaderUniform::MVP, uniforms.mvp);
}

inline void setup_legacy_canvas_draw_merge_texture_alpha_shader(
    const LegacyCanvasDrawMergeTextureAlphaUniforms& uniforms)
{
    setup_legacy_canvas_draw_merge_texture_alpha_shader(uniforms, legacy_shader_manager_draw_merge_execution());
}

inline void setup_legacy_canvas_draw_merge_texture_blend_shader(
    const LegacyCanvasDrawMergeTextureBlendUniforms& uniforms,
    const LegacyCanvasDrawMergeShaderExecution& execution) noexcept
{
    execution.use_shader(kShader::TextureBlend);
    execution.set_int(kShaderUniform::Tex, uniforms.texture_slot);
    if (uniforms.use_destination_texture) {
        execution.set_int(kShaderUniform::TexBG, uniforms.destination_texture_slot);
    }
    execution.set_int(kShaderUniform::BlendMode, uniforms.blend_mode);
    execution.set_float(kShaderUniform::Alpha, uniforms.alpha);
    execution.set_mat4(kShaderUniform::MVP, uniforms.mvp);
}

inline void setup_legacy_canvas_draw_merge_texture_blend_shader(
    const LegacyCanvasDrawMergeTextureBlendUniforms& uniforms)
{
    setup_legacy_canvas_draw_merge_texture_blend_shader(uniforms, legacy_shader_manager_draw_merge_execution());
}

inline void setup_legacy_canvas_draw_merge_texture_colorize_shader(
    const LegacyCanvasDrawMergeTextureColorizeUniforms& uniforms,
    const LegacyCanvasDrawMergeShaderExecution& execution) noexcept
{
    execution.use_shader(kShader::TextureColorize);
    execution.set_int(kShaderUniform::Tex, uniforms.texture_slot);
    execution.set_vec4(kShaderUniform::Col, uniforms.color);
    execution.set_mat4(kShaderUniform::MVP, uniforms.mvp);
}

inline void setup_legacy_canvas_draw_merge_texture_colorize_shader(
    const LegacyCanvasDrawMergeTextureColorizeUniforms& uniforms)
{
    setup_legacy_canvas_draw_merge_texture_colorize_shader(uniforms, legacy_shader_manager_draw_merge_execution());
}

inline void execute_legacy_canvas_draw_merge_layer_blend(
    const LegacyCanvasDrawMergeLayerBlendUniforms& uniforms,
    const LegacyCanvasDrawMergeLayerBlendExecution& execution)
{
    execution.unbind_merge_framebuffer();
    execution.bind_samplers();
    setup_legacy_canvas_draw_merge_texture_blend_shader(uniforms.shader);
    execution.bind_merge_texture();

    if (uniforms.copy_destination) {
        execution.bind_destination_texture();
        execution.copy_destination_framebuffer();
    }

    execution.draw();

    if (uniforms.copy_destination) {
        execution.unbind_destination_texture();
    }

    execution.unbind_merge_texture();
}

inline void execute_legacy_canvas_draw_merge_layer_texture(
    const LegacyCanvasDrawMergeTextureAlphaUniforms& uniforms,
    const LegacyCanvasDrawMergeLayerTextureExecution& execution)
{
    execution.bind_sampler();
    setup_legacy_canvas_draw_merge_texture_alpha_shader(uniforms);
    execution.bind_layer_texture();
    execution.draw();
    execution.unbind_layer_texture();
}

template <typename LayerMergeT, typename SamplerT, typename FacePlaneT, typename SetActiveTextureUnit, typename PlaneTransform>
[[nodiscard]] inline auto make_legacy_canvas_draw_merge_layer_texture_draw(
    LayerMergeT* layer_merge,
    SamplerT* sampler,
    FacePlaneT* face_plane,
    SetActiveTextureUnit set_active_texture_unit,
    glm::mat4 proj,
    glm::mat4 camera,
    PlaneTransform plane_transform)
{
    return [layer_merge, sampler, face_plane, set_active_texture_unit, proj, camera, plane_transform](int plane_index) {
        const auto mvp = proj * camera *
            plane_transform[plane_index] *
            glm::translate(glm::vec3(0, 0, -1));
        execute_legacy_canvas_draw_merge_layer_texture(
            {
                .mvp = mvp,
                .texture_slot = 0,
                .alpha = 1.f,
                .highlight = false,
            },
            {
                .bind_sampler = [sampler, set_active_texture_unit] {
                    sampler->bind(0);
                    set_active_texture_unit(0);
                },
                .bind_layer_texture = [layer_merge, plane_index] {
                    layer_merge->rtt(plane_index).bindTexture();
                },
                .draw = [face_plane] {
                    face_plane->draw_fill();
                },
                .unbind_layer_texture = [layer_merge, plane_index] {
                    layer_merge->rtt(plane_index).unbindTexture();
                },
        });
    };
}

template <typename LayerT, typename FacePlaneT, typename SetActiveTextureUnit>
[[nodiscard]] inline auto make_legacy_canvas_draw_merge_layer_frame_draw(
    LayerT* layer,
    FacePlaneT* face_plane,
    SetActiveTextureUnit set_active_texture_unit,
    int plane_index,
    float layer_opacity)
{
    return [layer, face_plane, set_active_texture_unit, plane_index, layer_opacity](int frame, float onion_alpha) {
        ShaderManager::u_float(kShaderUniform::Alpha, layer_opacity * onion_alpha);
        set_active_texture_unit(0);
        layer->rtt(plane_index, frame).bindTexture();
        face_plane->draw_fill();
        set_active_texture_unit(0);
        layer->rtt(plane_index, frame).unbindTexture();
    };
}

inline void execute_legacy_canvas_draw_merge_layer_composite(
    bool is_temporary_erase,
    bool is_temporary_paint,
    bool use_blend,
    const LegacyCanvasDrawMergeLayerCompositeExecution& execution)
{
    if (is_temporary_erase) {
        execution.execute_temporary_erase();
    } else if (is_temporary_paint) {
        execution.execute_temporary_paint();
    } else {
        execution.execute_layer_texture();
    }

    if (use_blend) {
        execution.execute_layer_blend();
    }
}

inline void execute_legacy_canvas_draw_merge_layer_plane(
    bool is_temporary_erase,
    bool is_temporary_paint,
    bool use_blend,
    int first_frame,
    int last_frame,
    const std::function<float(int)>& frame_alpha,
    const LegacyCanvasDrawMergeLayerPlaneExecution& execution)
{
    if (use_blend) {
        execution.bind_blender_framebuffer();
        execution.clear_blender_framebuffer();
    }

    if (is_temporary_erase) {
        execution.prepare_temporary_erase();
        for (int frame = first_frame; frame <= last_frame; frame++) {
            execution.draw_temporary_erase_frame(frame, frame_alpha(frame));
        }
        execution.cleanup_temporary_erase();
    } else if (is_temporary_paint) {
        execution.prepare_temporary_paint();
        for (int frame = first_frame; frame <= last_frame; frame++) {
            execution.draw_temporary_paint_frame(frame, frame_alpha(frame));
        }
        execution.cleanup_temporary_paint();
    } else {
        execution.prepare_layer_texture();
        for (int frame = first_frame; frame <= last_frame; frame++) {
            execution.draw_layer_texture_frame(frame, frame_alpha(frame));
        }
        execution.cleanup_layer_texture();
    }

    if (use_blend) {
        execution.unbind_blender_framebuffer();
        execution.draw_blend();
    }
}

inline void execute_legacy_canvas_draw_merge_layer_path(
    bool is_temporary_erase,
    bool is_temporary_paint,
    bool use_blend,
    int first_frame,
    int last_frame,
    const std::function<float(int)>& frame_alpha,
    const LegacyCanvasDrawMergeLayerPathExecution& execution)
{
    if (use_blend) {
        execution.bind_blender_framebuffer();
        execution.clear_blender_framebuffer();
    }

    if (is_temporary_erase) {
        execution.prepare_temporary_erase();
        for (int frame = first_frame; frame <= last_frame; frame++) {
            execution.draw_frame(frame, frame_alpha(frame));
        }
        execution.cleanup_temporary_erase();
    } else if (is_temporary_paint) {
        execution.prepare_temporary_paint();
        for (int frame = first_frame; frame <= last_frame; frame++) {
            execution.draw_frame(frame, frame_alpha(frame));
        }
        execution.cleanup_temporary_paint();
    } else {
        execution.prepare_layer_texture();
        for (int frame = first_frame; frame <= last_frame; frame++) {
            execution.draw_frame(frame, frame_alpha(frame));
        }
        execution.cleanup_layer_texture();
    }

    if (use_blend) {
        execution.unbind_blender_framebuffer();
        execution.draw_blend();
    }

    execution.draw_debug_outline();
}

[[nodiscard]] inline LegacyCanvasDrawMergeLayerPathExecution make_legacy_canvas_draw_merge_layer_path_gl_execution(
    const LegacyCanvasDrawMergeLayerPathGlUniforms& uniforms,
    const LegacyCanvasDrawMergeLayerPathGlExecution& execution)
{
    return {
        .bind_blender_framebuffer = execution.bind_blender_framebuffer,
        .clear_blender_framebuffer = execution.clear_blender_framebuffer,
        .unbind_blender_framebuffer = execution.unbind_blender_framebuffer,
        .prepare_temporary_erase = [uniforms, execution] {
            execution.bind_sampler(0);
            execution.bind_sampler(1);
            execution.bind_sampler(2);
            setup_legacy_stroke_erase_shader(uniforms.temporary_erase);
            execution.set_active_texture_unit(1);
            execution.bind_temporary_texture();
            execution.set_active_texture_unit(2);
            execution.bind_smask_texture();
        },
        .cleanup_temporary_erase = [execution] {
            execution.set_active_texture_unit(2);
            execution.unbind_smask_texture();
            execution.set_active_texture_unit(1);
            execution.unbind_temporary_texture();
        },
        .prepare_temporary_paint = [uniforms, execution] {
            execution.bind_sampler(0);
            execution.bind_sampler(1);
            execution.bind_sampler(2);
            execution.bind_sampler(3);
            execution.bind_stencil_sampler(4);
            setup_legacy_stroke_composite_shader(uniforms.temporary_paint);
            execution.set_active_texture_unit(1);
            execution.bind_temporary_texture();
            execution.set_active_texture_unit(2);
            execution.bind_smask_texture();
            execution.set_active_texture_unit(3);
            if (uniforms.use_dual_texture) {
                execution.bind_temporary_dual_texture();
            }
            execution.set_active_texture_unit(4);
            execution.bind_pattern_texture();
        },
        .cleanup_temporary_paint = [uniforms, execution] {
            execution.set_active_texture_unit(3);
            if (uniforms.use_dual_texture) {
                execution.unbind_temporary_dual_texture();
            }
            execution.set_active_texture_unit(2);
            execution.unbind_smask_texture();
            execution.set_active_texture_unit(1);
            execution.unbind_temporary_texture();
        },
        .prepare_layer_texture = [uniforms, execution] {
            if (uniforms.use_nearest_sampler) {
                execution.bind_nearest_sampler(0);
            } else {
                execution.bind_sampler(0);
            }
            setup_legacy_canvas_draw_merge_texture_alpha_shader(uniforms.layer_texture);
        },
        .cleanup_layer_texture = [] {
        },
        .draw_blend = [uniforms, execution] {
            execute_legacy_canvas_draw_merge_layer_blend(
                uniforms.blend,
                {
                    .unbind_merge_framebuffer = execution.unbind_blender_framebuffer,
                    .bind_samplers = [execution] {
                        execution.bind_sampler(0);
                        execution.bind_sampler(2);
                    },
                    .bind_merge_texture = [execution] {
                        execution.set_active_texture_unit(0);
                        execution.bind_blender_texture();
                    },
                    .bind_destination_texture = [execution] {
                        execution.set_active_texture_unit(2);
                        execution.bind_destination_texture();
                    },
                    .copy_destination_framebuffer = execution.copy_destination_framebuffer,
                    .draw = execution.draw_face,
                    .unbind_destination_texture = [execution] {
                        execution.set_active_texture_unit(2);
                        execution.unbind_destination_texture();
                    },
                    .unbind_merge_texture = [execution] {
                        execution.set_active_texture_unit(0);
                        execution.unbind_blender_texture();
                    },
                });
        },
        .draw_debug_outline = execution.draw_debug_outline,
        .draw_frame = execution.draw_frame,
    };
}

template <
    typename BindBlenderFramebuffer,
    typename ClearBlenderFramebuffer,
    typename UnbindBlenderFramebuffer,
    typename BindSampler,
    typename BindNearestSampler,
    typename BindStencilSampler,
    typename SetActiveTextureUnit,
    typename BindTemporaryTexture,
    typename UnbindTemporaryTexture,
    typename BindSmaskTexture,
    typename UnbindSmaskTexture,
    typename BindTemporaryDualTexture,
    typename UnbindTemporaryDualTexture,
    typename BindPatternTexture,
    typename DrawFace,
    typename BindBlenderTexture,
    typename UnbindBlenderTexture,
    typename BindDestinationTexture,
    typename UnbindDestinationTexture,
    typename CopyDestinationFramebuffer,
    typename DrawDebugOutline,
    typename DrawFrame>
[[nodiscard]] inline LegacyCanvasDrawMergeLayerPathExecution make_legacy_canvas_draw_merge_layer_path_gl_execution(
    const LegacyCanvasDrawMergeLayerPathGlUniforms& uniforms,
    BindBlenderFramebuffer&& bind_blender_framebuffer,
    ClearBlenderFramebuffer&& clear_blender_framebuffer,
    UnbindBlenderFramebuffer&& unbind_blender_framebuffer,
    BindSampler&& bind_sampler,
    BindNearestSampler&& bind_nearest_sampler,
    BindStencilSampler&& bind_stencil_sampler,
    SetActiveTextureUnit&& set_active_texture_unit,
    BindTemporaryTexture&& bind_temporary_texture,
    UnbindTemporaryTexture&& unbind_temporary_texture,
    BindSmaskTexture&& bind_smask_texture,
    UnbindSmaskTexture&& unbind_smask_texture,
    BindTemporaryDualTexture&& bind_temporary_dual_texture,
    UnbindTemporaryDualTexture&& unbind_temporary_dual_texture,
    BindPatternTexture&& bind_pattern_texture,
    DrawFace&& draw_face,
    BindBlenderTexture&& bind_blender_texture,
    UnbindBlenderTexture&& unbind_blender_texture,
    BindDestinationTexture&& bind_destination_texture,
    UnbindDestinationTexture&& unbind_destination_texture,
    CopyDestinationFramebuffer&& copy_destination_framebuffer,
    DrawDebugOutline&& draw_debug_outline,
    DrawFrame&& draw_frame)
{
    return make_legacy_canvas_draw_merge_layer_path_gl_execution(
        uniforms,
        LegacyCanvasDrawMergeLayerPathGlExecution {
            .bind_blender_framebuffer = std::forward<BindBlenderFramebuffer>(bind_blender_framebuffer),
            .clear_blender_framebuffer = std::forward<ClearBlenderFramebuffer>(clear_blender_framebuffer),
            .unbind_blender_framebuffer = std::forward<UnbindBlenderFramebuffer>(unbind_blender_framebuffer),
            .bind_sampler = std::forward<BindSampler>(bind_sampler),
            .bind_nearest_sampler = std::forward<BindNearestSampler>(bind_nearest_sampler),
            .bind_stencil_sampler = std::forward<BindStencilSampler>(bind_stencil_sampler),
            .set_active_texture_unit = std::forward<SetActiveTextureUnit>(set_active_texture_unit),
            .bind_temporary_texture = std::forward<BindTemporaryTexture>(bind_temporary_texture),
            .unbind_temporary_texture = std::forward<UnbindTemporaryTexture>(unbind_temporary_texture),
            .bind_smask_texture = std::forward<BindSmaskTexture>(bind_smask_texture),
            .unbind_smask_texture = std::forward<UnbindSmaskTexture>(unbind_smask_texture),
            .bind_temporary_dual_texture = std::forward<BindTemporaryDualTexture>(bind_temporary_dual_texture),
            .unbind_temporary_dual_texture = std::forward<UnbindTemporaryDualTexture>(unbind_temporary_dual_texture),
            .bind_pattern_texture = std::forward<BindPatternTexture>(bind_pattern_texture),
            .draw_face = std::forward<DrawFace>(draw_face),
            .bind_blender_texture = std::forward<BindBlenderTexture>(bind_blender_texture),
            .unbind_blender_texture = std::forward<UnbindBlenderTexture>(unbind_blender_texture),
            .bind_destination_texture = std::forward<BindDestinationTexture>(bind_destination_texture),
            .unbind_destination_texture = std::forward<UnbindDestinationTexture>(unbind_destination_texture),
            .copy_destination_framebuffer = std::forward<CopyDestinationFramebuffer>(copy_destination_framebuffer),
            .draw_debug_outline = std::forward<DrawDebugOutline>(draw_debug_outline),
            .draw_frame = std::forward<DrawFrame>(draw_frame),
        });
}

template <typename PlanOnionRange, typename ShouldDrawPlane, typename VisitLayerPlane, typename LogOnionRangeFailure>
inline void execute_legacy_canvas_draw_layer_traversal(
    size_t layer_count,
    PlanOnionRange&& plan_onion_range,
    ShouldDrawPlane&& should_draw_plane,
    VisitLayerPlane&& visit_layer_plane,
    LogOnionRangeFailure&& log_onion_range_failure)
{
    for (size_t i = 0; i < layer_count; ++i) {
        const auto layer_index = i;
        const auto onion_range_result = plan_onion_range(layer_index);
        if (!onion_range_result) {
            log_onion_range_failure(onion_range_result.status().message);
            continue;
        }

        const auto onion_range = onion_range_result.value();
        for (int plane_index = 0; plane_index < 6; ++plane_index) {
            if (!should_draw_plane(layer_index, plane_index, onion_range.first_frame, onion_range.last_frame)) {
                continue;
            }

            visit_layer_plane(LegacyCanvasDrawLayerVisit {
                .layer_index = layer_index,
                .plane_index = plane_index,
                .z = static_cast<int>(layer_count - i),
                .first_frame = onion_range.first_frame,
                .last_frame = onion_range.last_frame,
            }, onion_range);
        }
    }
}

inline void execute_legacy_canvas_draw_merge_temporary_composite(
    const LegacyCanvasDrawMergeTemporaryCompositeExecution& execution)
{
    execution.setup();
    execution.bind_samplers();
    execution.bind_textures();
    execution.draw();
    execution.unbind_textures();
}

inline void execute_legacy_canvas_draw_merge_background_setup(
    const LegacyCanvasDrawMergeBackgroundSetupUniforms& uniforms,
    const LegacyCanvasDrawMergeBackgroundSetupExecution& execution)
{
    if (uniforms.draw_merged) {
        execution.disable_blend();
    }

    if (uniforms.draw_merged || !uniforms.use_blend) {
        for (int plane_index = 0; plane_index < 6; ++plane_index) {
            execution.draw_checkerboard_plane(plane_index);
        }
    }
}

template <
    typename PrepareBlendCache,
    typename DrawBackground,
    typename ConfigureBlendState,
    typename DisableDepthTest,
    typename PlanOnionRange,
    typename ShouldDrawPlane,
    typename VisitLayerPlane,
    typename LogOnionRangeFailure,
    typename CompositeBlendCache>
inline void execute_legacy_canvas_draw_unmerged_shell(
    bool use_blend,
    size_t layer_count,
    PrepareBlendCache&& prepare_blend_cache,
    DrawBackground&& draw_background,
    ConfigureBlendState&& configure_blend_state,
    DisableDepthTest&& disable_depth_test,
    PlanOnionRange&& plan_onion_range,
    ShouldDrawPlane&& should_draw_plane,
    VisitLayerPlane&& visit_layer_plane,
    LogOnionRangeFailure&& log_onion_range_failure,
    CompositeBlendCache&& composite_blend_cache)
{
    if (use_blend) {
        prepare_blend_cache();
    }

    draw_background();
    configure_blend_state(use_blend);
    disable_depth_test();

    execute_legacy_canvas_draw_layer_traversal(
        layer_count,
        std::forward<PlanOnionRange>(plan_onion_range),
        std::forward<ShouldDrawPlane>(should_draw_plane),
        std::forward<VisitLayerPlane>(visit_layer_plane),
        std::forward<LogOnionRangeFailure>(log_onion_range_failure));

    if (use_blend) {
        composite_blend_cache();
    }
}

template <
    typename MakeLayerPathExecution,
    typename ShouldDrawTemporaryErase,
    typename ShouldDrawTemporaryPaint,
    typename FrameAlpha>
inline void execute_legacy_canvas_draw_unmerged_layer_path(
    const LegacyCanvasDrawLayerVisit& visit,
    const auto& onion_range,
    bool use_blend,
    const glm::mat4& proj,
    const glm::mat4& camera,
    const glm::mat4& orientation,
    const auto& plane_transform,
    MakeLayerPathExecution&& make_layer_path_execution,
    ShouldDrawTemporaryErase&& should_draw_temporary_erase,
    ShouldDrawTemporaryPaint&& should_draw_temporary_paint,
    FrameAlpha&& frame_alpha)
{
    const auto plane_index = visit.plane_index;
    const auto plane_mvp_z = proj * camera *
        glm::scale(glm::vec3(visit.z + 1)) *
        orientation *
        plane_transform[plane_index] *
        glm::translate(glm::vec3(0, 0, -1));
    const auto layer_path_execution = make_layer_path_execution(visit.layer_index, plane_index, plane_mvp_z);

    execute_legacy_canvas_draw_merge_layer_path(
        should_draw_temporary_erase(visit),
        should_draw_temporary_paint(visit),
        use_blend,
        visit.first_frame,
        visit.last_frame,
        [&](int frame) {
            return frame_alpha(onion_range, frame);
        },
        layer_path_execution);
}

template <
    typename NodeCanvasT,
    typename PrepareBlendCache,
    typename DrawBackground,
    typename ConfigureBlendState,
    typename DisableDepthTest,
    typename MakeLayerPathExecution,
    typename LogOnionRangeFailure,
    typename CompositeBlendCache>
inline void execute_legacy_canvas_draw_unmerged_node_canvas_shell(
    NodeCanvasT& node_canvas,
    bool use_blend,
    const glm::mat4& proj,
    const glm::mat4& camera,
    const glm::mat4& orientation,
    PrepareBlendCache&& prepare_blend_cache,
    DrawBackground&& draw_background,
    ConfigureBlendState&& configure_blend_state,
    DisableDepthTest&& disable_depth_test,
    MakeLayerPathExecution&& make_layer_path_execution,
    LogOnionRangeFailure&& log_onion_range_failure,
    CompositeBlendCache&& composite_blend_cache)
{
    if (use_blend) {
        prepare_blend_cache();
    }

    draw_background();
    configure_blend_state(use_blend);
    disable_depth_test();

    const auto plan_onion_range = [&](size_t layer_index) {
        return pp::app::plan_animation_onion_frame_range(
            node_canvas.m_canvas->m_layers[layer_index]->frames_count(),
            node_canvas.m_canvas->m_layers[layer_index]->m_frame_index,
            App::I->animation->get_onion_size());
    };

    const auto should_draw_plane = [&](size_t layer_index, int plane_index, int first_frame, int last_frame) {
        bool faces = false;
        for (int frame = first_frame; frame <= last_frame; ++frame) {
            faces |= node_canvas.m_canvas->m_layers[layer_index]->face(plane_index, frame);
        }

        if (node_canvas.m_canvas->m_show_tmp && node_canvas.m_canvas->m_current_layer_idx == layer_index) {
            return true;
        }

        return node_canvas.m_canvas->m_layers[layer_index]->m_visible &&
            node_canvas.m_canvas->m_layers[layer_index]->m_opacity != .0f &&
            faces;
    };

    execute_legacy_canvas_draw_layer_traversal(
        node_canvas.m_canvas->m_layers.size(),
        plan_onion_range,
        should_draw_plane,
        [&](const LegacyCanvasDrawLayerVisit& visit, const auto& onion_range) {
            execute_legacy_canvas_draw_unmerged_layer_path(
                visit,
                onion_range,
                use_blend,
                proj,
                camera,
                orientation,
                node_canvas.m_canvas->m_plane_transform,
                make_layer_path_execution,
                [&](const LegacyCanvasDrawLayerVisit& visit) {
                    return node_canvas.m_canvas->m_current_stroke && node_canvas.m_canvas->m_current_mode == kCanvasMode::Erase && node_canvas.m_canvas->m_show_tmp && node_canvas.m_canvas->m_current_layer_idx == visit.layer_index;
                },
                [&](const LegacyCanvasDrawLayerVisit& visit) {
                    return node_canvas.m_canvas->m_current_stroke && node_canvas.m_canvas->m_show_tmp && node_canvas.m_canvas->m_current_layer_idx == visit.layer_index;
                },
                [&](const auto& onion_range, int frame) {
                    return pp::app::animation_onion_frame_alpha(onion_range, frame);
                });
        },
        std::forward<LogOnionRangeFailure>(log_onion_range_failure));

    if (use_blend) {
        composite_blend_cache();
    }
}

template <
    typename PrepareDensityRenderTarget,
    typename PrepareStandardRenderTarget,
    typename DrawMergedPass,
    typename DrawUnmergedPass>
inline void execute_legacy_canvas_draw_node_canvas_shell(
    bool use_density_render_target,
    bool draw_merged,
    PrepareDensityRenderTarget&& prepare_density_render_target,
    PrepareStandardRenderTarget&& prepare_standard_render_target,
    DrawMergedPass&& draw_merged_pass,
    DrawUnmergedPass&& draw_unmerged_pass)
{
    if (use_density_render_target) {
        prepare_density_render_target();
    } else {
        prepare_standard_render_target();
    }

    if (draw_merged) {
        draw_merged_pass();
        return;
    }

    draw_unmerged_pass();
}

template <typename NodeCanvasT>
inline void prepare_legacy_node_canvas_draw_setup(
    NodeCanvasT& node_canvas,
    const glm::vec4& box,
    const glm::ivec4& c,
    const glm::mat4& proj,
    const glm::mat4& camera)
{
    node_canvas.m_canvas->m_mv = camera;
    node_canvas.m_canvas->m_proj = proj;
    node_canvas.m_canvas->m_box = box;
    node_canvas.m_canvas->m_vp = c;

    for (int plane_index = 0; plane_index < 6; plane_index++) {
        node_canvas.m_canvas->m_plane_unproject[plane_index] =
            glm::inverse(node_canvas.m_canvas->m_proj * node_canvas.m_canvas->m_mv * node_canvas.m_canvas->m_plane_transform[plane_index]);
        node_canvas.m_canvas->m_plane_dir[plane_index] =
            -(node_canvas.m_canvas->m_plane_transform[plane_index] * glm::vec4(node_canvas.m_canvas->m_plane_origin[plane_index], 1));
        node_canvas.m_canvas->m_plane_shape[plane_index] =
            node_canvas.m_canvas->face_to_shape2D(plane_index);
    }
}

template <
    typename NodeCanvasT,
    typename ApplyCapability,
    typename SetActiveTextureUnit,
    typename DrawFacePlane>
inline void execute_legacy_canvas_draw_merged_pass(
    NodeCanvasT& node_canvas,
    const glm::mat4& proj,
    const glm::mat4& camera,
    ApplyCapability&& apply_node_canvas_capability,
    SetActiveTextureUnit&& set_active_texture_unit,
    DrawFacePlane&& draw_face_plane)
{
    execute_legacy_canvas_draw_merge_background_setup(
        {
            .draw_merged = true,
        },
        {
            .disable_blend = [&] {
                apply_node_canvas_capability(pp::renderer::gl::blend_state(), false);
            },
            .draw_checkerboard_plane = make_legacy_canvas_draw_merge_background_checkerboard_plane(
                proj,
                camera,
                node_canvas.m_canvas->m_layers.size() + 500.f,
                node_canvas.m_canvas->m_plane_transform,
                std::forward<DrawFacePlane>(draw_face_plane)),
        });

    const auto draw_merged_texture_plane = make_legacy_canvas_draw_merge_layer_texture_draw(
        &node_canvas.m_canvas->m_layers_merge,
        &node_canvas.m_sampler,
        &node_canvas.m_face_plane,
        std::forward<SetActiveTextureUnit>(set_active_texture_unit),
        proj,
        camera,
        node_canvas.m_canvas->m_plane_transform);

    for (int plane_index = 0; plane_index < 6; plane_index++) {
        draw_merged_texture_plane(plane_index);
    }
}

template <
    typename NodeCanvasT,
    typename ApplyViewport,
    typename ApplyCapability,
    typename DrawFacePlane,
    typename BindSampler,
    typename MakeLayerPathExecution,
    typename MakeCacheToScreenCheckerboardPlane,
    typename LogOnionRangeFailure>
inline void execute_legacy_canvas_draw_unmerged_node_canvas_pass(
    NodeCanvasT& node_canvas,
    bool use_blend,
    const glm::mat4& proj,
    const glm::mat4& camera,
    const glm::mat4& orientation,
    const glm::ivec4& c,
    ApplyViewport&& apply_node_canvas_viewport,
    ApplyCapability&& apply_node_canvas_capability,
    DrawFacePlane&& draw_face_plane,
    BindSampler&& bind_sampler,
    MakeLayerPathExecution&& make_layer_path_execution,
    MakeCacheToScreenCheckerboardPlane&& make_cache_to_screen_checkerboard_plane,
    LogOnionRangeFailure&& log_onion_range_failure)
{
    const auto prepare_blend_cache = [&] {
        apply_node_canvas_viewport(0, 0, node_canvas.m_cache_rtt.getWidth(), node_canvas.m_cache_rtt.getHeight());
        node_canvas.m_cache_rtt.bindFramebuffer();
        node_canvas.m_cache_rtt.clear({ 1, 1, 1, 0 });
    };

    const auto draw_background = [&] {
        execute_legacy_canvas_draw_merge_background_setup(
            {
                .use_blend = use_blend,
            },
            {
                .disable_blend = [&] {
                    apply_node_canvas_capability(pp::renderer::gl::blend_state(), false);
                },
                .draw_checkerboard_plane = make_legacy_canvas_draw_merge_background_checkerboard_plane(
                    proj,
                    camera,
                    node_canvas.m_canvas->m_layers.size() + 500.f,
                    node_canvas.m_canvas->m_plane_transform,
                    draw_face_plane),
            });
    };

    const auto configure_blend_state = [&](bool enable_shader_blend) {
        apply_node_canvas_capability(pp::renderer::gl::blend_state(), !enable_shader_blend);
    };

    const auto disable_depth_test = [&] {
        apply_node_canvas_capability(pp::renderer::gl::depth_test_state(), false);
    };

    execute_legacy_canvas_draw_unmerged_node_canvas_shell(
        node_canvas,
        use_blend,
        proj,
        camera,
        orientation,
        prepare_blend_cache,
        draw_background,
        configure_blend_state,
        disable_depth_test,
        make_layer_path_execution,
        log_onion_range_failure,
        [&] {
            node_canvas.m_cache_rtt.unbindFramebuffer();
            if (node_canvas.m_density != 1.f) {
                apply_node_canvas_viewport(0, 0, node_canvas.m_rtt.getWidth(), node_canvas.m_rtt.getHeight());
            } else {
                apply_node_canvas_viewport(c.x + App::I->off_x, c.y + App::I->off_y, c.z, c.w);
            }

            execute_legacy_canvas_draw_merge_cache_to_screen_composite(
                LegacyCanvasDrawMergeCacheToScreenCompositeUniforms {
                    .checkerboard = {
                        .colorize = false,
                    },
                    .texture = {
                        .mvp = glm::ortho<float>(-1, 1, -1, 1),
                        .texture_slot = 0,
                    },
                },
                {
                    .enable_blend = [&] {
                        apply_node_canvas_capability(pp::renderer::gl::blend_state(), true);
                    },
                    .draw_checkerboard_plane = make_cache_to_screen_checkerboard_plane(),
                    .bind_sampler = [&] {
                        bind_sampler();
                    },
                    .bind_cache_texture = [&] {
                        node_canvas.m_cache_rtt.bindTexture();
                    },
                    .draw_cache_texture = [&] {
                        draw_face_plane();
                    },
                    .unbind_cache_texture = [&] {
                        node_canvas.m_cache_rtt.unbindTexture();
                    },
                });
        });
}

template <
    typename NodeCanvasT,
    typename ApplyViewport,
    typename ApplyCapability,
    typename SetSampler,
    typename MakeLayerPathExecution,
    typename LogOnionRangeFailure>
inline void execute_legacy_canvas_draw_node_canvas_unmerged_pass(
    NodeCanvasT& node_canvas,
    bool use_blend,
    bool copy_blend_destination,
    const glm::mat4& proj,
    const glm::mat4& camera,
    const glm::mat4& layer_orientation,
    const glm::ivec4& c,
    ApplyViewport&& apply_node_canvas_viewport,
    ApplyCapability&& apply_node_canvas_capability,
    SetSampler&& set_sampler,
    MakeLayerPathExecution&& make_layer_path_execution,
    LogOnionRangeFailure&& log_onion_range_failure)
{
    const auto& brush = node_canvas.m_canvas->m_current_stroke->m_brush;
    execute_legacy_canvas_draw_unmerged_node_canvas_pass(
        node_canvas,
        use_blend,
        proj,
        camera,
        layer_orientation,
        c,
        std::forward<ApplyViewport>(apply_node_canvas_viewport),
        std::forward<ApplyCapability>(apply_node_canvas_capability),
        [&] {
            node_canvas.m_face_plane.draw_fill();
        },
        std::forward<SetSampler>(set_sampler),
        [&](size_t layer_index, int plane_index, const glm::mat4& plane_mvp_z) {
            return make_layer_path_execution(
                layer_index,
                plane_index,
                plane_mvp_z,
                brush.get(),
                copy_blend_destination,
                node_canvas.m_canvas->m_cam_fov < 20.f);
        },
        [&] {
            return make_legacy_canvas_draw_merge_cache_to_screen_checkerboard_plane(
                proj,
                camera,
                node_canvas.m_canvas->m_layers.size() + 500.f,
                node_canvas.m_canvas->m_plane_transform,
                [&] {
                    node_canvas.m_face_plane.draw_fill();
                });
        },
        std::forward<LogOnionRangeFailure>(log_onion_range_failure));
}

inline void execute_legacy_canvas_draw_merge_plane_setup(
    const LegacyCanvasDrawMergePlaneSetupUniforms& uniforms,
    const LegacyCanvasDrawMergePlaneSetupExecution& execution)
{
    execution.clear_plane();

    if (uniforms.use_blend) {
        execution.disable_blend();
        execution.clear_plane();
        return;
    }

    if (uniforms.draw_checkerboard) {
        setup_legacy_canvas_draw_merge_checkerboard_shader(uniforms.checkerboard);
        execution.draw();
    }

    execution.enable_blend();
}

inline void execute_legacy_canvas_draw_merge_final_plane_composite(
    const LegacyCanvasDrawMergeFinalPlaneCompositeUniforms& uniforms,
    const LegacyCanvasDrawMergeFinalPlaneCompositeExecution& execution)
{
    execution.bind_merged_texture_copy_target();
    execution.copy_merged_framebuffer();

    execution.enable_blend();

    if (uniforms.draw_checkerboard) {
        setup_legacy_canvas_draw_merge_checkerboard_shader(uniforms.checkerboard);
        execution.draw();
    }

    execution.bind_sampler();
    execution.bind_merged_texture();
    setup_legacy_canvas_draw_merge_texture_shader(uniforms.texture);
    execution.draw();
    execution.unbind_merged_texture();
}

inline void execute_legacy_canvas_draw_merge_cache_to_screen_composite(
    const LegacyCanvasDrawMergeCacheToScreenCompositeUniforms& uniforms,
    const LegacyCanvasDrawMergeCacheToScreenCompositeExecution& execution)
{
    execution.enable_blend();

    for (int plane_index = 0; plane_index < 6; ++plane_index) {
        execution.draw_checkerboard_plane(uniforms.checkerboard, plane_index);
    }

    execution.bind_sampler();
    execution.bind_cache_texture();
    setup_legacy_canvas_draw_merge_texture_shader(uniforms.texture);
    execution.draw_cache_texture();
    execution.unbind_cache_texture();
}

template <typename PlaneTransform, typename DrawPlane>
[[nodiscard]] inline auto make_legacy_canvas_draw_merge_cache_to_screen_checkerboard_plane(
    glm::mat4 proj,
    glm::mat4 camera,
    float layer_scale,
    PlaneTransform plane_transform,
    DrawPlane draw_plane)
{
    return [proj, camera, layer_scale, plane_transform, draw_plane](
               const LegacyCanvasDrawMergeCheckerboardUniforms& uniforms,
               int plane_index) {
        auto checkerboard_uniforms = uniforms;
        checkerboard_uniforms.mvp = proj * camera *
            glm::scale(glm::vec3(layer_scale)) *
            plane_transform[plane_index] *
            glm::translate(glm::vec3(0, 0, -1.f));

        setup_legacy_canvas_draw_merge_checkerboard_shader(checkerboard_uniforms);
        draw_plane();
    };
}

inline void execute_legacy_canvas_draw_merge_display_resolve(
    const LegacyCanvasDrawMergeDisplayResolveUniforms& uniforms,
    const LegacyCanvasDrawMergeDisplayResolveExecution& execution)
{
    execution.unbind_resolve_framebuffer();
    execution.clear_color_buffer();
    execution.apply_viewport();
    execution.bind_sampler();
    execution.bind_resolve_texture();
    setup_legacy_canvas_draw_merge_texture_shader(uniforms.texture);
    execution.draw();
    execution.unbind_resolve_texture();
}

inline void execute_legacy_canvas_draw_merge_post_draw(
    bool smask_active,
    bool draw_mask_overlay,
    int smask_mode,
    bool draw_grid_modes,
    const LegacyCanvasDrawMergePostDrawExecution& execution)
{
    if (smask_active || draw_mask_overlay) {
        if (smask_mode == 1) {
            execution.draw_mask_free();
        } else if (smask_mode == 2) {
            execution.draw_mask_line();
        }
    }

    if (smask_active) {
        execution.draw_smask_faces();
    }

    if (draw_grid_modes) {
        execution.draw_grid_modes();
    }

    execution.draw_heightmap();
    execution.draw_current_modes();
}

template <
    typename DrawMaskFree,
    typename DrawMaskLine,
    typename DrawSmaskFaces,
    typename DrawGridModes,
    typename DrawHeightmap,
    typename DrawCurrentModes>
inline void execute_legacy_canvas_draw_merge_post_draw_callbacks(
    bool smask_active,
    bool draw_mask_overlay,
    int smask_mode,
    bool draw_grid_modes_enabled,
    DrawMaskFree&& draw_mask_free,
    DrawMaskLine&& draw_mask_line,
    DrawSmaskFaces&& draw_smask_faces,
    DrawGridModes&& draw_grid_modes_callback,
    DrawHeightmap&& draw_heightmap,
    DrawCurrentModes&& draw_current_modes)
{
    execute_legacy_canvas_draw_merge_post_draw(
        smask_active,
        draw_mask_overlay,
        smask_mode,
        draw_grid_modes_enabled,
        LegacyCanvasDrawMergePostDrawExecution {
            .draw_mask_free = std::forward<DrawMaskFree>(draw_mask_free),
            .draw_mask_line = std::forward<DrawMaskLine>(draw_mask_line),
            .draw_smask_faces = std::forward<DrawSmaskFaces>(draw_smask_faces),
            .draw_grid_modes = std::forward<DrawGridModes>(draw_grid_modes_callback),
            .draw_heightmap = std::forward<DrawHeightmap>(draw_heightmap),
            .draw_current_modes = std::forward<DrawCurrentModes>(draw_current_modes),
        });
}

inline void execute_legacy_canvas_draw_merge_smask_faces(
    const LegacyCanvasDrawMergeTextureMaskUniforms& uniforms,
    const glm::mat4& proj,
    const glm::mat4& camera,
    float layer_scale,
    const std::array<glm::mat4, 6>& plane_transform,
    const LegacyCanvasDrawMergeSmaskFacesExecution& execution)
{
    setup_legacy_canvas_draw_merge_texture_mask_shader(uniforms);
    execution.set_active_texture_unit();
    execution.enable_blend();

    for (int plane_index = 0; plane_index < 6; ++plane_index) {
        auto plane_mvp = proj * camera *
            glm::scale(glm::vec3(layer_scale)) *
            plane_transform[plane_index] *
            glm::translate(glm::vec3(0, 0, -1.f));

        apply_legacy_canvas_draw_merge_mvp(plane_mvp);
        execution.bind_face_texture(plane_index);
        execution.draw_face();
        execution.unbind_face_texture(plane_index);
    }
}

} // namespace pp::panopainter
