#include "pch.h"
#include "log.h"
#include "node_stroke_preview.h"
#include "texture.h"
#include "shader.h"
#include "bezier.h"
#include "canvas.h"
#include "app.h"
#include "legacy_canvas_draw_merge_services.h"
#include "legacy_canvas_stroke_composite_services.h"
#include "legacy_canvas_stroke_execution_services.h"
#include "legacy_canvas_stroke_preview_services.h"
#include "legacy_canvas_stroke_shader_services.h"
#include "legacy_canvas_stroke_services.h"
#include "legacy_node_stroke_preview_execution_services.h"
#include "legacy_ui_gl_dispatch.h"
#include "paint_renderer/compositor.h"
#include "renderer_gl/opengl_capabilities.h"
#include "util.h"
#include <array>
#include <cstdint>

namespace {

pp::renderer::RenderDeviceFeatures stroke_preview_render_device_features() noexcept
{
    return ShaderManager::render_device_features();
}

void set_active_texture_unit(std::uint32_t unit_index)
{
    pp::legacy::ui_gl::activate_texture_unit(unit_index, "NodeStrokePreview");
}

void unbind_texture_2d()
{
    pp::legacy::ui_gl::unbind_texture_2d("NodeStrokePreview");
}

void apply_stroke_preview_viewport(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height)
{
    pp::legacy::ui_gl::apply_viewport(x, y, width, height, "NodeStrokePreview");
}

pp::renderer::gl::OpenGlViewportRect query_stroke_preview_viewport()
{
    return pp::legacy::ui_gl::query_viewport_rect("NodeStrokePreview");
}

std::array<float, 4> query_stroke_preview_clear_color()
{
    return pp::legacy::ui_gl::query_clear_color("NodeStrokePreview");
}

void apply_stroke_preview_clear_color(std::array<float, 4> color)
{
    pp::legacy::ui_gl::set_clear_color(color, "NodeStrokePreview");
}

void apply_stroke_preview_scissor(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height)
{
    pp::legacy::ui_gl::apply_scissor_rect(x, y, width, height, "NodeStrokePreview");
}

void apply_stroke_preview_capability(std::uint32_t state, bool enabled)
{
    pp::legacy::ui_gl::set_capability(state, enabled, "NodeStrokePreview");
}

namespace stroke_preview_composite_slots {
constexpr std::uint32_t kBackground = 0U;
constexpr std::uint32_t kStroke = 1U;
constexpr std::uint32_t kDual = 3U;
constexpr std::uint32_t kPattern = 4U;
}

namespace stroke_preview_live_slots {
constexpr std::uint32_t kTip = 0U;
constexpr std::uint32_t kDestination = 1U;
constexpr std::uint32_t kPattern = 2U;
constexpr std::uint32_t kMixer = 3U;
constexpr std::uint32_t kReservedLinear = 4U;
}

struct StrokePreviewCompositePassInputs {
    glm::vec2 resolution;
    glm::vec2 pattern_scale;
    const Brush& brush;
    const pp::paint_renderer::CanvasStrokeCompositePassPlan& composite_pass;
    Texture2D& background_texture;
    Texture2D& stroke_texture;
    Texture2D& dual_texture;
    Sampler& linear_sampler;
    Sampler& repeat_sampler;
    std::function<void()> draw_composite;
};

pp::panopainter::LegacyNodeStrokePreviewMixPassRequest make_stroke_preview_mix_pass_request(
    const Brush& brush,
    glm::vec2 resolution) noexcept
{
    return {
        .resolution = resolution,
        .pattern_scale = brush.m_pattern_scale,
        .pattern_flipx = brush.m_pattern_flipx,
        .pattern_flipy = brush.m_pattern_flipy,
        .pattern_invert = brush.m_pattern_invert,
        .pattern_brightness = brush.m_pattern_brightness,
        .pattern_contrast = brush.m_pattern_contrast,
        .pattern_depth = brush.m_pattern_depth,
        .pattern_rand_offset = brush.m_pattern_rand_offset,
        .pattern_enabled = brush.m_pattern_enabled,
        .pattern_eachsample = brush.m_pattern_eachsample,
        .tip_wet = brush.m_tip_wet,
        .tip_mix = brush.m_tip_mix,
        .tip_noise = brush.m_tip_noise,
        .dual_enabled = brush.m_dual_enabled,
        .dual_blend_mode = brush.m_dual_blend_mode,
        .pattern_blend_mode = brush.m_pattern_blend_mode,
        .dual_opacity = brush.m_dual_opacity,
        .blend_mode = brush.m_blend_mode,
    };
}

pp::panopainter::LegacyStrokeCompositeUniforms make_stroke_preview_mix_composite_uniforms(
    const pp::panopainter::LegacyNodeStrokePreviewMixPassPlan::ShaderPlan& shader_plan) noexcept
{
    return {
        .resolution = shader_plan.resolution,
        .pattern = {
            .scale = shader_plan.pattern_scale,
            .invert = shader_plan.pattern_invert,
            .brightness = shader_plan.pattern_brightness,
            .contrast = shader_plan.pattern_contrast,
            .depth = shader_plan.pattern_depth,
            .blend_mode = shader_plan.pattern_blend_mode,
            .offset = shader_plan.pattern_offset,
        },
        .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f),
        .layer_alpha = 1.0f,
        .alpha_lock = false,
        .mask_enabled = false,
        .use_fragcoord = false,
        .blend_mode = shader_plan.blend_mode,
        .use_dual = shader_plan.use_dual,
        .dual_blend_mode = shader_plan.dual_blend_mode,
        .dual_alpha = shader_plan.dual_alpha,
        .use_pattern = shader_plan.use_pattern,
    };
}

void execute_stroke_preview_final_composite_pass(const StrokePreviewCompositePassInputs& inputs)
{
    [[maybe_unused]] const bool composite_ok =
        pp::panopainter::execute_legacy_node_stroke_preview_final_composite(
            pp::panopainter::LegacyNodeStrokePreviewFinalCompositeRequest {
                .resolution = inputs.resolution,
                .pattern_scale = inputs.pattern_scale,
                .brush = &inputs.brush,
                .composite_pass = &inputs.composite_pass,
                .setup_composite_shader = [&] {
                    pp::panopainter::setup_legacy_stroke_composite_shader(
                        pp::panopainter::LegacyStrokeCompositeUniforms {
                            .resolution = inputs.resolution,
                            .pattern = {
                                .scale = inputs.pattern_scale,
                                .invert = static_cast<float>(inputs.brush.m_pattern_invert),
                                .brightness = inputs.brush.m_pattern_brightness,
                                .contrast = inputs.brush.m_pattern_contrast,
                                .depth = inputs.brush.m_pattern_depth,
                                .blend_mode = inputs.composite_pass.pattern_blend_mode,
                                .offset = glm::vec2(inputs.brush.m_pattern_rand_offset ? 0.5f : 0.0f),
                            },
                            .mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f),
                            .layer_alpha = 1.0f,
                            .alpha_lock = false,
                            .mask_enabled = false,
                            .use_fragcoord = false,
                            .blend_mode = inputs.brush.m_blend_mode,
                            .use_dual = inputs.composite_pass.use_dual,
                            .dual_blend_mode = inputs.composite_pass.dual_blend_mode,
                            .dual_alpha = inputs.composite_pass.dual_alpha,
                            .use_pattern = inputs.composite_pass.use_pattern,
                        });
                },
                .bind_composite_samplers = [&] {
                    inputs.linear_sampler.bind(stroke_preview_composite_slots::kBackground);
                    inputs.linear_sampler.bind(stroke_preview_composite_slots::kStroke);
                    inputs.linear_sampler.bind(2U);
                    inputs.linear_sampler.bind(stroke_preview_composite_slots::kDual);
                    inputs.repeat_sampler.bind(stroke_preview_composite_slots::kPattern);
                },
                .bind_composite_inputs = [&] {
                    set_active_texture_unit(stroke_preview_composite_slots::kBackground);
                    inputs.background_texture.bind();
                    set_active_texture_unit(stroke_preview_composite_slots::kStroke);
                    inputs.stroke_texture.bind();
                    set_active_texture_unit(stroke_preview_composite_slots::kDual);
                    inputs.dual_texture.bind();
                    set_active_texture_unit(stroke_preview_composite_slots::kPattern);
                    inputs.brush.m_pattern_texture ?
                        inputs.brush.m_pattern_texture->bind() :
                        unbind_texture_2d();
                },
                .draw_composite = [&] {
                    inputs.draw_composite();
                },
    });
    assert(composite_ok);
}

void copy_stroke_preview_result_to_texture(Texture2D& texture, glm::vec2 size);

void execute_stroke_preview_final_composite_and_copy(
    const StrokePreviewCompositePassInputs& inputs,
    Texture2D& preview_texture,
    glm::vec2 size)
{
    execute_stroke_preview_final_composite_pass(inputs);
    copy_stroke_preview_result_to_texture(preview_texture, size);
}

void copy_stroke_preview_framebuffer_to_texture(
    Texture2D& texture,
    glm::vec2 size,
    std::uint32_t texture_unit)
{
    set_active_texture_unit(texture_unit);
    texture.bind();
    copy_framebuffer_to_texture_2d(
        0,
        0,
        0,
        0,
        static_cast<int>(size.x),
        static_cast<int>(size.y));
}

void bind_stroke_preview_live_samplers(
    Sampler& mipmap_sampler,
    Sampler& linear_sampler,
    Sampler& repeat_sampler)
{
    mipmap_sampler.bind(stroke_preview_live_slots::kTip);
    linear_sampler.bind(stroke_preview_live_slots::kDestination);
    repeat_sampler.bind(stroke_preview_live_slots::kPattern);
    linear_sampler.bind(stroke_preview_live_slots::kMixer);
    linear_sampler.bind(stroke_preview_live_slots::kReservedLinear);
}

void bind_stroke_preview_dual_pass_textures(const Brush& dual_brush)
{
    set_active_texture_unit(stroke_preview_live_slots::kTip);
    dual_brush.m_tip_texture ?
        dual_brush.m_tip_texture->bind() :
        unbind_texture_2d();
}

void bind_stroke_preview_main_pass_textures(
    const Brush& brush,
    Texture2D& stroke_destination_texture,
    RTT& mixer_rtt,
    bool copy_stroke_destination,
    bool uses_mixer)
{
    pp::panopainter::bind_legacy_node_stroke_preview_main_pass_textures(
        pp::panopainter::make_legacy_node_stroke_preview_main_pass_texture_dispatch(
            [&](int texture_slot) {
                set_active_texture_unit(texture_slot);
            },
            [&] {
                brush.m_tip_texture ?
                    brush.m_tip_texture->bind() :
                    unbind_texture_2d();
            },
            [&] {
                stroke_destination_texture.bind();
            },
            [&] {
                brush.m_pattern_texture ?
                    brush.m_pattern_texture->bind() :
                    unbind_texture_2d();
            },
            [&] {
                mixer_rtt.bindTexture();
            }),
        copy_stroke_destination,
        uses_mixer);
}

void bind_stroke_preview_destination_texture(Texture2D& texture)
{
    set_active_texture_unit(stroke_preview_live_slots::kDestination);
    texture.bind();
}

void unbind_stroke_preview_destination_texture(Texture2D& texture)
{
    set_active_texture_unit(stroke_preview_live_slots::kDestination);
    texture.unbind();
}

void copy_stroke_preview_destination_texture_region(
    int src_x,
    int src_y,
    int dst_x,
    int dst_y,
    int width,
    int height)
{
    copy_framebuffer_to_texture_2d(src_x, src_y, dst_x, dst_y, width, height);
}

std::array<pp::paint_renderer::CanvasStrokePoint, 4> make_stroke_preview_sample_points(
    const std::array<vertex_t, 4>& vertices)
{
    return {
        pp::paint_renderer::CanvasStrokePoint { .x = vertices[0].pos.x, .y = vertices[0].pos.y },
        pp::paint_renderer::CanvasStrokePoint { .x = vertices[1].pos.x, .y = vertices[1].pos.y },
        pp::paint_renderer::CanvasStrokePoint { .x = vertices[2].pos.x, .y = vertices[2].pos.y },
        pp::paint_renderer::CanvasStrokePoint { .x = vertices[3].pos.x, .y = vertices[3].pos.y },
    };
}

void upload_stroke_preview_brush_vertices(DynamicShape& brush_shape, std::span<const vertex_t> vertices)
{
    brush_shape.update_vertices(
        const_cast<vertex_t*>(vertices.data()),
        static_cast<int>(vertices.size()));
}

glm::vec4 execute_stroke_preview_sample_pass(
    std::array<vertex_t, 4>& vertices,
    glm::vec2 target_size,
    Texture2D& blend_texture,
    DynamicShape& brush_shape,
    bool copy_stroke_destination)
{
    const auto sample_points = make_stroke_preview_sample_points(vertices);
    const auto result = pp::panopainter::execute_legacy_canvas_stroke_sample(
        pp::panopainter::LegacyStrokeSampleExecutionRequest {
            .context = "NodeStrokePreview::stroke_draw_samples",
            .target_size = target_size,
            .vertices = vertices,
            .sample_points = sample_points,
            .copy_stroke_destination = copy_stroke_destination,
            .bind_destination_texture = [&] {
                bind_stroke_preview_destination_texture(blend_texture);
            },
            .copy_framebuffer_to_destination_texture = [](
                int src_x,
                int src_y,
                int dst_x,
                int dst_y,
                int width,
                int height) {
                copy_stroke_preview_destination_texture_region(
                    src_x,
                    src_y,
                    dst_x,
                    dst_y,
                    width,
                    height);
            },
            .unbind_destination_texture = [&] {
                unbind_stroke_preview_destination_texture(blend_texture);
            },
            .upload_brush_vertices = [&](std::span<const vertex_t> brush_vertices) {
                upload_stroke_preview_brush_vertices(brush_shape, brush_vertices);
            },
            .draw_brush_shape = [&] {
                brush_shape.draw_fill();
            },
        });
    return result.dirty_bounds;
}

void execute_stroke_preview_background_capture_pass(
    glm::vec2 size,
    bool colorize,
    Texture2D& background_texture,
    const std::function<void()>& draw_checkerboard)
{
    pp::panopainter::execute_legacy_stroke_preview_background_capture(
        [&] {
            const float aspect = size.x / size.y;
            pp::panopainter::setup_legacy_canvas_draw_merge_checkerboard_shader(
                pp::panopainter::LegacyCanvasDrawMergeCheckerboardUniforms {
                    .mvp = glm::ortho(-.5f, .5f, -.5f / aspect, .5f / aspect, -1.f, 1.f),
                    .colorize = colorize,
                });
        },
        [&] {
            draw_checkerboard();
        },
        [&] {
            background_texture.bind();
        },
        [](
            int src_x,
            int src_y,
            int dst_x,
            int dst_y,
            int width,
            int height) {
            copy_framebuffer_to_texture_2d(src_x, src_y, dst_x, dst_y, width, height);
        },
        pp::panopainter::LegacyStrokePreviewCopySize {
            .width = static_cast<int>(size.x),
            .height = static_cast<int>(size.y),
        });
}

void copy_stroke_preview_result_to_texture(Texture2D& preview_texture, glm::vec2 size)
{
    [[maybe_unused]] const bool copy_ok =
        pp::panopainter::copy_legacy_node_stroke_preview_result(
            pp::panopainter::LegacyNodeStrokePreviewCopyResultRequest {
                .preview_texture = &preview_texture,
                .size = size,
                .copy_framebuffer_to_texture = [](
                    int src_x,
                    int src_y,
                    int dst_x,
                    int dst_y,
                    int width,
                    int height) {
                    copy_framebuffer_to_texture_2d(src_x, src_y, dst_x, dst_y, width, height);
                },
            });
    assert(copy_ok);
}

}

std::atomic_int NodeStrokePreview::s_instances{ 0 };
std::atomic_bool NodeStrokePreview::s_running{ false };
std::mutex NodeStrokePreview::s_render_mutex;
std::thread NodeStrokePreview::s_renderer;
BlockingQueue<std::shared_ptr<NodeStrokePreview>> NodeStrokePreview::s_queue;

RTT NodeStrokePreview::m_rtt;
RTT NodeStrokePreview::m_rtt_mixer;
Texture2D NodeStrokePreview::m_tex; // blending tmp texture
Texture2D NodeStrokePreview::m_tex_dual;
Texture2D NodeStrokePreview::m_tex_background;
Sampler NodeStrokePreview::m_sampler_linear;
Sampler NodeStrokePreview::m_sampler_linear_repeat;
Sampler NodeStrokePreview::m_sampler_mipmap;
DynamicShape NodeStrokePreview::m_brush_shape;


void NodeStrokePreview::terminate_renderer()
{
    if (s_running && s_renderer.joinable())
    {
        s_running = false;
        s_queue.UnlockGetters();
        s_renderer.join();
    }
}

void NodeStrokePreview::empty_queue()
{
    s_queue.q.clear();
}

Node* NodeStrokePreview::clone_instantiate() const
{
    return new NodeStrokePreview();
}

void NodeStrokePreview::clone_copy(Node* dest) const
{
    NodeBorder::clone_copy(dest);
}

void NodeStrokePreview::clone_children(Node* dest) const
{
    // stop children cloning
}

void NodeStrokePreview::clone_finalize(Node* dest) const
{
    NodeStrokePreview* n = (NodeStrokePreview*)dest;
    n->init_controls();
}

void NodeStrokePreview::init_controls()
{
//     TextureManager::load("data/thumbs/Round-Hard.png");
//     Canvas::I->m_current_brush.m_tex_id = const_hash("data/thumbs/Round-Hard.png");
}

void NodeStrokePreview::restore_context()
{
    NodeBorder::restore_context();
    init_controls();
    if (m_size.x > 0 && m_size.y > 0)
        m_tex_preview.create(static_cast<int>(m_size.x), static_cast<int>(m_size.y));
    draw_stroke();
}

void NodeStrokePreview::clear_context()
{
    NodeBorder::clear_context();
    m_tex_preview.destroy();
}

void NodeStrokePreview::stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz)
{
    const auto& b = m_brush;
    const auto mix_pass = pp::panopainter::plan_legacy_node_stroke_preview_mix_pass(
        make_stroke_preview_mix_pass_request(*b, m_size));
    gl_state gl;
    [[maybe_unused]] const bool mix_ok = pp::panopainter::execute_legacy_node_stroke_preview_mix_pass(
        pp::panopainter::LegacyNodeStrokePreviewMixExecutionRequest {
            .shader = mix_pass.shader,
            .mixer_width = m_rtt_mixer.getWidth(),
            .mixer_height = m_rtt_mixer.getHeight(),
            .scissor_x = static_cast<int>(bb_min.x),
            .scissor_y = static_cast<int>(bb_min.y),
            .scissor_width = static_cast<int>(bb_sz.x),
            .scissor_height = static_cast<int>(bb_sz.y),
            .save_state = [&] {
                gl.save();
            },
            .setup_mix_shader = [&](const pp::panopainter::LegacyNodeStrokePreviewMixPassPlan::ShaderPlan& shader_plan) {
                pp::panopainter::setup_legacy_stroke_composite_shader(
                    make_stroke_preview_mix_composite_uniforms(shader_plan));
            },
            .bind_mixer_framebuffer = [&] {
                m_rtt_mixer.bindFramebuffer();
            },
            .configure_mix_target_state = [&](
                int mixer_width,
                int mixer_height,
                int scissor_x,
                int scissor_y,
                int scissor_width,
                int scissor_height) {
                apply_stroke_preview_viewport(0, 0, mixer_width, mixer_height);
                apply_stroke_preview_capability(pp::renderer::gl::depth_test_state(), false);
                apply_stroke_preview_capability(pp::renderer::gl::scissor_test_state(), true);
                apply_stroke_preview_capability(pp::renderer::gl::blend_state(), false);
                apply_stroke_preview_scissor(
                    scissor_x,
                    scissor_y,
                    scissor_width,
                    scissor_height);
            },
            .bind_mix_inputs = [&] {
                m_sampler_linear.bind(stroke_preview_composite_slots::kBackground);
                set_active_texture_unit(stroke_preview_composite_slots::kBackground);
                m_tex_background.bind();
                set_active_texture_unit(stroke_preview_composite_slots::kStroke);
                m_rtt.bindTexture();
                set_active_texture_unit(stroke_preview_composite_slots::kDual);
                m_tex_dual.bind();
                set_active_texture_unit(stroke_preview_composite_slots::kPattern);
                b->m_pattern_texture ?
                    b->m_pattern_texture->bind() :
                    unbind_texture_2d();
            },
            .draw_mix = [&] {
                m_plane.draw_fill();
            },
            .unbind_mixer_framebuffer = [&] {
                m_rtt_mixer.unbindFramebuffer();
            },
            .restore_state = [&] {
                gl.restore();
            },
        });
    assert(mix_ok);
}

glm::vec4 NodeStrokePreview::stroke_draw_samples(
    std::array<vertex_t, 4>& P,
    Texture2D& blend_tex,
    bool copy_stroke_destination)
{
    const glm::vec2 size = { m_rtt.getWidth(), m_rtt.getHeight() };
    return execute_stroke_preview_sample_pass(
        P,
        size,
        blend_tex,
        m_brush_shape,
        copy_stroke_destination);
}

std::vector<NodeStrokePreview::StrokeFrame> NodeStrokePreview::stroke_draw_compute(Stroke& stroke, float zoom) const
{
    auto samples = stroke.compute_samples();
    StrokeSample previous_sample = stroke.m_prev_sample;
    previous_sample.size *= zoom;
    for (auto& sample : samples) {
        sample.size *= zoom;
    }

    return pp::panopainter::plan_legacy_canvas_stroke_frames(
        pp::panopainter::LegacyCanvasStrokeComputeRequest {
            .previous_sample = previous_sample,
            .samples = samples,
            .zoom = 1.0f,
            .mixer_size = glm::vec2(m_rtt.getWidth(), m_rtt.getHeight()),
        },
        [](
            std::array<vertex_t, 4>& brush_quad,
            bool /*project_3d*/,
            glm::mat4 /*model_view*/) {
            return brush_quad;
        },
        [](
            glm::vec4 mixer_rect,
            glm::vec4 color,
            float flow,
            float opacity,
            std::array<vertex_t, 4>&& shapes) {
            return StrokeFrame {
                .col = color,
                .flow = flow,
                .opacity = opacity,
                .shapes = std::move(shapes),
                .m_mixer_rect = mixer_rect,
            };
        });
}

void NodeStrokePreview::draw_stroke_immediate()
{
    if (m_size.x == 0 || m_size.y == 0)
        return;

    const auto vp = query_stroke_preview_viewport();
    const auto cc = query_stroke_preview_clear_color();
    
    float zoom = root()->m_zoom;

    glm::vec2 size = { m_rtt.getWidth(), m_rtt.getHeight() };
    const auto stroke_setup = pp::panopainter::plan_legacy_node_stroke_preview_stroke_setup(
        pp::panopainter::LegacyNodeStrokePreviewStrokeSetupRequest {
            .preview_size = m_size,
            .zoom = zoom,
            .brush_tip_size = m_brush->m_tip_size,
            .stroke_max_size_override = m_max_size,
            .pad_override = m_pad_override,
            .tip_size_pressure = m_brush->m_tip_size_pressure,
            .dual_enabled = m_brush->m_dual_enabled,
            .dual_size = m_brush->m_dual_size,
            .pattern_scale = m_brush->m_pattern_scale,
            .pattern_flipx = m_brush->m_pattern_flipx,
            .pattern_flipy = m_brush->m_pattern_flipy,
        });
    glm::mat4 ortho_proj = glm::ortho<float>(0, size.x, 0, size.y, -1, 1);
    apply_stroke_preview_viewport(0, 0, m_rtt.getWidth(), m_rtt.getHeight());
    m_rtt.bindFramebuffer();
    m_rtt.clear();
    bind_stroke_preview_live_samplers(
        m_sampler_mipmap,
        m_sampler_linear,
        m_sampler_linear_repeat);

    const auto& b = m_brush;

    Stroke m_stroke;
    Stroke m_dual_stroke;

    m_stroke.m_filter_points = false;
    m_stroke.m_max_size = stroke_setup.stroke_max_size;
    m_stroke.m_camera.fov = Canvas::I->m_cam_fov;
    m_stroke.m_camera.rot = Canvas::I->m_cam_rot;
    m_stroke.reset(true);
    m_stroke.start(b);

    auto dual_brush = std::make_shared<Brush>();
    dual_brush->m_tip_scale = b->m_dual_scale;
    dual_brush->m_tip_angle = b->m_dual_angle;
    dual_brush->m_tip_flow = b->m_dual_flow;
    dual_brush->m_tip_opacity = b->m_dual_opacity;
    dual_brush->m_tip_flipx = b->m_dual_flipx;
    dual_brush->m_tip_flipy = b->m_dual_flipy;
    dual_brush->m_tip_invert = b->m_dual_invert;
    dual_brush->m_blend_mode = b->m_dual_blend_mode;
    dual_brush->m_tip_randflipx = b->m_dual_randflip;
    dual_brush->m_tip_randflipy = b->m_dual_randflip;
    dual_brush->m_tip_size = b->m_dual_size * b->m_tip_size;
    dual_brush->m_tip_spacing = b->m_dual_spacing;
    dual_brush->m_jitter_scatter = b->m_dual_scatter;
    dual_brush->m_jitter_scatter_bothaxis = b->m_dual_scatter_bothaxis;
    dual_brush->m_jitter_angle = b->m_dual_rotate;
    dual_brush->m_tip_texture = b->m_dual_texture;
    dual_brush->m_tip_aspect = b->m_dual_aspect;

    if (stroke_setup.dual_enabled)
    {
        m_dual_stroke.m_filter_points = false;
        m_dual_stroke.m_max_size = stroke_setup.dual_stroke_max_size;
        m_dual_stroke.m_camera.fov = Canvas::I->m_cam_fov;
        m_dual_stroke.m_camera.rot = Canvas::I->m_cam_rot;
        m_dual_stroke.reset(true);
        m_dual_stroke.start(dual_brush);
    }

    for (const auto& point : stroke_setup.points)
    {
        m_stroke.add_point(point.position, point.pressure);
        if (stroke_setup.dual_enabled)
            m_dual_stroke.add_point(point.position, point.pressure);
    }

    const glm::vec2 patt_scale = stroke_setup.pattern_scale;

    apply_stroke_preview_capability(pp::renderer::gl::blend_state(), false);
    const auto pass_orchestration = pp::panopainter::plan_legacy_node_stroke_preview_pass_orchestration(
        pp::panopainter::LegacyNodeStrokePreviewPassOrchestrationRequest {
            .features = stroke_preview_render_device_features(),
            .preview_size = size,
            .pattern_scale = b->m_pattern_scale,
            .pattern_flipx = b->m_pattern_flipx,
            .pattern_flipy = b->m_pattern_flipy,
            .pattern_invert = b->m_pattern_invert,
            .pattern_brightness = b->m_pattern_brightness,
            .pattern_contrast = b->m_pattern_contrast,
            .pattern_depth = b->m_pattern_depth,
            .pattern_rand_offset = b->m_pattern_rand_offset,
            .pattern_enabled = b->m_pattern_enabled,
            .pattern_eachsample = b->m_pattern_eachsample,
            .tip_mix = b->m_tip_mix,
            .tip_wet = b->m_tip_wet,
            .tip_noise = b->m_tip_noise,
            .dual_enabled = b->m_dual_enabled,
            .dual_blend_mode = b->m_dual_blend_mode,
            .dual_opacity = b->m_dual_opacity,
            .pattern_blend_mode = b->m_pattern_blend_mode,
            .blend_mode = b->m_blend_mode,
            .mvp = ortho_proj,
        });
    const bool copy_stroke_destination = pass_orchestration.copy_stroke_destination;
    const auto& material = pass_orchestration.material;
    pp::panopainter::setup_legacy_stroke_shader(pass_orchestration.stroke_shader);

    const bool sequence_ok = pp::panopainter::execute_legacy_node_stroke_preview_pass_sequence(
        make_stroke_draw_immediate_pass_sequence_request(
            *b,
            pass_orchestration,
            dual_brush,
            copy_stroke_destination,
            zoom,
            size));
    assert(sequence_ok);

    m_rtt.unbindFramebuffer();

    apply_stroke_preview_viewport(vp.x, vp.y, vp.width, vp.height);
    apply_stroke_preview_clear_color(cc);
}

NodeStrokePreview::StrokeMainLivePassRequest
NodeStrokePreview::make_stroke_draw_immediate_main_live_pass_request(
    const Brush& brush,
    const pp::panopainter::LegacyNodeStrokePreviewPassOrchestrationPlan& pass_orchestration,
    bool copy_stroke_destination,
    float zoom,
    const glm::vec2& size)
{
    return StrokeMainLivePassRequest {
        .setup_blend_uniforms = [&] {
            pp::panopainter::apply_legacy_stroke_blend_uniforms(
                pass_orchestration.material.stroke_pass.uses_pattern,
                brush.m_tip_mix,
                brush.m_tip_wet,
                brush.m_tip_noise);
        },
        .bind_main_pass_textures = [&] {
            bind_stroke_preview_main_pass_textures(
                brush,
                m_tex,
                m_rtt_mixer,
                copy_stroke_destination,
                pass_orchestration.material.stroke_pass.uses_mixer);
        },
        .clear_target = [&] {
            m_rtt.clear();
        },
        .compute_frames = [&] {
            return stroke_draw_compute(m_stroke, zoom);
        },
        .before_frame = [&](auto& frame) {
            if (brush.m_tip_mix > 0.f)
            {
                stroke_draw_mix(xy(frame.m_mixer_rect), zw(frame.m_mixer_rect));
            }

            frame.col = brush.m_blend_mode != 0 || brush.m_tip_mix > 0.f ?
                glm::vec4 { .7, .4, .1, 1 } :
                glm::vec4 { 0, 0, 0, 1 };
            frame.flow = glm::max(frame.flow, m_min_flow);
        },
        .setup_sample_shader = [&](auto& frame) {
            pp::panopainter::use_legacy_stroke_shader();
            pp::panopainter::apply_legacy_stroke_sample_uniforms(
                pp::panopainter::LegacyStrokeSampleUniforms {
                    .color = frame.col,
                    .alpha = frame.flow,
                    .opacity = frame.opacity,
                });
        },
        .draw_sample = [&](auto& frame) {
            /*auto rect =*/ stroke_draw_samples(frame.shapes, m_tex, copy_stroke_destination);
        },
        .copy_pass_result = [&] {
            copy_stroke_preview_framebuffer_to_texture(
                m_tex,
                size,
                stroke_preview_composite_slots::kStroke);
        },
        .finish_main_pass = [&] {
            set_active_texture_unit(stroke_preview_live_slots::kMixer);
            m_rtt_mixer.unbindTexture();
        },
    };
}

void NodeStrokePreview::execute_stroke_draw_immediate_dual_pass(
    const Brush& brush,
    const pp::panopainter::LegacyNodeStrokePreviewPassOrchestrationPlan& pass_orchestration,
    std::shared_ptr<Brush> dual_brush,
    bool copy_stroke_destination,
    float zoom,
    const glm::vec2& size)
{
    (void)pass_orchestration;
    (void)dual_brush;
    pp::panopainter::execute_legacy_stroke_preview_live_pass(
        [&] {
            m_rtt.clear();
        },
        [&] {
            return stroke_draw_compute(m_dual_stroke, zoom);
        },
        [](auto& frame) {
            frame.col = { 0, 0, 0, 1 };
        },
        [&](auto& frame) {
            pp::panopainter::use_legacy_stroke_shader();
            pp::panopainter::apply_legacy_stroke_sample_uniforms(
                pp::panopainter::LegacyStrokeSampleUniforms {
                    .color = frame.col,
                    .alpha = frame.flow,
                    .opacity = frame.opacity,
                });
        },
        [&](auto& frame) {
            /*auto rect =*/ stroke_draw_samples(frame.shapes, m_tex_dual, copy_stroke_destination);
        },
        [&] {
            copy_stroke_preview_framebuffer_to_texture(
                m_tex_dual,
                size,
                stroke_preview_composite_slots::kStroke);
        });
}

pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest
NodeStrokePreview::make_stroke_draw_immediate_pass_sequence_request(
    const Brush& brush,
    const pp::panopainter::LegacyNodeStrokePreviewPassOrchestrationPlan& pass_orchestration,
    std::shared_ptr<Brush> dual_brush,
    bool copy_stroke_destination,
    float zoom,
    const glm::vec2& size)
{
    const auto& material = pass_orchestration.material;
    return pp::panopainter::LegacyNodeStrokePreviewPassSequenceRequest {
        .dual_pass_enabled = material.dual_pass.enabled,
        .prepare_dual_pass = [&] {
            pp::panopainter::setup_legacy_stroke_dual_shader(material.dual_pass.uses_pattern);
            bind_stroke_preview_dual_pass_textures(*dual_brush);
        },
        .execute_dual_pass = [&] {
            execute_stroke_draw_immediate_dual_pass(
                brush,
                pass_orchestration,
                dual_brush,
                copy_stroke_destination,
                zoom,
                size);
        },
        .capture_background = [&] {
            execute_stroke_preview_background_capture_pass(
                size,
                pass_orchestration.background_colorize,
                m_tex_background,
                [&] {
                    m_plane.draw_fill();
                });
        },
        .prepare_main_pass = [&] {},
        .execute_main_pass = [&] {
            [[maybe_unused]] const bool main_live_ok =
                pp::panopainter::execute_legacy_node_stroke_preview_main_live_pass(
                    make_stroke_draw_immediate_main_live_pass_request(
                        brush,
                        pass_orchestration,
                        copy_stroke_destination,
                        zoom,
                        size));
        },
        .finish_main_pass = [&] {},
        .execute_final_composite = [&] {
            execute_stroke_preview_final_composite_and_copy(
                StrokePreviewCompositePassInputs {
                    .resolution = size,
                    .pattern_scale = brush.m_pattern_scale,
                    .brush = brush,
                    .composite_pass = material.composite_pass,
                    .background_texture = m_tex_background,
                    .stroke_texture = m_tex,
                    .dual_texture = m_tex_dual,
                    .linear_sampler = m_sampler_linear,
                    .repeat_sampler = m_sampler_linear_repeat,
                    .draw_composite = [&] {
                        m_plane.draw_fill();
                    },
                },
                m_tex_preview,
                size);
        },
        .copy_preview_result = [&] {},
    };
}

Image NodeStrokePreview::render_to_image()
{
    std::lock_guard<std::mutex> _lock(s_render_mutex);

    App::I->render_task([this] {
        auto new_size = m_preview_size;
        if (!m_tex_preview.ready() || m_tex_preview.size() != new_size)
            m_tex_preview.create((int)new_size.x, (int)new_size.y);
        if (m_tex.size() != new_size)
        {
            m_rtt.create((int)new_size.x, (int)new_size.y);
            m_rtt_mixer.create((int)new_size.x, (int)new_size.y);
            m_tex.create((int)new_size.x, (int)new_size.y);
            m_tex_dual.create((int)new_size.x, (int)new_size.y);
            m_tex_background.create((int)new_size.x, (int)new_size.y);
        }
        draw_stroke_immediate();
    });
    return m_tex_preview.get_image();
}

void NodeStrokePreview::draw_stroke()
{
    if (m_size.x == 0 || m_size.y == 0)
        return;
    s_queue.mutex.lock();
    if (!s_running)
    {
        s_running = true;
        s_renderer = std::thread([] {
            BT_SetTerminate();

            m_sampler_linear.create();
            m_sampler_linear_repeat.create(
                pp::renderer::gl::linear_texture_filter(),
                pp::renderer::gl::repeat_texture_wrap());
            m_sampler_mipmap.create();
            m_sampler_mipmap.set_filter(
                pp::renderer::gl::linear_mipmap_linear_texture_filter(),
                pp::renderer::gl::linear_texture_filter());
            m_brush_shape.create();
            while (s_running)
            {
                auto node = s_queue.Get();
                if (node)
                {
                    std::lock_guard<std::mutex> _lock(s_render_mutex);

                    // if the brush is not already loaded, load it and then destroy it
                    bool to_unload = (node->m_brush->m_tip_texture == nullptr);
                    node->m_brush->preload();

                    App::I->render_task([node, to_unload]
                    {
                        gl_state gl;
                        gl.save();

                        auto new_size = node->m_preview_size;
                        if (!node->m_tex_preview.ready() || node->m_tex_preview.size() != new_size)
                            node->m_tex_preview.create((int)new_size.x, (int)new_size.y);
                        if (m_tex.size() != new_size)
                        {
                            m_rtt.create((int)new_size.x, (int)new_size.y);
                            m_rtt_mixer.create((int)new_size.x, (int)new_size.y);
                            m_tex.create((int)new_size.x, (int)new_size.y);
                            m_tex_dual.create((int)new_size.x, (int)new_size.y);
                            m_tex_background.create((int)new_size.x, (int)new_size.y);
                        }

                        node->m_brush->load();
                        node->draw_stroke_immediate();
                        if (to_unload)
                            node->m_brush->unload();

                        gl.restore();
                    });
                    
                    node->app_redraw();

                    //std::this_thread::sleep_for(std::chrono::milliseconds(30));
                    std::this_thread::yield();
                }
            }
            m_rtt.destroy();
            m_rtt_mixer.destroy();
            m_tex.destroy();
            m_tex_dual.destroy();
            m_tex_background.destroy();
            m_brush_shape.destroy();
        });
    }
    s_queue.mutex.unlock();
    s_queue.PostUnique(std::static_pointer_cast<NodeStrokePreview>(shared_from_this()), m_draw_first);
}

void NodeStrokePreview::draw()
{
    pp::panopainter::setup_legacy_canvas_draw_merge_texture_shader(
        pp::panopainter::LegacyCanvasDrawMergeTextureUniforms {
            .mvp = m_mvp,
            .texture_slot = 0,
        });
    m_tex_preview.bind();
    m_sampler_linear.bind(0);
    m_plane.draw_fill();
    m_sampler_linear.unbind();
    m_tex_preview.unbind();
}

void NodeStrokePreview::handle_resize(glm::vec2 old_size, glm::vec2 new_size, float zoom)
{
    if (m_preview_size == (new_size * root()->m_zoom) || !m_brush)
        return;

    m_preview_size = new_size * root()->m_zoom;

    if (m_on_screen)
        draw_stroke();
}

void NodeStrokePreview::destroy()
{
    m_tex_preview.destroy();
    Node::destroy();
}

void NodeStrokePreview::handle_on_screen(bool old_visibility, bool new_visibility)
{
    parent::handle_on_screen(old_visibility, new_visibility);
    if (new_visibility)
    {
        draw_stroke();
    }
    else
    {
        s_queue.Remove(std::static_pointer_cast<NodeStrokePreview>(shared_from_this()));
        m_tex_preview.destroy();
    }
}
