#pragma once

#include "foundation/result.h"

#include <cmath>
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
#include <utility>

namespace pp::app {

inline constexpr std::size_t document_layer_name_max_length = 128;
inline constexpr int document_layer_legacy_blend_mode_count = 5;

enum class DocumentLayerRenameAction {
    no_op_same_name,
    rename_and_record_undo,
};

enum class DocumentLayerOperation {
    add,
    duplicate,
    select,
    reorder,
    remove,
    set_opacity,
    set_visibility,
    set_alpha_lock,
    set_blend_mode,
    set_highlight,
};

enum class DocumentLayerMenuCommand {
    clear,
    rename,
    merge_down,
};

enum class DocumentLayerMenuAction {
    clear_current_layer,
    show_rename_dialog,
    merge_with_lower_layer,
    show_merge_animated_not_supported,
    no_op_select_layer,
    no_op_select_upper_layer,
};

struct DocumentLayerRenamePlan {
    std::string old_name;
    std::string new_name;
    DocumentLayerRenameAction action = DocumentLayerRenameAction::no_op_same_name;
};

struct DocumentLayerOperationPlan {
    DocumentLayerOperation operation = DocumentLayerOperation::select;
    int index = 0;
    int from_index = 0;
    int to_index = 0;
    int insert_index = 0;
    int source_index = 0;
    std::string name;
    float opacity = 1.0F;
    bool flag = false;
    int blend_mode = 0;
    bool mutates_document = false;
    bool marks_unsaved = false;
    bool reloads_animation_layers = false;
    bool updates_title = false;
};

struct DocumentLayerMenuPlan {
    DocumentLayerMenuCommand command = DocumentLayerMenuCommand::clear;
    DocumentLayerMenuAction action = DocumentLayerMenuAction::clear_current_layer;
    std::string label;
    int from_index = 0;
    int to_index = 0;
};

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

    virtual void clear_current_layer() = 0;
    virtual void show_rename_dialog() = 0;
    virtual void merge_with_lower_layer(int from_index, int to_index) = 0;
    virtual void show_merge_animated_not_supported() = 0;
};

[[nodiscard]] inline pp::foundation::Status validate_layer_index(
    int layer_count,
    int index) noexcept
{
    if (layer_count <= 0) {
        return pp::foundation::Status::invalid_argument("document must contain at least one layer");
    }

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

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

[[nodiscard]] inline pp::foundation::Status validate_layer_insert_index(
    int layer_count,
    int index) noexcept
{
    if (layer_count < 0) {
        return pp::foundation::Status::invalid_argument("layer count must not be negative");
    }

    if (index < 0 || index > layer_count) {
        return pp::foundation::Status::out_of_range("layer insert index is outside the document");
    }

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

[[nodiscard]] inline pp::foundation::Result<DocumentLayerRenamePlan> plan_document_layer_rename(
    std::string_view old_name,
    std::string_view requested_name)
{
    if (requested_name.empty()) {
        return pp::foundation::Result<DocumentLayerRenamePlan>::failure(
            pp::foundation::Status::invalid_argument("layer name must not be empty"));
    }

    if (requested_name.size() > document_layer_name_max_length) {
        return pp::foundation::Result<DocumentLayerRenamePlan>::failure(
            pp::foundation::Status::out_of_range("layer name length exceeds the configured limit"));
    }

    DocumentLayerRenamePlan plan;
    plan.old_name = std::string(old_name);
    plan.new_name = std::string(requested_name);
    plan.action = old_name == requested_name
        ? DocumentLayerRenameAction::no_op_same_name
        : DocumentLayerRenameAction::rename_and_record_undo;
    return pp::foundation::Result<DocumentLayerRenamePlan>::success(std::move(plan));
}

[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_add(
    int layer_count,
    int insert_index,
    std::string_view name)
{
    const auto index_status = validate_layer_insert_index(layer_count, insert_index);
    if (!index_status.ok()) {
        return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
    }

    const auto rename = plan_document_layer_rename({}, name);
    if (!rename) {
        return pp::foundation::Result<DocumentLayerOperationPlan>::failure(rename.status());
    }

    DocumentLayerOperationPlan plan;
    plan.operation = DocumentLayerOperation::add;
    plan.insert_index = insert_index;
    plan.name = std::string(name);
    plan.mutates_document = true;
    plan.marks_unsaved = true;
    plan.reloads_animation_layers = true;
    plan.updates_title = true;
    return pp::foundation::Result<DocumentLayerOperationPlan>::success(std::move(plan));
}

[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_duplicate(
    int layer_count,
    int source_index)
{
    const auto index_status = validate_layer_index(layer_count, source_index);
    if (!index_status.ok()) {
        return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
    }

    DocumentLayerOperationPlan plan;
    plan.operation = DocumentLayerOperation::duplicate;
    plan.source_index = source_index;
    plan.insert_index = source_index + 1;
    plan.mutates_document = true;
    plan.marks_unsaved = true;
    plan.reloads_animation_layers = true;
    plan.updates_title = true;
    return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_select(
    int layer_count,
    int index)
{
    const auto index_status = validate_layer_index(layer_count, index);
    if (!index_status.ok()) {
        return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
    }

    DocumentLayerOperationPlan plan;
    plan.operation = DocumentLayerOperation::select;
    plan.index = index;
    plan.reloads_animation_layers = true;
    return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_reorder(
    int layer_count,
    int from_index,
    int to_index)
{
    auto index_status = validate_layer_index(layer_count, from_index);
    if (!index_status.ok()) {
        return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
    }

    index_status = validate_layer_index(layer_count, to_index);
    if (!index_status.ok()) {
        return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
    }

    DocumentLayerOperationPlan plan;
    plan.operation = DocumentLayerOperation::reorder;
    plan.from_index = from_index;
    plan.to_index = to_index;
    plan.mutates_document = from_index != to_index;
    plan.marks_unsaved = plan.mutates_document;
    plan.reloads_animation_layers = plan.mutates_document;
    plan.updates_title = plan.mutates_document;
    return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_remove(
    int layer_count,
    int index)
{
    const auto index_status = validate_layer_index(layer_count, index);
    if (!index_status.ok()) {
        return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
    }

    if (layer_count <= 1) {
        return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
            pp::foundation::Status::invalid_argument("document must keep at least one layer"));
    }

    DocumentLayerOperationPlan plan;
    plan.operation = DocumentLayerOperation::remove;
    plan.index = index;
    plan.mutates_document = true;
    plan.marks_unsaved = true;
    plan.reloads_animation_layers = true;
    plan.updates_title = true;
    return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_opacity(
    int layer_count,
    int index,
    float opacity)
{
    const auto index_status = validate_layer_index(layer_count, index);
    if (!index_status.ok()) {
        return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
    }

    if (!std::isfinite(opacity) || opacity < 0.0F || opacity > 1.0F) {
        return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
            pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1"));
    }

    DocumentLayerOperationPlan plan;
    plan.operation = DocumentLayerOperation::set_opacity;
    plan.index = index;
    plan.opacity = opacity;
    plan.mutates_document = true;
    plan.marks_unsaved = true;
    plan.updates_title = true;
    return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_visibility(
    int layer_count,
    int index,
    bool visible)
{
    const auto index_status = validate_layer_index(layer_count, index);
    if (!index_status.ok()) {
        return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
    }

    DocumentLayerOperationPlan plan;
    plan.operation = DocumentLayerOperation::set_visibility;
    plan.index = index;
    plan.flag = visible;
    plan.mutates_document = true;
    plan.marks_unsaved = true;
    plan.reloads_animation_layers = true;
    plan.updates_title = true;
    return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_alpha_lock(
    int layer_count,
    int index,
    bool locked)
{
    const auto index_status = validate_layer_index(layer_count, index);
    if (!index_status.ok()) {
        return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
    }

    DocumentLayerOperationPlan plan;
    plan.operation = DocumentLayerOperation::set_alpha_lock;
    plan.index = index;
    plan.flag = locked;
    plan.mutates_document = true;
    plan.marks_unsaved = true;
    plan.updates_title = true;
    return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_blend_mode(
    int layer_count,
    int index,
    int blend_mode)
{
    const auto index_status = validate_layer_index(layer_count, index);
    if (!index_status.ok()) {
        return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
    }

    if (blend_mode < 0 || blend_mode >= document_layer_legacy_blend_mode_count) {
        return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
            pp::foundation::Status::out_of_range("layer blend mode is outside the supported range"));
    }

    DocumentLayerOperationPlan plan;
    plan.operation = DocumentLayerOperation::set_blend_mode;
    plan.index = index;
    plan.blend_mode = blend_mode;
    plan.mutates_document = true;
    plan.marks_unsaved = true;
    plan.updates_title = true;
    return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_highlight(
    int layer_count,
    int index,
    bool highlight)
{
    const auto index_status = validate_layer_index(layer_count, index);
    if (!index_status.ok()) {
        return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
    }

    DocumentLayerOperationPlan plan;
    plan.operation = DocumentLayerOperation::set_highlight;
    plan.index = index;
    plan.flag = highlight;
    return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<DocumentLayerMenuPlan> plan_document_layer_menu(
    DocumentLayerMenuCommand command,
    bool has_current_layer,
    int current_index,
    int animation_duration,
    std::string_view current_layer_name,
    std::string_view lower_layer_name)
{
    if (current_index < 0) {
        return pp::foundation::Result<DocumentLayerMenuPlan>::failure(
            pp::foundation::Status::out_of_range("current layer index must not be negative"));
    }
    if (animation_duration < 0) {
        return pp::foundation::Result<DocumentLayerMenuPlan>::failure(
            pp::foundation::Status::out_of_range("animation duration must not be negative"));
    }

    DocumentLayerMenuPlan plan;
    plan.command = command;
    plan.from_index = current_index;
    plan.to_index = current_index > 0 ? current_index - 1 : 0;

    switch (command) {
    case DocumentLayerMenuCommand::clear:
        plan.action = has_current_layer
            ? DocumentLayerMenuAction::clear_current_layer
            : DocumentLayerMenuAction::no_op_select_layer;
        plan.label = has_current_layer
            ? "Clear Layer " + std::string(current_layer_name)
            : "Clear Layer (Select a layer)";
        break;
    case DocumentLayerMenuCommand::rename:
        plan.action = has_current_layer
            ? DocumentLayerMenuAction::show_rename_dialog
            : DocumentLayerMenuAction::no_op_select_layer;
        plan.label = has_current_layer
            ? "Rename Layer " + std::string(current_layer_name)
            : "Rename Layer (Select a layer)";
        break;
    case DocumentLayerMenuCommand::merge_down:
        if (!has_current_layer) {
            plan.action = DocumentLayerMenuAction::no_op_select_layer;
            plan.label = "Merge Layer (Select a layer)";
        } else if (animation_duration > 1) {
            plan.action = DocumentLayerMenuAction::show_merge_animated_not_supported;
            plan.label = "Merge Layer (Animation not supported)";
        } else if (current_index <= 0) {
            plan.action = DocumentLayerMenuAction::no_op_select_upper_layer;
            plan.label = "Merge Layer (Select upper layers)";
        } else {
            plan.action = DocumentLayerMenuAction::merge_with_lower_layer;
            plan.label = "Merge with " + std::string(lower_layer_name);
        }
        break;
    }

    return pp::foundation::Result<DocumentLayerMenuPlan>::success(std::move(plan));
}

[[nodiscard]] inline pp::foundation::Status execute_document_layer_menu_plan(
    const DocumentLayerMenuPlan& plan,
    DocumentLayerMenuServices& services)
{
    switch (plan.action) {
    case DocumentLayerMenuAction::clear_current_layer:
        services.clear_current_layer();
        return pp::foundation::Status::success();
    case DocumentLayerMenuAction::show_rename_dialog:
        services.show_rename_dialog();
        return pp::foundation::Status::success();
    case DocumentLayerMenuAction::merge_with_lower_layer:
        services.merge_with_lower_layer(plan.from_index, plan.to_index);
        return pp::foundation::Status::success();
    case DocumentLayerMenuAction::show_merge_animated_not_supported:
        services.show_merge_animated_not_supported();
        return pp::foundation::Status::success();
    case DocumentLayerMenuAction::no_op_select_layer:
    case DocumentLayerMenuAction::no_op_select_upper_layer:
        return pp::foundation::Status::success();
    }

    return pp::foundation::Status::invalid_argument("unknown document layer menu action");
}

}
