#pragma once

#include "renderer_api/renderer_api.h"

#include <span>
#include <vector>

namespace pp::renderer {

enum class RecordedRenderCommandKind : std::uint8_t {
    begin_render_pass,
    set_viewport,
    set_scissor,
    set_blend_state,
    set_depth_state,
    bind_shader,
    set_shader_uniform,
    bind_texture,
    bind_sampler,
    bind_mesh,
    draw,
    upload_texture,
    generate_mipmaps,
    transition_texture,
    copy_texture,
    read_texture,
    capture_frame,
    blit_render_target,
    end_render_pass,
    trace_marker,
    trace_begin_scope,
    trace_end_scope,
};

struct RecordedRenderCommand {
    RecordedRenderCommandKind kind = RecordedRenderCommandKind::draw;
    TextureDesc target_desc {};
    bool clear_color_enabled = false;
    ClearColor clear_color {};
    bool clear_depth_enabled = false;
    float clear_depth = 1.0F;
    bool clear_stencil_enabled = false;
    std::uint8_t clear_stencil = 0;
    Viewport viewport {};
    ScissorRect scissor {};
    BlendState blend_state {};
    DepthState depth_state {};
    MeshDesc mesh_desc {};
    DrawDesc draw_desc {};
    TextureDesc texture_desc {};
    std::uint32_t texture_slot = 0;
    SamplerDesc sampler_desc {};
    std::uint32_t sampler_slot = 0;
    TextureDesc source_desc {};
    TextureDesc destination_desc {};
    TextureState before_state = TextureState::undefined;
    TextureState after_state = TextureState::undefined;
    ReadbackRegion readback_region {};
    ReadbackRegion source_region {};
    ReadbackRegion destination_region {};
    BlitFilter blit_filter = BlitFilter::nearest;
    std::uint64_t upload_bytes = 0;
    std::uint32_t generated_mip_levels = 0;
    std::uint64_t generated_mip_bytes = 0;
    std::uint64_t copy_source_bytes = 0;
    std::uint64_t copy_destination_bytes = 0;
    std::uint64_t readback_bytes = 0;
    std::uint64_t capture_bytes = 0;
    std::uint64_t blit_source_bytes = 0;
    std::uint64_t blit_destination_bytes = 0;
    std::uint64_t uniform_bytes = 0;
    const char* component = "";
    const char* name = "";
};

class RecordingTexture2D final : public ITexture2D {
public:
    explicit RecordingTexture2D(TextureDesc desc) noexcept;
    [[nodiscard]] TextureDesc desc() const noexcept override;

private:
    TextureDesc desc_ {};
};

class RecordingRenderTarget final : public IRenderTarget {
public:
    explicit RecordingRenderTarget(TextureDesc color_desc) noexcept;
    [[nodiscard]] TextureDesc color_desc() const noexcept override;

private:
    TextureDesc color_desc_ {};
};

class RecordingShaderProgram final : public IShaderProgram {
public:
    explicit RecordingShaderProgram(const char* debug_name) noexcept;
    [[nodiscard]] const char* debug_name() const noexcept override;

private:
    const char* debug_name_ = "";
};

class RecordingMesh final : public IMesh {
public:
    explicit RecordingMesh(MeshDesc desc) noexcept;
    [[nodiscard]] MeshDesc desc() const noexcept override;

private:
    MeshDesc desc_ {};
};

class RecordingReadbackBuffer final : public IReadbackBuffer {
public:
    explicit RecordingReadbackBuffer(std::uint64_t size_bytes) noexcept;
    [[nodiscard]] std::uint64_t size_bytes() const noexcept override;

private:
    std::uint64_t size_bytes_ = 0;
};

class RecordingCommandContext final : public ICommandContext {
public:
    explicit RecordingCommandContext(std::vector<RecordedRenderCommand>& commands) noexcept;

    [[nodiscard]] pp::foundation::Status begin_render_pass(
        IRenderTarget& target,
        RenderPassDesc desc) noexcept override;
    [[nodiscard]] pp::foundation::Status set_viewport(Viewport viewport) noexcept override;
    [[nodiscard]] pp::foundation::Status set_scissor(ScissorRect scissor) noexcept override;
    [[nodiscard]] pp::foundation::Status set_blend_state(BlendState state) noexcept override;
    [[nodiscard]] pp::foundation::Status set_depth_state(DepthState state) noexcept override;
    [[nodiscard]] pp::foundation::Status bind_shader(IShaderProgram& shader) noexcept override;
    [[nodiscard]] pp::foundation::Status set_shader_uniform(
        const char* name,
        std::span<const std::byte> bytes) noexcept override;
    [[nodiscard]] pp::foundation::Status bind_texture(
        std::uint32_t slot,
        ITexture2D& texture) noexcept override;
    [[nodiscard]] pp::foundation::Status bind_sampler(
        std::uint32_t slot,
        SamplerDesc sampler) noexcept override;
    [[nodiscard]] pp::foundation::Status bind_mesh(IMesh& mesh) noexcept override;
    [[nodiscard]] pp::foundation::Status draw(DrawDesc desc) noexcept override;
    [[nodiscard]] pp::foundation::Status read_texture(
        ITexture2D& texture,
        ReadbackRegion region,
        IReadbackBuffer& destination) noexcept override;
    [[nodiscard]] pp::foundation::Status upload_texture(
        ITexture2D& texture,
        ReadbackRegion region,
        std::span<const std::byte> rgba_or_channel_bytes) noexcept override;
    [[nodiscard]] pp::foundation::Status generate_mipmaps(
        ITexture2D& texture) noexcept override;
    [[nodiscard]] pp::foundation::Status transition_texture(
        ITexture2D& texture,
        TextureState before,
        TextureState after) noexcept override;
    [[nodiscard]] pp::foundation::Status copy_texture(
        ITexture2D& source,
        ReadbackRegion source_region,
        ITexture2D& destination,
        ReadbackRegion destination_region) noexcept override;
    [[nodiscard]] pp::foundation::Status capture_frame(
        IRenderTarget& target,
        IReadbackBuffer& destination) noexcept override;
    [[nodiscard]] pp::foundation::Status blit_render_target(
        IRenderTarget& source,
        ReadbackRegion source_region,
        IRenderTarget& destination,
        ReadbackRegion destination_region,
        BlitFilter filter) noexcept override;
    void end_render_pass() noexcept override;

    [[nodiscard]] bool in_render_pass() const noexcept;
    void reset() noexcept;

private:
    std::vector<RecordedRenderCommand>* commands_ = nullptr;
    TextureDesc active_target_ {};
    MeshDesc active_mesh_ {};
    bool in_render_pass_ = false;
    bool shader_bound_ = false;
    bool mesh_bound_ = false;
};

class RecordingRenderTrace final : public IRenderTrace {
public:
    explicit RecordingRenderTrace(std::vector<RecordedRenderCommand>& commands) noexcept;
    [[nodiscard]] pp::foundation::Status marker(const char* component, const char* name) noexcept override;
    [[nodiscard]] pp::foundation::Status begin_scope(const char* component, const char* name) noexcept override;
    [[nodiscard]] pp::foundation::Status end_scope() noexcept override;
    void reset() noexcept;

private:
    std::vector<RecordedRenderCommand>* commands_ = nullptr;
    std::uint32_t scope_depth_ = 0;
};

class RecordingRenderDevice final : public IRenderDevice {
public:
    RecordingRenderDevice() noexcept;

    [[nodiscard]] const char* backend_name() const noexcept override;
    [[nodiscard]] RenderDeviceFeatures features() const noexcept override;
    [[nodiscard]] pp::foundation::Result<std::unique_ptr<ITexture2D>> create_texture(
        TextureDesc desc) noexcept override;
    [[nodiscard]] pp::foundation::Result<std::unique_ptr<IRenderTarget>> create_render_target(
        TextureDesc color_desc) noexcept override;
    [[nodiscard]] pp::foundation::Result<std::unique_ptr<IShaderProgram>> create_shader_program(
        ShaderProgramDesc desc) noexcept override;
    [[nodiscard]] pp::foundation::Result<std::unique_ptr<IMesh>> create_mesh(
        MeshDesc desc) noexcept override;
    [[nodiscard]] pp::foundation::Result<std::unique_ptr<IReadbackBuffer>> create_readback_buffer(
        std::uint64_t size_bytes) noexcept override;
    [[nodiscard]] ICommandContext& immediate_context() noexcept override;
    [[nodiscard]] IRenderTrace* trace() noexcept override;

    [[nodiscard]] std::span<const RecordedRenderCommand> commands() const noexcept;
    void clear() noexcept;

private:
    std::vector<RecordedRenderCommand> commands_;
    RecordingCommandContext context_;
    RecordingRenderTrace trace_;
};

[[nodiscard]] const char* recorded_render_command_kind_name(RecordedRenderCommandKind kind) noexcept;

}
