"""
    This file is part of gempy.

    gempy is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    gempy 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 General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with gempy.  If not, see <http://www.gnu.org/licenses/>.


Module with classes and methods to visualize structural geology data and potential fields of the regional modelling based on
the potential field method.
Tested on Ubuntu 14

Created on 10.04.2019

@author: Miguel de la Varga
"""

import os
import matplotlib.colors as mcolors
import copy
import pandas as pn
import numpy as np
import sys
import gempy_legacy as gp
import warnings


warnings.filterwarnings("ignore",
                        message='.*Conversion of the second argument of issubdtype *.',
                        append=True)
try:
    import vtk
    from vtk.util.numpy_support import numpy_to_vtk
    VTK_IMPORT = True
except ImportError:
    VTK_IMPORT = False

try:
    import steno3d
    STENO_IMPORT = True
except ImportError:
    STENO_IMPORT = False

try:
    import ipyvolume as ipv
    IPV_IMPORT = True
except ImportError:
    IPV_IMPORT = False

import pyvista as pv

class vtkVisualization(object):
    """
    Class to visualize data and results in 3D. Init will create all the render properties while the method render
    model will lunch the window. Using set_surface_points, set_orientations and set_surfaces in between can be chosen what
    will be displayed.

    Args:
        geo_data(gempy.InputData): All values of a DataManagement object
        ren_name (str): Name of the renderer window
        verbose (int): Verbosity for certain functions

    Attributes:
        renWin(vtk.vtkRenderWindow())
        camera_list (list): list of cameras for the distinct renderers
        ren_list (list): list containing the vtk renderers
    """

    def __init__(self, geo_data, ren_name='GemPy 3D-Editor', verbose=0, real_time=False, bg_color=None, ve=1,
                 **kwargs):
        if VTK_IMPORT is False:
            raise ImportError('vtk is not installed. No vtk capabilities are possible')

        self.ve = ve

        self.real_time = real_time
        self.geo_model = geo_data
        self.interp_data = None
        self.layer_visualization = True
        # self.C_LOT = #dict(zip(self.geo_model.surfaces.df['id'], self.geo_model.surfaces.df['color']))
        #
        # for surf, color in self.C_LOT.items(): #convert hex to rgb
        #     self.C_LOT[surf] = mcolors.hex2color(color)

        self.ren_name = ren_name
        # Number of renders
        self.n_ren = 4
        self.id = geo_data._surface_points.df['id'].unique()
        self.surface_name = geo_data._surface_points.df['surface'].unique()

        # Extents
        self.extent = copy.copy(self.geo_model._grid.regular_grid.extent)
        self.extent[-1] = ve * self.extent[-1]
        self.extent[-2] = ve * self.extent[-2]
        _e = self.extent
        self._e_dx = _e[1] - _e[0]
        self._e_dy = _e[3] - _e[2]
        self._e_dz = _e[5] - _e[4]
        self._e_d_avrg = (self._e_dx + self._e_dy + self._e_dz) / 3

        # Create containers of the vtk objectes
        self.s_rend_1 = pn.DataFrame(columns=['val'])
        self.s_rend_2 = pn.DataFrame(columns=['val'])
        self.s_rend_3 = pn.DataFrame(columns=['val'])
        self.s_rend_4 = pn.DataFrame(columns=['val'])

        self.o_rend_1 = pn.DataFrame(columns=['val'])
        self.o_rend_2 = pn.DataFrame(columns=['val'])
        self.o_rend_3 = pn.DataFrame(columns=['val'])
        self.o_rend_4 = pn.DataFrame(columns=['val'])

        # Resolution
        self.res = self.geo_model._grid.regular_grid.resolution

        # create render window, settings
        self.renwin = vtk.vtkRenderWindow()
        self.renwin.SetWindowName(ren_name)

        # Set 4 renderers. ie 3D, X,Y,Z projections
        self.ren_list = self.create_ren_list()

        # create interactor and set interactor style, assign render window
        self.interactor = vtk.vtkRenderWindowInteractor()
        self.interactor.SetRenderWindow(self.renwin)
        self.interactor.AddObserver("KeyPressEvent", self.key_callbacks)

        # 3d model camera for the 4 renders
        self.camera_list = self._create_cameras(self.extent, verbose=verbose)
        # Setting the camera and the background color to the renders
        self.set_camera_backcolor(color=bg_color)

        # Creating the axis
        for e, r in enumerate(self.ren_list):
            # add axes actor to all renderers
            axe = self._create_axes(self.camera_list[e])

            r.AddActor(axe)
            r.ResetCamera()
        # TODO panel does not deal with text
        self.set_text()

    #

    # def restart(self):
    #     try:
    #         self.close_window()
    #     except AttributeError:
    #         pass
    #
    #     self.__init__(self.geo_model, )

    def render_model(self, **kwargs):
        """
        Method to launch the window

        Args:
            size (tuple): Resolution of the window
            fullscreen (bool): Launch window in full screen or not
        Returns:

        """
        # initialize and start the app
        if 'size' not in kwargs:
            kwargs['size'] = (1920, 1080)

        if 'fullscreen' in kwargs:
            self.renwin.FullScreenOn()

        silent = kwargs.get('silent', False)

        self.renwin.SetSize(kwargs['size'])

        if silent is False:
            self.interactor.Initialize()
            self.interactor.Start()

    def set_text(self):
        txt = vtk.vtkTextActor()
        txt.SetInput("Press L to toggle layers visibility \n"
                     "Press R to toggle real time updates \n"
                     "Press H or P to go back to Python \n"
                     "Press Q to quit")
        txtprop = txt.GetTextProperty()
        txtprop.SetFontFamilyToArial()
        txtprop.SetFontSize(18)
        txtprop.SetColor(1, 1, 1)
        txt.SetDisplayPosition(20, 60)

        # assign actor to the renderer
        self.ren_list[0].AddActor(txt)

    def close_window(self):
        # close_window(interactor)
        render_window = self.interactor.GetRenderWindow()
        render_window.Finalize()
        self.interactor.TerminateApp()
        del self.renwin, self.interactor

    def key_callbacks(self, obj, event):
        key = self.interactor.GetKeySym()

        if key == 'h' or key == 'p':
            print('holding... Use vtk.resume to go back to the interactive window')
            self.interactor.ExitCallback()
            self.interactor.holding = True

        if key == 'l':
            if self.layer_visualization is True:
                for layer in self.surf_rend_1:
                    layer.VisibilityOff()
                self.layer_visualization = False
                self.interactor.Render()
            elif self.layer_visualization is False:
                for layer in self.surf_rend_1:
                    layer.VisibilityOn()
                self.layer_visualization = True
                self.interactor.Render()

        if key == 't':
            if self.topo_visualization is True:
                self.topography_surface.VisibilityOff()
                self.topo_visualization = False
                self.interactor.Render()
            elif self.topo_visualization is False:
                self.topography_surface.VisibilityOn()
                self.topo_visualization = True
                self.interactor.Render()

        if key == 'q':
            print('closing vtk')
            self.close_window()

            # TODO: Decide if this even matter
            # # create render window, settings
            # self.renwin = vtk.vtkRenderWindow()
            # self.renwin.SetWindowName(self.ren_name)
            #
            # # Set 4 renderers. ie 3D, X,Y,Z projections
            # self.ren_list = self.create_ren_list()
            #
            # # create interactor and set interactor style, assign render window
            # self.interactor = vtk.vtkRenderWindowInteractor()
            # self.interactor.SetRenderWindow(self.renwin)
            # self.interactor.AddObserver("KeyPressEvent", self.key_callbacks)
            #
            # # 3d model camera for the 4 renders
            # self.camera_list = self._create_cameras(self.extent)
            # # Setting the camera and the background color to the renders
            # self.set_camera_backcolor()
            #
            # # Creating the axis
            # for e, r in enumerate(self.ren_list):
            #     # add axes actor to all renderer
            #     axe = self._create_axes(self.camera_list[e])
            #
            #     r.AddActor(axe)
            #     r.ResetCamera()

        if key == 'r':
            self.real_time = self.real_time ^ True

    def create_surface_points(self, vertices):
        """
        Method to create the points that form the surfaces
        Args:
            vertices (numpy.array): 2D array (XYZ) with the coordinates of the points

        Returns:
            vtk.vtkPoints: with the coordinates of the points
        """
        Points = vtk.vtkPoints()
        if self.ve != 1:
            vertices[:, 2] = vertices[:, 2] * self.ve
        #     raise NotImplementedError('Vertical exageration for surfaces not implemented yet.')
        # for v in vertices:
        #     v[-1] = self.ve * v[-1]
        #     Points.InsertNextPoint(v)
        Points.SetData(numpy_to_vtk(vertices))

        return Points

    @staticmethod
    def create_surface_triangles(simplices):
        """
        Method to create the Triangles that form the surfaces
        Args:
            simplices (numpy.array): 2D array with the value of the vertices that form every single triangle

        Returns:
            vtk.vtkTriangle
        """

        Triangles = vtk.vtkCellArray()
        Triangle = vtk.vtkTriangle()

        for s in simplices:
            Triangle.GetPointIds().SetId(0, s[0])
            Triangle.GetPointIds().SetId(1, s[1])
            Triangle.GetPointIds().SetId(2, s[2])

            Triangles.InsertNextCell(Triangle)
        return Triangles

    def create_surface(self, vertices, simplices, fn, alpha=.8):
        """
        Method to create the polydata that define the surfaces

        Args:
            vertices (numpy.array): 2D array (XYZ) with the coordinates of the points
            simplices (numpy.array): 2D array with the value of the vertices that form every single triangle
            fn (int): id
            alpha (float): Opacity

        Returns:
            vtk.vtkActor, vtk.vtkPolyDataMapper, vtk.vtkPolyData
        """
        vertices_c = copy.deepcopy(vertices)
        simplices_c = copy.deepcopy(simplices)

        surf_polydata = vtk.vtkPolyData()

        surf_polydata.SetPoints(self.create_surface_points(vertices_c))
        surf_polydata.SetPolys(self.create_surface_triangles(simplices_c))
        surf_polydata.Modified()

        surf_mapper = vtk.vtkPolyDataMapper()
        surf_mapper.SetInputData(surf_polydata)
        surf_mapper.Update()

        surf_actor = vtk.vtkActor()
        surf_actor.SetMapper(surf_mapper)
        surf_actor.GetProperty().SetColor(mcolors.hex2color(self.geo_model._surfaces.df.set_index('id')['color'][fn]))#self.C_LOT[fn])
        surf_actor.GetProperty().SetOpacity(alpha)

        return surf_actor, surf_mapper, surf_polydata

    def create_sphere(self, X, Y, Z, fn, n_sphere=0, n_render=0, n_index=0, r=0.03):
        """
        Method to create the sphere that represent the surface_points points
        Args:
            X: X coord
            Y: Y coord
            Z: Z corrd
            fn (int): id
            n_sphere (int): Number of the sphere
            n_render (int): Number of the render where the sphere belongs
            n_index (int): index value in the PandasDataframe of InupData.surface_points
            r (float): radius of the sphere

        Returns:
            vtk.vtkSphereWidget
        """
        s = vtk.vtkSphereWidget()
        s.SetInteractor(self.interactor)
        s.SetRepresentationToSurface()
        s.SetPriority(2)
        Z = Z * self.ve
        s.r_f = self._e_d_avrg * r
        s.PlaceWidget(X - s.r_f, X + s.r_f, Y - s.r_f, Y + s.r_f, Z - s.r_f, Z + s.r_f)
        s.GetSphereProperty().SetColor(mcolors.hex2color(self.geo_model._surfaces.df.set_index('id')['color'][fn]))#self.C_LOT[fn])

        s.SetCurrentRenderer(self.ren_list[n_render])
        s.n_sphere = n_sphere
        s.n_render = n_render
        s.index = n_index
        s.AddObserver("EndInteractionEvent", self.sphereCallback)  # EndInteractionEvent
        s.AddObserver("InteractionEvent", self.Callback_camera_reset)

        s.On()

        return s

    def create_foliation(self, X, Y, Z, fn,
                         Gx, Gy, Gz,
                         n_plane=0, n_render=0, n_index=0, alpha=0.5):
        """
        Method to create a plane given a foliation

        Args:
            X : X coord
            Y: Y coord
            Z: Z coord
            fn (int): id
            Gx (str): Component of the gradient x
            Gy (str): Component of the gradient y
            Gz (str): Component of the gradient z
            n_plane (int): Number of the plane
            n_render (int): Number of the render where the plane belongs
            n_index (int): index value in the PandasDataframe of InupData.surface_points
            alpha: Opacity of the plane

        Returns:
            vtk.vtkPlaneWidget
        """

        Z = Z * self.ve

        d = vtk.vtkPlaneWidget()
        d.SetInteractor(self.interactor)
        d.SetRepresentationToSurface()

        # Position
        source = vtk.vtkPlaneSource()

        source.SetNormal(Gx, Gy, Gz)
        source.SetCenter(X, Y, Z)
        a, b, c, d_, e, f = self.extent

        source.SetPoint1(X+self._e_dx*.01, Y-self._e_dy*.01, Z)
        source.SetPoint2(X-self._e_dx*.01, Y+self._e_dy*.01, Z)
        source.Update()
        d.SetInputData(source.GetOutput())
        d.SetHandleSize(.05)
        min_extent = np.min([self._e_dx, self._e_dy, self._e_dz])
        d.SetPlaceFactor(0.1)

        d.PlaceWidget(a, b, c, d_, e, f)
        d.SetNormal(Gx, Gy, Gz)
        d.SetCenter(X, Y, Z)
        d.GetPlaneProperty().SetColor(mcolors.hex2color(self.geo_model._surfaces.df.set_index('id')['color'][fn]))#self.C_LOT[fn])
        d.GetHandleProperty().SetColor(mcolors.hex2color(self.geo_model._surfaces.df.set_index('id')['color'][fn]))#self.C_LOT[fn])
        d.GetHandleProperty().SetOpacity(alpha)
        d.SetCurrentRenderer(self.ren_list[n_render])
        d.n_plane = n_plane
        d.n_render = n_render
        d.index = n_index
        d.AddObserver("EndInteractionEvent", self.planesCallback)
        d.AddObserver("InteractionEvent", self.Callback_camera_reset)

        d.On()

        return d

    def set_surfaces_old(self, vertices, simplices, alpha=1):
        """
        Create all the surfaces and set them to the corresponding renders for their posterior visualization with
        render_model

        Args:
            vertices (list): list of 3D numpy arrays containing the points that form each plane
            simplices (list): list of 3D numpy arrays containing the verticies that form every triangle
            surfaces (list): ordered list of strings containing the name of the surfaces to represent
            fns (list): ordered list of surface_numbers (int)
            alpha: Opacity of the plane

        Returns:
            None
        """
        self.surf_rend_1 = []

        surfaces = self.surface_name

        fns = self.geo_model._surface_points.df['id'].unique().squeeze()
        assert type(vertices) is list or type(vertices) is np.ndarray, 'vertices and simpleces have to be a list of' \
                                                                       ' arrays even when only one' \
                                                                       ' surface is passed'
        assert 'DefaultBasement' not in surfaces, 'Remove DefaultBasement from the list of surfaces'
        for v, s, fn in zip(vertices, simplices, np.atleast_1d(fns)):
            act, map, pol = self.create_surface(v, s, fn, alpha)
            self.surf_rend_1.append(act)

            self.ren_list[0].AddActor(act)
            self.ren_list[1].AddActor(act)
            self.ren_list[2].AddActor(act)
            self.ren_list[3].AddActor(act)

    def set_surfaces(self, surfaces, alpha=1):
        self.surf_rend_1 = []
        for idx, val in surfaces.df[['vertices', 'edges', 'id']].dropna().iterrows():
            act, map, pol = self.create_surface(val['vertices'], val['edges'], val['id'], alpha)
            self.surf_rend_1.append(act)

            self.ren_list[0].AddActor(act)
            self.ren_list[1].AddActor(act)
            self.ren_list[2].AddActor(act)
            self.ren_list[3].AddActor(act)

    def set_topography(self):
        # Create points on an XY grid with random Z coordinate
        vertices = copy.copy(self.geo_model._grid.topography.values)

        points = vtk.vtkPoints()
        # for v in vertices:
        #     v[-1] = v[-1]
        #     points.InsertNextPoint(v)
        if self.ve !=1:
            vertices[:, 2]= vertices[:, 2]*self.ve
        points.SetData(numpy_to_vtk(vertices))

        # Add the grid points to a polydata object
        polydata = vtk.vtkPolyData()
        polydata.SetPoints(points)
        #
        # glyphFilter = vtk.vtkVertexGlyphFilter()
        # glyphFilter.SetInputData(polydata)
        # glyphFilter.Update()
        #
        # # Create a mapper and actor
        # pointsMapper = vtk.vtkPolyDataMapper()
        # pointsMapper.SetInputConnection(glyphFilter.GetOutputPort())

        #
        # pointsActor = vtk.vtkActor()
        # pointsActor.SetMapper(pointsMapper)
        # pointsActor.GetProperty().SetPointSize(3)
        # pointsActor.GetProperty().SetColor(colors.GetColor3d("Red"))

        # Triangulate the grid points
        delaunay = vtk.vtkDelaunay2D()
        delaunay.SetInputData(polydata)
        delaunay.Update()

        # Create a mapper and actor
        triangulatedMapper = vtk.vtkPolyDataMapper()
        triangulatedMapper.SetInputConnection(delaunay.GetOutputPort())

        triangulatedActor = vtk.vtkActor()
        triangulatedActor.SetMapper(triangulatedMapper)

        self.topography_surface = triangulatedActor
        self._topography_polydata = polydata
        self._topography_delauny = delaunay
        self.ren_list[0].AddActor(triangulatedActor)
        self.ren_list[1].AddActor(triangulatedActor)
        self.ren_list[2].AddActor(triangulatedActor)
        self.ren_list[3].AddActor(triangulatedActor)
        try:
            if self.geo_model.solutions.geological_map is not None:
                self.set_geological_map()
        except AttributeError as ae:
            warnings.warn(str(ae))

    def set_geological_map(self):
        assert self.geo_model.solutions.geological_map is not None, 'Geological map not computed. First' \
                                                                    'set active the topography grid'
        arr_ = np.empty((0, 3), dtype='int')

        # Convert hex colors to rgb
        for idx, val in self.geo_model._surfaces.df['color'].iteritems():
            rgb = (255 * np.array(mcolors.hex2color(val)))
            arr_ = np.vstack((arr_, rgb))

        sel = np.round(self.geo_model.solutions.geological_map[0]).astype(int)[0]
        nv = numpy_to_vtk(arr_[sel - 1], array_type=3)
        self._topography_delauny.GetOutput().GetPointData().SetScalars(nv)

    def set_surface_points(self, indices=None):
        """
        Create all the surface_points points and set them to the corresponding renders for their posterior visualization
         with render_model

        Returns:
            None
        """

        if not indices:

            for e, val in enumerate(self.geo_model._surface_points.df.iterrows()):
                index = val[0]
                row = val[1]
                self.s_rend_1.at[index] = (self.create_sphere(row['X'], row['Y'], row['Z'], row['id'],
                                           n_sphere=e, n_render=0, n_index=index))
                self.s_rend_2.at[index] = (self.create_sphere(row['X'], row['Y'], row['Z'], row['id'],
                                           n_sphere=e, n_render=1, n_index=index))
                self.s_rend_3.at[index] = (self.create_sphere(row['X'], row['Y'], row['Z'], row['id'],
                                           n_sphere=e, n_render=2, n_index=index))
                self.s_rend_4.at[index] = (self.create_sphere(row['X'], row['Y'], row['Z'], row['id'],
                                           n_sphere=e, n_render=3, n_index=index))
        else:
            for e, val in enumerate(self.geo_model._surface_points.df.loc[np.atleast_1d(indices)].iterrows()):
                index = val[0]
                row = val[1]
                self.s_rend_1.at[index] = (self.create_sphere(row['X'], row['Y'], row['Z'], row['id'],
                                           n_sphere=e, n_render=0, n_index=index))
                self.s_rend_2.at[index] = (self.create_sphere(row['X'], row['Y'], row['Z'], row['id'],
                                           n_sphere=e, n_render=1, n_index=index))
                self.s_rend_3.at[index] = (self.create_sphere(row['X'], row['Y'], row['Z'], row['id'],
                                           n_sphere=e, n_render=2, n_index=index))
                self.s_rend_4.at[index] = (self.create_sphere(row['X'], row['Y'], row['Z'], row['id'],
                                           n_sphere=e, n_render=3, n_index=index))

    def set_orientations(self, indices=None):
        """
        Create all the orientations and set them to the corresponding renders for their posterior visualization with
        render_model
        Returns:
            None
        """

        if not indices:
            for e, val in enumerate(self.geo_model._orientations.df.iterrows()):
                index = val[0]
                row = val[1]
                self.o_rend_1.at[index] = (self.create_foliation(row['X'], row['Y'], row['Z'], row['id'],
                                           row['G_x'], row['G_y'], row['G_z'],
                                           n_plane=e, n_render=0, n_index=index))
                self.o_rend_2.at[index] = (self.create_foliation(row['X'], row['Y'], row['Z'], row['id'],
                                           row['G_x'], row['G_y'], row['G_z'],
                                           n_plane=e, n_render=1, n_index=index))
                self.o_rend_3.at[index] = (self.create_foliation(row['X'], row['Y'], row['Z'], row['id'],
                                           row['G_x'], row['G_y'], row['G_z'],
                                           n_plane=e, n_render=2, n_index=index))
                self.o_rend_4.at[index] = (self.create_foliation(row['X'], row['Y'], row['Z'], row['id'],
                                           row['G_x'], row['G_y'], row['G_z'],
                                           n_plane=e, n_render=3, n_index=index))
        else:
            for e, val in enumerate(self.geo_model._orientations.df.loc[np.atleast_1d(indices)].iterrows()):
                index = val[0]
                row = val[1]
                self.o_rend_1.at[index] = (self.create_foliation(row['X'], row['Y'], row['Z'], row['id'],
                                           row['G_x'], row['G_y'], row['G_z'],
                                           n_plane=e, n_render=0, n_index=index))
                self.o_rend_2.at[index] = (self.create_foliation(row['X'], row['Y'], row['Z'], row['id'],
                                           row['G_x'], row['G_y'], row['G_z'],
                                           n_plane=e, n_render=1, n_index=index))
                self.o_rend_3.at[index] = (self.create_foliation(row['X'], row['Y'], row['Z'], row['id'],
                                           row['G_x'], row['G_y'], row['G_z'],
                                           n_plane=e, n_render=2, n_index=index))
                self.o_rend_4.at[index] = (self.create_foliation(row['X'], row['Y'], row['Z'], row['id'],
                                           row['G_x'], row['G_y'], row['G_z'],
                                           n_plane=e, n_render=3, n_index=index))

    def create_slider_rep(self, min=0, max=10, val=0):

        slider_rep = vtk.vtkSliderRepresentation2D()
        slider_rep.SetMinimumValue(min)
        slider_rep.SetMaximumValue(max)
        slider_rep.SetValue(val)
        slider_rep.GetPoint1Coordinate().SetCoordinateSystemToDisplay()
        slider_rep.GetPoint1Coordinate().SetValue(0, 40)
        slider_rep.GetPoint2Coordinate().SetCoordinateSystemToDisplay()
        slider_rep.GetPoint2Coordinate().SetValue(300, 40)

        self.slider_w = vtk.vtkSliderWidget()
        self.slider_w.SetInteractor(self.interactor)
        self.slider_w.SetRepresentation(slider_rep)
        self.slider_w.SetCurrentRenderer(self.ren_list[0])
        self.slider_w.SetAnimationModeToAnimate()
        self.slider_w.On()
        self.slider_w.AddObserver('EndInteractionEvent', self.sliderCallback_traces)

    def create_slider_interactor(self, min=0, max=1, val=1):

        slider_rep = vtk.vtkSliderRepresentation2D()
        slider_rep.SetMinimumValue(min)
        slider_rep.SetMaximumValue(max)
        slider_rep.SetValue(val)
        slider_rep.GetPoint1Coordinate().SetCoordinateSystemToDisplay()
        slider_rep.GetPoint1Coordinate().SetValue(0, 60)
        slider_rep.GetPoint2Coordinate().SetCoordinateSystemToDisplay()
        slider_rep.GetPoint2Coordinate().SetValue(100, 60)
        slider_rep.SetTitleText('Interactor')

        self.slider_w = vtk.vtkSliderWidget()
        self.slider_w.SetInteractor(self.interactor)
        self.slider_w.SetRepresentation(slider_rep)
        self.slider_w.SetCurrentRenderer(self.ren_list[0])
        self.slider_w.SetAnimationModeToJump()

        self.slider_w.On()
        self.slider_w.AddObserver('EndInteractionEvent', self.sliderCallback_interactor)

    def sliderCallback_interactor(self, obj, event):
        if int(obj.GetRepresentation().GetValue()) == 0:
            self.interactor.ExitCallback()

    def sliderCallback_traces(self, obj, event):
        # TODO Check post class
        self.post.change_input_data(self.geo_model, obj.GetRepresentation().GetValue())
        try:
            # for surf in self.surf_rend_1:
            #     self.ren_list[0].RemoveActor(surf)
            #     self.ren_list[1].RemoveActor(surf)
            #     self.ren_list[2].RemoveActor(surf)
            #     self.ren_list[3].RemoveActor(surf)
            self.update_surfaces_real_time()
          #  ver, sim =
          #  self.set_surfaces(ver, sim)
        except AttributeError:
            print('no surf')
            pass
        try:
            for sph in zip(self.s_rend_1['val'], self.s_rend_2['val'], self.s_rend_3['val'],
                           self.s_rend_4['val'], self.geo_model._surface_points.df.iterrows()):

                row_i = sph[4][1]
                sph[0].PlaceWidget(row_i['X'] - sph[0].r_f, row_i['X'] + sph[0].r_f,
                                   row_i['Y'] - sph[0].r_f, row_i['Y'] + sph[0].r_f,
                                   row_i['Z'] - sph[0].r_f, row_i['Z'] + sph[0].r_f)

                sph[1].PlaceWidget(row_i['X'] - sph[1].r_f, row_i['X'] + sph[1].r_f,
                                   row_i['Y'] - sph[1].r_f, row_i['Y'] + sph[1].r_f,
                                   row_i['Z'] - sph[1].r_f, row_i['Z'] + sph[1].r_f)

                sph[2].PlaceWidget(row_i['X'] - sph[2].r_f, row_i['X'] + sph[2].r_f,
                                   row_i['Y'] - sph[2].r_f, row_i['Y'] + sph[2].r_f,
                                   row_i['Z'] - sph[2].r_f, row_i['Z'] + sph[2].r_f)

                sph[3].PlaceWidget(row_i['X'] - sph[3].r_f, row_i['X'] + sph[3].r_f,
                                   row_i['Y'] - sph[3].r_f, row_i['Y'] + sph[3].r_f,
                                   row_i['Z'] - sph[3].r_f, row_i['Z'] + sph[3].r_f)
        except AttributeError:
            pass
        try:
            for fol in (zip(self.f_rend_1, self.f_rend_2, self.f_rend_3, self.f_rend_4, self.geo_model._orientations.iterrows())):
                row_f = fol[4][1]

                fol[0].SetCenter(row_f['X'], row_f['Y'], row_f['Z'])
                fol[0].SetNormal(row_f['G_x'], row_f['G_y'], row_f['G_z'])

        except AttributeError:
            pass

    def sphereCallback(self, obj, event):
        """
        Function that rules what happens when we move a sphere. At the moment we update the other 3 renderers and
        update the pandas data frame.
        """
        #self.interactor.ExitCallback()

       # self.Callback_camera_reset()

        # Get new position of the sphere
        new_center = obj.GetCenter()

        # Get which sphere we are moving
        index = obj.index

        # Check what render we are working with
        render = obj.n_render

        # This must be the radius
        #r_f = obj.r_f

        self.SphereCallback_change_df(index, new_center)
        self.SphereCallbak_move_changes(index)

        if self.real_time:
            try:
                self.update_surfaces_real_time()
                #vertices, simpleces =
                #self.set_surfaces(vertices, simpleces)
            except AssertionError:
                print('Not enough data to compute the model')

    def Callback_camera_reset(self,  obj, event):

        # Resetting the xy camera when a sphere is moving to be able to change only 2D
        fp = self.ren_list[1].GetActiveCamera().GetFocalPoint()
        p = self.ren_list[1].GetActiveCamera().GetPosition()
        dist = np.sqrt((p[0] - fp[0]) ** 2 + (p[1] - fp[1]) ** 2 + (p[2] - fp[2]) ** 2)
        self.ren_list[1].GetActiveCamera().SetPosition(fp[0], fp[1], fp[2] + dist)
        self.ren_list[1].GetActiveCamera().SetViewUp(0.0, 1.0, 0.0)

        # Resetting the yz camera when a sphere is moving to be able to change only 2D
        fp = self.ren_list[2].GetActiveCamera().GetFocalPoint()
        p = self.ren_list[2].GetActiveCamera().GetPosition()
        dist = np.sqrt((p[0] - fp[0]) ** 2 + (p[1] - fp[1]) ** 2 + (p[2] - fp[2]) ** 2)
        self.ren_list[2].GetActiveCamera().SetPosition(fp[0] + dist, fp[1], fp[2])
        self.ren_list[2].GetActiveCamera().SetViewUp(0.0, -1.0, 0.0)
        self.ren_list[2].GetActiveCamera().Roll(90)

        # Resetting the xz camera when a sphere is moving to be able to change only 2D
        fp = self.ren_list[3].GetActiveCamera().GetFocalPoint()
        p = self.ren_list[3].GetActiveCamera().GetPosition()
        dist = np.sqrt((p[0] - fp[0]) ** 2 + (p[1] - fp[1]) ** 2 + (p[2] - fp[2]) ** 2)
        self.ren_list[3].GetActiveCamera().SetPosition(fp[0], fp[1] - dist, fp[2])
        self.ren_list[3].GetActiveCamera().SetViewUp(-1.0, 0.0, 0.0)
        self.ren_list[3].GetActiveCamera().Roll(90)

    def SphereCallback_change_df(self, index, new_center):
        index = np.atleast_1d(index)
        # Modify Pandas DataFrame
        self.geo_model.modify_surface_points(index, X=[new_center[0]], Y=[new_center[1]], Z=[new_center[2]])

    def SphereCallbak_move_changes(self, indices):
       # print(indices)
        df_changes = self.geo_model._surface_points.df.loc[np.atleast_1d(indices)][['X', 'Y', 'Z', 'id']]
        for index, df_row in df_changes.iterrows():
            new_center = df_row[['X', 'Y', 'Z']].values

            # Update renderers
            s1 = self.s_rend_1.loc[index, 'val']

            s1.PlaceWidget(new_center[0] - s1.r_f, new_center[0] + s1.r_f,
                           new_center[1] - s1.r_f, new_center[1] + s1.r_f,
                           new_center[2] - s1.r_f, new_center[2] + s1.r_f)

            s1.GetSphereProperty().SetColor(mcolors.hex2color(
                self.geo_model._surfaces.df.set_index('id')['color'][df_row['id']]))#self.C_LOT[df_row['id']])

            s2 = self.s_rend_2.loc[index, 'val']
            s2.PlaceWidget(new_center[0] - s2.r_f, new_center[0] + s2.r_f,
                           new_center[1] - s2.r_f, new_center[1] + s2.r_f,
                           new_center[2] - s2.r_f, new_center[2] + s2.r_f)

            s2.GetSphereProperty().SetColor(mcolors.hex2color(
                self.geo_model._surfaces.df.set_index('id')['color'][df_row['id']]))

            s3 = self.s_rend_3.loc[index, 'val']
            s3.PlaceWidget(new_center[0] - s3.r_f, new_center[0] + s3.r_f,
                           new_center[1] - s3.r_f, new_center[1] + s3.r_f,
                           new_center[2] - s3.r_f, new_center[2] + s3.r_f)

            s3.GetSphereProperty().SetColor(mcolors.hex2color(
                self.geo_model._surfaces.df.set_index('id')['color'][df_row['id']]))

            s4 = self.s_rend_4.loc[index, 'val']
            s4.PlaceWidget(new_center[0] - s4.r_f, new_center[0] + s4.r_f,
                           new_center[1] - s4.r_f, new_center[1] + s4.r_f,
                           new_center[2] - s4.r_f, new_center[2] + s4.r_f)

            s4.GetSphereProperty().SetColor(mcolors.hex2color(
                self.geo_model._surfaces.df.set_index('id')['color'][df_row['id']]))

    def SphereCallback_delete_point(self, ind_i):
        """
        Warning this deletion system will lead to memory leaks until the vtk object is reseted (hopefully). This is
        mainly a vtk problem to delete objects
        """
        ind_i = np.atleast_1d(ind_i)
        # Deactivating widget
        for i in ind_i:
            self.s_rend_1.loc[i, 'val'].Off()
            self.s_rend_2.loc[i, 'val'].Off()
            self.s_rend_3.loc[i, 'val'].Off()
            self.s_rend_4.loc[i, 'val'].Off()

        self.s_rend_1.drop(ind_i)
        self.s_rend_2.drop(ind_i)
        self.s_rend_3.drop(ind_i)
        self.s_rend_4.drop(ind_i)

    def planesCallback(self, obj, event):
        """
        Function that rules what happens when we move a plane. At the moment we update the other 3 renderers and
        update the pandas data frame
        """

      # self.Callback_camera_reset()

        # Get new position of the plane and GxGyGz
        new_center = obj.GetCenter()
        new_normal = obj.GetNormal()
        # Get which plane we are moving
        index = obj.index

        self.planesCallback_change_df(index, new_center, new_normal)
        self.planesCallback_move_changes(index)

        if self.real_time:
            # try:
            #     if self.real_time:
            #         for surf in self.surf_rend_1:
            #             self.ren_list[0].RemoveActor(surf)
            #             self.ren_list[1].RemoveActor(surf)
            #             self.ren_list[2].RemoveActor(surf)
            #             self.ren_list[3].RemoveActor(surf)
            # except AttributeError:
            #     pass

            try:
                self.update_surfaces_real_time()
              #  vertices, simpleces =
              #  self.set_surfaces(vertices, simpleces)
            except AssertionError:
                print('Not enough data to compute the model')

    def planesCallback_change_df(self, index, new_center, new_normal):

        # Modify Pandas DataFrame
        # update the gradient vector components and its location
        self.geo_model.modify_orientations(index, X=new_center[0], Y=new_center[1], Z=new_center[2],
                                           G_x=new_normal[0], G_y=new_normal[1], G_z=new_normal[2])
        # update the dip and azimuth values according to the new gradient
        # self.geo_model.calculate_orientations()

    def planesCallback_move_changes(self, indices):
        df_changes = self.geo_model._orientations.df.loc[np.atleast_1d(indices)][['X', 'Y', 'Z', 'G_x', 'G_y', 'G_z', 'id']]
        for index, new_values_df in df_changes.iterrows():
            new_center = new_values_df[['X', 'Y', 'Z']].values
            new_normal = new_values_df[['G_x', 'G_y', 'G_z']].values
            new_source = vtk.vtkPlaneSource()
            new_source.SetCenter(new_center)
            new_source.SetNormal(new_normal)
            new_source.Update()

            plane1 = self.o_rend_1.loc[index, 'val']
          #  plane1.SetInputData(new_source.GetOutput())
            plane1.SetNormal(new_normal)
            plane1.SetCenter(new_center[0], new_center[1], new_center[2])
            plane1.GetPlaneProperty().SetColor(mcolors.hex2color(
                self.geo_model._surfaces.df.set_index('id')['color'][new_values_df['id']]))#self.C_LOT[new_values_df['id']])
            plane1.GetHandleProperty().SetColor(mcolors.hex2color(
                self.geo_model._surfaces.df.set_index('id')['color'][new_values_df['id']]))

            plane2 = self.o_rend_2.loc[index, 'val']
            plane2.SetInputData(new_source.GetOutput())
            plane2.SetNormal(new_normal)
            plane2.SetCenter(new_center[0], new_center[1], new_center[2])
            plane2.GetPlaneProperty().SetColor(mcolors.hex2color(
                self.geo_model._surfaces.df.set_index('id')['color'][new_values_df['id']]))
            plane2.GetHandleProperty().SetColor(mcolors.hex2color(
                self.geo_model._surfaces.df.set_index('id')['color'][new_values_df['id']]))

            plane3 = self.o_rend_3.loc[index, 'val']
            plane3.SetInputData(new_source.GetOutput())
            plane3.SetNormal(new_normal)
            plane3.SetCenter(new_center[0], new_center[1], new_center[2])
            plane3.GetPlaneProperty().SetColor(mcolors.hex2color(
                self.geo_model._surfaces.df.set_index('id')['color'][new_values_df['id']]))
            plane3.GetHandleProperty().SetColor(mcolors.hex2color(
                self.geo_model._surfaces.df.set_index('id')['color'][new_values_df['id']]))

            plane4 = self.o_rend_4.loc[index, 'val']
            plane4.SetInputData(new_source.GetOutput())
            plane4.SetNormal(new_normal)
            plane4.SetCenter(new_center[0], new_center[1], new_center[2])
            plane4.GetPlaneProperty().SetColor(mcolors.hex2color(
                self.geo_model._surfaces.df.set_index('id')['color'][new_values_df['id']]))
            plane4.GetHandleProperty().SetColor(mcolors.hex2color(
                self.geo_model._surfaces.df.set_index('id')['color'][new_values_df['id']]))

    def planesCallback_delete_point(self, ind_o):
        """
        Warning this deletion system will lead to memory leaks until the vtk object is reseted (hopefully). This is
        mainly a vtk problem to delete objects
        """
        ind_o = np.atleast_1d(ind_o)
        # Deactivating widget
        for i in ind_o:
            self.o_rend_1.loc[i, 'val'].Off()
            self.o_rend_2.loc[i, 'val'].Off()
            self.o_rend_3.loc[i, 'val'].Off()
            self.o_rend_4.loc[i, 'val'].Off()

        self.s_rend_1.drop(ind_o)
        self.s_rend_2.drop(ind_o)
        self.s_rend_3.drop(ind_o)
        self.s_rend_4.drop(ind_o)

    def create_ren_list(self):
        """
        Create a list of the 4 renderers we use. One general view and 3 cartersian projections
        Returns:
            list: list of renderers
        """

        # viewport dimensions setup
        xmins = [0, 0.6, 0.6, 0.6]
        xmaxs = [0.6, 1, 1, 1]
        ymins = [0, 0, 0.33, 0.66]
        ymaxs = [1, 0.33, 0.66, 1]

        # create list of renderers, set vieport values
        ren_list = []
        for i in range(self.n_ren):
            # append each renderer to list of renderers
            ren_list.append(vtk.vtkRenderer())
            # add each renderer to window
            self.renwin.AddRenderer(ren_list[-1])
            # set viewport for each renderer
            ren_list[-1].SetViewport(xmins[i], ymins[i], xmaxs[i], ymaxs[i])

        return ren_list

    def _create_cameras(self, extent, verbose=0):
        """
        Create the 4 cameras for each renderer
        """
        _e = extent
        _e_dx = _e[1] - _e[0]
        _e_dy = _e[3] - _e[2]
        _e_dz = _e[5] - _e[4]
        _e_d_avrg = (_e_dx + _e_dy + _e_dz) / 3
        _e_max = np.argmax(_e)

        # General camera
        model_cam = vtk.vtkCamera()
        model_cam.SetPosition(_e[_e_max] * 5, _e[_e_max] * 5, _e[_e_max] * 5)
        model_cam.SetFocalPoint(np.min(_e[0:2]) + _e_dx / 2,
                                np.min(_e[2:4]) + _e_dy / 2,
                                np.min(_e[4:]) + _e_dz / 2)

        model_cam.SetViewUp(-0.239, 0.155, 0.958)

        # XY camera RED
        xy_cam = vtk.vtkCamera()

        xy_cam.SetPosition(np.min(_e[0:2]) + _e_dx / 2,
                           np.min(_e[2:4]) + _e_dy / 2,
                           _e[_e_max] * 4)

        xy_cam.SetFocalPoint(np.min(_e[0:2]) + _e_dx / 2,
                             np.min(_e[2:4]) + _e_dy / 2,
                             np.min(_e[4:]) + _e_dz / 2)

        # YZ camera GREEN
        yz_cam = vtk.vtkCamera()
        yz_cam.SetPosition(_e[_e_max] * 4,
                           np.min(_e[2:4]) + _e_dy / 2,
                           np.min(_e[4:]) + _e_dz / 2)

        yz_cam.SetFocalPoint(np.min(_e[0:2]) + _e_dx / 2,
                             np.min(_e[2:4]) + _e_dy / 2,
                             np.min(_e[4:]) + _e_dz / 2)
        yz_cam.SetViewUp(0, -1, 0)
        yz_cam.Roll(90)

        # XZ camera BLUE
        xz_cam = vtk.vtkCamera()
        xz_cam.SetPosition(np.min(_e[0:2]) + _e_dx / 2,
                           - _e[_e_max] * 4,
                           np.min(_e[4:]) + _e_dz / 2)

        xz_cam.SetFocalPoint(np.min(_e[0:2]) + _e_dx / 2,
                             np.min(_e[2:4]) + _e_dy / 2,
                             np.min(_e[4:]) + _e_dz / 2)
        xz_cam.SetViewUp(0, 1, 0)
        xz_cam.Roll(0)

        # camera position debugging
        if verbose == 1:
            print("RED XY:", xy_cam.GetPosition())
            print("RED FP:", xy_cam.GetFocalPoint())
            print("GREEN YZ:", yz_cam.GetPosition())
            print("GREEN FP:", yz_cam.GetFocalPoint())
            print("BLUE XZ:", xz_cam.GetPosition())
            print("BLUE FP:", xz_cam.GetFocalPoint())

        return [model_cam, xy_cam, yz_cam, xz_cam]

    def set_camera_backcolor(self, color=None):
        """
        define background colors of the renderers
        """
        if color is None:
            color = (66 / 250, 66 / 250, 66 / 250)

        ren_color = [color for i in range(self.n_ren)]

        for i in range(self.n_ren):
            # set active camera for each renderer
            self.ren_list[i].SetActiveCamera(self.camera_list[i])
            # set background color for each renderer
            self.ren_list[i].SetBackground(ren_color[i][0], ren_color[i][1], ren_color[i][2])

    def _create_axes(self, camera, verbose=0, tick_vis=False):
        """
        Create the axes boxes
        """
        cube_axes_actor = vtk.vtkCubeAxesActor()
        cube_axes_actor.SetBounds(self.extent)
        cube_axes_actor.SetCamera(camera)
        if verbose == 1:
            print(cube_axes_actor.GetAxisOrigin())

        # set axes and label colors
        cube_axes_actor.GetTitleTextProperty(0).SetColor(1.0, 0.0, 0.0)
        cube_axes_actor.GetLabelTextProperty(0).SetColor(1.0, 0.0, 0.0)

        cube_axes_actor.GetTitleTextProperty(1).SetColor(0.0, 1.0, 0.0)
        cube_axes_actor.GetLabelTextProperty(1).SetColor(0.0, 1.0, 0.0)
        cube_axes_actor.GetTitleTextProperty(2).SetColor(0.0, 0.0, 1.0)
        cube_axes_actor.GetLabelTextProperty(2).SetColor(0.0, 0.0, 1.0)

        cube_axes_actor.DrawXGridlinesOn()
        cube_axes_actor.DrawYGridlinesOn()
        cube_axes_actor.DrawZGridlinesOn()

        if not tick_vis:
            cube_axes_actor.XAxisMinorTickVisibilityOff()
            cube_axes_actor.YAxisMinorTickVisibilityOff()
            cube_axes_actor.ZAxisMinorTickVisibilityOff()

        cube_axes_actor.SetXTitle("X")
        cube_axes_actor.SetYTitle("Y")
        cube_axes_actor.SetZTitle("Z")

        cube_axes_actor.SetXAxisLabelVisibility(1)
        cube_axes_actor.SetYAxisLabelVisibility(1)
        cube_axes_actor.SetZAxisLabelVisibility(1)

        # only plot grid lines furthest from viewpoint
        # ensure platform compatibility for the grid line options
        if sys.platform == "win32":
            cube_axes_actor.SetGridLineLocation(cube_axes_actor.VTK_GRID_LINES_FURTHEST)
        else:  # rather use elif == "linux" ? but what about other platforms
            try:  # apparently this can also go wrong on linux, maybe depends on vtk version?
                cube_axes_actor.SetGridLineLocation(cube_axes_actor.VTK_GRID_LINES_FURTHEST)
            except AttributeError:
                pass

        return cube_axes_actor

    def delete_surfaces(self):
        try:
            for surf in self.surf_rend_1:
                self.ren_list[0].RemoveActor(surf)
                self.ren_list[1].RemoveActor(surf)
                self.ren_list[2].RemoveActor(surf)
                self.ren_list[3].RemoveActor(surf)
        except AttributeError:
            pass

    def update_surfaces_real_time(self, delete=True):

        if delete is True:
            self.delete_surfaces()
        try:
            gp.compute_model(self.geo_model, sort_surfaces=False, compute_mesh=True)
        except IndexError:
            print('IndexError: Model not computed. Laking data in some surface')
        except AssertionError:
            print('AssertionError: Model not computed. Laking data in some surface')
        # try:
        #     v_l, s_l = self.geo_model.surfaces.df['vertices'], self.geo_model.surfaces.df['edges']
        # except IndexError:
        #     try:
        #         v_l, s_l = self.geo_model.surfaces.df['vertices'], self.geo_model.surfaces.df['edges']
        #     except IndexError:
        #         v_l, s_l = self.geo_model.surfaces.df['vertices'], self.geo_model.surfaces.df['edges']

        self.set_surfaces(self.geo_model._surfaces)
        # if self.geo_model.solutions.geological_map is not None:
        #     try:
        #         self.set_geological_map()
        #     except AttributeError:
        #         pass
        return True

    @staticmethod
    def export_vtk_lith_block(geo_data, lith_block=None, path="./default"):
        """
        Export data to a vtk file for posterior visualizations

        Args:
            geo_data(:class:''): All values of a DataManagement object
            block(numpy.array): 3D array containing the lithology block
            path (str): path to the location of the vtk

        Returns:
            None
        """



        # Dimensions

        nx, ny, nz = geo_data._grid.regular_grid.resolution

        lx = geo_data._grid.regular_grid.extent[1] - geo_data._grid.regular_grid.extent[0]
        ly = geo_data._grid.regular_grid.extent[3] - geo_data._grid.regular_grid.extent[2]
        lz = geo_data._grid.regular_grid.extent[5] - geo_data._grid.regular_grid.extent[4]

        dx, dy, dz = lx / nx, ly / ny, lz / nz

        ncells = nx * ny * nz

        npoints = (nx + 1) * (ny + 1) * (nz + 1)

        # Coordinates
        x = np.arange(geo_data._grid.regular_grid.extent[0], geo_data._grid.regular_grid.extent[1] + 0.1, dx, dtype='float64')

        y = np.arange(geo_data._grid.regular_grid.extent[2], geo_data._grid.regular_grid.extent[3] + 0.1, dy, dtype='float64')

        z = np.arange(geo_data._grid.regular_grid.extent[4], geo_data._grid.regular_grid.extent[5] + 0.1, dz, dtype='float64')

        if lith_block is None:
            lith_block = geo_data.solutions.lith_block
        lith = lith_block.reshape((nx, ny, nz)).flatten(order='F')
        out = pv.RectilinearGrid(x,y,z)
        out['Lithology'] = lith
        out.save(path+'_lith_block.vtr')

    @staticmethod
    def export_vtk_surfaces(geo_model, vertices=None, simplices=None, path=None, name='_surfaces', alpha=1):
        """
        Export data to a vtk file for posterior visualizations

        Args:
            geo_model(gempy.InputData): All values of a DataManagement object
            block(numpy.array): 3D array containing the lithology block
            path (str): path to the location of the vtk

        Returns:
            None
        """
        import vtk

        if vertices is None or simplices is None:
            vertices = geo_model.solutions.vertices
            simplices = geo_model.solutions.edges

        s_n = 0
        for e, values in enumerate(vertices):
            # setup points and vertices
            s_n += 1
            Points = vtk.vtkPoints()
            Triangles = vtk.vtkCellArray()
            Triangle = vtk.vtkTriangle()
            for p in values:
                Points.InsertNextPoint(p)

            # Unfortunately in this simple example the following lines are ambiguous.
            # The first 0 is the index of the triangle vertex which is ALWAYS 0-2.
            # The second 0 is the index into the point (geometry) array, so this can range from 0-(NumPoints-1)
            # i.e. a more general statement is triangle->GetPointIds()->SetId(0, PointId);
            for i in simplices[e]:
                Triangle.GetPointIds().SetId(0, i[0])
                Triangle.GetPointIds().SetId(1, i[1])
                Triangle.GetPointIds().SetId(2, i[2])

                Triangles.InsertNextCell(Triangle)

            polydata = vtk.vtkPolyData()
            polydata.SetPoints(Points)
            polydata.SetPolys(Triangles)

            polydata.Modified()
            if vtk.VTK_MAJOR_VERSION <= 5:
                polydata.Update()

            writer = vtk.vtkXMLPolyDataWriter();

            # Add colors
            surf_mapper = vtk.vtkPolyDataMapper()
            surf_mapper.SetInputData(polydata)
            surf_mapper.Update()

            surf_actor = vtk.vtkActor()
            surf_actor.SetMapper(surf_mapper)
            surf_actor.GetProperty().SetColor(mcolors.hex2color(geo_model._surfaces.df.set_index('id')['color'][s_n]))
            surf_actor.GetProperty().SetOpacity(alpha)

            if not path:
                path = "./default_"

            writer.SetFileName(path+'_surfaces_'+str(e)+'.vtp')
            if vtk.VTK_MAJOR_VERSION <= 5:
                writer.SetInput(polydata)
            else:
                writer.SetInputData(polydata)
            writer.Write()


class GemPyvtkInteract(vtkVisualization):

    def resume(self):
        self.interactor.Start()

    def restart(self, render_surfaces=True, **kwargs):
        self.set_surface_points()
        self.set_orientations()
        if render_surfaces is True:
            self.set_surfaces(self.geo_model._surfaces)

        self.render_model(**kwargs)

    def set_real_time_on(self):

        self.real_time = True

    def set_real_time_off(self):
        self.real_time = False

    def render_move_surface_points(self, indices):
        self.SphereCallbak_move_changes(indices)
       # print('vtk-gempy real time is:' + str(self.real_time))
        if self.real_time is True:
            self.update_surfaces_real_time()
        self.interactor.Render()

    def render_add_surface_points(self, indices):
        self.set_surface_points(indices)
       # print('vtk-gempy real time is:' + str(self.real_time))
        if self.real_time is True:
            self.update_surfaces_real_time()
        self.interactor.Render()

    def render_delete_surface_points(self, indices):
        self.SphereCallback_delete_point(indices)
      #  print('vtk-gempy real time is:' + str(self.real_time))
        if self.real_time is True:
            self.update_surfaces_real_time()
        self.interactor.Render()

    def render_move_orientations(self, indices):
        self.planesCallback_move_changes(indices)
      #  print('vtk-gempy real time is:' + str(self.real_time))
        if self.real_time is True:
            self.update_surfaces_real_time()
        self.interactor.Render()

    def render_add_orientations(self, indices):
        self.set_orientations(indices)
     #   print('vtk-gempy real time is:' + str(self.real_time))
        if self.real_time is True:
            self.update_surfaces_real_time()
        self.interactor.Render()

    def render_delete_orientations(self, indices):
        self.planesCallback_delete_point(indices)
     #   print('vtk-gempy real time is:' + str(self.real_time))
        if self.real_time is True:
            self.update_surfaces_real_time()
        self.interactor.Render()

    def render_surfaces(self, alpha=1):
        self.delete_surfaces()
        self.set_surfaces(self.geo_model._surfaces, alpha=alpha)
        self.interactor.Render()

    def render_topography(self):
        try:
            self.ren_list[0].RemoveActor(self.topography_surface)
            self.ren_list[1].RemoveActor(self.topography_surface)
            self.ren_list[2].RemoveActor(self.topography_surface)
            self.ren_list[3].RemoveActor(self.topography_surface)
        except AttributeError:
            pass

      #  print('vtk-gempy real time is:' +str(self.real_time))
        if self.real_time is True:
            self.update_surfaces_real_time()
        self.set_topography()
        self.interactor.Render()

    def update_model(self):
        if self.real_time is True:
            self.update_surfaces_real_time()
            self.interactor.Render()


class steno3D():
    def __init__(self, geo_data, project, **kwargs ):
        if STENO_IMPORT is False:
            raise ImportError( 'Steno 3D package is not installed. No 3D online visualization available.')
        description = kwargs.get('description', 'Nothing')

        self._data = geo_data
        self.surfaces = pn.DataFrame.from_dict(geo_data.get_surface_number(), orient='index')


        steno3d.login()

        self.proj = steno3d.Project(
            title=project,
            description=description,
            public=True,
        )

    def plot3D_steno_grid(self, block, plot=False, **kwargs):


        mesh = steno3d.Mesh3DGrid(h1=np.ones(self._data.resolution[0]) * (self._data.extent[1] - self._data.extent[0]) /
                                                                         (self._data.resolution[0] - 1),
                                  h2=np.ones(self._data.resolution[1]) * (self._data.extent[3] - self._data.extent[2]) /
                                                                         (self._data.resolution[1] - 1),
                                  h3=np.ones(self._data.resolution[2]) * (self._data.extent[5] - self._data.extent[4]) /
                                                                         (self._data.resolution[2] - 1),
                                  O=[self._data.extent[0], self._data.extent[2], self._data.extent[4]])

        data = steno3d.DataArray(
            title='Lithologies_block',
            array=block)

        vol = steno3d.Volume(project=self.proj, mesh=mesh, data=[dict(location='CC', data=data)])
       # vol.upload()

        if plot:
            return vol.plot()

    def plot3D_steno_surface(self, ver, sim):

        for surface in self.surfaces.iterrows():
            if surface[1].values[0] == 0:
                pass

            #for vertices, simpleces in zip(ver[surface[1].values[0]], sim[surface[1].values[0]]):
            surface_mesh = steno3d.Mesh2D(
                vertices=ver[surface[1].values[0]-1],
                triangles=sim[surface[1].values[0]-1],
                opts=dict(wireframe=False)
            )
            surface_obj = steno3d.Surface(
                project=self.proj,
                title='Surface: {}'.format(surface[0]),
                mesh=surface_mesh,
                opts=dict(opacity=1)
            )


def get_fault_ellipse_params(fault_points:np.ndarray):
    """Get the fault ellipse parameters a and b from griven fault points (should
    be the rotated ones.

    Args:
        fault_points (np.ndarray): Fault points

    Returns:
        (tuple) main axis scalars of fault ellipse a,b
    """
    a = (fault_points[:, 0].max() - fault_points[:, 0].min()) / 2
    b = (fault_points[:, 1].max() - fault_points[:, 1].min()) / 2
    return a, b


def get_fault_rotation_objects(geo_model, fault:str):
    """Gets fault rotation objects: rotation matrix U, the rotated fault points,
    rotated centroid, and the ellipse parameters a and b.

    Args:
        geo_model (gempy.core.model.Project): gempy geo_model object
        fault (str): Name of the fault surface.

    Returns:
        U (np.ndarray): Rotation matrix.
        rfpts (np.ndarray): Rotated fault points.
        rctr (np.array): Centroid of the rotated fault points.
        a (float): Horizontal ellipse parameter.
        b (float): Vertical ellipse parameter.
    """
    filter_ = geo_model._surface_points.df.surface == fault
    fpts = geo_model._surface_points.df[filter_][["X", "Y", "Z"]].values.T
    ctr = np.mean(fpts, axis=1)
    x = fpts - ctr.reshape((-1, 1))
    M = np.dot(x, x.T)
    U = np.linalg.svd(M)
    rfpts = np.dot(fpts.T, U[0])
    # rfpts = np.dot(rfpts, U[-1])
    rctr = np.mean(rfpts, axis=0)

    a, b = get_fault_ellipse_params(rfpts)
    return U, rfpts, rctr, a, b


def cut_finite_fault_surfaces(geo_model, ver:dict, sim:dict):
    """Cut vertices and simplices for finite fault surfaces to finite fault ellipse

    Args:
        geo_model (gempy.core.model.Project): gempy geo_model object
        ver (dict): Dictionary with surfaces as keys and vertices ndarray as values.
        sim (dict): Dictionary with surfaces as keys and simplices ndarray as values.

    Returns:
        ver, sim (dict, dict): Updated vertices and simplices with finite fault
            surfaces cut to ellipses.
    """
    from scipy.spatial import Delaunay
    from copy import copy

    finite_ver = copy(ver)
    finite_sim = copy(sim)

    finite_fault_series = list(geo_model._faults.df[geo_model._faults.df["isFinite"] == True].index)
    finite_fault_surfaces = list(
        geo_model._surfaces.df[geo_model._surfaces.df._stack == finite_fault_series].surface.unique())

    for fault in finite_fault_surfaces:
        U, fpoints_rot, fctr_rot, a, b = get_fault_rotation_objects(geo_model, "Fault 1")
        rpoints = np.dot(ver[fault], U[0])
        # rpoints = np.dot(rpoints, U[-1])
        r = (rpoints[:, 0] - fctr_rot[0]) ** 2 / a ** 2 + (rpoints[:, 1] - fctr_rot[1]) ** 2 / b ** 2

        finite_ver[fault] = finite_ver[fault][r < 1]
        delaunay = Delaunay(finite_ver[fault])
        finite_sim[fault] = delaunay.simplices
        # finite_sim[fault] = finite_sim[fault][np.isin(sim[fault], np.argwhere(r<0.33))]

    return finite_ver, finite_sim
