#include "renderer_gl/command_plan.h"

namespace pp::renderer::gl {

namespace {

[[nodiscard]] bool texture_format_supported(OpenGlRendererTextureFormat format) noexcept
{
    return format.internal_format != 0U
        && format.pixel_format != 0U
        && format.component_type != 0U
        && format.bytes_per_pixel != 0U;
}

[[nodiscard]] bool texture_state_supported(pp::renderer::TextureState state) noexcept
{
    switch (state) {
    case pp::renderer::TextureState::undefined:
    case pp::renderer::TextureState::shader_read:
    case pp::renderer::TextureState::render_target:
    case pp::renderer::TextureState::upload_destination:
    case pp::renderer::TextureState::copy_source:
    case pp::renderer::TextureState::copy_destination:
    case pp::renderer::TextureState::readback_source:
    case pp::renderer::TextureState::present:
        return true;
    default:
        return false;
    }
}

[[nodiscard]] bool non_empty_name(const char* name) noexcept
{
    return name != nullptr && name[0] != '\0';
}

[[nodiscard]] bool requires_render_pass(pp::renderer::RecordedRenderCommandKind kind) noexcept
{
    switch (kind) {
    case pp::renderer::RecordedRenderCommandKind::set_viewport:
    case pp::renderer::RecordedRenderCommandKind::set_scissor:
    case pp::renderer::RecordedRenderCommandKind::set_blend_state:
    case pp::renderer::RecordedRenderCommandKind::set_depth_state:
    case pp::renderer::RecordedRenderCommandKind::bind_shader:
    case pp::renderer::RecordedRenderCommandKind::set_shader_uniform:
    case pp::renderer::RecordedRenderCommandKind::bind_texture:
    case pp::renderer::RecordedRenderCommandKind::bind_sampler:
    case pp::renderer::RecordedRenderCommandKind::bind_mesh:
    case pp::renderer::RecordedRenderCommandKind::draw:
        return true;
    default:
        return false;
    }
}

void record_unsupported_command(OpenGlCommandPlan& plan, std::size_t index) noexcept
{
    ++plan.unsupported_command_count;
    if (plan.first_unsupported_command == OpenGlCommandPlan::npos) {
        plan.first_unsupported_command = index;
    }
}

void record_render_pass_order_error(OpenGlCommandPlan& plan, std::size_t index) noexcept
{
    ++plan.render_pass_order_error_count;
    if (plan.first_render_pass_order_error == OpenGlCommandPlan::npos) {
        plan.first_render_pass_order_error = index;
    }
}

void record_dependency_error(OpenGlCommandPlan& plan, std::size_t index) noexcept
{
    ++plan.dependency_error_count;
    if (plan.first_dependency_error == OpenGlCommandPlan::npos) {
        plan.first_dependency_error = index;
    }
}

}

const char* planned_command_kind_name(OpenGlPlannedCommandKind kind) noexcept
{
    switch (kind) {
    case OpenGlPlannedCommandKind::unknown:
        return "unknown";
    case OpenGlPlannedCommandKind::begin_render_pass:
        return "begin_render_pass";
    case OpenGlPlannedCommandKind::set_viewport:
        return "set_viewport";
    case OpenGlPlannedCommandKind::set_scissor:
        return "set_scissor";
    case OpenGlPlannedCommandKind::set_blend_state:
        return "set_blend_state";
    case OpenGlPlannedCommandKind::set_depth_state:
        return "set_depth_state";
    case OpenGlPlannedCommandKind::bind_shader:
        return "bind_shader";
    case OpenGlPlannedCommandKind::set_shader_uniform:
        return "set_shader_uniform";
    case OpenGlPlannedCommandKind::bind_texture:
        return "bind_texture";
    case OpenGlPlannedCommandKind::bind_sampler:
        return "bind_sampler";
    case OpenGlPlannedCommandKind::bind_mesh:
        return "bind_mesh";
    case OpenGlPlannedCommandKind::draw:
        return "draw";
    case OpenGlPlannedCommandKind::upload_texture:
        return "upload_texture";
    case OpenGlPlannedCommandKind::generate_mipmaps:
        return "generate_mipmaps";
    case OpenGlPlannedCommandKind::transition_texture:
        return "transition_texture";
    case OpenGlPlannedCommandKind::copy_texture:
        return "copy_texture";
    case OpenGlPlannedCommandKind::read_texture:
        return "read_texture";
    case OpenGlPlannedCommandKind::capture_frame:
        return "capture_frame";
    case OpenGlPlannedCommandKind::blit_render_target:
        return "blit_render_target";
    case OpenGlPlannedCommandKind::end_render_pass:
        return "end_render_pass";
    case OpenGlPlannedCommandKind::trace:
        return "trace";
    case OpenGlPlannedCommandKind::passthrough:
        return "passthrough";
    }

    return "unknown";
}

OpenGlPlannedCommand plan_recorded_render_command(pp::renderer::RecordedRenderCommand command) noexcept
{
    OpenGlPlannedCommand planned {};
    planned.requires_render_pass = requires_render_pass(command.kind);
    planned.supported = true;

    switch (command.kind) {
    case pp::renderer::RecordedRenderCommandKind::begin_render_pass:
        planned.kind = OpenGlPlannedCommandKind::begin_render_pass;
        planned.clear_mask = clear_mask_for_render_pass(pp::renderer::RenderPassDesc {
            .clear_color_enabled = command.clear_color_enabled,
            .clear_color = command.clear_color,
            .clear_depth_enabled = command.clear_depth_enabled,
            .clear_depth = command.clear_depth,
            .clear_stencil_enabled = command.clear_stencil_enabled,
            .clear_stencil = command.clear_stencil,
        });
        planned.clear_values = clear_values_for_render_pass(pp::renderer::RenderPassDesc {
            .clear_color_enabled = command.clear_color_enabled,
            .clear_color = command.clear_color,
            .clear_depth_enabled = command.clear_depth_enabled,
            .clear_depth = command.clear_depth,
            .clear_stencil_enabled = command.clear_stencil_enabled,
            .clear_stencil = command.clear_stencil,
        });
        planned.texture_format = texture_format_for_renderer_format(command.target_desc.format);
        planned.supported = texture_format_supported(planned.texture_format);
        break;
    case pp::renderer::RecordedRenderCommandKind::set_viewport:
        planned.kind = OpenGlPlannedCommandKind::set_viewport;
        planned.viewport = viewport_for_renderer_viewport(command.viewport);
        break;
    case pp::renderer::RecordedRenderCommandKind::set_scissor:
        planned.kind = OpenGlPlannedCommandKind::set_scissor;
        planned.scissor = scissor_rect_for_renderer_scissor(command.scissor);
        break;
    case pp::renderer::RecordedRenderCommandKind::set_blend_state:
        planned.kind = OpenGlPlannedCommandKind::set_blend_state;
        planned.blend = blend_state_for_renderer_blend_state(command.blend_state);
        planned.supported = planned.blend.supported;
        break;
    case pp::renderer::RecordedRenderCommandKind::set_depth_state:
        planned.kind = OpenGlPlannedCommandKind::set_depth_state;
        planned.depth = depth_state_for_renderer_depth_state(command.depth_state);
        planned.supported = planned.depth.supported;
        break;
    case pp::renderer::RecordedRenderCommandKind::bind_shader:
        planned.kind = OpenGlPlannedCommandKind::bind_shader;
        planned.name = command.name;
        planned.supported = non_empty_name(planned.name);
        break;
    case pp::renderer::RecordedRenderCommandKind::set_shader_uniform:
        planned.kind = OpenGlPlannedCommandKind::set_shader_uniform;
        planned.name = command.name;
        planned.uniform_bytes = command.uniform_bytes;
        planned.supported = non_empty_name(planned.name) && planned.uniform_bytes > 0U;
        break;
    case pp::renderer::RecordedRenderCommandKind::bind_texture:
        planned.kind = OpenGlPlannedCommandKind::bind_texture;
        planned.texture_format = texture_format_for_renderer_format(command.texture_desc.format);
        planned.texture_slot = command.texture_slot;
        planned.supported = texture_format_supported(planned.texture_format)
            && planned.texture_slot < pp::renderer::max_texture_slots;
        break;
    case pp::renderer::RecordedRenderCommandKind::bind_sampler:
        planned.kind = OpenGlPlannedCommandKind::bind_sampler;
        planned.sampler = sampler_state_for_renderer_sampler_desc(command.sampler_desc);
        planned.sampler_slot = command.sampler_slot;
        planned.supported = planned.sampler.supported
            && planned.sampler_slot < pp::renderer::max_texture_slots;
        break;
    case pp::renderer::RecordedRenderCommandKind::bind_mesh:
        planned.kind = OpenGlPlannedCommandKind::bind_mesh;
        planned.primitive_mode = primitive_mode_for_renderer_topology(command.mesh_desc.topology);
        planned.supported = planned.primitive_mode != 0U;
        break;
    case pp::renderer::RecordedRenderCommandKind::draw:
        planned.kind = OpenGlPlannedCommandKind::draw;
        planned.primitive_mode = primitive_mode_for_renderer_topology(command.mesh_desc.topology);
        planned.draw_vertex_count = command.draw_desc.vertex_count;
        planned.draw_index_count = command.draw_desc.index_count;
        planned.supported = planned.primitive_mode != 0U;
        break;
    case pp::renderer::RecordedRenderCommandKind::upload_texture:
        planned.kind = OpenGlPlannedCommandKind::upload_texture;
        planned.texture_format = texture_format_for_renderer_format(command.texture_desc.format);
        planned.readback_region = command.readback_region;
        planned.upload_bytes = command.upload_bytes;
        planned.supported = texture_format_supported(planned.texture_format);
        break;
    case pp::renderer::RecordedRenderCommandKind::generate_mipmaps:
        planned.kind = OpenGlPlannedCommandKind::generate_mipmaps;
        planned.texture_format = texture_format_for_renderer_format(command.texture_desc.format);
        planned.generated_mip_levels = command.generated_mip_levels;
        planned.generated_mip_bytes = command.generated_mip_bytes;
        planned.supported = texture_format_supported(planned.texture_format);
        break;
    case pp::renderer::RecordedRenderCommandKind::transition_texture:
        planned.kind = OpenGlPlannedCommandKind::transition_texture;
        planned.texture_format = texture_format_for_renderer_format(command.texture_desc.format);
        planned.before_state = command.before_state;
        planned.after_state = command.after_state;
        planned.supported = texture_format_supported(planned.texture_format)
            && texture_state_supported(planned.before_state)
            && texture_state_supported(planned.after_state);
        break;
    case pp::renderer::RecordedRenderCommandKind::copy_texture:
        planned.kind = OpenGlPlannedCommandKind::copy_texture;
        planned.source_texture_format = texture_format_for_renderer_format(command.source_desc.format);
        planned.destination_texture_format = texture_format_for_renderer_format(command.destination_desc.format);
        planned.source_region = command.source_region;
        planned.destination_region = command.destination_region;
        planned.copy_source_bytes = command.copy_source_bytes;
        planned.copy_destination_bytes = command.copy_destination_bytes;
        planned.supported = texture_format_supported(planned.source_texture_format)
            && texture_format_supported(planned.destination_texture_format);
        break;
    case pp::renderer::RecordedRenderCommandKind::read_texture:
        planned.kind = OpenGlPlannedCommandKind::read_texture;
        planned.texture_format = texture_format_for_renderer_format(command.texture_desc.format);
        planned.readback_region = command.readback_region;
        planned.readback_bytes = command.readback_bytes;
        planned.supported = texture_format_supported(planned.texture_format);
        break;
    case pp::renderer::RecordedRenderCommandKind::capture_frame:
        planned.kind = OpenGlPlannedCommandKind::capture_frame;
        planned.texture_format = texture_format_for_renderer_format(command.target_desc.format);
        planned.capture_bytes = command.capture_bytes;
        planned.supported = texture_format_supported(planned.texture_format);
        break;
    case pp::renderer::RecordedRenderCommandKind::blit_render_target:
        planned.kind = OpenGlPlannedCommandKind::blit_render_target;
        planned.blit_filter = blit_filter_for_renderer_filter(command.blit_filter);
        planned.source_texture_format = texture_format_for_renderer_format(command.source_desc.format);
        planned.destination_texture_format = texture_format_for_renderer_format(command.destination_desc.format);
        planned.source_region = command.source_region;
        planned.destination_region = command.destination_region;
        planned.blit_source_bytes = command.blit_source_bytes;
        planned.blit_destination_bytes = command.blit_destination_bytes;
        planned.supported = planned.blit_filter.supported
            && texture_format_supported(planned.source_texture_format)
            && texture_format_supported(planned.destination_texture_format);
        break;
    case pp::renderer::RecordedRenderCommandKind::end_render_pass:
        planned.kind = OpenGlPlannedCommandKind::end_render_pass;
        break;
    case pp::renderer::RecordedRenderCommandKind::trace_marker:
    case pp::renderer::RecordedRenderCommandKind::trace_begin_scope:
    case pp::renderer::RecordedRenderCommandKind::trace_end_scope:
        planned.kind = OpenGlPlannedCommandKind::trace;
        break;
    default:
        planned.kind = OpenGlPlannedCommandKind::unknown;
        planned.supported = false;
        break;
    }

    return planned;
}

OpenGlCommandPlan plan_recorded_render_commands(
    std::span<const pp::renderer::RecordedRenderCommand> commands)
{
    OpenGlCommandPlan plan;
    plan.commands.reserve(commands.size());

    bool in_render_pass = false;
    bool shader_bound_in_pass = false;
    bool mesh_bound_in_pass = false;
    for (std::size_t index = 0; index < commands.size(); ++index) {
        OpenGlPlannedCommand planned = plan_recorded_render_command(commands[index]);

        if (!planned.supported) {
            record_unsupported_command(plan, index);
        }

        switch (planned.kind) {
        case OpenGlPlannedCommandKind::begin_render_pass:
            ++plan.render_pass_count;
            if (in_render_pass) {
                record_render_pass_order_error(plan, index);
            }
            in_render_pass = true;
            shader_bound_in_pass = false;
            mesh_bound_in_pass = false;
            break;
        case OpenGlPlannedCommandKind::end_render_pass:
            if (!in_render_pass) {
                record_render_pass_order_error(plan, index);
            }
            in_render_pass = false;
            shader_bound_in_pass = false;
            mesh_bound_in_pass = false;
            break;
        case OpenGlPlannedCommandKind::draw:
            ++plan.draw_command_count;
            if (!in_render_pass) {
                record_render_pass_order_error(plan, index);
            }
            if (!shader_bound_in_pass || !mesh_bound_in_pass) {
                record_dependency_error(plan, index);
            }
            break;
        case OpenGlPlannedCommandKind::bind_shader:
            ++plan.shader_bind_command_count;
            if (!in_render_pass) {
                record_render_pass_order_error(plan, index);
            }
            shader_bound_in_pass = true;
            break;
        case OpenGlPlannedCommandKind::set_shader_uniform:
            ++plan.uniform_command_count;
            if (!in_render_pass) {
                record_render_pass_order_error(plan, index);
            }
            if (!shader_bound_in_pass) {
                record_dependency_error(plan, index);
            }
            break;
        case OpenGlPlannedCommandKind::bind_texture:
            ++plan.texture_bind_command_count;
            if (!in_render_pass) {
                record_render_pass_order_error(plan, index);
            }
            break;
        case OpenGlPlannedCommandKind::bind_sampler:
            ++plan.sampler_bind_command_count;
            if (!in_render_pass) {
                record_render_pass_order_error(plan, index);
            }
            break;
        case OpenGlPlannedCommandKind::bind_mesh:
            if (!in_render_pass) {
                record_render_pass_order_error(plan, index);
            }
            mesh_bound_in_pass = true;
            break;
        case OpenGlPlannedCommandKind::upload_texture:
            ++plan.upload_command_count;
            if (in_render_pass) {
                record_render_pass_order_error(plan, index);
            }
            break;
        case OpenGlPlannedCommandKind::generate_mipmaps:
            ++plan.mipmap_command_count;
            if (in_render_pass) {
                record_render_pass_order_error(plan, index);
            }
            break;
        case OpenGlPlannedCommandKind::transition_texture:
            ++plan.transition_command_count;
            if (in_render_pass) {
                record_render_pass_order_error(plan, index);
            }
            break;
        case OpenGlPlannedCommandKind::copy_texture:
            ++plan.copy_command_count;
            if (in_render_pass) {
                record_render_pass_order_error(plan, index);
            }
            break;
        case OpenGlPlannedCommandKind::read_texture:
            ++plan.readback_command_count;
            if (in_render_pass) {
                record_render_pass_order_error(plan, index);
            }
            break;
        case OpenGlPlannedCommandKind::capture_frame:
            ++plan.capture_command_count;
            if (in_render_pass) {
                record_render_pass_order_error(plan, index);
            }
            break;
        case OpenGlPlannedCommandKind::passthrough:
            ++plan.passthrough_command_count;
            if (planned.requires_render_pass && !in_render_pass) {
                record_render_pass_order_error(plan, index);
            }
            break;
        case OpenGlPlannedCommandKind::trace:
            ++plan.trace_command_count;
            break;
        case OpenGlPlannedCommandKind::blit_render_target:
            if (in_render_pass) {
                record_render_pass_order_error(plan, index);
            }
            break;
        default:
            if (planned.requires_render_pass && !in_render_pass) {
                record_render_pass_order_error(plan, index);
            }
            break;
        }

        plan.commands.push_back(planned);
    }

    plan.ended_in_render_pass = in_render_pass;
    if (in_render_pass) {
        record_render_pass_order_error(plan, commands.size());
    }

    plan.supported = plan.unsupported_command_count == 0U
        && plan.render_pass_order_error_count == 0U
        && plan.dependency_error_count == 0U;
    return plan;
}

}
