#include "pch.h"

#include "legacy_document_canvas_services.h"

#include "action.h"
#include "app.h"
#include "canvas.h"

#include <algorithm>
#include <cstring>
#include <cstdint>
#include <span>
#include <string>
#include <vector>

namespace pp::panopainter {
namespace {

class LegacyDocumentCanvasClearServices final : public pp::app::DocumentCanvasClearServices {
public:
    explicit LegacyDocumentCanvasClearServices(App& app) noexcept
        : app_(app)
    {
    }

    void clear_current_canvas(float r, float g, float b, float a) override
    {
        if (!legacy_document_canvas_available(app_)) {
            return;
        }

        app_.canvas->m_canvas->clear({ r, g, b, a });
    }

private:
    App& app_;
};

class LegacyDocumentResizeServices final : public pp::app::DocumentResizeServices {
public:
    explicit LegacyDocumentResizeServices(App& app) noexcept
        : app_(app)
    {
    }

    void resize_document(int width, int height) override
    {
        if (!legacy_document_canvas_available(app_)) {
            return;
        }

        app_.canvas->m_canvas->resize(width, height);
    }

    void update_title() override
    {
        app_.title_update();
    }

    void clear_history() override
    {
        ActionManager::clear();
    }

private:
    App& app_;
};

[[nodiscard]] std::uint32_t legacy_u32_or_zero(int value) noexcept
{
    return value <= 0 ? 0U : static_cast<std::uint32_t>(value);
}

[[nodiscard]] std::size_t pending_face_payload_count(const Layer& layer) noexcept
{
    const auto frame_count = layer.frames_count();
    if (frame_count <= 0) {
        return 0U;
    }
    return static_cast<std::size_t>(frame_count) * pp::document::cube_face_count;
}

[[nodiscard]] std::uint32_t legacy_nonnegative_u32(float value) noexcept
{
    if (value <= 0.0F) {
        return 0U;
    }
    return static_cast<std::uint32_t>(value);
}

struct LegacyLayerPayloadStorage {
    std::vector<std::vector<std::uint8_t>> bytes;
    std::vector<pp::app::DocumentCanvasFacePayloadInput> payloads;
};

void append_legacy_layer_payloads(
    Layer& layer,
    int frame_index,
    LegacyLayerPayloadStorage& storage)
{
    auto snapshot = layer.snapshot(frame_index);
    for (std::uint32_t face_index = 0; face_index < pp::document::cube_face_count; ++face_index) {
        if (!snapshot.m_dirty_face[face_index] || snapshot.image[face_index] == nullptr) {
            continue;
        }

        const auto& box = snapshot.m_dirty_box[face_index];
        const auto x = legacy_nonnegative_u32(box.x);
        const auto y = legacy_nonnegative_u32(box.y);
        const auto width = legacy_nonnegative_u32(box.z - box.x);
        const auto height = legacy_nonnegative_u32(box.w - box.y);
        if (width == 0U || height == 0U) {
            continue;
        }

        const auto byte_count = static_cast<std::size_t>(width) * static_cast<std::size_t>(height)
            * pp::document::rgba8_components;
        storage.bytes.push_back(std::vector<std::uint8_t>(byte_count));
        std::memcpy(storage.bytes.back().data(), snapshot.image[face_index].get(), byte_count);
        storage.payloads.push_back(pp::app::DocumentCanvasFacePayloadInput {
            .frame_index = static_cast<std::uint32_t>(std::max(frame_index, 0)),
            .face_index = face_index,
            .x = x,
            .y = y,
            .width = width,
            .height = height,
            .rgba8 = std::span<const std::uint8_t>(storage.bytes.back().data(), storage.bytes.back().size()),
        });
    }
}

} // namespace

bool legacy_document_canvas_available(const App& app) noexcept
{
    return app.canvas != nullptr && app.canvas->m_canvas != nullptr;
}

pp::foundation::Result<pp::app::DocumentCanvasSnapshotResult> capture_legacy_canvas_document_snapshot(
    const Canvas& canvas)
{
    std::vector<std::string> layer_names;
    std::vector<std::vector<std::uint32_t>> layer_frame_durations;
    std::vector<pp::app::DocumentCanvasLayerSnapshotInput> layers;
    layer_names.reserve(canvas.m_layers.size());
    layer_frame_durations.reserve(canvas.m_layers.size());
    layers.reserve(canvas.m_layers.size());

    for (const auto& legacy_layer : canvas.m_layers) {
        if (!legacy_layer) {
            continue;
        }

        layer_names.push_back(legacy_layer->m_name);
        auto& durations = layer_frame_durations.emplace_back();
        const auto frame_count = legacy_layer->frames_count();
        durations.reserve(static_cast<std::size_t>(std::max(frame_count, 0)));
        for (int frame_index = 0; frame_index < frame_count; ++frame_index) {
            durations.push_back(legacy_u32_or_zero(legacy_layer->frame_duration(frame_index)));
        }

        layers.push_back(pp::app::DocumentCanvasLayerSnapshotInput {
            .name = layer_names.back(),
            .visible = legacy_layer->m_visible,
            .alpha_locked = legacy_layer->m_alpha_locked,
            .opacity = legacy_layer->m_opacity,
            .blend_mode = legacy_layer->m_blend_mode,
            .frame_durations_ms = std::span<const std::uint32_t>(durations),
            .pending_face_payloads = pending_face_payload_count(*legacy_layer),
        });
    }

    return pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput {
        .has_canvas = true,
        .width = legacy_u32_or_zero(canvas.m_width),
        .height = legacy_u32_or_zero(canvas.m_height),
        .active_layer_index = static_cast<std::size_t>(std::max(canvas.m_current_layer_idx, 0)),
        .active_frame_index = static_cast<std::size_t>(std::max(canvas.m_anim_frame, 0)),
        .layers = std::span<const pp::app::DocumentCanvasLayerSnapshotInput>(layers),
    });
}

pp::foundation::Result<pp::app::DocumentCanvasSnapshotResult> capture_legacy_canvas_document_snapshot(
    const App& app)
{
    if (!legacy_document_canvas_available(app)) {
        return pp::foundation::Result<pp::app::DocumentCanvasSnapshotResult>::failure(
            pp::foundation::Status::invalid_argument("legacy document canvas snapshot requires a canvas"));
    }

    return capture_legacy_canvas_document_snapshot(*app.canvas->m_canvas);
}

pp::foundation::Result<pp::app::DocumentCanvasSnapshotResult> capture_legacy_canvas_document_payload_snapshot(
    Canvas& canvas)
{
    std::vector<std::string> layer_names;
    std::vector<std::vector<std::uint32_t>> layer_frame_durations;
    std::vector<LegacyLayerPayloadStorage> layer_payload_storage;
    std::vector<pp::app::DocumentCanvasLayerSnapshotInput> layers;
    layer_names.reserve(canvas.m_layers.size());
    layer_frame_durations.reserve(canvas.m_layers.size());
    layer_payload_storage.reserve(canvas.m_layers.size());
    layers.reserve(canvas.m_layers.size());

    for (const auto& legacy_layer : canvas.m_layers) {
        if (!legacy_layer) {
            continue;
        }

        layer_names.push_back(legacy_layer->m_name);
        auto& durations = layer_frame_durations.emplace_back();
        auto& payload_storage = layer_payload_storage.emplace_back();
        const auto frame_count = legacy_layer->frames_count();
        durations.reserve(static_cast<std::size_t>(std::max(frame_count, 0)));
        for (int frame_index = 0; frame_index < frame_count; ++frame_index) {
            durations.push_back(legacy_u32_or_zero(legacy_layer->frame_duration(frame_index)));
            append_legacy_layer_payloads(*legacy_layer, frame_index, payload_storage);
        }

        layers.push_back(pp::app::DocumentCanvasLayerSnapshotInput {
            .name = layer_names.back(),
            .visible = legacy_layer->m_visible,
            .alpha_locked = legacy_layer->m_alpha_locked,
            .opacity = legacy_layer->m_opacity,
            .blend_mode = legacy_layer->m_blend_mode,
            .frame_durations_ms = std::span<const std::uint32_t>(durations),
            .pending_face_payloads = payload_storage.payloads.size(),
            .captured_face_payloads = std::span<const pp::app::DocumentCanvasFacePayloadInput>(
                payload_storage.payloads),
        });
    }

    return pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput {
        .has_canvas = true,
        .width = legacy_u32_or_zero(canvas.m_width),
        .height = legacy_u32_or_zero(canvas.m_height),
        .active_layer_index = static_cast<std::size_t>(std::max(canvas.m_current_layer_idx, 0)),
        .active_frame_index = static_cast<std::size_t>(std::max(canvas.m_anim_frame, 0)),
        .layers = std::span<const pp::app::DocumentCanvasLayerSnapshotInput>(layers),
    });
}

pp::foundation::Result<pp::app::DocumentCanvasSnapshotResult> capture_legacy_canvas_document_payload_snapshot(
    App& app)
{
    if (!legacy_document_canvas_available(app)) {
        return pp::foundation::Result<pp::app::DocumentCanvasSnapshotResult>::failure(
            pp::foundation::Status::invalid_argument("legacy document canvas payload snapshot requires a canvas"));
    }

    return capture_legacy_canvas_document_payload_snapshot(*app.canvas->m_canvas);
}

pp::foundation::Status execute_legacy_document_canvas_clear_plan(
    App& app,
    const pp::app::DocumentCanvasClearPlan& plan)
{
    LegacyDocumentCanvasClearServices services(app);
    return pp::app::execute_document_canvas_clear_plan(plan, services);
}

pp::foundation::Status execute_legacy_document_resize_plan(
    App& app,
    const pp::app::DocumentResizePlan& plan)
{
    LegacyDocumentResizeServices services(app);
    return pp::app::execute_document_resize_plan(plan, services);
}

} // namespace pp::panopainter
