#include "paint/blend.h"
#include "test_harness.h"

#include <cmath>
#include <string_view>

using pp::paint::BlendMode;
using pp::paint::Rgba;
using pp::paint::StrokeBlendMode;
using pp::paint::blend_mode_name;
using pp::paint::blend_pixels;
using pp::paint::blend_stroke_alpha;
using pp::paint::stroke_blend_mode_name;

namespace {

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

void normal_blend_matches_source_over_alpha(pp::tests::Harness& h)
{
    const auto result = blend_pixels(
        Rgba { .r = 0.2F, .g = 0.4F, .b = 0.6F, .a = 0.5F },
        Rgba { .r = 0.8F, .g = 0.2F, .b = 0.1F, .a = 0.25F },
        BlendMode::normal);

    PP_EXPECT(h, near(result.a, 0.625F));
    PP_EXPECT(h, near(result.r, 0.44F));
    PP_EXPECT(h, near(result.g, 0.32F));
    PP_EXPECT(h, near(result.b, 0.4F));
}

void zero_alpha_stroke_leaves_base_unchanged(pp::tests::Harness& h)
{
    const Rgba base { .r = 0.2F, .g = 0.3F, .b = 0.4F, .a = 0.5F };
    const auto result = blend_pixels(
        base,
        Rgba { .r = 1.0F, .g = 1.0F, .b = 1.0F, .a = 0.0F },
        BlendMode::screen);

    PP_EXPECT(h, near(result.r, base.r));
    PP_EXPECT(h, near(result.g, base.g));
    PP_EXPECT(h, near(result.b, base.b));
    PP_EXPECT(h, near(result.a, base.a));
}

void multiply_and_screen_are_bounded(pp::tests::Harness& h)
{
    const Rgba base { .r = 0.25F, .g = 0.5F, .b = 0.75F, .a = 1.0F };
    const Rgba stroke { .r = 0.5F, .g = 0.5F, .b = 0.5F, .a = 1.0F };
    const auto multiply = blend_pixels(base, stroke, BlendMode::multiply);
    const auto screen = blend_pixels(base, stroke, BlendMode::screen);

    PP_EXPECT(h, near(multiply.r, 0.125F));
    PP_EXPECT(h, near(multiply.g, 0.25F));
    PP_EXPECT(h, near(multiply.b, 0.375F));
    PP_EXPECT(h, near(screen.r, 0.625F));
    PP_EXPECT(h, near(screen.g, 0.75F));
    PP_EXPECT(h, near(screen.b, 0.875F));
}

void color_dodge_and_overlay_handle_extremes(pp::tests::Harness& h)
{
    const auto dodge = blend_pixels(
        Rgba { .r = 0.4F, .g = 0.5F, .b = 0.6F, .a = 1.0F },
        Rgba { .r = 1.0F, .g = 0.5F, .b = 0.0F, .a = 1.0F },
        BlendMode::color_dodge);
    const auto overlay = blend_pixels(
        Rgba { .r = 0.25F, .g = 0.5F, .b = 0.75F, .a = 1.0F },
        Rgba { .r = 0.5F, .g = 0.5F, .b = 0.5F, .a = 1.0F },
        BlendMode::overlay);

    PP_EXPECT(h, near(dodge.r, 1.0F));
    PP_EXPECT(h, near(dodge.g, 1.0F));
    PP_EXPECT(h, near(dodge.b, 0.6F));
    PP_EXPECT(h, near(overlay.r, 0.25F));
    PP_EXPECT(h, near(overlay.g, 0.5F));
    PP_EXPECT(h, near(overlay.b, 0.75F));
}

void clamps_inputs_and_names_modes(pp::tests::Harness& h)
{
    const auto result = blend_pixels(
        Rgba { .r = -1.0F, .g = 2.0F, .b = 0.5F, .a = 2.0F },
        Rgba { .r = 2.0F, .g = -1.0F, .b = 0.5F, .a = 2.0F },
        BlendMode::normal);

    PP_EXPECT(h, near(result.r, 1.0F));
    PP_EXPECT(h, near(result.g, 0.0F));
    PP_EXPECT(h, near(result.b, 0.5F));
    PP_EXPECT(h, near(result.a, 1.0F));
    PP_EXPECT(h, blend_mode_name(BlendMode::overlay) == std::string_view("overlay"));
}

void stroke_alpha_blend_modes_match_shader_reference_vectors(pp::tests::Harness& h)
{
    PP_EXPECT(h, near(blend_stroke_alpha(0.2F, 0.8F, 0.25F, StrokeBlendMode::normal), 0.35F));
    PP_EXPECT(h, near(blend_stroke_alpha(0.6F, 0.5F, 1.0F, StrokeBlendMode::multiply), 0.3F));
    PP_EXPECT(h, near(blend_stroke_alpha(0.6F, 0.2F, 1.0F, StrokeBlendMode::subtract), 0.4F));
    PP_EXPECT(h, near(blend_stroke_alpha(0.6F, 0.2F, 1.0F, StrokeBlendMode::darken), 0.2F));
    PP_EXPECT(h, near(blend_stroke_alpha(0.75F, 0.25F, 1.0F, StrokeBlendMode::overlay), 0.625F));
    PP_EXPECT(h, near(blend_stroke_alpha(0.4F, 0.5F, 1.0F, StrokeBlendMode::color_dodge), 0.8F));
    PP_EXPECT(h, near(blend_stroke_alpha(0.6F, 0.5F, 1.0F, StrokeBlendMode::color_burn), 0.2F));
    PP_EXPECT(h, near(blend_stroke_alpha(0.75F, 0.5F, 1.0F, StrokeBlendMode::linear_burn), 0.25F));
    PP_EXPECT(h, near(blend_stroke_alpha(0.2F, 0.2F, 1.0F, StrokeBlendMode::hard_mix), 0.0F));
    PP_EXPECT(h, near(blend_stroke_alpha(0.7F, 0.8F, 1.0F, StrokeBlendMode::hard_mix), 1.0F));
    PP_EXPECT(h, near(blend_stroke_alpha(0.5F, 0.25F, 0.16F, StrokeBlendMode::linear_height), 0.637170F));
    PP_EXPECT(h, near(blend_stroke_alpha(0.5F, 0.25F, 0.16F, StrokeBlendMode::height), 0.228217F));
}

void stroke_alpha_blend_modes_handle_edges_and_names(pp::tests::Harness& h)
{
    PP_EXPECT(h, near(blend_stroke_alpha(0.8F, 0.2F, 0.0F, StrokeBlendMode::multiply), 0.8F));
    PP_EXPECT(h, near(blend_stroke_alpha(0.0F, 0.8F, 1.0F, StrokeBlendMode::color_dodge), 0.0F));
    PP_EXPECT(h, near(blend_stroke_alpha(0.8F, 1.0F, 1.0F, StrokeBlendMode::color_dodge), 1.0F));
    PP_EXPECT(h, near(blend_stroke_alpha(1.0F, 0.2F, 1.0F, StrokeBlendMode::color_burn), 1.0F));
    PP_EXPECT(h, near(blend_stroke_alpha(0.8F, 0.0F, 1.0F, StrokeBlendMode::color_burn), 0.0F));
    PP_EXPECT(h, near(blend_stroke_alpha(0.0F, 1.0F, 1.0F, StrokeBlendMode::hard_mix), 0.0F));
    PP_EXPECT(h, near(blend_stroke_alpha(-1.0F, 2.0F, 2.0F, StrokeBlendMode::normal), 1.0F));
    PP_EXPECT(h, near(blend_stroke_alpha(0.2F, 0.2F, 0.5F, static_cast<StrokeBlendMode>(255)), 1.0F));

    PP_EXPECT(h, stroke_blend_mode_name(StrokeBlendMode::normal) == std::string_view("normal"));
    PP_EXPECT(h, stroke_blend_mode_name(StrokeBlendMode::linear_height) == std::string_view("linear_height"));
    PP_EXPECT(h, stroke_blend_mode_name(StrokeBlendMode::height) == std::string_view("height"));
    PP_EXPECT(h, stroke_blend_mode_name(static_cast<StrokeBlendMode>(255)) == std::string_view("unknown"));
}

}

int main()
{
    pp::tests::Harness harness;
    harness.run("normal_blend_matches_source_over_alpha", normal_blend_matches_source_over_alpha);
    harness.run("zero_alpha_stroke_leaves_base_unchanged", zero_alpha_stroke_leaves_base_unchanged);
    harness.run("multiply_and_screen_are_bounded", multiply_and_screen_are_bounded);
    harness.run("color_dodge_and_overlay_handle_extremes", color_dodge_and_overlay_handle_extremes);
    harness.run("clamps_inputs_and_names_modes", clamps_inputs_and_names_modes);
    harness.run("stroke_alpha_blend_modes_match_shader_reference_vectors", stroke_alpha_blend_modes_match_shader_reference_vectors);
    harness.run("stroke_alpha_blend_modes_handle_edges_and_names", stroke_alpha_blend_modes_handle_edges_and_names);
    return harness.finish();
}
