#include "paint_renderer/compositor.h"
#include "test_harness.h"

#include <cmath>
#include <string_view>
#include <vector>

using pp::foundation::StatusCode;
using pp::paint::BlendMode;
using pp::paint::Rgba;
using pp::paint::StrokeBlendMode;
using pp::paint_renderer::LayerCompositeView;
using pp::paint_renderer::CanvasBlendGateRequest;
using pp::paint_renderer::StrokeCompositePath;
using pp::paint_renderer::StrokeCompositeRequest;
using pp::paint_renderer::composite_layer;
using pp::paint_renderer::plan_canvas_blend_gate;
using pp::paint_renderer::plan_canvas_stroke_feedback;
using pp::paint_renderer::plan_stroke_composite;
using pp::paint_renderer::stroke_composite_path_name;
using pp::paint_renderer::stroke_composite_requires_feedback;
using pp::renderer::Extent2D;
using pp::renderer::RenderDeviceFeatures;
using pp::renderer::TextureFormat;
using pp::renderer::TextureUsage;

namespace {

bool near(float a, float b)
{
    return std::fabs(a - b) < 0.0001F;
}

void composites_visible_layer_with_opacity(pp::tests::Harness& h)
{
    std::vector<Rgba> destination {
        Rgba { .r = 0.2F, .g = 0.4F, .b = 0.6F, .a = 0.5F },
    };
    const std::vector<Rgba> foreground {
        Rgba { .r = 0.8F, .g = 0.2F, .b = 0.1F, .a = 0.5F },
    };

    const auto status = composite_layer(
        destination,
        Extent2D { .width = 1, .height = 1 },
        LayerCompositeView {
            .pixels = foreground,
            .opacity = 0.5F,
            .visible = true,
            .blend_mode = BlendMode::normal,
        });

    PP_EXPECT(h, status.ok());
    PP_EXPECT(h, near(destination[0].a, 0.625F));
    PP_EXPECT(h, near(destination[0].r, 0.44F));
    PP_EXPECT(h, near(destination[0].g, 0.32F));
    PP_EXPECT(h, near(destination[0].b, 0.4F));
}

void invisible_and_zero_opacity_layers_are_noops(pp::tests::Harness& h)
{
    const Rgba original { .r = 0.1F, .g = 0.2F, .b = 0.3F, .a = 0.4F };
    std::vector<Rgba> destination { original };
    const std::vector<Rgba> foreground {
        Rgba { .r = 1.0F, .g = 1.0F, .b = 1.0F, .a = 1.0F },
    };

    PP_EXPECT(h, composite_layer(
        destination,
        Extent2D { .width = 1, .height = 1 },
        LayerCompositeView { .pixels = foreground, .opacity = 1.0F, .visible = false }).ok());
    PP_EXPECT(h, near(destination[0].r, original.r));
    PP_EXPECT(h, near(destination[0].g, original.g));
    PP_EXPECT(h, near(destination[0].b, original.b));
    PP_EXPECT(h, near(destination[0].a, original.a));

    PP_EXPECT(h, composite_layer(
        destination,
        Extent2D { .width = 1, .height = 1 },
        LayerCompositeView { .pixels = foreground, .opacity = 0.0F, .visible = true }).ok());
    PP_EXPECT(h, near(destination[0].r, original.r));
    PP_EXPECT(h, near(destination[0].g, original.g));
    PP_EXPECT(h, near(destination[0].b, original.b));
    PP_EXPECT(h, near(destination[0].a, original.a));
}

void rejects_invalid_sizes_and_opacity(pp::tests::Harness& h)
{
    std::vector<Rgba> destination(2);
    const std::vector<Rgba> foreground(1);

    const auto mismatched = composite_layer(
        destination,
        Extent2D { .width = 2, .height = 1 },
        LayerCompositeView { .pixels = foreground });
    const auto bad_opacity = composite_layer(
        destination,
        Extent2D { .width = 2, .height = 1 },
        LayerCompositeView { .pixels = destination, .opacity = 1.5F });
    const auto bad_extent = composite_layer(
        destination,
        Extent2D { .width = 0, .height = 1 },
        LayerCompositeView { .pixels = destination });

    PP_EXPECT(h, !mismatched.ok());
    PP_EXPECT(h, mismatched.code == StatusCode::invalid_argument);
    PP_EXPECT(h, !bad_opacity.ok());
    PP_EXPECT(h, bad_opacity.code == StatusCode::out_of_range);
    PP_EXPECT(h, !bad_extent.ok());
    PP_EXPECT(h, bad_extent.code == StatusCode::invalid_argument);
}

void detects_feedback_requirements(pp::tests::Harness& h)
{
    PP_EXPECT(h, !stroke_composite_requires_feedback(
        BlendMode::normal,
        StrokeBlendMode::normal,
        false,
        false));
    PP_EXPECT(h, stroke_composite_requires_feedback(
        BlendMode::multiply,
        StrokeBlendMode::normal,
        false,
        false));
    PP_EXPECT(h, stroke_composite_requires_feedback(
        BlendMode::normal,
        StrokeBlendMode::overlay,
        false,
        false));
    PP_EXPECT(h, stroke_composite_requires_feedback(
        BlendMode::normal,
        StrokeBlendMode::normal,
        true,
        false));
    PP_EXPECT(h, stroke_composite_requires_feedback(
        BlendMode::normal,
        StrokeBlendMode::normal,
        false,
        true));
}

void plans_stroke_composite_paths(pp::tests::Harness& h)
{
    const StrokeCompositeRequest simple {
        .extent = Extent2D { .width = 64, .height = 32 },
        .target_usage = TextureUsage::render_target,
    };
    const auto fixed = plan_stroke_composite(
        RenderDeviceFeatures {},
        simple);
    PP_EXPECT(h, fixed);
    if (fixed) {
        PP_EXPECT(h, fixed.value().path == StrokeCompositePath::fixed_function_blend);
        PP_EXPECT(h, !fixed.value().complex_blend);
        PP_EXPECT(h, !fixed.value().reads_destination_color);
        PP_EXPECT(h, fixed.value().target_bytes == 8192U);
        PP_EXPECT(h, fixed.value().estimated_working_bytes == 8192U);
    }

    const StrokeCompositeRequest complex_fetch {
        .extent = Extent2D { .width = 32, .height = 16 },
        .target_usage = TextureUsage::render_target,
        .stroke_blend_mode = StrokeBlendMode::height,
    };
    const auto fetch = plan_stroke_composite(
        RenderDeviceFeatures {
            .framebuffer_fetch = true,
            .explicit_texture_transitions = true,
        },
        complex_fetch);
    PP_EXPECT(h, fetch);
    if (fetch) {
        PP_EXPECT(h, fetch.value().path == StrokeCompositePath::framebuffer_fetch);
        PP_EXPECT(h, fetch.value().complex_blend);
        PP_EXPECT(h, fetch.value().reads_destination_color);
        PP_EXPECT(h, !fetch.value().requires_auxiliary_texture);
        PP_EXPECT(h, fetch.value().requires_explicit_transition);
        PP_EXPECT(h, fetch.value().target_bytes == 2048U);
    }

    const StrokeCompositeRequest complex_copy {
        .extent = Extent2D { .width = 32, .height = 16 },
        .layer_blend_mode = BlendMode::overlay,
        .dual_brush_blend = true,
    };
    const auto copy = plan_stroke_composite(
        RenderDeviceFeatures { .texture_copy = true },
        complex_copy);
    PP_EXPECT(h, copy);
    if (copy) {
        PP_EXPECT(h, copy.value().path == StrokeCompositePath::ping_pong_textures);
        PP_EXPECT(h, copy.value().requires_auxiliary_texture);
        PP_EXPECT(h, copy.value().requires_texture_copy);
        PP_EXPECT(h, !copy.value().requires_render_target_blit);
        PP_EXPECT(h, copy.value().target_bytes == 2048U);
        PP_EXPECT(h, copy.value().auxiliary_bytes == 2048U);
        PP_EXPECT(h, copy.value().estimated_working_bytes == 4096U);
    }

    const auto blit = plan_stroke_composite(
        RenderDeviceFeatures { .render_target_blit = true },
        StrokeCompositeRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .pattern_blend = true,
        });
    PP_EXPECT(h, blit);
    if (blit) {
        PP_EXPECT(h, blit.value().path == StrokeCompositePath::ping_pong_textures);
        PP_EXPECT(h, !blit.value().requires_texture_copy);
        PP_EXPECT(h, blit.value().requires_render_target_blit);
    }
}

void rejects_bad_stroke_composite_plans(pp::tests::Harness& h)
{
    const auto unsupported = plan_stroke_composite(
        RenderDeviceFeatures {},
        StrokeCompositeRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .layer_blend_mode = BlendMode::multiply,
        });
    const auto missing_usage = plan_stroke_composite(
        RenderDeviceFeatures { .texture_copy = true },
        StrokeCompositeRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .target_usage = TextureUsage::render_target,
            .layer_blend_mode = BlendMode::multiply,
        });
    const auto depth = plan_stroke_composite(
        RenderDeviceFeatures { .texture_copy = true },
        StrokeCompositeRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .target_format = TextureFormat::depth24_stencil8,
            .layer_blend_mode = BlendMode::multiply,
        });
    const auto bad_blend = plan_stroke_composite(
        RenderDeviceFeatures { .texture_copy = true },
        StrokeCompositeRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .layer_blend_mode = static_cast<BlendMode>(255),
        });
    const auto bad_stroke_blend = plan_stroke_composite(
        RenderDeviceFeatures { .texture_copy = true },
        StrokeCompositeRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .stroke_blend_mode = static_cast<StrokeBlendMode>(255),
        });

    PP_EXPECT(h, !unsupported.ok());
    PP_EXPECT(h, unsupported.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, !missing_usage.ok());
    PP_EXPECT(h, missing_usage.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, !depth.ok());
    PP_EXPECT(h, depth.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, !bad_blend.ok());
    PP_EXPECT(h, bad_blend.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, !bad_stroke_blend.ok());
    PP_EXPECT(h, bad_stroke_blend.status().code == StatusCode::invalid_argument);
    PP_EXPECT(h, stroke_composite_path_name(StrokeCompositePath::fixed_function_blend) == std::string_view("fixed_function_blend"));
    PP_EXPECT(h, stroke_composite_path_name(StrokeCompositePath::framebuffer_fetch) == std::string_view("framebuffer_fetch"));
    PP_EXPECT(h, stroke_composite_path_name(StrokeCompositePath::ping_pong_textures) == std::string_view("ping_pong_textures"));
    PP_EXPECT(h, stroke_composite_path_name(static_cast<StrokeCompositePath>(255)) == std::string_view("unknown"));
}

void plans_canvas_blend_gate_from_persisted_indices(pp::tests::Harness& h)
{
    const std::vector<int> normal_layers { 0, 0, 0 };
    const auto normal = plan_canvas_blend_gate(
        RenderDeviceFeatures {},
        CanvasBlendGateRequest {
            .extent = Extent2D { .width = 0, .height = 0 },
            .layer_blend_modes = normal_layers,
            .has_stroke_blend_mode = true,
            .stroke_blend_mode = 0,
        });
    PP_EXPECT(h, normal);
    if (normal) {
        PP_EXPECT(h, !normal.value().shader_blend);
        PP_EXPECT(h, !normal.value().complex_blend);
        PP_EXPECT(h, !normal.value().compatibility_fallback);
    }

    const std::vector<int> layer_blend { 0, 4 };
    const auto layer = plan_canvas_blend_gate(
        RenderDeviceFeatures { .framebuffer_fetch = true },
        CanvasBlendGateRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .layer_blend_modes = layer_blend,
        });
    PP_EXPECT(h, layer);
    if (layer) {
        PP_EXPECT(h, layer.value().shader_blend);
        PP_EXPECT(h, layer.value().complex_blend);
        PP_EXPECT(h, layer.value().first_complex_layer_index == 1);
        PP_EXPECT(h, layer.value().path == StrokeCompositePath::framebuffer_fetch);
        PP_EXPECT(h, layer.value().reads_destination_color);
    }

    const auto stroke = plan_canvas_blend_gate(
        RenderDeviceFeatures { .texture_copy = true },
        CanvasBlendGateRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .layer_blend_modes = normal_layers,
            .has_stroke_blend_mode = true,
            .stroke_blend_mode = 10,
        });
    PP_EXPECT(h, stroke);
    if (stroke) {
        PP_EXPECT(h, stroke.value().shader_blend);
        PP_EXPECT(h, stroke.value().stroke_complex);
        PP_EXPECT(h, stroke.value().first_complex_layer_index == -1);
        PP_EXPECT(h, stroke.value().path == StrokeCompositePath::ping_pong_textures);
        PP_EXPECT(h, !stroke.value().reads_destination_color);
        PP_EXPECT(h, stroke.value().requires_texture_copy);
    }
}

void canvas_blend_gate_preserves_legacy_fallbacks(pp::tests::Harness& h)
{
    const std::vector<int> unknown_layer { 0, 99 };
    const auto unknown = plan_canvas_blend_gate(
        RenderDeviceFeatures { .texture_copy = true },
        CanvasBlendGateRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .layer_blend_modes = unknown_layer,
        });
    PP_EXPECT(h, unknown);
    if (unknown) {
        PP_EXPECT(h, unknown.value().shader_blend);
        PP_EXPECT(h, unknown.value().complex_blend);
        PP_EXPECT(h, unknown.value().compatibility_fallback);
        PP_EXPECT(h, unknown.value().first_complex_layer_index == 1);
        PP_EXPECT(h, unknown.value().path == StrokeCompositePath::ping_pong_textures);
        PP_EXPECT(h, unknown.value().requires_auxiliary_texture);
        PP_EXPECT(h, unknown.value().requires_texture_copy);
    }

    const std::vector<int> normal_layers { 0 };
    const auto unsupported = plan_canvas_blend_gate(
        RenderDeviceFeatures {},
        CanvasBlendGateRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .layer_blend_modes = normal_layers,
            .has_stroke_blend_mode = true,
            .stroke_blend_mode = 10,
        });
    PP_EXPECT(h, unsupported);
    if (unsupported) {
        PP_EXPECT(h, unsupported.value().shader_blend);
        PP_EXPECT(h, unsupported.value().stroke_complex);
        PP_EXPECT(h, unsupported.value().compatibility_fallback);
        PP_EXPECT(h, !unsupported.value().requires_texture_copy);
    }

    const auto unknown_fetch = plan_canvas_blend_gate(
        RenderDeviceFeatures { .framebuffer_fetch = true },
        CanvasBlendGateRequest {
            .extent = Extent2D { .width = 32, .height = 16 },
            .layer_blend_modes = unknown_layer,
        });
    PP_EXPECT(h, unknown_fetch);
    if (unknown_fetch) {
        PP_EXPECT(h, unknown_fetch.value().compatibility_fallback);
        PP_EXPECT(h, unknown_fetch.value().path == StrokeCompositePath::framebuffer_fetch);
        PP_EXPECT(h, unknown_fetch.value().reads_destination_color);
        PP_EXPECT(h, !unknown_fetch.value().requires_texture_copy);
    }

    const auto dual_pattern = plan_canvas_blend_gate(
        RenderDeviceFeatures { .render_target_blit = true },
        CanvasBlendGateRequest {
            .extent = Extent2D { .width = 16, .height = 16 },
            .layer_blend_modes = normal_layers,
            .dual_brush_blend = true,
            .pattern_blend = true,
        });
    PP_EXPECT(h, dual_pattern);
    if (dual_pattern) {
        PP_EXPECT(h, dual_pattern.value().shader_blend);
        PP_EXPECT(h, dual_pattern.value().dual_brush_complex);
        PP_EXPECT(h, dual_pattern.value().pattern_complex);
        PP_EXPECT(h, dual_pattern.value().requires_render_target_blit);
    }
}

void plans_canvas_stroke_feedback_paths(pp::tests::Harness& h)
{
    const Extent2D extent { .width = 32, .height = 16 };
    const auto fetch = plan_canvas_stroke_feedback(
        RenderDeviceFeatures { .framebuffer_fetch = true },
        extent);
    PP_EXPECT(h, fetch);
    if (fetch) {
        PP_EXPECT(h, fetch.value().path == StrokeCompositePath::framebuffer_fetch);
        PP_EXPECT(h, fetch.value().reads_destination_color);
        PP_EXPECT(h, !fetch.value().requires_auxiliary_texture);
        PP_EXPECT(h, !fetch.value().compatibility_fallback);
    }

    const auto copy = plan_canvas_stroke_feedback(
        RenderDeviceFeatures { .texture_copy = true },
        extent);
    PP_EXPECT(h, copy);
    if (copy) {
        PP_EXPECT(h, copy.value().path == StrokeCompositePath::ping_pong_textures);
        PP_EXPECT(h, !copy.value().reads_destination_color);
        PP_EXPECT(h, copy.value().requires_auxiliary_texture);
        PP_EXPECT(h, copy.value().requires_texture_copy);
        PP_EXPECT(h, !copy.value().requires_render_target_blit);
    }

    const auto blit = plan_canvas_stroke_feedback(
        RenderDeviceFeatures { .render_target_blit = true },
        extent);
    PP_EXPECT(h, blit);
    if (blit) {
        PP_EXPECT(h, blit.value().path == StrokeCompositePath::ping_pong_textures);
        PP_EXPECT(h, blit.value().requires_auxiliary_texture);
        PP_EXPECT(h, !blit.value().requires_texture_copy);
        PP_EXPECT(h, blit.value().requires_render_target_blit);
    }
}

void canvas_stroke_feedback_preserves_legacy_fallback(pp::tests::Harness& h)
{
    const auto fallback = plan_canvas_stroke_feedback(
        RenderDeviceFeatures {},
        Extent2D { .width = 32, .height = 16 });
    PP_EXPECT(h, fallback);
    if (fallback) {
        PP_EXPECT(h, fallback.value().path == StrokeCompositePath::ping_pong_textures);
        PP_EXPECT(h, fallback.value().requires_auxiliary_texture);
        PP_EXPECT(h, !fallback.value().requires_texture_copy);
        PP_EXPECT(h, fallback.value().compatibility_fallback);
    }

    const auto invalid = plan_canvas_stroke_feedback(
        RenderDeviceFeatures { .texture_copy = true },
        Extent2D { .width = 0, .height = 16 });
    PP_EXPECT(h, !invalid.ok());
    PP_EXPECT(h, invalid.status().code == StatusCode::invalid_argument);
}

}

int main()
{
    pp::tests::Harness harness;
    harness.run("composites_visible_layer_with_opacity", composites_visible_layer_with_opacity);
    harness.run("invisible_and_zero_opacity_layers_are_noops", invisible_and_zero_opacity_layers_are_noops);
    harness.run("rejects_invalid_sizes_and_opacity", rejects_invalid_sizes_and_opacity);
    harness.run("detects_feedback_requirements", detects_feedback_requirements);
    harness.run("plans_stroke_composite_paths", plans_stroke_composite_paths);
    harness.run("rejects_bad_stroke_composite_plans", rejects_bad_stroke_composite_plans);
    harness.run("plans_canvas_blend_gate_from_persisted_indices", plans_canvas_blend_gate_from_persisted_indices);
    harness.run("canvas_blend_gate_preserves_legacy_fallbacks", canvas_blend_gate_preserves_legacy_fallbacks);
    harness.run("plans_canvas_stroke_feedback_paths", plans_canvas_stroke_feedback_paths);
    harness.run("canvas_stroke_feedback_preserves_legacy_fallback", canvas_stroke_feedback_preserves_legacy_fallback);
    return harness.finish();
}
