#pragma once

#include "../libs/glm/glm/glm.hpp"
#include "../libs/glm/glm/ext/matrix_clip_space.hpp"

#include "legacy_canvas_stroke_shader_services.h"
#include "legacy_canvas_stroke_composite_services.h"
#include "legacy_canvas_stroke_preview_services.h"
#include "legacy_canvas_stroke_services.h"
#include "legacy_ui_gl_dispatch.h"
#include "paint_renderer/compositor.h"
#include "brush.h"
#include "texture.h"
#include "util.h"

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <functional>
#include <utility>
#include <vector>

namespace pp::panopainter {

[[nodiscard]] inline pp::paint_renderer::CanvasStrokeFeedbackPlan plan_legacy_node_stroke_preview_feedback(
    pp::renderer::RenderDeviceFeatures features,
    int width,
    int height) noexcept
{
    const auto plan = pp::paint_renderer::plan_canvas_stroke_feedback(
        features,
        pp::renderer::Extent2D {
            .width = static_cast<std::uint32_t>(std::max(width, 0)),
            .height = static_cast<std::uint32_t>(std::max(height, 0)),
        });
    if (plan) {
        return plan.value();
    }

    pp::paint_renderer::CanvasStrokeFeedbackPlan fallback;
    fallback.compatibility_fallback = true;
    fallback.path = pp::paint_renderer::StrokeCompositePath::ping_pong_textures;
    fallback.requires_auxiliary_texture = true;
    return fallback;
}

[[nodiscard]] inline pp::paint_renderer::StrokePreviewCompositePlan plan_legacy_node_stroke_preview_composite(
    bool uses_mixer,
    bool uses_dual,
    bool uses_pattern) noexcept
{
    return pp::paint_renderer::plan_stroke_preview_composite(
        pp::paint_renderer::StrokePreviewCompositeRequest {
            .uses_mixer = uses_mixer,
            .uses_dual = uses_dual,
            .uses_pattern = uses_pattern,
        });
}

struct LegacyNodeStrokePreviewMixPassPlan {
    pp::paint_renderer::CanvasStrokeMaterialPlan material {};
    struct ShaderPlan {
        glm::vec2 resolution {};
        glm::vec2 pattern_scale {};
        float pattern_invert = 0.0f;
        float pattern_brightness = 0.0f;
        float pattern_contrast = 0.0f;
        float pattern_depth = 0.0f;
        int pattern_blend_mode = 0;
        glm::vec2 pattern_offset {};
        int blend_mode = 0;
        bool use_dual = false;
        int dual_blend_mode = 0;
        float dual_alpha = 0.0f;
        bool use_pattern = false;
    } shader {};
};

struct LegacyNodeStrokePreviewMixPassRequest {
    glm::vec2 resolution {};
    float pattern_scale = 0.0f;
    bool pattern_flipx = false;
    bool pattern_flipy = false;
    bool pattern_invert = false;
    float pattern_brightness = 0.0f;
    float pattern_contrast = 0.0f;
    float pattern_depth = 0.0f;
    bool pattern_rand_offset = false;
    bool pattern_enabled = false;
    bool pattern_eachsample = false;
    float tip_wet = 0.0f;
    float tip_mix = 0.0f;
    float tip_noise = 0.0f;
    bool dual_enabled = false;
    int dual_blend_mode = 0;
    int pattern_blend_mode = 0;
    float dual_opacity = 0.0f;
    int blend_mode = 0;
};

[[nodiscard]] inline LegacyNodeStrokePreviewMixPassPlan plan_legacy_node_stroke_preview_mix_pass(
    const LegacyNodeStrokePreviewMixPassRequest& request) noexcept
{
    glm::vec2 pattern_scale(request.pattern_scale);
    if (request.pattern_flipx) {
        pattern_scale.x *= -1.0f;
    }
    if (request.pattern_flipy) {
        pattern_scale.y *= -1.0f;
    }

    LegacyNodeStrokePreviewMixPassPlan plan;
    plan.material = plan_legacy_canvas_stroke_material(
        pp::paint_renderer::CanvasStrokeMaterialRequest {
            .destination_feedback_needed = false,
            .pattern_enabled = request.pattern_enabled,
            .pattern_eachsample = request.pattern_eachsample,
            .wet_blend = request.tip_wet > 0.0f,
            .mix_blend = request.tip_mix > 0.0f,
            .noise_enabled = request.tip_noise > 0.0f,
            .dual_brush_enabled = request.dual_enabled,
            .dual_blend_mode = request.dual_blend_mode,
            .pattern_blend_mode = request.pattern_blend_mode,
            .dual_alpha = request.dual_opacity,
        });
    plan.shader = LegacyNodeStrokePreviewMixPassPlan::ShaderPlan {
        .resolution = request.resolution,
        .pattern_scale = pattern_scale,
        .pattern_invert = static_cast<float>(request.pattern_invert),
        .pattern_brightness = request.pattern_brightness,
        .pattern_contrast = request.pattern_contrast,
        .pattern_depth = request.pattern_depth,
        .pattern_blend_mode = plan.material.composite_pass.pattern_blend_mode,
        .pattern_offset = glm::vec2(request.pattern_rand_offset ? 0.5f : 0.0f),
        .blend_mode = request.blend_mode,
        .use_dual = plan.material.composite_pass.use_dual,
        .dual_blend_mode = plan.material.composite_pass.dual_blend_mode,
        .dual_alpha = plan.material.composite_pass.dual_alpha,
        .use_pattern = plan.material.composite_pass.use_pattern,
    };
    return plan;
}

struct LegacyNodeStrokePreviewPassOrchestrationPlan {
    pp::paint_renderer::CanvasStrokeFeedbackPlan feedback {};
    pp::paint_renderer::CanvasStrokeMaterialPlan material {};
    pp::paint_renderer::StrokePreviewCompositePlan composite {};
    LegacyStrokeShaderSetupUniforms stroke_shader {};
    bool copy_stroke_destination = false;
    bool background_colorize = false;
};

struct LegacyNodeStrokePreviewRenderTargetSetup {
    RTT& preview_rtt;
    RTT& preview_rtt_mixer;
    Texture2D& preview_stroke_texture;
    Texture2D& preview_dual_texture;
    Texture2D& preview_background_texture;
    Texture2D& preview_image_texture;
    glm::vec2 size {};
};

[[nodiscard]] inline bool ensure_legacy_node_stroke_preview_render_targets(
    const LegacyNodeStrokePreviewRenderTargetSetup& setup)
{
    if (setup.size.x <= 0.0f || setup.size.y <= 0.0f) {
        return false;
    }

    const auto width = static_cast<int>(setup.size.x);
    const auto height = static_cast<int>(setup.size.y);

    if (!setup.preview_image_texture.ready() || setup.preview_image_texture.size() != setup.size) {
        setup.preview_image_texture.create(width, height);
    }
    if (setup.preview_stroke_texture.size() != setup.size) {
        setup.preview_rtt.create(width, height);
        setup.preview_rtt_mixer.create(width, height);
        setup.preview_stroke_texture.create(width, height);
        setup.preview_dual_texture.create(width, height);
        setup.preview_background_texture.create(width, height);
    }

    return true;
}

struct LegacyNodeStrokePreviewMainPassTextureDispatch {
    std::function<void(int)> activate_texture_unit;
    std::function<void()> bind_brush_tip;
    std::function<void()> bind_stroke_destination;
    std::function<void()> bind_pattern;
    std::function<void()> bind_mixer;
};

[[nodiscard]] inline LegacyNodeStrokePreviewMainPassTextureDispatch make_legacy_node_stroke_preview_main_pass_texture_dispatch(
    std::function<void(int)> activate_texture_unit,
    std::function<void()> bind_brush_tip,
    std::function<void()> bind_stroke_destination,
    std::function<void()> bind_pattern,
    std::function<void()> bind_mixer)
{
    return LegacyNodeStrokePreviewMainPassTextureDispatch {
        .activate_texture_unit = std::move(activate_texture_unit),
        .bind_brush_tip = std::move(bind_brush_tip),
        .bind_stroke_destination = std::move(bind_stroke_destination),
        .bind_pattern = std::move(bind_pattern),
        .bind_mixer = std::move(bind_mixer),
    };
}

inline void bind_legacy_node_stroke_preview_main_pass_textures(
    const LegacyNodeStrokePreviewMainPassTextureDispatch& dispatch,
    bool copy_stroke_destination,
    bool uses_mixer)
{
    dispatch.activate_texture_unit(0);
    dispatch.bind_brush_tip();
    if (copy_stroke_destination) {
        dispatch.activate_texture_unit(1);
        dispatch.bind_stroke_destination();
    }
    dispatch.activate_texture_unit(2);
    dispatch.bind_pattern();
    dispatch.activate_texture_unit(3);
    if (uses_mixer) {
        dispatch.bind_mixer();
    }
}

inline void copy_legacy_node_stroke_preview_framebuffer_to_texture(
    Texture2D& texture,
    glm::vec2 size,
    std::uint32_t texture_slot)
{
    pp::legacy::ui_gl::activate_texture_unit(texture_slot, "NodeStrokePreview");
    texture.bind();
    copy_framebuffer_to_texture_2d(
        0,
        0,
        0,
        0,
        static_cast<int>(size.x),
        static_cast<int>(size.y));
}

template <typename Frame>
struct LegacyNodeStrokePreviewMainLivePassRequestT {
    std::function<void()> setup_blend_uniforms;
    std::function<void()> bind_main_pass_textures;
    std::function<void()> clear_target;
    std::function<std::vector<Frame>()> compute_frames;
    std::function<void(Frame&)> before_frame;
    std::function<void(Frame&)> setup_sample_shader;
    std::function<void(Frame&)> draw_sample;
    std::function<void()> copy_pass_result;
    std::function<void()> finish_main_pass;
};

inline constexpr std::uint32_t kLegacyNodeStrokePreviewStrokeTextureSlot = 1U;

template <
    typename Frame,
    typename ComputeFrames,
    typename BeforeFrame,
    typename SetupSampleShader,
    typename DrawSample,
    typename FinishMainPass>
[[nodiscard]] inline LegacyNodeStrokePreviewMainLivePassRequestT<Frame>
make_legacy_node_stroke_preview_main_live_pass_request(
    const Brush& brush,
    const LegacyNodeStrokePreviewPassOrchestrationPlan& pass_orchestration,
    Texture2D& stroke_texture,
    RTT& mixer_rtt,
    RTT& render_target,
    bool copy_stroke_destination,
    glm::vec2 size,
    std::uint32_t stroke_texture_slot,
    ComputeFrames&& compute_frames,
    BeforeFrame&& before_frame,
    SetupSampleShader&& setup_sample_shader,
    DrawSample&& draw_sample,
    FinishMainPass&& finish_main_pass)
{
    return LegacyNodeStrokePreviewMainLivePassRequestT<Frame> {
        .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 = [&] {
            pp::panopainter::bind_legacy_node_stroke_preview_main_pass_textures(
                pp::panopainter::make_legacy_node_stroke_preview_main_pass_texture_dispatch(
                    [&](int texture_slot) {
                        pp::legacy::ui_gl::activate_texture_unit(texture_slot, "NodeStrokePreview");
                    },
                    [&] {
                        brush.m_tip_texture ?
                            brush.m_tip_texture->bind() :
                            pp::legacy::ui_gl::unbind_texture_2d("NodeStrokePreview");
                    },
                    [&] {
                        stroke_texture.bind();
                    },
                    [&] {
                        brush.m_pattern_texture ?
                            brush.m_pattern_texture->bind() :
                            pp::legacy::ui_gl::unbind_texture_2d("NodeStrokePreview");
                    },
                    [&] {
                        mixer_rtt.bindTexture();
                    }),
                copy_stroke_destination,
                pass_orchestration.material.stroke_pass.uses_mixer);
        },
        .clear_target = [&] {
            render_target.clear();
        },
        .compute_frames = std::forward<ComputeFrames>(compute_frames),
        .before_frame = std::forward<BeforeFrame>(before_frame),
        .setup_sample_shader = std::forward<SetupSampleShader>(setup_sample_shader),
        .draw_sample = std::forward<DrawSample>(draw_sample),
        .copy_pass_result = [&] {
            copy_legacy_node_stroke_preview_framebuffer_to_texture(
                stroke_texture,
                size,
                stroke_texture_slot);
        },
        .finish_main_pass = std::forward<FinishMainPass>(finish_main_pass),
    };
}

template <typename Frame>
[[nodiscard]] inline bool execute_legacy_node_stroke_preview_main_live_pass(
    const LegacyNodeStrokePreviewMainLivePassRequestT<Frame>& request)
{
    if (!request.setup_blend_uniforms ||
        !request.bind_main_pass_textures ||
        !request.clear_target ||
        !request.compute_frames ||
        !request.before_frame ||
        !request.setup_sample_shader ||
        !request.draw_sample ||
        !request.copy_pass_result ||
        !request.finish_main_pass) {
        return false;
    }

    request.setup_blend_uniforms();
    request.bind_main_pass_textures();
    pp::panopainter::execute_legacy_stroke_preview_live_pass(
        [&] {
            request.clear_target();
        },
        [&] {
            return request.compute_frames();
        },
        [&](Frame& frame) {
            request.before_frame(frame);
        },
        [&](Frame& frame) {
            request.setup_sample_shader(frame);
        },
        [&](Frame& frame) {
            request.draw_sample(frame);
        },
        [&] {
            request.copy_pass_result();
        });
    request.finish_main_pass();
    return true;
}

struct LegacyNodeStrokePreviewImmediatePassSequenceRequest {
    std::function<void()> execute_dual_pass;
    std::function<void()> capture_background;
    std::function<bool()> execute_main_live_pass;
    std::function<bool()> execute_final_composite;
};

[[nodiscard]] inline bool execute_legacy_node_stroke_preview_immediate_pass_sequence(
    const LegacyNodeStrokePreviewImmediatePassSequenceRequest& request)
{
    if (!request.capture_background ||
        !request.execute_main_live_pass ||
        !request.execute_final_composite) {
        return false;
    }

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

    request.capture_background();
    if (!request.execute_main_live_pass()) {
        return false;
    }

    return request.execute_final_composite();
}

[[nodiscard]] inline bool execute_legacy_node_stroke_preview_final_composite(
    glm::vec2 size,
    glm::vec2 pattern_scale,
    const Brush& brush,
    const pp::paint_renderer::CanvasStrokeCompositePassPlan& composite_pass,
    Texture2D& background_texture,
    Texture2D& stroke_texture,
    Texture2D& dual_texture,
    Texture2D& preview_texture,
    Sampler& linear_sampler,
    Sampler& repeat_sampler,
    std::function<void()> bind_pattern_texture,
    std::function<void()> draw_composite);

template <
    typename Frame,
    typename ExecuteDualPass,
    typename CaptureBackground,
    typename ComputeFrames,
    typename BeforeFrame,
    typename SetupSampleShader,
    typename DrawSample,
    typename FinishMainPass,
    typename BindPatternTexture,
    typename DrawComposite>
[[nodiscard]] inline bool execute_legacy_node_stroke_preview_draw_immediate_shell(
    const Brush& brush,
    const LegacyNodeStrokePreviewPassOrchestrationPlan& pass_orchestration,
    Texture2D& stroke_texture,
    RTT& mixer_rtt,
    RTT& render_target,
    Texture2D& background_texture,
    Texture2D& dual_texture,
    Texture2D& preview_texture,
    Sampler& linear_sampler,
    Sampler& repeat_sampler,
    bool copy_stroke_destination,
    glm::vec2 size,
    std::uint32_t stroke_texture_slot,
    ExecuteDualPass&& execute_dual_pass,
    CaptureBackground&& capture_background,
    ComputeFrames&& compute_frames,
    BeforeFrame&& before_frame,
    SetupSampleShader&& setup_sample_shader,
    DrawSample&& draw_sample,
    FinishMainPass&& finish_main_pass,
    BindPatternTexture&& bind_pattern_texture,
    DrawComposite&& draw_composite)
{
    return execute_legacy_node_stroke_preview_immediate_pass_sequence(
        LegacyNodeStrokePreviewImmediatePassSequenceRequest {
            .execute_dual_pass = std::forward<ExecuteDualPass>(execute_dual_pass),
            .capture_background = std::forward<CaptureBackground>(capture_background),
            .execute_main_live_pass = [&]() -> bool {
                return execute_legacy_node_stroke_preview_main_live_pass(
                    make_legacy_node_stroke_preview_main_live_pass_request<Frame>(
                        brush,
                        pass_orchestration,
                        stroke_texture,
                        mixer_rtt,
                        render_target,
                        copy_stroke_destination,
                        size,
                        stroke_texture_slot,
                        std::forward<ComputeFrames>(compute_frames),
                        std::forward<BeforeFrame>(before_frame),
                        std::forward<SetupSampleShader>(setup_sample_shader),
                        std::forward<DrawSample>(draw_sample),
                        std::forward<FinishMainPass>(finish_main_pass)));
            },
            .execute_final_composite = [&]() -> bool {
                return execute_legacy_node_stroke_preview_final_composite(
                    size,
                    glm::vec2(brush.m_pattern_scale),
                    brush,
                    pass_orchestration.material.composite_pass,
                    background_texture,
                    stroke_texture,
                    dual_texture,
                    preview_texture,
                    linear_sampler,
                    repeat_sampler,
                    std::forward<BindPatternTexture>(bind_pattern_texture),
                    std::forward<DrawComposite>(draw_composite));
            },
        });
}

[[nodiscard]] inline bool execute_legacy_node_stroke_preview_final_composite(
    glm::vec2 size,
    glm::vec2 pattern_scale,
    const Brush& brush,
    const pp::paint_renderer::CanvasStrokeCompositePassPlan& composite_pass,
    Texture2D& background_texture,
    Texture2D& stroke_texture,
    Texture2D& dual_texture,
    Texture2D& preview_texture,
    Sampler& linear_sampler,
    Sampler& repeat_sampler,
    std::function<void()> bind_pattern_texture,
    std::function<void()> draw_composite)
{
    if (!bind_pattern_texture || !draw_composite) {
        return false;
    }

    pp::panopainter::execute_legacy_stroke_preview_final_composite(
        [&] {
            pp::panopainter::setup_legacy_stroke_composite_shader(
                pp::panopainter::LegacyStrokeCompositeUniforms {
                    .resolution = size,
                    .pattern = {
                        .scale = pattern_scale,
                        .invert = static_cast<float>(brush.m_pattern_invert),
                        .brightness = brush.m_pattern_brightness,
                        .contrast = brush.m_pattern_contrast,
                        .depth = brush.m_pattern_depth,
                        .blend_mode = composite_pass.pattern_blend_mode,
                        .offset = glm::vec2(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 = brush.m_blend_mode,
                    .use_dual = composite_pass.use_dual,
                    .dual_blend_mode = composite_pass.dual_blend_mode,
                    .dual_alpha = composite_pass.dual_alpha,
                    .use_pattern = composite_pass.use_pattern,
                });
        },
        [&] {
            linear_sampler.bind(0U);
            linear_sampler.bind(1U);
            linear_sampler.bind(2U);
            linear_sampler.bind(3U);
            repeat_sampler.bind(4U);
        },
        [&] {
            pp::legacy::ui_gl::activate_texture_unit(0U, "NodeStrokePreview");
            background_texture.bind();
            pp::legacy::ui_gl::activate_texture_unit(1U, "NodeStrokePreview");
            stroke_texture.bind();
            pp::legacy::ui_gl::activate_texture_unit(3U, "NodeStrokePreview");
            dual_texture.bind();
            pp::legacy::ui_gl::activate_texture_unit(4U, "NodeStrokePreview");
            bind_pattern_texture();
        },
        [&] {
            draw_composite();
        });

    const auto copy_status = pp::paint_renderer::copy_stroke_preview_result_to_texture(
        [&] {
            preview_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::paint_renderer::StrokePreviewCopySize {
            .width = static_cast<int>(size.x),
            .height = static_cast<int>(size.y),
        });
    return copy_status.ok();
}

struct LegacyNodeStrokePreviewPassOrchestrationRequest {
    pp::renderer::RenderDeviceFeatures features {};
    glm::vec2 preview_size {};
    float pattern_scale = 0.0f;
    bool pattern_flipx = false;
    bool pattern_flipy = false;
    bool pattern_invert = false;
    float pattern_brightness = 0.0f;
    float pattern_contrast = 0.0f;
    float pattern_depth = 0.0f;
    bool pattern_rand_offset = false;
    bool pattern_enabled = false;
    bool pattern_eachsample = false;
    float tip_mix = 0.0f;
    float tip_wet = 0.0f;
    float tip_noise = 0.0f;
    bool dual_enabled = false;
    int dual_blend_mode = 0;
    float dual_opacity = 0.0f;
    int pattern_blend_mode = 0;
    int blend_mode = 0;
    glm::mat4 mvp { 1.0f };
};

[[nodiscard]] inline LegacyNodeStrokePreviewPassOrchestrationRequest
make_legacy_node_stroke_preview_pass_orchestration_request(
    pp::renderer::RenderDeviceFeatures features,
    glm::vec2 preview_size,
    const Brush& brush,
    const glm::mat4& mvp) noexcept
{
    return LegacyNodeStrokePreviewPassOrchestrationRequest {
        .features = features,
        .preview_size = preview_size,
        .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_mix = brush.m_tip_mix,
        .tip_wet = brush.m_tip_wet,
        .tip_noise = brush.m_tip_noise,
        .dual_enabled = brush.m_dual_enabled,
        .dual_blend_mode = brush.m_dual_blend_mode,
        .dual_opacity = brush.m_dual_opacity,
        .pattern_blend_mode = brush.m_pattern_blend_mode,
        .blend_mode = brush.m_blend_mode,
        .mvp = mvp,
    };
}

[[nodiscard]] inline LegacyNodeStrokePreviewPassOrchestrationPlan
plan_legacy_node_stroke_preview_pass_orchestration(
    const LegacyNodeStrokePreviewPassOrchestrationRequest& request) noexcept
{
    LegacyNodeStrokePreviewPassOrchestrationPlan plan;
    plan.feedback = plan_legacy_node_stroke_preview_feedback(
        request.features,
        static_cast<int>(request.preview_size.x),
        static_cast<int>(request.preview_size.y));
    plan.copy_stroke_destination = !plan.feedback.reads_destination_color;
    plan.material = plan_legacy_canvas_stroke_material(
        pp::paint_renderer::CanvasStrokeMaterialRequest {
            .destination_feedback_needed = plan.copy_stroke_destination,
            .pattern_enabled = request.pattern_enabled,
            .pattern_eachsample = request.pattern_eachsample,
            .wet_blend = request.tip_wet > 0.0f,
            .mix_blend = request.tip_mix > 0.0f,
            .noise_enabled = request.tip_noise > 0.0f,
            .dual_brush_enabled = request.dual_enabled,
            .dual_blend_mode = request.dual_blend_mode,
            .pattern_blend_mode = request.pattern_blend_mode,
            .dual_alpha = request.dual_opacity,
        });
    plan.composite = plan_legacy_node_stroke_preview_composite(
        request.tip_mix > 0.0f,
        plan.material.composite_pass.use_dual,
        plan.material.composite_pass.use_pattern);

    glm::vec2 preview_pattern_scale(request.pattern_scale);
    if (request.pattern_flipx) {
        preview_pattern_scale.x *= -1.0f;
    }
    if (request.pattern_flipy) {
        preview_pattern_scale.y *= -1.0f;
    }

    plan.stroke_shader = LegacyStrokeShaderSetupUniforms {
        .resolution = request.preview_size,
        .pattern = {
            .scale = preview_pattern_scale,
            .invert = static_cast<float>(request.pattern_invert),
            .brightness = request.pattern_brightness,
            .contrast = request.pattern_contrast,
            .depth = request.pattern_depth,
            .blend_mode = request.pattern_blend_mode,
            .offset = glm::vec2(request.pattern_rand_offset ? 0.5f : 0.0f),
        },
        .mvp = request.mvp,
        .uses_destination_feedback = plan.copy_stroke_destination,
        .uses_pattern = false,
        .mix_alpha = 0.0f,
        .wet = 0.0f,
        .noise = 0.0f,
        .set_opacity = false,
    };
    plan.background_colorize = request.tip_mix > 0.0f || request.blend_mode != 0;
    return plan;
}

struct LegacyNodeStrokePreviewStrokePoint {
    glm::vec3 position {};
    float pressure = 0.0f;
};

struct LegacyNodeStrokePreviewStrokeSetupPlan {
    float stroke_max_size = 0.0f;
    float dual_stroke_max_size = 0.0f;
    bool dual_enabled = false;
    glm::vec2 pattern_scale {};
    std::vector<LegacyNodeStrokePreviewStrokePoint> points;
};

struct LegacyNodeStrokePreviewStrokeSetupRequest {
    glm::vec2 preview_size {};
    float zoom = 1.0f;
    float brush_tip_size = 0.0f;
    float stroke_max_size_override = 0.0f;
    float pad_override = NAN;
    bool tip_size_pressure = false;
    bool dual_enabled = false;
    float dual_size = 1.0f;
    float pattern_scale = 0.0f;
    bool pattern_flipx = false;
    bool pattern_flipy = false;
    int preview_point_count = 100;
};

struct LegacyNodeStrokePreviewPreparedStrokes {
    Stroke stroke;
    Stroke dual_stroke;
    std::shared_ptr<Brush> dual_brush;
};

[[nodiscard]] inline std::shared_ptr<Brush> make_legacy_node_stroke_preview_dual_brush(const Brush& brush)
{
    auto dual_brush = std::make_shared<Brush>();
    dual_brush->m_tip_scale = brush.m_dual_scale;
    dual_brush->m_tip_angle = brush.m_dual_angle;
    dual_brush->m_tip_flow = brush.m_dual_flow;
    dual_brush->m_tip_opacity = brush.m_dual_opacity;
    dual_brush->m_tip_flipx = brush.m_dual_flipx;
    dual_brush->m_tip_flipy = brush.m_dual_flipy;
    dual_brush->m_tip_invert = brush.m_dual_invert;
    dual_brush->m_blend_mode = brush.m_dual_blend_mode;
    dual_brush->m_tip_randflipx = brush.m_dual_randflip;
    dual_brush->m_tip_randflipy = brush.m_dual_randflip;
    dual_brush->m_tip_size = brush.m_dual_size * brush.m_tip_size;
    dual_brush->m_tip_spacing = brush.m_dual_spacing;
    dual_brush->m_jitter_scatter = brush.m_dual_scatter;
    dual_brush->m_jitter_scatter_bothaxis = brush.m_dual_scatter_bothaxis;
    dual_brush->m_jitter_angle = brush.m_dual_rotate;
    dual_brush->m_tip_texture = brush.m_dual_texture;
    dual_brush->m_tip_aspect = brush.m_dual_aspect;
    return dual_brush;
}

[[nodiscard]] inline LegacyNodeStrokePreviewPreparedStrokes prepare_legacy_node_stroke_preview_strokes(
    const std::shared_ptr<Brush>& brush,
    const LegacyNodeStrokePreviewStrokeSetupPlan& stroke_setup,
    float camera_fov,
    const glm::mat4& camera_rot)
{
    LegacyNodeStrokePreviewPreparedStrokes prepared;
    prepared.stroke.m_filter_points = false;
    prepared.stroke.m_max_size = stroke_setup.stroke_max_size;
    prepared.stroke.m_camera.fov = camera_fov;
    prepared.stroke.m_camera.rot = camera_rot;
    prepared.stroke.reset(true);
    prepared.stroke.start(brush);

    prepared.dual_brush = make_legacy_node_stroke_preview_dual_brush(*brush);
    if (stroke_setup.dual_enabled) {
        prepared.dual_stroke.m_filter_points = false;
        prepared.dual_stroke.m_max_size = stroke_setup.dual_stroke_max_size;
        prepared.dual_stroke.m_camera.fov = camera_fov;
        prepared.dual_stroke.m_camera.rot = camera_rot;
        prepared.dual_stroke.reset(true);
        prepared.dual_stroke.start(prepared.dual_brush);
    }

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

    return prepared;
}

[[nodiscard]] inline glm::vec2 evaluate_legacy_node_stroke_preview_bezier(
    std::vector<glm::vec2> control_points,
    float t) noexcept
{
    if (control_points.empty()) {
        return {};
    }

    for (std::size_t remaining = control_points.size(); remaining > 1; --remaining) {
        for (std::size_t i = 0; i + 1 < remaining; ++i) {
            control_points[i] = glm::mix(control_points[i], control_points[i + 1], t);
        }
    }

    return control_points.front();
}

[[nodiscard]] inline LegacyNodeStrokePreviewStrokeSetupPlan plan_legacy_node_stroke_preview_stroke_setup(
    const LegacyNodeStrokePreviewStrokeSetupRequest& request) noexcept
{
    LegacyNodeStrokePreviewStrokeSetupPlan plan;
    plan.stroke_max_size = request.stroke_max_size_override > 0.0f ?
        request.stroke_max_size_override :
        request.preview_size.y * 0.75f;
    plan.dual_enabled = request.dual_enabled;
    plan.dual_stroke_max_size = plan.stroke_max_size * request.dual_size;
    plan.pattern_scale = glm::vec2(request.pattern_scale);
    if (request.pattern_flipx) {
        plan.pattern_scale.x *= -1.0f;
    }
    if (request.pattern_flipy) {
        plan.pattern_scale.y *= -1.0f;
    }

    const float min_pad = request.preview_size.x * 0.05f;
    float pad = (5.0f + glm::max(glm::min(plan.stroke_max_size, request.brush_tip_size) / 2.0f, min_pad)) * request.zoom;
    if (request.tip_size_pressure) {
        pad = min_pad * request.zoom;
    }
    if (!std::isnan(request.pad_override)) {
        pad = request.pad_override;
    }

    const float width = request.preview_size.x * request.zoom;
    const float height = request.preview_size.y * request.zoom;
    const std::vector<glm::vec2> keypoints {
        { pad, height / 2.0f },
        { width / 2.0f, 0.0f },
        { width / 2.0f, height },
        { width - pad, height / 2.0f },
    };

    const int point_count = std::max(request.preview_point_count, 0);
    plan.points.reserve(static_cast<std::size_t>(point_count));
    for (int i = 0; i < point_count; ++i) {
        const float t = static_cast<float>(i) / static_cast<float>(point_count);
        const float pressure = glm::clamp((1.0f - glm::abs(t * 2.0f - 1.0f)) * 1.1f, 0.0f, 1.0f);
        plan.points.push_back(LegacyNodeStrokePreviewStrokePoint {
            .position = glm::vec3(evaluate_legacy_node_stroke_preview_bezier(keypoints, t), 0.0f),
            .pressure = pressure,
        });
    }

    return plan;
}

} // namespace pp::panopainter
