#include "document/document.h"

#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <string>
#include <utility>

namespace pp::document {

namespace {

[[nodiscard]] pp::foundation::Status validate_config(DocumentConfig config) noexcept
{
    if (config.width == 0 || config.height == 0) {
        return pp::foundation::Status::invalid_argument("document dimensions must be greater than zero");
    }

    if (config.width > max_canvas_dimension || config.height > max_canvas_dimension) {
        return pp::foundation::Status::out_of_range("document dimensions exceed the configured limit");
    }

    if (config.layer_count == 0) {
        return pp::foundation::Status::invalid_argument("document must contain at least one layer");
    }

    if (config.layer_count > max_layer_count) {
        return pp::foundation::Status::out_of_range("document layer count exceeds the configured limit");
    }

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

[[nodiscard]] std::string default_layer_name(std::size_t index)
{
    return "Layer " + std::to_string(index + 1U);
}

[[nodiscard]] std::uint64_t frame_duration_sum(std::span<const AnimationFrame> frames) noexcept
{
    std::uint64_t duration = 0;
    for (const auto& frame : frames) {
        duration += frame.duration_ms;
    }
    return duration;
}

[[nodiscard]] pp::foundation::Status validate_layer_name(std::string_view name) noexcept
{
    if (name.empty()) {
        return pp::foundation::Status::invalid_argument("layer name must not be empty");
    }

    if (name.size() > max_layer_name_length) {
        return pp::foundation::Status::out_of_range("layer name length exceeds the configured limit");
    }

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

[[nodiscard]] pp::foundation::Status validate_layer_opacity(float opacity) noexcept
{
    if (!std::isfinite(opacity) || opacity < 0.0F || opacity > 1.0F) {
        return pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1");
    }

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

[[nodiscard]] pp::foundation::Status validate_blend_mode(pp::paint::BlendMode blend_mode) noexcept
{
    switch (blend_mode) {
    case pp::paint::BlendMode::normal:
    case pp::paint::BlendMode::multiply:
    case pp::paint::BlendMode::screen:
    case pp::paint::BlendMode::color_dodge:
    case pp::paint::BlendMode::overlay:
        return pp::foundation::Status::success();
    }

    return pp::foundation::Status::invalid_argument("layer blend mode is not supported");
}

[[nodiscard]] pp::foundation::Status validate_frame_duration(std::uint32_t duration_ms) noexcept
{
    if (duration_ms < min_frame_duration_ms) {
        return pp::foundation::Status::invalid_argument("frame duration must be greater than zero");
    }

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

[[nodiscard]] pp::foundation::Status validate_layer_index(std::size_t index, std::size_t layer_count) noexcept
{
    if (index >= layer_count) {
        return pp::foundation::Status::out_of_range("layer index is outside the document");
    }

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

[[nodiscard]] pp::foundation::Status validate_frame_index(std::size_t index, std::size_t frame_count) noexcept
{
    if (index >= frame_count) {
        return pp::foundation::Status::out_of_range("frame index is outside the document");
    }

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

[[nodiscard]] pp::foundation::Result<std::size_t> byte_size(
    std::uint32_t width,
    std::uint32_t height,
    std::uint32_t components,
    const char* dimensions_overflow_message,
    const char* byte_size_overflow_message,
    const char* payload_limit_message,
    const char* addressable_memory_message) noexcept
{
    const auto width64 = static_cast<std::uint64_t>(width);
    const auto height64 = static_cast<std::uint64_t>(height);
    if (width64 > std::numeric_limits<std::uint64_t>::max() / height64) {
        return pp::foundation::Result<std::size_t>::failure(
            pp::foundation::Status::out_of_range(dimensions_overflow_message));
    }

    const auto pixels = width64 * height64;
    if (pixels > std::numeric_limits<std::uint64_t>::max() / components) {
        return pp::foundation::Result<std::size_t>::failure(
            pp::foundation::Status::out_of_range(byte_size_overflow_message));
    }

    const auto bytes = pixels * components;
    if (bytes > max_face_pixel_payload_bytes) {
        return pp::foundation::Result<std::size_t>::failure(
            pp::foundation::Status::out_of_range(payload_limit_message));
    }

    if (bytes > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
        return pp::foundation::Result<std::size_t>::failure(
            pp::foundation::Status::out_of_range(addressable_memory_message));
    }

    return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(bytes));
}

[[nodiscard]] pp::foundation::Result<std::size_t> rgba8_byte_size(
    std::uint32_t width,
    std::uint32_t height) noexcept
{
    return byte_size(
        width,
        height,
        rgba8_components,
        "face pixel dimensions overflow",
        "face pixel byte size overflows",
        "face pixel payload exceeds the configured limit",
        "face pixel payload exceeds addressable memory");
}

[[nodiscard]] pp::foundation::Result<std::size_t> alpha8_byte_size(
    std::uint32_t width,
    std::uint32_t height) noexcept
{
    return byte_size(
        width,
        height,
        alpha8_components,
        "selection mask dimensions overflow",
        "selection mask byte size overflows",
        "selection mask payload exceeds the configured limit",
        "selection mask payload exceeds addressable memory");
}

[[nodiscard]] pp::foundation::Status validate_face_pixels(
    LayerFacePixels pixels,
    std::uint32_t document_width,
    std::uint32_t document_height) noexcept
{
    if (pixels.face_index >= cube_face_count) {
        return pp::foundation::Status::out_of_range("cube face index is outside the document");
    }

    if (pixels.width == 0 || pixels.height == 0) {
        return pp::foundation::Status::invalid_argument("face pixel dimensions must be greater than zero");
    }

    if (pixels.x > document_width || pixels.width > document_width - pixels.x
        || pixels.y > document_height || pixels.height > document_height - pixels.y) {
        return pp::foundation::Status::out_of_range("face pixel rectangle is outside the document");
    }

    const auto expected_bytes = rgba8_byte_size(pixels.width, pixels.height);
    if (!expected_bytes) {
        return expected_bytes.status();
    }

    if (pixels.rgba8.size() != expected_bytes.value()) {
        return pp::foundation::Status::invalid_argument("face pixel payload byte size does not match dimensions");
    }

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

[[nodiscard]] pp::foundation::Status validate_selection_mask(
    SelectionMask mask,
    std::uint32_t document_width,
    std::uint32_t document_height) noexcept
{
    if (mask.face_index >= cube_face_count) {
        return pp::foundation::Status::out_of_range("selection mask cube face index is outside the document");
    }

    if (mask.width == 0 || mask.height == 0) {
        return pp::foundation::Status::invalid_argument("selection mask dimensions must be greater than zero");
    }

    if (mask.x > document_width || mask.width > document_width - mask.x
        || mask.y > document_height || mask.height > document_height - mask.y) {
        return pp::foundation::Status::out_of_range("selection mask rectangle is outside the document");
    }

    const auto expected_bytes = alpha8_byte_size(mask.width, mask.height);
    if (!expected_bytes) {
        return expected_bytes.status();
    }

    if (mask.alpha8.size() != expected_bytes.value()) {
        return pp::foundation::Status::invalid_argument("selection mask byte size does not match dimensions");
    }

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

[[nodiscard]] pp::foundation::Status validate_frame_face_pixels(
    std::span<const AnimationFrame> frames,
    std::uint32_t document_width,
    std::uint32_t document_height) noexcept
{
    for (const auto& frame : frames) {
        std::array<bool, cube_face_count> seen_faces {};
        for (const auto& pixels : frame.face_pixels) {
            const auto pixels_status = validate_face_pixels(pixels, document_width, document_height);
            if (!pixels_status.ok()) {
                return pixels_status;
            }

            if (seen_faces[pixels.face_index]) {
                return pp::foundation::Status::invalid_argument(
                    "snapshot contains duplicate face pixel payloads for a cube face");
            }
            seen_faces[pixels.face_index] = true;
        }
    }

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

}

pp::foundation::Result<CanvasDocument> CanvasDocument::create(DocumentConfig config)
{
    const auto status = validate_config(config);
    if (!status.ok()) {
        return pp::foundation::Result<CanvasDocument>::failure(status);
    }

    CanvasDocument document;
    document.width_ = config.width;
    document.height_ = config.height;
    document.frames_.push_back(AnimationFrame {});
    document.layers_.reserve(config.layer_count);
    for (std::uint32_t i = 0; i < config.layer_count; ++i) {
        document.layers_.push_back(Layer {
            .name = default_layer_name(i),
            .frames = document.frames_,
        });
    }

    return pp::foundation::Result<CanvasDocument>::success(document);
}

pp::foundation::Result<CanvasDocument> CanvasDocument::create_from_snapshot(DocumentSnapshotConfig config)
{
    const auto status = validate_config(DocumentConfig {
        .width = config.width,
        .height = config.height,
        .layer_count = static_cast<std::uint32_t>(config.layers.size()),
    });
    if (!status.ok()) {
        return pp::foundation::Result<CanvasDocument>::failure(status);
    }

    if (config.frames.empty()) {
        return pp::foundation::Result<CanvasDocument>::failure(
            pp::foundation::Status::invalid_argument("document must contain at least one frame"));
    }

    if (config.frames.size() > max_frame_count) {
        return pp::foundation::Result<CanvasDocument>::failure(
            pp::foundation::Status::out_of_range("document frame count exceeds the configured limit"));
    }

    CanvasDocument document;
    document.width_ = config.width;
    document.height_ = config.height;
    document.layers_.reserve(config.layers.size());
    for (const auto& layer_config : config.layers) {
        const auto name_status = validate_layer_name(layer_config.name);
        if (!name_status.ok()) {
            return pp::foundation::Result<CanvasDocument>::failure(name_status);
        }

        const auto opacity_status = validate_layer_opacity(layer_config.opacity);
        if (!opacity_status.ok()) {
            return pp::foundation::Result<CanvasDocument>::failure(opacity_status);
        }

        const auto blend_status = validate_blend_mode(layer_config.blend_mode);
        if (!blend_status.ok()) {
            return pp::foundation::Result<CanvasDocument>::failure(blend_status);
        }

        const auto layer_frames = layer_config.frames.empty() ? config.frames : layer_config.frames;
        if (layer_frames.empty()) {
            return pp::foundation::Result<CanvasDocument>::failure(
                pp::foundation::Status::invalid_argument("document layer must contain at least one frame"));
        }

        if (layer_frames.size() > max_frame_count) {
            return pp::foundation::Result<CanvasDocument>::failure(
                pp::foundation::Status::out_of_range("document layer frame count exceeds the configured limit"));
        }

        const auto face_pixels_status = validate_frame_face_pixels(layer_frames, config.width, config.height);
        if (!face_pixels_status.ok()) {
            return pp::foundation::Result<CanvasDocument>::failure(face_pixels_status);
        }

        for (const auto& frame_config : layer_frames) {
            const auto duration_status = validate_frame_duration(frame_config.duration_ms);
            if (!duration_status.ok()) {
                return pp::foundation::Result<CanvasDocument>::failure(duration_status);
            }
        }

        document.layers_.push_back(Layer {
            .name = std::string(layer_config.name),
            .visible = layer_config.visible,
            .alpha_locked = layer_config.alpha_locked,
            .opacity = layer_config.opacity,
            .blend_mode = layer_config.blend_mode,
            .frames = {},
        });
        document.layers_.back().frames.assign(layer_frames.begin(), layer_frames.end());
    }

    document.frames_.reserve(config.frames.size());
    for (const auto& frame_config : config.frames) {
        const auto duration_status = validate_frame_duration(frame_config.duration_ms);
        if (!duration_status.ok()) {
            return pp::foundation::Result<CanvasDocument>::failure(duration_status);
        }

        const auto face_pixels_status = validate_frame_face_pixels(
            std::span<const AnimationFrame>(&frame_config, 1),
            config.width,
            config.height);
        if (!face_pixels_status.ok()) {
            return pp::foundation::Result<CanvasDocument>::failure(face_pixels_status);
        }

        document.frames_.push_back(frame_config);
    }

    std::array<bool, cube_face_count> seen_selection_masks {};
    for (const auto& mask : config.selection_masks) {
        const auto mask_status = validate_selection_mask(mask, document.width_, document.height_);
        if (!mask_status.ok()) {
            return pp::foundation::Result<CanvasDocument>::failure(mask_status);
        }

        if (seen_selection_masks[mask.face_index]) {
            return pp::foundation::Result<CanvasDocument>::failure(
                pp::foundation::Status::invalid_argument(
                    "snapshot contains duplicate selection masks for a cube face"));
        }
        seen_selection_masks[mask.face_index] = true;
        document.selection_masks_.push_back(mask);
    }

    return pp::foundation::Result<CanvasDocument>::success(document);
}

std::uint32_t CanvasDocument::width() const noexcept
{
    return width_;
}

std::uint32_t CanvasDocument::height() const noexcept
{
    return height_;
}

std::size_t CanvasDocument::active_layer_index() const noexcept
{
    return active_layer_index_;
}

std::size_t CanvasDocument::active_frame_index() const noexcept
{
    return active_frame_index_;
}

std::uint64_t CanvasDocument::animation_duration_ms() const noexcept
{
    std::uint64_t duration = frame_duration_sum(frames_);
    for (const auto& layer : layers_) {
        duration = std::max(duration, frame_duration_sum(layer.frames));
    }
    return duration;
}

pp::foundation::Result<std::uint64_t> CanvasDocument::layer_animation_duration_ms(std::size_t index) const noexcept
{
    const auto index_status = validate_layer_index(index, layers_.size());
    if (!index_status.ok()) {
        return pp::foundation::Result<std::uint64_t>::failure(index_status);
    }

    return pp::foundation::Result<std::uint64_t>::success(frame_duration_sum(layers_[index].frames));
}

std::size_t CanvasDocument::face_pixel_payload_count() const noexcept
{
    std::size_t count = 0;
    for (const auto& layer : layers_) {
        for (const auto& frame : layer.frames) {
            count += frame.face_pixels.size();
        }
    }
    return count;
}

std::size_t CanvasDocument::selection_mask_payload_count() const noexcept
{
    return selection_masks_.size();
}

std::span<const Layer> CanvasDocument::layers() const noexcept
{
    return layers_;
}

std::span<const AnimationFrame> CanvasDocument::frames() const noexcept
{
    return frames_;
}

std::span<const SelectionMask> CanvasDocument::selection_masks() const noexcept
{
    return selection_masks_;
}

pp::foundation::Result<std::size_t> CanvasDocument::add_layer(std::string_view name)
{
    if (layers_.size() >= max_layer_count) {
        return pp::foundation::Result<std::size_t>::failure(
            pp::foundation::Status::out_of_range("document layer count exceeds the configured limit"));
    }

    Layer layer;
    if (name.empty()) {
        layer.name = default_layer_name(layers_.size());
    } else {
        const auto name_status = validate_layer_name(name);
        if (!name_status.ok()) {
            return pp::foundation::Result<std::size_t>::failure(name_status);
        }
        layer.name = std::string(name);
    }
    layer.frames = frames_;
    layers_.push_back(layer);
    active_layer_index_ = layers_.size() - 1U;
    return pp::foundation::Result<std::size_t>::success(active_layer_index_);
}

pp::foundation::Status CanvasDocument::remove_layer(std::size_t index)
{
    if (index >= layers_.size()) {
        return pp::foundation::Status::out_of_range("layer index is outside the document");
    }

    if (layers_.size() == 1U) {
        return pp::foundation::Status::invalid_argument("document must keep at least one layer");
    }

    layers_.erase(layers_.begin() + static_cast<std::ptrdiff_t>(index));
    if (active_layer_index_ >= layers_.size()) {
        active_layer_index_ = layers_.size() - 1U;
    } else if (active_layer_index_ > index) {
        --active_layer_index_;
    }

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

pp::foundation::Status CanvasDocument::move_layer(std::size_t from, std::size_t to)
{
    if (from >= layers_.size() || to >= layers_.size()) {
        return pp::foundation::Status::out_of_range("layer index is outside the document");
    }

    if (from == to) {
        return pp::foundation::Status::success();
    }

    auto layer = layers_[from];
    layers_.erase(layers_.begin() + static_cast<std::ptrdiff_t>(from));
    layers_.insert(layers_.begin() + static_cast<std::ptrdiff_t>(to), layer);

    if (active_layer_index_ == from) {
        active_layer_index_ = to;
    } else if (from < active_layer_index_ && active_layer_index_ <= to) {
        --active_layer_index_;
    } else if (to <= active_layer_index_ && active_layer_index_ < from) {
        ++active_layer_index_;
    }

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

pp::foundation::Status CanvasDocument::set_active_layer(std::size_t index) noexcept
{
    const auto index_status = validate_layer_index(index, layers_.size());
    if (!index_status.ok()) {
        return index_status;
    }

    active_layer_index_ = index;
    return pp::foundation::Status::success();
}

pp::foundation::Status CanvasDocument::rename_layer(std::size_t index, std::string_view name)
{
    const auto index_status = validate_layer_index(index, layers_.size());
    if (!index_status.ok()) {
        return index_status;
    }

    const auto name_status = validate_layer_name(name);
    if (!name_status.ok()) {
        return name_status;
    }

    layers_[index].name = std::string(name);
    return pp::foundation::Status::success();
}

pp::foundation::Status CanvasDocument::set_layer_visible(std::size_t index, bool visible) noexcept
{
    const auto index_status = validate_layer_index(index, layers_.size());
    if (!index_status.ok()) {
        return index_status;
    }

    layers_[index].visible = visible;
    return pp::foundation::Status::success();
}

pp::foundation::Status CanvasDocument::set_layer_alpha_locked(std::size_t index, bool alpha_locked) noexcept
{
    const auto index_status = validate_layer_index(index, layers_.size());
    if (!index_status.ok()) {
        return index_status;
    }

    layers_[index].alpha_locked = alpha_locked;
    return pp::foundation::Status::success();
}

pp::foundation::Status CanvasDocument::set_layer_opacity(std::size_t index, float opacity) noexcept
{
    const auto index_status = validate_layer_index(index, layers_.size());
    if (!index_status.ok()) {
        return index_status;
    }

    const auto opacity_status = validate_layer_opacity(opacity);
    if (!opacity_status.ok()) {
        return opacity_status;
    }

    layers_[index].opacity = opacity;
    return pp::foundation::Status::success();
}

pp::foundation::Status CanvasDocument::set_layer_blend_mode(std::size_t index, pp::paint::BlendMode blend_mode) noexcept
{
    const auto index_status = validate_layer_index(index, layers_.size());
    if (!index_status.ok()) {
        return index_status;
    }

    const auto blend_status = validate_blend_mode(blend_mode);
    if (!blend_status.ok()) {
        return blend_status;
    }

    layers_[index].blend_mode = blend_mode;
    return pp::foundation::Status::success();
}

pp::foundation::Result<std::size_t> CanvasDocument::add_frame(std::uint32_t duration_ms)
{
    if (frames_.size() >= max_frame_count) {
        return pp::foundation::Result<std::size_t>::failure(
            pp::foundation::Status::out_of_range("document frame count exceeds the configured limit"));
    }

    const auto duration_status = validate_frame_duration(duration_ms);
    if (!duration_status.ok()) {
        return pp::foundation::Result<std::size_t>::failure(
            duration_status);
    }

    frames_.push_back(AnimationFrame { .duration_ms = duration_ms, .face_pixels = {} });
    for (auto& layer : layers_) {
        layer.frames.push_back(AnimationFrame { .duration_ms = duration_ms, .face_pixels = {} });
    }
    active_frame_index_ = frames_.size() - 1U;
    return pp::foundation::Result<std::size_t>::success(active_frame_index_);
}

pp::foundation::Result<std::size_t> CanvasDocument::duplicate_frame(std::size_t index)
{
    const auto index_status = validate_frame_index(index, frames_.size());
    if (!index_status.ok()) {
        return pp::foundation::Result<std::size_t>::failure(
            index_status);
    }

    if (frames_.size() >= max_frame_count) {
        return pp::foundation::Result<std::size_t>::failure(
            pp::foundation::Status::out_of_range("document frame count exceeds the configured limit"));
    }

    const auto insert_at = index + 1U;
    frames_.insert(frames_.begin() + static_cast<std::ptrdiff_t>(insert_at), frames_[index]);
    for (auto& layer : layers_) {
        if (index < layer.frames.size()) {
            layer.frames.insert(
                layer.frames.begin() + static_cast<std::ptrdiff_t>(insert_at),
                layer.frames[index]);
        }
    }
    active_frame_index_ = insert_at;
    return pp::foundation::Result<std::size_t>::success(active_frame_index_);
}

pp::foundation::Status CanvasDocument::remove_frame(std::size_t index)
{
    const auto index_status = validate_frame_index(index, frames_.size());
    if (!index_status.ok()) {
        return index_status;
    }

    if (frames_.size() == 1U) {
        return pp::foundation::Status::invalid_argument("document must keep at least one frame");
    }

    frames_.erase(frames_.begin() + static_cast<std::ptrdiff_t>(index));
    for (auto& layer : layers_) {
        if (index < layer.frames.size() && layer.frames.size() > 1U) {
            layer.frames.erase(layer.frames.begin() + static_cast<std::ptrdiff_t>(index));
        }
    }
    if (active_frame_index_ >= frames_.size()) {
        active_frame_index_ = frames_.size() - 1U;
    } else if (active_frame_index_ > index) {
        --active_frame_index_;
    }

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

pp::foundation::Status CanvasDocument::move_frame(std::size_t from, std::size_t to)
{
    if (from >= frames_.size() || to >= frames_.size()) {
        return pp::foundation::Status::out_of_range("frame index is outside the document");
    }

    if (from == to) {
        return pp::foundation::Status::success();
    }

    const auto frame = frames_[from];
    frames_.erase(frames_.begin() + static_cast<std::ptrdiff_t>(from));
    frames_.insert(frames_.begin() + static_cast<std::ptrdiff_t>(to), frame);
    for (auto& layer : layers_) {
        if (from < layer.frames.size() && to < layer.frames.size()) {
            const auto layer_frame = layer.frames[from];
            layer.frames.erase(layer.frames.begin() + static_cast<std::ptrdiff_t>(from));
            layer.frames.insert(layer.frames.begin() + static_cast<std::ptrdiff_t>(to), layer_frame);
        }
    }

    if (active_frame_index_ == from) {
        active_frame_index_ = to;
    } else if (from < active_frame_index_ && active_frame_index_ <= to) {
        --active_frame_index_;
    } else if (to <= active_frame_index_ && active_frame_index_ < from) {
        ++active_frame_index_;
    }

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

pp::foundation::Status CanvasDocument::set_frame_duration(std::size_t index, std::uint32_t duration_ms) noexcept
{
    const auto index_status = validate_frame_index(index, frames_.size());
    if (!index_status.ok()) {
        return index_status;
    }

    const auto duration_status = validate_frame_duration(duration_ms);
    if (!duration_status.ok()) {
        return duration_status;
    }

    frames_[index].duration_ms = duration_ms;
    for (auto& layer : layers_) {
        if (index < layer.frames.size()) {
            layer.frames[index].duration_ms = duration_ms;
        }
    }
    return pp::foundation::Status::success();
}

pp::foundation::Status CanvasDocument::set_active_frame(std::size_t index) noexcept
{
    const auto index_status = validate_frame_index(index, frames_.size());
    if (!index_status.ok()) {
        return index_status;
    }

    active_frame_index_ = index;
    return pp::foundation::Status::success();
}

pp::foundation::Status CanvasDocument::set_layer_frame_face_pixels(
    std::size_t layer_index,
    std::size_t frame_index,
    LayerFacePixels pixels)
{
    const auto layer_status = validate_layer_index(layer_index, layers_.size());
    if (!layer_status.ok()) {
        return layer_status;
    }

    const auto frame_status = validate_frame_index(frame_index, layers_[layer_index].frames.size());
    if (!frame_status.ok()) {
        return frame_status;
    }

    const auto pixels_status = validate_face_pixels(pixels, width_, height_);
    if (!pixels_status.ok()) {
        return pixels_status;
    }

    auto& faces = layers_[layer_index].frames[frame_index].face_pixels;
    const auto existing = std::find_if(
        faces.begin(),
        faces.end(),
        [face_index = pixels.face_index](const LayerFacePixels& face) {
            return face.face_index == face_index;
        });
    if (existing == faces.end()) {
        faces.push_back(std::move(pixels));
    } else {
        *existing = std::move(pixels);
    }
    return pp::foundation::Status::success();
}

pp::foundation::Status CanvasDocument::set_selection_mask(SelectionMask mask)
{
    const auto mask_status = validate_selection_mask(mask, width_, height_);
    if (!mask_status.ok()) {
        return mask_status;
    }

    const auto existing = std::find_if(
        selection_masks_.begin(),
        selection_masks_.end(),
        [face_index = mask.face_index](const SelectionMask& candidate) {
            return candidate.face_index == face_index;
        });
    if (existing == selection_masks_.end()) {
        selection_masks_.push_back(std::move(mask));
    } else {
        *existing = std::move(mask);
    }
    return pp::foundation::Status::success();
}

pp::foundation::Status CanvasDocument::clear_selection_mask(std::uint32_t face_index) noexcept
{
    if (face_index >= cube_face_count) {
        return pp::foundation::Status::out_of_range("selection mask cube face index is outside the document");
    }

    const auto existing = std::find_if(
        selection_masks_.begin(),
        selection_masks_.end(),
        [face_index](const SelectionMask& candidate) {
            return candidate.face_index == face_index;
        });
    if (existing == selection_masks_.end()) {
        return pp::foundation::Status::out_of_range("selection mask face is not present");
    }

    selection_masks_.erase(existing);
    return pp::foundation::Status::success();
}

pp::foundation::Result<DocumentHistory> DocumentHistory::create(
    CanvasDocument initial_document,
    std::size_t max_entries)
{
    if (max_entries < min_document_history_entries) {
        return pp::foundation::Result<DocumentHistory>::failure(
            pp::foundation::Status::invalid_argument("document history must keep at least two entries"));
    }

    if (max_entries > max_document_history_entries) {
        return pp::foundation::Result<DocumentHistory>::failure(
            pp::foundation::Status::out_of_range("document history entry limit exceeds the configured limit"));
    }

    DocumentHistory history;
    history.max_entries_ = max_entries;
    history.entries_.reserve(max_entries);
    history.entries_.push_back(initial_document);
    return pp::foundation::Result<DocumentHistory>::success(history);
}

const CanvasDocument& DocumentHistory::current() const noexcept
{
    return entries_[current_index_];
}

std::size_t DocumentHistory::size() const noexcept
{
    return entries_.size();
}

std::size_t DocumentHistory::current_index() const noexcept
{
    return current_index_;
}

bool DocumentHistory::can_undo() const noexcept
{
    return current_index_ > 0;
}

bool DocumentHistory::can_redo() const noexcept
{
    return current_index_ + 1U < entries_.size();
}

pp::foundation::Status DocumentHistory::apply(CanvasDocument next_document)
{
    if (entries_.empty()) {
        return pp::foundation::Status::invalid_argument("document history is not initialized");
    }

    if (can_redo()) {
        entries_.erase(entries_.begin() + static_cast<std::ptrdiff_t>(current_index_ + 1U), entries_.end());
    }

    entries_.push_back(next_document);
    if (entries_.size() > max_entries_) {
        entries_.erase(entries_.begin());
    } else {
        ++current_index_;
    }

    current_index_ = entries_.size() - 1U;
    return pp::foundation::Status::success();
}

pp::foundation::Status DocumentHistory::undo() noexcept
{
    if (!can_undo()) {
        return pp::foundation::Status::out_of_range("document history has no undo entry");
    }

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

pp::foundation::Status DocumentHistory::redo() noexcept
{
    if (!can_redo()) {
        return pp::foundation::Status::out_of_range("document history has no redo entry");
    }

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

}
