#include "pch.h"

#include "legacy_document_export_services.h"

#include "app.h"
#include "legacy_document_canvas_services.h"
#include "paint_renderer/compositor.h"

#include <array>
#include <condition_variable>
#include <fstream>
#include <deque>
#include <functional>
#include <mutex>
#include <limits>
#include <span>
#include <string>
#include <stop_token>
#include <thread>

namespace pp::panopainter {
namespace {

void show_export_success_dialog(
    App& app,
    const pp::app::DocumentExportSuccessDialogPlan& plan)
{
    if (plan.show_dialog) {
        app.message_box(plan.dialog.title, plan.dialog.message, plan.dialog.show_cancel);
    }
}

struct LegacyDocumentExportSnapshotReports {
    pp::app::DocumentCanvasSnapshotResult snapshot;
    pp::paint_renderer::DocumentFrameFacePngExportResult face_pngs;
};

class LegacyDocumentVideoExportWorker final {
public:
    LegacyDocumentVideoExportWorker()
        : worker_([this](std::stop_token stop_token) {
            run(stop_token);
        })
    {
    }

    ~LegacyDocumentVideoExportWorker()
    {
        shutdown();
    }

    void post(std::function<void()> task)
    {
        {
            std::lock_guard<std::mutex> lock(mutex_);
            if (stopping_)
                return;
            tasks_.push_back(std::move(task));
        }
        cv_.notify_one();
    }

private:
    void shutdown()
    {
        {
            std::lock_guard<std::mutex> lock(mutex_);
            stopping_ = true;
        }
        cv_.notify_all();
    }

    void run(std::stop_token stop_token)
    {
        for (;;) {
            std::function<void()> task;
            {
                std::unique_lock<std::mutex> lock(mutex_);
                cv_.wait(lock, [&] {
                    return stopping_ || stop_token.stop_requested() || !tasks_.empty();
                });
                if ((stopping_ || stop_token.stop_requested()) && tasks_.empty())
                    break;
                task = std::move(tasks_.front());
                tasks_.pop_front();
            }

            if (task) {
                try {
                    task();
                } catch (...) {
                    LOG("document video export worker task failed");
                }
            }
        }
    }

    std::mutex mutex_;
    std::condition_variable cv_;
    std::deque<std::function<void()>> tasks_;
    bool stopping_ = false;
    std::jthread worker_;
};

LegacyDocumentVideoExportWorker& document_video_export_worker()
{
    static LegacyDocumentVideoExportWorker worker;
    return worker;
}

pp::foundation::Status write_export_binary_file(std::string_view path, std::span<const std::byte> bytes)
{
    if (path.empty()) {
        return pp::foundation::Status::invalid_argument("export path must not be empty");
    }
    if (bytes.size() > static_cast<std::size_t>(std::numeric_limits<std::streamsize>::max())) {
        return pp::foundation::Status::out_of_range("export payload is too large to write");
    }

    std::ofstream stream(std::string(path), std::ios::binary);
    if (!stream) {
        return pp::foundation::Status::invalid_argument("export path could not be opened for writing");
    }

    stream.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
    if (!stream) {
        return pp::foundation::Status::invalid_argument("export payload could not be written");
    }

    return pp::foundation::Status::success();
}

class LegacyExportWriteServices final
    : public pp::app::DocumentCubeFaceExportWriteServices
    , public pp::app::DocumentDepthExportWriteServices
    , public pp::app::DocumentExportFileWriteServices
    , public pp::app::DocumentExportCollectionWriteServices {
public:
    explicit LegacyExportWriteServices(App& app) noexcept
        : app_(app)
    {
    }

    pp::foundation::Status write_binary_file(
        std::string_view path,
        std::span<const std::byte> bytes) override
    {
        return write_export_binary_file(path, bytes);
    }

    void publish_exported_image(std::string_view path) override
    {
        app_.publish_exported_image(std::string(path));
    }

private:
    App& app_;
};

pp::foundation::Result<LegacyDocumentExportSnapshotReports> prepare_legacy_document_export_snapshot(
    App& app,
    const char* context)
{
    auto snapshot = capture_legacy_canvas_document_payload_snapshot(app);
    if (!snapshot) {
        LOG("%s document export snapshot failed: %s", context, snapshot.status().message);
        return pp::foundation::Result<LegacyDocumentExportSnapshotReports>::failure(snapshot.status());
    }

    const auto report = pp::app::make_document_canvas_save_snapshot_report(snapshot.value());
    LOG(
        "%s document export snapshot: %ux%u layers=%zu frames=%zu capturedFaces=%zu pendingFaces=%zu ppiReady=%s",
        context,
        report.width,
        report.height,
        report.layer_count,
        report.frame_count,
        report.captured_face_payloads,
        report.pending_face_payloads,
        report.can_export_ppi ? "true" : "false");

    LegacyDocumentExportSnapshotReports reports {
        .snapshot = std::move(snapshot.value()),
    };
    if (!report.can_export_ppi) {
        return pp::foundation::Result<LegacyDocumentExportSnapshotReports>::success(std::move(reports));
    }

    auto readiness = pp::paint_renderer::prepare_document_frame_export_readiness(
        pp::paint_renderer::DocumentFrameCompositeRequest {
            .document = &reports.snapshot.document,
            .frame_index = reports.snapshot.document.active_frame_index(),
            .clear_color = {},
        });
    if (!readiness) {
        LOG("%s document export renderer readiness failed: %s", context, readiness.status().message);
        return pp::foundation::Result<LegacyDocumentExportSnapshotReports>::failure(readiness.status());
    }

    auto readiness_value = std::move(readiness.value());
    const auto& recorded_upload = readiness_value.recorded_upload;
    const auto& uploaded = recorded_upload.upload;
    LOG(
        "%s document export renderer upload: textures=%zu bytes=%llu transitions=%zu facePayloads=%zu commands=%zu uploadCommands=%zu transitionCommands=%zu",
        context,
        uploaded.texture_count,
        static_cast<unsigned long long>(uploaded.uploaded_bytes),
        uploaded.transition_count,
        uploaded.composite.face_payload_count,
        recorded_upload.command_count,
        recorded_upload.upload_command_count,
        recorded_upload.transition_command_count);

    LOG(
        "%s document export face PNG export: faces=%zu bytes=%llu facePayloads=%zu compositedLayerFaces=%zu",
        context,
        readiness_value.face_pngs.face_count,
        static_cast<unsigned long long>(readiness_value.face_pngs.encoded_bytes),
        readiness_value.face_pngs.composite.face_payload_count,
        readiness_value.face_pngs.composite.composited_layer_face_count);
    reports.face_pngs = std::move(readiness_value.face_pngs);

    return pp::foundation::Result<LegacyDocumentExportSnapshotReports>::success(std::move(reports));
}

bool should_use_document_snapshot_writer(
    const char* context,
    pp::app::DocumentExportExecutionKind kind,
    const LegacyDocumentExportSnapshotReports& reports,
    std::string_view target_path)
{
    const auto report = pp::app::make_document_canvas_save_snapshot_report(reports.snapshot);
    const auto route = pp::app::plan_document_export_snapshot_route_for_current_platform(
        kind,
        report,
        target_path);
    if (!route.uses_document_snapshot_writer) {
        LOG(
            "%s document export writer retained legacy export: %.*s",
            context,
            static_cast<int>(route.fallback_reason.size()),
            route.fallback_reason.data());
        return false;
    }

    LOG(
        "%s document export writer route: document-snapshot payloadComplete=%s capturedFaces=%zu pendingFaces=%zu",
        context,
        route.payload_complete ? "true" : "false",
        report.captured_face_payloads,
        report.pending_face_payloads);
    return true;
}

pp::foundation::Status export_cube_faces_from_document_snapshot(
    App& app,
    std::string_view document_name,
    const LegacyDocumentExportSnapshotReports& reports)
{
    const auto target = pp::app::make_document_cube_face_export_target(app.work_path, document_name);
    if (!target) {
        return target.status();
    }
    if (reports.face_pngs.face_count != pp::document::cube_face_count) {
        return pp::foundation::Status::invalid_argument("document snapshot did not produce all cube face PNGs");
    }

    std::array<pp::app::DocumentCubeFaceExportPayload, pp::document::cube_face_count> payloads;
    for (std::size_t face_index = 0; face_index < payloads.size(); ++face_index) {
        payloads[face_index].bytes = std::span<const std::byte>(reports.face_pngs.face_pngs[face_index]);
    }

    LegacyExportWriteServices services(app);
    return pp::app::execute_document_cube_face_export_write(target.value(), payloads, services);
}

pp::foundation::Status export_layers_from_document_snapshot(
    App& app,
    const pp::app::DocumentExportCollectionTarget& target,
    const LegacyDocumentExportSnapshotReports& reports)
{
    const auto report = pp::app::make_document_canvas_save_snapshot_report(reports.snapshot);
    if (!report.payload_complete) {
        return pp::foundation::Status::invalid_argument(
            "document snapshot layer export still requires renderer payload readback");
    }

    auto exported = pp::paint_renderer::export_document_layers_equirectangular_pngs(
        pp::paint_renderer::DocumentLayerEquirectangularPngExportRequest {
            .document = &reports.snapshot.document,
            .frame_index = reports.snapshot.document.active_frame_index(),
            .clear_color = {},
        });
    if (!exported) {
        return exported.status();
    }

    auto exported_value = std::move(exported.value());
    LOG(
        "export-layers document export PNG writer: layers=%zu bytes=%llu activeFrame=%zu",
        exported_value.layer_count,
        static_cast<unsigned long long>(exported_value.encoded_bytes),
        reports.snapshot.document.active_frame_index());

    std::vector<pp::app::DocumentExportCollectionPngPayload> payloads;
    payloads.reserve(exported_value.layers.size());
    for (const auto& layer : exported_value.layers) {
        payloads.push_back(pp::app::DocumentExportCollectionPngPayload {
            .path_suffix = pp::app::make_document_layer_export_path_suffix(layer.layer_index, layer.layer_name),
            .bytes = std::span<const std::byte>(layer.png.data(), layer.png.size()),
        });
    }

    LegacyExportWriteServices services(app);
    return pp::app::execute_document_export_collection_write(target, payloads, services);
}

pp::foundation::Status export_animation_frames_from_document_snapshot(
    App& app,
    const pp::app::DocumentExportCollectionTarget& target,
    const LegacyDocumentExportSnapshotReports& reports)
{
    const auto report = pp::app::make_document_canvas_save_snapshot_report(reports.snapshot);
    if (!report.payload_complete) {
        return pp::foundation::Status::invalid_argument(
            "document snapshot animation-frame export still requires renderer payload readback");
    }

    auto exported = pp::paint_renderer::export_document_animation_frames_equirectangular_pngs(
        pp::paint_renderer::DocumentAnimationFrameEquirectangularPngExportRequest {
            .document = &reports.snapshot.document,
            .clear_color = {},
        });
    if (!exported) {
        return exported.status();
    }

    auto exported_value = std::move(exported.value());
    LOG(
        "export-animation-frames document export PNG writer: frames=%zu bytes=%llu",
        exported_value.frame_count,
        static_cast<unsigned long long>(exported_value.encoded_bytes));

    std::vector<pp::app::DocumentExportCollectionPngPayload> payloads;
    payloads.reserve(exported_value.frames.size());
    for (const auto& frame : exported_value.frames) {
        payloads.push_back(pp::app::DocumentExportCollectionPngPayload {
            .path_suffix = pp::app::make_document_animation_frame_export_path_suffix(frame.frame_index),
            .bytes = std::span<const std::byte>(frame.png.data(), frame.png.size()),
        });
    }

    LegacyExportWriteServices services(app);
    return pp::app::execute_document_export_collection_write(target, payloads, services);
}

pp::foundation::Status export_equirectangular_from_document_snapshot(
    App& app,
    const pp::app::DocumentExportFileTarget& target,
    const LegacyDocumentExportSnapshotReports& reports)
{
    std::span<const std::byte> bytes;
    pp::paint_renderer::DocumentFrameEquirectangularPngExportResult png_export;
    pp::paint_renderer::DocumentFrameEquirectangularJpegExportResult jpeg_export;

    if (pp::app::document_export_path_is_png_target(target.path)) {
        auto exported = pp::paint_renderer::export_document_frame_equirectangular_png(reports.face_pngs.composite);
        if (!exported) {
            return exported.status();
        }

        png_export = std::move(exported.value());
        LOG(
            "export-equirectangular document export PNG writer: %ux%u bytes=%llu facePayloads=%zu compositedLayerFaces=%zu",
            png_export.equirectangular_extent.width,
            png_export.equirectangular_extent.height,
            static_cast<unsigned long long>(png_export.encoded_bytes),
            png_export.face_payload_count,
            png_export.composited_layer_face_count);
        bytes = std::span<const std::byte>(png_export.png.data(), png_export.png.size());
    } else if (pp::app::document_export_path_is_jpeg_target(target.path)) {
        auto exported = pp::paint_renderer::export_document_frame_equirectangular_jpeg(reports.face_pngs.composite);
        if (!exported) {
            return exported.status();
        }

        jpeg_export = std::move(exported.value());
        LOG(
            "export-equirectangular document export JPEG writer: %ux%u bytes=%llu facePayloads=%zu compositedLayerFaces=%zu xmp=%s",
            jpeg_export.equirectangular_extent.width,
            jpeg_export.equirectangular_extent.height,
            static_cast<unsigned long long>(jpeg_export.encoded_bytes),
            jpeg_export.face_payload_count,
            jpeg_export.composited_layer_face_count,
            jpeg_export.xmp_injected ? "true" : "false");
        bytes = std::span<const std::byte>(jpeg_export.jpeg.data(), jpeg_export.jpeg.size());
    } else {
        return pp::foundation::Status::invalid_argument(
            "document snapshot equirectangular export currently supports PNG and JPEG targets only");
    }

    LegacyExportWriteServices services(app);
    return pp::app::execute_document_export_file_write(
        target,
        pp::app::DocumentExportFilePayload {
            .bytes = bytes,
        },
        services);
}

pp::foundation::Status export_depth_from_document_snapshot(
    App& app,
    const pp::app::DocumentDepthExportTarget& target,
    const LegacyDocumentExportSnapshotReports& reports)
{
    auto exported = pp::paint_renderer::export_document_depth_pngs(
        pp::paint_renderer::DocumentDepthExportRenderPlanRequest {
            .document = &reports.snapshot.document,
            .frame_index = reports.snapshot.document.active_frame_index(),
        });
    if (!exported) {
        return exported.status();
    }

    LOG(
        "export-depth document export PNG writer: %ux%u imageBytes=%llu depthBytes=%llu mergedFaceDraws=%zu layerDepthDraws=%zu visitedLayers=%zu visibleLayers=%zu facePayloads=%zu",
        exported.value().output_extent.width,
        exported.value().output_extent.height,
        static_cast<unsigned long long>(exported.value().image_encoded_bytes),
        static_cast<unsigned long long>(exported.value().depth_encoded_bytes),
        exported.value().merged_face_draw_count,
        exported.value().layer_depth_draw_count,
        exported.value().visited_layer_count,
        exported.value().visible_layer_count,
        exported.value().face_payload_count);

    LegacyExportWriteServices services(app);
    return pp::app::execute_document_depth_export_write(
        target,
        pp::app::DocumentDepthExportPayload {
            .image_bytes = std::span<const std::byte>(exported.value().image_png),
            .depth_bytes = std::span<const std::byte>(exported.value().depth_png),
        },
        services);
}

class LegacyDocumentExportServices final : public pp::app::DocumentExportServices {
public:
    explicit LegacyDocumentExportServices(App& app) noexcept
        : app_(app)
    {
    }

    bool create_directory(std::string_view directory) override
    {
        return Asset::create_dir(std::string(directory));
    }

    void export_equirectangular(const pp::app::DocumentExportFileTarget& target) override
    {
        auto* app = &app_;
        const auto prepared = prepare_legacy_document_export_snapshot(app_, "export-equirectangular");
        if (prepared) {
            if (should_use_document_snapshot_writer(
                    "export-equirectangular",
                    pp::app::DocumentExportExecutionKind::equirectangular_file,
                    prepared.value(),
                    target.path)) {
                const auto exported = export_equirectangular_from_document_snapshot(app_, target, prepared.value());
                if (exported.ok()) {
                    show_export_success_dialog(
                        app_,
                        pp::app::plan_document_export_success_dialog(
                            pp::app::DocumentExportSuccessKind::equirectangular,
                            pp::app::document_export_equirectangular_platform_destination(),
                            app_.work_path));
                    return;
                }

                LOG(
                    "export-equirectangular document export writer retained legacy export after failure: %s",
                    exported.message);
            }
        } else {
            LOG(
                "export-equirectangular document export snapshot bridge retained legacy export after failure: %s",
                prepared.status().message);
        }
        app_.canvas->m_canvas->export_equirectangular(target.path, [app, target] {
#if __WEB__
            app->ui_task([app, target] {
                app->save_prepared_file(target.path, target.suggested_name, [](const std::string&, bool) { });
            });
#else
            (void)target;
            show_export_success_dialog(
                *app,
                pp::app::plan_document_export_success_dialog(
                    pp::app::DocumentExportSuccessKind::equirectangular,
                    pp::app::document_export_equirectangular_platform_destination(),
                    app->work_path));
#endif
        });
    }

    void export_layers_to_stem(const pp::app::DocumentExportStemTarget& target) override
    {
        auto* app = &app_;
        const auto prepared = prepare_legacy_document_export_snapshot(app_, "export-layers");
        if (prepared) {
            const auto collection_target = pp::app::DocumentExportCollectionTarget {
                .stem_path = target.stem_path,
            };
            if (should_use_document_snapshot_writer(
                    "export-layers",
                    pp::app::DocumentExportExecutionKind::layers_stem,
                    prepared.value(),
                    {})) {
                const auto exported = export_layers_from_document_snapshot(app_, collection_target, prepared.value());
                if (exported.ok()) {
                    show_export_success_dialog(
                        app_,
                        pp::app::plan_document_export_success_dialog(
                            pp::app::DocumentExportSuccessKind::layers,
                            pp::app::DocumentExportSuccessDestination::path,
                            target.stem_path));
                    return;
                }

                LOG("export-layers document export writer retained legacy export after failure: %s", exported.message);
            }
        } else {
            LOG(
                "export-layers document export snapshot bridge retained legacy export after failure: %s",
                prepared.status().message);
        }
        app_.canvas->m_canvas->export_layers(target.stem_path, [app, target] {
            show_export_success_dialog(
                *app,
                pp::app::plan_document_export_success_dialog(
                    pp::app::DocumentExportSuccessKind::layers,
                    pp::app::DocumentExportSuccessDestination::path,
                    target.stem_path));
        });
    }

    void export_layers_to_collection(const pp::app::DocumentExportCollectionTarget& target) override
    {
        auto* app = &app_;
        const auto prepared = prepare_legacy_document_export_snapshot(app_, "export-layers");
        if (prepared) {
            if (should_use_document_snapshot_writer(
                    "export-layers",
                    pp::app::DocumentExportExecutionKind::layers_collection,
                    prepared.value(),
                    {})) {
                const auto exported = export_layers_from_document_snapshot(app_, target, prepared.value());
                if (exported.ok()) {
                    show_export_success_dialog(
                        app_,
                        pp::app::plan_document_export_success_dialog(
                            pp::app::DocumentExportSuccessKind::layers,
                            pp::app::DocumentExportSuccessDestination::files_panopainter));
                    return;
                }

                LOG("export-layers document export writer retained legacy export after failure: %s", exported.message);
            }
        } else {
            LOG(
                "export-layers document export snapshot bridge retained legacy export after failure: %s",
                prepared.status().message);
        }
        app_.canvas->m_canvas->export_layers(target.stem_path, [app] {
            show_export_success_dialog(
                *app,
                pp::app::plan_document_export_success_dialog(
                    pp::app::DocumentExportSuccessKind::layers,
                    pp::app::DocumentExportSuccessDestination::files_panopainter));
        });
    }

    void export_animation_frames_to_stem(const pp::app::DocumentExportStemTarget& target) override
    {
        auto* app = &app_;
        const auto prepared = prepare_legacy_document_export_snapshot(app_, "export-animation-frames");
        if (prepared) {
            const auto collection_target = pp::app::DocumentExportCollectionTarget {
                .stem_path = target.stem_path,
            };
            if (should_use_document_snapshot_writer(
                    "export-animation-frames",
                    pp::app::DocumentExportExecutionKind::animation_frames_stem,
                    prepared.value(),
                    {})) {
                const auto exported = export_animation_frames_from_document_snapshot(
                    app_,
                    collection_target,
                    prepared.value());
                if (exported.ok()) {
                    show_export_success_dialog(
                        app_,
                        pp::app::plan_document_export_success_dialog(
                            pp::app::DocumentExportSuccessKind::animation_frames,
                            pp::app::DocumentExportSuccessDestination::path,
                            target.stem_path));
                    return;
                }

                LOG(
                    "export-animation-frames document export writer retained legacy export after failure: %s",
                    exported.message);
            }
        } else {
            LOG(
                "export-animation-frames document export snapshot bridge retained legacy export after failure: %s",
                prepared.status().message);
        }
        app_.canvas->m_canvas->export_anim_frames(target.stem_path, [app, target] {
            show_export_success_dialog(
                *app,
                pp::app::plan_document_export_success_dialog(
                    pp::app::DocumentExportSuccessKind::animation_frames,
                    pp::app::DocumentExportSuccessDestination::path,
                    target.stem_path));
        });
    }

    void export_animation_frames_to_collection(const pp::app::DocumentExportCollectionTarget& target) override
    {
        auto* app = &app_;
        const auto prepared = prepare_legacy_document_export_snapshot(app_, "export-animation-frames");
        if (prepared) {
            if (should_use_document_snapshot_writer(
                    "export-animation-frames",
                    pp::app::DocumentExportExecutionKind::animation_frames_collection,
                    prepared.value(),
                    {})) {
                const auto exported = export_animation_frames_from_document_snapshot(app_, target, prepared.value());
                if (exported.ok()) {
                    show_export_success_dialog(
                        app_,
                        pp::app::plan_document_export_success_dialog(
                            pp::app::DocumentExportSuccessKind::animation_frames,
                            pp::app::DocumentExportSuccessDestination::files_panopainter));
                    return;
                }

                LOG(
                    "export-animation-frames document export writer retained legacy export after failure: %s",
                    exported.message);
            }
        } else {
            LOG(
                "export-animation-frames document export snapshot bridge retained legacy export after failure: %s",
                prepared.status().message);
        }
        app_.canvas->m_canvas->export_anim_frames(target.stem_path, [app] {
            show_export_success_dialog(
                *app,
                pp::app::plan_document_export_success_dialog(
                    pp::app::DocumentExportSuccessKind::animation_frames,
                    pp::app::DocumentExportSuccessDestination::files_panopainter));
        });
    }

    void export_depth(std::string_view document_name) override
    {
        auto* app = &app_;
        const auto target = pp::app::make_document_depth_export_target(app_.work_path, document_name);
        if (target) {
            LOG(
                "export-depth document export target: image=%s depth=%s",
                target.value().image_path.c_str(),
                target.value().depth_path.c_str());
        } else {
            LOG("export-depth document export target planning failed: %s", target.status().message);
        }

        const auto prepared = prepare_legacy_document_export_snapshot(app_, "export-depth");
        if (prepared) {
            if (should_use_document_snapshot_writer(
                    "export-depth",
                    pp::app::DocumentExportExecutionKind::depth,
                    prepared.value(),
                    {})) {
                if (target) {
                    const auto exported = export_depth_from_document_snapshot(app_, target.value(), prepared.value());
                    if (exported.ok()) {
                        show_export_success_dialog(
                            app_,
                            pp::app::plan_document_export_success_dialog(
                                pp::app::DocumentExportSuccessKind::depth,
                                pp::app::document_export_media_platform_destination(),
                                app_.work_path));
                        return;
                    }

                    LOG("export-depth document export writer retained legacy export after failure: %s", exported.message);
                } else {
                    LOG(
                        "export-depth document export writer retained legacy export after target failure: %s",
                        target.status().message);
                }
            }
#if !__WEB__
            const auto plan = pp::paint_renderer::plan_document_depth_export_render(
                pp::paint_renderer::DocumentDepthExportRenderPlanRequest {
                    .document = &prepared.value().snapshot.document,
                    .frame_index = prepared.value().snapshot.document.active_frame_index(),
                });
            if (plan) {
                LOG(
                    "export-depth document export render plan: output=%ux%u mergedFaceDraws=%zu layerDepthDraws=%zu visitedLayers=%zu visibleLayers=%zu facePayloads=%zu requiresRendererReadback=%s",
                    plan.value().output_extent.width,
                    plan.value().output_extent.height,
                    plan.value().merged_face_draw_count,
                    plan.value().layer_depth_draw_count,
                    plan.value().visited_layer_count,
                    plan.value().visible_layer_count,
                    plan.value().face_payload_count,
                    plan.value().requires_renderer_readback ? "true" : "false");
            } else {
                LOG(
                    "export-depth document export render plan retained legacy export after failure: %s",
                    plan.status().message);
            }
#endif
        } else {
            LOG(
                "export-depth document export snapshot bridge retained legacy export after failure: %s",
                prepared.status().message);
        }
        app_.canvas->m_canvas->export_depth(std::string(document_name), [app] {
            show_export_success_dialog(
                *app,
                pp::app::plan_document_export_success_dialog(
                    pp::app::DocumentExportSuccessKind::depth,
                    pp::app::document_export_media_platform_destination(),
                    app->work_path));
        });
    }

    void export_cube_faces(std::string_view document_name) override
    {
        auto* app = &app_;
        const auto prepared = prepare_legacy_document_export_snapshot(app_, "export-cube-faces");
        if (prepared) {
            if (should_use_document_snapshot_writer(
                    "export-cube-faces",
                    pp::app::DocumentExportExecutionKind::cube_faces,
                    prepared.value(),
                    {})) {
                const auto exported = export_cube_faces_from_document_snapshot(app_, document_name, prepared.value());
                if (exported.ok()) {
                    show_export_success_dialog(
                        app_,
                        pp::app::plan_document_export_success_dialog(
                            pp::app::DocumentExportSuccessKind::cube_faces,
                            pp::app::document_export_media_platform_destination(),
                            app_.work_path));
                    return;
                }

                LOG(
                    "export-cube-faces document export writer retained legacy export after failure: %s",
                    exported.message);
            }
        } else {
            LOG(
                "export-cube-faces document export snapshot bridge retained legacy export after failure: %s",
                prepared.status().message);
        }

        app_.canvas->m_canvas->export_cube_faces(std::string(document_name), [app] {
            show_export_success_dialog(
                *app,
                pp::app::plan_document_export_success_dialog(
                    pp::app::DocumentExportSuccessKind::cube_faces,
                    pp::app::document_export_media_platform_destination(),
                    app->work_path));
        });
    }

private:
    App& app_;
};

class LegacyDocumentVideoExportServices final : public pp::app::DocumentVideoExportServices {
public:
    LegacyDocumentVideoExportServices(App& app, bool asynchronous) noexcept
        : app_(app)
        , asynchronous_(asynchronous)
    {
    }

    void export_animation_mp4(std::string_view path) override
    {
        auto* app = &app_;
        auto path_string = std::string(path);
        if (asynchronous_) {
            Canvas::I->export_anim_mp4(path_string, [app, path_string] {
                show_export_success_dialog(
                    *app,
                    pp::app::plan_document_export_success_dialog(
                        pp::app::DocumentExportSuccessKind::animation_mp4,
                        pp::app::DocumentExportSuccessDestination::path,
                        path_string));
            });
            return;
        }

        Canvas::I->export_anim_mp4(path_string, [app] {
            show_export_success_dialog(
                *app,
                pp::app::plan_document_export_success_dialog(
                    pp::app::DocumentExportSuccessKind::animation_mp4,
                    pp::app::DocumentExportSuccessDestination::generic_success));
        });
    }

    void export_timelapse_mp4(std::string_view path) override
    {
        auto* app = &app_;
        auto path_string = std::string(path);
        if (asynchronous_) {
            document_video_export_worker().post([app, path_string = std::move(path_string)]() mutable {
                BT_SetTerminate();
                app->rec_export(path_string);
                app->ui_task([app, path_string = std::move(path_string)]() mutable {
                    show_export_success_dialog(
                        *app,
                        pp::app::plan_document_export_success_dialog(
                            pp::app::DocumentExportSuccessKind::timelapse,
                            pp::app::DocumentExportSuccessDestination::path,
                            path_string));
                });
            });
            return;
        }

        app->rec_export(path_string);
    }

    void show_animation_export_success(std::string_view path) override
    {
        (void)path;
    }

    void show_timelapse_export_success(std::string_view path) override
    {
        if (asynchronous_) {
            (void)path;
        } else {
            (void)path;
            show_export_success_dialog(
                app_,
                pp::app::plan_document_export_success_dialog(
                    pp::app::DocumentExportSuccessKind::timelapse,
                    pp::app::DocumentExportSuccessDestination::generic_success));
        }
    }

private:
    App& app_;
    bool asynchronous_ = false;
};

} // namespace

pp::foundation::Status execute_legacy_document_export_file(
    App& app,
    const pp::app::DocumentExportFileTarget& target)
{
    LegacyDocumentExportServices services(app);
    return pp::app::execute_document_export_file(target, services);
}

pp::foundation::Status execute_legacy_document_export_stem(
    App& app,
    pp::app::DocumentExportCollectionKind kind,
    const pp::app::DocumentExportStemTarget& target)
{
    LegacyDocumentExportServices services(app);
    return pp::app::execute_document_export_stem(kind, target, services);
}

pp::foundation::Status execute_legacy_document_export_collection(
    App& app,
    pp::app::DocumentExportCollectionKind kind,
    const pp::app::DocumentExportCollectionTarget& target)
{
    LegacyDocumentExportServices services(app);
    return pp::app::execute_document_export_collection(kind, target, services);
}

pp::foundation::Status execute_legacy_document_export_depth(
    App& app,
    std::string_view document_name)
{
    LegacyDocumentExportServices services(app);
    return pp::app::execute_document_export_depth(document_name, services);
}

pp::foundation::Status execute_legacy_document_export_cube_faces(
    App& app,
    std::string_view document_name)
{
    LegacyDocumentExportServices services(app);
    return pp::app::execute_document_export_cube_faces(document_name, services);
}

pp::foundation::Status execute_legacy_document_video_export(
    App& app,
    pp::app::DocumentVideoExportKind kind,
    std::string_view path,
    bool asynchronous)
{
    LegacyDocumentVideoExportServices services(app, asynchronous);
    return pp::app::execute_document_video_export(kind, path, services);
}

} // namespace pp::panopainter
