# ***************************************************************************
# *   Copyright (c) 2017 Markus Hovorka <m.hovorka@live.de>                 *
# *   Copyright (c) 2018 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                                                                   *
# *                                                                         *
# ***************************************************************************
""" Collection of functions for the Fem module.

This module contains function for managing a analysis and all the different
types of objects it contains, helper for executing a simulation.
"""


__title__ = "FEM analysis tools"
__author__ = "Markus Hovorka, Bernd Hahnebach"
__url__ = "https://www.freecad.org"


from . import femutils


def get_member(analysis, t):
    """Return list of all members of *analysis* of type *t*.

    Search *analysis* for members of type *t*. This method checks the custom
    python typesystem (BaseType class property) used by the Fem module if
    possible. If the object does not use the python typesystem the usual
    isDerivedFrom from the C++ dynamic type system is used.

    :param analysis: only objects part of this analysis are considered
    :param t:        only objects of this type are returned

    :note:
     Inheritance of Fem types is not checked. If *obj* uses Fems typesystem the
     type is just checked for equality. If the type doesn't match
     ``obj.isDerivedFrom`` is called as usual. See
     https://forum.freecad.org/viewtopic.php?f=10&t=32625
    """
    if analysis is None:
        raise ValueError("Analysis must not be None")
    matching = []
    for m in analysis.Group:
        # since is _derived_from is used the father could be used
        # to test too (ex. "Fem::FemMeshObject")
        if femutils.is_derived_from(m, t) and not (
            m.hasExtension("App::SuppressibleExtension") and m.Suppressed
        ):
            matching.append(m)
    return matching


def get_single_member(analysis, t):
    """Return one object of type *t* and part of *analysis*.

    Search *analysis* for members of type *t* and return the first one that's
    found. This method checks the custom python typesystem (BaseType class
    property) used by the Fem module if possible. If the object doesn't use the
    python typesystem the usual isDerivedFrom from the C++ dynamic type system
    is used.

    :param analysis: only objects part of this analysis are considered
    :param t:        only a object of this type is returned

    :note:
     Inheritance of Fem types is not checked. If *obj* uses Fems typesystem the
     type is just checked for equality. If the type doesn't match
     ``obj.isDerivedFrom`` is called as usual. See
     https://forum.freecad.org/viewtopic.php?f=10&t=32625
    """
    objs = get_member(analysis, t)
    return objs[0] if objs else None


def get_several_member(analysis, t):
    """Get members and pack them for Calculix/Z88.

    Collect members by calling :py:func:`get_member` and pack them into a
    data structure that can be consumed by calculix and Z88 solver modules.

    :param analysis: see :py:func:`get_member`
    :param t: see :py:func:`get_member`

    :returns:
     A list containing one dict per member. Each dict has two entries:
     ``"Object"`` and ``"RefShapeType"``. ``dict["Object"]`` contains the
     member document object. ``dict["RefShapeType"]`` contains the shape type
     of the *References* property of the member (used by constraints) as a
     string ("Vertex", "Edge", "Face" or "Solid"). If the member doesn't have a
     *References* property ``dict["RefShapeType"]`` is the empty string ``""``.

    :note:
     Undefined behaviour if one of the members has a *References* property
     which is empty.

    :note:
     Undefined behaviour if the type of the references of one object are not
     all the same.

    :note:
     Inheritance of Fem types is not checked. If *obj* uses Fems typesystem the
     type is just checked for equality. If the type doesn't match
     ``obj.isDerivedFrom`` is called as usual. See
     https://forum.freecad.org/viewtopic.php?f=10&t=32625
    """
    # if no member is found, an empty list is returned
    objs = get_member(analysis, t)
    members = []
    for m in objs:
        obj_dict = {}
        obj_dict["Object"] = m
        obj_dict["RefShapeType"] = femutils.get_refshape_type(m)
        members.append(obj_dict)
    return members


def get_mesh_to_solve(analysis):
    """Find one and only mesh object of *analysis*.

    :returns:
     A tuple ``(object, message)``. If and only if the analysis contains
     exactly one mesh object the first value of the tuple is the mesh document
     object. Otherwise the first value is ``None`` and the second value is a
     error message indicating what went wrong.
    """
    mesh_to_solve = None
    for m in analysis.Group:
        if (
            m.isDerivedFrom("Fem::FemMeshObject")
            # the next line should not be needed as the result mesh is not a analysis member
            and not femutils.is_of_type(m, "Fem::MeshResult")
        ):
            if not mesh_to_solve:
                mesh_to_solve = m
            else:
                return (None, "FEM: multiple mesh in analysis not yet supported!")
    if mesh_to_solve is not None:
        return (mesh_to_solve, "")
    else:
        return (None, "FEM: no mesh object found in analysis.")


class AnalysisMember:

    def __init__(self, analysis):
        self.analysis = analysis
        """
        # members of the analysis. All except solvers and the mesh

        materials:
        materials_linear : list of dictionaries
            list of nonlinear materials from the analysis.
            [{"Object":materials_linear}, {}, ...]

        materials_nonlinear : list of dictionaries
            list of nonlinear materials from the analysis.
            [{"Object":materials_nonlinear}, {}, ...]

        geometries:
        beam_sections : list of dictionaries
            list of beam sections from the analysis.
            [{"Object":beam_section_obj, "xxxxxxxx":value}, {}, ...]

        beam_rotations :  list of dictionaries
            list of beam rotations from the analysis.
            [{"Object":beam_rotation_obj, "xxxxxxxx":value}, {}, ...]

        fluid_sections : list of dictionaries
            list of fluid sections from the analysis.
            [{"Object":fluid_section_obj, "xxxxxxxx":value}, {}, ...]

        shell_thicknesses : list of dictionaries
            list of shell thicknesses from the analysis.
            [{"Object":shell_thickness_obj, "xxxxxxxx":value}, {}, ...]

        constraints:
        constraints_centrif : list of dictionaries
            list of centrifs for the analysis.
            [{"Object":centrif_obj, "xxxxxxxx":value}, {}, ...]

        constraints_contact : list of dictionaries
            list of contact constraints from the analysis.
            [{"Object":contact_obj, "xxxxxxxx":value}, {}, ...]

        constraints_displacement : list of dictionaries
            list of displacements for the analysis.
            [{"Object":displacement_obj, "xxxxxxxx":value}, {}, ...]

        constraints_fixed :  list of dictionaries
            list of fixed constraints from the analysis.
            [{"Object":fixed_obj, "NodeSupports":bool}, {}, ...]

        constraints_rigidbody : list of dictionaries
            list of displacements for the analysis.
            [{"Object":rigidbody_obj, "xxxxxxxx":value}, {}, ...]

        constraints_force : list of dictionaries
            list of force constraints from the analysis.
            [{"Object":force_obj, "NodeLoad":value}, {}, ...

        constraints_heatflux : list of dictionaries
            list of heatflux constraints for the analysis.
            [{"Object":heatflux_obj, "xxxxxxxx":value}, {}, ...]

        constraints_initialtemperature : list of dictionaries
            list of initial temperatures for the analysis.
            [{"Object":initialtemperature_obj, "xxxxxxxx":value}, {}, ...]

        constraints_planerotation : list of dictionaries
            list of plane rotation constraints from the analysis.
            [{"Object":planerotation_obj, "xxxxxxxx":value}, {}, ...]

        constraints_pressure : list of dictionaries
            list of pressure constraints from the analysis.
            [{"Object":pressure_obj, "xxxxxxxx":value}, {}, ...]

        constraints_sectionprint : list of dictionaries
            list of sectionprints for the analysis.
            [{"Object":sectionprint_obj, "xxxxxxxx":value}, {}, ...]

        constraints_selfweight : list of dictionaries
            list of selfweight constraints from the analysis.
            [{"Object":selfweight_obj, "xxxxxxxx":value}, {}, ...]

        constraints_temperature : list of dictionaries
            list of temperatures for the analysis.
            [{"Object":temperature_obj, "xxxxxxxx":value}, {}, ...]

        constraints_tie : list of dictionaries
            list of ties for the analysis.
            [{"Object":tie_obj, "xxxxxxxx":value}, {}, ...]

        constraints_transform : list of dictionaries
            list of transform constraints from the analysis.
            [{"Object":transform_obj, "xxxxxxxx":value}, {}, ...]
        """

        # get member
        # constants
        self.cota_vacuumpermittivity = self.get_several_member("Fem::ConstantVacuumPermittivity")

        # materials
        std_mats = self.get_several_member("Fem::MaterialCommon")
        rei_mats = self.get_several_member("Fem::MaterialReinforced")
        self.mats_linear = std_mats + rei_mats

        self.mats_nonlinear = self.get_several_member("Fem::MaterialMechanicalNonlinear")

        # geometries
        self.geos_beamsection = self.get_several_member("Fem::ElementGeometry1D")
        self.geos_beamrotation = self.get_several_member("Fem::ElementRotation1D")
        self.geos_fluidsection = self.get_several_member("Fem::ElementFluid1D")
        self.geos_shellthickness = self.get_several_member("Fem::ElementGeometry2D")

        # constraints
        self.cons_centrif = self.get_several_member("Fem::ConstraintCentrif")
        self.cons_bodyheatsource = self.get_several_member("Fem::ConstraintBodyHeatSource")
        self.cons_contact = self.get_several_member("Fem::ConstraintContact")
        self.cons_displacement = self.get_several_member("Fem::ConstraintDisplacement")
        self.cons_fixed = self.get_several_member("Fem::ConstraintFixed")
        self.cons_rigidbody = self.get_several_member("Fem::ConstraintRigidBody")
        self.cons_rigidbody_step = self.get_several_member("Fem::ConstraintRigidBody")
        self.cons_force = self.get_several_member("Fem::ConstraintForce")
        self.cons_heatflux = self.get_several_member("Fem::ConstraintHeatflux")
        self.cons_initialtemperature = self.get_several_member("Fem::ConstraintInitialTemperature")
        self.cons_planerotation = self.get_several_member("Fem::ConstraintPlaneRotation")
        self.cons_pressure = self.get_several_member("Fem::ConstraintPressure")
        self.cons_sectionprint = self.get_several_member("Fem::ConstraintSectionPrint")
        self.cons_selfweight = self.get_several_member("Fem::ConstraintSelfWeight")
        self.cons_temperature = self.get_several_member("Fem::ConstraintTemperature")
        self.cons_tie = self.get_several_member("Fem::ConstraintTie")
        self.cons_transform = self.get_several_member("Fem::ConstraintTransform")

    def get_several_member(self, t):
        return get_several_member(self.analysis, t)
