# ***************************************************************************
# *   Copyright (c) 2020 Przemo Firszt <przemo@firszt.eu>                   *
# *   Copyright (c) 2020 Bernd Hahnebach <bernd@bimstatik.org>              *
# *                                                                         *
# *   This file is part of the FreeCAD CAx development system.              *
# *                                                                         *
# *   This program is free software; you can redistribute it and/or modify  *
# *   it under the terms of the GNU Lesser General Public License (LGPL)    *
# *   as published by the Free Software Foundation; either version 2 of     *
# *   the License, or (at your option) any later version.                   *
# *   for detail see the LICENCE text file.                                 *
# *                                                                         *
# *   This program is distributed in the hope that it will be useful,       *
# *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
# *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
# *   GNU Library General Public License for more details.                  *
# *                                                                         *
# *   You should have received a copy of the GNU Library General Public     *
# *   License along with this program; if not, write to the Free Software   *
# *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  *
# *   USA                                                                   *
# *                                                                         *
# ***************************************************************************

__title__ = "Analysis Checks"
__author__ = "Przemo Firszt, Bernd Hahnebach"
__url__ = "https://www.freecad.org"

## \addtogroup FEM
#  @{

import FreeCAD

from FreeCAD import Units

from . import femutils
from femsolver.calculix.solver import ANALYSIS_TYPES


def check_member_for_solver_calculix(analysis, solver, mesh, member):

    message = ""

    # solver
    if solver.AnalysisType not in ANALYSIS_TYPES:
        message += (
            "Unknown analysis type: {}\n"
            .format(solver.AnalysisType)
        )
    if solver.AnalysisType == "frequency":
        if not hasattr(solver, "EigenmodeHighLimit"):
            message += "Frequency analysis: Solver has no EigenmodeHighLimit.\n"
        elif not hasattr(solver, "EigenmodeLowLimit"):
            message += "Frequency analysis: Solver has no EigenmodeLowLimit.\n"
        elif not hasattr(solver, "EigenmodesCount"):
            message += "Frequency analysis: Solver has no EigenmodesCount.\n"
    if hasattr(solver, "MaterialNonlinearity") \
            and solver.MaterialNonlinearity == "nonlinear":
        if not member.mats_nonlinear:
            message += (
                "Solver is set to nonlinear materials, "
                "but there is no nonlinear material in the analysis.\n"
            )
        if solver.Proxy.Type == "Fem::SolverCcxTools" \
                and solver.GeometricalNonlinearity != "nonlinear":
            # nonlinear geometry --> should be set
            # https://forum.freecad.org/viewtopic.php?f=18&t=23101&p=180489#p180489
            message += (
                "Solver CalculiX triggers nonlinear geometry for nonlinear material, "
                "thus it should to be set too.\n"
            )

    # mesh
    if not mesh:
        message += "No mesh object defined in the analysis.\n"
    if mesh:
        if mesh.FemMesh.VolumeCount == 0 \
                and mesh.FemMesh.FaceCount > 0 \
                and not member.geos_shellthickness:
            message += (
                "FEM mesh has no volume elements, "
                "either define a shell thicknesses or "
                "provide a FEM mesh with volume elements.\n"
            )
        if mesh.FemMesh.VolumeCount == 0 \
                and mesh.FemMesh.FaceCount == 0 \
                and mesh.FemMesh.EdgeCount > 0 \
                and not member.geos_beamsection \
                and not member.geos_fluidsection:
            message += (
                "FEM mesh has no volume and no shell elements, "
                "either define a beam/fluid section or provide "
                "a FEM mesh with volume elements.\n"
            )
        if mesh.FemMesh.VolumeCount == 0 \
                and mesh.FemMesh.FaceCount == 0 \
                and mesh.FemMesh.EdgeCount == 0:
            message += (
                "FEM mesh has neither volume nor shell or edge elements. "
                "Provide a FEM mesh with elements.\n"
            )

    # material linear and nonlinear
    if not member.mats_linear:
        message += "No material object defined in the analysis.\n"
    has_no_references = False
    for m in member.mats_linear:
        if len(m["Object"].References) == 0:
            if has_no_references is True:
                message += (
                    "More than one material has an empty references list "
                    "(Only one empty references list is allowed!).\n"
                )
            has_no_references = True
    mat_ref_shty = ""
    for m in member.mats_linear:
        ref_shty = femutils.get_refshape_type(m["Object"])
        if not mat_ref_shty:
            mat_ref_shty = ref_shty
        if mat_ref_shty and ref_shty and ref_shty != mat_ref_shty:
            # mat_ref_shty could be empty in one material
            # only the not empty ones should have the same shape type
            message += (
                "Some material objects do not have the same reference shape type "
                "(all material objects must have the same reference shape type, "
                "at the moment).\n"
            )
    for m in member.mats_linear:
        mat_map = m["Object"].Material
        mat_obj = m["Object"]
        if mat_obj.Category == "Solid":
            if "YoungsModulus" in mat_map:
                # print(Units.Quantity(mat_map["YoungsModulus"]).Value)
                if not Units.Quantity(mat_map["YoungsModulus"]).Value:
                    message += "Value of YoungsModulus is set to 0.0.\n"
            else:
                message += "No YoungsModulus defined for at least one material.\n"
            if "PoissonRatio" not in mat_map:
                # PoissonRatio is allowed to be 0.0 (in ccx), but it should be set anyway.
                message += "No PoissonRatio defined for at least one material.\n"
        if solver.AnalysisType == "frequency" or member.cons_selfweight:
            if "Density" not in mat_map:
                message += "No Density defined for at least one material.\n"
        if solver.AnalysisType == "thermomech":
            if "ThermalConductivity" in mat_map:
                if not Units.Quantity(mat_map["ThermalConductivity"]).Value:
                    message += "Value of ThermalConductivity is set to 0.0.\n"
            else:
                message += (
                    "Thermomechanical analysis: No ThermalConductivity defined "
                    "for at least one material.\n"
                )
            if "ThermalExpansionCoefficient" not in mat_map and mat_obj.Category == "Solid":
                message += (
                    "Thermomechanical analysis: No ThermalExpansionCoefficient defined "
                    "for at least one material.\n"  # allowed to be 0.0 (in ccx)
                )
            if "SpecificHeat" not in mat_map:
                message += (
                    "Thermomechanical analysis: No SpecificHeat "
                    "defined for at least one material.\n"  # allowed to be 0.0 (in ccx)
                )
        if femutils.is_of_type(mat_obj, "Fem::MaterialReinforced"):
            # additional tests for reinforced materials,
            # they are needed for result calculation not for ccx analysis
            mat_map_m = mat_obj.Material
            if "AngleOfFriction" in mat_map_m:
                # print(Units.Quantity(mat_map_m["AngleOfFriction"]).Value)
                if not Units.Quantity(mat_map_m["AngleOfFriction"]).Value:
                    message += (
                        "Value of AngleOfFriction is set to 0.0 "
                        "for the matrix of a reinforced material.\n"
                    )
            else:
                message += (
                    "No AngleOfFriction defined for the matrix "
                    "of at least one reinforced material.\n"
                )
            if "CompressiveStrength" in mat_map_m:
                # print(Units.Quantity(mat_map_m["CompressiveStrength"]).Value)
                if not Units.Quantity(mat_map_m["CompressiveStrength"]).Value:
                    message += (
                        "Value of CompressiveStrength is set to 0.0 "
                        "for the matrix of a reinforced material.\n"
                    )
            else:
                message += (
                    "No CompressiveStrength defined for the matrinx "
                    "of at least one reinforced material.\n"
                )
            mat_map_r = mat_obj.Reinforcement
            if "YieldStrength" in mat_map_r:
                # print(Units.Quantity(mat_map_r["YieldStrength"]).Value)
                if not Units.Quantity(mat_map_r["YieldStrength"]).Value:
                    message += (
                        "Value of YieldStrength is set to 0.0 "
                        "for the reinforcement of a reinforced material.\n"
                    )
            else:
                message += (
                    "No YieldStrength defined for the reinforcement "
                    "of at least one reinforced material.\n"
                )
    if len(member.mats_linear) == 1:
        mobj = member.mats_linear[0]["Object"]
        if hasattr(mobj, "References") and mobj.References:
            FreeCAD.Console.PrintError(
                "Only one material object, but this one has a reference shape. "
                "The reference shape will be ignored.\n"
            )
    for m in member.mats_linear:
        has_nonlinear_material = False
        for nlm in member.mats_nonlinear:
            if nlm["Object"].LinearBaseMaterial == m["Object"]:
                if has_nonlinear_material is False:
                    has_nonlinear_material = True
                else:
                    message += (
                        "At least two nonlinear materials use the same linear base material. "
                        "Only one nonlinear material for each linear material allowed.\n"
                    )

    # which analysis needs which constraints
    # no check in the regard of loads existence (constraint force, pressure, self weight)
    # is done, because an analysis without loads at all is an valid analysis too
    if solver.AnalysisType == "static":
        if not (member.cons_fixed or member.cons_displacement):
            message += (
                "Static analysis: Neither constraint fixed nor "
                "constraint displacement defined.\n"
            )
    if solver.AnalysisType == "thermomech":
        if not member.cons_initialtemperature:
            if not member.geos_fluidsection:
                message += "Thermomechanical analysis: No initial temperature defined.\n"
        if len(member.cons_initialtemperature) > 1:
            message += "Thermomechanical analysis: Only one initial temperature is allowed.\n"

    # constraints
    # fixed
    if member.cons_fixed:
        for c in member.cons_fixed:
            if len(c["Object"].References) == 0:
                message += "{} has empty references.".format(c["Object"].Name)
    # displacement
    if member.cons_displacement:
        for di in member.cons_displacement:
            if len(di["Object"].References) == 0:
                message += "{} has empty references.".format(c["Object"].Name)
    # plane rotation
    if member.cons_planerotation:
        for c in member.cons_planerotation:
            if len(c["Object"].References) == 0:
                message += "{} has empty references.".format(c["Object"].Name)
    # contact
    if member.cons_contact:
        for c in member.cons_contact:
            if len(c["Object"].References) == 0:
                message += "{} has empty references.".format(c["Object"].Name)
    # tie
    if member.cons_tie:
        for c in member.cons_tie:
            items = 0
            for reference in c["Object"].References:
                items += len(reference[1])
            if items != 2:
                message += (
                    "{} doesn't references exactly two needed faces.\n"
                    .format(c["Object"].Name)
                )
    # sectionprint
    if member.cons_sectionprint:
        for c in member.cons_sectionprint:
            items = 0
            for reference in c["Object"].References:
                items += len(reference[1])
            if items != 1:
                message += (
                    "{} doesn't reference exactly one needed face.\n"
                    .format(c["Object"].Name)
                )
    # transform
    if member.cons_transform:
        for c in member.cons_transform:
            if len(c["Object"].References) == 0:
                message += "{} has empty references.".format(c["Object"].Name)
    # pressure
    if member.cons_pressure:
        for c in member.cons_pressure:
            if len(c["Object"].References) == 0:
                message += "{} has empty references.".format(c["Object"].Name)
    # force
    if member.cons_force:
        for c in member.cons_force:
            if len(c["Object"].References) == 0:
                message += "{} has empty references.".format(c["Object"].Name)
    # temperature
    if member.cons_temperature:
        for c in member.cons_temperature:
            if len(c["Object"].References) == 0:
                message += "{} has empty references.".format(c["Object"].Name)
    # heat flux
    if member.cons_heatflux:
        for c in member.cons_heatflux:
            if len(c["Object"].References) == 0:
                message += "{} has empty references.".format(c["Object"].Name)

    # geometries
    # beam section
    if member.geos_beamsection:
        if member.geos_shellthickness:
            # this needs to be checked only once either here or in shell_thicknesses
            message += (
                "Beam sections and shell thicknesses in one analysis "
                "is not supported at the moment.\n"
            )
        if member.geos_fluidsection:
            # this needs to be checked only once either here or in shell_thicknesses
            message += (
                "Beam sections and fluid sections in one analysis "
                "is not supported at the moment.\n"
            )
        has_no_references = False
        for b in member.geos_beamsection:
            if len(b["Object"].References) == 0:
                if has_no_references is True:
                    message += (
                        "More than one beam section has an empty references "
                        "list (Only one empty references list is allowed!).\n"
                    )
                has_no_references = True
        if mesh:
            if mesh.FemMesh.FaceCount > 0 or mesh.FemMesh.VolumeCount > 0:
                message += (
                    "Beam sections defined but FEM mesh has volume or shell elements.\n"
                )
            if mesh.FemMesh.EdgeCount == 0:
                message += (
                    "Beam sections defined but FEM mesh has no edge elements.\n"
                )
            if not (
                hasattr(mesh, "Shape")
                or hasattr(mesh, "Part")
            ):
                message += (
                    "Mesh without geometry link. "
                    "The mesh needs to know its geometry for the beam rotations.\n"
                )
        if len(member.geos_beamrotation) > 1:
            message += (
                "Multiple beam rotations in one analysis are not supported at the moment.\n"
            )
    # beam rotations
    if member.geos_beamrotation and not member.geos_beamsection:
        message += "Beam rotations in the analysis but no beam sections defined.\n"
    # shell thickness
    if member.geos_shellthickness:
        has_no_references = False
        for s in member.geos_shellthickness:
            if len(s["Object"].References) == 0:
                if has_no_references is True:
                    message += (
                        "More than one shell thickness has an empty references "
                        "list (Only one empty references list is allowed!).\n"
                    )
                has_no_references = True
        if mesh:
            if mesh.FemMesh.VolumeCount > 0:
                message += "Shell thicknesses defined but FEM mesh has volume elements.\n"
            if mesh.FemMesh.FaceCount == 0:
                message += "Shell thicknesses defined but FEM mesh has no shell elements.\n"
    # fluid section
    if member.geos_fluidsection:
        if not member.cons_selfweight:
            message += (
                "A fluid network analysis requires self weight constraint to be applied\n"
            )
        if solver.AnalysisType != "thermomech":
            message += "A fluid network analysis can only be done in a thermomech analysis\n"
        has_no_references = False
        for f in member.geos_fluidsection:
            if len(f["Object"].References) == 0:
                if has_no_references is True:
                    message += (
                        "More than one fluid section has an empty references list "
                        "(Only one empty references list is allowed!).\n"
                    )
                has_no_references = True
        if mesh:
            if mesh.FemMesh.FaceCount > 0 or mesh.FemMesh.VolumeCount > 0:
                message += (
                    "Fluid sections defined but FEM mesh has volume or shell elements.\n"
                )
            if mesh.FemMesh.EdgeCount == 0:
                message += "Fluid sections defined but FEM mesh has no edge elements.\n"

    return message

##  @}
