#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_services.h"
#include "paint_renderer/compositor.h"
#include "texture.h"

#include <algorithm>
#include <cmath>
#include <cstdint>
#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;
};

struct LegacyNodeStrokePreviewMixExecutionRequest {
    LegacyNodeStrokePreviewMixPassPlan::ShaderPlan shader {};
    int mixer_width = 0;
    int mixer_height = 0;
    int scissor_x = 0;
    int scissor_y = 0;
    int scissor_width = 0;
    int scissor_height = 0;
    std::function<void()> save_state;
    std::function<void(const LegacyNodeStrokePreviewMixPassPlan::ShaderPlan&)> setup_mix_shader;
    std::function<void()> bind_mixer_framebuffer;
    std::function<void(int, int, int, int, int, int)> configure_mix_target_state;
    std::function<void()> bind_mix_inputs;
    std::function<void()> draw_mix;
    std::function<void()> unbind_mixer_framebuffer;
    std::function<void()> restore_state;
};

[[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;
}

[[nodiscard]] inline bool execute_legacy_node_stroke_preview_mix_pass(
    const LegacyNodeStrokePreviewMixExecutionRequest& request)
{
    if (request.mixer_width <= 0 ||
        request.mixer_height <= 0 ||
        !request.save_state ||
        !request.setup_mix_shader ||
        !request.bind_mixer_framebuffer ||
        !request.configure_mix_target_state ||
        !request.bind_mix_inputs ||
        !request.draw_mix ||
        !request.unbind_mixer_framebuffer ||
        !request.restore_state) {
        return false;
    }

    request.save_state();
    request.setup_mix_shader(request.shader);
    request.bind_mixer_framebuffer();
    request.configure_mix_target_state(
        request.mixer_width,
        request.mixer_height,
        request.scissor_x,
        request.scissor_y,
        request.scissor_width,
        request.scissor_height);
    request.bind_mix_inputs();
    request.draw_mix();
    request.unbind_mixer_framebuffer();
    request.restore_state();
    return true;
}

struct LegacyNodeStrokePreviewPassSequenceRequest {
    bool dual_pass_enabled = false;
    std::function<void()> prepare_dual_pass;
    std::function<void()> execute_dual_pass;
    std::function<void()> capture_background;
    std::function<void()> prepare_main_pass;
    std::function<void()> execute_main_pass;
    std::function<void()> finish_main_pass;
    std::function<void()> execute_final_composite;
    std::function<void()> copy_preview_result;
};

[[nodiscard]] inline bool execute_legacy_node_stroke_preview_pass_sequence(
    const LegacyNodeStrokePreviewPassSequenceRequest& request)
{
    if (!request.capture_background ||
        !request.prepare_main_pass ||
        !request.execute_main_pass ||
        !request.finish_main_pass ||
        !request.execute_final_composite ||
        !request.copy_preview_result) {
        return false;
    }

    if (request.dual_pass_enabled) {
        if (!request.prepare_dual_pass || !request.execute_dual_pass) {
            return false;
        }
        request.prepare_dual_pass();
        request.execute_dual_pass();
    }

    request.capture_background();
    request.prepare_main_pass();
    request.execute_main_pass();
    request.finish_main_pass();
    request.execute_final_composite();
    request.copy_preview_result();
    return true;
}

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 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;
};

struct LegacyNodeStrokePreviewFinalCompositeRequest {
    glm::vec2 resolution {};
    glm::vec2 pattern_scale {};
    const Brush* brush = nullptr;
    const pp::paint_renderer::CanvasStrokeCompositePassPlan* composite_pass = nullptr;
    std::function<void()> setup_composite_shader;
    std::function<void()> bind_composite_samplers;
    std::function<void()> bind_composite_inputs;
    std::function<void()> draw_composite;
};

[[nodiscard]] inline bool execute_legacy_node_stroke_preview_final_composite(
    const LegacyNodeStrokePreviewFinalCompositeRequest& request)
{
    if (!request.brush ||
        !request.composite_pass ||
        !request.setup_composite_shader ||
        !request.bind_composite_samplers ||
        !request.bind_composite_inputs ||
        !request.draw_composite) {
        return false;
    }

    pp::panopainter::execute_legacy_stroke_preview_final_composite(
        [&] {
            request.setup_composite_shader();
        },
        [&] {
            request.bind_composite_samplers();
        },
        [&] {
            request.bind_composite_inputs();
        },
        [&] {
            request.draw_composite();
        });
    return true;
}

struct LegacyNodeStrokePreviewCopyResultRequest {
    Texture2D* preview_texture = nullptr;
    glm::vec2 size {};
    std::function<void(int, int, int, int, int, int)> copy_framebuffer_to_texture;
};

[[nodiscard]] inline bool copy_legacy_node_stroke_preview_result(
    const LegacyNodeStrokePreviewCopyResultRequest& request)
{
    if (!request.preview_texture || !request.copy_framebuffer_to_texture) {
        return false;
    }
    pp::panopainter::copy_legacy_stroke_preview_texture(
        [&] {
            request.preview_texture->bind();
        },
        request.copy_framebuffer_to_texture,
        pp::panopainter::LegacyStrokePreviewCopySize {
            .width = static_cast<int>(request.size.x),
            .height = static_cast<int>(request.size.y),
        });
    return true;
}

[[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();
    }
}

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;
};

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 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 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;
};

[[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
