#include <fesa/model/domain_builder.hpp>

#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <map>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>

namespace fesa {
namespace {

using IndexLookup = std::unordered_map<std::int64_t, std::size_t>;
using OriginKey = std::pair<std::string, std::int64_t>;

void add_error(
    std::vector<Diagnostic>& diagnostics,
    std::string code,
    std::string message) {
    diagnostics.push_back({
        DiagnosticStage::model,
        Severity::error,
        std::move(code),
        std::move(message),
        std::nullopt,
    });
}

OriginKey origin_key(const EntityOrigin& origin) {
    return {origin.instance_name, origin.local_label};
}

template <class Entity, class IdAccessor>
IndexLookup collect_ids(
    const std::vector<Entity>& entities,
    IdAccessor id_of,
    const std::string_view duplicate_code,
    const std::string_view entity_name,
    std::vector<Diagnostic>& diagnostics) {
    IndexLookup indices;
    for (std::size_t index = 0; index < entities.size(); ++index) {
        const std::int64_t id = id_of(entities[index]).value();
        if (!indices.emplace(id, index).second) {
            add_error(
                diagnostics,
                std::string{duplicate_code},
                "Duplicate " + std::string{entity_name} +
                    " internal ID " + std::to_string(id) + ".");
        }
    }
    return indices;
}

template <class Entity>
void collect_duplicate_origins(
    const std::vector<Entity>& entities,
    const std::string_view duplicate_code,
    const std::string_view entity_name,
    std::vector<Diagnostic>& diagnostics) {
    std::map<OriginKey, std::size_t> origins;
    for (std::size_t index = 0; index < entities.size(); ++index) {
        const bool inserted =
            origins.emplace(origin_key(entities[index].origin), index).second;
        if (!inserted) {
            const EntityOrigin& origin = entities[index].origin;
            add_error(
                diagnostics,
                std::string{duplicate_code},
                "Duplicate " + std::string{entity_name} + " origin (" +
                    origin.instance_name + ", " +
                    std::to_string(origin.local_label) + ").");
        }
    }
}

bool has_id(const IndexLookup& indices, const std::int64_t id) {
    return indices.contains(id);
}

bool is_finite(const std::array<double, 2>& value) {
    return std::isfinite(value[0]) && std::isfinite(value[1]);
}

bool is_finite(const std::array<double, 6>& value) {
    return std::ranges::all_of(
        value, [](const double component) {
            return std::isfinite(component);
        });
}

void validate_section_property(
    const BeamSection& section,
    const double value,
    const std::string_view property_name,
    std::vector<Diagnostic>& diagnostics) {
    if (!std::isfinite(value)) {
        add_error(
            diagnostics,
            "model.nonfinite_value",
            "Section " + std::to_string(section.id.value()) + " has a "
                "nonfinite " +
                std::string{property_name} + ".");
    } else if (value <= 0.0) {
        add_error(
            diagnostics,
            "model.invalid_section",
            "Section " + std::to_string(section.id.value()) + " requires " +
                std::string{property_name} + " > 0.");
    }
}

double length(const Vec3 value) {
    return std::hypot(value.x, value.y, value.z);
}

bool is_parallel(const Vec3 first, const Vec3 second) {
    const double first_length = length(first);
    const double second_length = length(second);
    if (first_length == 0.0 || second_length == 0.0) {
        return false;
    }

    const Vec3 first_unit{
        first.x / first_length,
        first.y / first_length,
        first.z / first_length,
    };
    const Vec3 second_unit{
        second.x / second_length,
        second.y / second_length,
        second.z / second_length,
    };
    const Vec3 cross{
        first_unit.y * second_unit.z -
            first_unit.z * second_unit.y,
        first_unit.z * second_unit.x -
            first_unit.x * second_unit.z,
        first_unit.x * second_unit.y -
            first_unit.y * second_unit.x,
    };
    return length(cross) <=
           64.0 * std::numeric_limits<double>::epsilon();
}

}  // namespace

void DomainBuilder::add_node(Node value) {
    nodes_.push_back(std::move(value));
}

void DomainBuilder::add_material(IsotropicElastic value) {
    materials_.push_back(std::move(value));
}

void DomainBuilder::add_section(BeamSection value) {
    sections_.push_back(std::move(value));
}

void DomainBuilder::add_beam_element(BeamElement value) {
    beam_elements_.push_back(std::move(value));
}

void DomainBuilder::add_node_set(NodeSet value) {
    node_sets_.push_back(std::move(value));
}

void DomainBuilder::add_element_set(ElementSet value) {
    element_sets_.push_back(std::move(value));
}

void DomainBuilder::set_step(StepDefinition value) {
    step_ = std::move(value);
}

DomainBuildResult DomainBuilder::build() && {
    std::vector<Diagnostic> diagnostics;

    const IndexLookup node_indices = collect_ids(
        nodes_,
        [](const Node& node) { return node.id; },
        "model.duplicate_node_id",
        "node",
        diagnostics);
    const IndexLookup material_indices = collect_ids(
        materials_,
        [](const IsotropicElastic& material) { return material.id; },
        "model.duplicate_material_id",
        "material",
        diagnostics);
    const IndexLookup section_indices = collect_ids(
        sections_,
        [](const BeamSection& section) { return section.id; },
        "model.duplicate_section_id",
        "section",
        diagnostics);
    const IndexLookup element_indices = collect_ids(
        beam_elements_,
        [](const BeamElement& element) { return element.id; },
        "model.duplicate_element_id",
        "Beam element",
        diagnostics);

    collect_duplicate_origins(
        nodes_,
        "model.duplicate_node_origin",
        "node",
        diagnostics);
    collect_duplicate_origins(
        beam_elements_,
        "model.duplicate_element_origin",
        "Beam element",
        diagnostics);

    for (const Node& node : nodes_) {
        if (!is_finite(node.position)) {
            add_error(
                diagnostics,
                "model.nonfinite_value",
                "Node " + std::to_string(node.id.value()) +
                    " has a nonfinite coordinate.");
        }
    }

    for (const IsotropicElastic& material : materials_) {
        if (!std::isfinite(material.young)) {
            add_error(
                diagnostics,
                "model.nonfinite_value",
                "Material " + std::to_string(material.id.value()) +
                    " has a nonfinite Young's modulus.");
        } else if (material.young <= 0.0) {
            add_error(
                diagnostics,
                "model.invalid_material",
                "Material " + std::to_string(material.id.value()) +
                    " requires E > 0.");
        }

        if (!std::isfinite(material.poisson)) {
            add_error(
                diagnostics,
                "model.nonfinite_value",
                "Material " + std::to_string(material.id.value()) +
                    " has a nonfinite Poisson ratio.");
        } else if (
            material.poisson <= -1.0 || material.poisson >= 0.5) {
            add_error(
                diagnostics,
                "model.invalid_material",
                "Material " + std::to_string(material.id.value()) +
                    " requires -1 < nu < 0.5.");
        }
    }

    for (const BeamSection& section : sections_) {
        validate_section_property(
            section, section.area, "A", diagnostics);
        validate_section_property(section, section.iy, "Iy", diagnostics);
        validate_section_property(section, section.iz, "Iz", diagnostics);
        validate_section_property(
            section, section.torsion_j, "J", diagnostics);
        validate_section_property(
            section, section.shear_area_y, "Asy", diagnostics);
        validate_section_property(
            section, section.shear_area_z, "Asz", diagnostics);

        if (!is_finite(section.orientation)) {
            add_error(
                diagnostics,
                "model.nonfinite_value",
                "Section " + std::to_string(section.id.value()) +
                    " has a nonfinite orientation.");
        } else if (length(section.orientation) == 0.0) {
            add_error(
                diagnostics,
                "model.invalid_orientation",
                "Section " + std::to_string(section.id.value()) +
                    " has a zero orientation vector.");
        }

        for (const auto& recovery_point : section.recovery_points) {
            if (!is_finite(recovery_point)) {
                add_error(
                    diagnostics,
                    "model.nonfinite_value",
                    "Section " + std::to_string(section.id.value()) +
                        " has a nonfinite recovery point.");
            }
        }
    }

    for (const BeamElement& element : beam_elements_) {
        const bool has_first_node =
            has_id(node_indices, element.nodes[0].value());
        const bool has_second_node =
            has_id(node_indices, element.nodes[1].value());
        if (!has_first_node) {
            add_error(
                diagnostics,
                "model.missing_node_reference",
                "Beam element " + std::to_string(element.id.value()) +
                    " references missing node " +
                    std::to_string(element.nodes[0].value()) + ".");
        }
        if (!has_second_node) {
            add_error(
                diagnostics,
                "model.missing_node_reference",
                "Beam element " + std::to_string(element.id.value()) +
                    " references missing node " +
                    std::to_string(element.nodes[1].value()) + ".");
        }
        if (!has_id(material_indices, element.material.value())) {
            add_error(
                diagnostics,
                "model.missing_material_reference",
                "Beam element " + std::to_string(element.id.value()) +
                    " references missing material " +
                    std::to_string(element.material.value()) + ".");
        }
        if (!has_id(section_indices, element.section.value())) {
            add_error(
                diagnostics,
                "model.missing_section_reference",
                "Beam element " + std::to_string(element.id.value()) +
                    " references missing section " +
                    std::to_string(element.section.value()) + ".");
        }

        if (!has_first_node || !has_second_node) {
            continue;
        }

        const Node& first = nodes_[node_indices.at(element.nodes[0].value())];
        const Node& second =
            nodes_[node_indices.at(element.nodes[1].value())];
        if (!is_finite(first.position) || !is_finite(second.position)) {
            continue;
        }

        const Vec3 axis{
            second.position.x - first.position.x,
            second.position.y - first.position.y,
            second.position.z - first.position.z,
        };
        if (length(axis) == 0.0) {
            add_error(
                diagnostics,
                "model.zero_length_element",
                "Beam element " + std::to_string(element.id.value()) +
                    " has zero length.");
            continue;
        }

        const auto section_found =
            section_indices.find(element.section.value());
        if (section_found == section_indices.end()) {
            continue;
        }
        const Vec3 orientation =
            sections_[section_found->second].orientation;
        if (is_finite(orientation) && length(orientation) > 0.0 &&
            is_parallel(axis, orientation)) {
            add_error(
                diagnostics,
                "model.invalid_orientation",
                "Beam element " + std::to_string(element.id.value()) +
                    " has an orientation parallel to its axis.");
        }
    }

    for (const NodeSet& node_set : node_sets_) {
        for (const NodeId member : node_set.members) {
            if (!has_id(node_indices, member.value())) {
                add_error(
                    diagnostics,
                    "model.missing_node_reference",
                    "Node set " + node_set.name +
                        " references missing node " +
                        std::to_string(member.value()) + ".");
            }
        }
    }

    for (const ElementSet& element_set : element_sets_) {
        for (const ElementId member : element_set.members) {
            if (!has_id(element_indices, member.value())) {
                add_error(
                    diagnostics,
                    "model.missing_element_reference",
                    "Element set " + element_set.name +
                        " references missing element " +
                        std::to_string(member.value()) + ".");
            }
        }
    }

    if (!step_.has_value()) {
        add_error(
            diagnostics,
            "model.missing_step",
            "The Domain requires one linear static step.");
    } else {
        std::map<std::pair<std::int64_t, std::uint8_t>, double>
            prescribed_values;
        for (const PrescribedDof& prescribed : step_->prescribed_dofs) {
            if (!has_id(node_indices, prescribed.node.value())) {
                add_error(
                    diagnostics,
                    "model.missing_node_reference",
                    "Boundary condition references missing node " +
                        std::to_string(prescribed.node.value()) + ".");
            }
            if (prescribed.dof < 1 || prescribed.dof > 6) {
                add_error(
                    diagnostics,
                    "model.invalid_dof",
                    "Boundary condition DOF must be in [1, 6].");
            }
            if (!std::isfinite(prescribed.value)) {
                add_error(
                    diagnostics,
                    "model.nonfinite_value",
                    "Boundary condition has a nonfinite value.");
            }

            const auto key =
                std::pair{prescribed.node.value(), prescribed.dof};
            if (!prescribed_values.emplace(key, prescribed.value).second) {
                add_error(
                    diagnostics,
                    "model.conflicting_boundary_condition",
                    "A node DOF has more than one prescribed value.");
            }
        }

        for (const NodalLoad& load : step_->nodal_loads) {
            if (!has_id(node_indices, load.node.value())) {
                add_error(
                    diagnostics,
                    "model.missing_node_reference",
                    "Concentrated load references missing node " +
                        std::to_string(load.node.value()) + ".");
            }
            if (!is_finite(load.values)) {
                add_error(
                    diagnostics,
                    "model.nonfinite_value",
                    "Concentrated load has a nonfinite component.");
            }
        }
    }

    if (!diagnostics.empty()) {
        return {std::nullopt, std::move(diagnostics)};
    }

    return {
        Domain{
            std::move(nodes_),
            std::move(beam_elements_),
            std::move(materials_),
            std::move(sections_),
            std::move(node_sets_),
            std::move(element_sets_),
            std::move(*step_),
        },
        {},
    };
}

}  // namespace fesa
