/**
 * \file
 * \copyright
 * Copyright (c) 2012-2019, OpenGeoSys Community (http://www.opengeosys.org)
 *            Distributed under a Modified BSD License.
 *              See accompanying file LICENSE.txt or
 *              http://www.opengeosys.org/project/license
 *
 */

#pragma once

#include <limits>

#include "MechanicsBase.h"
#include "ParameterLib/Parameter.h"

namespace MaterialLib
{
namespace Solids
{
template <int DisplacementDim>
class LinearElasticOrthotropic : public MechanicsBase<DisplacementDim>
{
public:
    struct EvaluatedMaterialProperties
    {
        /// Youngs moduli for 1-based indices.
        constexpr double E(int const i) const
        {
            assert(i == 1 || i == 2 || i == 3);
            return youngs_moduli[i - 1];
        }

        /// Shear moduli for 1-based indices.
        constexpr double G(int const i, int const j) const
        {
            assert(i == 1 || i == 2 || i == 3);
            assert(j == 1 || j == 2 || j == 3);
            if (i == 1 && j == 2)
            {
                return shear_moduli[0];
            }
            if (i == 2 && j == 3)
            {
                return shear_moduli[1];
            }
            if (i == 1 && j == 3)
            {
                return shear_moduli[2];
            }

            return std::numeric_limits<double>::quiet_NaN();
        }

        /// Poisson's ratios for 1-based indices.
        constexpr double nu(int const i, int const j) const
        {
            assert(i == 1 || i == 2 || i == 3);
            assert(j == 1 || j == 2 || j == 3);
            if (i == 1 && j == 2)
            {
                return poissons_ratios[0];
            }
            if (i == 2 && j == 3)
            {
                return poissons_ratios[1];
            }
            if (i == 1 && j == 3)
            {
                return poissons_ratios[2];
            }

            // The next set is scaled by Youngs modulus s.t.
            // nu_ji = nu_ij * E_j/E_i.
            if (i == 2 && j == 1)
            {
                return poissons_ratios[0] * E(i) / E(j);
            }
            if (i == 3 && j == 2)
            {
                return poissons_ratios[1] * E(i) / E(j);
            }
            if (i == 3 && j == 1)
            {
                return poissons_ratios[2] * E(i) / E(j);
            }

            return std::numeric_limits<double>::quiet_NaN();
        }

        std::array<double, 3> youngs_moduli;
        std::array<double, 3> shear_moduli;
        std::array<double, 3> poissons_ratios;
    };

    /// Variables specific to the material model
    struct MaterialProperties
    {
        using P = ParameterLib::Parameter<double>;
        using X = ParameterLib::SpatialPosition;

        MaterialProperties(P const& youngs_moduli_, P const& shear_moduli_,
                           P const& poissons_ratios_)
            : youngs_moduli(youngs_moduli_),
              shear_moduli(shear_moduli_),
              poissons_ratios(poissons_ratios_)
        {
        }

        EvaluatedMaterialProperties evaluate(
            double const t, ParameterLib::SpatialPosition const& x) const
        {
            auto const E = youngs_moduli(t, x);
            auto const G = shear_moduli(t, x);
            auto const nu = poissons_ratios(t, x);
            return {
                {E[0], E[1], E[2]}, {G[0], G[1], G[2]}, {nu[0], nu[1], nu[2]}};
        }

        P const& youngs_moduli;
        P const& shear_moduli;
        P const& poissons_ratios;  // Stored as nu_12, nu_23, nu_13
    };

public:
    static int const KelvinVectorSize =
        MathLib::KelvinVector::KelvinVectorDimensions<DisplacementDim>::value;
    using KelvinVector =
        MathLib::KelvinVector::KelvinVectorType<DisplacementDim>;
    using KelvinMatrix =
        MathLib::KelvinVector::KelvinMatrixType<DisplacementDim>;

    LinearElasticOrthotropic(
        MaterialProperties material_properties,
        boost::optional<ParameterLib::CoordinateSystem> const&
            local_coordinate_system)
        : _mp(std::move(material_properties)),
          _local_coordinate_system(local_coordinate_system)
    {
    }

    double computeFreeEnergyDensity(
        double const /*t*/,
        ParameterLib::SpatialPosition const& /*x*/,
        double const /*dt*/,
        KelvinVector const& eps,
        KelvinVector const& sigma,
        typename MechanicsBase<DisplacementDim>::
            MaterialStateVariables const& /* material_state_variables */)
        const override
    {
        return eps.dot(sigma) / 2;
    }

    boost::optional<
        std::tuple<typename MechanicsBase<DisplacementDim>::KelvinVector,
                   std::unique_ptr<typename MechanicsBase<
                       DisplacementDim>::MaterialStateVariables>,
                   typename MechanicsBase<DisplacementDim>::KelvinMatrix>>
    integrateStress(
        double const t, ParameterLib::SpatialPosition const& x,
        double const /*dt*/, KelvinVector const& eps_prev,
        KelvinVector const& eps, KelvinVector const& sigma_prev,
        typename MechanicsBase<DisplacementDim>::MaterialStateVariables const&
            material_state_variables,
        double const T) const override;

    KelvinMatrix getElasticTensor(double const t,
                                  ParameterLib::SpatialPosition const& x,
                                  double const T) const;

    MaterialProperties getMaterialProperties() const { return _mp; }

private:
    /// Rotation matrix of a forth order tensor in 3D.
    MathLib::KelvinVector::KelvinMatrixType<3> fourthOrderRotationMatrix(
        ParameterLib::SpatialPosition const& x) const
    {
        if (!_local_coordinate_system)
        {
            return MathLib::KelvinVector::KelvinMatrixType<3>::Identity();
        }

        // 1-based index access for convenience.
        auto Q = [R = _local_coordinate_system->transformation<3>(x)](
                     int const i, int const j) { return R(i - 1, j - 1); };

        MathLib::KelvinVector::KelvinMatrixType<3> R;
        R << Q(1, 1) * Q(1, 1), Q(1, 2) * Q(1, 2), Q(1, 3) * Q(1, 3),
            std::sqrt(2) * Q(1, 1) * Q(1, 2), std::sqrt(2) * Q(1, 2) * Q(1, 3),
            std::sqrt(2) * Q(1, 1) * Q(1, 3), Q(2, 1) * Q(2, 1),
            Q(2, 2) * Q(2, 2), Q(2, 3) * Q(2, 3),
            std::sqrt(2) * Q(2, 1) * Q(2, 2), std::sqrt(2) * Q(2, 2) * Q(2, 3),
            std::sqrt(2) * Q(2, 1) * Q(2, 3), Q(3, 1) * Q(3, 1),
            Q(3, 2) * Q(3, 2), Q(3, 3) * Q(3, 3),
            std::sqrt(2) * Q(3, 1) * Q(3, 2), std::sqrt(2) * Q(3, 2) * Q(3, 3),
            std::sqrt(2) * Q(3, 1) * Q(3, 3), std::sqrt(2) * Q(1, 1) * Q(2, 1),
            std::sqrt(2) * Q(1, 2) * Q(2, 2), std::sqrt(2) * Q(1, 3) * Q(2, 3),
            Q(1, 1) * Q(2, 2) + Q(1, 2) * Q(2, 1),
            Q(1, 2) * Q(2, 3) + Q(1, 3) * Q(2, 2),
            Q(1, 1) * Q(2, 3) + Q(1, 3) * Q(2, 1),
            std::sqrt(2) * Q(2, 1) * Q(3, 1), std::sqrt(2) * Q(2, 2) * Q(3, 2),
            std::sqrt(2) * Q(2, 3) * Q(3, 3),
            Q(2, 1) * Q(3, 2) + Q(2, 2) * Q(3, 1),
            Q(2, 2) * Q(3, 3) + Q(2, 3) * Q(3, 2),
            Q(2, 1) * Q(3, 3) + Q(2, 3) * Q(3, 1),
            std::sqrt(2) * Q(1, 1) * Q(3, 1), std::sqrt(2) * Q(1, 2) * Q(3, 2),
            std::sqrt(2) * Q(1, 3) * Q(3, 3),
            Q(1, 1) * Q(3, 2) + Q(1, 2) * Q(3, 1),
            Q(1, 2) * Q(3, 3) + Q(1, 3) * Q(3, 2),
            Q(1, 1) * Q(3, 3) + Q(1, 3) * Q(3, 1);
        return R;
    }

protected:
    MaterialProperties _mp;
    boost::optional<ParameterLib::CoordinateSystem> const&
        _local_coordinate_system;
};

extern template class LinearElasticOrthotropic<2>;
extern template class LinearElasticOrthotropic<3>;

}  // namespace Solids
}  // namespace MaterialLib