#include "fesa/analysis/analysis_state.hpp"

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

namespace fesa {
namespace {

constexpr std::size_t kShellLocationsPerElement = 4U;

Status shellCandidateFailure(
    const std::string& code,
    const std::string& identity,
    const std::string& message) {
    return Status::failure(
        FailureCategory::model,
        {{Severity::error,
          code,
          {},
          "ANALYSIS_STATE",
          identity,
          message}});
}

template<std::size_t Size>
bool finite(const std::array<double, Size>& values) {
    return std::all_of(
        values.begin(), values.end(),
        [](const double value) { return std::isfinite(value); });
}

bool finite(const ShellResultRow& row) {
    if (!finite(row.naturalCoordinates) ||
        !finite(row.generalizedStrain) ||
        !finite(row.sectionResultant)) {
        return false;
    }
    for (const auto& axis : row.localFrame) {
        if (!finite(axis)) {
            return false;
        }
    }
    return std::all_of(
        row.stress.begin(), row.stress.end(),
        [](const ShellSectionStressRow& stress) {
            return std::isfinite(stress.zeta) && finite(stress.components);
        });
}

} // namespace

AnalysisState AnalysisState::create(
    const DofManager& dofs, StepFrameIdentity identity) {
    return AnalysisState{dofs.fullDofCount(), std::move(identity)};
}

Vector& AnalysisState::displacement() noexcept {
    return displacement_;
}

const Vector& AnalysisState::displacement() const noexcept {
    return displacement_;
}

Vector& AnalysisState::externalForce() noexcept {
    return externalForce_;
}

const Vector& AnalysisState::externalForce() const noexcept {
    return externalForce_;
}

Vector& AnalysisState::internalForce() noexcept {
    return internalForce_;
}

const Vector& AnalysisState::internalForce() const noexcept {
    return internalForce_;
}

Vector& AnalysisState::residual() noexcept {
    return residual_;
}

const Vector& AnalysisState::residual() const noexcept {
    return residual_;
}

Vector& AnalysisState::reaction() noexcept {
    return reaction_;
}

const Vector& AnalysisState::reaction() const noexcept {
    return reaction_;
}

const StepFrameIdentity& AnalysisState::identity() const noexcept {
    return identity_;
}

std::vector<EndpointResultRow>& AnalysisState::endpointResults() noexcept {
    return endpointResults_;
}

const std::vector<EndpointResultRow>& AnalysisState::endpointResults() const noexcept {
    return endpointResults_;
}

std::vector<GaussResultRow>& AnalysisState::gaussResults() noexcept {
    return gaussResults_;
}

const std::vector<GaussResultRow>& AnalysisState::gaussResults() const noexcept {
    return gaussResults_;
}

std::vector<StressS11Row>& AnalysisState::stressResults() noexcept {
    return stressResults_;
}

const std::vector<StressS11Row>& AnalysisState::stressResults() const noexcept {
    return stressResults_;
}

Status AnalysisState::commitShellResults(
    const std::vector<EntityIndex>& expectedElementOrder,
    ShellStateCandidate candidate) {
    if (expectedElementOrder.size() >
        (std::numeric_limits<std::size_t>::max)() /
            kShellLocationsPerElement) {
        return shellCandidateFailure(
            "invalid-shell-state-inventory",
            identity_.stepName,
            "The expected shell result inventory is too large.");
    }
    const std::size_t expectedRowCount =
        expectedElementOrder.size() * kShellLocationsPerElement;
    if (candidate.rows.size() != expectedRowCount) {
        return shellCandidateFailure(
            "invalid-shell-state-inventory",
            identity_.stepName,
            "Shell results require exactly four rows per expected element.");
    }
    if (std::adjacent_find(
            expectedElementOrder.begin(), expectedElementOrder.end(),
            [](const EntityIndex left, const EntityIndex right) {
                return left >= right;
            }) != expectedElementOrder.end()) {
        return shellCandidateFailure(
            "invalid-shell-state-inventory",
            identity_.stepName,
            "Expected shell elements must be unique and in stable index order.");
    }

    const std::array<ShellMidsurfaceLocation, kShellLocationsPerElement>
        expectedLocations{
            ShellMidsurfaceLocation::gp1,
            ShellMidsurfaceLocation::gp2,
            ShellMidsurfaceLocation::gp3,
            ShellMidsurfaceLocation::gp4};
    const double gauss = 1.0 / std::sqrt(3.0);
    const std::array<std::array<double, 2>, kShellLocationsPerElement>
        expectedCoordinates{
            std::array<double, 2>{-gauss, -gauss},
            std::array<double, 2>{gauss, -gauss},
            std::array<double, 2>{gauss, gauss},
            std::array<double, 2>{-gauss, gauss}};
    const std::array<ShellSectionPosition, 3> expectedPositions{
        ShellSectionPosition::bottom,
        ShellSectionPosition::middle,
        ShellSectionPosition::top};
    constexpr std::array<double, 3> expectedZeta{-1.0, 0.0, 1.0};
    for (std::size_t elementOrder = 0U;
         elementOrder < expectedElementOrder.size();
         ++elementOrder) {
        for (std::size_t point = 0U;
             point < kShellLocationsPerElement;
             ++point) {
            const auto& row = candidate.rows[
                elementOrder * kShellLocationsPerElement + point];
            if (row.element != expectedElementOrder[elementOrder] ||
                row.location != expectedLocations[point] ||
                row.naturalCoordinates != expectedCoordinates[point]) {
                return shellCandidateFailure(
                    "invalid-shell-state-inventory",
                    std::to_string(row.element),
                    "Shell rows must preserve element and GP1 through GP4 identity.");
            }
            for (std::size_t position = 0U;
                 position < expectedPositions.size();
                 ++position) {
                if (row.stress[position].position !=
                        expectedPositions[position] ||
                    row.stress[position].zeta != expectedZeta[position]) {
                    return shellCandidateFailure(
                        "invalid-shell-state-inventory",
                        std::to_string(row.element),
                        "Shell stress rows require BOTTOM, MIDDLE, TOP identity.");
                }
            }
            if (!finite(row)) {
                return shellCandidateFailure(
                    "nonfinite-shell-state-value",
                    std::to_string(row.element),
                    "Shell result rows must contain only finite values.");
            }
        }
    }

    if (!std::isfinite(candidate.physicalStrainEnergy) ||
        !finite(candidate.equilibrium) ||
        !finite(candidate.verificationMetrics)) {
        return shellCandidateFailure(
            "nonfinite-shell-state-value",
            identity_.stepName,
            "Shell energy, equilibrium, and normalized metrics must be finite.");
    }

    shellResults_ = std::move(candidate.rows);
    physicalStrainEnergy_ = candidate.physicalStrainEnergy;
    equilibrium_ = candidate.equilibrium;
    verificationMetrics_ = candidate.verificationMetrics;
    return Status::ok();
}

const std::vector<ShellResultRow>& AnalysisState::shellResults() const noexcept {
    return shellResults_;
}

double AnalysisState::physicalStrainEnergy() const noexcept {
    return physicalStrainEnergy_;
}

const std::array<double, 6>& AnalysisState::equilibrium() const noexcept {
    return equilibrium_;
}

const std::array<double, 3>& AnalysisState::verificationMetrics() const noexcept {
    return verificationMetrics_;
}

AnalysisState::AnalysisState(
    std::size_t fullDofCount, StepFrameIdentity identity)
    : identity_{std::move(identity)},
      displacement_{fullDofCount},
      externalForce_{fullDofCount},
      internalForce_{fullDofCount},
      residual_{fullDofCount},
      reaction_{fullDofCount} {}

} // namespace fesa
