#pragma once

#include "foundation/result.h"

#include <algorithm>
#include <cstdint>
#include <limits>

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,
    goto_frame,
    goto_next,
    goto_previous,
    set_onion_size,
};

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;
    bool requires_selected_frame = false;
    bool mutates_document = false;
    bool reloads_animation_layers = false;
    bool updates_canvas_animation = false;
    bool marks_unsaved = false;
};

[[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<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_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_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);
}

} // namespace pp::app
