#include "Blend2D_Cache.h"

#include <algorithm>
#include <array>
#include <cstring>
#include <filesystem>
#include <initializer_list>
#include <limits>
#include <mutex>
#include <numbers>
#include <vector>

namespace renderive::detail {
namespace {

void clear_image(BLImage& image) {
    BLImageData data{};
    if (image.get_data(&data) != BL_SUCCESS)
        return;
    for (int y = 0; y < data.size.h; ++y) {
        auto* row = static_cast<std::byte*>(data.pixel_data) +
                    static_cast<std::ptrdiff_t>(y) * data.stride;
        std::memset(row, 0, static_cast<std::size_t>(data.size.w) * sizeof(Pixel));
    }
}

void load_font_face(BLFontFace& face, std::initializer_list<const char*> candidates) {
    for (const char* path : candidates) {
        std::error_code error;
        if (std::filesystem::exists(path, error) && face.create_from_file(path) == BL_SUCCESS)
            return;
    }
}

BLFontFace& default_font_face(const Font& font) {
    static std::array<BLFontFace, 4> faces;
    static std::once_flag once;
    std::call_once(once, [] {
        load_font_face(faces[0], {
            "C:/Windows/Fonts/msyh.ttc",
            "C:/Windows/Fonts/simsun.ttc",
            "C:/Windows/Fonts/segoeui.ttf",
            "C:/Windows/Fonts/arial.ttf",
            "C:/Windows/Fonts/tahoma.ttf"
        });
        load_font_face(faces[1], {
            "C:/Windows/Fonts/msyhbd.ttc",
            "C:/Windows/Fonts/segoeuib.ttf",
            "C:/Windows/Fonts/arialbd.ttf",
            "C:/Windows/Fonts/tahomabd.ttf"
        });
        load_font_face(faces[2], {
            "C:/Windows/Fonts/segoeuii.ttf",
            "C:/Windows/Fonts/ariali.ttf",
            "C:/Windows/Fonts/tahomai.ttf"
        });
        load_font_face(faces[3], {
            "C:/Windows/Fonts/segoeuiz.ttf",
            "C:/Windows/Fonts/arialbi.ttf",
            "C:/Windows/Fonts/tahomabi.ttf"
        });
    });
    const std::size_t index = (font.weight >= 600 ? 1u : 0u) | (font.italic ? 2u : 0u);
    return faces[index] ? faces[index] : faces[0];
}

BLStrokeCap stroke_cap(Line_Cap cap) noexcept {
    switch (cap) {
    case Line_Cap::Square:
        return BL_STROKE_CAP_SQUARE;
    case Line_Cap::Round:
        return BL_STROKE_CAP_ROUND;
    case Line_Cap::Butt:
    default:
        return BL_STROKE_CAP_BUTT;
    }
}

BLStrokeJoin stroke_join(Line_Join join) noexcept {
    switch (join) {
    case Line_Join::Bevel:
        return BL_STROKE_JOIN_BEVEL;
    case Line_Join::Round:
        return BL_STROKE_JOIN_ROUND;
    case Line_Join::Miter:
    default:
        return BL_STROKE_JOIN_MITER_CLIP;
    }
}

double cubic_weight(double distance) noexcept {
    constexpr double a = -0.5;
    distance = std::abs(distance);
    if (distance < 1.0)
        return (a + 2.0) * distance * distance * distance -
               (a + 3.0) * distance * distance + 1.0;
    if (distance < 2.0)
        return a * distance * distance * distance - 5.0 * a * distance * distance +
               8.0 * a * distance - 4.0 * a;
    return 0.0;
}

std::uint8_t pixel_channel(Pixel pixel, int shift) noexcept {
    return static_cast<std::uint8_t>((pixel >> shift) & 0xFFu);
}

std::vector<Pixel> bicubic_resample(std::span<const Pixel> source,
                                    int source_width,
                                    int source_height,
                                    int destination_width,
                                    int destination_height) {
    std::vector<Pixel> destination(
        static_cast<std::size_t>(destination_width) * destination_height);
    for (int y = 0; y < destination_height; ++y) {
        const double source_y = (static_cast<double>(y) + 0.5) * source_height /
                                    destination_height -
                                0.5;
        const int base_y = static_cast<int>(std::floor(source_y));
        for (int x = 0; x < destination_width; ++x) {
            const double source_x = (static_cast<double>(x) + 0.5) * source_width /
                                        destination_width -
                                    0.5;
            const int base_x = static_cast<int>(std::floor(source_x));
            double channels[4]{};
            double total_weight{};
            for (int offset_y = -1; offset_y <= 2; ++offset_y) {
                const int sample_y = std::clamp(base_y + offset_y, 0, source_height - 1);
                const double weight_y = cubic_weight(source_y - (base_y + offset_y));
                for (int offset_x = -1; offset_x <= 2; ++offset_x) {
                    const int sample_x = std::clamp(base_x + offset_x, 0, source_width - 1);
                    const double weight = weight_y *
                                          cubic_weight(source_x - (base_x + offset_x));
                    const Pixel pixel = source[static_cast<std::size_t>(sample_y) *
                                                   source_width +
                                               sample_x];
                    channels[0] += pixel_channel(pixel, 0) * weight;
                    channels[1] += pixel_channel(pixel, 8) * weight;
                    channels[2] += pixel_channel(pixel, 16) * weight;
                    channels[3] += pixel_channel(pixel, 24) * weight;
                    total_weight += weight;
                }
            }
            const auto channel = [total_weight](double value) {
                if (total_weight != 0.0)
                    value /= total_weight;
                return static_cast<std::uint32_t>(std::clamp(std::lround(value), 0L, 255L));
            };
            destination[static_cast<std::size_t>(y) * destination_width + x] =
                channel(channels[0]) | (channel(channels[1]) << 8) |
                (channel(channels[2]) << 16) | (channel(channels[3]) << 24);
        }
    }
    return destination;
}

} // namespace

void Blend2D_Color_Cache::clear() {
    clear_image(image_);
}

void Blend2D_Color_Cache::composite(const ::Color_Cache& source) {
    const auto& typed = dynamic_cast<const Blend2D_Color_Cache&>(source);
    if (typed.image_.is_empty())
        return;
    const Size source_size = typed.size();
    if (image_.is_empty() || size() != source_size) {
        ensure_size(source_size);
        clear_image(image_);
    }

    BLContext context(image_);
    if (!context)
        return;
    context.set_comp_op(BL_COMP_OP_SRC_OVER);
    context.blit_image(BLRect(0.0, 0.0,
                             static_cast<double>(source_size.width),
                             static_cast<double>(source_size.height)),
                       typed.image_,
                       BLRectI(0, 0, source_size.width, source_size.height));
    context.end();
}

void Blend2D_Color_Cache::ensure_size(Size requested) {
    if (requested.empty()) {
        image_.reset();
        return;
    }
    if (size() == requested)
        return;
    image_.create(requested.width, requested.height, BL_FORMAT_PRGB32);
    clear_image(image_);
}

Size Blend2D_Color_Cache::size() const noexcept {
    return {image_.width(), image_.height()};
}

Image_View Blend2D_Color_Cache::view() const noexcept {
    BLImageData data{};
    if (image_.get_data(&data) != BL_SUCCESS)
        return {};
    return {
        static_cast<const std::byte*>(data.pixel_data),
        data.size.w,
        data.size.h,
        static_cast<int>(data.stride),
        Pixel_Format::Premultiplied_32
    };
}

Painter::Painter(Blend2D_Color_Cache& cache, Size size) {
    cache.ensure_size(size);
    if (!cache.image_.is_empty() && context_.begin(cache.image_) == BL_SUCCESS) {
        context_.set_comp_op(BL_COMP_OP_SRC_OVER);
        active_ = true;
    }
}

Painter::~Painter() {
    if (active_)
        context_.end();
}

Painter::Clip_Scope::Clip_Scope(Painter& painter, RectF value) {
    value = value.normalized();
    if (!painter.active_ || value.empty())
        return;
    painter.context_.save();
    painter.context_.clip_to_rect(BLRect(value.x, value.y, value.width, value.height));
    painter_ = &painter;
}

Painter::Clip_Scope::~Clip_Scope() {
    if (painter_)
        painter_->context_.restore();
}

Painter::Clip_Scope Painter::scoped_clip(RectF value) {
    return Clip_Scope(*this, value);
}

void Painter::composite(const Blend2D_Color_Cache& source) {
    if (!active_ || source.image_.is_empty())
        return;
    const Size source_size = source.size();
    context_.blit_image(BLRect(0.0, 0.0,
                              static_cast<double>(source_size.width),
                              static_cast<double>(source_size.height)),
                        source.image_,
                        BLRectI(0, 0, source_size.width, source_size.height));
}

BLRgba Painter::rgba(Color color) noexcept {
    constexpr double scale = 1.0 / 255.0;
    return BLRgba(color.r * scale, color.g * scale, color.b * scale, color.a * scale);
}

void Painter::apply_pen(const Pen& pen) {
    context_.set_stroke_style(rgba(pen.color));
    context_.set_stroke_width(std::max(0.1, pen.width));
    context_.set_stroke_caps(stroke_cap(pen.cap));
    context_.set_stroke_join(stroke_join(pen.join));
    BLArray<double> pattern;
    if (pen.style == Line_Style::Dash) {
        pattern.append(std::max(1.0, pen.width * 4.0));
        pattern.append(std::max(1.0, pen.width * 2.0));
    } else if (pen.style == Line_Style::Dot) {
        pattern.append(std::max(1.0, pen.width));
        pattern.append(std::max(1.0, pen.width * 2.0));
    }
    context_.set_stroke_dash_array(pattern);
}

void Painter::line(PointF first, PointF second, const Pen& pen) {
    if (!active_ || !pen.enabled())
        return;
    apply_pen(pen);
    context_.stroke_line(first.x, first.y, second.x, second.y);
}

void Painter::polyline(std::span<const PointF> points, const Pen& pen) {
    if (!active_ || !pen.enabled() || points.size() < 2)
        return;
    BLPath path;
    path.move_to(points.front().x, points.front().y);
    for (std::size_t index = 1; index < points.size(); ++index)
        path.line_to(points[index].x, points[index].y);
    apply_pen(pen);
    context_.stroke_path(path);
}

void Painter::polygon(std::span<const PointF> points, const Pen& pen, const Brush& brush) {
    if (!active_ || points.size() < 3)
        return;
    BLPath path;
    path.move_to(points.front().x, points.front().y);
    for (std::size_t index = 1; index < points.size(); ++index)
        path.line_to(points[index].x, points[index].y);
    path.close();
    if (brush.enabled()) {
        context_.set_fill_style(rgba(brush.color));
        context_.fill_path(path);
    }
    if (pen.enabled()) {
        apply_pen(pen);
        context_.stroke_path(path);
    }
}

void Painter::rect(RectF value, const Pen& pen, const Brush& brush) {
    if (!active_)
        return;
    value = value.normalized();
    if (value.empty())
        return;
    const BLRect rect(value.x, value.y, value.width, value.height);
    if (brush.enabled()) {
        context_.set_fill_style(rgba(brush.color));
        context_.fill_rect(rect);
    }
    if (pen.enabled()) {
        apply_pen(pen);
        context_.stroke_rect(rect);
    }
}

void Painter::circle(PointF center, double radius, const Pen& pen, const Brush& brush) {
    if (!active_ || radius <= 0.0)
        return;
    BLPath path;
    path.add_ellipse(BLEllipse(center.x, center.y, radius, radius));
    if (brush.enabled()) {
        context_.set_fill_style(rgba(brush.color));
        context_.fill_path(path);
    }
    if (pen.enabled()) {
        apply_pen(pen);
        context_.stroke_path(path);
    }
}

BLFont Painter::make_font(const Font& font) {
    BLFont result;
    auto& face = default_font_face(font);
    if (face)
        result.create_from_face(face, static_cast<float>(std::max(1.0, font.size)));
    return result;
}

void Painter::text(PointF position, std::string_view value, const Font& font, const Pen& pen,
                   double rotation_degrees) {
    if (!active_ || value.empty() || !pen.enabled())
        return;
    BLFont bl_font = make_font(font);
    if (!bl_font)
        return;
    context_.set_fill_style(rgba(pen.color));
    const auto& metrics = bl_font.metrics();
    const double baseline = position.y + metrics.ascent;
    const bool rotated = std::isfinite(rotation_degrees) && rotation_degrees != 0.0;
    if (rotated) {
        context_.save();
        context_.rotate(rotation_degrees * std::numbers::pi / 180.0, position.x, baseline);
    }
    context_.fill_utf8_text(BLPoint(position.x, baseline),
                            bl_font, value.data(), value.size());
    if (rotated)
        context_.restore();
}

void Painter::heatmap(RectF target,
                      int width,
                      int height,
                      std::span<const Pixel> pixels,
                      Image_Interpolation_Mode interpolation) {
    target = target.normalized();
    if (!active_ || target.empty() || width <= 0 || height <= 0 ||
        pixels.size() < static_cast<std::size_t>(width) * static_cast<std::size_t>(height))
        return;

    std::vector<Pixel> resampled;
    if (interpolation == Image_Interpolation_Mode::Bicubic &&
        target.width <= static_cast<double>(std::numeric_limits<int>::max()) &&
        target.height <= static_cast<double>(std::numeric_limits<int>::max())) {
        const int destination_width = std::max(1, static_cast<int>(std::lround(target.width)));
        const int destination_height = std::max(1, static_cast<int>(std::lround(target.height)));
        resampled = bicubic_resample(pixels, width, height,
                                     destination_width, destination_height);
        pixels = resampled;
        width = destination_width;
        height = destination_height;
    }

    BLImage image(width, height, BL_FORMAT_PRGB32);
    BLImageData data{};
    if (image.get_data(&data) != BL_SUCCESS)
        return;
    for (int y = 0; y < height; ++y) {
        auto* destination = reinterpret_cast<Pixel*>(
            static_cast<std::byte*>(data.pixel_data) + static_cast<std::ptrdiff_t>(y) * data.stride);
        std::copy_n(pixels.data() + static_cast<std::size_t>(y) * width, width, destination);
    }
    context_.set_pattern_quality(interpolation == Image_Interpolation_Mode::Bilinear
                                     ? BL_PATTERN_QUALITY_BILINEAR
                                     : BL_PATTERN_QUALITY_NEAREST);
    context_.blit_image(BLRect(target.x, target.y, target.width, target.height),
                        image, BLRectI(0, 0, width, height));
}

} // namespace renderive::detail
