from __future__ import annotations

import typing
import warnings
from typing import Optional, Union

import matplotlib.pyplot as plt
import numpy as np
import numpy.typing as npt
import pandas as pd

from pyoma2.algorithms.data.result import BaseResult
from pyoma2.functions.gen import (
    check_on_geo1,
    check_on_geo2,
    read_excel_file,
)

from .data import Geometry1, Geometry2
from .mpl_plotter import Geo1MplPlotter, Geo2MplPlotter
from .pyvista_plotter import PvGeoPlotter

if typing.TYPE_CHECKING:
    try:
        import pyvista as pv
    except ImportError:
        warnings.warn(
            "Optional package 'pyvista' is not installed. Some features may not be available.",
            ImportWarning,
            stacklevel=2,
        )
        warnings.warn(
            "Install 'pyvista' with 'pip install pyvista' or 'pip install pyoma_2[pyvista]'",
            ImportWarning,
            stacklevel=2,
        )


class GeometryMixin:
    """
    Mixin that gives the ability to define the geometry the instance of the setup class.

    This mixin provides methods to define the geometry setups for the instance, including
    sensor names, coordinates, directions, and optional elements like lines, surfaces, and
    background nodes. It also includes methods to plot the geometry setups using Matplotlib
    and PyVista for 2D and 3D visualization, respectively.
    """

    geo1: typing.Optional[Geometry1] = None
    geo2: typing.Optional[Geometry2] = None

    def def_geo1(
        self,
        # # MANDATORY
        sens_names: typing.Union[
            typing.List[str],
            typing.List[typing.List[str]],
            pd.DataFrame,
            npt.NDArray[np.str_],
        ],  # sensors' names
        sens_coord: pd.DataFrame,  # sensors' coordinates
        sens_dir: npt.NDArray[np.int64],  # sensors' directions
        # # OPTIONAL
        sens_lines: npt.NDArray[np.int64] = None,  # lines connecting sensors
        bg_nodes: npt.NDArray[np.float64] = None,  # Background nodes
        bg_lines: npt.NDArray[np.int64] = None,  # Background lines
        bg_surf: npt.NDArray[np.float64] = None,  # Background surfaces
    ) -> None:
        """
        Defines the first geometry setup (geo1) for the instance.

        This method sets up the geometry involving sensors' names, coordinates, directions,
        and optional elements like sensor lines, background nodes, lines, and surfaces.

        Parameters
        ----------
        sens_names : Union[numpy.ndarray of string, List of string]
            An array or list containing the names of the sensors.
        sens_coord : pandas.DataFrame
            A DataFrame containing the coordinates of the sensors.
        sens_dir : numpy.ndarray of int64
            An array defining the directions of the sensors.
        sens_lines : numpy.ndarray of int64, optional
            An array defining lines connecting sensors. Default is None.
        bg_nodes : numpy.ndarray of float64, optional
            An array defining background nodes. Default is None.
        bg_lines : numpy.ndarray of int64, optional
            An array defining background lines. Default is None.
        bg_surf : numpy.ndarray of float64, optional
            An array defining background surfaces. Default is None.
        """
        # Get reference index (if any)
        ref_ind = getattr(self, "ref_ind", None)

        # Assemble dictionary for check function
        file_dict = {
            "sensors names": sens_names,
            "sensors coordinates": sens_coord,
            "sensors directions": sens_dir,
            "sensors lines": sens_lines if sens_lines is not None else pd.DataFrame(),
            "BG nodes": bg_nodes if bg_nodes is not None else pd.DataFrame(),
            "BG lines": bg_lines if bg_lines is not None else pd.DataFrame(),
            "BG surfaces": bg_surf if bg_surf is not None else pd.DataFrame(),
        }

        # check on input
        res_ok = check_on_geo1(file_dict, ref_ind=ref_ind)

        self.geo1 = Geometry1(
            sens_names=res_ok[0],
            sens_coord=res_ok[1],
            sens_dir=res_ok[2],
            sens_lines=res_ok[3],
            bg_nodes=res_ok[4],
            bg_lines=res_ok[5],
            bg_surf=res_ok[6],
        )

    # metodo per definire geometria 2
    def def_geo2(
        self,
        # MANDATORY
        sens_names: typing.Union[
            typing.List[str],
            typing.List[typing.List[str]],
            pd.DataFrame,
            npt.NDArray[np.str_],
        ],  # sensors' names
        pts_coord: pd.DataFrame,  # points' coordinates
        sens_map: pd.DataFrame,  # mapping
        # OPTIONAL
        cstr: pd.DataFrame = None,
        sens_sign: pd.DataFrame = None,
        sens_lines: npt.NDArray[np.int64] = None,  # lines connecting sensors
        sens_surf: npt.NDArray[np.int64] = None,  # surf connecting sensors
        bg_nodes: npt.NDArray[np.float64] = None,  # Background nodes
        bg_lines: npt.NDArray[np.float64] = None,  # Background lines
        bg_surf: npt.NDArray[np.float64] = None,  # Background lines
    ) -> None:
        """
        Defines the second geometry setup (geo2) for the instance.

        This method sets up an alternative geometry configuration, including sensors' names,
        points' coordinates, mapping, sign data, and optional elements like constraints,
        sensor lines, background nodes, lines, and surfaces.

        Parameters
        ----------
        sens_names : Union[list of str, list of list of str, pandas.DataFrame, numpy.ndarray of str]
            Sensors' names. It can be a list of strings, a list of lists of strings, a DataFrame, or a NumPy array.
        pts_coord : pandas.DataFrame
            A DataFrame containing the coordinates of the points.
        sens_map : pandas.DataFrame
            A DataFrame containing the mapping data for sensors.
        cstrn : pandas.DataFrame, optional
            A DataFrame containing constraints. Default is None.
        sens_sign : pandas.DataFrame, optional
            A DataFrame containing sign data for the sensors. Default is None.
        sens_lines : numpy.ndarray of int64, optional
            An array defining lines connecting sensors. Default is None.
        bg_nodes : numpy.ndarray of float64, optional
            An array defining background nodes. Default is None.
        bg_lines : numpy.ndarray of float64, optional
            An array defining background lines. Default is None.
        bg_surf : numpy.ndarray of float64, optional
            An array defining background surfaces. Default is None.

        Notes
        -----
        This method adapts indices for 0-indexed lines in `bg_lines`, `sens_lines`, and `bg_surf`.
        """
        # Get reference index
        ref_ind = getattr(self, "ref_ind", None)

        # Assemble dictionary for check function
        file_dict = {
            "sensors names": sens_names,
            "points coordinates": pts_coord,
            "mapping": sens_map,
            "constraints": cstr if cstr is not None else pd.DataFrame(),
            "sensors sign": sens_sign if sens_sign is not None else pd.DataFrame(),
            "sensors lines": sens_lines if sens_lines is not None else pd.DataFrame(),
            "sensors surfaces": sens_surf if sens_surf is not None else pd.DataFrame(),
            "BG nodes": bg_nodes if bg_nodes is not None else pd.DataFrame(),
            "BG lines": bg_lines if bg_lines is not None else pd.DataFrame(),
            "BG surfaces": bg_surf if bg_surf is not None else pd.DataFrame(),
        }

        # check on input
        res_ok = check_on_geo2(file_dict, ref_ind=ref_ind)

        # Save to geometry
        self.geo2 = Geometry2(
            sens_names=res_ok[0],
            pts_coord=res_ok[1].astype(float),
            sens_map=res_ok[2],
            cstrn=res_ok[3],
            sens_sign=res_ok[4],
            sens_lines=res_ok[5],
            sens_surf=res_ok[6],
            bg_nodes=res_ok[7],
            bg_lines=res_ok[8],
            bg_surf=res_ok[9],
        )

    def _def_geo_by_file(
        self, geo_type: str, path: str, **read_excel_file_kwargs
    ) -> None:
        """
        Defines the geometry setup from an Excel file.

        This method reads an Excel file to extract sensor information, including sensor names,
        coordinates, and other optional geometry elements such as lines and background nodes.
        The information is used to set up the geometry for the instance.

        Parameters
        ----------
        geo_type : str
            The type of geometry to define: 'geo1' or 'geo2'.
        path : str
            The file path to the Excel file containing the geometry data.
        read_excel_file_kwargs : dict, optional
            Additional keyword arguments to pass to the `read_excel_file` function.

        Raises
        ------
        ValueError
            If the input data is invalid or missing required fields.
        """
        # Get reference index
        ref_ind = getattr(self, "ref_ind", None)

        # Read the Excel file
        file_dict = read_excel_file(path=path, **read_excel_file_kwargs)

        # Check on input
        if geo_type == "geo1":
            res_ok = check_on_geo1(file_dict, ref_ind=ref_ind)
            self.geo1 = Geometry1(
                sens_names=res_ok[0],
                sens_coord=res_ok[1],
                sens_dir=res_ok[2],
                sens_lines=res_ok[3],
                bg_nodes=res_ok[4],
                bg_lines=res_ok[5],
                bg_surf=res_ok[6],
            )
        elif geo_type == "geo2":
            res_ok = check_on_geo2(file_dict, ref_ind=ref_ind)
            self.geo2 = Geometry2(
                sens_names=res_ok[0],
                pts_coord=res_ok[1].astype(float),
                sens_map=res_ok[2],
                cstrn=res_ok[3],
                sens_sign=res_ok[4],
                sens_lines=res_ok[5],
                sens_surf=res_ok[6],
                bg_nodes=res_ok[7],
                bg_lines=res_ok[8],
                bg_surf=res_ok[9],
            )
        else:
            raise ValueError(f"Invalid geometry type: {geo_type}")

    # metodo per definire geometria 1 da file
    def def_geo1_by_file(self, path: str, **read_excel_file_kwargs) -> None:
        """
        Defines the first geometry (geo1) from an Excel file.

        This method reads an Excel file to extract sensor information, including sensor names,
        coordinates, and other optional geometry elements such as lines and background nodes.
        The information is used to set up the geometry for the instance.

        Parameters
        ----------
        path : str
            The file path to the Excel file containing the geometry data.
        read_excel_file_kwargs : dict, optional
            Additional keyword arguments to pass to the `read_excel_file` function.

        Raises
        ------
        ValueError
            If the input data is invalid or missing required fields.
        """
        self._def_geo_by_file(geo_type="geo1", path=path, **read_excel_file_kwargs)

    def def_geo2_by_file(self, path: str, **read_excel_file_kwargs) -> None:
        """
        Defines the second geometry (geo2) from an Excel file.

        This method reads an Excel file to extract information related to the geometry configuration,
        including sensor names, points' coordinates, mapping, and optional background nodes and surfaces.
        The information is used to set up the second geometry for the instance.

        Parameters
        ----------
        path : str
            The file path to the Excel file containing the geometry data.
        read_excel_file_kwargs : dict, optional
            Additional keyword arguments to pass to the `read_excel_file` function.

        Raises
        ------
        ValueError
            If the input data is invalid or missing required fields.
        """
        self._def_geo_by_file(geo_type="geo2", path=path, **read_excel_file_kwargs)

    # PLOT GEO1 - mpl plotter
    def plot_geo1(
        self,
        scaleF: int = 1,
        view: typing.Literal["3D", "xy", "xz", "yz"] = "3D",
        col_sns: str = "red",
        col_sns_lines: str = "red",
        col_BG_nodes: str = "gray",
        col_BG_lines: str = "gray",
        col_BG_surf: str = "gray",
        col_txt: str = "red",
    ) -> typing.Tuple[plt.Figure, plt.Axes]:
        """
        Plots the first geometry setup (geo1) using Matplotlib.

        This method creates a 2D or 3D plot of the first geometry, including sensors, lines, background nodes,
        and surfaces, using customizable color schemes for each element.

        Parameters
        ----------
        scaleF : int, optional
            Scaling factor for the plot. Default is 1.
        view : {'3D', 'xy', 'xz', 'yz'}, optional
            The view angle of the plot. Default is '3D'.
        col_sns : str, optional
            Color of the sensors. Default is 'red'.
        col_sns_lines : str, optional
            Color of the lines connecting sensors. Default is 'red'.
        col_BG_nodes : str, optional
            Color of the background nodes. Default is 'gray'.
        col_BG_lines : str, optional
            Color of the background lines. Default is 'gray'.
        col_BG_surf : str, optional
            Color of the background surfaces. Default is 'gray'.
        col_txt : str, optional
            Color of the text labels for sensors. Default is 'red'.

        Returns
        -------
        tuple
            A tuple containing the Matplotlib figure and axes objects.

        Raises
        ------
        ValueError
            If `geo1` is not defined.
        """

        if self.geo1 is None:
            raise ValueError("geo1 is not defined. Call def_geo1 first.")

        Plotter = Geo1MplPlotter(self.geo1)

        fig, ax = Plotter.plot_geo(
            scaleF,
            view,
            col_sns,
            col_sns_lines,
            col_BG_nodes,
            col_BG_lines,
            col_BG_surf,
            col_txt,
        )
        return fig, ax

    # PLOT GEO2 - PyVista plotter
    # PLOT GEO2 - PyVista plotter
    def plot_geo2(
        self,
        *,
        scaleF: float = 1.0,
        col_sens: str = "red",
        show_points: bool = True,
        show_lines: bool = True,
        show_surf: bool = True,
        points_sett: Optional[dict] = None,
        lines_sett: Optional[dict] = None,
        surf_sett: Optional[dict] = None,
        background: bool = True,
        notebook: bool = False,
    ) -> pv.Plotter:
        """
        Plots the second geometry setup (geo2) using PyVista for 3D visualization.

        This method creates a 3D interactive plot of the second geometry setup with options
        to visualize sensor points, connecting lines, and surfaces. It provides various
        customization options for coloring and rendering.

        Parameters
        ----------
        scaleF : float, optional
            Scaling factor for sensor arrow length. Default is 1.0.
        col_sens : str, optional
            Color of the sensors. Default is 'red'.
        show_points : bool, optional
            Whether to plot sensor points. Default is True.
        show_lines : bool, optional
            Whether to plot lines connecting sensors. Default is True.
        show_surf : bool, optional
            Whether to plot surfaces connecting sensors. Default is True.
        points_sett : dict or None, optional
            Settings for the points' appearance; falls back to defaults if None.
        lines_sett : dict or None, optional
            Settings for the lines' appearance; falls back to defaults if None.
        surf_sett : dict or None, optional
            Settings for the surfaces' appearance; falls back to defaults if None.
        background : bool, optional
            Whether to use a background Qt plotter if creating new. Default is True.
        notebook : bool, optional
            Whether to render the plot in a Jupyter notebook environment. Default is False.

        Returns
        -------
        pyvista.Plotter
            A PyVista Plotter object with the geometry visualization.

        Raises
        ------
        ValueError
            If `geo2` is not defined.
        """
        if self.geo2 is None:
            raise ValueError("geo2 is not defined. Call def_geo2 first.")

        Plotter = PvGeoPlotter(self.geo2)

        pl = Plotter.plot_geo(
            scaleF=scaleF,
            col_sens=col_sens,
            show_points=show_points,
            points_sett=points_sett,
            show_lines=show_lines,
            lines_sett=lines_sett,
            show_surf=show_surf,
            surf_sett=surf_sett,
            pl=None,
            background=background,
            notebook=notebook,
        )
        return pl

    # PLOT GEO2 - Matplotlib plotter
    def plot_geo2_mpl(
        self,
        scaleF: int = 1,
        view: typing.Literal["3D", "xy", "xz", "yz", "x", "y", "z"] = "3D",
        col_sns: str = "red",
        col_sns_lines: str = "black",
        col_sns_surf: str = "lightcoral",
        col_BG_nodes: str = "gray",
        col_BG_lines: str = "gray",
        col_BG_surf: str = "gray",
        col_txt: str = "red",
    ) -> typing.Tuple[plt.Figure, plt.Axes]:
        """
        Plots the second geometry setup (geo2) using Matplotlib.

        This method creates a 2D or 3D plot of the second geometry, including sensors, lines,
        surfaces, background nodes, and surfaces, with customizable colors.

        Parameters
        ----------
        scaleF : int, optional
            Scaling factor for the plot. Default is 1.
        view : {'3D', 'xy', 'xz', 'yz', 'x', 'y', 'z'}, optional
            The view angle of the plot. Default is '3D'.
        col_sns : str, optional
            Color of the sensors. Default is 'red'.
        col_sns_lines : str, optional
            Color of the lines connecting sensors. Default is 'black'.
        col_sns_surf : str, optional
            Color of the surfaces connecting sensors. Default is 'lightcoral'.
        col_BG_nodes : str, optional
            Color of the background nodes. Default is 'gray'.
        col_BG_lines : str, optional
            Color of the background lines. Default is 'gray'.
        col_BG_surf : str, optional
            Color of the background surfaces. Default is 'gray'.
        col_txt : str, optional
            Color of the text labels for sensors. Default is 'red'.

        Returns
        -------
        tuple
            A tuple containing the Matplotlib figure and axes objects.

        Raises
        ------
        ValueError
            If `geo2` is not defined.
        """
        if self.geo2 is None:
            raise ValueError("geo2 is not defined. Call def_geo2 first.")

        Plotter = Geo2MplPlotter(self.geo2)

        fig, ax = Plotter.plot_geo(
            scaleF,
            view,
            col_sns,
            col_sns_lines,
            col_sns_surf,
            col_BG_nodes,
            col_BG_lines,
            col_BG_surf,
            col_txt,
        )
        return fig, ax

    def plot_mode_geo1(
        self,
        algo_res: BaseResult,
        mode_nr: int,
        scaleF: int = 1,
        view: typing.Literal["3D", "xy", "xz", "yz"] = "3D",
        col_sns: str = "red",
        col_sns_lines: str = "red",
        col_BG_nodes: str = "gray",
        col_BG_lines: str = "gray",
        col_BG_surf: str = "gray",
    ) -> typing.Tuple[plt.Figure, plt.Axes]:
        """
        Plots the mode shapes for the first geometry setup (geo1) using Matplotlib.

        This method visualizes the mode shapes corresponding to the specified mode number, with customizable
        colors and scaling for different geometrical elements such as sensors, lines, and background surfaces.

        Parameters
        ----------
        algo_res : BaseResult
            The result object containing modal parameters and mode shape data.
        mode_nr : int
            The mode number to be plotted.
        scaleF : int, optional
            Scaling factor to adjust the size of the mode shapes. Default is 1.
        view : {'3D', 'xy', 'xz', 'yz'}, optional
            The viewing plane or angle for the plot. Default is '3D'.
        col_sns : str, optional
            Color of the sensors in the plot. Default is 'red'.
        col_sns_lines : str, optional
            Color of the lines connecting the sensors. Default is 'red'.
        col_BG_nodes : str, optional
            Color of the background nodes in the plot. Default is 'gray'.
        col_BG_lines : str, optional
            Color of the background lines in the plot. Default is 'gray'.
        col_BG_surf : str, optional
            Color of the background surfaces in the plot. Default is 'gray'.

        Returns
        -------
        tuple
            A tuple containing the Matplotlib figure and axes objects for further customization or saving.

        Raises
        ------
        ValueError
            If `geo1` is not defined or if the algorithm results are missing.
        """
        if self.geo1 is None:
            raise ValueError("geo1 is not defined. Call def_geo1 first.")

        if algo_res.Fn is None:
            raise ValueError("Run algorithm first")
        Plotter = Geo1MplPlotter(self.geo1, algo_res)

        fig, ax = Plotter.plot_mode(
            mode_nr,
            scaleF,
            view,
            col_sns,
            col_sns_lines,
            col_BG_nodes,
            col_BG_lines,
            col_BG_surf,
        )
        return fig, ax

    # PLOT MODI - PyVista plotter
    def plot_mode_geo2(
        self,
        algo_res: BaseResult,
        *,
        mode_nr: int = 1,
        scaleF: float = 1.0,
        show_lines: bool = True,
        show_surf: bool = True,
        def_sett: Optional[dict] = None,
        undef_sett: Optional[dict] = None,
        background: bool = True,
        notebook: bool = False,
    ) -> pv.Plotter:
        """
        Plots the mode shapes for the second geometry setup (geo2) using PyVista.

        This method creates an interactive 3D plot of a single mode shape (with undeformed
        geometry underneath) for the second geometry setup. You can toggle connection lines
        and surface faces, and supply custom plot settings.

        Parameters
        ----------
        algo_res : BaseResult
            The result object containing modal parameters and mode shape data.
        mode_nr : int, optional
            Mode number to visualize (1-based). Default is 1.
        scaleF : float, optional
            Scale factor for deformation amplitude. Default is 1.0.
        show_lines : bool, optional
            Whether to render connection lines on the mode shape. Default is True.
        show_surf : bool, optional
            Whether to render surface faces on the mode shape. Default is True.
        def_sett : dict or None, optional
            Plot settings for the deformed shape; falls back to defaults if None.
        undef_sett : dict or None, optional
            Plot settings for the undeformed shape; falls back to defaults if None.
        background : bool, optional
            Whether to use a background Qt plotter if creating new. Default is True.
        notebook : bool, optional
            Whether to render the plot in a Jupyter notebook environment. Default is False.

        Returns
        -------
        pv.Plotter
            A PyVista Plotter object with the mode‐shape visualization.

        Raises
        ------
        ValueError
            If `geo2` is not defined or if `algo_res.Fn` is None.
        """
        if self.geo2 is None:
            raise ValueError("geo2 is not defined. Call def_geo2 first.")
        if algo_res.Fn is None:
            raise ValueError("Run algorithm first")

        Plotter = PvGeoPlotter(self.geo2, algo_res)

        pl = Plotter.plot_mode(
            mode_nr=mode_nr,
            scaleF=scaleF,
            show_lines=show_lines,
            show_surf=show_surf,
            def_sett=def_sett,
            undef_sett=undef_sett,
            pl=None,
            background=background,
            notebook=notebook,
        )
        return pl

    # PLOT MODI - Matplotlib plotter
    def plot_mode_geo2_mpl(
        self,
        algo_res: BaseResult,
        mode_nr: typing.Optional[int],
        scaleF: int = 1,
        view: typing.Literal["3D", "xy", "xz", "yz"] = "3D",
        color: str = "cmap",
        *args,
        **kwargs,
    ) -> typing.Tuple[plt.Figure, plt.Axes]:
        """
        Plots the mode shapes for the second geometry setup (geo2) using Matplotlib.

        This method visualizes the mode shapes for geo2, with customizable scaling, color, and viewing options.
        The plot can be configured for different modes and color maps.

        Parameters
        ----------
        algo_res : BaseResult
            The result object containing modal parameters and mode shape data.
        mode_nr : int, optional
            The mode number to be plotted. If None, the default mode is plotted.
        scaleF : int, optional
            Scaling factor to adjust the size of the mode shapes. Default is 1.
        view : {'3D', 'xy', 'xz', 'yz'}, optional
            The viewing plane or angle for the plot. Default is '3D'.
        color : str, optional
            Color scheme or colormap to be used for the mode shapes. Default is 'cmap'.

        Returns
        -------
        tuple
            A tuple containing the Matplotlib figure and axes objects for further customization or saving.

        Raises
        ------
        ValueError
            If `geo2` is not defined or if the algorithm results are missing (e.g., `Fn` is None).
        """
        if self.geo2 is None:
            raise ValueError("geo2 is not defined. Call def_geo2 first.")

        if algo_res.Fn is None:
            raise ValueError("Run algorithm first")

        Plotter = Geo2MplPlotter(self.geo2, algo_res)

        fig, ax = Plotter.plot_mode(mode_nr, scaleF, view, color)
        return fig, ax

    # PLOT MODI - PyVista plotter
    def anim_mode_geo2(
        self,
        algo_res: BaseResult,
        *,
        mode_nr: int = 1,
        scaleF: float = 1.0,
        show_lines: bool = True,
        show_surf: bool = True,
        def_sett: Optional[dict] = None,
        save_gif: bool = False,
        pl: Optional[pv.Plotter] = None,
    ) -> Union[pv.Plotter, str]:
        """
        Creates an animation of the mode shape for the second geometry setup (geo2).

        This wraps PvGeoPlotter.animate_mode, letting you animate a single mode
        (with optional GIF export) on geo2.

        Parameters
        ----------
        algo_res : BaseResult
            The result object containing modal parameters and mode shape data.
        mode_nr : int, optional
            Mode number to animate (1-based). Default is 1.
        scaleF : float, optional
            Scale factor for oscillation amplitude. Default is 1.0.
        show_lines : bool, optional
            Whether to render connection lines during the animation. Default is True.
        show_surf : bool, optional
            Whether to render surface faces during the animation. Default is True.
        def_sett : dict or None, optional
            Plot settings for animation frames; falls back to defaults if None.
        save_gif : bool, optional
            If True, save the animation as a GIF and return its filepath. Default is False.
        pl : pv.Plotter or None, optional
            Existing Plotter to use; if None, one is created. Default is None.

        Returns
        -------
        pv.Plotter or str
            The Plotter instance for live animation, or filepath string if GIF was saved.

        Raises
        ------
        ValueError
            If `geo2` is not defined or if `algo_res.Fn` is None.
        """
        if self.geo2 is None:
            raise ValueError("geo2 is not defined. Call def_geo2 first.")
        if algo_res.Fn is None:
            raise ValueError("Run algorithm first")

        Plotter = PvGeoPlotter(self.geo2, algo_res)

        result = Plotter.animate_mode(
            mode_nr=mode_nr,
            scaleF=scaleF,
            show_lines=show_lines,
            show_surf=show_surf,
            def_sett=def_sett,
            save_gif=save_gif,
            pl=pl,
        )
        return result
