#pragma once

#include "foundation/result.h"

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

namespace pp::app {

enum class BrushUiTextureSlot {
    tip,
    pattern,
    dual,
};

enum class BrushUiOperation {
    set_tip_color,
    set_texture,
    replace_brush_from_preset,
    stroke_settings_changed,
};

enum class BrushTextureListOperation {
    add_texture,
    remove_texture,
    move_texture,
};

enum class BrushPresetListOperation {
    add_current_brush,
    remove_preset,
    move_preset,
    select_preset,
    clear_presets,
};

enum class BrushStrokeControlOperation {
    set_float,
    set_bool,
    set_blend_mode,
    reset_tip_aspect,
    reset_default_brush,
};

enum class BrushStrokeFloatSetting {
    tip_size,
    tip_spacing,
    tip_flow,
    tip_opacity,
    tip_angle,
    tip_angle_smooth,
    tip_mix,
    tip_wet,
    tip_noise,
    tip_hue,
    tip_saturation,
    tip_value,
    jitter_scale,
    jitter_angle,
    jitter_scatter,
    jitter_flow,
    jitter_opacity,
    jitter_hue,
    jitter_saturation,
    jitter_value,
    jitter_aspect,
    dual_size,
    dual_spacing,
    dual_scatter,
    tip_aspect,
    dual_opacity,
    dual_flow,
    dual_rotate,
    pattern_scale,
    pattern_brightness,
    pattern_contrast,
    pattern_depth,
};

enum class BrushStrokeBoolSetting {
    tip_angle_init,
    tip_angle_follow,
    tip_flow_pressure,
    tip_opacity_pressure,
    tip_size_pressure,
    jitter_scatter_both_axis,
    jitter_aspect_both_axis,
    jitter_hsv_each_sample,
    tip_invert,
    tip_flip_x,
    tip_flip_y,
    pattern_enabled,
    dual_enabled,
    dual_scatter_both_axis,
    dual_invert,
    dual_flip_x,
    dual_flip_y,
    dual_random_flip,
    tip_random_flip_x,
    tip_random_flip_y,
    pattern_each_sample,
    pattern_invert,
    pattern_flip_x,
    pattern_flip_y,
    pattern_random_offset,
};

enum class BrushStrokeBlendSetting {
    tip,
    dual,
    pattern,
};

struct BrushUiPlan {
    BrushUiOperation operation = BrushUiOperation::stroke_settings_changed;
    BrushUiTextureSlot texture_slot = BrushUiTextureSlot::tip;
    std::string path;
    std::string thumbnail_path;
    float r = 0.0F;
    float g = 0.0F;
    float b = 0.0F;
    float a = 1.0F;
    bool mutates_brush = false;
    bool preserves_existing_color = false;
    bool loads_brush_resources = false;
    bool update_color_ui = false;
    bool update_brush_ui = false;
};

struct BrushTextureListPlan {
    BrushTextureListOperation operation = BrushTextureListOperation::add_texture;
    int item_count = 0;
    int current_index = -1;
    int target_index = -1;
    int move_offset = 0;
    std::string source_path;
    std::string high_path;
    std::string thumbnail_path;
    std::string brush_name;
    bool user_texture = false;
    bool deletes_texture_files = false;
    bool saves_list = false;
    bool notifies_selection = false;
    bool converts_brush_alpha = false;
    bool no_op = false;
};

struct BrushPresetListPlan {
    BrushPresetListOperation operation = BrushPresetListOperation::select_preset;
    int item_count = 0;
    int current_index = -1;
    int target_index = -1;
    int move_offset = 0;
    bool saves_list = false;
    bool updates_empty_notification = false;
    bool selects_target = false;
    bool clears_selection = false;
    bool notifies_brush_changed = false;
    bool no_op = false;
};

struct BrushStrokeControlPlan {
    BrushStrokeControlOperation operation = BrushStrokeControlOperation::set_float;
    BrushStrokeFloatSetting float_setting = BrushStrokeFloatSetting::tip_size;
    BrushStrokeBoolSetting bool_setting = BrushStrokeBoolSetting::tip_angle_init;
    BrushStrokeBlendSetting blend_setting = BrushStrokeBlendSetting::tip;
    float float_value = 0.0F;
    bool bool_value = false;
    int blend_mode = 0;
    bool mutates_brush = false;
    bool updates_controls = false;
    bool refreshes_preview = false;
    bool notifies_stroke_change = false;
};

struct BrushStrokeFloatValue {
    BrushStrokeFloatSetting setting = BrushStrokeFloatSetting::tip_size;
    float value = 0.0F;
};

struct BrushStrokeBoolValue {
    BrushStrokeBoolSetting setting = BrushStrokeBoolSetting::tip_angle_init;
    bool value = false;
};

struct BrushStrokeBlendValue {
    BrushStrokeBlendSetting setting = BrushStrokeBlendSetting::tip;
    int blend_mode = 0;
};

struct BrushStrokePanelInput {
    std::vector<BrushStrokeFloatValue> float_values;
    std::vector<BrushStrokeBoolValue> bool_values;
    std::vector<BrushStrokeBlendValue> blend_values;
    std::string tip_thumbnail_path;
    std::string dual_thumbnail_path;
    std::string pattern_thumbnail_path;
};

struct BrushStrokePanelView {
    std::vector<BrushStrokeFloatValue> float_values;
    std::vector<BrushStrokeBoolValue> bool_values;
    std::vector<BrushStrokeBlendValue> blend_values;
    std::string tip_thumbnail_path;
    std::string dual_thumbnail_path;
    std::string pattern_thumbnail_path;
    bool updates_preview = true;
    bool updates_thumbnails = true;
};

struct BrushUiRefreshInput {
    bool update_color = false;
    bool update_brush = false;
    bool has_current_brush = true;
    bool has_floating_picker = false;
    bool has_floating_color_panel = false;
    float tip_flow = 0.0F;
    float tip_size = 0.0F;
    float r = 0.0F;
    float g = 0.0F;
    float b = 0.0F;
    float a = 1.0F;
};

struct BrushUiRefreshView {
    bool updates_stroke_controls = false;
    bool updates_quick_flow = false;
    bool updates_quick_size = false;
    bool updates_quick_brush_preview = false;
    bool updates_quick_color = false;
    bool updates_floating_picker = false;
    bool updates_floating_color_panel = false;
    bool no_op = false;
    float tip_flow = 0.0F;
    float tip_size = 0.0F;
    float r = 0.0F;
    float g = 0.0F;
    float b = 0.0F;
    float a = 1.0F;
};

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

    virtual void set_tip_color(float r, float g, float b, float a) = 0;
    virtual void set_texture(BrushUiTextureSlot slot, std::string_view path, std::string_view thumbnail_path) = 0;
    virtual void replace_brush_from_preset(bool preserve_existing_color, bool load_resources) = 0;
    virtual void refresh_brush_ui(bool update_color_ui, bool update_brush_ui) = 0;
};

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

    virtual pp::foundation::Status add_texture_from_source(
        std::string_view source_path,
        std::string_view high_path,
        std::string_view thumbnail_path,
        std::string_view brush_name,
        bool converts_brush_alpha) = 0;
    virtual void remove_texture(int index, bool delete_texture_files) = 0;
    virtual void move_texture(int from_index, int to_index) = 0;
    virtual void select_texture(int index) = 0;
    virtual void save_texture_list() = 0;
};

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

    virtual pp::foundation::Status add_current_brush_preset(int target_index) = 0;
    virtual void remove_brush_preset(
        int current_index,
        int target_index,
        bool selects_target,
        bool clears_selection) = 0;
    virtual void move_brush_preset(int from_index, int to_index) = 0;
    virtual void select_brush_preset(int index, bool notify_brush_changed) = 0;
    virtual void clear_brush_presets(bool clears_selection) = 0;
    virtual void update_preset_empty_notification() = 0;
    virtual void save_preset_list() = 0;
};

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

    virtual void set_float_setting(BrushStrokeFloatSetting setting, float value) = 0;
    virtual void set_bool_setting(BrushStrokeBoolSetting setting, bool value) = 0;
    virtual void set_blend_mode(BrushStrokeBlendSetting setting, int blend_mode) = 0;
    virtual void reset_tip_aspect(float value) = 0;
    virtual void reset_default_brush() = 0;
    virtual void update_stroke_controls() = 0;
    virtual void refresh_stroke_preview() = 0;
    virtual void notify_stroke_changed() = 0;
};

[[nodiscard]] inline pp::foundation::Result<std::string_view> brush_texture_source_stem(
    std::string_view source_path) noexcept
{
    const auto slash = source_path.find_last_of("/\\");
    const auto name_begin = slash == std::string_view::npos ? 0U : slash + 1U;
    if (name_begin >= source_path.size()) {
        return pp::foundation::Result<std::string_view>::failure(
            pp::foundation::Status::invalid_argument("brush texture source path must contain a file name"));
    }

    const auto dot = source_path.find_last_of('.');
    if (dot == std::string_view::npos || dot <= name_begin || dot + 1U >= source_path.size()) {
        return pp::foundation::Result<std::string_view>::failure(
            pp::foundation::Status::invalid_argument("brush texture source path must include a file extension"));
    }

    return pp::foundation::Result<std::string_view>::success(source_path.substr(name_begin, dot - name_begin));
}

[[nodiscard]] inline pp::foundation::Status validate_brush_ui_color_channel(float value) noexcept
{
    if (!std::isfinite(value) || value < 0.0F || value > 1.0F) {
        return pp::foundation::Status::out_of_range("brush color channels must be finite and within 0..1");
    }

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

[[nodiscard]] inline pp::foundation::Status validate_brush_stroke_float(float value) noexcept
{
    if (!std::isfinite(value)) {
        return pp::foundation::Status::invalid_argument("brush stroke float setting must be finite");
    }

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

[[nodiscard]] inline pp::foundation::Status validate_brush_stroke_blend_mode(int blend_mode) noexcept
{
    if (blend_mode < 0 || blend_mode > 63) {
        return pp::foundation::Status::out_of_range("brush stroke blend mode must be within 0..63");
    }

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

[[nodiscard]] inline pp::foundation::Result<BrushStrokePanelView> plan_brush_stroke_panel_view(
    BrushStrokePanelInput input)
{
    for (const auto& value : input.float_values) {
        const auto status = validate_brush_stroke_float(value.value);
        if (!status.ok()) {
            return pp::foundation::Result<BrushStrokePanelView>::failure(status);
        }
    }

    for (const auto& value : input.blend_values) {
        const auto status = validate_brush_stroke_blend_mode(value.blend_mode);
        if (!status.ok()) {
            return pp::foundation::Result<BrushStrokePanelView>::failure(status);
        }
    }

    BrushStrokePanelView view;
    view.float_values = std::move(input.float_values);
    view.bool_values = std::move(input.bool_values);
    view.blend_values = std::move(input.blend_values);
    view.tip_thumbnail_path = std::move(input.tip_thumbnail_path);
    view.dual_thumbnail_path = std::move(input.dual_thumbnail_path);
    view.pattern_thumbnail_path = std::move(input.pattern_thumbnail_path);
    return pp::foundation::Result<BrushStrokePanelView>::success(std::move(view));
}

[[nodiscard]] inline pp::foundation::Result<BrushUiRefreshView> plan_brush_ui_refresh(
    const BrushUiRefreshInput& input)
{
    if (!input.update_color && !input.update_brush) {
        BrushUiRefreshView view;
        view.no_op = true;
        return pp::foundation::Result<BrushUiRefreshView>::success(view);
    }

    if (!input.has_current_brush) {
        return pp::foundation::Result<BrushUiRefreshView>::failure(
            pp::foundation::Status::invalid_argument("brush refresh requires a current brush"));
    }

    if (input.update_brush) {
        const auto flow_status = validate_brush_stroke_float(input.tip_flow);
        if (!flow_status.ok()) {
            return pp::foundation::Result<BrushUiRefreshView>::failure(flow_status);
        }
        const auto size_status = validate_brush_stroke_float(input.tip_size);
        if (!size_status.ok()) {
            return pp::foundation::Result<BrushUiRefreshView>::failure(size_status);
        }
    }

    if (input.update_color) {
        for (const auto value : { input.r, input.g, input.b, input.a }) {
            const auto channel_status = validate_brush_ui_color_channel(value);
            if (!channel_status.ok()) {
                return pp::foundation::Result<BrushUiRefreshView>::failure(channel_status);
            }
        }
    }

    BrushUiRefreshView view;
    view.updates_stroke_controls = input.update_brush;
    view.updates_quick_flow = input.update_brush;
    view.updates_quick_size = input.update_brush;
    view.updates_quick_brush_preview = input.update_brush;
    view.updates_quick_color = input.update_color;
    view.updates_floating_picker = input.update_color && input.has_floating_picker;
    view.updates_floating_color_panel = input.update_color && input.has_floating_color_panel;
    view.tip_flow = input.tip_flow;
    view.tip_size = input.tip_size;
    view.r = input.r;
    view.g = input.g;
    view.b = input.b;
    view.a = input.a;
    return pp::foundation::Result<BrushUiRefreshView>::success(view);
}

[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_color(
    float r,
    float g,
    float b,
    float a)
{
    for (const auto value : { r, g, b, a }) {
        const auto channel_status = validate_brush_ui_color_channel(value);
        if (!channel_status.ok()) {
            return pp::foundation::Result<BrushUiPlan>::failure(channel_status);
        }
    }

    BrushUiPlan plan;
    plan.operation = BrushUiOperation::set_tip_color;
    plan.r = r;
    plan.g = g;
    plan.b = b;
    plan.a = a;
    plan.mutates_brush = true;
    plan.update_color_ui = true;
    return pp::foundation::Result<BrushUiPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_texture(
    BrushUiTextureSlot slot,
    std::string_view path,
    std::string_view thumbnail_path)
{
    if (path.empty()) {
        return pp::foundation::Result<BrushUiPlan>::failure(
            pp::foundation::Status::invalid_argument("brush texture path must not be empty"));
    }

    BrushUiPlan plan;
    plan.operation = BrushUiOperation::set_texture;
    plan.texture_slot = slot;
    plan.path = std::string(path);
    plan.thumbnail_path = std::string(thumbnail_path);
    plan.mutates_brush = true;
    plan.loads_brush_resources = true;
    plan.update_color_ui = true;
    plan.update_brush_ui = true;
    return pp::foundation::Result<BrushUiPlan>::success(std::move(plan));
}

[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_preset_replace(bool has_preset_brush)
{
    if (!has_preset_brush) {
        return pp::foundation::Result<BrushUiPlan>::failure(
            pp::foundation::Status::invalid_argument("preset brush must be available"));
    }

    BrushUiPlan plan;
    plan.operation = BrushUiOperation::replace_brush_from_preset;
    plan.mutates_brush = true;
    plan.preserves_existing_color = true;
    plan.loads_brush_resources = true;
    plan.update_color_ui = true;
    plan.update_brush_ui = true;
    return pp::foundation::Result<BrushUiPlan>::success(plan);
}

[[nodiscard]] inline constexpr BrushUiPlan plan_brush_ui_stroke_settings_changed() noexcept
{
    BrushUiPlan plan;
    plan.operation = BrushUiOperation::stroke_settings_changed;
    plan.mutates_brush = true;
    plan.update_color_ui = true;
    plan.update_brush_ui = true;
    return plan;
}

[[nodiscard]] inline pp::foundation::Result<BrushStrokeControlPlan> plan_brush_stroke_float_setting(
    BrushStrokeFloatSetting setting,
    float value)
{
    const auto status = validate_brush_stroke_float(value);
    if (!status.ok()) {
        return pp::foundation::Result<BrushStrokeControlPlan>::failure(status);
    }

    BrushStrokeControlPlan plan;
    plan.operation = BrushStrokeControlOperation::set_float;
    plan.float_setting = setting;
    plan.float_value = value;
    plan.mutates_brush = true;
    plan.refreshes_preview = true;
    plan.notifies_stroke_change = true;
    return pp::foundation::Result<BrushStrokeControlPlan>::success(plan);
}

[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_stroke_bool_setting(
    BrushStrokeBoolSetting setting,
    bool value) noexcept
{
    BrushStrokeControlPlan plan;
    plan.operation = BrushStrokeControlOperation::set_bool;
    plan.bool_setting = setting;
    plan.bool_value = value;
    plan.mutates_brush = true;
    plan.refreshes_preview = true;
    plan.notifies_stroke_change = true;
    return plan;
}

[[nodiscard]] inline pp::foundation::Result<BrushStrokeControlPlan> plan_brush_stroke_blend_mode(
    BrushStrokeBlendSetting setting,
    int blend_mode)
{
    const auto status = validate_brush_stroke_blend_mode(blend_mode);
    if (!status.ok()) {
        return pp::foundation::Result<BrushStrokeControlPlan>::failure(status);
    }

    BrushStrokeControlPlan plan;
    plan.operation = BrushStrokeControlOperation::set_blend_mode;
    plan.blend_setting = setting;
    plan.blend_mode = blend_mode;
    plan.mutates_brush = true;
    plan.refreshes_preview = true;
    plan.notifies_stroke_change = true;
    return pp::foundation::Result<BrushStrokeControlPlan>::success(plan);
}

[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_tip_aspect_reset(float value = 0.5F) noexcept
{
    BrushStrokeControlPlan plan;
    plan.operation = BrushStrokeControlOperation::reset_tip_aspect;
    plan.float_setting = BrushStrokeFloatSetting::tip_aspect;
    plan.float_value = value;
    plan.mutates_brush = true;
    plan.refreshes_preview = true;
    plan.notifies_stroke_change = true;
    return plan;
}

[[nodiscard]] inline constexpr BrushStrokeControlPlan plan_brush_default_settings_reset() noexcept
{
    BrushStrokeControlPlan plan;
    plan.operation = BrushStrokeControlOperation::reset_default_brush;
    plan.mutates_brush = true;
    plan.updates_controls = true;
    plan.refreshes_preview = true;
    plan.notifies_stroke_change = true;
    return plan;
}

[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_add(
    std::string_view directory_name,
    std::string_view data_path,
    std::string_view source_path)
{
    if (directory_name.empty()) {
        return pp::foundation::Result<BrushTextureListPlan>::failure(
            pp::foundation::Status::invalid_argument("brush texture directory must not be empty"));
    }
    if (data_path.empty()) {
        return pp::foundation::Result<BrushTextureListPlan>::failure(
            pp::foundation::Status::invalid_argument("brush texture data path must not be empty"));
    }

    const auto stem = brush_texture_source_stem(source_path);
    if (!stem) {
        return pp::foundation::Result<BrushTextureListPlan>::failure(stem.status());
    }

    BrushTextureListPlan plan;
    plan.operation = BrushTextureListOperation::add_texture;
    plan.source_path = std::string(source_path);
    plan.brush_name = std::string(stem.value());
    plan.high_path = std::string(data_path) + "/" + std::string(directory_name) + "/" + plan.brush_name + ".png";
    plan.thumbnail_path = std::string(data_path) + "/" + std::string(directory_name) + "/thumbs/"
        + plan.brush_name + ".png";
    plan.user_texture = true;
    plan.saves_list = true;
    plan.converts_brush_alpha = directory_name == "brushes";
    return pp::foundation::Result<BrushTextureListPlan>::success(std::move(plan));
}

[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_remove(
    int item_count,
    int current_index,
    bool current_is_user_texture)
{
    if (item_count <= 0) {
        return pp::foundation::Result<BrushTextureListPlan>::failure(
            pp::foundation::Status::invalid_argument("brush texture list must contain an item to remove"));
    }
    if (current_index < 0 || current_index >= item_count) {
        return pp::foundation::Result<BrushTextureListPlan>::failure(
            pp::foundation::Status::out_of_range("selected brush texture index is outside the list"));
    }

    BrushTextureListPlan plan;
    plan.operation = BrushTextureListOperation::remove_texture;
    plan.item_count = item_count;
    plan.current_index = current_index;
    plan.target_index = item_count > 1 ? std::min(current_index, item_count - 2) : -1;
    plan.user_texture = current_is_user_texture;
    plan.deletes_texture_files = current_is_user_texture;
    plan.saves_list = true;
    plan.notifies_selection = plan.target_index >= 0;
    return pp::foundation::Result<BrushTextureListPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<BrushTextureListPlan> plan_brush_texture_list_move(
    int item_count,
    int current_index,
    int offset)
{
    if (item_count <= 0) {
        return pp::foundation::Result<BrushTextureListPlan>::failure(
            pp::foundation::Status::invalid_argument("brush texture list must contain an item to move"));
    }
    if (current_index < 0 || current_index >= item_count) {
        return pp::foundation::Result<BrushTextureListPlan>::failure(
            pp::foundation::Status::out_of_range("selected brush texture index is outside the list"));
    }
    if (offset == 0) {
        return pp::foundation::Result<BrushTextureListPlan>::failure(
            pp::foundation::Status::invalid_argument("brush texture move offset must not be zero"));
    }

    BrushTextureListPlan plan;
    plan.operation = BrushTextureListOperation::move_texture;
    plan.item_count = item_count;
    plan.current_index = current_index;
    plan.target_index = std::clamp(current_index + offset, 0, item_count - 1);
    plan.move_offset = offset;
    plan.saves_list = true;
    plan.no_op = plan.target_index == current_index;
    return pp::foundation::Result<BrushTextureListPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<BrushPresetListPlan> plan_brush_preset_list_add(
    int item_count,
    bool has_current_brush)
{
    if (item_count < 0) {
        return pp::foundation::Result<BrushPresetListPlan>::failure(
            pp::foundation::Status::out_of_range("brush preset item count must not be negative"));
    }
    if (!has_current_brush) {
        return pp::foundation::Result<BrushPresetListPlan>::failure(
            pp::foundation::Status::invalid_argument("current brush must be available to add a preset"));
    }

    BrushPresetListPlan plan;
    plan.operation = BrushPresetListOperation::add_current_brush;
    plan.item_count = item_count;
    plan.target_index = item_count;
    plan.saves_list = true;
    plan.updates_empty_notification = true;
    return pp::foundation::Result<BrushPresetListPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<BrushPresetListPlan> plan_brush_preset_list_remove(
    int item_count,
    int current_index)
{
    if (item_count <= 0) {
        return pp::foundation::Result<BrushPresetListPlan>::failure(
            pp::foundation::Status::invalid_argument("brush preset list must contain an item to remove"));
    }
    if (current_index < 0 || current_index >= item_count) {
        return pp::foundation::Result<BrushPresetListPlan>::failure(
            pp::foundation::Status::out_of_range("selected brush preset index is outside the list"));
    }

    BrushPresetListPlan plan;
    plan.operation = BrushPresetListOperation::remove_preset;
    plan.item_count = item_count;
    plan.current_index = current_index;
    plan.target_index = item_count > 1 ? std::min(current_index, item_count - 2) : -1;
    plan.saves_list = true;
    plan.updates_empty_notification = true;
    plan.selects_target = plan.target_index >= 0;
    plan.clears_selection = plan.target_index < 0;
    return pp::foundation::Result<BrushPresetListPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<BrushPresetListPlan> plan_brush_preset_list_move(
    int item_count,
    int current_index,
    int offset)
{
    if (item_count <= 0) {
        return pp::foundation::Result<BrushPresetListPlan>::failure(
            pp::foundation::Status::invalid_argument("brush preset list must contain an item to move"));
    }
    if (current_index < 0 || current_index >= item_count) {
        return pp::foundation::Result<BrushPresetListPlan>::failure(
            pp::foundation::Status::out_of_range("selected brush preset index is outside the list"));
    }
    if (offset == 0) {
        return pp::foundation::Result<BrushPresetListPlan>::failure(
            pp::foundation::Status::invalid_argument("brush preset move offset must not be zero"));
    }

    BrushPresetListPlan plan;
    plan.operation = BrushPresetListOperation::move_preset;
    plan.item_count = item_count;
    plan.current_index = current_index;
    plan.target_index = std::clamp(current_index + offset, 0, item_count - 1);
    plan.move_offset = offset;
    plan.saves_list = true;
    plan.no_op = plan.target_index == current_index;
    return pp::foundation::Result<BrushPresetListPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<BrushPresetListPlan> plan_brush_preset_list_select(
    int item_count,
    int index)
{
    if (item_count <= 0) {
        return pp::foundation::Result<BrushPresetListPlan>::failure(
            pp::foundation::Status::invalid_argument("brush preset list must contain an item to select"));
    }
    if (index < 0 || index >= item_count) {
        return pp::foundation::Result<BrushPresetListPlan>::failure(
            pp::foundation::Status::out_of_range("selected brush preset index is outside the list"));
    }

    BrushPresetListPlan plan;
    plan.operation = BrushPresetListOperation::select_preset;
    plan.item_count = item_count;
    plan.current_index = index;
    plan.target_index = index;
    plan.selects_target = true;
    plan.notifies_brush_changed = true;
    return pp::foundation::Result<BrushPresetListPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Result<BrushPresetListPlan> plan_brush_preset_list_clear(int item_count)
{
    if (item_count < 0) {
        return pp::foundation::Result<BrushPresetListPlan>::failure(
            pp::foundation::Status::out_of_range("brush preset item count must not be negative"));
    }

    BrushPresetListPlan plan;
    plan.operation = BrushPresetListOperation::clear_presets;
    plan.item_count = item_count;
    plan.saves_list = true;
    plan.updates_empty_notification = true;
    plan.clears_selection = true;
    plan.no_op = item_count == 0;
    return pp::foundation::Result<BrushPresetListPlan>::success(plan);
}

[[nodiscard]] inline pp::foundation::Status execute_brush_ui_plan(
    const BrushUiPlan& plan,
    BrushUiServices& services)
{
    switch (plan.operation) {
    case BrushUiOperation::set_tip_color:
    {
        for (const auto value : { plan.r, plan.g, plan.b, plan.a }) {
            const auto channel_status = validate_brush_ui_color_channel(value);
            if (!channel_status.ok()) {
                return channel_status;
            }
        }
        services.set_tip_color(plan.r, plan.g, plan.b, plan.a);
        services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
        return pp::foundation::Status::success();
    }

    case BrushUiOperation::set_texture:
        if (plan.path.empty()) {
            return pp::foundation::Status::invalid_argument("brush texture path must not be empty");
        }
        services.set_texture(plan.texture_slot, plan.path, plan.thumbnail_path);
        services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
        return pp::foundation::Status::success();

    case BrushUiOperation::replace_brush_from_preset:
        services.replace_brush_from_preset(plan.preserves_existing_color, plan.loads_brush_resources);
        services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
        return pp::foundation::Status::success();

    case BrushUiOperation::stroke_settings_changed:
        services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
        return pp::foundation::Status::success();
    }

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

[[nodiscard]] inline pp::foundation::Status execute_brush_stroke_control_plan(
    const BrushStrokeControlPlan& plan,
    BrushStrokeControlServices& services)
{
    switch (plan.operation) {
    case BrushStrokeControlOperation::set_float:
    {
        const auto status = validate_brush_stroke_float(plan.float_value);
        if (!status.ok()) {
            return status;
        }
        services.set_float_setting(plan.float_setting, plan.float_value);
        break;
    }

    case BrushStrokeControlOperation::set_bool:
        services.set_bool_setting(plan.bool_setting, plan.bool_value);
        break;

    case BrushStrokeControlOperation::set_blend_mode:
    {
        const auto status = validate_brush_stroke_blend_mode(plan.blend_mode);
        if (!status.ok()) {
            return status;
        }
        services.set_blend_mode(plan.blend_setting, plan.blend_mode);
        break;
    }

    case BrushStrokeControlOperation::reset_tip_aspect:
    {
        const auto status = validate_brush_stroke_float(plan.float_value);
        if (!status.ok()) {
            return status;
        }
        services.reset_tip_aspect(plan.float_value);
        break;
    }

    case BrushStrokeControlOperation::reset_default_brush:
        services.reset_default_brush();
        break;
    }

    if (plan.updates_controls) {
        services.update_stroke_controls();
    }
    if (plan.refreshes_preview) {
        services.refresh_stroke_preview();
    }
    if (plan.notifies_stroke_change) {
        services.notify_stroke_changed();
    }

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

[[nodiscard]] inline pp::foundation::Status execute_brush_texture_list_plan(
    const BrushTextureListPlan& plan,
    BrushTextureListServices& services)
{
    switch (plan.operation) {
    case BrushTextureListOperation::add_texture:
    {
        if (plan.source_path.empty() || plan.high_path.empty() || plan.thumbnail_path.empty()
            || plan.brush_name.empty()) {
            return pp::foundation::Status::invalid_argument("brush texture add plan has incomplete paths");
        }

        const auto add_status = services.add_texture_from_source(
            plan.source_path,
            plan.high_path,
            plan.thumbnail_path,
            plan.brush_name,
            plan.converts_brush_alpha);
        if (!add_status.ok()) {
            return add_status;
        }
        if (plan.saves_list) {
            services.save_texture_list();
        }
        return pp::foundation::Status::success();
    }

    case BrushTextureListOperation::remove_texture:
        if (plan.item_count <= 0 || plan.current_index < 0 || plan.current_index >= plan.item_count) {
            return pp::foundation::Status::out_of_range("brush texture remove plan has invalid selection");
        }
        services.remove_texture(plan.current_index, plan.deletes_texture_files);
        if (plan.notifies_selection && plan.target_index >= 0) {
            services.select_texture(plan.target_index);
        }
        if (plan.saves_list) {
            services.save_texture_list();
        }
        return pp::foundation::Status::success();

    case BrushTextureListOperation::move_texture:
        if (plan.item_count <= 0 || plan.current_index < 0 || plan.current_index >= plan.item_count
            || plan.target_index < 0 || plan.target_index >= plan.item_count) {
            return pp::foundation::Status::out_of_range("brush texture move plan has invalid indices");
        }
        services.move_texture(plan.current_index, plan.target_index);
        if (plan.saves_list) {
            services.save_texture_list();
        }
        return pp::foundation::Status::success();
    }

    return pp::foundation::Status::invalid_argument("unknown brush texture list operation");
}

[[nodiscard]] inline pp::foundation::Status execute_brush_preset_list_plan(
    const BrushPresetListPlan& plan,
    BrushPresetListServices& services)
{
    switch (plan.operation) {
    case BrushPresetListOperation::add_current_brush:
    {
        if (plan.item_count < 0 || plan.target_index != plan.item_count) {
            return pp::foundation::Status::out_of_range("brush preset add plan has invalid target");
        }

        const auto add_status = services.add_current_brush_preset(plan.target_index);
        if (!add_status.ok()) {
            return add_status;
        }
        if (plan.updates_empty_notification) {
            services.update_preset_empty_notification();
        }
        if (plan.saves_list) {
            services.save_preset_list();
        }
        return pp::foundation::Status::success();
    }

    case BrushPresetListOperation::remove_preset:
        if (plan.item_count <= 0 || plan.current_index < 0 || plan.current_index >= plan.item_count) {
            return pp::foundation::Status::out_of_range("brush preset remove plan has invalid selection");
        }
        if (plan.selects_target && (plan.target_index < 0 || plan.target_index >= plan.item_count - 1)) {
            return pp::foundation::Status::out_of_range("brush preset remove plan has invalid target");
        }
        services.remove_brush_preset(
            plan.current_index,
            plan.target_index,
            plan.selects_target,
            plan.clears_selection);
        if (plan.updates_empty_notification) {
            services.update_preset_empty_notification();
        }
        if (plan.saves_list) {
            services.save_preset_list();
        }
        return pp::foundation::Status::success();

    case BrushPresetListOperation::move_preset:
        if (plan.item_count <= 0 || plan.current_index < 0 || plan.current_index >= plan.item_count
            || plan.target_index < 0 || plan.target_index >= plan.item_count) {
            return pp::foundation::Status::out_of_range("brush preset move plan has invalid indices");
        }
        services.move_brush_preset(plan.current_index, plan.target_index);
        if (plan.saves_list) {
            services.save_preset_list();
        }
        return pp::foundation::Status::success();

    case BrushPresetListOperation::select_preset:
        if (plan.item_count <= 0 || plan.target_index < 0 || plan.target_index >= plan.item_count) {
            return pp::foundation::Status::out_of_range("brush preset select plan has invalid target");
        }
        services.select_brush_preset(plan.target_index, plan.notifies_brush_changed);
        return pp::foundation::Status::success();

    case BrushPresetListOperation::clear_presets:
        if (plan.item_count < 0) {
            return pp::foundation::Status::out_of_range("brush preset clear plan has invalid item count");
        }
        services.clear_brush_presets(plan.clears_selection);
        if (plan.updates_empty_notification) {
            services.update_preset_empty_notification();
        }
        if (plan.saves_list) {
            services.save_preset_list();
        }
        return pp::foundation::Status::success();
    }

    return pp::foundation::Status::invalid_argument("unknown brush preset list operation");
}

} // namespace pp::app
