#pragma once

#include "document/document.h"
#include "document/ppi_export.h"
#include "foundation/result.h"

#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

namespace pp::app {

struct DocumentCanvasClearPlan {
    float r = 0.0F;
    float g = 0.0F;
    float b = 0.0F;
    float a = 0.0F;
    bool clears_canvas = false;
    bool records_undo = false;
    bool marks_unsaved = false;
    bool no_op = true;
};

struct DocumentCanvasFacePayloadInput {
    std::uint32_t frame_index = 0;
    std::uint32_t face_index = 0;
    std::uint32_t x = 0;
    std::uint32_t y = 0;
    std::uint32_t width = 0;
    std::uint32_t height = 0;
    std::span<const std::uint8_t> rgba8;
};

struct DocumentCanvasLayerSnapshotInput {
    std::string_view name;
    bool visible = true;
    bool alpha_locked = false;
    float opacity = 1.0F;
    int blend_mode = 0;
    std::span<const std::uint32_t> frame_durations_ms;
    std::size_t pending_face_payloads = 0;
    std::span<const DocumentCanvasFacePayloadInput> captured_face_payloads;
};

struct DocumentCanvasSnapshotInput {
    bool has_canvas = true;
    std::uint32_t width = 0;
    std::uint32_t height = 0;
    std::size_t active_layer_index = 0;
    std::size_t active_frame_index = 0;
    std::span<const DocumentCanvasLayerSnapshotInput> layers;
};

struct DocumentCanvasSnapshotResult {
    pp::document::CanvasDocument document;
    std::size_t layer_count = 0;
    std::size_t frame_count = 0;
    std::size_t pending_face_payloads = 0;
    std::size_t captured_face_payloads = 0;
    bool metadata_only = false;
    bool requires_renderer_payload_readback = false;
};

struct DocumentCanvasSaveSnapshotReport {
    std::uint32_t width = 0;
    std::uint32_t height = 0;
    std::size_t layer_count = 0;
    std::size_t frame_count = 0;
    std::size_t captured_face_payloads = 0;
    std::size_t pending_face_payloads = 0;
    bool payload_complete = false;
    bool can_export_ppi = false;
};

enum class DocumentCanvasSaveWriterAction {
    use_document_ppi_writer,
    use_legacy_project_save,
};

struct DocumentCanvasSaveWriterRoutePlan {
    DocumentCanvasSaveWriterAction action = DocumentCanvasSaveWriterAction::use_legacy_project_save;
    bool payload_complete = false;
    bool can_export_ppi = false;
    bool uses_document_ppi_writer = false;
    std::string_view fallback_reason;
};

struct DocumentCanvasPpiExportResult {
    DocumentCanvasSaveSnapshotReport report;
    std::vector<std::byte> bytes;
};

struct DocumentCanvasProjectSaveTargetPlan {
    std::string target_path;
    std::string file_name;
    std::string temporary_path;
    std::string timelapse_path;
};

enum class DocumentCanvasProjectSaveWriteAction {
    write_direct_to_target,
    write_temporary_then_swap,
};

struct DocumentCanvasProjectSaveWritePlan {
    DocumentCanvasProjectSaveWriteAction action = DocumentCanvasProjectSaveWriteAction::write_direct_to_target;
    std::string write_path;
    std::string target_path;
    std::string temporary_path;
    bool target_exists = false;
    bool uses_temporary = false;
    bool falls_back_to_direct_on_temporary_open_failure = false;
};

struct DocumentCanvasProjectSaveCommitInput {
    bool used_temporary = false;
    bool target_remove_attempted = false;
    bool target_remove_succeeded = false;
    bool temporary_rename_attempted = false;
    bool temporary_rename_succeeded = false;
};

struct DocumentCanvasProjectSaveCommitPlan {
    bool saved = false;
    bool used_temporary = false;
    bool target_removed = false;
    bool temporary_renamed = false;
    bool target_may_be_missing = false;
    std::string_view log_message;
};

struct DocumentCanvasProjectSavePostCommitInput {
    bool save_succeeded = false;
    bool timelapse_encoder_available = false;
    bool progress_ui_visible = false;
};

struct DocumentCanvasProjectSavePostCommitPlan {
    bool marks_document_clean = false;
    bool marks_new_document_committed = false;
    bool saves_timelapse_sidecar = false;
    bool flushes_platform_storage = false;
    bool dismisses_progress_ui = false;
    bool updates_title = true;
};

class DocumentCanvasClearServices {
public:
    virtual ~DocumentCanvasClearServices() = default;

    virtual void clear_current_canvas(float r, float g, float b, float a) = 0;
};

[[nodiscard]] inline pp::foundation::Status validate_clear_color_channel(float value) noexcept
{
    if (!std::isfinite(value)) {
        return pp::foundation::Status::invalid_argument("clear color channel must be finite");
    }
    if (value < 0.0F || value > 1.0F) {
        return pp::foundation::Status::out_of_range("clear color channel must be within 0..1");
    }
    return pp::foundation::Status::success();
}

[[nodiscard]] inline pp::foundation::Result<DocumentCanvasSnapshotResult> plan_document_canvas_snapshot(
    DocumentCanvasSnapshotInput input)
{
    if (!input.has_canvas) {
        return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
            pp::foundation::Status::invalid_argument("document canvas snapshot requires a canvas"));
    }

    if (input.layers.empty()) {
        return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
            pp::foundation::Status::invalid_argument("document canvas snapshot requires at least one layer"));
    }

    std::size_t frame_count = 1U;
    std::size_t pending_face_payloads = 0U;
    std::size_t captured_face_payloads = 0U;
    for (const auto& layer : input.layers) {
        frame_count = std::max(frame_count, layer.frame_durations_ms.size());
        pending_face_payloads += layer.pending_face_payloads;
        captured_face_payloads += layer.captured_face_payloads.size();
    }

    if (input.active_layer_index >= input.layers.size()) {
        return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
            pp::foundation::Status::out_of_range("active canvas layer is outside the document snapshot"));
    }

    if (input.active_frame_index >= frame_count) {
        return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(
            pp::foundation::Status::out_of_range("active canvas frame is outside the document snapshot"));
    }

    std::vector<pp::document::AnimationFrame> root_frames;
    root_frames.reserve(frame_count);
    for (std::size_t frame_index = 0; frame_index < frame_count; ++frame_index) {
        std::uint32_t duration_ms = 100U;
        for (const auto& layer : input.layers) {
            if (frame_index < layer.frame_durations_ms.size()) {
                duration_ms = layer.frame_durations_ms[frame_index];
                break;
            }
        }
        root_frames.push_back(pp::document::AnimationFrame { .duration_ms = duration_ms });
    }

    std::vector<std::string> layer_names;
    std::vector<std::vector<pp::document::AnimationFrame>> layer_frames;
    std::vector<pp::document::DocumentLayerConfig> layer_configs;
    layer_names.reserve(input.layers.size());
    layer_frames.reserve(input.layers.size());
    layer_configs.reserve(input.layers.size());

    for (std::size_t layer_index = 0; layer_index < input.layers.size(); ++layer_index) {
        const auto& layer = input.layers[layer_index];
        if (layer.name.empty()) {
            layer_names.push_back("Layer " + std::to_string(layer_index + 1U));
        } else {
            layer_names.push_back(std::string(layer.name));
        }

        layer_frames.push_back({});
        auto& frames = layer_frames.back();
        frames.reserve(layer.frame_durations_ms.empty() ? root_frames.size() : layer.frame_durations_ms.size());
        if (layer.frame_durations_ms.empty()) {
            frames = root_frames;
        } else {
            for (const auto duration_ms : layer.frame_durations_ms) {
                frames.push_back(pp::document::AnimationFrame { .duration_ms = duration_ms });
            }
        }

        layer_configs.push_back(pp::document::DocumentLayerConfig {
            .name = layer_names.back(),
            .visible = layer.visible,
            .alpha_locked = layer.alpha_locked,
            .opacity = layer.opacity,
            .blend_mode = static_cast<pp::paint::BlendMode>(layer.blend_mode),
            .frames = std::span<const pp::document::AnimationFrame>(frames),
        });
    }

    auto document = pp::document::CanvasDocument::create_from_snapshot(pp::document::DocumentSnapshotConfig {
        .width = input.width,
        .height = input.height,
        .layers = std::span<const pp::document::DocumentLayerConfig>(layer_configs),
        .frames = std::span<const pp::document::AnimationFrame>(root_frames),
    });
    if (!document) {
        return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(document.status());
    }

    for (std::size_t layer_index = 0; layer_index < input.layers.size(); ++layer_index) {
        for (const auto& payload : input.layers[layer_index].captured_face_payloads) {
            pp::document::LayerFacePixels pixels {
                .face_index = payload.face_index,
                .x = payload.x,
                .y = payload.y,
                .width = payload.width,
                .height = payload.height,
                .rgba8 = std::vector<std::uint8_t>(payload.rgba8.begin(), payload.rgba8.end()),
            };
            const auto payload_status = document.value().set_layer_frame_face_pixels(
                layer_index,
                payload.frame_index,
                std::move(pixels));
            if (!payload_status.ok()) {
                return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(payload_status);
            }
        }
    }

    auto active_status = document.value().set_active_layer(input.active_layer_index);
    if (!active_status.ok()) {
        return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(active_status);
    }

    active_status = document.value().set_active_frame(input.active_frame_index);
    if (!active_status.ok()) {
        return pp::foundation::Result<DocumentCanvasSnapshotResult>::failure(active_status);
    }

    return pp::foundation::Result<DocumentCanvasSnapshotResult>::success(DocumentCanvasSnapshotResult {
        .document = std::move(document.value()),
        .layer_count = input.layers.size(),
        .frame_count = frame_count,
        .pending_face_payloads = pending_face_payloads,
        .captured_face_payloads = captured_face_payloads,
        .metadata_only = captured_face_payloads == 0U,
        .requires_renderer_payload_readback = pending_face_payloads > captured_face_payloads,
    });
}

[[nodiscard]] inline DocumentCanvasSaveSnapshotReport make_document_canvas_save_snapshot_report(
    const DocumentCanvasSnapshotResult& snapshot) noexcept
{
    return DocumentCanvasSaveSnapshotReport {
        .width = snapshot.document.width(),
        .height = snapshot.document.height(),
        .layer_count = snapshot.layer_count,
        .frame_count = snapshot.frame_count,
        .captured_face_payloads = snapshot.captured_face_payloads,
        .pending_face_payloads = snapshot.pending_face_payloads,
        .payload_complete = !snapshot.requires_renderer_payload_readback,
        .can_export_ppi = !snapshot.requires_renderer_payload_readback,
    };
}

[[nodiscard]] constexpr DocumentCanvasSaveWriterRoutePlan plan_document_canvas_save_writer_route(
    DocumentCanvasSaveSnapshotReport report) noexcept
{
    DocumentCanvasSaveWriterRoutePlan plan;
    plan.payload_complete = report.payload_complete;
    plan.can_export_ppi = report.can_export_ppi;

    if (!report.payload_complete || !report.can_export_ppi) {
        plan.fallback_reason = "canvas document snapshot still requires renderer payload readback";
        return plan;
    }

    plan.action = DocumentCanvasSaveWriterAction::use_document_ppi_writer;
    plan.uses_document_ppi_writer = true;
    return plan;
}

[[nodiscard]] inline pp::foundation::Result<DocumentCanvasPpiExportResult>
export_document_canvas_save_snapshot_to_ppi(const DocumentCanvasSnapshotResult& snapshot)
{
    const auto report = make_document_canvas_save_snapshot_report(snapshot);
    const auto route = plan_document_canvas_save_writer_route(report);
    if (!route.uses_document_ppi_writer) {
        return pp::foundation::Result<DocumentCanvasPpiExportResult>::failure(
            pp::foundation::Status::invalid_argument(route.fallback_reason.data()));
    }

    auto bytes = pp::document::export_ppi_project_document(snapshot.document);
    if (!bytes) {
        return pp::foundation::Result<DocumentCanvasPpiExportResult>::failure(bytes.status());
    }

    return pp::foundation::Result<DocumentCanvasPpiExportResult>::success(DocumentCanvasPpiExportResult {
        .report = report,
        .bytes = std::move(bytes.value()),
    });
}

[[nodiscard]] inline pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>
plan_document_canvas_project_save_target(
    std::string_view data_directory,
    std::string_view target_path)
{
    if (data_directory.empty()) {
        return pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>::failure(
            pp::foundation::Status::invalid_argument("project save data directory must not be empty"));
    }
    if (target_path.empty()) {
        return pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>::failure(
            pp::foundation::Status::invalid_argument("project save target path must not be empty"));
    }

    const auto basename_start = target_path.find_last_of("/\\");
    const auto file_name_start = basename_start == std::string_view::npos ? 0U : basename_start + 1U;
    auto file_name = target_path.substr(file_name_start);
    if (file_name.empty()) {
        return pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>::failure(
            pp::foundation::Status::invalid_argument("project save target file name must not be empty"));
    }

    constexpr std::string_view ppi_extension = ".ppi";
    if (file_name.size() > ppi_extension.size()
        && file_name.substr(file_name.size() - ppi_extension.size()) == ppi_extension) {
        file_name.remove_suffix(ppi_extension.size());
    }

    DocumentCanvasProjectSaveTargetPlan plan;
    plan.target_path = std::string(target_path);
    plan.file_name = std::string(file_name);
    plan.temporary_path.reserve(data_directory.size() + plan.file_name.size() + 10U);
    plan.temporary_path += data_directory;
    plan.temporary_path += "/";
    plan.temporary_path += plan.file_name;
    plan.temporary_path += ".tmp.ppi";
    plan.timelapse_path.reserve(data_directory.size() + plan.file_name.size() + 6U);
    plan.timelapse_path += data_directory;
    plan.timelapse_path += "/";
    plan.timelapse_path += plan.file_name;
    plan.timelapse_path += ".pptl";
    return pp::foundation::Result<DocumentCanvasProjectSaveTargetPlan>::success(std::move(plan));
}

[[nodiscard]] inline pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>
plan_document_canvas_project_save_write(
    const DocumentCanvasProjectSaveTargetPlan& target,
    bool target_exists)
{
    if (target.target_path.empty()) {
        return pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>::failure(
            pp::foundation::Status::invalid_argument("project save write target path must not be empty"));
    }

    DocumentCanvasProjectSaveWritePlan plan;
    plan.target_exists = target_exists;
    plan.target_path = target.target_path;
    plan.temporary_path = target.temporary_path;

    if (!target_exists) {
        plan.write_path = target.target_path;
        return pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>::success(std::move(plan));
    }

    if (target.temporary_path.empty()) {
        return pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>::failure(
            pp::foundation::Status::invalid_argument("project save temporary path must not be empty"));
    }

    plan.action = DocumentCanvasProjectSaveWriteAction::write_temporary_then_swap;
    plan.write_path = target.temporary_path;
    plan.uses_temporary = true;
    plan.falls_back_to_direct_on_temporary_open_failure = true;
    return pp::foundation::Result<DocumentCanvasProjectSaveWritePlan>::success(std::move(plan));
}

[[nodiscard]] constexpr DocumentCanvasProjectSaveCommitPlan plan_document_canvas_project_save_commit(
    DocumentCanvasProjectSaveCommitInput input) noexcept
{
    DocumentCanvasProjectSaveCommitPlan plan;
    plan.used_temporary = input.used_temporary;

    if (!input.used_temporary) {
        plan.saved = true;
        plan.log_message = "project saved to target";
        return plan;
    }

    if (!input.target_remove_attempted || !input.target_remove_succeeded) {
        plan.log_message = "could not remove target project before temporary swap";
        return plan;
    }

    plan.target_removed = true;
    if (!input.temporary_rename_attempted || !input.temporary_rename_succeeded) {
        plan.target_may_be_missing = true;
        plan.log_message = "temporary project not swapped after original removal";
        return plan;
    }

    plan.saved = true;
    plan.temporary_renamed = true;
    plan.log_message = "temporary project swapped successfully";
    return plan;
}

[[nodiscard]] constexpr DocumentCanvasProjectSavePostCommitPlan plan_document_canvas_project_save_post_commit(
    DocumentCanvasProjectSavePostCommitInput input) noexcept
{
    DocumentCanvasProjectSavePostCommitPlan plan;
    plan.dismisses_progress_ui = input.progress_ui_visible;

    if (!input.save_succeeded) {
        return plan;
    }

    plan.marks_document_clean = true;
    plan.marks_new_document_committed = true;
    plan.saves_timelapse_sidecar = input.timelapse_encoder_available;
    plan.flushes_platform_storage = true;
    return plan;
}

[[nodiscard]] inline pp::foundation::Result<DocumentCanvasClearPlan> plan_document_canvas_clear(
    bool has_canvas,
    float r = 0.0F,
    float g = 0.0F,
    float b = 0.0F,
    float a = 0.0F) noexcept
{
    const float channels[] { r, g, b, a };
    for (const float channel : channels) {
        const auto status = validate_clear_color_channel(channel);
        if (!status.ok()) {
            return pp::foundation::Result<DocumentCanvasClearPlan>::failure(status);
        }
    }

    DocumentCanvasClearPlan plan;
    plan.r = r;
    plan.g = g;
    plan.b = b;
    plan.a = a;
    plan.clears_canvas = has_canvas;
    plan.records_undo = has_canvas;
    plan.marks_unsaved = has_canvas;
    plan.no_op = !has_canvas;
    return pp::foundation::Result<DocumentCanvasClearPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Status execute_document_canvas_clear_plan(
    const DocumentCanvasClearPlan& plan,
    DocumentCanvasClearServices& services)
{
    const float channels[] { plan.r, plan.g, plan.b, plan.a };
    for (const float channel : channels) {
        const auto status = validate_clear_color_channel(channel);
        if (!status.ok()) {
            return status;
        }
    }

    if (plan.no_op || !plan.clears_canvas) {
        return pp::foundation::Status::success();
    }

    services.clear_current_canvas(plan.r, plan.g, plan.b, plan.a);
    return pp::foundation::Status::success();
}

} // namespace pp::app
