#pragma once

#include "app_core/app_dialog.h"
#include "app_core/document_route.h"
#include "app_core/history_ui.h"
#include "foundation/result.h"

#include <array>
#include <cctype>
#include <cstdio>
#include <string>
#include <string_view>
#include <utility>

namespace pp::app {

enum class ProjectOpenDecision {
    open_now,
    prompt_discard_unsaved,
};

enum class CloseRequestDecision {
    close_now,
    show_unsaved_prompt,
    wait_for_existing_prompt,
};

enum class DocumentSaveIntent {
    save,
    save_as,
    save_version,
    save_dirty_version,
};

enum class DocumentSaveDecision {
    no_op,
    show_save_dialog,
    save_existing,
    save_version,
};

enum class DocumentWorkflowDecision {
    unavailable,
    continue_now,
    prompt_save_before_continue,
};

enum class DocumentFileWriteDecision {
    save_now,
    prompt_overwrite,
};

enum class DocumentOpenPlanAction {
    open_project_now,
    prompt_discard_unsaved_project,
    prompt_import_abr,
    prompt_import_ppbr,
};

enum class DocumentSessionPromptKind {
    close_unsaved_document,
    save_before_workflow_continue,
    new_document_overwrite,
    document_file_overwrite,
    document_save_error,
};

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

    virtual void prompt_import_abr(const DocumentOpenRoute& route) = 0;
    virtual void prompt_import_ppbr(const DocumentOpenRoute& route) = 0;
    virtual void open_project_now(const DocumentOpenRoute& route) = 0;
    virtual void prompt_discard_unsaved_project(const DocumentOpenRoute& route) = 0;
};

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

    virtual void request_close_now() = 0;
    virtual void show_unsaved_close_prompt() = 0;
};

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

    virtual void show_save_dialog() = 0;
    virtual void save_existing_document() = 0;
    virtual void save_document_version() = 0;
};

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

    virtual void continue_workflow_now() = 0;
    virtual void prompt_save_before_continue() = 0;
};

struct DocumentFileTarget {
    std::string name;
    std::string directory;
    std::string path;
};

struct DocumentVersionTarget {
    std::string name;
    std::string path;
};

struct DocumentFileSavePlan {
    DocumentFileTarget target;
    DocumentFileWriteDecision write_decision = DocumentFileWriteDecision::save_now;
};

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

    virtual void save_document_file(const DocumentFileSavePlan& plan) = 0;
    virtual void prompt_overwrite_document_file(const DocumentFileSavePlan& plan) = 0;
};

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

    virtual void save_document_version(const DocumentVersionTarget& target) = 0;
};

struct NewDocumentPlan {
    DocumentFileTarget target;
    int resolution = 0;
    DocumentFileWriteDecision write_decision = DocumentFileWriteDecision::save_now;
};

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

    virtual void create_new_document(const NewDocumentPlan& plan) = 0;
    virtual void prompt_overwrite_new_document(const NewDocumentPlan& plan) = 0;
};

[[nodiscard]] HistoryUiPlan plan_document_open_history(const DocumentOpenRoute& route) noexcept;
[[nodiscard]] HistoryUiPlan plan_close_request_history(CloseRequestDecision decision) noexcept;
[[nodiscard]] HistoryUiPlan plan_document_save_history(DocumentSaveDecision decision) noexcept;
[[nodiscard]] HistoryUiPlan plan_document_workflow_history(DocumentWorkflowDecision decision) noexcept;
[[nodiscard]] HistoryUiPlan plan_document_file_save_history(const DocumentFileSavePlan& plan) noexcept;
[[nodiscard]] HistoryUiPlan plan_document_version_save_history(const DocumentVersionTarget& target) noexcept;
[[nodiscard]] HistoryUiPlan plan_new_document_history(const NewDocumentPlan& plan) noexcept;

[[nodiscard]] inline AppMessageDialogPlan plan_document_session_prompt(
    DocumentSessionPromptKind kind,
    std::string_view document_name = {})
{
    switch (kind) {
    case DocumentSessionPromptKind::close_unsaved_document:
        return plan_app_message_dialog(
            "Unsaved document",
            "Do you want to close without saving?",
            true,
            "Yes",
            "No");
    case DocumentSessionPromptKind::save_before_workflow_continue:
        return plan_app_message_dialog(
            "Unsaved document",
            "Would you like to save this document before closing?",
            true,
            "Yes",
            "No");
    case DocumentSessionPromptKind::new_document_overwrite:
        return plan_app_message_dialog(
            "Warning",
            "A document with this name already exists, continue?",
            true);
    case DocumentSessionPromptKind::document_file_overwrite:
    {
        std::string message = "Are you sure you want to overwrite ";
        message += document_name;
        message += "?";
        return plan_app_message_dialog("Warning", message, true);
    }
    case DocumentSessionPromptKind::document_save_error:
        return plan_app_message_dialog(
            "Saving Error",
            "There was a problem saving the document",
            false);
    }

    return plan_app_message_dialog("Warning", "", false);
}

[[nodiscard]] constexpr ProjectOpenDecision plan_project_open(bool has_unsaved_changes) noexcept
{
    return has_unsaved_changes
        ? ProjectOpenDecision::prompt_discard_unsaved
        : ProjectOpenDecision::open_now;
}

[[nodiscard]] constexpr DocumentOpenPlanAction plan_document_open(
    DocumentOpenKind kind,
    bool has_unsaved_changes) noexcept
{
    switch (kind) {
    case DocumentOpenKind::import_abr:
        return DocumentOpenPlanAction::prompt_import_abr;
    case DocumentOpenKind::import_ppbr:
        return DocumentOpenPlanAction::prompt_import_ppbr;
    case DocumentOpenKind::open_project:
        return has_unsaved_changes
            ? DocumentOpenPlanAction::prompt_discard_unsaved_project
            : DocumentOpenPlanAction::open_project_now;
    }

    return DocumentOpenPlanAction::open_project_now;
}

[[nodiscard]] inline pp::foundation::Status execute_document_open_plan(
    DocumentOpenPlanAction action,
    const DocumentOpenRoute& route,
    DocumentOpenServices& services)
{
    switch (action) {
    case DocumentOpenPlanAction::open_project_now:
        if (route.kind != DocumentOpenKind::open_project) {
            return pp::foundation::Status::invalid_argument("open-project action requires a project route");
        }
        services.open_project_now(route);
        return pp::foundation::Status::success();
    case DocumentOpenPlanAction::prompt_discard_unsaved_project:
        if (route.kind != DocumentOpenKind::open_project) {
            return pp::foundation::Status::invalid_argument("discard prompt requires a project route");
        }
        services.prompt_discard_unsaved_project(route);
        return pp::foundation::Status::success();
    case DocumentOpenPlanAction::prompt_import_abr:
        if (route.kind != DocumentOpenKind::import_abr) {
            return pp::foundation::Status::invalid_argument("ABR import prompt requires an ABR route");
        }
        services.prompt_import_abr(route);
        return pp::foundation::Status::success();
    case DocumentOpenPlanAction::prompt_import_ppbr:
        if (route.kind != DocumentOpenKind::import_ppbr) {
            return pp::foundation::Status::invalid_argument("PPBR import prompt requires a PPBR route");
        }
        services.prompt_import_ppbr(route);
        return pp::foundation::Status::success();
    }

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

[[nodiscard]] constexpr CloseRequestDecision plan_close_request(
    bool has_unsaved_changes,
    bool close_prompt_already_open) noexcept
{
    if (!has_unsaved_changes) {
        return CloseRequestDecision::close_now;
    }

    return close_prompt_already_open
        ? CloseRequestDecision::wait_for_existing_prompt
        : CloseRequestDecision::show_unsaved_prompt;
}

[[nodiscard]] inline pp::foundation::Status execute_close_request_decision(
    CloseRequestDecision decision,
    CloseRequestServices& services)
{
    switch (decision) {
    case CloseRequestDecision::close_now:
        services.request_close_now();
        return pp::foundation::Status::success();
    case CloseRequestDecision::show_unsaved_prompt:
        services.show_unsaved_close_prompt();
        return pp::foundation::Status::success();
    case CloseRequestDecision::wait_for_existing_prompt:
        return pp::foundation::Status::success();
    }

    return pp::foundation::Status::invalid_argument("unknown close request decision");
}

[[nodiscard]] constexpr DocumentSaveDecision plan_document_save(
    bool is_new_document,
    bool has_unsaved_changes,
    DocumentSaveIntent intent) noexcept
{
    switch (intent) {
    case DocumentSaveIntent::save:
        if (is_new_document) {
            return DocumentSaveDecision::show_save_dialog;
        }
        return has_unsaved_changes
            ? DocumentSaveDecision::save_existing
            : DocumentSaveDecision::no_op;
    case DocumentSaveIntent::save_as:
        return DocumentSaveDecision::show_save_dialog;
    case DocumentSaveIntent::save_version:
        return is_new_document
            ? DocumentSaveDecision::show_save_dialog
            : DocumentSaveDecision::save_version;
    case DocumentSaveIntent::save_dirty_version:
        if (is_new_document) {
            return DocumentSaveDecision::show_save_dialog;
        }
        return has_unsaved_changes
            ? DocumentSaveDecision::save_version
            : DocumentSaveDecision::no_op;
    }

    return DocumentSaveDecision::no_op;
}

[[nodiscard]] inline pp::foundation::Status execute_document_save_decision(
    DocumentSaveDecision decision,
    DocumentSaveServices& services)
{
    switch (decision) {
    case DocumentSaveDecision::no_op:
        return pp::foundation::Status::success();
    case DocumentSaveDecision::show_save_dialog:
        services.show_save_dialog();
        return pp::foundation::Status::success();
    case DocumentSaveDecision::save_existing:
        services.save_existing_document();
        return pp::foundation::Status::success();
    case DocumentSaveDecision::save_version:
        services.save_document_version();
        return pp::foundation::Status::success();
    }

    return pp::foundation::Status::invalid_argument("unknown document save decision");
}

[[nodiscard]] constexpr DocumentWorkflowDecision plan_document_workflow(
    bool has_canvas,
    bool has_unsaved_changes) noexcept
{
    if (!has_canvas) {
        return DocumentWorkflowDecision::unavailable;
    }

    return has_unsaved_changes
        ? DocumentWorkflowDecision::prompt_save_before_continue
        : DocumentWorkflowDecision::continue_now;
}

[[nodiscard]] inline pp::foundation::Status execute_document_workflow_decision(
    DocumentWorkflowDecision decision,
    DocumentWorkflowServices& services)
{
    switch (decision) {
    case DocumentWorkflowDecision::unavailable:
        return pp::foundation::Status::success();
    case DocumentWorkflowDecision::continue_now:
        services.continue_workflow_now();
        return pp::foundation::Status::success();
    case DocumentWorkflowDecision::prompt_save_before_continue:
        services.prompt_save_before_continue();
        return pp::foundation::Status::success();
    }

    return pp::foundation::Status::invalid_argument("unknown document workflow decision");
}

[[nodiscard]] inline pp::foundation::Result<DocumentFileTarget> make_document_file_target(
    std::string_view work_directory,
    std::string_view document_name)
{
    if (document_name.empty()) {
        return pp::foundation::Result<DocumentFileTarget>::failure(
            pp::foundation::Status::invalid_argument("document name must not be empty"));
    }

    DocumentFileTarget target;
    target.name = std::string(document_name);
    target.directory = std::string(work_directory);
    target.path.reserve(target.directory.size() + target.name.size() + 5);
    target.path += target.directory;
    target.path += "/";
    target.path += target.name;
    target.path += ".ppi";
    return pp::foundation::Result<DocumentFileTarget>::success(std::move(target));
}

[[nodiscard]] constexpr DocumentFileWriteDecision plan_document_file_write(
    bool target_exists) noexcept
{
    return target_exists
        ? DocumentFileWriteDecision::prompt_overwrite
        : DocumentFileWriteDecision::save_now;
}

template <typename ExistsPredicate>
[[nodiscard]] pp::foundation::Result<DocumentFileSavePlan> plan_document_file_save(
    std::string_view work_directory,
    std::string_view document_name,
    ExistsPredicate&& exists)
{
    auto target = make_document_file_target(work_directory, document_name);
    if (!target) {
        return pp::foundation::Result<DocumentFileSavePlan>::failure(target.status());
    }

    DocumentFileSavePlan plan;
    plan.target = std::move(target.value());
    plan.write_decision = plan_document_file_write(exists(plan.target.path));
    return pp::foundation::Result<DocumentFileSavePlan>::success(std::move(plan));
}

[[nodiscard]] inline pp::foundation::Status execute_document_file_save_plan(
    const DocumentFileSavePlan& plan,
    DocumentFileSaveServices& services)
{
    switch (plan.write_decision) {
    case DocumentFileWriteDecision::save_now:
        services.save_document_file(plan);
        return pp::foundation::Status::success();
    case DocumentFileWriteDecision::prompt_overwrite:
        services.prompt_overwrite_document_file(plan);
        return pp::foundation::Status::success();
    }

    return pp::foundation::Status::invalid_argument("unknown document file save write decision");
}

[[nodiscard]] constexpr pp::foundation::Result<int> document_resolution_from_index(int index) noexcept
{
    constexpr std::array<int, 6> resolutions{ 512, 1024, 1536, 2048, 4096, 8192 };
    if (index < 0 || static_cast<std::size_t>(index) >= resolutions.size()) {
        return pp::foundation::Result<int>::failure(
            pp::foundation::Status::out_of_range("document resolution index is out of range"));
    }

    return pp::foundation::Result<int>::success(resolutions[static_cast<std::size_t>(index)]);
}

template <typename ExistsPredicate>
[[nodiscard]] pp::foundation::Result<NewDocumentPlan> plan_new_document(
    std::string_view work_directory,
    std::string_view document_name,
    int resolution_index,
    ExistsPredicate&& exists)
{
    const auto resolution = document_resolution_from_index(resolution_index);
    if (!resolution) {
        return pp::foundation::Result<NewDocumentPlan>::failure(resolution.status());
    }

    auto save_plan = plan_document_file_save(
        work_directory,
        document_name,
        std::forward<ExistsPredicate>(exists));
    if (!save_plan) {
        return pp::foundation::Result<NewDocumentPlan>::failure(save_plan.status());
    }

    NewDocumentPlan plan;
    plan.target = std::move(save_plan.value().target);
    plan.resolution = resolution.value();
    plan.write_decision = save_plan.value().write_decision;
    return pp::foundation::Result<NewDocumentPlan>::success(std::move(plan));
}

[[nodiscard]] inline pp::foundation::Status execute_new_document_plan(
    const NewDocumentPlan& plan,
    NewDocumentServices& services)
{
    switch (plan.write_decision) {
    case DocumentFileWriteDecision::save_now:
        services.create_new_document(plan);
        return pp::foundation::Status::success();
    case DocumentFileWriteDecision::prompt_overwrite:
        services.prompt_overwrite_new_document(plan);
        return pp::foundation::Status::success();
    }

    return pp::foundation::Status::invalid_argument("unknown new document write decision");
}

[[nodiscard]] inline bool has_legacy_two_character_version_suffix(std::string_view document_name) noexcept
{
    const auto dot = document_name.rfind('.');
    if (dot == std::string_view::npos || dot + 3 != document_name.size()) {
        return false;
    }

    const auto is_word = [](char ch) noexcept {
        return std::isalnum(static_cast<unsigned char>(ch)) != 0 || ch == '_';
    };
    return is_word(document_name[dot + 1]) && is_word(document_name[dot + 2]);
}

[[nodiscard]] inline int legacy_version_number(std::string_view suffix) noexcept
{
    int value = 0;
    for (const char ch : suffix) {
        if (ch < '0' || ch > '9') {
            break;
        }

        value = value * 10 + (ch - '0');
    }
    return value;
}

[[nodiscard]] inline std::string make_legacy_version_name(std::string_view base_name, int version)
{
    char suffix[4] {};
    std::snprintf(suffix, sizeof(suffix), ".%02d", version);
    std::string name;
    name.reserve(base_name.size() + 3);
    name += base_name;
    name += suffix;
    return name;
}

template <typename ExistsPredicate>
[[nodiscard]] pp::foundation::Result<DocumentVersionTarget> find_next_document_version_target(
    std::string_view directory,
    std::string_view document_name,
    ExistsPredicate&& exists)
{
    if (directory.empty()) {
        return pp::foundation::Result<DocumentVersionTarget>::failure(
            pp::foundation::Status::invalid_argument("directory must not be empty"));
    }

    if (document_name.empty()) {
        return pp::foundation::Result<DocumentVersionTarget>::failure(
            pp::foundation::Status::invalid_argument("document name must not be empty"));
    }

    int current = 0;
    std::string_view base = document_name;
    if (has_legacy_two_character_version_suffix(document_name)) {
        const auto dot = document_name.rfind('.');
        base = document_name.substr(0, dot);
        current = legacy_version_number(document_name.substr(dot + 1));
    }

    for (int version = current + 1; version < 99; ++version) {
        DocumentVersionTarget target;
        target.name = make_legacy_version_name(base, version);
        target.path.reserve(directory.size() + target.name.size() + 5);
        target.path += directory;
        target.path += "/";
        target.path += target.name;
        target.path += ".ppi";
        if (!exists(target.path)) {
            return pp::foundation::Result<DocumentVersionTarget>::success(std::move(target));
        }
    }

    return pp::foundation::Result<DocumentVersionTarget>::failure(
        pp::foundation::Status::out_of_range("no available document version target"));
}

[[nodiscard]] inline pp::foundation::Status execute_document_version_save(
    const DocumentVersionTarget& target,
    DocumentVersionSaveServices& services)
{
    if (target.name.empty() || target.path.empty()) {
        return pp::foundation::Status::invalid_argument("document version target requires a name and path");
    }

    services.save_document_version(target);
    return pp::foundation::Status::success();
}

}
