#include "paint_renderer/compositor.h"

#include "assets/image_pixels.h"
#include "renderer_api/recording_renderer.h"

#include <algorithm>
#include <cmath>
#include <limits>
#include <utility>

namespace pp::paint_renderer {

pp::foundation::Result<DocumentFrameCompositeResult> composite_document_layer_frame(
    const pp::document::CanvasDocument& document,
    std::size_t layer_index,
    std::size_t frame_index,
    pp::paint::Rgba clear_color);

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]] CanvasStrokeBox box_union(CanvasStrokeBox lhs, CanvasStrokeBox rhs) noexcept
{
    return CanvasStrokeBox {
        .min_x = std::min(lhs.min_x, rhs.min_x),
        .min_y = std::min(lhs.min_y, rhs.min_y),
        .max_x = std::max(lhs.max_x, rhs.max_x),
        .max_y = std::max(lhs.max_y, rhs.max_y),
    };
}

[[nodiscard]] CanvasStrokeBox clamp_box_to_legacy_dirty_extent(
    CanvasStrokeBox box,
    pp::renderer::Extent2D extent) noexcept
{
    const auto max_value = static_cast<float>(extent.width);
    return CanvasStrokeBox {
        .min_x = std::clamp(box.min_x, 0.0F, max_value),
        .min_y = std::clamp(box.min_y, 0.0F, max_value),
        .max_x = std::clamp(box.max_x, 0.0F, max_value),
        .max_y = std::clamp(box.max_y, 0.0F, max_value),
    };
}

[[nodiscard]] bool has_positive_area(CanvasStrokeBox box) noexcept
{
    return box.max_x > box.min_x && box.max_y > box.min_y;
}

[[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]] pp::paint::Rgba rgba8_pixel(std::span<const std::uint8_t> bytes) noexcept
{
    constexpr auto inv = 1.0F / 255.0F;
    return pp::paint::Rgba {
        .r = static_cast<float>(bytes[0]) * inv,
        .g = static_cast<float>(bytes[1]) * inv,
        .b = static_cast<float>(bytes[2]) * inv,
        .a = static_cast<float>(bytes[3]) * inv,
    };
}

[[nodiscard]] std::byte rgba8_channel(float value) noexcept
{
    const auto clamped = std::clamp(value, 0.0F, 1.0F);
    return static_cast<std::byte>(static_cast<std::uint8_t>(clamped * 255.0F + 0.5F));
}

struct CubeFaceSample {
    std::size_t face_index = 0;
    float s = 0.0F;
    float t = 0.0F;
};

constexpr float document_depth_export_default_fov_degrees = 85.0F;

[[nodiscard]] CubeFaceSample panopainter_cube_face_sample(float x, float y, float z) noexcept
{
    const auto ax = std::fabs(x);
    const auto ay = std::fabs(y);
    const auto az = std::fabs(z);

    if (ax >= ay && ax >= az) {
        if (x >= 0.0F) {
            return CubeFaceSample { .face_index = 3U, .s = -z / ax, .t = -y / ax };
        }
        return CubeFaceSample { .face_index = 1U, .s = z / ax, .t = -y / ax };
    }

    if (ay >= ax && ay >= az) {
        if (y >= 0.0F) {
            return CubeFaceSample { .face_index = 5U, .s = x / ay, .t = z / ay };
        }
        return CubeFaceSample { .face_index = 4U, .s = x / ay, .t = -z / ay };
    }

    if (z >= 0.0F) {
        return CubeFaceSample { .face_index = 2U, .s = x / az, .t = -y / az };
    }
    return CubeFaceSample { .face_index = 0U, .s = -x / az, .t = -y / az };
}

[[nodiscard]] pp::paint::Rgba sample_face_nearest(
    const DocumentFaceCompositeResult& face,
    float s,
    float t) noexcept
{
    const auto width = face.extent.width;
    const auto height = face.extent.height;
    const auto u = std::clamp((s + 1.0F) * 0.5F, 0.0F, 1.0F);
    const auto v = std::clamp((t + 1.0F) * 0.5F, 0.0F, 1.0F);
    const auto x = std::min(static_cast<std::uint32_t>(u * static_cast<float>(width)), width - 1U);
    const auto y = std::min(static_cast<std::uint32_t>(v * static_cast<float>(height)), height - 1U);
    return face.pixels[static_cast<std::size_t>(y) * width + x];
}

void append_rgba8_bytes(std::vector<std::byte>& bytes, std::span<const pp::paint::Rgba> pixels)
{
    bytes.clear();
    bytes.reserve(pixels.size() * pp::document::rgba8_components);
    for (const auto& pixel : pixels) {
        bytes.push_back(rgba8_channel(pixel.r));
        bytes.push_back(rgba8_channel(pixel.g));
        bytes.push_back(rgba8_channel(pixel.b));
        bytes.push_back(rgba8_channel(pixel.a));
    }
}

[[nodiscard]] pp::foundation::Status composite_face_payload(
    std::span<pp::paint::Rgba> destination,
    pp::renderer::Extent2D extent,
    const pp::document::LayerFacePixels& payload,
    const pp::document::Layer& layer) noexcept
{
    if (payload.x > extent.width || payload.width > extent.width - payload.x
        || payload.y > extent.height || payload.height > extent.height - payload.y) {
        return pp::foundation::Status::out_of_range("document face payload rectangle is outside the render extent");
    }

    const auto payload_pixel_count = static_cast<std::uint64_t>(payload.width)
        * static_cast<std::uint64_t>(payload.height);
    if (payload_pixel_count > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max() / 4U)
        || payload.rgba8.size() != static_cast<std::size_t>(payload_pixel_count) * 4U) {
        return pp::foundation::Status::invalid_argument("document face payload byte size does not match dimensions");
    }

    for (std::uint32_t y = 0; y < payload.height; ++y) {
        for (std::uint32_t x = 0; x < payload.width; ++x) {
            const auto payload_index = (static_cast<std::size_t>(y) * payload.width + x) * 4U;
            const auto destination_index = static_cast<std::size_t>(payload.y + y) * extent.width
                + static_cast<std::size_t>(payload.x + x);
            auto stroke = rgba8_pixel(std::span<const std::uint8_t>(&payload.rgba8[payload_index], 4U));
            stroke.a *= layer.opacity;
            destination[destination_index] = pp::paint::blend_pixels(
                destination[destination_index],
                stroke,
                layer.blend_mode);
        }
    }

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

[[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;
    }
}

void append_rgba8_bytes(std::vector<std::uint8_t>& bytes, std::span<const pp::paint::Rgba> pixels)
{
    bytes.clear();
    bytes.reserve(pixels.size() * pp::document::rgba8_components);
    for (const auto& pixel : pixels) {
        bytes.push_back(static_cast<std::uint8_t>(rgba8_channel(pixel.r)));
        bytes.push_back(static_cast<std::uint8_t>(rgba8_channel(pixel.g)));
        bytes.push_back(static_cast<std::uint8_t>(rgba8_channel(pixel.b)));
        bytes.push_back(static_cast<std::uint8_t>(rgba8_channel(pixel.a)));
    }
}

struct EquirectangularProjectionResult {
    pp::renderer::Extent2D face_extent {};
    pp::renderer::Extent2D equirectangular_extent {};
    std::vector<pp::paint::Rgba> pixels;
    std::size_t face_payload_count = 0;
    std::size_t composited_layer_face_count = 0;
};

struct PerspectiveProjectionMap {
    pp::renderer::Extent2D output_extent {};
    std::vector<CubeFaceSample> samples;
};

pp::foundation::Result<PerspectiveProjectionMap> make_perspective_projection_map(
    pp::renderer::Extent2D output_extent,
    float vertical_fov_degrees)
{
    if (!std::isfinite(vertical_fov_degrees)) {
        return pp::foundation::Result<PerspectiveProjectionMap>::failure(
            pp::foundation::Status::invalid_argument("document depth export field of view must be finite"));
    }
    if (vertical_fov_degrees <= 0.0F || vertical_fov_degrees >= 180.0F) {
        return pp::foundation::Result<PerspectiveProjectionMap>::failure(
            pp::foundation::Status::out_of_range("document depth export field of view must be between 0 and 180"));
    }

    const auto output_pixel_count = expected_pixel_count(output_extent);
    if (!output_pixel_count) {
        return pp::foundation::Result<PerspectiveProjectionMap>::failure(output_pixel_count.status());
    }

    PerspectiveProjectionMap map;
    map.output_extent = output_extent;
    map.samples.reserve(output_pixel_count.value());

    constexpr auto pi = 3.14159265358979323846F;
    const auto tan_half_fov = std::tan(vertical_fov_degrees * pi / 360.0F);
    const auto aspect = static_cast<float>(output_extent.width) / static_cast<float>(output_extent.height);
    for (std::uint32_t y = 0; y < output_extent.height; ++y) {
        const auto ny = 1.0F
            - ((static_cast<float>(y) + 0.5F) / static_cast<float>(output_extent.height)) * 2.0F;
        for (std::uint32_t x = 0; x < output_extent.width; ++x) {
            const auto nx = ((static_cast<float>(x) + 0.5F) / static_cast<float>(output_extent.width)) * 2.0F
                - 1.0F;
            const auto dir_x = nx * aspect * tan_half_fov;
            const auto dir_y = ny * tan_half_fov;
            constexpr auto dir_z = -1.0F;
            const auto inv_length = 1.0F / std::sqrt(dir_x * dir_x + dir_y * dir_y + dir_z * dir_z);
            map.samples.push_back(panopainter_cube_face_sample(
                dir_x * inv_length,
                dir_y * inv_length,
                dir_z * inv_length));
        }
    }

    return pp::foundation::Result<PerspectiveProjectionMap>::success(std::move(map));
}

pp::foundation::Result<EquirectangularProjectionResult> project_document_frame_equirectangular(
    const DocumentFrameCompositeResult& composite)
{
    const auto face_pixel_count = expected_pixel_count(composite.extent);
    if (!face_pixel_count) {
        return pp::foundation::Result<EquirectangularProjectionResult>::failure(face_pixel_count.status());
    }

    for (const auto& face : composite.faces) {
        if (face.extent.width != composite.extent.width || face.extent.height != composite.extent.height
            || face.pixels.size() != face_pixel_count.value()) {
            return pp::foundation::Result<EquirectangularProjectionResult>::failure(
                pp::foundation::Status::invalid_argument("document equirectangular export requires complete cube faces"));
        }
    }

    const auto output_width = static_cast<std::uint64_t>(composite.extent.width) * 4U;
    const auto output_height = static_cast<std::uint64_t>(composite.extent.height) * 2U;
    if (output_width > std::numeric_limits<std::uint32_t>::max()
        || output_height > std::numeric_limits<std::uint32_t>::max()) {
        return pp::foundation::Result<EquirectangularProjectionResult>::failure(
            pp::foundation::Status::out_of_range("document equirectangular extent exceeds uint32"));
    }

    EquirectangularProjectionResult result;
    result.face_extent = composite.extent;
    result.equirectangular_extent = pp::renderer::Extent2D {
        .width = static_cast<std::uint32_t>(output_width),
        .height = static_cast<std::uint32_t>(output_height),
    };
    result.face_payload_count = composite.face_payload_count;
    result.composited_layer_face_count = composite.composited_layer_face_count;

    const auto output_pixel_count = expected_pixel_count(result.equirectangular_extent);
    if (!output_pixel_count) {
        return pp::foundation::Result<EquirectangularProjectionResult>::failure(output_pixel_count.status());
    }

    constexpr auto pi = 3.14159265358979323846F;
    constexpr auto two_pi = 6.28318530717958647692F;
    result.pixels.assign(output_pixel_count.value(), pp::paint::Rgba {});
    for (std::uint32_t y = 0; y < result.equirectangular_extent.height; ++y) {
        const auto v = (static_cast<float>(y) + 0.5F) / static_cast<float>(result.equirectangular_extent.height);
        const auto angle_y = (1.0F - v) * pi;
        const auto sin_y = std::sin(angle_y);
        const auto cos_y = std::cos(angle_y);
        for (std::uint32_t x = 0; x < result.equirectangular_extent.width; ++x) {
            const auto u = (static_cast<float>(x) + 0.5F) / static_cast<float>(result.equirectangular_extent.width);
            const auto angle_x = (1.25F - u) * two_pi;
            const auto sample = panopainter_cube_face_sample(
                sin_y * std::cos(angle_x),
                cos_y,
                sin_y * std::sin(angle_x));
            result.pixels[static_cast<std::size_t>(y) * result.equirectangular_extent.width + x] =
                sample_face_nearest(composite.faces[sample.face_index], sample.s, sample.t);
        }
    }

    return pp::foundation::Result<EquirectangularProjectionResult>::success(std::move(result));
}

pp::foundation::Result<std::vector<pp::paint::Rgba>> project_document_frame_perspective(
    const DocumentFrameCompositeResult& composite,
    const PerspectiveProjectionMap& projection)
{
    const auto face_pixel_count = expected_pixel_count(composite.extent);
    if (!face_pixel_count) {
        return pp::foundation::Result<std::vector<pp::paint::Rgba>>::failure(face_pixel_count.status());
    }

    for (const auto& face : composite.faces) {
        if (face.extent.width != composite.extent.width || face.extent.height != composite.extent.height
            || face.pixels.size() != face_pixel_count.value()) {
            return pp::foundation::Result<std::vector<pp::paint::Rgba>>::failure(
                pp::foundation::Status::invalid_argument("document depth export requires complete cube faces"));
        }
    }

    std::vector<pp::paint::Rgba> pixels;
    pixels.reserve(projection.samples.size());
    for (const auto& sample : projection.samples) {
        pixels.push_back(sample_face_nearest(composite.faces[sample.face_index], sample.s, sample.t));
    }

    return pp::foundation::Result<std::vector<pp::paint::Rgba>>::success(std::move(pixels));
}

float sample_face_alpha_nearest(
    const DocumentFaceCompositeResult& face,
    float s,
    float t) noexcept
{
    return sample_face_nearest(face, s, t).a;
}

pp::foundation::Result<std::vector<pp::paint::Rgba>> project_document_depth_perspective(
    const pp::document::CanvasDocument& document,
    std::size_t frame_index,
    const PerspectiveProjectionMap& projection)
{
    std::vector<pp::paint::Rgba> pixels(
        projection.samples.size(),
        pp::paint::Rgba {
            .r = 0.0F,
            .g = 0.0F,
            .b = 0.0F,
            .a = 1.0F,
        });
    const auto layer_count = document.layers().size();
    for (std::size_t layer_index = 0; layer_index < layer_count; ++layer_index) {
        const auto& layer = document.layers()[layer_index];
        if (!layer.visible || layer.opacity == 0.0F || frame_index >= layer.frames.size()) {
            continue;
        }

        auto composite = composite_document_layer_frame(document, layer_index, frame_index, {});
        if (!composite) {
            return pp::foundation::Result<std::vector<pp::paint::Rgba>>::failure(composite.status());
        }

        const auto gray = static_cast<float>(layer_index + 1U) / static_cast<float>(layer_count + 1U);
        const pp::paint::Rgba layer_color {
            .r = gray,
            .g = gray,
            .b = gray,
            .a = 1.0F,
        };
        for (std::size_t pixel_index = 0; pixel_index < projection.samples.size(); ++pixel_index) {
            const auto& sample = projection.samples[pixel_index];
            const auto alpha = sample_face_alpha_nearest(
                composite.value().faces[sample.face_index],
                sample.s,
                sample.t);
            if (alpha > 0.01F) {
                pixels[pixel_index] = layer_color;
            }
        }
    }

    return pp::foundation::Result<std::vector<pp::paint::Rgba>>::success(std::move(pixels));
}

}

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();
}

pp::foundation::Result<DocumentFaceCompositeResult> composite_document_face(
    DocumentFaceCompositeRequest request)
{
    if (request.document == nullptr) {
        return pp::foundation::Result<DocumentFaceCompositeResult>::failure(
            pp::foundation::Status::invalid_argument("document composite request requires a document"));
    }

    if (request.face_index >= pp::document::cube_face_count) {
        return pp::foundation::Result<DocumentFaceCompositeResult>::failure(
            pp::foundation::Status::out_of_range("document composite face index is outside the cube"));
    }

    if (request.frame_index >= request.document->frames().size()) {
        return pp::foundation::Result<DocumentFaceCompositeResult>::failure(
            pp::foundation::Status::out_of_range("document composite frame index is outside the document"));
    }

    const pp::renderer::Extent2D extent {
        .width = request.document->width(),
        .height = request.document->height(),
    };
    const auto pixel_count = expected_pixel_count(extent);
    if (!pixel_count) {
        return pp::foundation::Result<DocumentFaceCompositeResult>::failure(pixel_count.status());
    }

    DocumentFaceCompositeResult result;
    result.extent = extent;
    result.pixels.assign(pixel_count.value(), request.clear_color);
    result.visited_layer_count = request.document->layers().size();

    for (const auto& layer : request.document->layers()) {
        if (!layer.visible || layer.opacity == 0.0F || request.frame_index >= layer.frames.size()) {
            continue;
        }

        bool composited_layer = false;
        const auto& frame = layer.frames[request.frame_index];
        for (const auto& payload : frame.face_pixels) {
            if (payload.face_index != request.face_index) {
                continue;
            }

            const auto status = composite_face_payload(
                result.pixels,
                extent,
                payload,
                layer);
            if (!status.ok()) {
                return pp::foundation::Result<DocumentFaceCompositeResult>::failure(status);
            }

            composited_layer = true;
            ++result.face_payload_count;
        }

        if (composited_layer) {
            ++result.composited_layer_count;
        }
    }

    return pp::foundation::Result<DocumentFaceCompositeResult>::success(std::move(result));
}

pp::foundation::Result<DocumentFrameCompositeResult> composite_document_frame(
    DocumentFrameCompositeRequest request)
{
    if (request.document == nullptr) {
        return pp::foundation::Result<DocumentFrameCompositeResult>::failure(
            pp::foundation::Status::invalid_argument("document frame composite request requires a document"));
    }

    if (request.frame_index >= request.document->frames().size()) {
        return pp::foundation::Result<DocumentFrameCompositeResult>::failure(
            pp::foundation::Status::out_of_range("document frame composite index is outside the document"));
    }

    DocumentFrameCompositeResult result;
    result.extent = pp::renderer::Extent2D {
        .width = request.document->width(),
        .height = request.document->height(),
    };
    result.visited_layer_count = request.document->layers().size();

    for (std::uint32_t face_index = 0; face_index < pp::document::cube_face_count; ++face_index) {
        auto face = composite_document_face(DocumentFaceCompositeRequest {
            .document = request.document,
            .frame_index = request.frame_index,
            .face_index = face_index,
            .clear_color = request.clear_color,
        });
        if (!face) {
            return pp::foundation::Result<DocumentFrameCompositeResult>::failure(face.status());
        }

        result.composited_layer_face_count += face.value().composited_layer_count;
        result.face_payload_count += face.value().face_payload_count;
        result.faces[face_index] = std::move(face.value());
    }

    return pp::foundation::Result<DocumentFrameCompositeResult>::success(std::move(result));
}

pp::foundation::Result<DocumentFrameCompositeResult> composite_document_layer_frame(
    const pp::document::CanvasDocument& document,
    std::size_t layer_index,
    std::size_t frame_index,
    pp::paint::Rgba clear_color)
{
    if (layer_index >= document.layers().size()) {
        return pp::foundation::Result<DocumentFrameCompositeResult>::failure(
            pp::foundation::Status::out_of_range("document layer export index is outside the document"));
    }

    if (frame_index >= document.frames().size()) {
        return pp::foundation::Result<DocumentFrameCompositeResult>::failure(
            pp::foundation::Status::out_of_range("document layer export frame index is outside the document"));
    }

    const pp::renderer::Extent2D extent {
        .width = document.width(),
        .height = document.height(),
    };
    const auto pixel_count = expected_pixel_count(extent);
    if (!pixel_count) {
        return pp::foundation::Result<DocumentFrameCompositeResult>::failure(pixel_count.status());
    }

    const auto& source_layer = document.layers()[layer_index];
    auto export_layer = source_layer;
    export_layer.visible = true;
    export_layer.opacity = 1.0F;
    export_layer.blend_mode = pp::paint::BlendMode::normal;

    DocumentFrameCompositeResult result;
    result.extent = extent;
    result.visited_layer_count = 1U;

    for (std::uint32_t face_index = 0; face_index < pp::document::cube_face_count; ++face_index) {
        DocumentFaceCompositeResult face;
        face.extent = extent;
        face.pixels.assign(pixel_count.value(), clear_color);
        face.visited_layer_count = 1U;

        if (frame_index < source_layer.frames.size()) {
            bool composited_face = false;
            const auto& frame = source_layer.frames[frame_index];
            for (const auto& payload : frame.face_pixels) {
                if (payload.face_index != face_index) {
                    continue;
                }

                const auto status = composite_face_payload(face.pixels, extent, payload, export_layer);
                if (!status.ok()) {
                    return pp::foundation::Result<DocumentFrameCompositeResult>::failure(status);
                }

                composited_face = true;
                ++face.face_payload_count;
                ++result.face_payload_count;
            }

            if (composited_face) {
                face.composited_layer_count = 1U;
                ++result.composited_layer_face_count;
            }
        }

        result.faces[face_index] = std::move(face);
    }

    return pp::foundation::Result<DocumentFrameCompositeResult>::success(std::move(result));
}

pp::foundation::Result<DocumentFrameUploadResult> upload_document_frame_faces(
    pp::renderer::IRenderDevice& device,
    DocumentFrameUploadRequest request)
{
    auto composite = composite_document_frame(DocumentFrameCompositeRequest {
        .document = request.document,
        .frame_index = request.frame_index,
        .clear_color = request.clear_color,
    });
    if (!composite) {
        return pp::foundation::Result<DocumentFrameUploadResult>::failure(composite.status());
    }

    DocumentFrameUploadResult result;
    result.composite = std::move(composite.value());
    std::vector<std::byte> upload_bytes;

    for (std::size_t face_index = 0; face_index < result.composite.faces.size(); ++face_index) {
        const pp::renderer::TextureDesc desc {
            .extent = result.composite.extent,
            .format = pp::renderer::TextureFormat::rgba8,
            .usage = pp::renderer::TextureUsage::sampled
                | pp::renderer::TextureUsage::upload_destination
                | pp::renderer::TextureUsage::readback_source
                | pp::renderer::TextureUsage::copy_source,
            .debug_name = "document-frame-face",
        };
        auto texture = device.create_texture(desc);
        if (!texture) {
            return pp::foundation::Result<DocumentFrameUploadResult>::failure(texture.status());
        }

        append_rgba8_bytes(upload_bytes, result.composite.faces[face_index].pixels);
        auto& context = device.immediate_context();
        const auto upload_status = context.upload_texture(
            *texture.value(),
            pp::renderer::ReadbackRegion {
                .x = 0,
                .y = 0,
                .width = result.composite.extent.width,
                .height = result.composite.extent.height,
            },
            upload_bytes);
        if (!upload_status.ok()) {
            return pp::foundation::Result<DocumentFrameUploadResult>::failure(upload_status);
        }

        result.uploaded_bytes += static_cast<std::uint64_t>(upload_bytes.size());
        if (request.transition_to_shader_read && device.features().explicit_texture_transitions) {
            const auto transition_status = context.transition_texture(
                *texture.value(),
                pp::renderer::TextureState::upload_destination,
                pp::renderer::TextureState::shader_read);
            if (!transition_status.ok()) {
                return pp::foundation::Result<DocumentFrameUploadResult>::failure(transition_status);
            }
            ++result.transition_count;
        }

        result.face_textures[face_index] = std::move(texture.value());
        ++result.texture_count;
    }

    return pp::foundation::Result<DocumentFrameUploadResult>::success(std::move(result));
}

pp::foundation::Result<RecordedDocumentFrameUploadResult> record_document_frame_upload(
    DocumentFrameUploadRequest request)
{
    pp::renderer::RecordingRenderDevice render_device;
    auto uploaded = upload_document_frame_faces(render_device, request);
    if (!uploaded) {
        return pp::foundation::Result<RecordedDocumentFrameUploadResult>::failure(uploaded.status());
    }

    RecordedDocumentFrameUploadResult result;
    result.upload = std::move(uploaded.value());
    const auto commands = render_device.commands();
    result.command_count = commands.size();
    for (const auto& command : commands) {
        if (command.kind == pp::renderer::RecordedRenderCommandKind::upload_texture) {
            ++result.upload_command_count;
        }
        if (command.kind == pp::renderer::RecordedRenderCommandKind::transition_texture) {
            ++result.transition_command_count;
        }
    }

    return pp::foundation::Result<RecordedDocumentFrameUploadResult>::success(std::move(result));
}

pp::foundation::Result<DocumentFrameFacePngExportResult> export_document_frame_face_pngs(
    DocumentFrameCompositeRequest request)
{
    auto composite = composite_document_frame(request);
    if (!composite) {
        return pp::foundation::Result<DocumentFrameFacePngExportResult>::failure(composite.status());
    }

    DocumentFrameFacePngExportResult result;
    result.composite = std::move(composite.value());
    std::vector<std::uint8_t> rgba8;
    for (std::size_t face_index = 0; face_index < result.composite.faces.size(); ++face_index) {
        append_rgba8_bytes(rgba8, result.composite.faces[face_index].pixels);
        auto encoded = pp::assets::encode_png_rgba8(
            result.composite.extent.width,
            result.composite.extent.height,
            rgba8);
        if (!encoded) {
            return pp::foundation::Result<DocumentFrameFacePngExportResult>::failure(encoded.status());
        }

        result.encoded_bytes += static_cast<std::uint64_t>(encoded.value().size());
        result.face_pngs[face_index] = std::move(encoded.value());
        ++result.face_count;
    }

    return pp::foundation::Result<DocumentFrameFacePngExportResult>::success(std::move(result));
}

pp::foundation::Result<DocumentFrameEquirectangularPngExportResult>
export_document_frame_equirectangular_png(const DocumentFrameCompositeResult& composite)
{
    auto projection = project_document_frame_equirectangular(composite);
    if (!projection) {
        return pp::foundation::Result<DocumentFrameEquirectangularPngExportResult>::failure(projection.status());
    }

    DocumentFrameEquirectangularPngExportResult result;
    result.face_extent = projection.value().face_extent;
    result.equirectangular_extent = projection.value().equirectangular_extent;
    result.face_payload_count = projection.value().face_payload_count;
    result.composited_layer_face_count = projection.value().composited_layer_face_count;

    std::vector<std::uint8_t> rgba8;
    append_rgba8_bytes(rgba8, projection.value().pixels);
    auto encoded = pp::assets::encode_png_rgba8(
        result.equirectangular_extent.width,
        result.equirectangular_extent.height,
        rgba8);
    if (!encoded) {
        return pp::foundation::Result<DocumentFrameEquirectangularPngExportResult>::failure(encoded.status());
    }

    result.encoded_bytes = static_cast<std::uint64_t>(encoded.value().size());
    result.png = std::move(encoded.value());
    return pp::foundation::Result<DocumentFrameEquirectangularPngExportResult>::success(std::move(result));
}

pp::foundation::Result<DocumentFrameEquirectangularPngExportResult>
export_document_frame_equirectangular_png(DocumentFrameCompositeRequest request)
{
    auto composite = composite_document_frame(request);
    if (!composite) {
        return pp::foundation::Result<DocumentFrameEquirectangularPngExportResult>::failure(composite.status());
    }

    return export_document_frame_equirectangular_png(composite.value());
}

pp::foundation::Result<DocumentFrameEquirectangularJpegExportResult>
export_document_frame_equirectangular_jpeg(const DocumentFrameCompositeResult& composite, int quality)
{
    auto projection = project_document_frame_equirectangular(composite);
    if (!projection) {
        return pp::foundation::Result<DocumentFrameEquirectangularJpegExportResult>::failure(projection.status());
    }

    DocumentFrameEquirectangularJpegExportResult result;
    result.face_extent = projection.value().face_extent;
    result.equirectangular_extent = projection.value().equirectangular_extent;
    result.face_payload_count = projection.value().face_payload_count;
    result.composited_layer_face_count = projection.value().composited_layer_face_count;

    std::vector<std::uint8_t> rgba8;
    append_rgba8_bytes(rgba8, projection.value().pixels);
    auto encoded = pp::assets::encode_jpeg_rgba8(
        result.equirectangular_extent.width,
        result.equirectangular_extent.height,
        rgba8,
        quality);
    if (!encoded) {
        return pp::foundation::Result<DocumentFrameEquirectangularJpegExportResult>::failure(encoded.status());
    }

    auto with_xmp = pp::assets::inject_gpano_xmp_into_jpeg(encoded.value());
    if (!with_xmp) {
        return pp::foundation::Result<DocumentFrameEquirectangularJpegExportResult>::failure(with_xmp.status());
    }

    result.encoded_bytes = static_cast<std::uint64_t>(with_xmp.value().size());
    result.jpeg = std::move(with_xmp.value());
    result.xmp_injected = true;
    return pp::foundation::Result<DocumentFrameEquirectangularJpegExportResult>::success(std::move(result));
}

pp::foundation::Result<DocumentFrameEquirectangularJpegExportResult>
export_document_frame_equirectangular_jpeg(DocumentFrameCompositeRequest request, int quality)
{
    auto composite = composite_document_frame(request);
    if (!composite) {
        return pp::foundation::Result<DocumentFrameEquirectangularJpegExportResult>::failure(composite.status());
    }

    return export_document_frame_equirectangular_jpeg(composite.value(), quality);
}

pp::foundation::Result<DocumentDepthExportRenderPlan> plan_document_depth_export_render(
    DocumentDepthExportRenderPlanRequest request) noexcept
{
    if (request.document == nullptr) {
        return pp::foundation::Result<DocumentDepthExportRenderPlan>::failure(
            pp::foundation::Status::invalid_argument("document depth export request requires a document"));
    }

    if (request.frame_index >= request.document->frames().size()) {
        return pp::foundation::Result<DocumentDepthExportRenderPlan>::failure(
            pp::foundation::Status::out_of_range("document depth export frame index is outside the document"));
    }

    const auto output_pixels = expected_pixel_count(request.output_extent);
    if (!output_pixels) {
        return pp::foundation::Result<DocumentDepthExportRenderPlan>::failure(output_pixels.status());
    }

    DocumentDepthExportRenderPlan plan;
    plan.output_extent = request.output_extent;
    plan.merged_face_draw_count = pp::document::cube_face_count;
    plan.visited_layer_count = request.document->layers().size();

    for (const auto& layer : request.document->layers()) {
        if (!layer.visible || layer.opacity == 0.0F || request.frame_index >= layer.frames.size()) {
            continue;
        }

        ++plan.visible_layer_count;
        std::array<bool, pp::document::cube_face_count> layer_faces {};
        const auto& frame = layer.frames[request.frame_index];
        for (const auto& payload : frame.face_pixels) {
            if (payload.face_index >= pp::document::cube_face_count) {
                return pp::foundation::Result<DocumentDepthExportRenderPlan>::failure(
                    pp::foundation::Status::out_of_range("document depth export face index is outside the cube"));
            }

            layer_faces[payload.face_index] = true;
            ++plan.face_payload_count;
        }

        for (const auto has_payload : layer_faces) {
            if (has_payload) {
                ++plan.layer_depth_draw_count;
            }
        }
    }

    return pp::foundation::Result<DocumentDepthExportRenderPlan>::success(plan);
}

pp::foundation::Result<DocumentDepthPngExportResult> export_document_depth_pngs(
    DocumentDepthExportRenderPlanRequest request)
{
    auto plan = plan_document_depth_export_render(request);
    if (!plan) {
        return pp::foundation::Result<DocumentDepthPngExportResult>::failure(plan.status());
    }

    auto projection = make_perspective_projection_map(
        plan.value().output_extent,
        document_depth_export_default_fov_degrees);
    if (!projection) {
        return pp::foundation::Result<DocumentDepthPngExportResult>::failure(projection.status());
    }

    auto image_composite = composite_document_frame(DocumentFrameCompositeRequest {
        .document = request.document,
        .frame_index = request.frame_index,
        .clear_color = pp::paint::Rgba {
            .r = 0.0F,
            .g = 0.0F,
            .b = 0.0F,
            .a = 1.0F,
        },
    });
    if (!image_composite) {
        return pp::foundation::Result<DocumentDepthPngExportResult>::failure(image_composite.status());
    }

    auto image_pixels = project_document_frame_perspective(image_composite.value(), projection.value());
    if (!image_pixels) {
        return pp::foundation::Result<DocumentDepthPngExportResult>::failure(image_pixels.status());
    }

    auto depth_pixels = project_document_depth_perspective(*request.document, request.frame_index, projection.value());
    if (!depth_pixels) {
        return pp::foundation::Result<DocumentDepthPngExportResult>::failure(depth_pixels.status());
    }

    std::vector<std::uint8_t> rgba8;
    append_rgba8_bytes(rgba8, image_pixels.value());
    auto image_png = pp::assets::encode_png_rgba8(
        plan.value().output_extent.width,
        plan.value().output_extent.height,
        rgba8);
    if (!image_png) {
        return pp::foundation::Result<DocumentDepthPngExportResult>::failure(image_png.status());
    }

    append_rgba8_bytes(rgba8, depth_pixels.value());
    auto depth_png = pp::assets::encode_png_rgba8(
        plan.value().output_extent.width,
        plan.value().output_extent.height,
        rgba8);
    if (!depth_png) {
        return pp::foundation::Result<DocumentDepthPngExportResult>::failure(depth_png.status());
    }

    DocumentDepthPngExportResult result;
    result.output_extent = plan.value().output_extent;
    result.image_encoded_bytes = static_cast<std::uint64_t>(image_png.value().size());
    result.depth_encoded_bytes = static_cast<std::uint64_t>(depth_png.value().size());
    result.merged_face_draw_count = plan.value().merged_face_draw_count;
    result.layer_depth_draw_count = plan.value().layer_depth_draw_count;
    result.visited_layer_count = plan.value().visited_layer_count;
    result.visible_layer_count = plan.value().visible_layer_count;
    result.face_payload_count = plan.value().face_payload_count;
    result.uses_perspective_camera = plan.value().uses_perspective_camera;
    result.image_png = std::move(image_png.value());
    result.depth_png = std::move(depth_png.value());
    return pp::foundation::Result<DocumentDepthPngExportResult>::success(std::move(result));
}

pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>
export_document_layers_equirectangular_pngs(DocumentLayerEquirectangularPngExportRequest request)
{
    if (request.document == nullptr) {
        return pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>::failure(
            pp::foundation::Status::invalid_argument("document layer export request requires a document"));
    }

    if (request.frame_index >= request.document->frames().size()) {
        return pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>::failure(
            pp::foundation::Status::out_of_range("document layer export frame index is outside the document"));
    }

    DocumentLayerEquirectangularPngExportResult result;
    result.layers.reserve(request.document->layers().size());
    for (std::size_t layer_index = 0; layer_index < request.document->layers().size(); ++layer_index) {
        auto composite = composite_document_layer_frame(
            *request.document,
            layer_index,
            request.frame_index,
            request.clear_color);
        if (!composite) {
            return pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>::failure(composite.status());
        }

        auto exported = export_document_frame_equirectangular_png(composite.value());
        if (!exported) {
            return pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>::failure(exported.status());
        }

        DocumentLayerEquirectangularPng layer;
        layer.layer_index = layer_index;
        layer.layer_name = request.document->layers()[layer_index].name;
        layer.face_extent = exported.value().face_extent;
        layer.equirectangular_extent = exported.value().equirectangular_extent;
        layer.encoded_bytes = exported.value().encoded_bytes;
        layer.face_payload_count = exported.value().face_payload_count;
        layer.composited_layer_face_count = exported.value().composited_layer_face_count;
        layer.png = std::move(exported.value().png);
        result.encoded_bytes += layer.encoded_bytes;
        result.layers.push_back(std::move(layer));
    }

    result.layer_count = result.layers.size();
    return pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>::success(std::move(result));
}

pp::foundation::Result<DocumentAnimationFrameEquirectangularPngExportResult>
export_document_animation_frames_equirectangular_pngs(
    DocumentAnimationFrameEquirectangularPngExportRequest request)
{
    if (request.document == nullptr) {
        return pp::foundation::Result<DocumentAnimationFrameEquirectangularPngExportResult>::failure(
            pp::foundation::Status::invalid_argument("document animation-frame export request requires a document"));
    }

    DocumentAnimationFrameEquirectangularPngExportResult result;
    result.frames.reserve(request.document->frames().size());
    for (std::size_t frame_index = 0; frame_index < request.document->frames().size(); ++frame_index) {
        auto exported = export_document_frame_equirectangular_png(DocumentFrameCompositeRequest {
            .document = request.document,
            .frame_index = frame_index,
            .clear_color = request.clear_color,
        });
        if (!exported) {
            return pp::foundation::Result<DocumentAnimationFrameEquirectangularPngExportResult>::failure(
                exported.status());
        }

        DocumentAnimationFrameEquirectangularPng frame;
        frame.frame_index = frame_index;
        frame.face_extent = exported.value().face_extent;
        frame.equirectangular_extent = exported.value().equirectangular_extent;
        frame.encoded_bytes = exported.value().encoded_bytes;
        frame.face_payload_count = exported.value().face_payload_count;
        frame.composited_layer_face_count = exported.value().composited_layer_face_count;
        frame.png = std::move(exported.value().png);
        result.encoded_bytes += frame.encoded_bytes;
        result.frames.push_back(std::move(frame));
    }

    result.frame_count = result.frames.size();
    return pp::foundation::Result<DocumentAnimationFrameEquirectangularPngExportResult>::success(std::move(result));
}

pp::foundation::Result<DocumentFrameExportReadinessResult> prepare_document_frame_export_readiness(
    DocumentFrameCompositeRequest request)
{
    auto recorded_upload = record_document_frame_upload(DocumentFrameUploadRequest {
        .document = request.document,
        .frame_index = request.frame_index,
        .clear_color = request.clear_color,
    });
    if (!recorded_upload) {
        return pp::foundation::Result<DocumentFrameExportReadinessResult>::failure(recorded_upload.status());
    }

    auto face_pngs = export_document_frame_face_pngs(request);
    if (!face_pngs) {
        return pp::foundation::Result<DocumentFrameExportReadinessResult>::failure(face_pngs.status());
    }

    DocumentFrameExportReadinessResult result;
    result.recorded_upload = std::move(recorded_upload.value());
    result.face_pngs = std::move(face_pngs.value());
    return pp::foundation::Result<DocumentFrameExportReadinessResult>::success(std::move(result));
}

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);
}

CanvasStrokeMaterialPlan plan_canvas_stroke_material(CanvasStrokeMaterialRequest request) noexcept
{
    CanvasStrokeMaterialPlan plan;

    auto bind = [&plan](CanvasStrokeTextureRole role, std::uint8_t slot) noexcept {
        if (plan.texture_binding_count >= plan.texture_bindings.size()) {
            return;
        }
        plan.texture_bindings[plan.texture_binding_count] = CanvasStrokeTextureBindingPlan {
            .role = role,
            .slot = slot,
        };
        ++plan.texture_binding_count;
    };

    bind(CanvasStrokeTextureRole::main_brush_tip, 0);

    plan.stroke_pass.uses_destination_feedback = request.destination_feedback_needed;
    if (request.destination_feedback_needed) {
        bind(CanvasStrokeTextureRole::destination_feedback, 1);
    }

    plan.stroke_pass.uses_pattern = request.pattern_enabled && request.pattern_eachsample;
    if (plan.stroke_pass.uses_pattern) {
        bind(CanvasStrokeTextureRole::pattern, 2);
    }

    plan.stroke_pass.uses_mixer = request.wet_blend || request.mix_blend || request.noise_enabled;
    if (plan.stroke_pass.uses_mixer) {
        bind(CanvasStrokeTextureRole::mixer, 3);
    }

    plan.dual_pass.enabled = request.dual_brush_enabled;
    plan.dual_pass.uses_pattern = false;
    if (request.dual_brush_enabled) {
        bind(CanvasStrokeTextureRole::dual_brush_tip, 4);
    }

    plan.composite_pass.use_dual = request.dual_brush_enabled;
    plan.composite_pass.use_pattern = request.pattern_enabled && !request.pattern_eachsample;
    plan.composite_pass.dual_blend_mode = request.dual_blend_mode;
    plan.composite_pass.pattern_blend_mode = request.pattern_blend_mode;
    plan.composite_pass.dual_alpha = request.dual_alpha;
    if (plan.composite_pass.use_pattern && !plan.stroke_pass.uses_pattern) {
        bind(CanvasStrokeTextureRole::pattern, 2);
    }

    return plan;
}

StrokePreviewCompositePlan plan_stroke_preview_composite(StrokePreviewCompositeRequest request) noexcept
{
    StrokePreviewCompositePlan plan;
    plan.uses_mixer = request.uses_mixer;
    plan.uses_dual = request.uses_dual;
    plan.uses_pattern = request.uses_pattern;

    auto append_step = [&plan](StrokePreviewCompositeStep step) noexcept {
        if (plan.step_count >= plan.steps.size()) {
            return;
        }
        plan.steps[plan.step_count] = step;
        ++plan.step_count;
    };
    auto bind = [&plan](StrokePreviewTextureRole role, std::uint8_t slot) noexcept {
        if (plan.texture_slot_count >= plan.texture_slots.size()) {
            return;
        }
        plan.texture_slots[plan.texture_slot_count] = StrokePreviewTextureSlotPlan {
            .role = role,
            .slot = slot,
        };
        ++plan.texture_slot_count;
    };

    append_step(StrokePreviewCompositeStep::checkerboard_background);
    append_step(StrokePreviewCompositeStep::capture_background_texture);
    append_step(StrokePreviewCompositeStep::bind_final_composite_inputs);
    append_step(StrokePreviewCompositeStep::final_composite_draw);
    append_step(StrokePreviewCompositeStep::copy_preview_texture);

    bind(StrokePreviewTextureRole::background, 0);
    bind(StrokePreviewTextureRole::stroke, 1);
    if (request.uses_dual) {
        bind(StrokePreviewTextureRole::dual, 3);
    }
    if (request.uses_pattern) {
        bind(StrokePreviewTextureRole::pattern, 4);
    }
    if (request.uses_mixer) {
        bind(StrokePreviewTextureRole::mixer, 3);
    }

    return plan;
}

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

    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;
        }

        if (gate.first_complex_layer_index < 0) {
            gate.first_complex_layer_index = static_cast<int>(i);
            first_complex_layer_blend = layer_blend;
        }
    }

    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;
    const bool layer_complex = gate.first_complex_layer_index >= 0;
    if (!layer_complex && !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,
            .layer_blend_mode = first_complex_layer_blend,
            .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);
}

pp::foundation::Result<CanvasStrokeRasterizationPlan> plan_canvas_stroke_rasterization(
    pp::renderer::RenderDeviceFeatures features,
    pp::renderer::Extent2D extent) noexcept
{
    const auto feedback = plan_canvas_stroke_feedback(features, extent);
    if (!feedback) {
        return pp::foundation::Result<CanvasStrokeRasterizationPlan>::failure(feedback.status());
    }

    CanvasStrokeRasterizationPlan plan;
    plan.feedback = feedback.value();
    plan.copy_stroke_destination = !plan.feedback.reads_destination_color;
    plan.can_route_feedback_through_renderer =
        plan.feedback.reads_destination_color
        || plan.feedback.requires_texture_copy
        || plan.feedback.requires_render_target_blit;
    plan.compatibility_fallback = plan.feedback.compatibility_fallback;
    return pp::foundation::Result<CanvasStrokeRasterizationPlan>::success(plan);
}

CanvasStrokeCommitSequencePlan plan_canvas_stroke_commit_sequence(CanvasStrokeCommitRequest request) noexcept
{
    CanvasStrokeCommitSequencePlan plan;
    plan.erase_mode = request.erase_mode;
    plan.alpha_locked = request.alpha_locked;
    plan.selection_mask_active = request.selection_mask_active;
    plan.uses_dual_stroke = !request.erase_mode && request.dual_stroke_enabled;
    plan.uses_pattern = !request.erase_mode && request.pattern_enabled;
    plan.updates_layer_bounds = !request.alpha_locked;

    auto append_step = [&plan](CanvasStrokeCommitStep step) noexcept {
        if (plan.step_count >= plan.steps.size()) {
            return;
        }
        plan.steps[plan.step_count] = step;
        ++plan.step_count;
    };
    auto bind = [&plan](CanvasStrokeCommitTextureRole role, std::uint8_t slot) noexcept {
        if (plan.texture_binding_count >= plan.texture_bindings.size()) {
            return;
        }
        plan.texture_bindings[plan.texture_binding_count] = CanvasStrokeCommitTextureBindingPlan {
            .role = role,
            .slot = slot,
        };
        ++plan.texture_binding_count;
    };

    append_step(CanvasStrokeCommitStep::readback_history_region);
    append_step(CanvasStrokeCommitStep::update_layer_dirty_state);
    append_step(CanvasStrokeCommitStep::copy_layer_rtt_to_scratch);
    append_step(CanvasStrokeCommitStep::bind_commit_inputs);
    append_step(request.erase_mode ? CanvasStrokeCommitStep::erase_draw : CanvasStrokeCommitStep::composite_draw);
    append_step(CanvasStrokeCommitStep::copy_committed_rtt_to_scratch);
    append_step(CanvasStrokeCommitStep::dilate_edges_draw);

    bind(CanvasStrokeCommitTextureRole::layer_scratch, 0);
    bind(CanvasStrokeCommitTextureRole::stroke, 1);
    bind(CanvasStrokeCommitTextureRole::selection_mask, 2);
    if (plan.uses_dual_stroke) {
        bind(CanvasStrokeCommitTextureRole::dual_stroke, 3);
    }
    if (plan.uses_pattern) {
        bind(CanvasStrokeCommitTextureRole::pattern, 4);
    }

    return plan;
}

CanvasStrokeSampleBoundsPlan plan_canvas_stroke_sample_bounds(
    CanvasStrokeSampleBoundsRequest request) noexcept
{
    CanvasStrokeSampleBoundsPlan plan;
    if (request.extent.width == 0U || request.extent.height == 0U || request.vertices.empty()) {
        return plan;
    }

    const auto target_width = static_cast<float>(request.extent.width);
    const auto target_height = static_cast<float>(request.extent.height);
    auto min_x = target_width;
    auto min_y = target_height;
    auto max_x = 0.0F;
    auto max_y = 0.0F;
    for (const auto& vertex : request.vertices) {
        min_x = std::max(0.0F, std::min(min_x, vertex.x));
        min_y = std::max(0.0F, std::min(min_y, vertex.y));
        max_x = std::min(target_width, std::max(max_x, vertex.x));
        max_y = std::min(target_height, std::max(max_y, vertex.y));
    }

    const auto pad = std::max(0.0F, request.sample_padding_pixels);
    const auto copy_x = static_cast<int>(std::clamp(std::floor(min_x) - pad, 0.0F, target_width));
    const auto copy_y = static_cast<int>(std::clamp(std::floor(min_y) - pad, 0.0F, target_height));
    const auto max_width = static_cast<int>(request.extent.width) - copy_x;
    const auto max_height = static_cast<int>(request.extent.height) - copy_y;
    const auto copy_width = static_cast<int>(std::clamp(
        std::ceil(max_x - min_x) + pad * 2.0F,
        0.0F,
        static_cast<float>(max_width)));
    const auto copy_height = static_cast<int>(std::clamp(
        std::ceil(max_y - min_y) + pad * 2.0F,
        0.0F,
        static_cast<float>(max_height)));

    plan.copy_region = CanvasStrokeCopyRegion {
        .x = copy_x,
        .y = copy_y,
        .width = copy_width,
        .height = copy_height,
    };
    plan.dirty_bounds = CanvasStrokeBox {
        .min_x = static_cast<float>(copy_x),
        .min_y = static_cast<float>(copy_y),
        .max_x = static_cast<float>(copy_x + copy_width),
        .max_y = static_cast<float>(copy_y + copy_height),
    };
    plan.has_pixels = copy_width > 0 && copy_height > 0;
    return plan;
}

CanvasStrokeFaceDirtyUpdatePlan plan_canvas_stroke_face_dirty_update(
    CanvasStrokeFaceDirtyUpdateRequest request) noexcept
{
    CanvasStrokeFaceDirtyUpdatePlan plan;
    plan.accumulated_dirty_box = request.previous_accumulated_dirty_box;
    plan.pass_dirty_box = box_union(request.previous_pass_dirty_box, request.sample_dirty_box);
    plan.has_dirty_pixels = has_positive_area(request.sample_dirty_box);
    plan.pass_dirty = plan.has_dirty_pixels;
    if (request.include_in_committed_dirty_box && plan.has_dirty_pixels) {
        plan.accumulated_dirty_box = clamp_box_to_legacy_dirty_extent(
            box_union(request.previous_accumulated_dirty_box, request.sample_dirty_box),
            request.extent);
        plan.committed_dirty = true;
    }
    return plan;
}

CanvasStrokePadRegionPlan plan_canvas_stroke_pad_region(
    CanvasStrokePadRegionRequest request) noexcept
{
    CanvasStrokePadRegionPlan plan;
    if (request.extent.width == 0U || request.extent.height == 0U) {
        return plan;
    }

    const auto pad = std::max(0.0F, request.pad_pixels);
    const auto width = static_cast<float>(request.extent.width);
    const auto height = static_cast<float>(request.extent.height);
    const auto origin_x = std::max(0.0F, request.pass_dirty_box.min_x - pad);
    const auto origin_y = std::max(0.0F, request.pass_dirty_box.min_y - pad);
    const auto max_x = std::min(width, request.pass_dirty_box.max_x + pad);
    const auto max_y = std::min(height, request.pass_dirty_box.max_y + pad);
    const auto copy_width = max_x - origin_x;
    const auto copy_height = max_y - origin_y;
    if (copy_width <= 0.0F || copy_height <= 0.0F) {
        return plan;
    }

    const auto left = origin_x * 2.0F / width - 1.0F;
    const auto bottom = origin_y * 2.0F / height - 1.0F;
    const auto right = max_x * 2.0F / width - 1.0F;
    const auto top = max_y * 2.0F / height - 1.0F;
    plan.copy_region = CanvasStrokeCopyRegion {
        .x = static_cast<int>(origin_x),
        .y = static_cast<int>(origin_y),
        .width = static_cast<int>(copy_width),
        .height = static_cast<int>(copy_height),
    };
    plan.ndc_quad = {
        CanvasStrokePoint { .x = left, .y = bottom },
        CanvasStrokePoint { .x = left, .y = top },
        CanvasStrokePoint { .x = right, .y = top },
        CanvasStrokePoint { .x = left, .y = bottom },
        CanvasStrokePoint { .x = right, .y = top },
        CanvasStrokePoint { .x = right, .y = bottom },
    };
    plan.has_pixels = true;
    return plan;
}

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";
}

}
