#pragma once

#include "paint_renderer/compositor.h"

#include <algorithm>
#include <array>
#include <cstdint>
#include <functional>
#include <string_view>
#include <utility>

namespace pp::panopainter {

struct LegacyCanvasStrokeCommitFace {
    int index = 0;
    bool dirty = false;
};

struct LegacyCanvasStrokeCommitCallbacks {
    std::function<void()> mark_commit_started;
    std::function<void()> capture_render_state;
    std::function<void()> prepare_render_state;
    std::function<void()> restore_render_state;
    std::function<void()> publish_history;
    std::function<void()> capture_timelapse_frame;

    std::function<void(int)> bind_layer_framebuffer;
    std::function<void(int)> capture_history_region;
    std::function<void(int)> apply_layer_dirty_region;
    std::function<void(int)> copy_layer_to_commit_destination;
    std::function<void(int)> bind_commit_inputs;
    std::function<void(int)> execute_erase_composite;
    std::function<void(int)> execute_paint_composite;
    std::function<void(int)> copy_committed_to_dilate_source;
    std::function<void(int)> execute_commit_dilate;
    std::function<void(int)> unbind_layer_framebuffer;
};

struct LegacyCanvasStrokeCommitRequest {
    std::string_view context;
    std::array<LegacyCanvasStrokeCommitFace, 6> faces {};
    pp::paint_renderer::CanvasStrokeCommitSequencePlan sequence;
    LegacyCanvasStrokeCommitCallbacks callbacks;
};

struct LegacyCanvasStrokeCommitResult {
    bool ok = false;
    int committed_faces = 0;
};

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

template <
    typename MarkCommitStarted,
    typename CaptureRenderState,
    typename PrepareRenderState,
    typename RestoreRenderState,
    typename PublishHistory,
    typename CaptureTimelapseFrame,
    typename BindLayerFramebuffer,
    typename CaptureHistoryRegion,
    typename ApplyLayerDirtyRegion,
    typename CopyLayerToCommitDestination,
    typename BindCommitInputs,
    typename ExecuteEraseComposite,
    typename ExecutePaintComposite,
    typename CopyCommittedToDilateSource,
    typename ExecuteCommitDilate,
    typename UnbindLayerFramebuffer>
[[nodiscard]] inline LegacyCanvasStrokeCommitCallbacks make_legacy_canvas_stroke_commit_callbacks(
    MarkCommitStarted&& mark_commit_started,
    CaptureRenderState&& capture_render_state,
    PrepareRenderState&& prepare_render_state,
    RestoreRenderState&& restore_render_state,
    PublishHistory&& publish_history,
    CaptureTimelapseFrame&& capture_timelapse_frame,
    BindLayerFramebuffer&& bind_layer_framebuffer,
    CaptureHistoryRegion&& capture_history_region,
    ApplyLayerDirtyRegion&& apply_layer_dirty_region,
    CopyLayerToCommitDestination&& copy_layer_to_commit_destination,
    BindCommitInputs&& bind_commit_inputs,
    ExecuteEraseComposite&& execute_erase_composite,
    ExecutePaintComposite&& execute_paint_composite,
    CopyCommittedToDilateSource&& copy_committed_to_dilate_source,
    ExecuteCommitDilate&& execute_commit_dilate,
    UnbindLayerFramebuffer&& unbind_layer_framebuffer)
{
    return LegacyCanvasStrokeCommitCallbacks {
        .mark_commit_started = std::forward<MarkCommitStarted>(mark_commit_started),
        .capture_render_state = std::forward<CaptureRenderState>(capture_render_state),
        .prepare_render_state = std::forward<PrepareRenderState>(prepare_render_state),
        .restore_render_state = std::forward<RestoreRenderState>(restore_render_state),
        .publish_history = std::forward<PublishHistory>(publish_history),
        .capture_timelapse_frame = std::forward<CaptureTimelapseFrame>(capture_timelapse_frame),
        .bind_layer_framebuffer = std::forward<BindLayerFramebuffer>(bind_layer_framebuffer),
        .capture_history_region = std::forward<CaptureHistoryRegion>(capture_history_region),
        .apply_layer_dirty_region = std::forward<ApplyLayerDirtyRegion>(apply_layer_dirty_region),
        .copy_layer_to_commit_destination = std::forward<CopyLayerToCommitDestination>(copy_layer_to_commit_destination),
        .bind_commit_inputs = std::forward<BindCommitInputs>(bind_commit_inputs),
        .execute_erase_composite = std::forward<ExecuteEraseComposite>(execute_erase_composite),
        .execute_paint_composite = std::forward<ExecutePaintComposite>(execute_paint_composite),
        .copy_committed_to_dilate_source = std::forward<CopyCommittedToDilateSource>(copy_committed_to_dilate_source),
        .execute_commit_dilate = std::forward<ExecuteCommitDilate>(execute_commit_dilate),
        .unbind_layer_framebuffer = std::forward<UnbindLayerFramebuffer>(unbind_layer_framebuffer),
    };
}

[[nodiscard]] inline std::size_t legacy_canvas_stroke_commit_step_count(
    const pp::paint_renderer::CanvasStrokeCommitSequencePlan& sequence) noexcept
{
    return std::min(sequence.step_count, sequence.steps.size());
}

[[nodiscard]] inline std::size_t legacy_canvas_stroke_commit_texture_binding_count(
    const pp::paint_renderer::CanvasStrokeCommitSequencePlan& sequence) noexcept
{
    return std::min(sequence.texture_binding_count, sequence.texture_bindings.size());
}

[[nodiscard]] inline int legacy_canvas_stroke_commit_texture_slot(
    const pp::paint_renderer::CanvasStrokeCommitSequencePlan& sequence,
    pp::paint_renderer::CanvasStrokeCommitTextureRole role) noexcept
{
    const auto binding_count = legacy_canvas_stroke_commit_texture_binding_count(sequence);
    for (std::size_t binding_index = 0; binding_index < binding_count; ++binding_index) {
        const auto& binding = sequence.texture_bindings[binding_index];
        if (binding.role == role) {
            return static_cast<int>(binding.slot);
        }
    }

    return -1;
}

template <typename SetActiveTextureUnit, typename BindTextureRole, typename BindSamplerRole>
inline void bind_legacy_canvas_stroke_commit_inputs(
    const pp::paint_renderer::CanvasStrokeCommitSequencePlan& sequence,
    SetActiveTextureUnit&& set_active_texture_unit,
    BindTextureRole&& bind_texture_role,
    BindSamplerRole&& bind_sampler_role)
{
    const auto binding_count = legacy_canvas_stroke_commit_texture_binding_count(sequence);
    for (std::size_t binding_index = 0; binding_index < binding_count; ++binding_index) {
        const auto& binding = sequence.texture_bindings[binding_index];
        set_active_texture_unit(static_cast<int>(binding.slot));
        bind_texture_role(binding.role);
        bind_sampler_role(binding.role, static_cast<int>(binding.slot));
    }
}

template <typename SetActiveTextureUnit, typename BindFaceTextureRole, typename BindFaceSamplerRole>
inline void bind_legacy_canvas_stroke_commit_face_inputs(
    const pp::paint_renderer::CanvasStrokeCommitSequencePlan& sequence,
    SetActiveTextureUnit&& set_active_texture_unit,
    BindFaceTextureRole&& bind_face_texture_role,
    BindFaceSamplerRole&& bind_face_sampler_role)
{
    bind_legacy_canvas_stroke_commit_inputs(
        sequence,
        std::forward<SetActiveTextureUnit>(set_active_texture_unit),
        std::forward<BindFaceTextureRole>(bind_face_texture_role),
        std::forward<BindFaceSamplerRole>(bind_face_sampler_role));
}

[[nodiscard]] inline LegacyCanvasStrokeCommitRequest make_legacy_canvas_stroke_commit_request(
    const std::array<LegacyCanvasStrokeCommitFace, 6>& faces,
    const pp::paint_renderer::CanvasStrokeCommitSequencePlan& sequence,
    const LegacyCanvasStrokeCommitCallbacks& callbacks)
{
    return LegacyCanvasStrokeCommitRequest {
        .context = "Canvas::stroke_commit",
        .faces = faces,
        .sequence = sequence,
        .callbacks = callbacks,
    };
}

template <typename SetupShader, typename DrawPlane>
inline void execute_legacy_canvas_stroke_commit_erase(
    SetupShader&& setup_shader,
    DrawPlane&& draw_plane)
{
    setup_shader();
    draw_plane();
}

template <typename SetupShader, typename DrawPlane>
inline void execute_legacy_canvas_stroke_commit_paint(
    SetupShader&& setup_shader,
    DrawPlane&& draw_plane)
{
    setup_shader();
    draw_plane();
}

template <typename SetupShader, typename SetActiveTextureUnit, typename BindLayerScratch, typename CopyFramebufferToTexture>
inline void copy_legacy_canvas_stroke_commit_to_dilate_source(
    const pp::paint_renderer::CanvasStrokeCommitSequencePlan& sequence,
    SetupShader&& setup_shader,
    SetActiveTextureUnit&& set_active_texture_unit,
    BindLayerScratch&& bind_layer_scratch,
    CopyFramebufferToTexture&& copy_framebuffer_to_texture,
    LegacyCanvasStrokeCommitCopyExtent extent)
{
    const auto layer_scratch_slot = legacy_canvas_stroke_commit_texture_slot(
        sequence,
        pp::paint_renderer::CanvasStrokeCommitTextureRole::layer_scratch);
    if (layer_scratch_slot < 0 || extent.width <= 0 || extent.height <= 0) {
        return;
    }

    setup_shader();
    set_active_texture_unit(layer_scratch_slot);
    bind_layer_scratch();
    copy_framebuffer_to_texture(0, 0, 0, 0, extent.width, extent.height);
}

template <typename DrawPlane>
inline void execute_legacy_canvas_stroke_commit_dilate(DrawPlane&& draw_plane)
{
    draw_plane();
}

[[nodiscard]] inline bool legacy_canvas_stroke_commit_callbacks_ready(
    const LegacyCanvasStrokeCommitCallbacks& callbacks) noexcept
{
    return callbacks.mark_commit_started &&
        callbacks.capture_render_state &&
        callbacks.prepare_render_state &&
        callbacks.restore_render_state &&
        callbacks.publish_history &&
        callbacks.capture_timelapse_frame &&
        callbacks.bind_layer_framebuffer &&
        callbacks.capture_history_region &&
        callbacks.apply_layer_dirty_region &&
        callbacks.copy_layer_to_commit_destination &&
        callbacks.bind_commit_inputs &&
        callbacks.execute_erase_composite &&
        callbacks.execute_paint_composite &&
        callbacks.copy_committed_to_dilate_source &&
        callbacks.execute_commit_dilate &&
        callbacks.unbind_layer_framebuffer;
}

[[nodiscard]] inline LegacyCanvasStrokeCommitResult execute_legacy_canvas_stroke_commit_sequence(
    const LegacyCanvasStrokeCommitRequest& request)
{
    LegacyCanvasStrokeCommitResult result;
    if (!legacy_canvas_stroke_commit_callbacks_ready(request.callbacks)) {
        return result;
    }

    request.callbacks.mark_commit_started();
    request.callbacks.capture_render_state();
    request.callbacks.prepare_render_state();

    for (const auto& face : request.faces) {
        if (!face.dirty) {
            continue;
        }

        request.callbacks.bind_layer_framebuffer(face.index);

        const auto step_count = legacy_canvas_stroke_commit_step_count(request.sequence);
        for (std::size_t step_index = 0; step_index < step_count; ++step_index) {
            switch (request.sequence.steps[step_index]) {
            case pp::paint_renderer::CanvasStrokeCommitStep::readback_history_region:
                request.callbacks.capture_history_region(face.index);
                break;
            case pp::paint_renderer::CanvasStrokeCommitStep::update_layer_dirty_state:
                request.callbacks.apply_layer_dirty_region(face.index);
                break;
            case pp::paint_renderer::CanvasStrokeCommitStep::copy_layer_rtt_to_scratch:
                request.callbacks.copy_layer_to_commit_destination(face.index);
                break;
            case pp::paint_renderer::CanvasStrokeCommitStep::bind_commit_inputs:
                request.callbacks.bind_commit_inputs(face.index);
                break;
            case pp::paint_renderer::CanvasStrokeCommitStep::erase_draw:
                request.callbacks.execute_erase_composite(face.index);
                break;
            case pp::paint_renderer::CanvasStrokeCommitStep::composite_draw:
                request.callbacks.execute_paint_composite(face.index);
                break;
            case pp::paint_renderer::CanvasStrokeCommitStep::copy_committed_rtt_to_scratch:
                request.callbacks.copy_committed_to_dilate_source(face.index);
                break;
            case pp::paint_renderer::CanvasStrokeCommitStep::dilate_edges_draw:
                request.callbacks.execute_commit_dilate(face.index);
                break;
            }
        }

        request.callbacks.unbind_layer_framebuffer(face.index);
        ++result.committed_faces;
    }

    request.callbacks.restore_render_state();
    request.callbacks.publish_history();
    request.callbacks.capture_timelapse_frame();

    result.ok = true;
    return result;
}

} // namespace pp::panopainter
