#include "paint_renderer/compositor.h"

#include <limits>

namespace pp::paint_renderer {

namespace {

[[nodiscard]] bool is_valid_blend_mode(pp::paint::BlendMode mode) noexcept
{
    switch (mode) {
    case pp::paint::BlendMode::normal:
    case pp::paint::BlendMode::multiply:
    case pp::paint::BlendMode::screen:
    case pp::paint::BlendMode::color_dodge:
    case pp::paint::BlendMode::overlay:
        return true;
    }

    return false;
}

[[nodiscard]] bool is_valid_stroke_blend_mode(pp::paint::StrokeBlendMode mode) noexcept
{
    switch (mode) {
    case pp::paint::StrokeBlendMode::normal:
    case pp::paint::StrokeBlendMode::multiply:
    case pp::paint::StrokeBlendMode::subtract:
    case pp::paint::StrokeBlendMode::darken:
    case pp::paint::StrokeBlendMode::overlay:
    case pp::paint::StrokeBlendMode::color_dodge:
    case pp::paint::StrokeBlendMode::color_burn:
    case pp::paint::StrokeBlendMode::linear_burn:
    case pp::paint::StrokeBlendMode::hard_mix:
    case pp::paint::StrokeBlendMode::linear_height:
    case pp::paint::StrokeBlendMode::height:
        return true;
    }

    return false;
}

[[nodiscard]] bool paint_blend_mode_from_persisted_index(int value, pp::paint::BlendMode& out) noexcept
{
    switch (value) {
    case 0: out = pp::paint::BlendMode::normal; return true;
    case 1: out = pp::paint::BlendMode::multiply; return true;
    case 2: out = pp::paint::BlendMode::screen; return true;
    case 3: out = pp::paint::BlendMode::color_dodge; return true;
    case 4: out = pp::paint::BlendMode::overlay; return true;
    default: return false;
    }
}

[[nodiscard]] bool stroke_blend_mode_from_persisted_index(int value, pp::paint::StrokeBlendMode& out) noexcept
{
    switch (value) {
    case 0: out = pp::paint::StrokeBlendMode::normal; return true;
    case 1: out = pp::paint::StrokeBlendMode::multiply; return true;
    case 2: out = pp::paint::StrokeBlendMode::subtract; return true;
    case 3: out = pp::paint::StrokeBlendMode::darken; return true;
    case 4: out = pp::paint::StrokeBlendMode::overlay; return true;
    case 5: out = pp::paint::StrokeBlendMode::color_dodge; return true;
    case 6: out = pp::paint::StrokeBlendMode::color_burn; return true;
    case 7: out = pp::paint::StrokeBlendMode::linear_burn; return true;
    case 8: out = pp::paint::StrokeBlendMode::hard_mix; return true;
    case 9: out = pp::paint::StrokeBlendMode::linear_height; return true;
    case 10: out = pp::paint::StrokeBlendMode::height; return true;
    default: return false;
    }
}

[[nodiscard]] pp::foundation::Result<std::size_t> expected_pixel_count(pp::renderer::Extent2D extent) noexcept
{
    const auto extent_status = pp::renderer::validate_extent(extent);
    if (!extent_status.ok()) {
        return pp::foundation::Result<std::size_t>::failure(extent_status);
    }

    const auto width = static_cast<std::uint64_t>(extent.width);
    const auto height = static_cast<std::uint64_t>(extent.height);
    if (width > std::numeric_limits<std::uint64_t>::max() / height) {
        return pp::foundation::Result<std::size_t>::failure(
            pp::foundation::Status::out_of_range("pixel count overflows uint64"));
    }

    const auto count = width * height;
    if (count > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
        return pp::foundation::Result<std::size_t>::failure(
            pp::foundation::Status::out_of_range("pixel count exceeds addressable memory"));
    }

    return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(count));
}

[[nodiscard]] StrokeCompositePath composite_path_from_feedback(pp::renderer::PaintFeedbackPath path) noexcept
{
    switch (path) {
    case pp::renderer::PaintFeedbackPath::none:
        return StrokeCompositePath::fixed_function_blend;
    case pp::renderer::PaintFeedbackPath::framebuffer_fetch:
        return StrokeCompositePath::framebuffer_fetch;
    case pp::renderer::PaintFeedbackPath::ping_pong_textures:
        return StrokeCompositePath::ping_pong_textures;
    }

    return StrokeCompositePath::fixed_function_blend;
}

void apply_stroke_plan(CanvasBlendGatePlan& gate, const StrokeCompositePlan& stroke) noexcept
{
    gate.path = stroke.path;
    gate.reads_destination_color = stroke.path == StrokeCompositePath::framebuffer_fetch;
    gate.requires_auxiliary_texture = stroke.requires_auxiliary_texture;
    gate.requires_texture_copy = stroke.requires_texture_copy;
    gate.requires_render_target_blit = stroke.requires_render_target_blit;
}

void apply_feedback_plan(CanvasStrokeFeedbackPlan& plan, const pp::renderer::PaintFeedbackPlan& feedback) noexcept
{
    plan.path = composite_path_from_feedback(feedback.path);
    plan.reads_destination_color = plan.path == StrokeCompositePath::framebuffer_fetch;
    plan.requires_auxiliary_texture = feedback.requires_auxiliary_texture;
    plan.requires_texture_copy = feedback.requires_texture_copy;
    plan.requires_render_target_blit = feedback.requires_render_target_blit;
}

void mark_shader_blend_fallback(
    CanvasBlendGatePlan& gate,
    pp::renderer::RenderDeviceFeatures features) noexcept
{
    gate.shader_blend = true;
    gate.complex_blend = true;
    gate.compatibility_fallback = true;
    if (features.framebuffer_fetch) {
        gate.path = StrokeCompositePath::framebuffer_fetch;
        gate.reads_destination_color = true;
    } else if (features.texture_copy || features.render_target_blit) {
        gate.path = StrokeCompositePath::ping_pong_textures;
        gate.requires_auxiliary_texture = true;
        gate.requires_texture_copy = features.texture_copy;
        gate.requires_render_target_blit = !features.texture_copy && features.render_target_blit;
    }
}

}

pp::foundation::Status composite_layer(
    std::span<pp::paint::Rgba> destination,
    pp::renderer::Extent2D extent,
    LayerCompositeView layer) noexcept
{
    const auto pixel_count = expected_pixel_count(extent);
    if (!pixel_count) {
        return pixel_count.status();
    }

    if (destination.size() != pixel_count.value() || layer.pixels.size() != pixel_count.value()) {
        return pp::foundation::Status::invalid_argument("composite buffers must match the render extent");
    }

    if (layer.opacity < 0.0F || layer.opacity > 1.0F) {
        return pp::foundation::Status::out_of_range("layer opacity must be between 0 and 1");
    }

    if (!layer.visible || layer.opacity == 0.0F) {
        return pp::foundation::Status::success();
    }

    for (std::size_t i = 0; i < destination.size(); ++i) {
        auto stroke = layer.pixels[i];
        stroke.a *= layer.opacity;
        destination[i] = pp::paint::blend_pixels(destination[i], stroke, layer.blend_mode);
    }

    return pp::foundation::Status::success();
}

bool stroke_composite_requires_feedback(
    pp::paint::BlendMode layer_blend_mode,
    pp::paint::StrokeBlendMode stroke_blend_mode,
    bool dual_brush_blend,
    bool pattern_blend) noexcept
{
    return layer_blend_mode != pp::paint::BlendMode::normal
        || stroke_blend_mode != pp::paint::StrokeBlendMode::normal
        || dual_brush_blend
        || pattern_blend;
}

pp::foundation::Result<StrokeCompositePlan> plan_stroke_composite(
    pp::renderer::RenderDeviceFeatures features,
    StrokeCompositeRequest request) noexcept
{
    if (!is_valid_blend_mode(request.layer_blend_mode)) {
        return pp::foundation::Result<StrokeCompositePlan>::failure(
            pp::foundation::Status::invalid_argument("unknown layer blend mode"));
    }

    if (!is_valid_stroke_blend_mode(request.stroke_blend_mode)) {
        return pp::foundation::Result<StrokeCompositePlan>::failure(
            pp::foundation::Status::invalid_argument("unknown stroke blend mode"));
    }

    const pp::renderer::TextureDesc target_desc {
        .extent = request.extent,
        .format = request.target_format,
        .usage = request.target_usage,
        .debug_name = "stroke-composite-target",
    };
    const auto complex_blend = stroke_composite_requires_feedback(
        request.layer_blend_mode,
        request.stroke_blend_mode,
        request.dual_brush_blend,
        request.pattern_blend);
    const auto feedback = pp::renderer::plan_paint_feedback(features, target_desc, complex_blend);
    if (!feedback) {
        return pp::foundation::Result<StrokeCompositePlan>::failure(feedback.status());
    }

    StrokeCompositePlan plan;
    plan.path = composite_path_from_feedback(feedback.value().path);
    plan.feedback = feedback.value();
    plan.target_desc = target_desc;
    plan.target_bytes = feedback.value().target_bytes;
    plan.auxiliary_bytes = feedback.value().requires_auxiliary_texture
        ? feedback.value().target_bytes
        : 0U;
    plan.estimated_working_bytes = plan.target_bytes + plan.auxiliary_bytes;
    plan.complex_blend = complex_blend;
    plan.reads_destination_color = feedback.value().reads_destination_color;
    plan.requires_auxiliary_texture = feedback.value().requires_auxiliary_texture;
    plan.requires_texture_copy = feedback.value().requires_texture_copy;
    plan.requires_render_target_blit = feedback.value().requires_render_target_blit;
    plan.requires_explicit_transition = feedback.value().requires_explicit_transition;
    return pp::foundation::Result<StrokeCompositePlan>::success(plan);
}

pp::foundation::Result<CanvasBlendGatePlan> plan_canvas_blend_gate(
    pp::renderer::RenderDeviceFeatures features,
    CanvasBlendGateRequest request) noexcept
{
    CanvasBlendGatePlan gate;

    for (std::size_t i = 0; i < request.layer_blend_modes.size(); ++i) {
        pp::paint::BlendMode layer_blend = pp::paint::BlendMode::normal;
        if (!paint_blend_mode_from_persisted_index(request.layer_blend_modes[i], layer_blend)) {
            if (request.layer_blend_modes[i] != 0) {
                gate.first_complex_layer_index = static_cast<int>(i);
                mark_shader_blend_fallback(gate, features);
                return pp::foundation::Result<CanvasBlendGatePlan>::success(gate);
            }
            continue;
        }

        if (layer_blend == pp::paint::BlendMode::normal) {
            continue;
        }

        gate.shader_blend = true;
        gate.complex_blend = true;
        gate.first_complex_layer_index = static_cast<int>(i);
        const auto stroke = plan_stroke_composite(
            features,
            StrokeCompositeRequest {
                .extent = request.extent,
                .layer_blend_mode = layer_blend,
            });
        if (stroke) {
            apply_stroke_plan(gate, stroke.value());
        } else {
            gate.compatibility_fallback = true;
        }
        return pp::foundation::Result<CanvasBlendGatePlan>::success(gate);
    }

    pp::paint::StrokeBlendMode stroke_blend = pp::paint::StrokeBlendMode::normal;
    if (request.has_stroke_blend_mode) {
        if (!stroke_blend_mode_from_persisted_index(request.stroke_blend_mode, stroke_blend)) {
            if (request.stroke_blend_mode != 0) {
                gate.stroke_complex = true;
                mark_shader_blend_fallback(gate, features);
                return pp::foundation::Result<CanvasBlendGatePlan>::success(gate);
            }
        } else if (stroke_blend != pp::paint::StrokeBlendMode::normal) {
            gate.stroke_complex = true;
        }
    }

    gate.dual_brush_complex = request.dual_brush_blend;
    gate.pattern_complex = request.pattern_blend;
    if (!gate.stroke_complex && !gate.dual_brush_complex && !gate.pattern_complex) {
        return pp::foundation::Result<CanvasBlendGatePlan>::success(gate);
    }

    gate.shader_blend = true;
    gate.complex_blend = true;
    const auto stroke = plan_stroke_composite(
        features,
        StrokeCompositeRequest {
            .extent = request.extent,
            .stroke_blend_mode = stroke_blend,
            .dual_brush_blend = request.dual_brush_blend,
            .pattern_blend = request.pattern_blend,
        });
    if (stroke) {
        apply_stroke_plan(gate, stroke.value());
    } else {
        gate.compatibility_fallback = true;
    }

    return pp::foundation::Result<CanvasBlendGatePlan>::success(gate);
}

pp::foundation::Result<CanvasStrokeFeedbackPlan> plan_canvas_stroke_feedback(
    pp::renderer::RenderDeviceFeatures features,
    pp::renderer::Extent2D extent) noexcept
{
    const auto extent_status = pp::renderer::validate_extent(extent);
    if (!extent_status.ok()) {
        return pp::foundation::Result<CanvasStrokeFeedbackPlan>::failure(extent_status);
    }

    const pp::renderer::TextureDesc target_desc {
        .extent = extent,
        .format = pp::renderer::TextureFormat::rgba8,
        .usage = pp::renderer::TextureUsage::render_target
            | pp::renderer::TextureUsage::sampled
            | pp::renderer::TextureUsage::copy_source
            | pp::renderer::TextureUsage::copy_destination,
        .debug_name = "canvas-stroke-feedback-target",
    };
    const auto feedback = pp::renderer::plan_paint_feedback(features, target_desc, true);
    if (feedback) {
        CanvasStrokeFeedbackPlan plan;
        apply_feedback_plan(plan, feedback.value());
        return pp::foundation::Result<CanvasStrokeFeedbackPlan>::success(plan);
    }

    CanvasStrokeFeedbackPlan fallback;
    fallback.compatibility_fallback = true;
    if (features.framebuffer_fetch) {
        fallback.path = StrokeCompositePath::framebuffer_fetch;
        fallback.reads_destination_color = true;
    } else {
        fallback.path = StrokeCompositePath::ping_pong_textures;
        fallback.requires_auxiliary_texture = true;
        fallback.requires_texture_copy = features.texture_copy;
        fallback.requires_render_target_blit = !features.texture_copy && features.render_target_blit;
    }
    return pp::foundation::Result<CanvasStrokeFeedbackPlan>::success(fallback);
}

const char* stroke_composite_path_name(StrokeCompositePath path) noexcept
{
    switch (path) {
    case StrokeCompositePath::fixed_function_blend:
        return "fixed_function_blend";
    case StrokeCompositePath::framebuffer_fetch:
        return "framebuffer_fetch";
    case StrokeCompositePath::ping_pong_textures:
        return "ping_pong_textures";
    }

    return "unknown";
}

}
