#include "fesa/model/shell_geometry.hpp"

#include <algorithm>
#include <cmath>
#include <tuple>
#include <utility>

namespace fesa {
namespace {

using Vector3 = std::array<double, 3>;

constexpr std::array<double, 4> kXiSigns{-1.0, 1.0, 1.0, -1.0};
constexpr std::array<double, 4> kEtaSigns{-1.0, -1.0, 1.0, 1.0};

struct ShapeData {
    std::array<double, 4> values;
    std::array<double, 4> xiDerivatives;
    std::array<double, 4> etaDerivatives;
};

struct ElementWork {
    std::array<Vector3, 4> coordinates;
    Vector3 normal;
    double areaWeight;
};

Vector3 add(const Vector3& left, const Vector3& right) {
    return {
        left[0] + right[0], left[1] + right[1], left[2] + right[2]};
}

Vector3 subtract(const Vector3& left, const Vector3& right) {
    return {
        left[0] - right[0], left[1] - right[1], left[2] - right[2]};
}

Vector3 scale(double factor, const Vector3& value) {
    return {factor * value[0], factor * value[1], factor * value[2]};
}

double dot(const Vector3& left, const Vector3& right) {
    return left[0] * right[0] + left[1] * right[1] + left[2] * right[2];
}

Vector3 cross(const Vector3& left, const Vector3& right) {
    return {
        left[1] * right[2] - left[2] * right[1],
        left[2] * right[0] - left[0] * right[2],
        left[0] * right[1] - left[1] * right[0]};
}

double norm(const Vector3& value) {
    return std::hypot(value[0], value[1], value[2]);
}

bool isFinite(const Vector3& value) {
    return std::all_of(value.begin(), value.end(), [](double component) {
        return std::isfinite(component);
    });
}

ShapeData shapeData(double xi, double eta) {
    ShapeData data{};
    for (std::size_t node = 0U; node < 4U; ++node) {
        data.values[node] =
            0.25 * (1.0 + kXiSigns[node] * xi) *
            (1.0 + kEtaSigns[node] * eta);
        data.xiDerivatives[node] =
            0.25 * kXiSigns[node] * (1.0 + kEtaSigns[node] * eta);
        data.etaDerivatives[node] =
            0.25 * kEtaSigns[node] * (1.0 + kXiSigns[node] * xi);
    }
    return data;
}

Vector3 weightedSum(
    const std::array<double, 4>& weights,
    const std::array<Vector3, 4>& values) {
    Vector3 result{};
    for (std::size_t node = 0U; node < values.size(); ++node) {
        result = add(result, scale(weights[node], values[node]));
    }
    return result;
}

Vector3 derivativeSum(
    const std::array<double, 4>& derivatives,
    const std::array<Vector3, 4>& coordinates) {
    // Shape derivatives sum to zero, so translating by node 1 improves the
    // numerical cancellation without changing the covariant tangent.
    std::array<Vector3, 4> relative{};
    for (std::size_t node = 0U; node < coordinates.size(); ++node) {
        relative[node] = subtract(coordinates[node], coordinates[0]);
    }
    return weightedSum(derivatives, relative);
}

Result<ShellGeometry> geometryFailure(
    std::string code,
    const SourceLocation& location,
    std::string keyword,
    std::string identity,
    std::string message) {
    return Result<ShellGeometry>::failure(Status::failure(
        FailureCategory::model,
        {{Severity::error,
          std::move(code),
          location,
          std::move(keyword),
          std::move(identity),
          std::move(message)}}));
}

bool sameCoordinates(const Vector3& left, const Vector3& right) {
    return left == right;
}

double orientation(
    const Vector3& first,
    const Vector3& second,
    const Vector3& third,
    const Vector3& normal) {
    return dot(cross(subtract(second, first), subtract(third, first)), normal);
}

bool hasOppositeSigns(double first, double second) {
    return (first < 0.0 && second > 0.0) ||
           (first > 0.0 && second < 0.0);
}

bool segmentsProperlyIntersect(
    const Vector3& firstStart,
    const Vector3& firstEnd,
    const Vector3& secondStart,
    const Vector3& secondEnd,
    const Vector3& normal) {
    const double firstSideStart =
        orientation(firstStart, firstEnd, secondStart, normal);
    const double firstSideEnd =
        orientation(firstStart, firstEnd, secondEnd, normal);
    const double secondSideStart =
        orientation(secondStart, secondEnd, firstStart, normal);
    const double secondSideEnd =
        orientation(secondStart, secondEnd, firstEnd, normal);
    return hasOppositeSigns(firstSideStart, firstSideEnd) &&
           hasOppositeSigns(secondSideStart, secondSideEnd);
}

bool sourceIdentityLess(
    const Mitc4ShellDefinition& left,
    std::size_t leftIndex,
    const Mitc4ShellDefinition& right,
    std::size_t rightIndex) {
    return std::tie(
               left.sourceId.instanceName,
               left.sourceId.sourceLabel,
               left.sourceId.sourceLabelText,
               leftIndex) <
           std::tie(
               right.sourceId.instanceName,
               right.sourceId.sourceLabel,
               right.sourceId.sourceLabelText,
               rightIndex);
}

} // namespace

const std::array<ShellGeometryValidationPoint, 17>&
shellGeometryValidationPoints() noexcept {
    static const std::array<ShellGeometryValidationPoint, 17> points = [] {
        const double g = 1.0 / std::sqrt(3.0);
        return std::array<ShellGeometryValidationPoint, 17>{
            ShellGeometryValidationPoint{
                ShellGeometryPointKind::center, 0U, {0.0, 0.0, 0.0}},
            {ShellGeometryPointKind::stiffness, 0U, {-g, -g, -g}},
            {ShellGeometryPointKind::stiffness, 1U, {-g, -g, g}},
            {ShellGeometryPointKind::stiffness, 2U, {g, -g, -g}},
            {ShellGeometryPointKind::stiffness, 3U, {g, -g, g}},
            {ShellGeometryPointKind::stiffness, 4U, {g, g, -g}},
            {ShellGeometryPointKind::stiffness, 5U, {g, g, g}},
            {ShellGeometryPointKind::stiffness, 6U, {-g, g, -g}},
            {ShellGeometryPointKind::stiffness, 7U, {-g, g, g}},
            {ShellGeometryPointKind::tying, 0U, {0.0, -1.0, 0.0}},
            {ShellGeometryPointKind::tying, 1U, {0.0, 1.0, 0.0}},
            {ShellGeometryPointKind::tying, 2U, {-1.0, 0.0, 0.0}},
            {ShellGeometryPointKind::tying, 3U, {1.0, 0.0, 0.0}},
            {ShellGeometryPointKind::recovery, 0U, {-g, -g, 0.0}},
            {ShellGeometryPointKind::recovery, 1U, {g, -g, 0.0}},
            {ShellGeometryPointKind::recovery, 2U, {g, g, 0.0}},
            {ShellGeometryPointKind::recovery, 3U, {-g, g, 0.0}}};
    }();
    return points;
}

Result<ShellGeometry> preprocessShellGeometry(
    const std::vector<Node>& nodes,
    const std::vector<Mitc4ShellDefinition>& elements,
    const std::vector<ShellSection>& sections) {
    ShellGeometry geometry;
    geometry.elementData.reserve(elements.size());
    std::vector<ElementWork> work;
    work.reserve(elements.size());

    const double g = 1.0 / std::sqrt(3.0);
    const std::array<Vector3, 4> surfaceGaussPoints{
        Vector3{-g, -g, 0.0},
        Vector3{g, -g, 0.0},
        Vector3{g, g, 0.0},
        Vector3{-g, g, 0.0}};

    for (std::size_t elementIndex = 0U;
         elementIndex < elements.size();
         ++elementIndex) {
        const auto& element = elements[elementIndex];
        ElementWork current{};
        for (std::size_t localNode = 0U; localNode < 4U; ++localNode) {
            if (element.nodeIndices[localNode] >= nodes.size()) {
                return geometryFailure(
                    "invalid-shell-geometry", element.location, "ELEMENT",
                    element.sourceId.sourceLabelText,
                    "Shell geometry references an unavailable internal node.");
            }
            current.coordinates[localNode] =
                nodes[element.nodeIndices[localNode]].coordinates;
            if (!isFinite(current.coordinates[localNode])) {
                return geometryFailure(
                    "invalid-shell-geometry", element.location, "ELEMENT",
                    element.sourceId.sourceLabelText,
                    "Shell geometry contains a nonfinite source coordinate.");
            }
        }
        for (std::size_t first = 0U; first < 4U; ++first) {
            for (std::size_t second = first + 1U; second < 4U; ++second) {
                if (element.nodeIndices[first] == element.nodeIndices[second] ||
                    sameCoordinates(
                        current.coordinates[first], current.coordinates[second])) {
                    return geometryFailure(
                        "invalid-shell-geometry", element.location, "ELEMENT",
                        element.sourceId.sourceLabelText,
                        "Shell geometry contains duplicate nodes.");
                }
            }
        }

        const ShapeData center = shapeData(0.0, 0.0);
        const Vector3 centerXi =
            derivativeSum(center.xiDerivatives, current.coordinates);
        const Vector3 centerEta =
            derivativeSum(center.etaDerivatives, current.coordinates);
        const Vector3 centerCross = cross(centerXi, centerEta);
        const double centerMeasure = norm(centerCross);
        if (!isFinite(centerXi) || !isFinite(centerEta) ||
            !isFinite(centerCross) || !std::isfinite(centerMeasure) ||
            !(centerMeasure > 0.0)) {
            return geometryFailure(
                "invalid-shell-geometry", element.location, "ELEMENT",
                element.sourceId.sourceLabelText,
                "Shell center has no finite nonzero normal candidate.");
        }
        current.normal = scale(1.0 / centerMeasure, centerCross);

        if (segmentsProperlyIntersect(
                current.coordinates[0], current.coordinates[1],
                current.coordinates[2], current.coordinates[3], current.normal) ||
            segmentsProperlyIntersect(
                current.coordinates[1], current.coordinates[2],
                current.coordinates[3], current.coordinates[0], current.normal)) {
            return geometryFailure(
                "invalid-shell-geometry", element.location, "ELEMENT",
                element.sourceId.sourceLabelText,
                "Shell boundary is self-intersecting in the center-normal projection.");
        }

        double areaWeight = 0.0;
        for (const auto& point : surfaceGaussPoints) {
            const ShapeData shape = shapeData(point[0], point[1]);
            const Vector3 tangentXi =
                derivativeSum(shape.xiDerivatives, current.coordinates);
            const Vector3 tangentEta =
                derivativeSum(shape.etaDerivatives, current.coordinates);
            const Vector3 areaVector = cross(tangentXi, tangentEta);
            const double measure = norm(areaVector);
            if (!isFinite(tangentXi) || !isFinite(tangentEta) ||
                !isFinite(areaVector) || !std::isfinite(measure) ||
                !(measure > 0.0) || !(dot(areaVector, current.normal) > 0.0)) {
                return geometryFailure(
                    "invalid-shell-geometry", element.location, "ELEMENT",
                    element.sourceId.sourceLabelText,
                    "Shell surface is zero-area or locally reversed at a required point.");
            }
            areaWeight += measure;
        }
        if (!std::isfinite(areaWeight) || !(areaWeight > 0.0)) {
            return geometryFailure(
                "invalid-shell-geometry", element.location, "ELEMENT",
                element.sourceId.sourceLabelText,
                "Shell surface-area weight is nonfinite or zero.");
        }
        current.areaWeight = areaWeight;
        work.push_back(current);
        geometry.elementData.push_back({
            static_cast<EntityIndex>(elementIndex), current.normal, areaWeight});
    }

    std::vector<std::vector<std::size_t>> incident(nodes.size());
    for (std::size_t elementIndex = 0U;
         elementIndex < elements.size();
         ++elementIndex) {
        for (const EntityIndex nodeIndex : elements[elementIndex].nodeIndices) {
            incident[nodeIndex].push_back(elementIndex);
        }
    }

    geometry.nodalFrames.reserve(nodes.size());
    std::vector<const ShellNodeInitialFrame*> frameByNode(nodes.size(), nullptr);
    for (std::size_t nodeIndex = 0U; nodeIndex < incident.size(); ++nodeIndex) {
        auto& nodeIncident = incident[nodeIndex];
        if (nodeIncident.empty()) {
            continue;
        }
        std::sort(
            nodeIncident.begin(), nodeIncident.end(),
            [&elements](std::size_t left, std::size_t right) {
                return sourceIdentityLess(
                    elements[left], left, elements[right], right);
            });
        for (std::size_t first = 0U; first < nodeIncident.size(); ++first) {
            for (std::size_t second = first + 1U;
                 second < nodeIncident.size();
                 ++second) {
                const double pairDot = dot(
                    work[nodeIncident[first]].normal,
                    work[nodeIncident[second]].normal);
                if (!std::isfinite(pairDot) || !(pairDot > 0.0)) {
                    return geometryFailure(
                        "opposed-incident-normal", nodes[nodeIndex].location,
                        "NODE", nodes[nodeIndex].sourceId.sourceLabelText,
                        "Incident shell normal candidates do not share a positive orientation hemisphere.");
                }
            }
        }

        double maximumWeight = 0.0;
        for (const std::size_t elementIndex : nodeIncident) {
            maximumWeight = std::max(maximumWeight, work[elementIndex].areaWeight);
        }
        Vector3 directorSum{};
        for (const std::size_t elementIndex : nodeIncident) {
            directorSum = add(
                directorSum,
                scale(
                    work[elementIndex].areaWeight / maximumWeight,
                    work[elementIndex].normal));
        }
        const double directorNorm = norm(directorSum);
        if (!isFinite(directorSum) || !std::isfinite(directorNorm) ||
            !(directorNorm > 0.0)) {
            return geometryFailure(
                "invalid-shell-director", nodes[nodeIndex].location, "NODE",
                nodes[nodeIndex].sourceId.sourceLabelText,
                "Area-weighted shell director is nonfinite or zero.");
        }
        const Vector3 director = scale(1.0 / directorNorm, directorSum);

        const std::array<Vector3, 3> globalAxes{
            Vector3{1.0, 0.0, 0.0},
            Vector3{0.0, 1.0, 0.0},
            Vector3{0.0, 0.0, 1.0}};
        std::size_t selectedAxis = 0U;
        double selectedAlignment = std::abs(dot(globalAxes[0], director));
        for (std::size_t axis = 1U; axis < globalAxes.size(); ++axis) {
            const double alignment = std::abs(dot(globalAxes[axis], director));
            if (alignment < selectedAlignment) {
                selectedAlignment = alignment;
                selectedAxis = axis;
            }
        }
        const Vector3 tangentCandidate = subtract(
            globalAxes[selectedAxis],
            scale(dot(globalAxes[selectedAxis], director), director));
        const double tangentNorm = norm(tangentCandidate);
        if (!isFinite(tangentCandidate) || !std::isfinite(tangentNorm) ||
            !(tangentNorm > 0.0)) {
            return geometryFailure(
                "invalid-shell-director", nodes[nodeIndex].location, "NODE",
                nodes[nodeIndex].sourceId.sourceLabelText,
                "Least-aligned-axis tangent frame construction failed.");
        }
        const Vector3 tangentA = scale(1.0 / tangentNorm, tangentCandidate);
        const Vector3 tangentB = cross(director, tangentA);
        if (!isFinite(tangentB) || !(norm(tangentB) > 0.0)) {
            return geometryFailure(
                "invalid-shell-director", nodes[nodeIndex].location, "NODE",
                nodes[nodeIndex].sourceId.sourceLabelText,
                "Right-handed shell tangent frame construction failed.");
        }
        geometry.nodalFrames.push_back({
            static_cast<EntityIndex>(nodeIndex), director, tangentA, tangentB});
    }

    // Build lookup only after frame storage is complete so later code never
    // observes a pointer invalidated by vector growth.
    for (const auto& frame : geometry.nodalFrames) {
        frameByNode[frame.nodeIndex] = &frame;
    }

    for (std::size_t elementIndex = 0U;
         elementIndex < elements.size();
         ++elementIndex) {
        const auto& element = elements[elementIndex];
        if (element.sectionIndex >= sections.size()) {
            return geometryFailure(
                "invalid-shell-jacobian", element.location, "ELEMENT",
                element.sourceId.sourceLabelText,
                "Shell geometry cannot resolve its thickness for Jacobian validation.");
        }
        const double thickness = sections[element.sectionIndex].thickness;
        std::array<Vector3, 4> directors{};
        for (std::size_t localNode = 0U; localNode < 4U; ++localNode) {
            const auto* frame = frameByNode[element.nodeIndices[localNode]];
            if (frame == nullptr) {
                return geometryFailure(
                    "invalid-shell-director", element.location, "ELEMENT",
                    element.sourceId.sourceLabelText,
                    "Shell element is missing a nodal director.");
            }
            directors[localNode] = frame->director;
        }

        for (const auto& point : shellGeometryValidationPoints()) {
            const double xi = point.naturalCoordinates[0];
            const double eta = point.naturalCoordinates[1];
            const double zeta = point.naturalCoordinates[2];
            const ShapeData shape = shapeData(xi, eta);
            const Vector3 midsurfaceXi =
                derivativeSum(shape.xiDerivatives, work[elementIndex].coordinates);
            const Vector3 midsurfaceEta =
                derivativeSum(shape.etaDerivatives, work[elementIndex].coordinates);
            const Vector3 areaVector = cross(midsurfaceXi, midsurfaceEta);
            const double surfaceMeasure = norm(areaVector);
            if (!isFinite(midsurfaceXi) || !isFinite(midsurfaceEta) ||
                !isFinite(areaVector) || !std::isfinite(surfaceMeasure) ||
                !(surfaceMeasure > 0.0) ||
                !(dot(areaVector, work[elementIndex].normal) > 0.0)) {
                return geometryFailure(
                    "invalid-shell-geometry", element.location, "ELEMENT",
                    element.sourceId.sourceLabelText,
                    "Shell surface basis is nonfinite, zero, or reversed at a required point.");
            }

            const Vector3 directorXi =
                weightedSum(shape.xiDerivatives, directors);
            const Vector3 directorEta =
                weightedSum(shape.etaDerivatives, directors);
            const Vector3 directorValue = weightedSum(shape.values, directors);
            const Vector3 covariantXi = add(
                midsurfaceXi, scale(0.5 * thickness * zeta, directorXi));
            const Vector3 covariantEta = add(
                midsurfaceEta, scale(0.5 * thickness * zeta, directorEta));
            const Vector3 covariantZeta = scale(0.5 * thickness, directorValue);
            const double jacobian =
                dot(covariantXi, cross(covariantEta, covariantZeta));
            if (!isFinite(covariantXi) || !isFinite(covariantEta) ||
                !isFinite(covariantZeta) || !std::isfinite(jacobian) ||
                !(jacobian > 0.0)) {
                return geometryFailure(
                    "invalid-shell-jacobian", element.location, "ELEMENT",
                    element.sourceId.sourceLabelText,
                    "Shell Jacobian is nonfinite or nonpositive at a required point.");
            }
            const Vector3 reciprocalXi =
                scale(1.0 / jacobian, cross(covariantEta, covariantZeta));
            const Vector3 reciprocalEta =
                scale(1.0 / jacobian, cross(covariantZeta, covariantXi));
            const Vector3 reciprocalZeta =
                scale(1.0 / jacobian, cross(covariantXi, covariantEta));
            if (!isFinite(reciprocalXi) || !isFinite(reciprocalEta) ||
                !isFinite(reciprocalZeta)) {
                return geometryFailure(
                    "invalid-shell-jacobian", element.location, "ELEMENT",
                    element.sourceId.sourceLabelText,
                    "Shell reciprocal basis is nonfinite at a required point.");
            }
        }
    }

    return Result<ShellGeometry>::success(std::move(geometry));
}

} // namespace fesa
