#include <fesa/io/hdf5/writer.hpp>

#include <hdf5.h>

#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>

namespace fesa {
namespace {

constexpr std::string_view schema_version = "1.0.0";

class Hdf5Error final : public std::runtime_error {
public:
    Hdf5Error(std::string code, std::string message)
        : std::runtime_error{std::move(message)}, code_{std::move(code)} {}

    [[nodiscard]] const std::string& code() const noexcept {
        return code_;
    }

private:
    std::string code_;
};

template <class Function>
auto call_hdf5(Function&& function) {
    using Return = std::invoke_result_t<Function>;
    Return result{};
    H5E_BEGIN_TRY {
        result = std::forward<Function>(function)();
    }
    H5E_END_TRY;
    return result;
}

struct Hdf5Context final {
    std::string close_error;

    void record_close_result(
        const herr_t result,
        const std::string_view resource) {
        if (result < 0 && close_error.empty()) {
            close_error = "Failed to release HDF5 " +
                          std::string{resource} + ".";
        }
    }
};

using CloseFunction = herr_t (*)(hid_t);

class Hdf5Handle final {
public:
    Hdf5Handle() = default;

    Hdf5Handle(
        Hdf5Context& context,
        const hid_t id,
        CloseFunction close,
        std::string resource)
        : context_{&context},
          id_{id},
          close_{close},
          resource_{std::move(resource)} {}

    ~Hdf5Handle() {
        reset();
    }

    Hdf5Handle(const Hdf5Handle&) = delete;
    Hdf5Handle& operator=(const Hdf5Handle&) = delete;

    Hdf5Handle(Hdf5Handle&& other) noexcept
        : context_{std::exchange(other.context_, nullptr)},
          id_{std::exchange(other.id_, H5I_INVALID_HID)},
          close_{std::exchange(other.close_, nullptr)},
          resource_{std::move(other.resource_)} {}

    Hdf5Handle& operator=(Hdf5Handle&& other) noexcept {
        if (this != &other) {
            reset();
            context_ = std::exchange(other.context_, nullptr);
            id_ = std::exchange(other.id_, H5I_INVALID_HID);
            close_ = std::exchange(other.close_, nullptr);
            resource_ = std::move(other.resource_);
        }
        return *this;
    }

    [[nodiscard]] hid_t get() const noexcept {
        return id_;
    }

    void reset() noexcept {
        if (id_ >= 0 && close_ != nullptr && context_ != nullptr) {
            const herr_t result = call_hdf5([this] { return close_(id_); });
            context_->record_close_result(result, resource_);
        }
        id_ = H5I_INVALID_HID;
        close_ = nullptr;
        context_ = nullptr;
    }

private:
    Hdf5Context* context_{nullptr};
    hid_t id_{H5I_INVALID_HID};
    CloseFunction close_{nullptr};
    std::string resource_;
};

class Hdf5AllocatedString final {
public:
    Hdf5AllocatedString(Hdf5Context& context, char* value)
        : context_{context}, value_{value} {}

    ~Hdf5AllocatedString() {
        if (value_ != nullptr) {
            context_.record_close_result(
                call_hdf5([this] { return H5free_memory(value_); }),
                "variable-length string");
        }
    }

    Hdf5AllocatedString(const Hdf5AllocatedString&) = delete;
    Hdf5AllocatedString& operator=(const Hdf5AllocatedString&) = delete;

private:
    Hdf5Context& context_;
    char* value_;
};

class Hdf5VlenBuffer final {
public:
    Hdf5VlenBuffer(
        Hdf5Context& context,
        const hid_t type,
        const hid_t space,
        void* data)
        : context_{context}, type_{type}, space_{space}, data_{data} {}

    ~Hdf5VlenBuffer() {
        context_.record_close_result(
            call_hdf5([this] {
                return H5Treclaim(type_, space_, H5P_DEFAULT, data_);
            }),
            "variable-length dataset buffer");
    }

    Hdf5VlenBuffer(const Hdf5VlenBuffer&) = delete;
    Hdf5VlenBuffer& operator=(const Hdf5VlenBuffer&) = delete;

private:
    Hdf5Context& context_;
    hid_t type_;
    hid_t space_;
    void* data_;
};

[[noreturn]] void fail(std::string code, std::string message) {
    throw Hdf5Error{std::move(code), std::move(message)};
}

void check_status(
    const herr_t result,
    const std::string_view operation,
    const std::string_view code) {
    if (result < 0) {
        fail(
            std::string{code},
            "HDF5 operation failed: " + std::string{operation} + ".");
    }
}

Hdf5Handle checked_handle(
    Hdf5Context& context,
    const hid_t id,
    CloseFunction close,
    std::string resource,
    const std::string_view code) {
    if (id < 0) {
        fail(
            std::string{code},
            "Failed to open or create HDF5 " + resource + ".");
    }
    return {context, id, close, std::move(resource)};
}

Hdf5Handle create_group(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name) {
    const std::string owned_name{name};
    return checked_handle(
        context,
        call_hdf5([&] {
            return H5Gcreate2(
                parent,
                owned_name.c_str(),
                H5P_DEFAULT,
                H5P_DEFAULT,
                H5P_DEFAULT);
        }),
        &H5Gclose,
        "group '" + owned_name + "'",
        "hdf5.write_failed");
}

Hdf5Handle open_group(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name) {
    const std::string owned_name{name};
    return checked_handle(
        context,
        call_hdf5([&] {
            return H5Gopen2(parent, owned_name.c_str(), H5P_DEFAULT);
        }),
        &H5Gclose,
        "group '" + owned_name + "'",
        "hdf5.read_failed");
}

Hdf5Handle create_space(
    Hdf5Context& context,
    const std::span<const hsize_t> dimensions,
    const std::string_view code) {
    return checked_handle(
        context,
        call_hdf5([&] {
            return H5Screate_simple(
                static_cast<int>(dimensions.size()),
                dimensions.data(),
                nullptr);
        }),
        &H5Sclose,
        "dataspace",
        code);
}

Hdf5Handle create_scalar_space(
    Hdf5Context& context,
    const std::string_view code) {
    return checked_handle(
        context,
        call_hdf5([] { return H5Screate(H5S_SCALAR); }),
        &H5Sclose,
        "scalar dataspace",
        code);
}

Hdf5Handle create_utf8_type(
    Hdf5Context& context,
    const std::string_view code) {
    auto type = checked_handle(
        context,
        call_hdf5([] { return H5Tcopy(H5T_C_S1); }),
        &H5Tclose,
        "UTF-8 string datatype",
        code);
    check_status(
        call_hdf5([&] { return H5Tset_size(type.get(), H5T_VARIABLE); }),
        "set variable string size",
        code);
    check_status(
        call_hdf5([&] { return H5Tset_cset(type.get(), H5T_CSET_UTF8); }),
        "set UTF-8 character set",
        code);
    return type;
}

std::string path_utf8(const std::filesystem::path& path) {
    const std::u8string value = path.u8string();
    return {reinterpret_cast<const char*>(value.data()), value.size()};
}

Diagnostic error_diagnostic(std::string code, std::string message) {
    return {
        DiagnosticStage::results,
        Severity::error,
        std::move(code),
        std::move(message),
        std::nullopt,
    };
}

std::optional<std::int64_t> find_unknown_result_node(
    const ResultDatabase& database,
    const std::unordered_set<std::int64_t>& model_node_ids) {
    for (const ResultStep& step : database.steps) {
        for (const ResultFrame& frame : step.frames) {
            for (const NodeId node_id : frame.nodal.node_ids) {
                if (!model_node_ids.contains(node_id.value())) {
                    return node_id.value();
                }
            }
        }
    }
    return std::nullopt;
}

void write_string_attribute(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name,
    const std::string_view value) {
    auto type = create_utf8_type(context, "hdf5.write_failed");
    auto space = create_scalar_space(context, "hdf5.write_failed");
    const std::string owned_name{name};
    auto attribute = checked_handle(
        context,
        call_hdf5([&] {
            return H5Acreate2(
                parent,
                owned_name.c_str(),
                type.get(),
                space.get(),
                H5P_DEFAULT,
                H5P_DEFAULT);
        }),
        &H5Aclose,
        "attribute '" + owned_name + "'",
        "hdf5.write_failed");
    const std::string owned_value{value};
    const char* pointer = owned_value.c_str();
    check_status(
        call_hdf5([&] {
            return H5Awrite(attribute.get(), type.get(), &pointer);
        }),
        "write string attribute '" + owned_name + "'",
        "hdf5.write_failed");
}

void write_double_attribute(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name,
    const double value) {
    auto space = create_scalar_space(context, "hdf5.write_failed");
    const std::string owned_name{name};
    auto attribute = checked_handle(
        context,
        call_hdf5([&] {
            return H5Acreate2(
                parent,
                owned_name.c_str(),
                H5T_IEEE_F64LE,
                space.get(),
                H5P_DEFAULT,
                H5P_DEFAULT);
        }),
        &H5Aclose,
        "attribute '" + owned_name + "'",
        "hdf5.write_failed");
    check_status(
        call_hdf5([&] {
            return H5Awrite(attribute.get(), H5T_NATIVE_DOUBLE, &value);
        }),
        "write floating-point attribute '" + owned_name + "'",
        "hdf5.write_failed");
}

template <class Value>
void write_numeric_1d(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name,
    const hid_t file_type,
    const hid_t memory_type,
    const std::span<const Value> values) {
    const std::array<hsize_t, 1> dimensions{
        static_cast<hsize_t>(values.size())};
    auto space = create_space(context, dimensions, "hdf5.write_failed");
    const std::string owned_name{name};
    auto dataset = checked_handle(
        context,
        call_hdf5([&] {
            return H5Dcreate2(
                parent,
                owned_name.c_str(),
                file_type,
                space.get(),
                H5P_DEFAULT,
                H5P_DEFAULT,
                H5P_DEFAULT);
        }),
        &H5Dclose,
        "dataset '" + owned_name + "'",
        "hdf5.write_failed");
    if (!values.empty()) {
        check_status(
            call_hdf5([&] {
                return H5Dwrite(
                    dataset.get(),
                    memory_type,
                    H5S_ALL,
                    H5S_ALL,
                    H5P_DEFAULT,
                    values.data());
            }),
            "write dataset '" + owned_name + "'",
            "hdf5.write_failed");
    }
}

template <class Value>
void write_numeric_2d(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name,
    const hid_t file_type,
    const hid_t memory_type,
    const std::size_t row_count,
    const std::size_t column_count,
    const std::span<const Value> values) {
    if (row_count * column_count != values.size()) {
        fail(
            "hdf5.write_failed",
            "Internal HDF5 dataset shape does not match its values.");
    }
    const std::array<hsize_t, 2> dimensions{
        static_cast<hsize_t>(row_count),
        static_cast<hsize_t>(column_count),
    };
    auto space = create_space(context, dimensions, "hdf5.write_failed");
    const std::string owned_name{name};
    auto dataset = checked_handle(
        context,
        call_hdf5([&] {
            return H5Dcreate2(
                parent,
                owned_name.c_str(),
                file_type,
                space.get(),
                H5P_DEFAULT,
                H5P_DEFAULT,
                H5P_DEFAULT);
        }),
        &H5Dclose,
        "dataset '" + owned_name + "'",
        "hdf5.write_failed");
    if (!values.empty()) {
        check_status(
            call_hdf5([&] {
                return H5Dwrite(
                    dataset.get(),
                    memory_type,
                    H5S_ALL,
                    H5S_ALL,
                    H5P_DEFAULT,
                    values.data());
            }),
            "write dataset '" + owned_name + "'",
            "hdf5.write_failed");
    }
}

void write_strings_1d(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name,
    const std::span<const std::string> values) {
    auto type = create_utf8_type(context, "hdf5.write_failed");
    const std::array<hsize_t, 1> dimensions{
        static_cast<hsize_t>(values.size())};
    auto space = create_space(context, dimensions, "hdf5.write_failed");
    const std::string owned_name{name};
    auto dataset = checked_handle(
        context,
        call_hdf5([&] {
            return H5Dcreate2(
                parent,
                owned_name.c_str(),
                type.get(),
                space.get(),
                H5P_DEFAULT,
                H5P_DEFAULT,
                H5P_DEFAULT);
        }),
        &H5Dclose,
        "dataset '" + owned_name + "'",
        "hdf5.write_failed");
    if (!values.empty()) {
        std::vector<const char*> pointers;
        pointers.reserve(values.size());
        for (const std::string& value : values) {
            pointers.push_back(value.c_str());
        }
        check_status(
            call_hdf5([&] {
                return H5Dwrite(
                    dataset.get(),
                    type.get(),
                    H5S_ALL,
                    H5S_ALL,
                    H5P_DEFAULT,
                    pointers.data());
            }),
            "write string dataset '" + owned_name + "'",
            "hdf5.write_failed");
    }
}

void write_model(
    Hdf5Context& context,
    const hid_t file,
    const Domain& domain) {
    auto model_group = create_group(context, file, "model");

    std::unordered_map<std::int64_t, std::uint64_t> node_dense_indices;
    std::vector<std::uint64_t> node_dense;
    std::vector<std::int64_t> node_ids;
    std::vector<std::string> part_names;
    std::vector<std::string> instance_names;
    std::vector<std::int64_t> local_labels;
    std::vector<double> coordinates;
    node_dense.reserve(domain.nodes().size());
    node_ids.reserve(domain.nodes().size());
    part_names.reserve(domain.nodes().size());
    instance_names.reserve(domain.nodes().size());
    local_labels.reserve(domain.nodes().size());
    coordinates.reserve(domain.nodes().size() * 3U);
    for (std::size_t index = 0; index < domain.nodes().size(); ++index) {
        const Node& node = domain.nodes()[index];
        const auto dense_index = static_cast<std::uint64_t>(index);
        if (!node_dense_indices.emplace(node.id.value(), dense_index).second) {
            fail("hdf5.write_failed", "Domain contains duplicate node IDs.");
        }
        node_dense.push_back(dense_index);
        node_ids.push_back(node.id.value());
        part_names.push_back(node.origin.part_name);
        instance_names.push_back(node.origin.instance_name);
        local_labels.push_back(node.origin.local_label);
        coordinates.push_back(node.position.x);
        coordinates.push_back(node.position.y);
        coordinates.push_back(node.position.z);
    }

    {
        auto nodes_group = create_group(context, model_group.get(), "nodes");
        write_numeric_1d(
            context,
            nodes_group.get(),
            "dense_index",
            H5T_STD_U64LE,
            H5T_NATIVE_UINT64,
            std::span<const std::uint64_t>{node_dense});
        write_numeric_1d(
            context,
            nodes_group.get(),
            "internal_id",
            H5T_STD_I64LE,
            H5T_NATIVE_INT64,
            std::span<const std::int64_t>{node_ids});
        write_strings_1d(context, nodes_group.get(), "part_name", part_names);
        write_strings_1d(
            context, nodes_group.get(), "instance_name", instance_names);
        write_numeric_1d(
            context,
            nodes_group.get(),
            "local_label",
            H5T_STD_I64LE,
            H5T_NATIVE_INT64,
            std::span<const std::int64_t>{local_labels});
        write_numeric_2d(
            context,
            nodes_group.get(),
            "coordinates",
            H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE,
            domain.nodes().size(),
            3U,
            std::span<const double>{coordinates});
    }

    std::vector<std::uint64_t> element_dense;
    std::vector<std::int64_t> element_ids;
    std::vector<std::uint64_t> connectivity;
    std::vector<std::int64_t> element_section_ids;
    element_dense.reserve(domain.beam_elements().size());
    element_ids.reserve(domain.beam_elements().size());
    connectivity.reserve(domain.beam_elements().size() * 2U);
    element_section_ids.reserve(domain.beam_elements().size());
    for (std::size_t index = 0; index < domain.beam_elements().size(); ++index) {
        const BeamElement& element = domain.beam_elements()[index];
        const auto first = node_dense_indices.find(element.nodes[0].value());
        const auto second = node_dense_indices.find(element.nodes[1].value());
        if (first == node_dense_indices.end() ||
            second == node_dense_indices.end()) {
            fail(
                "hdf5.write_failed",
                "Beam connectivity references a missing node ID.");
        }
        element_dense.push_back(static_cast<std::uint64_t>(index));
        element_ids.push_back(element.id.value());
        connectivity.push_back(first->second);
        connectivity.push_back(second->second);
        element_section_ids.push_back(element.section.value());
    }

    {
        auto elements_group =
            create_group(context, model_group.get(), "elements");
        write_numeric_1d(
            context,
            elements_group.get(),
            "dense_index",
            H5T_STD_U64LE,
            H5T_NATIVE_UINT64,
            std::span<const std::uint64_t>{element_dense});
        write_numeric_1d(
            context,
            elements_group.get(),
            "internal_id",
            H5T_STD_I64LE,
            H5T_NATIVE_INT64,
            std::span<const std::int64_t>{element_ids});
        write_numeric_2d(
            context,
            elements_group.get(),
            "connectivity",
            H5T_STD_U64LE,
            H5T_NATIVE_UINT64,
            domain.beam_elements().size(),
            2U,
            std::span<const std::uint64_t>{connectivity});
        write_numeric_1d(
            context,
            elements_group.get(),
            "section_id",
            H5T_STD_I64LE,
            H5T_NATIVE_INT64,
            std::span<const std::int64_t>{element_section_ids});
    }

    std::vector<std::int64_t> section_ids;
    std::vector<double> shear_area_y;
    std::vector<double> shear_area_z;
    std::vector<std::uint8_t> shear_sources;
    section_ids.reserve(domain.sections().size());
    shear_area_y.reserve(domain.sections().size());
    shear_area_z.reserve(domain.sections().size());
    shear_sources.reserve(domain.sections().size());
    for (const BeamSection& section : domain.sections()) {
        section_ids.push_back(section.id.value());
        shear_area_y.push_back(section.shear_area_y);
        shear_area_z.push_back(section.shear_area_z);
        shear_sources.push_back(
            section.shear_source == ShearPropertySource::input ? 0U : 1U);
    }

    {
        auto sections_group =
            create_group(context, model_group.get(), "sections");
        write_numeric_1d(
            context,
            sections_group.get(),
            "internal_id",
            H5T_STD_I64LE,
            H5T_NATIVE_INT64,
            std::span<const std::int64_t>{section_ids});
        write_numeric_1d(
            context,
            sections_group.get(),
            "shear_area_y",
            H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE,
            std::span<const double>{shear_area_y});
        write_numeric_1d(
            context,
            sections_group.get(),
            "shear_area_z",
            H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE,
            std::span<const double>{shear_area_z});
        write_numeric_1d(
            context,
            sections_group.get(),
            "shear_source",
            H5T_STD_U8LE,
            H5T_NATIVE_UINT8,
            std::span<const std::uint8_t>{shear_sources});
    }
}

void write_results(
    Hdf5Context& context,
    const hid_t file,
    const ResultDatabase& database) {
    auto results_group = create_group(context, file, "results");
    auto steps_group = create_group(context, results_group.get(), "steps");
    for (std::size_t step_index = 0;
         step_index < database.steps.size();
         ++step_index) {
        const ResultStep& step = database.steps[step_index];
        auto step_group = create_group(
            context, steps_group.get(), std::to_string(step_index));
        write_string_attribute(context, step_group.get(), "name", step.name);
        auto frames_group =
            create_group(context, step_group.get(), "frames");
        for (std::size_t frame_index = 0;
             frame_index < step.frames.size();
             ++frame_index) {
            const ResultFrame& frame = step.frames[frame_index];
            auto frame_group = create_group(
                context, frames_group.get(), std::to_string(frame_index));
            write_double_attribute(
                context, frame_group.get(), "step_time", frame.step_time);
            auto nodal_group =
                create_group(context, frame_group.get(), "nodal");

            std::vector<std::int64_t> node_ids;
            std::vector<double> displacement;
            std::vector<double> reaction;
            node_ids.reserve(frame.nodal.node_ids.size());
            displacement.reserve(frame.nodal.displacement.size() * 6U);
            reaction.reserve(frame.nodal.reaction.size() * 6U);
            for (const NodeId node_id : frame.nodal.node_ids) {
                node_ids.push_back(node_id.value());
            }
            for (const auto& values : frame.nodal.displacement) {
                displacement.insert(
                    displacement.end(), values.begin(), values.end());
            }
            for (const auto& values : frame.nodal.reaction) {
                reaction.insert(reaction.end(), values.begin(), values.end());
            }
            write_numeric_1d(
                context,
                nodal_group.get(),
                "node_ids",
                H5T_STD_I64LE,
                H5T_NATIVE_INT64,
                std::span<const std::int64_t>{node_ids});
            write_numeric_2d(
                context,
                nodal_group.get(),
                "displacement",
                H5T_IEEE_F64LE,
                H5T_NATIVE_DOUBLE,
                frame.nodal.node_ids.size(),
                6U,
                std::span<const double>{displacement});
            write_numeric_2d(
                context,
                nodal_group.get(),
                "reaction",
                H5T_IEEE_F64LE,
                H5T_NATIVE_DOUBLE,
                frame.nodal.node_ids.size(),
                6U,
                std::span<const double>{reaction});
        }
    }
}

void require_numeric_type(
    Hdf5Context& context,
    const hid_t dataset,
    const hid_t expected,
    const std::string_view name) {
    auto type = checked_handle(
        context,
        call_hdf5([&] { return H5Dget_type(dataset); }),
        &H5Tclose,
        "datatype for dataset '" + std::string{name} + "'",
        "hdf5.read_failed");
    const htri_t equal =
        call_hdf5([&] { return H5Tequal(type.get(), expected); });
    if (equal <= 0) {
        fail(
            "hdf5.read_failed",
            "Dataset '" + std::string{name} +
                "' has an unexpected datatype.");
    }
}

std::size_t checked_size(
    const hsize_t value,
    const std::string_view description) {
    if (value > static_cast<hsize_t>(std::numeric_limits<std::size_t>::max())) {
        fail(
            "hdf5.read_failed",
            std::string{description} + " exceeds addressable memory.");
    }
    return static_cast<std::size_t>(value);
}

template <class Value>
std::vector<Value> read_numeric_1d(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name,
    const hid_t file_type,
    const hid_t memory_type) {
    const std::string owned_name{name};
    auto dataset = checked_handle(
        context,
        call_hdf5([&] {
            return H5Dopen2(parent, owned_name.c_str(), H5P_DEFAULT);
        }),
        &H5Dclose,
        "dataset '" + owned_name + "'",
        "hdf5.read_failed");
    require_numeric_type(context, dataset.get(), file_type, owned_name);
    auto space = checked_handle(
        context,
        call_hdf5([&] { return H5Dget_space(dataset.get()); }),
        &H5Sclose,
        "dataspace for dataset '" + owned_name + "'",
        "hdf5.read_failed");
    const int rank = call_hdf5(
        [&] { return H5Sget_simple_extent_ndims(space.get()); });
    if (rank != 1) {
        fail(
            "hdf5.read_failed",
            "Dataset '" + owned_name + "' must have rank 1.");
    }
    std::array<hsize_t, 1> dimensions{};
    check_status(
        call_hdf5([&] {
            return H5Sget_simple_extent_dims(
                space.get(), dimensions.data(), nullptr);
        }),
        "read shape for dataset '" + owned_name + "'",
        "hdf5.read_failed");
    std::vector<Value> values(
        checked_size(dimensions[0], "Dataset row count"));
    if (!values.empty()) {
        check_status(
            call_hdf5([&] {
                return H5Dread(
                    dataset.get(),
                    memory_type,
                    H5S_ALL,
                    H5S_ALL,
                    H5P_DEFAULT,
                    values.data());
            }),
            "read dataset '" + owned_name + "'",
            "hdf5.read_failed");
    }
    return values;
}

template <class Value>
std::vector<Value> read_numeric_2d(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name,
    const hid_t file_type,
    const hid_t memory_type,
    const std::size_t expected_columns) {
    const std::string owned_name{name};
    auto dataset = checked_handle(
        context,
        call_hdf5([&] {
            return H5Dopen2(parent, owned_name.c_str(), H5P_DEFAULT);
        }),
        &H5Dclose,
        "dataset '" + owned_name + "'",
        "hdf5.read_failed");
    require_numeric_type(context, dataset.get(), file_type, owned_name);
    auto space = checked_handle(
        context,
        call_hdf5([&] { return H5Dget_space(dataset.get()); }),
        &H5Sclose,
        "dataspace for dataset '" + owned_name + "'",
        "hdf5.read_failed");
    const int rank = call_hdf5(
        [&] { return H5Sget_simple_extent_ndims(space.get()); });
    if (rank != 2) {
        fail(
            "hdf5.read_failed",
            "Dataset '" + owned_name + "' must have rank 2.");
    }
    std::array<hsize_t, 2> dimensions{};
    check_status(
        call_hdf5([&] {
            return H5Sget_simple_extent_dims(
                space.get(), dimensions.data(), nullptr);
        }),
        "read shape for dataset '" + owned_name + "'",
        "hdf5.read_failed");
    if (dimensions[1] != static_cast<hsize_t>(expected_columns)) {
        fail(
            "hdf5.read_failed",
            "Dataset '" + owned_name +
                "' has an unexpected column count.");
    }
    const std::size_t row_count =
        checked_size(dimensions[0], "Dataset row count");
    if (row_count >
        std::numeric_limits<std::size_t>::max() / expected_columns) {
        fail("hdf5.read_failed", "Dataset element count overflows.");
    }
    std::vector<Value> values(row_count * expected_columns);
    if (!values.empty()) {
        check_status(
            call_hdf5([&] {
                return H5Dread(
                    dataset.get(),
                    memory_type,
                    H5S_ALL,
                    H5S_ALL,
                    H5P_DEFAULT,
                    values.data());
            }),
            "read dataset '" + owned_name + "'",
            "hdf5.read_failed");
    }
    return values;
}

void require_utf8_string_type(
    const hid_t type,
    const std::string_view description) {
    const H5T_class_t type_class =
        call_hdf5([&] { return H5Tget_class(type); });
    const htri_t is_variable =
        call_hdf5([&] { return H5Tis_variable_str(type); });
    const H5T_cset_t character_set =
        call_hdf5([&] { return H5Tget_cset(type); });
    if (
        type_class != H5T_STRING || is_variable <= 0 ||
        character_set != H5T_CSET_UTF8) {
        fail(
            "hdf5.read_failed",
            std::string{description} +
                " must be a variable-length UTF-8 string.");
    }
}

std::vector<std::string> read_strings_1d(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name) {
    const std::string owned_name{name};
    auto dataset = checked_handle(
        context,
        call_hdf5([&] {
            return H5Dopen2(parent, owned_name.c_str(), H5P_DEFAULT);
        }),
        &H5Dclose,
        "dataset '" + owned_name + "'",
        "hdf5.read_failed");
    auto type = checked_handle(
        context,
        call_hdf5([&] { return H5Dget_type(dataset.get()); }),
        &H5Tclose,
        "datatype for dataset '" + owned_name + "'",
        "hdf5.read_failed");
    require_utf8_string_type(type.get(), "Dataset '" + owned_name + "'");
    auto space = checked_handle(
        context,
        call_hdf5([&] { return H5Dget_space(dataset.get()); }),
        &H5Sclose,
        "dataspace for dataset '" + owned_name + "'",
        "hdf5.read_failed");
    const int rank = call_hdf5(
        [&] { return H5Sget_simple_extent_ndims(space.get()); });
    if (rank != 1) {
        fail(
            "hdf5.read_failed",
            "Dataset '" + owned_name + "' must have rank 1.");
    }
    std::array<hsize_t, 1> dimensions{};
    check_status(
        call_hdf5([&] {
            return H5Sget_simple_extent_dims(
                space.get(), dimensions.data(), nullptr);
        }),
        "read shape for dataset '" + owned_name + "'",
        "hdf5.read_failed");
    const std::size_t count =
        checked_size(dimensions[0], "String dataset row count");
    std::vector<char*> raw_values(count, nullptr);
    if (raw_values.empty()) {
        return {};
    }
    check_status(
        call_hdf5([&] {
            return H5Dread(
                dataset.get(),
                type.get(),
                H5S_ALL,
                H5S_ALL,
                H5P_DEFAULT,
                raw_values.data());
        }),
        "read string dataset '" + owned_name + "'",
        "hdf5.read_failed");
    Hdf5VlenBuffer reclaim{
        context, type.get(), space.get(), raw_values.data()};
    std::vector<std::string> values;
    values.reserve(count);
    for (const char* value : raw_values) {
        if (value == nullptr) {
            fail(
                "hdf5.read_failed",
                "String dataset '" + owned_name +
                    "' contains a null value.");
        }
        values.emplace_back(value);
    }
    return values;
}

std::string read_string_attribute(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name) {
    const std::string owned_name{name};
    auto attribute = checked_handle(
        context,
        call_hdf5([&] {
            return H5Aopen(parent, owned_name.c_str(), H5P_DEFAULT);
        }),
        &H5Aclose,
        "attribute '" + owned_name + "'",
        "hdf5.read_failed");
    auto type = checked_handle(
        context,
        call_hdf5([&] { return H5Aget_type(attribute.get()); }),
        &H5Tclose,
        "datatype for attribute '" + owned_name + "'",
        "hdf5.read_failed");
    require_utf8_string_type(type.get(), "Attribute '" + owned_name + "'");
    auto space = checked_handle(
        context,
        call_hdf5([&] { return H5Aget_space(attribute.get()); }),
        &H5Sclose,
        "dataspace for attribute '" + owned_name + "'",
        "hdf5.read_failed");
    const H5S_class_t space_class =
        call_hdf5([&] { return H5Sget_simple_extent_type(space.get()); });
    if (space_class != H5S_SCALAR) {
        fail(
            "hdf5.read_failed",
            "Attribute '" + owned_name + "' must be scalar.");
    }
    char* raw_value = nullptr;
    check_status(
        call_hdf5([&] {
            return H5Aread(attribute.get(), type.get(), &raw_value);
        }),
        "read string attribute '" + owned_name + "'",
        "hdf5.read_failed");
    Hdf5AllocatedString allocated{context, raw_value};
    if (raw_value == nullptr) {
        fail(
            "hdf5.read_failed",
            "Attribute '" + owned_name + "' contains a null string.");
    }
    return std::string{raw_value};
}

double read_double_attribute(
    Hdf5Context& context,
    const hid_t parent,
    const std::string_view name) {
    const std::string owned_name{name};
    auto attribute = checked_handle(
        context,
        call_hdf5([&] {
            return H5Aopen(parent, owned_name.c_str(), H5P_DEFAULT);
        }),
        &H5Aclose,
        "attribute '" + owned_name + "'",
        "hdf5.read_failed");
    auto type = checked_handle(
        context,
        call_hdf5([&] { return H5Aget_type(attribute.get()); }),
        &H5Tclose,
        "datatype for attribute '" + owned_name + "'",
        "hdf5.read_failed");
    const htri_t equal =
        call_hdf5([&] { return H5Tequal(type.get(), H5T_IEEE_F64LE); });
    if (equal <= 0) {
        fail(
            "hdf5.read_failed",
            "Attribute '" + owned_name +
                "' has an unexpected datatype.");
    }
    auto space = checked_handle(
        context,
        call_hdf5([&] { return H5Aget_space(attribute.get()); }),
        &H5Sclose,
        "dataspace for attribute '" + owned_name + "'",
        "hdf5.read_failed");
    const H5S_class_t space_class =
        call_hdf5([&] { return H5Sget_simple_extent_type(space.get()); });
    if (space_class != H5S_SCALAR) {
        fail(
            "hdf5.read_failed",
            "Attribute '" + owned_name + "' must be scalar.");
    }
    double value{};
    check_status(
        call_hdf5([&] {
            return H5Aread(attribute.get(), H5T_NATIVE_DOUBLE, &value);
        }),
        "read floating-point attribute '" + owned_name + "'",
        "hdf5.read_failed");
    return value;
}

std::size_t group_link_count(
    const hid_t group,
    const std::string_view description) {
    H5G_info_t information{};
    check_status(
        call_hdf5([&] { return H5Gget_info(group, &information); }),
        "inspect " + std::string{description},
        "hdf5.read_failed");
    return checked_size(information.nlinks, "Group link count");
}

Hdf5ModelSnapshot read_model(
    Hdf5Context& context,
    const hid_t file) {
    auto model_group = open_group(context, file, "model");
    Hdf5ModelSnapshot model;

    {
        auto nodes_group = open_group(context, model_group.get(), "nodes");
        const auto dense_indices = read_numeric_1d<std::uint64_t>(
            context,
            nodes_group.get(),
            "dense_index",
            H5T_STD_U64LE,
            H5T_NATIVE_UINT64);
        const auto ids = read_numeric_1d<std::int64_t>(
            context,
            nodes_group.get(),
            "internal_id",
            H5T_STD_I64LE,
            H5T_NATIVE_INT64);
        const auto part_names =
            read_strings_1d(context, nodes_group.get(), "part_name");
        const auto instance_names =
            read_strings_1d(context, nodes_group.get(), "instance_name");
        const auto local_labels = read_numeric_1d<std::int64_t>(
            context,
            nodes_group.get(),
            "local_label",
            H5T_STD_I64LE,
            H5T_NATIVE_INT64);
        const auto coordinates = read_numeric_2d<double>(
            context,
            nodes_group.get(),
            "coordinates",
            H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE,
            3U);
        const std::size_t count = dense_indices.size();
        if (
            ids.size() != count || part_names.size() != count ||
            instance_names.size() != count || local_labels.size() != count ||
            coordinates.size() != count * 3U) {
            fail(
                "hdf5.read_failed",
                "Node datasets have inconsistent row counts.");
        }
        model.nodes.reserve(count);
        std::unordered_set<std::int64_t> unique_ids;
        for (std::size_t index = 0; index < count; ++index) {
            if (dense_indices[index] != static_cast<std::uint64_t>(index)) {
                fail(
                    "hdf5.read_failed",
                    "Node dense_index must be contiguous and 0-based.");
            }
            if (!unique_ids.insert(ids[index]).second) {
                fail(
                    "hdf5.invalid_model_data",
                    "Serialized model contains duplicate node internal IDs.");
            }
            const Vec3 position{
                coordinates[index * 3U],
                coordinates[index * 3U + 1U],
                coordinates[index * 3U + 2U],
            };
            if (!is_finite(position)) {
                fail(
                    "hdf5.invalid_model_data",
                    "Serialized model contains nonfinite node coordinates.");
            }
            model.nodes.push_back({
                dense_indices[index],
                NodeId{ids[index]},
                EntityOrigin{
                    part_names[index],
                    instance_names[index],
                    local_labels[index],
                },
                position,
            });
        }
    }

    {
        auto sections_group =
            open_group(context, model_group.get(), "sections");
        const auto ids = read_numeric_1d<std::int64_t>(
            context,
            sections_group.get(),
            "internal_id",
            H5T_STD_I64LE,
            H5T_NATIVE_INT64);
        const auto shear_area_y = read_numeric_1d<double>(
            context,
            sections_group.get(),
            "shear_area_y",
            H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE);
        const auto shear_area_z = read_numeric_1d<double>(
            context,
            sections_group.get(),
            "shear_area_z",
            H5T_IEEE_F64LE,
            H5T_NATIVE_DOUBLE);
        const auto shear_sources = read_numeric_1d<std::uint8_t>(
            context,
            sections_group.get(),
            "shear_source",
            H5T_STD_U8LE,
            H5T_NATIVE_UINT8);
        if (
            shear_area_y.size() != ids.size() ||
            shear_area_z.size() != ids.size() ||
            shear_sources.size() != ids.size()) {
            fail(
                "hdf5.read_failed",
                "Section datasets have inconsistent row counts.");
        }
        model.sections.reserve(ids.size());
        std::unordered_set<std::int64_t> unique_ids;
        for (std::size_t index = 0; index < ids.size(); ++index) {
            if (!unique_ids.insert(ids[index]).second) {
                fail(
                    "hdf5.invalid_model_data",
                    "Serialized model contains duplicate section internal "
                    "IDs.");
            }
            if (
                !std::isfinite(shear_area_y[index]) ||
                !std::isfinite(shear_area_z[index]) ||
                shear_area_y[index] <= 0.0 || shear_area_z[index] <= 0.0) {
                fail(
                    "hdf5.invalid_model_data",
                    "Serialized model requires finite positive shear areas.");
            }
            ShearPropertySource source;
            if (shear_sources[index] == 0U) {
                source = ShearPropertySource::input;
            } else if (shear_sources[index] == 1U) {
                source = ShearPropertySource::phase1_default;
            } else {
                fail(
                    "hdf5.read_failed",
                    "Section shear_source contains an unknown value.");
            }
            model.sections.push_back({
                SectionId{ids[index]},
                shear_area_y[index],
                shear_area_z[index],
                source,
            });
        }
    }

    {
        auto elements_group =
            open_group(context, model_group.get(), "elements");
        const auto dense_indices = read_numeric_1d<std::uint64_t>(
            context,
            elements_group.get(),
            "dense_index",
            H5T_STD_U64LE,
            H5T_NATIVE_UINT64);
        const auto ids = read_numeric_1d<std::int64_t>(
            context,
            elements_group.get(),
            "internal_id",
            H5T_STD_I64LE,
            H5T_NATIVE_INT64);
        const auto connectivity = read_numeric_2d<std::uint64_t>(
            context,
            elements_group.get(),
            "connectivity",
            H5T_STD_U64LE,
            H5T_NATIVE_UINT64,
            2U);
        const auto section_ids = read_numeric_1d<std::int64_t>(
            context,
            elements_group.get(),
            "section_id",
            H5T_STD_I64LE,
            H5T_NATIVE_INT64);
        const std::size_t count = dense_indices.size();
        if (
            ids.size() != count || connectivity.size() != count * 2U ||
            section_ids.size() != count) {
            fail(
                "hdf5.read_failed",
                "Element datasets have inconsistent row counts.");
        }
        std::unordered_set<std::int64_t> available_sections;
        for (const Hdf5SectionSnapshot& section : model.sections) {
            available_sections.insert(section.id.value());
        }
        model.elements.reserve(count);
        std::unordered_set<std::int64_t> unique_ids;
        for (std::size_t index = 0; index < count; ++index) {
            if (dense_indices[index] != static_cast<std::uint64_t>(index)) {
                fail(
                    "hdf5.read_failed",
                    "Element dense_index must be contiguous and 0-based.");
            }
            if (!unique_ids.insert(ids[index]).second) {
                fail(
                    "hdf5.invalid_model_data",
                    "Serialized model contains duplicate element internal "
                    "IDs.");
            }
            const std::uint64_t first = connectivity[index * 2U];
            const std::uint64_t second = connectivity[index * 2U + 1U];
            if (
                first >= static_cast<std::uint64_t>(model.nodes.size()) ||
                second >= static_cast<std::uint64_t>(model.nodes.size())) {
                fail(
                    "hdf5.read_failed",
                    "Element connectivity references a missing dense node.");
            }
            if (!available_sections.contains(section_ids[index])) {
                fail(
                    "hdf5.read_failed",
                    "Element section_id references a missing section.");
            }
            model.elements.push_back({
                dense_indices[index],
                ElementId{ids[index]},
                {first, second},
                SectionId{section_ids[index]},
            });
        }
    }

    return model;
}

ResultDatabase read_database(
    Hdf5Context& context,
    const hid_t file,
    const std::string& version) {
    ResultDatabase database{version, {}};
    auto results_group = open_group(context, file, "results");
    auto steps_group = open_group(context, results_group.get(), "steps");
    const std::size_t step_count =
        group_link_count(steps_group.get(), "results/steps group");
    database.steps.reserve(step_count);
    for (std::size_t step_index = 0; step_index < step_count; ++step_index) {
        auto step_group = open_group(
            context, steps_group.get(), std::to_string(step_index));
        ResultStep step;
        step.name = read_string_attribute(context, step_group.get(), "name");
        auto frames_group =
            open_group(context, step_group.get(), "frames");
        const std::size_t frame_count =
            group_link_count(frames_group.get(), "frames group");
        step.frames.reserve(frame_count);
        for (std::size_t frame_index = 0;
             frame_index < frame_count;
             ++frame_index) {
            auto frame_group = open_group(
                context, frames_group.get(), std::to_string(frame_index));
            ResultFrame frame;
            frame.step_time = read_double_attribute(
                context, frame_group.get(), "step_time");
            auto nodal_group =
                open_group(context, frame_group.get(), "nodal");
            const auto node_ids = read_numeric_1d<std::int64_t>(
                context,
                nodal_group.get(),
                "node_ids",
                H5T_STD_I64LE,
                H5T_NATIVE_INT64);
            const auto displacement = read_numeric_2d<double>(
                context,
                nodal_group.get(),
                "displacement",
                H5T_IEEE_F64LE,
                H5T_NATIVE_DOUBLE,
                6U);
            const auto reaction = read_numeric_2d<double>(
                context,
                nodal_group.get(),
                "reaction",
                H5T_IEEE_F64LE,
                H5T_NATIVE_DOUBLE,
                6U);
            if (
                displacement.size() != node_ids.size() * 6U ||
                reaction.size() != node_ids.size() * 6U) {
                fail(
                    "hdf5.read_failed",
                    "Nodal result datasets have inconsistent row counts.");
            }
            frame.nodal.node_ids.reserve(node_ids.size());
            frame.nodal.displacement.reserve(node_ids.size());
            frame.nodal.reaction.reserve(node_ids.size());
            for (std::size_t index = 0; index < node_ids.size(); ++index) {
                frame.nodal.node_ids.emplace_back(node_ids[index]);
                std::array<double, 6> displacement_values{};
                std::array<double, 6> reaction_values{};
                std::copy_n(
                    displacement.begin() +
                        static_cast<std::ptrdiff_t>(index * 6U),
                    6U,
                    displacement_values.begin());
                std::copy_n(
                    reaction.begin() +
                        static_cast<std::ptrdiff_t>(index * 6U),
                    6U,
                    reaction_values.begin());
                frame.nodal.displacement.push_back(displacement_values);
                frame.nodal.reaction.push_back(reaction_values);
            }
            step.frames.push_back(std::move(frame));
        }
        database.steps.push_back(std::move(step));
    }
    return database;
}

}  // namespace

std::vector<Diagnostic> write_hdf5(
    const std::filesystem::path& path,
    const Domain& domain,
    const ResultDatabase& database) {
    const Status validation = validate_result_database(database);
    if (!validation.succeeded) {
        return validation.diagnostics;
    }
    if (database.schema_version != schema_version) {
        return {error_diagnostic(
            "hdf5.unsupported_schema",
            "HDF5 writer supports schema version 1.0.0 only.")};
    }
    for (const ResultStep& step : database.steps) {
        for (const ResultFrame& frame : step.frames) {
            if (!frame.diagnostics.empty()) {
                return {error_diagnostic(
                    "hdf5.unsupported_result_diagnostics",
                    "HDF5 schema 1.0.0 does not store frame diagnostics.")};
            }
        }
    }
    std::unordered_set<std::int64_t> model_node_ids;
    for (const Node& node : domain.nodes()) {
        model_node_ids.insert(node.id.value());
    }
    if (const auto unknown =
            find_unknown_result_node(database, model_node_ids)) {
        return {error_diagnostic(
            "hdf5.result_node_not_in_model",
            "Result node " + std::to_string(*unknown) +
                " is not present in the serialized model.")};
    }

    Hdf5Context context;
    try {
        const std::string encoded_path = path_utf8(path);
        auto file = checked_handle(
            context,
            call_hdf5([&] {
                return H5Fcreate(
                    encoded_path.c_str(),
                    H5F_ACC_TRUNC,
                    H5P_DEFAULT,
                    H5P_DEFAULT);
            }),
            &H5Fclose,
            "file '" + encoded_path + "'",
            "hdf5.create_failed");
        write_string_attribute(
            context, file.get(), "schema_version", schema_version);
        write_model(context, file.get(), domain);
        write_results(context, file.get(), database);
        check_status(
            call_hdf5([&] { return H5Fflush(file.get(), H5F_SCOPE_GLOBAL); }),
            "flush output file",
            "hdf5.write_failed");
        file.reset();
        if (!context.close_error.empty()) {
            fail("hdf5.close_failed", context.close_error);
        }
        return {};
    } catch (const Hdf5Error& error) {
        return {error_diagnostic(error.code(), error.what())};
    } catch (const std::exception& error) {
        return {error_diagnostic("hdf5.write_failed", error.what())};
    }
}

Hdf5ReadResult read_hdf5_results(const std::filesystem::path& path) {
    Hdf5Context context;
    try {
        const std::string encoded_path = path_utf8(path);
        auto file = checked_handle(
            context,
            call_hdf5([&] {
                return H5Fopen(
                    encoded_path.c_str(), H5F_ACC_RDONLY, H5P_DEFAULT);
            }),
            &H5Fclose,
            "file '" + encoded_path + "'",
            "hdf5.open_failed");
        const std::string version =
            read_string_attribute(context, file.get(), "schema_version");
        if (version != schema_version) {
            fail(
                "hdf5.unsupported_schema",
                "HDF5 reader supports schema version 1.0.0 only.");
        }
        Hdf5ModelSnapshot model = read_model(context, file.get());
        ResultDatabase database =
            read_database(context, file.get(), version);
        const Status validation = validate_result_database(database);
        if (!validation.succeeded) {
            file.reset();
            return {std::nullopt, validation.diagnostics, std::nullopt};
        }
        std::unordered_set<std::int64_t> model_node_ids;
        for (const Hdf5NodeSnapshot& node : model.nodes) {
            model_node_ids.insert(node.id.value());
        }
        if (const auto unknown =
                find_unknown_result_node(database, model_node_ids)) {
            fail(
                "hdf5.result_node_not_in_model",
                "Result node " + std::to_string(*unknown) +
                    " is not present in the serialized model.");
        }
        file.reset();
        if (!context.close_error.empty()) {
            fail("hdf5.close_failed", context.close_error);
        }
        return {
            std::move(database),
            {},
            std::move(model),
        };
    } catch (const Hdf5Error& error) {
        return {
            std::nullopt,
            {error_diagnostic(error.code(), error.what())},
            std::nullopt,
        };
    } catch (const std::exception& error) {
        return {
            std::nullopt,
            {error_diagnostic("hdf5.read_failed", error.what())},
            std::nullopt,
        };
    }
}

}  // namespace fesa
