import os
import numpy as np
import openpnm as op
import pandas as pd


def project_to_paraview(project, filename):  # pragma: no cover
    r"""
    Exports an OpenPNM network to a paraview state file.

    Parameters
    ----------
    network : Network
        The network containing the desired data.
    filename : str
        Path to saved .vtp file.

    Notes
    -----
    Outputs a pvsm file that can be opened in Paraview. The pvsm file will
    be saved with the same name as .vtp file

    """
    try:
        import paraview.simple
    except ModuleNotFoundError:
        msg = (
            "The paraview python bindings must be installed using "
            "conda install -c conda-forge paraview, however this may require"
            " using a virtualenv since conflicts with other packages are common."
            " This is why it is not explicitly included as a dependency in"
            " OpenPNM."
        )
        raise ModuleNotFoundError(msg)
    paraview.simple._DisableFirstRenderCameraReset()
    file = os.path.splitext(filename)[0]
    network = project.network
    x, y, z = np.ptp(network.coords, axis=0)
    if sum(op.topotools.dimensionality(network)) == 2:
        zshape = 0
        xshape = y
        yshape = x
    elif sum(op.topotools.dimensionality(network)) == 3:
        zshape = x
        xshape = z
        yshape = y
    maxshape = max(xshape, yshape, zshape)
    shape = np.array([xshape, yshape, zshape])
    # Create a new 'XML PolyData Reader'
    Path = os.getcwd() + "\\" + file + '.vtp'
    net_vtp = paraview.simple.XMLPolyDataReader(FileName=[Path])
    p = op.io.Dict.to_dict(project=project, element=['pore'],
                           flatten=False,
                           categorize_by=['data' 'object'])
    # The folloing lines are bit complex but avoid using the flatdict lib
    p = pd.json_normalize(p, sep='.').to_dict(orient='records')[0]
    for k in list(p.keys()):
        p[k.replace('.', ' | ')] = p.pop(k)
    t = op.io.Dict.to_dict(project=project, element=['throat'],
                           flatten=False,
                           categorize_by=['data' 'object'])
    t = pd.json_normalize(t, sep='.').to_dict(orient='records')[0]
    for k in list(t.keys()):
        t[k.replace('.', '/')] = t.pop(k)
    # Now write the p and t dictionaries
    net_vtp.CellArrayStatus = t.keys()
    net_vtp.PointArrayStatus = p.keys()
    # Get active view
    render_view = paraview.simple.GetActiveViewOrCreate('RenderView')
    # Uncomment following to set a specific view size
    # render_view.ViewSize = [1524, 527]
    # Get layout
    _ = paraview.simple.GetLayout()
    # Show data in view
    net_vtp_display = paraview.simple.Show(
        net_vtp, render_view, 'GeometryRepresentation'
    )
    # Trace defaults for the display properties.
    net_vtp_display.Representation = 'Surface'
    net_vtp_display.ColorArrayName = [None, '']
    net_vtp_display.OSPRayScaleArray = [
        f'network | {network.name} | labels | pore.all']
    net_vtp_display.OSPRayScaleFunction = 'PiecewiseFunction'
    net_vtp_display.SelectOrientationVectors = 'None'
    net_vtp_display.ScaleFactor = (maxshape-1)/10
    net_vtp_display.SelectScaleArray = 'None'
    net_vtp_display.GlyphType = 'Arrow'
    net_vtp_display.GlyphTableIndexArray = 'None'
    net_vtp_display.GaussianRadius = (maxshape-1)/200
    net_vtp_display.SetScaleArray = \
        ['POINTS', 'network | ' + network.name + ' | labels | pore.all']
    net_vtp_display.ScaleTransferFunction = 'PiecewiseFunction'
    net_vtp_display.OpacityArray = \
        ['POINTS', 'network | ' + network.name + ' | labels | pore.all']
    net_vtp_display.OpacityTransferFunction = 'PiecewiseFunction'
    net_vtp_display.DataAxesGrid = 'GridAxesRepresentation'
    net_vtp_display.PolarAxes = 'PolarAxesRepresentation'
    # Init the 'PiecewiseFunction' selected for 'ScaleTransferFunction'
    net_vtp_display.ScaleTransferFunction.Points = [1, 0, 0.5, 0, 1, 1, 0.5, 0]
    # Init the 'PiecewiseFunction' selected for 'OpacityTransferFunction'
    net_vtp_display.OpacityTransferFunction.Points = [1, 0, 0.5, 0, 1, 1, 0.5, 0]
    # Reset view to fit data
    render_view.ResetCamera()
    # Get the material library
    _ = paraview.simple.GetMaterialLibrary()
    # Update the view to ensure updated data information
    render_view.Update()
    # Create a new 'Glyph'
    glyph = paraview.simple.Glyph(Input=net_vtp, GlyphType='Arrow')
    glyph.OrientationArray = ['POINTS', 'No orientation array']
    glyph.ScaleArray = ['POINTS', 'No scale array']
    glyph.ScaleFactor = 1
    glyph.GlyphTransform = 'Transform2'
    # Properties modified on glyph
    glyph.GlyphType = 'Sphere'
    glyph.ScaleArray = \
        ['POINTS', 'network | ' + network.name + ' | properties | pore.diameter']
    # Show data in view
    glyph_display = paraview.simple.Show(
        glyph, render_view, 'GeometryRepresentation')
    # Trace defaults for the display properties.
    glyph_display.Representation = 'Surface'
    glyph_display.ColorArrayName = [None, '']
    glyph_display.OSPRayScaleArray = 'Normals'
    glyph_display.OSPRayScaleFunction = 'PiecewiseFunction'
    glyph_display.SelectOrientationVectors = 'None'
    glyph_display.ScaleFactor = (maxshape - 1) / 10
    glyph_display.SelectScaleArray = 'None'
    glyph_display.GlyphType = 'Arrow'
    glyph_display.GlyphTableIndexArray = 'None'
    glyph_display.GaussianRadius = (maxshape - 1) / 200
    glyph_display.SetScaleArray = ['POINTS', 'Normals']
    glyph_display.ScaleTransferFunction = 'PiecewiseFunction'
    glyph_display.OpacityArray = ['POINTS', 'Normals']
    glyph_display.OpacityTransferFunction = 'PiecewiseFunction'
    glyph_display.DataAxesGrid = 'GridAxesRepresentation'
    glyph_display.PolarAxes = 'PolarAxesRepresentation'
    # Init the 'PiecewiseFunction' selected for 'ScaleTransferFunction'
    glyph_display.ScaleTransferFunction.Points = [-0.97, 0, 0.5, 0, 0.97, 1, 0.5, 0]
    # Init the 'PiecewiseFunction' selected for 'OpacityTransferFunction'
    glyph_display.OpacityTransferFunction.Points = [-0.97, 0, 0.5, 0, 0.97, 1, 0.5, 0]
    # Update the view to ensure updated data information
    render_view.Update()
    # Set active source
    paraview.simple.SetActiveSource(net_vtp)
    # Create a new 'Shrink'
    shrink1 = paraview.simple.Shrink(Input=net_vtp)
    # Properties modified on shrink1
    shrink1.ShrinkFactor = 1.0
    # Show data in view
    shrink_display = paraview.simple.Show(
        shrink1, render_view, 'UnstructuredGridRepresentation')
    # Trace defaults for the display properties.
    shrink_display.Representation = 'Surface'
    shrink_display.ColorArrayName = [None, '']
    shrink_display.OSPRayScaleArray = \
        ['network | ' + network.name + ' | labels | pore.all']
    shrink_display.OSPRayScaleFunction = 'PiecewiseFunction'
    shrink_display.SelectOrientationVectors = 'None'
    shrink_display.ScaleFactor = (maxshape-1)/10
    shrink_display.SelectScaleArray = 'None'
    shrink_display.GlyphType = 'Arrow'
    shrink_display.GlyphTableIndexArray = 'None'
    shrink_display.GaussianRadius = (maxshape-1)/200
    shrink_display.SetScaleArray = \
        ['POINTS', 'network | ' + network.name + ' | labels | pore.all']
    shrink_display.ScaleTransferFunction = 'PiecewiseFunction'
    shrink_display.OpacityArray = \
        ['POINTS', 'network | ' + network.name + ' | labels | pore.all']
    shrink_display.OpacityTransferFunction = 'PiecewiseFunction'
    shrink_display.DataAxesGrid = 'GridAxesRepresentation'
    shrink_display.PolarAxes = 'PolarAxesRepresentation'
    shrink_display.ScalarOpacityUnitDistance = 1.0349360947089783
    # Init the 'PiecewiseFunction' selected for 'ScaleTransferFunction'
    shrink_display.ScaleTransferFunction.Points = [1, 0, 0.5, 0, 1, 1, 0.5, 0]
    # Init the 'PiecewiseFunction' selected for 'OpacityTransferFunction'
    shrink_display.OpacityTransferFunction.Points = [1, 0, 0.5, 0, 1, 1, 0.5, 0]
    # Hide data in view
    paraview.simple.Hide(net_vtp, render_view)
    # Update the view to ensure updated data information
    render_view.Update()
    # Create a new 'Cell Data to Point Data'
    cellDatatoPointData1 = paraview.simple.CellDatatoPointData(Input=shrink1)
    cellDatatoPointData1.CellDataArraytoprocess = t
    # Show data in view
    cell_data_to_point_data_display = paraview.simple.Show(
        cellDatatoPointData1, render_view, 'UnstructuredGridRepresentation'
    )
    # Trace defaults for the display properties.
    cell_data_to_point_data_display.Representation = 'Surface'
    cell_data_to_point_data_display.ColorArrayName = [None, '']
    cell_data_to_point_data_display.OSPRayScaleArray = \
        ['network | ' + network.name + ' | labels | pore.all']
    cell_data_to_point_data_display.OSPRayScaleFunction = 'PiecewiseFunction'
    cell_data_to_point_data_display.SelectOrientationVectors = 'None'
    cell_data_to_point_data_display.ScaleFactor = (maxshape-1)/10
    cell_data_to_point_data_display.SelectScaleArray = 'None'
    cell_data_to_point_data_display.GlyphType = 'Arrow'
    cell_data_to_point_data_display.GlyphTableIndexArray = 'None'
    cell_data_to_point_data_display.GaussianRadius = (maxshape-1)/200
    cell_data_to_point_data_display.SetScaleArray = \
        ['POINTS', 'network | ' + network.name + ' | labels | pore.all']
    cell_data_to_point_data_display.ScaleTransferFunction = 'PiecewiseFunction'
    cell_data_to_point_data_display.OpacityArray = \
        ['POINTS', 'network | ' + network.name + ' | labels | pore.all']
    cell_data_to_point_data_display.OpacityTransferFunction = 'PiecewiseFunction'
    cell_data_to_point_data_display.DataAxesGrid = 'GridAxesRepresentation'
    cell_data_to_point_data_display.PolarAxes = 'PolarAxesRepresentation'
    cell_data_to_point_data_display.ScalarOpacityUnitDistance = 1.0349360947089783
    # Init the 'PiecewiseFunction' selected for 'ScaleTransferFunction'
    cell_data_to_point_data_display.ScaleTransferFunction.Points = [
        1, 0, 0.5, 0, 1, 1, 0.5, 0]
    # Init the 'PiecewiseFunction' selected for 'OpacityTransferFunction'
    cell_data_to_point_data_display.OpacityTransferFunction.Points = [
        1, 0, 0.5, 0, 1, 1, 0.5, 0]
    # Hide data in view
    paraview.simple.Hide(shrink1, render_view)
    # Update the view to ensure updated data information
    render_view.Update()
    # Set active source
    paraview.simple.SetActiveSource(shrink1)
    # Set active source
    paraview.simple.SetActiveSource(cellDatatoPointData1)
    # Set active source
    paraview.simple.SetActiveSource(shrink1)
    # Create a new 'Extract Surface'
    extractSurface1 = paraview.simple.ExtractSurface(Input=shrink1)
    # Show data in view
    extract_surface_display = paraview.simple.Show(
        extractSurface1, render_view, 'GeometryRepresentation')
    # Trace defaults for the display properties.
    extract_surface_display.Representation = 'Surface'
    extract_surface_display.ColorArrayName = [None, '']
    extract_surface_display.OSPRayScaleArray = \
        ['network | ' + network.name + ' | labels | pore.all']
    extract_surface_display.OSPRayScaleFunction = 'PiecewiseFunction'
    extract_surface_display.SelectOrientationVectors = 'None'
    extract_surface_display.ScaleFactor = (maxshape-1)/10
    extract_surface_display.SelectScaleArray = 'None'
    extract_surface_display.GlyphType = 'Arrow'
    extract_surface_display.GlyphTableIndexArray = 'None'
    extract_surface_display.GaussianRadius = (maxshape-1)/200
    extract_surface_display.SetScaleArray = \
        ['POINTS', 'network | ' + network.name + ' | labels | pore.all']
    extract_surface_display.ScaleTransferFunction = 'PiecewiseFunction'
    extract_surface_display.OpacityArray = \
        ['POINTS', 'network | ' + network.name + ' | labels | pore.all']
    extract_surface_display.OpacityTransferFunction = 'PiecewiseFunction'
    extract_surface_display.DataAxesGrid = 'GridAxesRepresentation'
    extract_surface_display.PolarAxes = 'PolarAxesRepresentation'
    # Init the 'PiecewiseFunction' selected for 'ScaleTransferFunction'
    extract_surface_display.ScaleTransferFunction.Points = [
        1, 0, 0.5, 0, 1, 1, 0.5, 0]
    # Init the 'PiecewiseFunction' selected for 'OpacityTransferFunction'
    extract_surface_display.OpacityTransferFunction.Points = [
        1, 0, 0.5, 0, 1, 1, 0.5, 0]
    # Hide data in view
    paraview.simple.Hide(shrink1, render_view)
    # Update the view to ensure updated data information
    render_view.Update()
    # create a new 'Tube'
    tube = paraview.simple.Tube(Input=extractSurface1)
    tube.Scalars = ['POINTS', 'network |' + network.name + '| labels | pore.all']
    tube.Vectors = [None, '1']
    tube.Radius = 0.04
    # Set active source
    paraview.simple.SetActiveSource(extractSurface1)
    # Destroy tube
    paraview.simple.Delete(tube)
    del tube
    # Set active source
    paraview.simple.SetActiveSource(shrink1)
    # Set active source
    paraview.simple.SetActiveSource(cellDatatoPointData1)
    # Set active source
    paraview.simple.SetActiveSource(extractSurface1)
    # Create a new 'Tube'
    tube = paraview.simple.Tube(Input=extractSurface1)
    tube.Scalars = [
        'POINTS', 'network |' + network.name + ' | labels | pore.all']
    tube.Vectors = [None, '1']
    tube.Radius = 0.04
    # Properties modified on tube
    tube.Vectors = ['POINTS', '1']
    # Show data in view
    tube_display = paraview.simple.Show(tube, render_view,
                                        'GeometryRepresentation')
    # Trace defaults for the display properties.
    tube_display.Representation = 'Surface'
    tube_display.ColorArrayName = [None, '']
    tube_display.OSPRayScaleArray = 'TubeNormals'
    tube_display.OSPRayScaleFunction = 'PiecewiseFunction'
    tube_display.SelectOrientationVectors = 'None'
    tube_display.ScaleFactor = (maxshape)/10
    tube_display.SelectScaleArray = 'None'
    tube_display.GlyphType = 'Arrow'
    tube_display.GlyphTableIndexArray = 'None'
    tube_display.GaussianRadius = (maxshape)/200
    tube_display.SetScaleArray = ['POINTS', 'TubeNormals']
    tube_display.ScaleTransferFunction = 'PiecewiseFunction'
    tube_display.OpacityArray = ['POINTS', 'TubeNormals']
    tube_display.OpacityTransferFunction = 'PiecewiseFunction'
    tube_display.DataAxesGrid = 'GridAxesRepresentation'
    tube_display.PolarAxes = 'PolarAxesRepresentation'
    # Init the 'PiecewiseFunction' selected for 'ScaleTransferFunction'
    tube_display.ScaleTransferFunction.Points = [-1, 0, 0.5, 0, 1, 1, 0.5, 0]
    # Init the 'PiecewiseFunction' selected for 'OpacityTransferFunction'
    tube_display.OpacityTransferFunction.Points = [-1, 0, 0.5, 0, 1, 1, 0.5, 0]
    # Hide data in view
    paraview.simple.Hide(extractSurface1, render_view)
    # Update the view to ensure updated data information
    render_view.Update()
    # Saving camera placements for all active views
    # Current camera placement for render_view
    render_view.CameraPosition = [
        (xshape + 1) / 2, (yshape + 1) / 2, 4.3 * np.sqrt(np.sum(shape / 2)**2)
    ]
    render_view.CameraFocalPoint = [(xi+1) / 2 for xi in shape]
    render_view.CameraParallelScale = np.sqrt(np.sum(shape / 2)**2)
    paraview.simple.SaveState(f"{file}.pvsm")
