#include "fesa/model/shell_geometry.hpp"

#include <gtest/gtest.h>

#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <limits>
#include <string>
#include <vector>

namespace {

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

fesa::Node node(fesa::EntityIndex label, Vector3 coordinates) {
    return {
        {"Shell-Instance", static_cast<std::int64_t>(label),
         std::to_string(label)},
        coordinates,
        {"shell-geometry.inp", static_cast<std::size_t>(label + 1U)}};
}

fesa::Mitc4ShellDefinition element(
    fesa::EntityIndex label,
    std::array<fesa::EntityIndex, 4> nodeIndices) {
    return {
        {"Shell-Instance", static_cast<std::int64_t>(label),
         std::to_string(label)},
        fesa::ShellSourceElementType::s4,
        nodeIndices,
        0U,
        0U,
        {"shell-geometry.inp", static_cast<std::size_t>(100U + label)}};
}

std::vector<fesa::ShellSection> sections(double thickness = 0.2) {
    return {{"Section", thickness, 0U, {"shell-geometry.inp", 90U}}};
}

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::sqrt(dot(value, value));
}

void expectVectorNear(
    const Vector3& actual,
    const Vector3& expected,
    double tolerance = 1.0e-12) {
    for (std::size_t component = 0U; component < actual.size(); ++component) {
        EXPECT_NEAR(actual[component], expected[component], tolerance);
    }
}

void expectRightHandedFrame(const fesa::ShellNodeInitialFrame& frame) {
    EXPECT_NEAR(norm(frame.director), 1.0, 1.0e-12);
    EXPECT_NEAR(norm(frame.tangentA), 1.0, 1.0e-12);
    EXPECT_NEAR(norm(frame.tangentB), 1.0, 1.0e-12);
    EXPECT_NEAR(dot(frame.director, frame.tangentA), 0.0, 1.0e-12);
    EXPECT_NEAR(dot(frame.director, frame.tangentB), 0.0, 1.0e-12);
    EXPECT_NEAR(dot(frame.tangentA, frame.tangentB), 0.0, 1.0e-12);
    expectVectorNear(cross(frame.tangentA, frame.tangentB), frame.director);
}

const fesa::ShellNodeInitialFrame& frameFor(
    const fesa::ShellGeometry& geometry,
    fesa::EntityIndex nodeIndex) {
    const auto found = std::find_if(
        geometry.nodalFrames.begin(),
        geometry.nodalFrames.end(),
        [nodeIndex](const fesa::ShellNodeInitialFrame& frame) {
            return frame.nodeIndex == nodeIndex;
        });
    EXPECT_NE(found, geometry.nodalFrames.end());
    return *found;
}

void expectFailureCode(
    const fesa::Result<fesa::ShellGeometry>& result,
    const std::string& code) {
    ASSERT_FALSE(result.hasValue());
    EXPECT_EQ(result.status().failureCategory(), fesa::FailureCategory::model);
    ASSERT_EQ(result.status().diagnostics().size(), 1U);
    EXPECT_EQ(result.status().diagnostics()[0].code, code);
}

} // namespace

// MITC4-GEO-001
TEST(Mitc4Geometry, BuildsDeterministicFramesForPlanarRotatedAndWarpedElements) {
    const std::vector<fesa::Node> planarNodes{
        node(0U, {0.0, 0.0, 0.0}),
        node(1U, {1.0, 0.0, 0.0}),
        node(2U, {1.0, 1.0, 0.0}),
        node(3U, {0.0, 1.0, 0.0})};
    auto planar = fesa::preprocessShellGeometry(
        planarNodes, {element(10U, {0U, 1U, 2U, 3U})}, sections());

    ASSERT_TRUE(planar.hasValue());
    ASSERT_EQ(planar.value().elementData.size(), 1U);
    expectVectorNear(planar.value().elementData[0].normalCandidate, {0.0, 0.0, 1.0});
    EXPECT_NEAR(planar.value().elementData[0].surfaceAreaWeight, 1.0, 1.0e-12);
    ASSERT_EQ(planar.value().nodalFrames.size(), 4U);
    for (const auto& frame : planar.value().nodalFrames) {
        expectVectorNear(frame.director, {0.0, 0.0, 1.0});
        expectVectorNear(frame.tangentA, {1.0, 0.0, 0.0});
        expectVectorNear(frame.tangentB, {0.0, 1.0, 0.0});
        expectRightHandedFrame(frame);
    }

    const std::vector<fesa::Node> rotatedNodes{
        node(0U, {0.0, 0.0, 0.0}),
        node(1U, {0.0, 1.0, 0.0}),
        node(2U, {0.0, 1.0, 1.0}),
        node(3U, {0.0, 0.0, 1.0})};
    auto rotated = fesa::preprocessShellGeometry(
        rotatedNodes, {element(11U, {0U, 1U, 2U, 3U})}, sections());

    ASSERT_TRUE(rotated.hasValue());
    const auto& rotatedFrame = frameFor(rotated.value(), 0U);
    expectVectorNear(rotatedFrame.director, {1.0, 0.0, 0.0});
    expectVectorNear(rotatedFrame.tangentA, {0.0, 1.0, 0.0});
    expectVectorNear(rotatedFrame.tangentB, {0.0, 0.0, 1.0});
    expectRightHandedFrame(rotatedFrame);

    const std::vector<fesa::Node> warpedNodes{
        node(0U, {0.0, 0.0, 0.0}),
        node(1U, {2.0, 0.0, 0.0}),
        node(2U, {2.0, 1.0, 0.2}),
        node(3U, {0.0, 1.0, 0.0})};
    auto warped = fesa::preprocessShellGeometry(
        warpedNodes, {element(12U, {0U, 1U, 2U, 3U})}, sections());

    ASSERT_TRUE(warped.hasValue());
    EXPECT_GT(warped.value().elementData[0].surfaceAreaWeight, 2.0);
    for (const auto& frame : warped.value().nodalFrames) {
        expectRightHandedFrame(frame);
    }
}

// MITC4-GEO-002
TEST(Mitc4Geometry, AreaWeightsSharedDirectorsInStableSourceIdentityOrder) {
    const std::vector<fesa::Node> nodes{
        node(0U, {0.0, 0.0, 0.0}),
        node(1U, {1.0, 0.0, 0.0}),
        node(2U, {1.0, 1.0, 0.0}),
        node(3U, {0.0, 1.0, 0.0}),
        node(4U, {2.0, 0.0, 1.0}),
        node(5U, {2.0, 1.0, 1.0})};
    const auto flat = element(10U, {0U, 1U, 2U, 3U});
    const auto tilted = element(20U, {1U, 4U, 5U, 2U});

    auto first = fesa::preprocessShellGeometry(
        nodes, {tilted, flat}, sections());
    auto second = fesa::preprocessShellGeometry(
        nodes, {flat, tilted}, sections());

    ASSERT_TRUE(first.hasValue());
    ASSERT_TRUE(second.hasValue());
    const Vector3 expectedSharedDirector{
        -1.0 / std::sqrt(5.0), 0.0, 2.0 / std::sqrt(5.0)};
    for (const auto sharedNode : {1U, 2U}) {
        const auto& firstFrame = frameFor(first.value(), sharedNode);
        const auto& secondFrame = frameFor(second.value(), sharedNode);
        expectVectorNear(firstFrame.director, expectedSharedDirector);
        expectVectorNear(firstFrame.director, secondFrame.director, 0.0);
        expectVectorNear(firstFrame.tangentA, {0.0, 1.0, 0.0});
        expectRightHandedFrame(firstFrame);
    }
}

// MITC4-GEO-003
TEST(Mitc4Geometry, RejectsInvalidSurfaceJacobianAndIncidentOrientationCases) {
    const auto validElement = element(10U, {0U, 1U, 2U, 3U});

    expectFailureCode(
        fesa::preprocessShellGeometry(
            {node(0U, {0.0, 0.0, 0.0}), node(1U, {0.0, 0.0, 0.0}),
             node(2U, {1.0, 1.0, 0.0}), node(3U, {0.0, 1.0, 0.0})},
            {validElement}, sections()),
        "invalid-shell-geometry");

    expectFailureCode(
        fesa::preprocessShellGeometry(
            {node(0U, {0.0, 0.0, 0.0}), node(1U, {1.0, 1.0, 0.0}),
             node(2U, {0.0, 1.0, 0.0}), node(3U, {1.0, 0.0, 0.0})},
            {validElement}, sections()),
        "invalid-shell-geometry");

    expectFailureCode(
        fesa::preprocessShellGeometry(
            {node(0U, {0.0, 0.0, 0.0}), node(1U, {1.0, 0.0, 0.0}),
             node(2U, {2.0, 0.0, 0.0}), node(3U, {3.0, 0.0, 0.0})},
            {validElement}, sections()),
        "invalid-shell-geometry");

    expectFailureCode(
        fesa::preprocessShellGeometry(
            {node(0U, {0.0, 0.0, 0.0}), node(1U, {1.0, 0.0, 0.0}),
             node(2U, {0.05, 0.05, 0.0}), node(3U, {0.0, 1.0, 0.0})},
            {validElement}, sections()),
        "invalid-shell-geometry");

    expectFailureCode(
        fesa::preprocessShellGeometry(
            {node(0U, {0.0, 0.0, 0.0}),
             node(1U, {1.0, 0.0, 0.0}),
             node(2U, {1.0, std::numeric_limits<double>::quiet_NaN(), 0.0}),
             node(3U, {0.0, 1.0, 0.0})},
            {validElement}, sections()),
        "invalid-shell-geometry");

    expectFailureCode(
        fesa::preprocessShellGeometry(
            {node(0U, {0.0, 0.0, 0.0}), node(1U, {1.0, 0.0, 0.0}),
             node(2U, {1.0, 1.0, 0.0}), node(3U, {0.0, 1.0, 0.0})},
            {validElement}, sections(0.0)),
        "invalid-shell-jacobian");

    const std::vector<fesa::Node> opposedNodes{
        node(0U, {0.0, 0.0, 0.0}), node(1U, {1.0, 0.0, 0.0}),
        node(2U, {1.0, 1.0, 0.0}), node(3U, {0.0, 1.0, 0.0})};
    expectFailureCode(
        fesa::preprocessShellGeometry(
            opposedNodes,
            {element(10U, {0U, 1U, 2U, 3U}),
             element(20U, {0U, 3U, 2U, 1U})},
            sections()),
        "opposed-incident-normal");
}

// MITC4-GEO-004
TEST(Mitc4Geometry, ExposesTheCompleteRequiredValidationPointInventory) {
    const auto& points = fesa::shellGeometryValidationPoints();
    ASSERT_EQ(points.size(), 17U);
    EXPECT_EQ(
        std::count_if(points.begin(), points.end(), [](const auto& point) {
            return point.kind == fesa::ShellGeometryPointKind::center;
        }),
        1);
    EXPECT_EQ(
        std::count_if(points.begin(), points.end(), [](const auto& point) {
            return point.kind == fesa::ShellGeometryPointKind::stiffness;
        }),
        8);
    EXPECT_EQ(
        std::count_if(points.begin(), points.end(), [](const auto& point) {
            return point.kind == fesa::ShellGeometryPointKind::tying;
        }),
        4);
    EXPECT_EQ(
        std::count_if(points.begin(), points.end(), [](const auto& point) {
            return point.kind == fesa::ShellGeometryPointKind::recovery;
        }),
        4);

    EXPECT_EQ(points.front().naturalCoordinates, (Vector3{0.0, 0.0, 0.0}));
    const double gauss = 1.0 / std::sqrt(3.0);
    EXPECT_EQ(points[1].naturalCoordinates, (Vector3{-gauss, -gauss, -gauss}));
    EXPECT_EQ(points[8].naturalCoordinates, (Vector3{-gauss, gauss, gauss}));
    EXPECT_EQ(points[9].naturalCoordinates, (Vector3{0.0, -1.0, 0.0}));
    EXPECT_EQ(points[12].naturalCoordinates, (Vector3{1.0, 0.0, 0.0}));
    EXPECT_EQ(points[13].naturalCoordinates, (Vector3{-gauss, -gauss, 0.0}));
    EXPECT_EQ(points[16].naturalCoordinates, (Vector3{-gauss, gauss, 0.0}));
}
