#pragma once

#include "foundation/result.h"

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <limits>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

namespace pp::app {

inline constexpr int document_animation_default_frame_duration = 1;

enum class DocumentAnimationOperation {
    add_frame,
    duplicate_frame,
    remove_frame,
    adjust_duration,
    move_frame,
    select_frame,
    goto_frame,
    goto_next,
    goto_previous,
    playback_step,
    toggle_playback,
    set_onion_size,
};

enum class DocumentAnimationPanelAction {
    goto_frame,
    next_frame,
    previous_frame,
    playback_step,
    toggle_playback,
};

struct DocumentAnimationPanelState {
    int total_duration = 1;
    int current_frame = 0;
    bool playback_active = false;
};

struct DocumentAnimationOperationPlan {
    DocumentAnimationOperation operation = DocumentAnimationOperation::goto_frame;
    int frame_count = 1;
    int current_frame = 0;
    int selected_frame = 0;
    int target_frame = 0;
    int frame_duration = document_animation_default_frame_duration;
    int duration_delta = 0;
    int move_offset = 0;
    int onion_size = 1;
    int layer_index = 0;
    std::uint32_t layer_id = 0;
    int playback_idle_ms = 100;
    bool requires_selected_frame = false;
    bool mutates_document = false;
    bool reloads_animation_layers = false;
    bool updates_canvas_animation = false;
    bool marks_unsaved = false;
    bool playback_was_active = false;
    bool playback_active = false;
    bool resets_playback_timer = false;
};

struct DocumentAnimationOnionFrameRange {
    int frame_count = 1;
    int current_frame = 0;
    int onion_size = 0;
    int first_frame = 0;
    int last_frame = 0;
};

inline constexpr float document_animation_timeline_frame_width = 35.0F;

struct DocumentAnimationTimelineScrubPlan {
    int total_duration = 1;
    float cursor_x = 0.0F;
    float frame_width = document_animation_timeline_frame_width;
    int target_frame = 0;
};

struct DocumentAnimationLayerInput {
    int layer_index = 0;
    std::uint32_t layer_id = 0;
    std::string name;
    bool visible = true;
    std::vector<int> frame_durations;
};

struct DocumentAnimationFrameView {
    int frame_index = 0;
    int duration = document_animation_default_frame_duration;
    bool selected = false;
};

struct DocumentAnimationLayerView {
    int layer_index = 0;
    std::uint32_t layer_id = 0;
    std::string name;
    bool visible = true;
    bool current = false;
    std::vector<DocumentAnimationFrameView> frames;
};

struct DocumentAnimationPanelView {
    int total_duration = 1;
    int current_frame = 0;
    int onion_size = 0;
    std::uint32_t selected_layer_id = 0;
    int selected_frame = -1;
    bool has_selected_frame = false;
    std::vector<DocumentAnimationLayerView> layers;
};

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

    virtual void add_frame() = 0;
    virtual void duplicate_frame(int selected_frame) = 0;
    virtual void remove_frame(int selected_frame, int target_frame) = 0;
    virtual void set_frame_duration(int selected_frame, int duration) = 0;
    virtual int move_frame(int selected_frame, int move_offset) = 0;
    virtual void select_frame(std::uint32_t layer_id, int layer_index, int selected_frame) = 0;
    virtual void select_layer(int layer_index) = 0;
    virtual void goto_frame(int target_frame) = 0;
    virtual void set_timeline_frame(int target_frame) = 0;
    virtual void set_onion_size(int onion_size) = 0;
    virtual void capture_playback_restore_mode() = 0;
    virtual void enter_playback_camera_mode() = 0;
    virtual void restore_playback_canvas_mode() = 0;
    virtual void set_playback_active(bool active) = 0;
    virtual void reset_playback_timer() = 0;
    virtual void set_playback_idle_ms(int idle_ms) = 0;
    virtual void update_canvas_animation() = 0;
    virtual void update_frame_status() = 0;
    virtual void reload_animation_layers() = 0;
    virtual void mark_unsaved() = 0;
};

[[nodiscard]] inline pp::foundation::Status validate_animation_frame_count(int frame_count) noexcept
{
    if (frame_count <= 0) {
        return pp::foundation::Status::invalid_argument("animation layer must contain at least one frame");
    }

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

[[nodiscard]] inline pp::foundation::Status validate_animation_frame_index(
    int frame_count,
    int index) noexcept
{
    const auto count_status = validate_animation_frame_count(frame_count);
    if (!count_status.ok()) {
        return count_status;
    }

    if (index < 0 || index >= frame_count) {
        return pp::foundation::Status::out_of_range("animation frame index is outside the layer");
    }

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

[[nodiscard]] inline pp::foundation::Status validate_animation_frame_duration(int duration) noexcept
{
    if (duration < 1) {
        return pp::foundation::Status::invalid_argument("animation frame duration must be at least 1");
    }

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

[[nodiscard]] inline pp::foundation::Result<DocumentAnimationPanelView> plan_animation_panel_view(
    const std::vector<DocumentAnimationLayerInput>& layers,
    int total_duration,
    int current_layer_index,
    int current_frame,
    std::uint32_t selected_layer_id,
    int selected_frame,
    int onion_size)
{
    if (layers.empty()) {
        return pp::foundation::Result<DocumentAnimationPanelView>::failure(
            pp::foundation::Status::invalid_argument("animation panel requires at least one layer"));
    }

    const auto timeline_status = validate_animation_frame_index(total_duration, current_frame);
    if (!timeline_status.ok()) {
        return pp::foundation::Result<DocumentAnimationPanelView>::failure(timeline_status);
    }

    if (current_layer_index < 0 || current_layer_index >= static_cast<int>(layers.size())) {
        return pp::foundation::Result<DocumentAnimationPanelView>::failure(
            pp::foundation::Status::out_of_range("current animation layer index is outside the document"));
    }

    if (onion_size < 0) {
        return pp::foundation::Result<DocumentAnimationPanelView>::failure(
            pp::foundation::Status::invalid_argument("animation onion size must not be negative"));
    }

    DocumentAnimationPanelView view;
    view.total_duration = total_duration;
    view.current_frame = current_frame;
    view.onion_size = onion_size;
    view.selected_layer_id = selected_layer_id;
    view.selected_frame = selected_frame;
    view.layers.reserve(layers.size());

    for (std::size_t i = 0; i < layers.size(); ++i) {
        const auto& input = layers[i];
        if (input.layer_index < 0) {
            return pp::foundation::Result<DocumentAnimationPanelView>::failure(
                pp::foundation::Status::out_of_range("animation layer index must not be negative"));
        }
        if (input.frame_durations.empty()) {
            return pp::foundation::Result<DocumentAnimationPanelView>::failure(
                pp::foundation::Status::invalid_argument("animation layer must contain at least one frame"));
        }

        DocumentAnimationLayerView layer;
        layer.layer_index = input.layer_index;
        layer.layer_id = input.layer_id;
        layer.name = input.name;
        layer.visible = input.visible;
        layer.current = input.layer_index == current_layer_index;
        layer.frames.reserve(input.frame_durations.size());

        for (std::size_t frame_index = 0; frame_index < input.frame_durations.size(); ++frame_index) {
            const int duration = input.frame_durations[frame_index];
            const auto duration_status = validate_animation_frame_duration(duration);
            if (!duration_status.ok()) {
                return pp::foundation::Result<DocumentAnimationPanelView>::failure(duration_status);
            }

            const bool selected = selected_frame >= 0
                && input.layer_id == selected_layer_id
                && static_cast<int>(frame_index) == selected_frame;
            view.has_selected_frame = view.has_selected_frame || selected;
            layer.frames.push_back(DocumentAnimationFrameView {
                .frame_index = static_cast<int>(frame_index),
                .duration = duration,
                .selected = selected,
            });
        }

        view.layers.push_back(std::move(layer));
    }

    return pp::foundation::Result<DocumentAnimationPanelView>::success(std::move(view));
}

[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOnionFrameRange> plan_animation_onion_frame_range(
    int frame_count,
    int current_frame,
    int onion_size)
{
    const auto index_status = validate_animation_frame_index(frame_count, current_frame);
    if (!index_status.ok()) {
        return pp::foundation::Result<DocumentAnimationOnionFrameRange>::failure(index_status);
    }

    if (onion_size < 0) {
        return pp::foundation::Result<DocumentAnimationOnionFrameRange>::failure(
            pp::foundation::Status::invalid_argument("animation onion size must not be negative"));
    }

    const auto first = std::max<std::int64_t>(
        static_cast<std::int64_t>(current_frame) - static_cast<std::int64_t>(onion_size),
        0);
    const auto last = std::min<std::int64_t>(
        static_cast<std::int64_t>(current_frame) + static_cast<std::int64_t>(onion_size),
        static_cast<std::int64_t>(frame_count) - 1);

    return pp::foundation::Result<DocumentAnimationOnionFrameRange>::success(
        DocumentAnimationOnionFrameRange {
            .frame_count = frame_count,
            .current_frame = current_frame,
            .onion_size = onion_size,
            .first_frame = static_cast<int>(first),
            .last_frame = static_cast<int>(last),
        });
}

[[nodiscard]] inline pp::foundation::Result<DocumentAnimationTimelineScrubPlan> plan_animation_timeline_scrub(
    int total_duration,
    float cursor_x,
    float frame_width = document_animation_timeline_frame_width)
{
    if (total_duration <= 0) {
        return pp::foundation::Result<DocumentAnimationTimelineScrubPlan>::failure(
            pp::foundation::Status::invalid_argument("animation timeline duration must be greater than zero"));
    }

    if (!std::isfinite(cursor_x)) {
        return pp::foundation::Result<DocumentAnimationTimelineScrubPlan>::failure(
            pp::foundation::Status::invalid_argument("animation timeline cursor position must be finite"));
    }

    if (!std::isfinite(frame_width) || frame_width <= 0.0F) {
        return pp::foundation::Result<DocumentAnimationTimelineScrubPlan>::failure(
            pp::foundation::Status::invalid_argument("animation timeline frame width must be positive and finite"));
    }

    const auto raw_frame = static_cast<std::int64_t>(std::floor(cursor_x / frame_width));
    const auto target_frame = std::clamp<std::int64_t>(raw_frame, 0, total_duration - 1);
    return pp::foundation::Result<DocumentAnimationTimelineScrubPlan>::success(
        DocumentAnimationTimelineScrubPlan {
            .total_duration = total_duration,
            .cursor_x = cursor_x,
            .frame_width = frame_width,
            .target_frame = static_cast<int>(target_frame),
        });
}

[[nodiscard]] inline float animation_onion_frame_alpha(
    const DocumentAnimationOnionFrameRange& range,
    int frame) noexcept
{
    if (frame < range.first_frame || frame > range.last_frame) {
        return 0.0f;
    }

    const int distance = frame >= range.current_frame
        ? frame - range.current_frame
        : range.current_frame - frame;
    if (distance > range.onion_size) {
        return 0.0f;
    }

    return 1.0f - static_cast<float>(distance) / static_cast<float>(range.onion_size + 1);
}

[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_add_frame(
    int frame_count,
    int current_frame)
{
    const auto count_status = validate_animation_frame_count(frame_count);
    if (!count_status.ok()) {
        return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(count_status);
    }

    if (current_frame < 0) {
        return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
            pp::foundation::Status::out_of_range("current animation frame must not be negative"));
    }

    DocumentAnimationOperationPlan plan;
    plan.operation = DocumentAnimationOperation::add_frame;
    plan.frame_count = frame_count;
    plan.current_frame = current_frame;
    plan.selected_frame = frame_count;
    plan.target_frame = current_frame;
    plan.mutates_document = true;
    plan.reloads_animation_layers = true;
    plan.updates_canvas_animation = true;
    plan.marks_unsaved = true;
    return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_duplicate_frame(
    int frame_count,
    int selected_frame)
{
    const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
    if (!index_status.ok()) {
        return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
    }

    DocumentAnimationOperationPlan plan;
    plan.operation = DocumentAnimationOperation::duplicate_frame;
    plan.frame_count = frame_count;
    plan.selected_frame = selected_frame;
    plan.target_frame = selected_frame + 1;
    plan.requires_selected_frame = true;
    plan.mutates_document = true;
    plan.reloads_animation_layers = true;
    plan.updates_canvas_animation = true;
    plan.marks_unsaved = true;
    return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_remove_frame(
    int frame_count,
    int selected_frame)
{
    const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
    if (!index_status.ok()) {
        return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
    }

    if (frame_count <= 1) {
        return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
            pp::foundation::Status::invalid_argument("animation layer must keep at least one frame"));
    }

    DocumentAnimationOperationPlan plan;
    plan.operation = DocumentAnimationOperation::remove_frame;
    plan.frame_count = frame_count;
    plan.selected_frame = selected_frame;
    plan.target_frame = std::min(selected_frame, frame_count - 2);
    plan.requires_selected_frame = true;
    plan.mutates_document = true;
    plan.reloads_animation_layers = true;
    plan.updates_canvas_animation = true;
    plan.marks_unsaved = true;
    return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_adjust_duration(
    int frame_count,
    int selected_frame,
    int current_duration,
    int delta)
{
    const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
    if (!index_status.ok()) {
        return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
    }

    const auto duration_status = validate_animation_frame_duration(current_duration);
    if (!duration_status.ok()) {
        return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(duration_status);
    }

    if (delta == 0) {
        return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
            pp::foundation::Status::invalid_argument("animation frame duration delta must not be zero"));
    }

    if (delta > 0 && current_duration > std::numeric_limits<int>::max() - delta) {
        return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
            pp::foundation::Status::out_of_range("animation frame duration would overflow"));
    }

    DocumentAnimationOperationPlan plan;
    plan.operation = DocumentAnimationOperation::adjust_duration;
    plan.frame_count = frame_count;
    plan.selected_frame = selected_frame;
    plan.target_frame = selected_frame;
    plan.frame_duration = std::max(current_duration + delta, 1);
    plan.duration_delta = delta;
    plan.requires_selected_frame = true;
    plan.mutates_document = plan.frame_duration != current_duration;
    plan.reloads_animation_layers = plan.mutates_document;
    plan.updates_canvas_animation = plan.mutates_document;
    plan.marks_unsaved = plan.mutates_document;
    return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_move_frame(
    int frame_count,
    int selected_frame,
    int offset)
{
    const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
    if (!index_status.ok()) {
        return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
    }

    if (offset == 0) {
        return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
            pp::foundation::Status::invalid_argument("animation frame move offset must not be zero"));
    }

    DocumentAnimationOperationPlan plan;
    plan.operation = DocumentAnimationOperation::move_frame;
    plan.frame_count = frame_count;
    plan.selected_frame = selected_frame;
    const auto unclamped_target = static_cast<std::int64_t>(selected_frame) + static_cast<std::int64_t>(offset);
    plan.target_frame = static_cast<int>(std::clamp<std::int64_t>(unclamped_target, 0, frame_count - 1));
    plan.move_offset = offset;
    plan.requires_selected_frame = true;
    plan.mutates_document = plan.target_frame != selected_frame;
    plan.reloads_animation_layers = true;
    plan.updates_canvas_animation = true;
    plan.marks_unsaved = plan.mutates_document;
    return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_select_frame(
    int frame_count,
    int layer_index,
    std::uint32_t layer_id,
    int selected_frame)
{
    const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
    if (!index_status.ok()) {
        return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
    }

    if (layer_index < 0) {
        return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
            pp::foundation::Status::out_of_range("animation layer index must not be negative"));
    }

    DocumentAnimationOperationPlan plan;
    plan.operation = DocumentAnimationOperation::select_frame;
    plan.frame_count = frame_count;
    plan.selected_frame = selected_frame;
    plan.target_frame = selected_frame;
    plan.layer_index = layer_index;
    plan.layer_id = layer_id;
    plan.requires_selected_frame = true;
    plan.updates_canvas_animation = true;
    return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_goto_frame(
    int total_duration,
    int frame)
{
    if (total_duration <= 0) {
        return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
            pp::foundation::Status::invalid_argument("animation duration must be greater than zero"));
    }

    if (frame < 0 || frame >= total_duration) {
        return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
            pp::foundation::Status::out_of_range("animation timeline frame is outside the document"));
    }

    DocumentAnimationOperationPlan plan;
    plan.operation = DocumentAnimationOperation::goto_frame;
    plan.frame_count = total_duration;
    plan.current_frame = frame;
    plan.target_frame = frame;
    plan.reloads_animation_layers = true;
    plan.updates_canvas_animation = true;
    return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_step_frame(
    int total_duration,
    int current_frame,
    int offset)
{
    const auto current_status = plan_animation_goto_frame(total_duration, current_frame);
    if (!current_status) {
        return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(current_status.status());
    }

    if (offset == 0) {
        return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
            pp::foundation::Status::invalid_argument("animation frame step offset must not be zero"));
    }

    DocumentAnimationOperationPlan plan;
    plan.operation = offset > 0 ? DocumentAnimationOperation::goto_next : DocumentAnimationOperation::goto_previous;
    plan.frame_count = total_duration;
    plan.current_frame = current_frame;
    auto target = (static_cast<std::int64_t>(current_frame) + static_cast<std::int64_t>(offset))
        % static_cast<std::int64_t>(total_duration);
    if (target < 0) {
        target += total_duration;
    }
    plan.target_frame = static_cast<int>(target);
    plan.updates_canvas_animation = true;
    plan.reloads_animation_layers = true;
    return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_playback_step(
    int total_duration,
    int current_frame,
    int offset)
{
    const auto step = plan_animation_step_frame(total_duration, current_frame, offset);
    if (!step) {
        return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(step.status());
    }

    auto plan = step.value();
    plan.operation = DocumentAnimationOperation::playback_step;
    plan.move_offset = offset;
    plan.reloads_animation_layers = false;
    return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_playback_toggle(
    bool playback_active)
{
    DocumentAnimationOperationPlan plan;
    plan.operation = DocumentAnimationOperation::toggle_playback;
    plan.playback_was_active = playback_active;
    plan.playback_active = !playback_active;
    plan.playback_idle_ms = playback_active ? 100 : 10;
    plan.resets_playback_timer = !playback_active;
    return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_onion_size(int onion_size)
{
    if (onion_size < 0) {
        return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
            pp::foundation::Status::invalid_argument("animation onion size must not be negative"));
    }

    DocumentAnimationOperationPlan plan;
    plan.operation = DocumentAnimationOperation::set_onion_size;
    plan.onion_size = onion_size;
    plan.updates_canvas_animation = true;
    return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_panel_action(
    DocumentAnimationPanelAction action,
    const DocumentAnimationPanelState& state,
    int target_frame = 0)
{
    switch (action) {
    case DocumentAnimationPanelAction::goto_frame:
        return plan_animation_goto_frame(state.total_duration, target_frame);

    case DocumentAnimationPanelAction::next_frame:
        return plan_animation_step_frame(state.total_duration, state.current_frame, 1);

    case DocumentAnimationPanelAction::previous_frame:
        return plan_animation_step_frame(state.total_duration, state.current_frame, -1);

    case DocumentAnimationPanelAction::playback_step:
        return plan_animation_playback_step(state.total_duration, state.current_frame, 1);

    case DocumentAnimationPanelAction::toggle_playback:
        return plan_animation_playback_toggle(state.playback_active);
    }

    return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
        pp::foundation::Status::invalid_argument("unknown animation panel action"));
}

[[nodiscard]] inline pp::foundation::Status validate_animation_operation_plan(
    const DocumentAnimationOperationPlan& plan) noexcept
{
    switch (plan.operation) {
    case DocumentAnimationOperation::add_frame:
        if (!plan.mutates_document || !plan.marks_unsaved) {
            return pp::foundation::Status::invalid_argument("animation add plan must mutate the document");
        }
        return validate_animation_frame_count(plan.frame_count);

    case DocumentAnimationOperation::duplicate_frame:
    case DocumentAnimationOperation::remove_frame:
        if (!plan.requires_selected_frame || !plan.mutates_document || !plan.marks_unsaved) {
            return pp::foundation::Status::invalid_argument("animation selected-frame plan must mutate the document");
        }
        if (plan.operation == DocumentAnimationOperation::remove_frame && plan.frame_count <= 1) {
            return pp::foundation::Status::invalid_argument("animation layer must keep at least one frame");
        }
        return validate_animation_frame_index(plan.frame_count, plan.selected_frame);

    case DocumentAnimationOperation::adjust_duration:
        if (!plan.requires_selected_frame) {
            return pp::foundation::Status::invalid_argument("animation duration plan must require a selected frame");
        }
        {
            const auto index_status = validate_animation_frame_index(plan.frame_count, plan.selected_frame);
            if (!index_status.ok()) {
                return index_status;
            }
        }
        return validate_animation_frame_duration(plan.frame_duration);

    case DocumentAnimationOperation::move_frame:
        if (!plan.requires_selected_frame || plan.move_offset == 0) {
            return pp::foundation::Status::invalid_argument("animation move plan must require selected frame and non-zero offset");
        }
        return validate_animation_frame_index(plan.frame_count, plan.selected_frame);

    case DocumentAnimationOperation::select_frame:
        if (!plan.requires_selected_frame || !plan.updates_canvas_animation || plan.layer_index < 0) {
            return pp::foundation::Status::invalid_argument("animation frame select plan has invalid state");
        }
        {
            const auto index_status = validate_animation_frame_index(plan.frame_count, plan.selected_frame);
            if (!index_status.ok()) {
                return index_status;
            }
        }
        return validate_animation_frame_index(plan.frame_count, plan.target_frame);

    case DocumentAnimationOperation::goto_frame:
    case DocumentAnimationOperation::goto_next:
    case DocumentAnimationOperation::goto_previous:
    case DocumentAnimationOperation::playback_step:
        if (!plan.updates_canvas_animation) {
            return pp::foundation::Status::invalid_argument("animation goto plan must update canvas animation");
        }
        if (plan.operation == DocumentAnimationOperation::playback_step && plan.move_offset == 0) {
            return pp::foundation::Status::invalid_argument("animation playback step offset must not be zero");
        }
        return validate_animation_frame_index(plan.frame_count, plan.target_frame);

    case DocumentAnimationOperation::toggle_playback:
        if (plan.playback_active == plan.playback_was_active) {
            return pp::foundation::Status::invalid_argument("animation playback toggle must change state");
        }
        if (plan.playback_idle_ms <= 0) {
            return pp::foundation::Status::invalid_argument("animation playback idle interval must be positive");
        }
        if (plan.playback_active && !plan.resets_playback_timer) {
            return pp::foundation::Status::invalid_argument("animation playback start must reset timer");
        }
        return pp::foundation::Status::success();

    case DocumentAnimationOperation::set_onion_size:
        if (plan.onion_size < 0) {
            return pp::foundation::Status::invalid_argument("animation onion size must not be negative");
        }
        if (!plan.updates_canvas_animation) {
            return pp::foundation::Status::invalid_argument("animation onion plan must update canvas animation");
        }
        return pp::foundation::Status::success();
    }

    return pp::foundation::Status::invalid_argument("unknown animation operation");
}

[[nodiscard]] inline pp::foundation::Status execute_animation_operation_plan(
    const DocumentAnimationOperationPlan& plan,
    DocumentAnimationServices& services)
{
    const auto validation = validate_animation_operation_plan(plan);
    if (!validation.ok()) {
        return validation;
    }

    switch (plan.operation) {
    case DocumentAnimationOperation::add_frame:
        services.add_frame();
        services.mark_unsaved();
        services.update_canvas_animation();
        services.reload_animation_layers();
        return pp::foundation::Status::success();

    case DocumentAnimationOperation::duplicate_frame:
        services.duplicate_frame(plan.selected_frame);
        services.mark_unsaved();
        services.update_canvas_animation();
        services.reload_animation_layers();
        return pp::foundation::Status::success();

    case DocumentAnimationOperation::remove_frame:
        services.remove_frame(plan.selected_frame, plan.target_frame);
        services.mark_unsaved();
        if (plan.updates_canvas_animation) {
            services.goto_frame(plan.target_frame);
        }
        if (plan.reloads_animation_layers) {
            services.reload_animation_layers();
        }
        return pp::foundation::Status::success();

    case DocumentAnimationOperation::adjust_duration:
        if (plan.mutates_document) {
            services.set_frame_duration(plan.selected_frame, plan.frame_duration);
            services.mark_unsaved();
            if (plan.updates_canvas_animation) {
                services.update_canvas_animation();
            }
            if (plan.reloads_animation_layers) {
                services.reload_animation_layers();
            }
        }
        return pp::foundation::Status::success();

    case DocumentAnimationOperation::move_frame:
    {
        const auto actual_target_frame = services.move_frame(plan.selected_frame, plan.move_offset);
        if (plan.marks_unsaved) {
            services.mark_unsaved();
        }
        if (plan.updates_canvas_animation) {
            services.goto_frame(actual_target_frame);
        }
        if (plan.reloads_animation_layers) {
            services.reload_animation_layers();
        }
        return pp::foundation::Status::success();
    }
    case DocumentAnimationOperation::goto_frame:
    case DocumentAnimationOperation::goto_next:
    case DocumentAnimationOperation::goto_previous:
        services.goto_frame(plan.target_frame);
        if (plan.reloads_animation_layers) {
            services.reload_animation_layers();
        }
        return pp::foundation::Status::success();

    case DocumentAnimationOperation::select_frame:
        services.select_frame(plan.layer_id, plan.layer_index, plan.selected_frame);
        if (plan.updates_canvas_animation) {
            services.goto_frame(plan.target_frame);
        }
        services.select_layer(plan.layer_index);
        return pp::foundation::Status::success();

    case DocumentAnimationOperation::playback_step:
        services.goto_frame(plan.target_frame);
        services.set_timeline_frame(plan.target_frame);
        services.update_frame_status();
        return pp::foundation::Status::success();

    case DocumentAnimationOperation::toggle_playback:
        if (plan.playback_active) {
            services.capture_playback_restore_mode();
            services.enter_playback_camera_mode();
            if (plan.resets_playback_timer) {
                services.reset_playback_timer();
            }
        } else {
            services.restore_playback_canvas_mode();
        }
        services.set_playback_active(plan.playback_active);
        services.set_playback_idle_ms(plan.playback_idle_ms);
        return pp::foundation::Status::success();

    case DocumentAnimationOperation::set_onion_size:
        services.set_onion_size(plan.onion_size);
        if (plan.updates_canvas_animation) {
            services.update_canvas_animation();
        }
        return pp::foundation::Status::success();
    }

    return pp::foundation::Status::invalid_argument("unknown animation operation");
}

} // namespace pp::app
