#include "paint/blend.h"

#include <algorithm>
#include <cmath>

namespace pp::paint {

namespace {

[[nodiscard]] float saturate(float value) noexcept
{
    if (!std::isfinite(value)) {
        return value < 0.0F ? 0.0F : 1.0F;
    }

    return std::clamp(value, 0.0F, 1.0F);
}

[[nodiscard]] float mix(float a, float b, float t) noexcept
{
    return a * (1.0F - t) + b * t;
}

[[nodiscard]] float blend_channel(float base, float stroke, BlendMode mode) noexcept
{
    switch (mode) {
    case BlendMode::normal:
        return stroke;
    case BlendMode::multiply:
        return base * stroke;
    case BlendMode::screen:
        return 1.0F - (1.0F - base) * (1.0F - stroke);
    case BlendMode::color_dodge:
        if (stroke >= 1.0F) {
            return 1.0F;
        }
        return saturate(base / (1.0F - stroke));
    case BlendMode::overlay:
        return base < 0.5F
            ? 2.0F * base * stroke
            : 1.0F - 2.0F * (1.0F - base) * (1.0F - stroke);
    }

    return stroke;
}

[[nodiscard]] float blend_stroke_screen(float base, float stroke) noexcept
{
    return base + stroke - (base * stroke);
}

[[nodiscard]] float blend_stroke_hard_light(float base, float stroke) noexcept
{
    return stroke < 0.5F
        ? base * (stroke * 2.0F)
        : blend_stroke_screen(base, 2.0F * stroke - 1.0F);
}

[[nodiscard]] float blend_stroke_hard_mix(float base, float stroke) noexcept
{
    if (base == 0.0F) {
        return 0.0F;
    }

    return base + stroke < 0.5F ? 0.0F : saturate(base + stroke);
}

[[nodiscard]] float blend_stroke_color_dodge(float base, float stroke) noexcept
{
    if (base == 0.0F) {
        return 0.0F;
    }

    if (stroke == 1.0F) {
        return 1.0F;
    }

    return base / (1.0F - stroke);
}

[[nodiscard]] float blend_stroke_color_burn(float base, float stroke) noexcept
{
    if (base == 1.0F) {
        return 1.0F;
    }

    if (stroke == 0.0F) {
        return 0.0F;
    }

    return 1.0F - std::min(1.0F, (1.0F - base) / stroke);
}

[[nodiscard]] float blend_stroke_linear_height(float base, float stroke, float depth) noexcept
{
    const auto partial = (1.0F - stroke) * std::pow(depth, 0.25F) + (base * depth * 10.0F);
    return base * partial;
}

[[nodiscard]] float blend_stroke_height(float base, float stroke, float depth) noexcept
{
    const auto a = std::pow(1.0F - stroke, std::max(1.0F, (1.0F - depth) * 10.0F))
        * std::pow(depth, 0.25F);
    const auto b = base * depth * 5.0F;
    return base * (a + b);
}

[[nodiscard]] float blend_rgb(float base, float stroke, float base_alpha, float stroke_alpha, float alpha_total, BlendMode mode) noexcept
{
    if (alpha_total <= 0.0F) {
        return 0.0F;
    }

    const auto stroke_weight = stroke_alpha / alpha_total;
    const auto base_weight = base_alpha / alpha_total;
    if (mode == BlendMode::normal) {
        return saturate(mix(base, stroke, stroke_weight));
    }

    const auto mode_value = blend_channel(base, stroke, mode);
    return saturate(mix(stroke, mix(base, mode_value, stroke_weight), base_weight));
}

}

Rgba blend_pixels(Rgba base, Rgba stroke, BlendMode mode) noexcept
{
    base.r = saturate(base.r);
    base.g = saturate(base.g);
    base.b = saturate(base.b);
    base.a = saturate(base.a);
    stroke.r = saturate(stroke.r);
    stroke.g = saturate(stroke.g);
    stroke.b = saturate(stroke.b);
    stroke.a = saturate(stroke.a);

    if (stroke.a == 0.0F) {
        return base;
    }

    const auto contribution = (1.0F - base.a) * stroke.a;
    const auto alpha_total = saturate(base.a + contribution);

    return {
        blend_rgb(base.r, stroke.r, base.a, stroke.a, alpha_total, mode),
        blend_rgb(base.g, stroke.g, base.a, stroke.a, alpha_total, mode),
        blend_rgb(base.b, stroke.b, base.a, stroke.a, alpha_total, mode),
        alpha_total,
    };
}

float blend_stroke_alpha(
    float base,
    float stroke,
    float depth,
    StrokeBlendMode mode) noexcept
{
    base = saturate(base);
    stroke = saturate(stroke);
    depth = saturate(depth);

    switch (mode) {
    case StrokeBlendMode::normal:
        return saturate(mix(base, stroke, depth));
    case StrokeBlendMode::multiply:
        return saturate(mix(base, base * stroke, depth));
    case StrokeBlendMode::subtract:
        return saturate(mix(base, std::max(0.0F, base - stroke), depth));
    case StrokeBlendMode::darken:
        return saturate(mix(base, std::min(base, stroke), depth));
    case StrokeBlendMode::overlay:
        return saturate(mix(base, blend_stroke_hard_light(stroke, base), depth));
    case StrokeBlendMode::color_dodge:
        return saturate(mix(base, blend_stroke_color_dodge(base, stroke), depth));
    case StrokeBlendMode::color_burn:
        return saturate(mix(base, blend_stroke_color_burn(base, stroke), depth));
    case StrokeBlendMode::linear_burn:
        return saturate(mix(base, saturate(base + stroke - 1.0F), depth));
    case StrokeBlendMode::hard_mix:
        return saturate(mix(base, blend_stroke_hard_mix(base, stroke), depth));
    case StrokeBlendMode::linear_height:
        return saturate(blend_stroke_linear_height(base, stroke, depth));
    case StrokeBlendMode::height:
        return saturate(blend_stroke_height(base, stroke, depth));
    }

    return 1.0F;
}

const char* blend_mode_name(BlendMode mode) noexcept
{
    switch (mode) {
    case BlendMode::normal:
        return "normal";
    case BlendMode::multiply:
        return "multiply";
    case BlendMode::screen:
        return "screen";
    case BlendMode::color_dodge:
        return "color_dodge";
    case BlendMode::overlay:
        return "overlay";
    }

    return "unknown";
}

const char* stroke_blend_mode_name(StrokeBlendMode mode) noexcept
{
    switch (mode) {
    case StrokeBlendMode::normal:
        return "normal";
    case StrokeBlendMode::multiply:
        return "multiply";
    case StrokeBlendMode::subtract:
        return "subtract";
    case StrokeBlendMode::darken:
        return "darken";
    case StrokeBlendMode::overlay:
        return "overlay";
    case StrokeBlendMode::color_dodge:
        return "color_dodge";
    case StrokeBlendMode::color_burn:
        return "color_burn";
    case StrokeBlendMode::linear_burn:
        return "linear_burn";
    case StrokeBlendMode::hard_mix:
        return "hard_mix";
    case StrokeBlendMode::linear_height:
        return "linear_height";
    case StrokeBlendMode::height:
        return "height";
    }

    return "unknown";
}

}
