# ***************************************************************************
# *   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                                                                   *
# *                                                                         *
# ***************************************************************************

__title__ = "Mesh FEM unit tests"
__author__ = "Bernd Hahnebach"
__url__ = "https://www.freecad.org"

import unittest
from os.path import join

import FreeCAD

import Fem
from . import support_utils as testtools
from .support_utils import fcc_print


class TestMeshCommon(unittest.TestCase):
    fcc_print("import TestMeshCommon")

    # ********************************************************************************************
    def setUp(self):
        # setUp is executed before every test

        # new document
        self.document = FreeCAD.newDocument(self.__class__.__name__)

    # ********************************************************************************************
    def tearDown(self):
        # tearDown is executed after every test
        FreeCAD.closeDocument(self.document.Name)

    # ********************************************************************************************
    def test_00print(self):
        # since method name starts with 00 this will be run first
        # this test just prints a line with stars

        fcc_print(
            "\n{0}\n{1} run FEM TestMeshCommon tests {2}\n{0}".format(100 * "*", 10 * "*", 60 * "*")
        )

    # ********************************************************************************************
    def test_mesh_seg2_python(self):
        seg2 = Fem.FemMesh()
        seg2.addNode(0, 0, 0, 1)
        seg2.addNode(2, 0, 0, 2)
        seg2.addNode(4, 0, 0, 3)
        seg2.addEdge([1, 2])
        seg2.addEdge([2, 3], 2)

        node_data = [seg2.NodeCount, seg2.Nodes]
        edge_data = [
            seg2.EdgeCount,
            seg2.Edges[0],
            seg2.getElementNodes(seg2.Edges[0]),
            seg2.Edges[1],
            seg2.getElementNodes(seg2.Edges[1]),
        ]
        expected_nodes = [
            3,
            {
                1: FreeCAD.Vector(0.0, 0.0, 0.0),
                2: FreeCAD.Vector(2.0, 0.0, 0.0),
                3: FreeCAD.Vector(4.0, 0.0, 0.0),
            },
        ]
        expected_edges = [2, 1, (1, 2), 2, (2, 3)]
        self.assertEqual(
            node_data, expected_nodes, "Nodes of Python created seg2 element are unexpected"
        )
        self.assertEqual(
            edge_data, expected_edges, "Edges of Python created seg2 element are unexpected"
        )

    # ********************************************************************************************
    def test_mesh_seg3_python(self):
        seg3 = Fem.FemMesh()
        seg3.addNode(0, 0, 0, 1)
        seg3.addNode(1, 0, 0, 2)
        seg3.addNode(2, 0, 0, 3)
        seg3.addNode(3, 0, 0, 4)
        seg3.addNode(4, 0, 0, 5)
        seg3.addEdge([1, 3, 2])
        seg3.addEdge([3, 5, 4], 2)

        node_data = [seg3.NodeCount, seg3.Nodes]
        edge_data = [
            seg3.EdgeCount,
            seg3.Edges[0],
            seg3.getElementNodes(seg3.Edges[0]),
            seg3.Edges[1],
            seg3.getElementNodes(seg3.Edges[1]),
        ]
        expected_nodes = [
            5,
            {
                1: FreeCAD.Vector(0.0, 0.0, 0.0),
                2: FreeCAD.Vector(1.0, 0.0, 0.0),
                3: FreeCAD.Vector(2.0, 0.0, 0.0),
                4: FreeCAD.Vector(3.0, 0.0, 0.0),
                5: FreeCAD.Vector(4.0, 0.0, 0.0),
            },
        ]
        expected_edges = [2, 1, (1, 3, 2), 2, (3, 5, 4)]
        self.assertEqual(
            node_data, expected_nodes, "Nodes of Python created seg3 element are unexpected"
        )
        self.assertEqual(
            edge_data, expected_edges, "Edges of Python created seg3 element are unexpected"
        )

    # ********************************************************************************************
    def test_unv_save_load(self):
        tetra10 = Fem.FemMesh()
        tetra10.addNode(6, 12, 18, 1)
        tetra10.addNode(0, 0, 18, 2)
        tetra10.addNode(12, 0, 18, 3)
        tetra10.addNode(6, 6, 0, 4)

        tetra10.addNode(3, 6, 18, 5)
        tetra10.addNode(6, 0, 18, 6)
        tetra10.addNode(9, 6, 18, 7)

        tetra10.addNode(6, 9, 9, 8)
        tetra10.addNode(3, 3, 9, 9)
        tetra10.addNode(9, 3, 9, 10)
        tetra10.addVolume([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

        unv_file = join(testtools.get_fem_test_tmp_dir("mesh_common_unv_save"), "tetra10_mesh.unv")
        tetra10.write(unv_file)
        newmesh = Fem.read(unv_file)
        expected = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        self.assertEqual(
            newmesh.getElementNodes(1),
            expected,
            "Nodes order of quadratic volume element is unexpected",
        )

    # ********************************************************************************************
    def test_writeAbaqus_precision(self):
        # https://forum.freecad.org/viewtopic.php?f=18&t=22759#p176669
        # ccx reads only F20.0 (i. e. Fortran floating point field 20 chars wide)
        # thus precision is set to 13 in writeAbaqus
        seg2 = Fem.FemMesh()
        seg2.addNode(0, 0, 0, 1)
        seg2.addNode(
            # 3456789012345678901234567
            -5000000000000000000.1,
            -1.123456789123456e-14,
            -0.1234567890123456789e-101,
            2,
        )
        seg2.addEdge([1, 2])

        inp_file = join(testtools.get_fem_test_tmp_dir("mesh_common_inp_preci"), "seg2_mesh.inp")
        seg2.writeABAQUS(inp_file, 1, False)

        read_file = open(inp_file)
        read_node_line = "line was not found"
        for ln in read_file:
            ln = ln.strip()
            if ln.startswith("2, -5"):
                read_node_line = ln
        read_file.close()

        #                  1234567  12345678901234567890  12345678901234567890
        expected_win = "2, -5e+018, -1.123456789123e-014, -1.234567890123e-102"
        expected_lin = "2, -5e+18, -1.123456789123e-14, -1.234567890123e-102"
        expected = [expected_lin, expected_win]
        self.assertTrue(
            True if read_node_line in expected else False,
            f"Problem in test_writeAbaqus_precision, \n{read_node_line}\n{expected}",
        )


# ************************************************************************************************
# ************************************************************************************************
class TestMeshEleTetra10(unittest.TestCase):
    fcc_print("import TestMeshEleTetra10")

    # ********************************************************************************************
    def setUp(self):
        # setUp is executed before every test

        # new document
        self.document = FreeCAD.newDocument(self.__class__.__name__)

        # more inits
        self.elem = "tetra10"

        self.base_testfile = join(testtools.get_fem_test_home_dir(), "mesh", (self.elem + "_mesh."))

        # 10 node tetrahedron --> tetra10
        femmesh = Fem.FemMesh()
        femmesh.addNode(6, 12, 18, 1)
        femmesh.addNode(0, 0, 18, 2)
        femmesh.addNode(12, 0, 18, 3)
        femmesh.addNode(6, 6, 0, 4)
        femmesh.addNode(3, 6, 18, 5)
        femmesh.addNode(6, 0, 18, 6)
        femmesh.addNode(9, 6, 18, 7)
        femmesh.addNode(6, 9, 9, 8)
        femmesh.addNode(3, 3, 9, 9)
        femmesh.addNode(9, 3, 9, 10)
        femmesh.addVolume([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
        self.femmesh = femmesh
        self.expected_nodes = {
            "count": 10,
            "nodes": {
                1: FreeCAD.Vector(6.0, 12.0, 18.0),
                2: FreeCAD.Vector(0.0, 0.0, 18.0),
                3: FreeCAD.Vector(12.0, 0.0, 18.0),
                4: FreeCAD.Vector(6.0, 6.0, 0.0),
                5: FreeCAD.Vector(3.0, 6.0, 18.0),
                6: FreeCAD.Vector(6.0, 0.0, 18.0),
                7: FreeCAD.Vector(9.0, 6.0, 18.0),
                8: FreeCAD.Vector(6.0, 9.0, 9.0),
                9: FreeCAD.Vector(3.0, 3.0, 9.0),
                10: FreeCAD.Vector(9.0, 3.0, 9.0),
            },
        }
        self.expected_elem = {
            "volcount": 1,
            "tetcount": 1,
            "volumes": [1, (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)],
        }
        """
        fcc_print("\n")
        fcc_print(expected_nodes)
        fcc_print(expected_elem)
        fcc_print("\n")
        """

    # ********************************************************************************************
    def tearDown(self):
        # tearDown is executed after every test
        FreeCAD.closeDocument(self.document.Name)

    # ********************************************************************************************
    def test_00print(self):
        # since method name starts with 00 this will be run first
        # this test just prints a line with stars

        fcc_print(
            "\n{0}\n{1} run FEM TestMeshEleTetra10 tests {2}\n{0}".format(
                100 * "*", 10 * "*", 56 * "*"
            )
        )

    # ********************************************************************************************
    def get_file_paths(self, file_extension):
        testfile = self.base_testfile + file_extension
        outfile = join(
            testtools.get_fem_test_tmp_dir("mesh_elements_" + self.elem + "_" + file_extension),
            self.elem + "_mesh." + file_extension,
        )

        # fcc_print("\n")
        # fcc_print(outfile)
        # fcc_print(testfile)
        return (outfile, testfile)

    # ********************************************************************************************
    def compare_mesh_files(self, femmesh_testfile, femmesh_outfile, filetyp):

        # """
        fcc_print(
            [
                femmesh_testfile.Volumes[0],
                femmesh_testfile.getElementNodes(femmesh_outfile.Volumes[0]),
            ]
        )
        # """

        # test reading the test mesh
        self.assertEqual(
            femmesh_testfile.Nodes,
            self.expected_nodes["nodes"],
            "Test reading {} mesh to {} file failed. Nodes are different.\n".format(
                self.elem, filetyp
            ),
        )
        self.assertEqual(
            [
                femmesh_testfile.Volumes[0],
                femmesh_testfile.getElementNodes(femmesh_outfile.Volumes[0]),
            ],
            self.expected_elem["volumes"],
            "Test reading {} mesh to {} file failed. Volumes are different.\n".format(
                self.elem, filetyp
            ),
        )
        # test reading the written mesh
        self.assertEqual(
            femmesh_outfile.Nodes,
            self.expected_nodes["nodes"],
            "Test reading {} mesh to {} file failed. Nodes are different.\n".format(
                self.elem, filetyp
            ),
        )
        self.assertEqual(
            [
                femmesh_outfile.Volumes[0],
                femmesh_outfile.getElementNodes(femmesh_outfile.Volumes[0]),
            ],
            self.expected_elem["volumes"],
            "Test reading {} mesh to {} file failed. Volumes are different.\n".format(
                self.elem, filetyp
            ),
        )
        # test if both are equal
        self.assertEqual(
            femmesh_outfile.Nodes,
            femmesh_testfile.Nodes,
            "Test reading {} mesh to {} file failed. Nodes are different.\n".format(
                self.elem, filetyp
            ),
        )
        self.assertEqual(
            femmesh_outfile.Volumes,
            femmesh_testfile.Volumes,
            "Test reading {} mesh to {} file failed. Volumes are different.\n".format(
                self.elem, filetyp
            ),
        )

    # ********************************************************************************************
    def test_tetra10_create(self):
        # tetra10 element: creating by Python
        node_data = {"count": self.femmesh.NodeCount, "nodes": self.femmesh.Nodes}
        elem_data = {
            "volcount": self.femmesh.VolumeCount,
            "tetcount": self.femmesh.TetraCount,
            "volumes": [
                self.femmesh.Volumes[0],
                self.femmesh.getElementNodes(self.femmesh.Volumes[0]),
            ],
        }
        self.assertEqual(
            node_data,
            self.expected_nodes,
            "Nodes of Python created " + self.elem + "mesh element are unexpected",
        )
        self.assertEqual(
            elem_data,
            self.expected_elem,
            "Elements of Python created " + self.elem + "mesh element are unexpected",
        )
        """
        obj = doc.addObject("Fem::FemMeshObject" , elem)
        obj.FemMesh = femmesh
        obj.Placement.Base = (30,50,0)
        obj.ViewObject.DisplayMode = "Faces, Wireframe & Nodes"
        """

    # ********************************************************************************************
    def test_tetra10_inp(self):
        # tetra10 element: reading from and writing to inp mesh file format

        file_extension = "inp"
        outfile, testfile = self.get_file_paths(file_extension)

        self.femmesh.writeABAQUS(outfile, 1, False)  # write the mesh
        femmesh_outfile = Fem.read(outfile)  # read the mesh from written mesh
        femmesh_testfile = Fem.read(testfile)  # read the mesh from test mesh

        self.compare_mesh_files(femmesh_testfile, femmesh_outfile, file_extension)

    # ********************************************************************************************
    def test_tetra10_unv(self):
        # tetra10 element: reading from and writing to unv mesh file format

        file_extension = "unv"
        outfile, testfile = self.get_file_paths(file_extension)

        self.femmesh.write(outfile)  # write the mesh
        femmesh_outfile = Fem.read(outfile)  # read the mesh from written mesh
        femmesh_testfile = Fem.read(testfile)  # read the mesh from test mesh

        self.compare_mesh_files(femmesh_testfile, femmesh_outfile, file_extension)

    # ********************************************************************************************
    def test_tetra10_vkt(self):
        # tetra10 element: reading from and writing to unv mesh file format

        file_extension = "vtk"
        outfile, testfile = self.get_file_paths(file_extension)

        if "BUILD_FEM_VTK" in FreeCAD.__cmake__:
            self.femmesh.write(outfile)  # write the mesh
            femmesh_outfile = Fem.read(outfile)  # read the mesh from written mesh
            femmesh_testfile = Fem.read(testfile)  # read the mesh from test mesh

            self.compare_mesh_files(femmesh_testfile, femmesh_outfile, file_extension)
        else:
            fcc_print("FEM_VTK post processing is disabled.")

    # ********************************************************************************************
    def test_tetra10_yml(self):
        # tetra10 element: reading from and writing to yaml/json mesh file format

        file_extension = "yml"
        outfile, testfile = self.get_file_paths(file_extension)

        # TODO: implement yaml/json mesh reader writer method calls in C++
        # self.femmesh.write(outfile)  # write the mesh
        # femmesh_testfile = Fem.read(outfile)  # read the mesh from written mesh
        # femmesh_outfile = Fem.read(testfile)  # read the mesh from test mesh
        # directly use Python methods to read and write files
        from feminout.importYamlJsonMesh import write

        write(outfile, self.femmesh)
        from feminout.importYamlJsonMesh import read

        femmesh_testfile = read(outfile)
        femmesh_outfile = read(testfile)

        self.compare_mesh_files(femmesh_testfile, femmesh_outfile, file_extension)

    # ********************************************************************************************
    def test_tetra10_z88(self):
        # tetra10 element: reading from and writing to z88 mesh file format

        file_extension = "z88"
        outfile, testfile = self.get_file_paths(file_extension)

        self.femmesh.write(outfile)  # write the mesh
        femmesh_testfile = Fem.read(outfile)  # read the mesh from written mesh
        femmesh_outfile = Fem.read(testfile)  # read the mesh from test mesh

        self.compare_mesh_files(femmesh_testfile, femmesh_outfile, file_extension)


# ************************************************************************************************
# ************************************************************************************************
# TODO: add elements to group with another type. Should be empty at the end.
class TestMeshGroups(unittest.TestCase):
    fcc_print("import TestMeshGroups")

    # ********************************************************************************************
    def setUp(self):
        # setUp is executed before every test

        # new document
        self.document = FreeCAD.newDocument(self.__class__.__name__)

    # ********************************************************************************************
    def tearDown(self):
        # tearDown is executed after every test
        FreeCAD.closeDocument(self.document.Name)

    # ********************************************************************************************
    def test_00print(self):
        # since method name starts with 00 this will be run first
        # this test just prints a line with stars

        fcc_print(
            "\n{0}\n{1} run FEM TestMeshGroups tests {2}\n{0}".format(100 * "*", 10 * "*", 57 * "*")
        )

    # ********************************************************************************************
    def test_add_groups(self):
        """
        Create different groups with different names. Check whether the
        ids are correct, the names are correct, and whether the GroupCount is
        correct.
        """

        from femexamples.meshes.mesh_canticcx_tetra10 import create_elements
        from femexamples.meshes.mesh_canticcx_tetra10 import create_nodes

        fm = Fem.FemMesh()
        control = create_nodes(fm)
        if not control:
            fcc_print("failed to create nodes")
        control = create_elements(fm)
        if not control:
            fcc_print("failed to create elements")

        # information
        # fcc_print(fm)

        expected_dict = {}
        expected_dict["ids"] = []
        expected_dict["names"] = [
            "MyNodeGroup",
            "MyEdgeGroup",
            "MyVolumeGroup",
            "My0DElementGroup",
            "MyBallGroup",
        ]
        expected_dict["types"] = ["Node", "Edge", "Volume", "0DElement", "Ball"]
        expected_dict["count"] = fm.GroupCount + 5
        result_dict = {}

        mygrpids = []
        for name, typ in zip(expected_dict["names"], expected_dict["types"]):
            mygrpids.append(fm.addGroup(name, typ))

        expected_dict["ids"] = sorted(tuple(mygrpids))

        # fcc_print("expected dict")
        # fcc_print(expected_dict)

        result_dict["count"] = fm.GroupCount
        result_dict["ids"] = sorted(fm.Groups)
        result_dict["types"] = list([fm.getGroupElementType(g) for g in fm.Groups])
        result_dict["names"] = list([fm.getGroupName(g) for g in fm.Groups])

        # fcc_print("result dict")
        # fcc_print(result_dict)

        self.assertEqual(
            expected_dict,
            result_dict,
            msg=f"expected: {expected_dict}\n\nresult: {result_dict}\n\n differ",
        )

    def test_delete_groups(self):
        """
        Adds a number of groups to FemMesh and deletes them
        afterwards. Checks whether GroupCount is OK
        """
        from femexamples.meshes.mesh_canticcx_tetra10 import create_elements
        from femexamples.meshes.mesh_canticcx_tetra10 import create_nodes

        fm = Fem.FemMesh()
        control = create_nodes(fm)
        if not control:
            fcc_print("failed to create nodes")
        control = create_elements(fm)
        if not control:
            fcc_print("failed to create elements")

        # information
        # fcc_print(fm)
        old_group_count = fm.GroupCount
        myids = []
        for i in range(1000):
            myids.append(fm.addGroup("group" + str(i), "Node"))
        for grpid in myids:
            fm.removeGroup(grpid)
        new_group_count = fm.GroupCount
        self.assertEqual(
            old_group_count,
            new_group_count,
            msg=(
                "GroupCount before and after adding and deleting groups differ: {} != {}".format(
                    old_group_count, new_group_count
                )
            ),
        )

    def test_add_group_elements(self):
        """
        Add a node group, add elements to it. Verify that elements added
        and elements in getGroupElements are the same.
        """
        from femexamples.meshes.mesh_canticcx_tetra10 import create_elements
        from femexamples.meshes.mesh_canticcx_tetra10 import create_nodes

        fm = Fem.FemMesh()
        control = create_nodes(fm)
        if not control:
            fcc_print("failed to create nodes")
        control = create_elements(fm)
        if not control:
            fcc_print("failed to create elements")

        # information
        # fcc_print(fm)

        elements_to_be_added = [1, 2, 3, 4, 49, 64, 88, 100, 102, 188, 189, 190, 191]
        myid = fm.addGroup("mynodegroup", "Node")

        # fcc_print(fm.getGroupElements(myid))

        fm.addGroupElements(myid, elements_to_be_added)
        elements_returned = list(fm.getGroupElements(myid))  # returns tuple
        # fcc_print(elements_returned)
        self.assertEqual(
            elements_to_be_added,
            elements_returned,
            msg=(
                "elements to be added {} and elements returned {} differ".format(
                    elements_to_be_added, elements_returned
                )
            ),
        )
