#pragma once

#include "document/document.h"
#include "foundation/result.h"
#include "paint/blend.h"
#include "renderer_api/renderer_api.h"

#include <array>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <span>
#include <string>
#include <vector>

namespace pp::paint_renderer {

enum class StrokeCompositePath : std::uint8_t {
    fixed_function_blend,
    framebuffer_fetch,
    ping_pong_textures,
};

struct LayerCompositeView {
    std::span<const pp::paint::Rgba> pixels;
    float opacity = 1.0F;
    bool visible = true;
    pp::paint::BlendMode blend_mode = pp::paint::BlendMode::normal;
};

struct StrokeCompositeRequest {
    pp::renderer::Extent2D extent {};
    pp::renderer::TextureFormat target_format = pp::renderer::TextureFormat::rgba8;
    pp::renderer::TextureUsage target_usage = pp::renderer::TextureUsage::render_target
        | pp::renderer::TextureUsage::sampled
        | pp::renderer::TextureUsage::copy_source
        | pp::renderer::TextureUsage::copy_destination;
    pp::paint::BlendMode layer_blend_mode = pp::paint::BlendMode::normal;
    pp::paint::StrokeBlendMode stroke_blend_mode = pp::paint::StrokeBlendMode::normal;
    bool dual_brush_blend = false;
    bool pattern_blend = false;
};

struct StrokeCompositePlan {
    StrokeCompositePath path = StrokeCompositePath::fixed_function_blend;
    pp::renderer::PaintFeedbackPlan feedback {};
    pp::renderer::TextureDesc target_desc {};
    std::uint64_t target_bytes = 0;
    std::uint64_t auxiliary_bytes = 0;
    std::uint64_t estimated_working_bytes = 0;
    bool complex_blend = false;
    bool reads_destination_color = false;
    bool requires_auxiliary_texture = false;
    bool requires_texture_copy = false;
    bool requires_render_target_blit = false;
    bool requires_explicit_transition = false;
};

enum class CanvasStrokeTextureRole : std::uint8_t {
    main_brush_tip,
    destination_feedback,
    pattern,
    mixer,
    dual_brush_tip,
};

struct CanvasStrokeTextureBindingPlan {
    CanvasStrokeTextureRole role = CanvasStrokeTextureRole::main_brush_tip;
    std::uint8_t slot = 0;
};

struct CanvasStrokeMaterialRequest {
    bool destination_feedback_needed = false;
    bool pattern_enabled = false;
    bool pattern_eachsample = false;
    bool wet_blend = false;
    bool mix_blend = false;
    bool noise_enabled = false;
    bool dual_brush_enabled = false;
    int dual_blend_mode = 0;
    int pattern_blend_mode = 0;
    float dual_alpha = 1.0F;
};

struct CanvasStrokeShaderPassPlan {
    bool uses_destination_feedback = false;
    bool uses_pattern = false;
    bool uses_mixer = false;
};

struct CanvasStrokeDualPassPlan {
    bool enabled = false;
    bool uses_pattern = false;
};

struct CanvasStrokeCompositePassPlan {
    bool use_dual = false;
    bool use_pattern = false;
    int dual_blend_mode = 0;
    int pattern_blend_mode = 0;
    float dual_alpha = 1.0F;
};

struct CanvasStrokeMaterialPlan {
    CanvasStrokeShaderPassPlan stroke_pass {};
    CanvasStrokeDualPassPlan dual_pass {};
    CanvasStrokeCompositePassPlan composite_pass {};
    std::array<CanvasStrokeTextureBindingPlan, 5> texture_bindings {};
    std::size_t texture_binding_count = 0;
};

enum class StrokePreviewCompositeStep : std::uint8_t {
    checkerboard_background,
    capture_background_texture,
    bind_final_composite_inputs,
    final_composite_draw,
    copy_preview_texture,
};

enum class StrokePreviewTextureRole : std::uint8_t {
    background,
    stroke,
    mask,
    dual,
    pattern,
    mixer,
};

struct StrokePreviewTextureSlotPlan {
    StrokePreviewTextureRole role = StrokePreviewTextureRole::background;
    std::uint8_t slot = 0;
};

struct StrokePreviewCompositeRequest {
    bool uses_mixer = false;
    bool uses_dual = false;
    bool uses_pattern = false;
};

struct StrokePreviewCompositePlan {
    std::array<StrokePreviewCompositeStep, 5> steps {};
    std::size_t step_count = 0;
    std::array<StrokePreviewTextureSlotPlan, 5> texture_slots {};
    std::size_t texture_slot_count = 0;
    bool uses_mixer = false;
    bool uses_dual = false;
    bool uses_pattern = false;
};

struct StrokePreviewCopySize {
    int width = 0;
    int height = 0;
};

struct CanvasBlendGateRequest {
    pp::renderer::Extent2D extent {};
    std::span<const int> layer_blend_modes;
    bool has_stroke_blend_mode = false;
    int stroke_blend_mode = 0;
    bool dual_brush_blend = false;
    bool pattern_blend = false;
};

struct CanvasBlendGatePlan {
    bool shader_blend = false;
    bool complex_blend = false;
    bool compatibility_fallback = false;
    bool stroke_complex = false;
    bool dual_brush_complex = false;
    bool pattern_complex = false;
    int first_complex_layer_index = -1;
    StrokeCompositePath path = StrokeCompositePath::fixed_function_blend;
    bool reads_destination_color = false;
    bool requires_auxiliary_texture = false;
    bool requires_texture_copy = false;
    bool requires_render_target_blit = false;
};

struct CanvasStrokeFeedbackPlan {
    StrokeCompositePath path = StrokeCompositePath::fixed_function_blend;
    bool reads_destination_color = false;
    bool requires_auxiliary_texture = false;
    bool requires_texture_copy = false;
    bool requires_render_target_blit = false;
    bool compatibility_fallback = false;
};

struct CanvasStrokeRasterizationPlan {
    CanvasStrokeFeedbackPlan feedback {};
    bool copy_stroke_destination = true;
    bool can_route_feedback_through_renderer = false;
    bool compatibility_fallback = false;
};

enum class CanvasStrokeCommitStep : std::uint8_t {
    readback_history_region,
    update_layer_dirty_state,
    copy_layer_rtt_to_scratch,
    bind_commit_inputs,
    erase_draw,
    composite_draw,
    copy_committed_rtt_to_scratch,
    dilate_edges_draw,
};

enum class CanvasStrokeCommitTextureRole : std::uint8_t {
    layer_scratch,
    stroke,
    selection_mask,
    dual_stroke,
    pattern,
};

struct CanvasStrokeCommitTextureBindingPlan {
    CanvasStrokeCommitTextureRole role = CanvasStrokeCommitTextureRole::layer_scratch;
    std::uint8_t slot = 0;
};

struct CanvasStrokeCommitRequest {
    bool erase_mode = false;
    bool alpha_locked = false;
    bool selection_mask_active = false;
    bool dual_stroke_enabled = false;
    bool pattern_enabled = false;
};

struct CanvasStrokeCommitSequencePlan {
    std::array<CanvasStrokeCommitStep, 8> steps {};
    std::size_t step_count = 0;
    std::array<CanvasStrokeCommitTextureBindingPlan, 5> texture_bindings {};
    std::size_t texture_binding_count = 0;
    bool erase_mode = false;
    bool alpha_locked = false;
    bool selection_mask_active = false;
    bool uses_dual_stroke = false;
    bool uses_pattern = false;
    bool requires_history_readback = true;
    bool updates_layer_bounds = true;
    bool requires_layer_scratch_copy = true;
    bool requires_committed_scratch_copy = true;
    bool requires_dilate = true;
};

struct CanvasStrokePoint {
    float x = 0.0F;
    float y = 0.0F;
};

struct CanvasStrokeBox {
    float min_x = 0.0F;
    float min_y = 0.0F;
    float max_x = 0.0F;
    float max_y = 0.0F;
};

struct CanvasStrokeCopyRegion {
    int x = 0;
    int y = 0;
    int width = 0;
    int height = 0;
};

struct CanvasStrokeSampleBoundsRequest {
    pp::renderer::Extent2D extent {};
    std::span<const CanvasStrokePoint> vertices;
    float sample_padding_pixels = 1.0F;
};

struct CanvasStrokeSampleBoundsPlan {
    CanvasStrokeCopyRegion copy_region {};
    CanvasStrokeBox dirty_bounds {};
    bool has_pixels = false;
};

struct CanvasStrokeFaceDirtyUpdateRequest {
    pp::renderer::Extent2D extent {};
    CanvasStrokeBox previous_accumulated_dirty_box {};
    CanvasStrokeBox previous_pass_dirty_box {};
    CanvasStrokeBox sample_dirty_box {};
    bool include_in_committed_dirty_box = true;
};

struct CanvasStrokeFaceDirtyUpdatePlan {
    CanvasStrokeBox accumulated_dirty_box {};
    CanvasStrokeBox pass_dirty_box {};
    bool has_dirty_pixels = false;
    bool committed_dirty = false;
    bool pass_dirty = false;
};

struct CanvasStrokePadRegionRequest {
    pp::renderer::Extent2D extent {};
    CanvasStrokeBox pass_dirty_box {};
    float pad_pixels = 20.0F;
};

struct CanvasStrokePadRegionPlan {
    CanvasStrokeCopyRegion copy_region {};
    std::array<CanvasStrokePoint, 6> ndc_quad {};
    bool has_pixels = false;
};

struct DocumentFaceCompositeRequest {
    const pp::document::CanvasDocument* document = nullptr;
    std::size_t frame_index = 0;
    std::uint32_t face_index = 0;
    pp::paint::Rgba clear_color {};
};

struct DocumentFaceCompositeResult {
    pp::renderer::Extent2D extent {};
    std::vector<pp::paint::Rgba> pixels;
    std::size_t visited_layer_count = 0;
    std::size_t composited_layer_count = 0;
    std::size_t face_payload_count = 0;
};

struct DocumentFrameCompositeRequest {
    const pp::document::CanvasDocument* document = nullptr;
    std::size_t frame_index = 0;
    pp::paint::Rgba clear_color {};
};

struct DocumentFrameCompositeResult {
    pp::renderer::Extent2D extent {};
    std::array<DocumentFaceCompositeResult, pp::document::cube_face_count> faces {};
    std::size_t visited_layer_count = 0;
    std::size_t composited_layer_face_count = 0;
    std::size_t face_payload_count = 0;
};

struct DocumentFrameUploadRequest {
    const pp::document::CanvasDocument* document = nullptr;
    std::size_t frame_index = 0;
    pp::paint::Rgba clear_color {};
    bool transition_to_shader_read = true;
};

struct DocumentFrameUploadResult {
    DocumentFrameCompositeResult composite {};
    std::array<std::unique_ptr<pp::renderer::ITexture2D>, pp::document::cube_face_count> face_textures {};
    std::size_t texture_count = 0;
    std::size_t transition_count = 0;
    std::uint64_t uploaded_bytes = 0;
};

struct RecordedDocumentFrameUploadResult {
    DocumentFrameUploadResult upload {};
    std::size_t command_count = 0;
    std::size_t upload_command_count = 0;
    std::size_t transition_command_count = 0;
};

struct DocumentFrameFacePngExportResult {
    DocumentFrameCompositeResult composite {};
    std::array<std::vector<std::byte>, pp::document::cube_face_count> face_pngs {};
    std::size_t face_count = 0;
    std::uint64_t encoded_bytes = 0;
};

struct DocumentFrameEquirectangularPngExportResult {
    pp::renderer::Extent2D face_extent {};
    pp::renderer::Extent2D equirectangular_extent {};
    std::vector<std::byte> png;
    std::uint64_t encoded_bytes = 0;
    std::size_t face_payload_count = 0;
    std::size_t composited_layer_face_count = 0;
};

struct DocumentFrameEquirectangularJpegExportResult {
    pp::renderer::Extent2D face_extent {};
    pp::renderer::Extent2D equirectangular_extent {};
    std::vector<std::byte> jpeg;
    std::uint64_t encoded_bytes = 0;
    std::size_t face_payload_count = 0;
    std::size_t composited_layer_face_count = 0;
    bool xmp_injected = false;
};

struct DocumentDepthExportRenderPlanRequest {
    const pp::document::CanvasDocument* document = nullptr;
    std::size_t frame_index = 0;
    pp::renderer::Extent2D output_extent {
        .width = 1024,
        .height = 1024,
    };
};

struct DocumentDepthExportRenderPlan {
    pp::renderer::Extent2D output_extent {
        .width = 1024,
        .height = 1024,
    };
    std::size_t merged_face_draw_count = 0;
    std::size_t layer_depth_draw_count = 0;
    std::size_t visited_layer_count = 0;
    std::size_t visible_layer_count = 0;
    std::size_t face_payload_count = 0;
    bool uses_perspective_camera = true;
    bool requires_renderer_readback = true;
};

struct DocumentDepthPngExportResult {
    pp::renderer::Extent2D output_extent {
        .width = 1024,
        .height = 1024,
    };
    std::vector<std::byte> image_png;
    std::vector<std::byte> depth_png;
    std::uint64_t image_encoded_bytes = 0;
    std::uint64_t depth_encoded_bytes = 0;
    std::size_t merged_face_draw_count = 0;
    std::size_t layer_depth_draw_count = 0;
    std::size_t visited_layer_count = 0;
    std::size_t visible_layer_count = 0;
    std::size_t face_payload_count = 0;
    bool uses_perspective_camera = true;
};

struct DocumentLayerEquirectangularPngExportRequest {
    const pp::document::CanvasDocument* document = nullptr;
    std::size_t frame_index = 0;
    pp::paint::Rgba clear_color {};
};

struct DocumentLayerEquirectangularPng {
    std::size_t layer_index = 0;
    std::string layer_name;
    pp::renderer::Extent2D face_extent {};
    pp::renderer::Extent2D equirectangular_extent {};
    std::vector<std::byte> png;
    std::uint64_t encoded_bytes = 0;
    std::size_t face_payload_count = 0;
    std::size_t composited_layer_face_count = 0;
};

struct DocumentLayerEquirectangularPngExportResult {
    std::vector<DocumentLayerEquirectangularPng> layers;
    std::uint64_t encoded_bytes = 0;
    std::size_t layer_count = 0;
};

struct DocumentAnimationFrameEquirectangularPngExportRequest {
    const pp::document::CanvasDocument* document = nullptr;
    pp::paint::Rgba clear_color {};
};

struct DocumentAnimationFrameEquirectangularPng {
    std::size_t frame_index = 0;
    pp::renderer::Extent2D face_extent {};
    pp::renderer::Extent2D equirectangular_extent {};
    std::vector<std::byte> png;
    std::uint64_t encoded_bytes = 0;
    std::size_t face_payload_count = 0;
    std::size_t composited_layer_face_count = 0;
};

struct DocumentAnimationFrameEquirectangularPngExportResult {
    std::vector<DocumentAnimationFrameEquirectangularPng> frames;
    std::uint64_t encoded_bytes = 0;
    std::size_t frame_count = 0;
};

struct DocumentFrameExportReadinessResult {
    RecordedDocumentFrameUploadResult recorded_upload {};
    DocumentFrameFacePngExportResult face_pngs {};
};

[[nodiscard]] pp::foundation::Status composite_layer(
    std::span<pp::paint::Rgba> destination,
    pp::renderer::Extent2D extent,
    LayerCompositeView layer) noexcept;

[[nodiscard]] pp::foundation::Result<DocumentFaceCompositeResult> composite_document_face(
    DocumentFaceCompositeRequest request);

[[nodiscard]] pp::foundation::Result<DocumentFrameCompositeResult> composite_document_frame(
    DocumentFrameCompositeRequest request);

[[nodiscard]] pp::foundation::Result<DocumentFrameUploadResult> upload_document_frame_faces(
    pp::renderer::IRenderDevice& device,
    DocumentFrameUploadRequest request);

[[nodiscard]] pp::foundation::Result<RecordedDocumentFrameUploadResult> record_document_frame_upload(
    DocumentFrameUploadRequest request);

[[nodiscard]] pp::foundation::Result<DocumentFrameFacePngExportResult> export_document_frame_face_pngs(
    DocumentFrameCompositeRequest request);

[[nodiscard]] pp::foundation::Result<DocumentFrameEquirectangularPngExportResult>
export_document_frame_equirectangular_png(const DocumentFrameCompositeResult& composite);

[[nodiscard]] pp::foundation::Result<DocumentFrameEquirectangularPngExportResult>
export_document_frame_equirectangular_png(DocumentFrameCompositeRequest request);

[[nodiscard]] pp::foundation::Result<DocumentFrameEquirectangularJpegExportResult>
export_document_frame_equirectangular_jpeg(
    const DocumentFrameCompositeResult& composite,
    int quality = 100);

[[nodiscard]] pp::foundation::Result<DocumentFrameEquirectangularJpegExportResult>
export_document_frame_equirectangular_jpeg(
    DocumentFrameCompositeRequest request,
    int quality = 100);

[[nodiscard]] pp::foundation::Result<DocumentDepthExportRenderPlan> plan_document_depth_export_render(
    DocumentDepthExportRenderPlanRequest request) noexcept;

[[nodiscard]] pp::foundation::Result<DocumentDepthPngExportResult> export_document_depth_pngs(
    DocumentDepthExportRenderPlanRequest request);

[[nodiscard]] pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>
export_document_layers_equirectangular_pngs(DocumentLayerEquirectangularPngExportRequest request);

[[nodiscard]] pp::foundation::Result<DocumentAnimationFrameEquirectangularPngExportResult>
export_document_animation_frames_equirectangular_pngs(
    DocumentAnimationFrameEquirectangularPngExportRequest request);

[[nodiscard]] pp::foundation::Result<DocumentFrameExportReadinessResult> prepare_document_frame_export_readiness(
    DocumentFrameCompositeRequest request);

[[nodiscard]] 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;

[[nodiscard]] pp::foundation::Result<StrokeCompositePlan> plan_stroke_composite(
    pp::renderer::RenderDeviceFeatures features,
    StrokeCompositeRequest request) noexcept;

[[nodiscard]] CanvasStrokeMaterialPlan plan_canvas_stroke_material(
    CanvasStrokeMaterialRequest request) noexcept;

[[nodiscard]] StrokePreviewCompositePlan plan_stroke_preview_composite(
    StrokePreviewCompositeRequest request) noexcept;

[[nodiscard]] pp::foundation::Status copy_stroke_preview_result_to_texture(
    std::function<void()> bind_preview_texture,
    std::function<void(
        int src_x,
        int src_y,
        int dst_x,
        int dst_y,
        int width,
        int height)> copy_framebuffer_to_texture,
    StrokePreviewCopySize copy_size) noexcept;

[[nodiscard]] pp::foundation::Result<CanvasBlendGatePlan> plan_canvas_blend_gate(
    pp::renderer::RenderDeviceFeatures features,
    CanvasBlendGateRequest request) noexcept;

[[nodiscard]] pp::foundation::Result<CanvasStrokeFeedbackPlan> plan_canvas_stroke_feedback(
    pp::renderer::RenderDeviceFeatures features,
    pp::renderer::Extent2D extent) noexcept;

[[nodiscard]] pp::foundation::Result<CanvasStrokeRasterizationPlan> plan_canvas_stroke_rasterization(
    pp::renderer::RenderDeviceFeatures features,
    pp::renderer::Extent2D extent) noexcept;

[[nodiscard]] CanvasStrokeCommitSequencePlan plan_canvas_stroke_commit_sequence(
    CanvasStrokeCommitRequest request) noexcept;

[[nodiscard]] CanvasStrokeSampleBoundsPlan plan_canvas_stroke_sample_bounds(
    CanvasStrokeSampleBoundsRequest request) noexcept;

[[nodiscard]] CanvasStrokeFaceDirtyUpdatePlan plan_canvas_stroke_face_dirty_update(
    CanvasStrokeFaceDirtyUpdateRequest request) noexcept;

[[nodiscard]] CanvasStrokePadRegionPlan plan_canvas_stroke_pad_region(
    CanvasStrokePadRegionRequest request) noexcept;

[[nodiscard]] const char* stroke_composite_path_name(StrokeCompositePath path) noexcept;

inline pp::foundation::Status copy_stroke_preview_result_to_texture(
    std::function<void()> bind_preview_texture,
    std::function<void(
        int src_x,
        int src_y,
        int dst_x,
        int dst_y,
        int width,
        int height)> copy_framebuffer_to_texture,
    StrokePreviewCopySize copy_size) noexcept
{
    bind_preview_texture();
    copy_framebuffer_to_texture(
        0,
        0,
        0,
        0,
        copy_size.width,
        copy_size.height);
    return pp::foundation::Status::success();
}

}
