# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from operator import attrgetter

from ansys.aedt.core.application.variables import decompose_variable_value
from ansys.aedt.core.generic.numbers import Quantity
from ansys.aedt.core.modeler.cad.modeler import CoordinateSystem
from ansys.aedt.core.modeler.cad.modeler import FaceCoordinateSystem
from ansys.aedt.core.modeler.geometry_operators import GeometryOperators as go
from ansys.aedt.toolkits.common.backend.api import AEDTCommon

from ansys.aedt.toolkits.magnet_segmentation.backend.models import properties


class AEDTWorkflow(AEDTCommon):
    """Controls the AEDT toolkit workflow.

    This class provides methods for connecting to a selected design,
    segment and skew the motor.

    Examples
    --------
        >>> from ansys.aedt.toolkits.magnet_segmentation.backend.api import AEDTWorkflow
        >>> toolkit = AEDTWorkflow()
        >>> msg1 = toolkit.launch_aedt()
        >>> toolkit.wait_to_be_idle()
        >>> toolkit.segmentation()
        >>> toolkit.apply_skew()
        >>> toolkit.release_aedt(True, True)
    """

    def __init__(self):
        AEDTCommon.__init__(self, properties)
        self.properties = properties

    # @thread.launch_thread
    def segmentation(self):
        """Apply object segmentation.

        This method automatically segments the rotor, rotor pockets, and magnets.

        .. warning::
            This method only works if the AEDT active project has
            ``SymmetryFactor`` and ``HalfAxial`` design settings defined.

        Returns
        -------
        bool
            ``True`` when successful, ``False`` when failed.
        """
        self.connect_design()

        # Requirements: Design needs  a design variable "HalfAxial"
        if self.aedtapp["HalfAxial"] == "1":
            self.aedtapp["HalfAxial"] = "0"

        for obj in self.aedtapp.modeler.unclassified_objects:
            obj.model = False

        self.aedtapp.set_active_design(self.properties.active_design)
        self.aedtapp.duplicate_design(self.properties.active_design)
        self.properties.active_design = self.aedtapp.design_name
        self.aedtapp.set_active_design(self.properties.active_design)
        self.properties.design_list[self.aedtapp.project_name].append(self.properties.active_design)
        self.set_properties(self.properties.model_dump())

        # try:
        if [bound for bound in self.aedtapp.boundaries if bound.type == "Insulating"]:
            for bound in self.aedtapp.boundaries[:]:
                if (
                    bound.type == "Insulating"
                    and bound.name in self.aedtapp.odesign.GetChildObject("Boundaries").GetChildNames()
                ):
                    bound.delete()

        vacuum_objects = [x for x in self.aedtapp.modeler.get_objects_by_material("vacuum") if x.object_type == "Solid"]
        rotor_pockets = self._get_rotor_pockets(vacuum_objects)

        # If model is already skewed only magnets can be segmented
        if not self.properties.is_skewed:
            magnets = self.aedtapp.modeler.get_objects_by_material(self.properties.magnets_material)
            if self.properties.rotor_material == self.properties.stator_material:
                rotor_stator = self.aedtapp.modeler.get_objects_by_material(self.properties.rotor_material)
                stator_obj = max(rotor_stator, key=attrgetter("volume"))
                rotor = [
                    x
                    for x in self.aedtapp.modeler.get_objects_by_material(self.properties.rotor_material)
                    if x != stator_obj and x.name != "Shaft"
                ][0]
            else:
                rotor = self.aedtapp.modeler.get_objects_by_material(self.properties.rotor_material)[0]

            if self.properties.rotor_slices > 1:
                # rotor segmentation
                rotor_slices = self.aedtapp.modeler.objects_segmentation(
                    rotor.id, segments=self.properties.rotor_slices, apply_mesh_sheets=False
                )
                # rotor and rotor pockets split
                rotor_objs = [rotor.name]
                magnets_names = [x.name for x in magnets]
                if len(rotor_pockets) > 0:
                    rotor_pockets_names = [x.name for x in rotor_pockets]
                for slice in rotor_slices[rotor.name]:
                    rotor_objs = self.aedtapp.modeler.split(rotor_objs, sides="Both", tool=slice.faces[0])
                    magnets_names = self.aedtapp.modeler.split(magnets_names, sides="Both", tool=slice.faces[0])
                    if len(rotor_pockets) > 0:
                        rotor_pockets_names = self.aedtapp.modeler.split(
                            rotor_pockets_names, sides="Both", tool=slice.faces[0]
                        )
                    self.aedtapp.modeler.delete_objects_containing(slice.name)
                rotor_slices.clear()

        magnets = self.aedtapp.modeler.get_objects_by_material(self.properties.magnets_material)
        for magnet in magnets:
            faces = []
            mesh_sheets = []
            cs = self.aedtapp.modeler.duplicate_coordinate_system_to_global(magnet.part_coordinate_system)
            magnet.part_coordinate_system = cs.name
            self.aedtapp.modeler.set_working_coordinate_system("Global")
            objects_segmentation = self.aedtapp.modeler.objects_segmentation(
                magnet.id,
                segments=self.properties.magnet_segments_per_slice,
                apply_mesh_sheets=self.properties.apply_mesh_sheets,
                mesh_sheets=self.properties.mesh_sheets_number,
            )
            if self.properties.apply_mesh_sheets:
                magnet_segments = objects_segmentation[0]
                mesh_sheets.extend([s.name for s in objects_segmentation[1][magnet.name]])
            else:
                magnet_segments = objects_segmentation
            for face in magnet_segments[magnet.name]:
                obj = self.aedtapp.modeler.create_object_from_face(face.top_face_z)
                faces.append(obj.top_face_z)
            [face.delete() for face in magnet_segments[magnet.name]]
            self.aedtapp.assign_insulating(faces, "{}_segments".format(magnet.name))
            if isinstance(cs, CoordinateSystem):
                self._update_cs(cs)

        magnets = self.aedtapp.modeler.get_objects_by_material(self.properties.magnets_material)
        segments = self.aedtapp.modeler.get_objects_in_group("Insulating")

        self.properties.objects.extend([m.name for m in magnets])
        self.properties.objects.extend(segments)
        # self.properties.objects.extend(mesh_sheets)
        self.set_properties(self.properties.model_dump())

        self.aedtapp.save_project()
        self.release_aedt(False, False)
        return True

    # @thread.launch_thread
    def apply_skew(self):
        """Apply skew to rotor slices.

        .. warning::
            This method only works if the active AEDT project contains a shaft
            named ``Shaft``.

        Returns
        -------
        bool
            ``True`` when successful, ``False`` when failed.
        """
        try:
            self.connect_design()

            magnets = self.aedtapp.modeler.get_objects_by_material(self.properties.magnets_material)
            rotor_objects = self.aedtapp.modeler.get_objects_by_material(self.properties.rotor_material)
            # Independent and dependent boundary conditions can be assigned either to an object or an object face
            independent = [bound for bound in self.aedtapp.boundaries if bound.type == "Independent"]
            dependent = [bound for bound in self.aedtapp.boundaries if bound.type == "Dependent"]
            # If no indep. o dep. boundary are found it means that the symmetry factor is 1 -> whole motor.
            if independent or dependent:
                bound_indep_props = independent[0].props
                if "Objects" in bound_indep_props:
                    bound_indep_id = bound_indep_props["Objects"][0]
                    indep = self.aedtapp.modeler.objects[bound_indep_id]
                elif "Faces" in bound_indep_props:
                    bound_indep_id = bound_indep_props["Faces"][0]
                    obj = [o for o in self.aedtapp.modeler.object_list for f in o.faces if f.id == bound_indep_id][0]
                    indep = [f for f in obj.faces if f.id == bound_indep_id][0]
                bound_dep_props = dependent[0].props
                if "Objects" in bound_indep_props:
                    bound_dep_id = bound_dep_props["Objects"][0]
                    dep = self.aedtapp.modeler.objects[bound_dep_id]
                elif "Faces" in bound_indep_props:
                    bound_dep_id = bound_dep_props["Faces"][0]
                    obj = [o for o in self.aedtapp.modeler.object_list for f in o.faces if f.id == bound_dep_id][0]
                    dep = [f for f in obj.faces if f.id == bound_dep_id][0]

            # check whether stator has same rotor material
            if self.properties.rotor_material == self.properties.stator_material:
                stator_obj = max(rotor_objects, key=attrgetter("volume"))
                rotor_objects = [
                    x
                    for x in self.aedtapp.modeler.get_objects_by_material(self.properties.rotor_material)
                    if x != stator_obj and x.name != "Shaft"
                ]
            rotor_skew_ang = 0
            # rotate objects and apply skew
            for rotor_object in rotor_objects:
                if rotor_skew_ang != 0:
                    touching_objects = [self.aedtapp.modeler.objects_by_name[o] for o in rotor_object.touching_objects]
                    touching_magnets = list(set(touching_objects).intersection(magnets))
                    obj_in_bb = self.aedtapp.modeler.objects_in_bounding_box(
                        rotor_object.bounding_box, check_lines=False, check_sheets=False
                    )
                    if not touching_magnets:
                        touching_magnets = list(set(obj_in_bb).intersection(magnets))
                    for obj in touching_magnets:
                        magnet_cs = [
                            cs
                            for cs in self.aedtapp.modeler.coordinate_systems
                            if cs.name == obj.part_coordinate_system
                        ][0]
                        if isinstance(magnet_cs, CoordinateSystem):
                            magnet_cs.props["Phi"] = "{}+{}deg".format(magnet_cs.props["Phi"], rotor_skew_ang)
                        elif isinstance(magnet_cs, FaceCoordinateSystem):
                            magnet_cs.props["ZRotationAngle"] = "{}deg".format(rotor_skew_ang)
                        self.aedtapp.modeler.set_working_coordinate_system("Global")
                    for obj in obj_in_bb:
                        obj.rotate(axis="Z", angle=rotor_skew_ang)
                    rotor_object.rotate(axis="Z", angle=rotor_skew_ang)

                    # It means that indep. and dep. boundaries exist -> symmetry factor != 1
                    if independent and dependent:
                        split = self.aedtapp.modeler.split(assignment=rotor_object, sides="Both", tool=indep.id)
                        if [s for s in split if s not in self.aedtapp.modeler.objects_by_name]:
                            self.aedtapp.odesign.Undo()
                        split = self.aedtapp.modeler.split(assignment=rotor_object, sides="Both", tool=dep.id)
                        split_objects = [self.aedtapp.modeler.objects_by_name[obj] for obj in split]
                        # Get object with minimum volume
                        min_vol_object = min(split_objects, key=lambda x: x.volume).volume
                        # Get object whose volume is equal to min_vol_object
                        obj_rotate = [obj for obj in split_objects if round(obj.volume, 4) == round(min_vol_object, 4)][
                            0
                        ]
                        # duplicate around z axis (-360/symmetry_multiplier)
                        self.aedtapp.modeler.rotate(
                            obj_rotate, self.aedtapp.AXIS.Z, -360 / self.aedtapp.symmetry_multiplier
                        )
                        self.aedtapp.modeler.unite([split_objects[0], split_objects[1]])
                rotor_skew_ang += decompose_variable_value(self.properties.skew_angle)[0]

            self.release_aedt(False, False)
            return True
        except:
            return False

    def validate_and_analyze(self):
        """Validate and analyze the design.

        Returns
        -------
        bool
            ``True`` when successful, ``False`` when failed.
        """
        self.connect_design()

        if self.aedtapp.validate_simple():
            self.aedtapp.analyze_setup(self.properties.setup_to_analyze, use_auto_settings=False)
            self.release_aedt(False, False)
            return True
        else:
            return False

    def get_magnet_loss(self):
        """Get magnet loss.

        Returns
        -------
        dict
            dictionary containing the average magnet loss value when successful, ``False`` when failed.
        """
        self.connect_design()

        try:
            self.aedtapp.post.create_report(expressions="SolidLoss", plot_name="Losses", primary_sweep_variable="Time")
            data = self.aedtapp.post.get_solution_data(
                expressions="SolidLoss", primary_sweep_variable="Time", domain="Sweep"
            )
            avg = Quantity(
                expression=sum(data.data_magnitude()) / len(data.data_magnitude()), unit=data.units_data["SolidLoss"]
            )
            avg_w = avg.to("W")
            self.release_aedt(False, False)
            return str(avg_w)
        except:
            return False

    def _get_rotor_pockets(self, vacuum_objects):
        """Get the rotor pockets if any.

        Parameters
        ----------
        vacuum_objects : list
            List of vacuum objects.

        Returns
        -------
        list
            List of the class:`ansys.aedt.core.modeler.cad.object3d.Object3d` classes.
        """
        rotor_pockets = []
        for obj in vacuum_objects:
            obj_in_bb = self.aedtapp.modeler.objects_in_bounding_box(
                obj.bounding_box, check_lines=False, check_sheets=False
            )
            obj_in_bb.remove(obj)
            if isinstance(obj_in_bb, list) and len(obj_in_bb) == 1:
                rotor_pockets.append(obj)
        return rotor_pockets

    def _update_cs(self, cs):
        """Update the coordinate system to Euler ZYZ mode.

        Parameters
        ----------
        cs : :class:`ansys.aedt.core.modeler.Modeler.CoordinateSystem`
            Coordinate system.

        Returns
        -------
        bool
            ``True`` when successful, ``False`` when failed.
        """
        try:
            x_pointing = [
                decompose_variable_value(cs.props["XAxisXvec"])[0],
                decompose_variable_value(cs.props["XAxisYvec"])[0],
                decompose_variable_value(cs.props["XAxisZvec"])[0],
            ]
            y_pointing = [
                decompose_variable_value(cs.props["YAxisXvec"])[0],
                decompose_variable_value(cs.props["YAxisYvec"])[0],
                decompose_variable_value(cs.props["YAxisZvec"])[0],
            ]
            x, y, z = go.pointing_to_axis(x_pointing, y_pointing)
            phi, theta, psi = go.axis_to_euler_zyz(x, y, z)
            magnet_angle = go.rad2deg(phi)
            cs.change_cs_mode(2)
            cs.props["Phi"] = "{}deg".format(str(magnet_angle))
            cs.props["Psi"] = "90deg"
            cs.props["Theta"] = "0deg"
            cs.update()
            return True
        except:
            return False

    # @thread.launch_thread
    def _get_project_materials(self):
        """Get the project materials."""
        self.connect_design()

        if not self.aedtapp:
            return
        mats = self.aedtapp.materials.get_used_project_material_names()

        self.release_aedt(False, False)

        return mats

    def _get_design_setup_names(self):
        """Get the design setup names."""
        self.connect_design()

        setup_names = [setup.name for setup in self.aedtapp.setups]

        self.release_aedt(False, False)

        return setup_names
