"""Functions to translate ladybug geometry objects into VTK polydata objects."""
import vtk
import math
from typing import List, Union
from ladybug_geometry.geometry2d import Point2D, LineSegment2D, Polyline2D, \
    Polygon2D, Mesh2D
from ladybug_geometry.geometry3d import Point3D, Polyline3D, Arc3D, LineSegment3D, \
    Mesh3D, Polyface3D, Cone, Cylinder, Sphere, Face3D, Plane, Vector3D
from .polydata import PolyData
[docs]
def from_point2d(point: Point2D) -> PolyData:
    """Create Polydata from a Ladybug Point2D object.
    Args:
        point: A ladybug Point object.
    Returns:
        Polydata containing a single point.
    """
    vtk_point = vtk.vtkPoints()
    vtk_vertice = vtk.vtkCellArray()
    vtk_point.InsertNextPoint(point.x, point.y, 0)
    vtk_vertice.InsertNextCell(1, [1])
    polydata = PolyData()
    polydata.SetPoints(vtk_point)
    polydata.SetVerts(vtk_vertice)
    polydata.Modified()
    return polydata 
def _polyline_from_points(points: List[Point2D]) -> PolyData:
    """Create Polydata from a list of Ladybug Point2D objects.
    Args:
        points: A list of Ladybug Point2D objects.
    Returns:
        Polydata containing a polyline created by joining the points.
    """
    pts = vtk.vtkPoints()
    for point in points:
        pts.InsertNextPoint(point.x, point.y, 0)
    polyline = vtk.vtkPolyLine()
    polyline.GetPointIds().SetNumberOfIds(len(points))
    for i in range(len(points)):
        polyline.GetPointIds().SetId(i, i)
    cells = vtk.vtkCellArray()
    cells.InsertNextCell(polyline)
    polydata = PolyData()
    polydata.SetPoints(pts)
    polydata.SetLines(cells)
    return polydata
[docs]
def from_points2d(points: List[Point2D], join: bool = False) -> PolyData:
    """Create Polydata from a list of Ladybug Point2D objects.
    Args:
        points: A list of Ladybug Point2D objects.
        join: Boolean to indicate whether the points should be joined into a polyline.
    Returns:
        Polydata containing all points or a polyline.
    """
    if join:
        return _polyline_from_points(points)
    vtk_points = vtk.vtkPoints()
    vtk_vertices = vtk.vtkCellArray()
    for point in points:
        vtk_points.InsertNextPoint(point.x, point.y, 0)
    vtk_vertices.InsertNextCell(len(points), list(range(len(points))))
    polydata = PolyData()
    polydata.SetPoints(vtk_points)
    polydata.SetVerts(vtk_vertices)
    polydata.Modified()
    return polydata 
[docs]
def from_line2d(line: LineSegment2D) -> PolyData:
    """Create Polydata from a Ladybug LineSegment2D object.
    Args:
        line: A Ladybug LineSegment2D object.
    Returns:
        Polydata containing a line.
    """
    return from_points2d(line.vertices, join=True) 
[docs]
def from_polyline2d(polyline: Polyline2D) -> PolyData:
    """Create Polydata from a Ladybug Polyline2D object.
    Args:
        polyline: A Ladybug Polyline2D object.
    Returns:
        Polydata containing a polyline.
    """
    return from_points2d(polyline.vertices, join=True) 
[docs]
def from_polygon2d(polygon: Polygon2D) -> PolyData:
    """Create Polydata from a Ladybug Polygon2D object.
    Args:
        polygon: A Ladybug Polygon2D object.
    Returns:
        Polydata containing a polygon.
    """
    verts = polygon.vertices + (polygon[0],)
    return from_points2d(verts, join=True) 
[docs]
def from_point3d(point: Point3D) -> PolyData:
    """Create Polydata from a Ladybug Point3D object.
    Args:
        point: A ladybug Point object.
    Returns:
        Polydata containing a single point.
    """
    vtk_point = vtk.vtkPoints()
    vtk_vertice = vtk.vtkCellArray()
    vtk_point.InsertNextPoint(tuple(point))
    vtk_vertice.InsertNextCell(1, [1])
    polydata = PolyData()
    polydata.SetPoints(vtk_point)
    polydata.SetVerts(vtk_vertice)
    polydata.Modified()
    return polydata 
def _polyline_from_points3d(points: List[Point3D]) -> PolyData:
    """Create Polydata from a list of Ladybug Point3D objects.
    Args:
        points: A list of Ladybug Point3D objects.
    Returns:
        Polydata containing a polyline created by joining the points.
    """
    pts = vtk.vtkPoints()
    for pt in points:
        pts.InsertNextPoint(tuple(pt))
    polyline = vtk.vtkPolyLine()
    polyline.GetPointIds().SetNumberOfIds(len(points))
    for i in range(len(points)):
        polyline.GetPointIds().SetId(i, i)
    cells = vtk.vtkCellArray()
    cells.InsertNextCell(polyline)
    polydata = PolyData()
    polydata.SetPoints(pts)
    polydata.SetLines(cells)
    return polydata
[docs]
def from_points3d(points: List[Point3D], join: bool = False) -> PolyData:
    """Create Polydata from a list of Ladybug Point3D objects.
    Args:
        points: A list of Ladybug Point3D objects.
        join: Boolean to indicate whether the points should be joined into a polyline.
    Returns:
        Polydata containing all points or a polyline.
    """
    if join:
        return _polyline_from_points3d(points)
    vtk_points = vtk.vtkPoints()
    vtk_vertices = vtk.vtkCellArray()
    for point in points:
        vtk_points.InsertNextPoint(tuple(point))
    vtk_vertices.InsertNextCell(len(points), list(range(len(points))))
    polydata = PolyData()
    polydata.SetPoints(vtk_points)
    polydata.SetVerts(vtk_vertices)
    polydata.Modified()
    return polydata 
[docs]
def from_line3d(line: LineSegment3D) -> PolyData:
    """Create Polydata from a Ladybug LineSegment3D object.
    Args:
        line: A Ladybug LineSegment3D object.
    Returns:
        Polydata containing a line.
    """
    return from_points3d(line.vertices, join=True) 
[docs]
def from_polyline3d(polyline: Polyline3D) -> PolyData:
    """Create Polydata from a Ladybug Polyline3D object.
    Args:
        polyline: A Ladybug Polyline3D object.
    Returns:
        Polydata containing a polyline.
    """
    return from_points3d(polyline.vertices, join=True) 
[docs]
def from_arc3d(arc3d: Arc3D, resolution: int = 3) -> PolyData:
    """Create Polydata from a Ladybug Arc3D object.
    Args:
        arc3d: A Ladybug Arc3D object.
        resolution: The number of degrees per subdivision. The default is 3 that creates
            120 segments for an full circle.
    Returns:
        Polydata containing an arc.
    """
    arc = vtk.vtkArcSource()
    arc.UseNormalAndAngleOn()
    arc.SetCenter(arc3d.c.x, arc3d.c.y, arc3d.c.z)
    polar_vector = LineSegment3D.from_end_points(arc3d.c, arc3d.p1).v
    arc.SetPolarVector(round(polar_vector.x, 2), round(
        polar_vector.y, 2), round(polar_vector.z, 2))
    normal = arc3d.plane.n
    arc.SetNormal(round(normal.x, 2), round(normal.y, 2), round(normal.z, 2))
    arc.SetAngle(math.degrees(arc3d.angle))
    vtk_resolution = max(int(math.degrees(arc3d.angle) / resolution), 2)
    arc.SetResolution(vtk_resolution)
    arc.Update()
    polydata = PolyData()
    polydata.ShallowCopy(arc.GetOutput())
    # delete the array named 'Texture Coordinates'
    # that's generated automatically for some reason
    polydata.GetPointData().RemoveArray('Texture Coordinates')
    return polydata 
[docs]
def from_mesh3d(mesh: Mesh3D) -> PolyData:
    """Create Polydata from a Ladybug mesh.
    Args:
        mesh: A Ladybug Mesh3D object.
    Returns:
        Polydata containing face and points of a mesh.
    """
    points = vtk.vtkPoints()
    polygon = vtk.vtkPolygon()
    cells = vtk.vtkCellArray()
    for ver in mesh.vertices:
        points.InsertNextPoint(*ver)
    for face in mesh.faces:
        polygon.GetPointIds().SetNumberOfIds(len(face))
        for count, i in enumerate(face):
            polygon.GetPointIds().SetId(count, i)
        cells.InsertNextCell(polygon)
    polydata = PolyData()
    polydata.SetPoints(points)
    polydata.SetPolys(cells)
    return polydata 
[docs]
def from_mesh2d(mesh: Mesh2D) -> PolyData:
    """Create Polydata from a Ladybug mesh 2D.
    Args:
        mesh: A Ladybug Mesh2D object.
    Returns:
        Polydata containing face and points of a mesh.
    """
    points = vtk.vtkPoints()
    polygon = vtk.vtkPolygon()
    cells = vtk.vtkCellArray()
    for ver in mesh.vertices:
        points.InsertNextPoint(ver[0], ver[1], 0)
    for face in mesh.faces:
        polygon.GetPointIds().SetNumberOfIds(len(face))
        for count, i in enumerate(face):
            polygon.GetPointIds().SetId(count, i)
        cells.InsertNextCell(polygon)
    polydata = PolyData()
    polydata.SetPoints(points)
    polydata.SetPolys(cells)
    return polydata 
[docs]
def from_face3d(face: Face3D) -> PolyData:
    """Create Polydata from a Ladybug face.
    Args:
        face: A Ladybug Face3D object.
    Returns:
        Polydata containing face and points of a face.
    """
    if face.has_holes or not face.is_convex:
        return from_mesh3d(face.triangulated_mesh3d)
    points = vtk.vtkPoints()
    polygon = vtk.vtkPolygon()
    cells = vtk.vtkCellArray()
    vertices_count = len(face.vertices)
    polygon.GetPointIds().SetNumberOfIds(vertices_count)
    for ver in face.vertices:
        points.InsertNextPoint(*ver)
    for count in range(vertices_count):
        polygon.GetPointIds().SetId(count, count)
    cells.InsertNextCell(polygon)
    polydata = PolyData()
    polydata.SetPoints(points)
    polydata.SetPolys(cells)
    return polydata 
[docs]
def from_polyface3d(polyface: Polyface3D) -> PolyData:
    """Create Polydata from a Ladybug Polyface.
    Args:
        polyface: A Ladybug Polyface3D object.
    Returns:
        A Polydata representing the PolyFace3D.
    """
    points = vtk.vtkPoints()
    polygon = vtk.vtkPolygon()
    cells = vtk.vtkCellArray()
    for ver in polyface.vertices:
        points.InsertNextPoint(*ver)
    # starting number to count for the new vertices that are being added for
    # triangulated meshes
    addition = len(polyface.vertices)
    for face, face_geo in zip(polyface.face_indices, polyface.faces):
        if face_geo.has_holes or not face_geo.is_convex:
            meshed_face = face_geo.triangulated_mesh3d
            for ver in meshed_face.vertices:
                points.InsertNextPoint(*ver)
            for face in meshed_face.faces:
                polygon.GetPointIds().SetNumberOfIds(len(face))
                for count, i in enumerate(face):
                    polygon.GetPointIds().SetId(count, i + addition)
                cells.InsertNextCell(polygon)
            addition += len(meshed_face.vertices)
        else:
            face = face[0]
            polygon.GetPointIds().SetNumberOfIds(len(face))
            for count, i in enumerate(face):
                polygon.GetPointIds().SetId(count, i)
            cells.InsertNextCell(polygon)
    polydata = PolyData()
    polydata.SetPoints(points)
    polydata.SetPolys(cells)
    return polydata 
[docs]
def from_cone(cone: Cone, resolution: int = 2, cap: bool = True) -> PolyData:
    """Create Polydata from a Ladybug Cone.
    Args:
        cone: A Ladybug Cone object.
        resolution: The number of segments into which the cone will be divided.
            If set to 0, a line will be created.
            If set to 1, a single triangle will be created.
            If set to 2, two crossed triangles will be created.
            If set to greater than 2, a 3D cone with the number of sides equal to
            the resolution will be created. Defaults to 2.
        cap: Boolean to indicate whether the cone should capped or not. Default to True.
    Returns:
        Polydata containing a cone.
    """
    cone_source = vtk.vtkConeSource()
    cone_source.SetResolution(resolution)
    cone_source.SetRadius(cone.radius)
    cone_source.SetHeight(cone.height)
    cone_source.SetDirection(tuple(cone.axis))
    center = cone.vertex.move(cone.axis.reverse())
    cone_source.SetCenter(tuple(center))
    if not cap:
        cone_source.CappingOff()
    cone_source.Update()
    polydata = PolyData()
    polydata.ShallowCopy(cone_source.GetOutput())
    return polydata 
[docs]
def from_sphere(sphere: Sphere, resolution: int = 50) -> PolyData:
    """Create Polydata from a Ladybug Sphere.
    Args:
        sphere: A Ladybug Sphere object.
        resolution: The number of segments into which the sphere will be divided.
            Defaults to 50.
    Returns:
        Polydata containing a sphere.
    """
    sphere_source = vtk.vtkSphereSource()
    sphere_source.SetCenter(tuple(sphere.center))
    sphere_source.SetRadius(sphere.radius)
    sphere_source.SetPhiResolution(resolution)
    sphere_source.SetThetaResolution(resolution)
    sphere_source.Update()
    polydata = PolyData()
    polydata.ShallowCopy(sphere_source.GetOutput())
    return polydata 
[docs]
def from_cylinder(
    cylinder: Cylinder, resolution: int = 50, cap: bool = True
) -> PolyData:
    """Create Polydata from a Ladybug Cylinder.
    Args:
        cylinder: A Ladybug Cylinder object.
        resolution: The number of segments into which the cylinder will be divided.
            Defaults to 50.
        cap: Boolean to indicate whether the cylinder should capped or not.
            Default to True.
    Returns:
        Polydata containing a cylinder.
    """
    cylinder_source = vtk.vtkCylinderSource()
    cylinder_source.SetCenter(tuple(cylinder.center))
    cylinder_source.SetRadius(cylinder.radius)
    cylinder_source.SetHeight(cylinder.height)
    cylinder_source.SetResolution(resolution)
    if not cap:
        cylinder_source.CappingOff()
    cylinder_source.Update()
    polydata = PolyData()
    polydata.ShallowCopy(cylinder_source.GetOutput())
    return polydata 
[docs]
def from_text(
    text: str, *, plane: Union[Point3D, Point2D, Plane], height: float = 2,
    horizontal_alignment: int = 0, vertical_alignment: int = 2
) -> PolyData:
    """Create a VTK text object from a text string and a ladybug Point3D.
    Args:
        text: A text string.
        plane: A ladybug Plane, Point3D or Point2D object to locate and orient the text
            in the VTK scene.
        height: A number for the height of the text in the scene. Defaults is set to 2.
        horizontal_alignment: An optional integer to specify the horizontal alignment
            of the text. Choose from: (0 = Left, 1 = Center, 2 = Right).
        vertical_alignment: An optional integer to specify the vertical alignment of
            the text. Choose from: (0 = Top, 1 = Middle, 2 = Bottom)
    Returns:
        A Polydata object containing the text.
    """
    def _apply_transformation(
        source: vtk.vtkVectorText,
        plane: Plane,
        height: float,
        offset_multiline: bool = False
    ) -> vtk.vtkTransformPolyDataFilter:
        transform = vtk.vtkTransform()
        transform.PostMultiply()
        # VTK generates the texts initially on a plane at the origin
        original_plane = Plane(
            n=Vector3D(0, 0, 1), o=Point3D(0, 0, 0), x=Vector3D(1, 0, 0)
        )
        rotated_plane = original_plane
        if plane.n != original_plane.n:
            # match the normal of the two plane
            angle_rad = original_plane.n.angle(plane.n)
            angle = math.degrees(angle_rad)
            if angle > 0.1:
                vector = original_plane.n.cross(plane.n)
                if vector == Vector3D(0, 0, 0):
                    vector = Vector3D(1, 0, 0)
                transform.RotateWXYZ(angle, vector.x, vector.y, vector.z)
                rotated_plane = original_plane.rotate(vector, angle_rad, original_plane.o)
        # now match the x axis
        angle_rad = rotated_plane.x.angle(plane.x)
        angle = math.degrees(angle_rad)
        if angle > 0.1:
            vector = rotated_plane.n
            # this is an edge case that I couldn't really figure out why is happening
            # we should try to find a more generic solution. Use the simple_box.vsf
            # sample file for testing
            if rotated_plane.x.angle(Vector3D(0, 0, 1)) < 0.01 and \
                    
plane.x.angle(Vector3D(0, -1, 0)) < 0.01 and \
                    
vector.angle(Vector3D(-1, 0, 0)) < 0.01:
                vector = Vector3D(1, 0, 0)
            transform.RotateWXYZ(angle, vector.x, vector.y, vector.z)
            rotated_plane = rotated_plane.rotate(vector, angle_rad, original_plane.o)
        transform.Scale(height, height, height)
        # add a transformation to move the text to the new plane origin
        almost_horizontal = math.degrees(rotated_plane.x.angle(Vector3D(1, 0, 0))) < 5
        if offset_multiline:
            if almost_horizontal and vertical_alignment == 0:
                # horizontal text with top adjustment. No need to move
                line_count = 0
            else:
                line_count = len(text.split('\n')) - 1
            move_vector = rotated_plane.y * (line_count * 1.5 * height)
            move_vector = move_vector.reverse()
            moved_point = plane.o.move(move_vector)
            transform.Translate(*moved_point)
        else:
            transform.Translate(*plane.o)
        transformFilter = vtk.vtkTransformPolyDataFilter()
        transformFilter.SetInputConnection(source.GetOutputPort())
        transformFilter.SetTransform(transform)
        transformFilter.Update()
        tf = transformFilter.GetOutput()
        return tf
    if isinstance(plane, Point3D):
        plane = Plane(o=plane)
    elif isinstance(plane, Point2D):
        plane = Plane(o=Point3D(plane.x, plane.y, 0))
    assert isinstance(plane, Plane), 'The plane for text must be from a Ladybug Plane.'
    source = vtk.vtkVectorText()
    source.SetText(text)
    # initial transform to project the text from origin to the target plane
    transform_filter = _apply_transformation(source, plane, height)
    # this vector is the initial difference between the left lower corner og the
    # generated text and the desired text insertion point. In theory, it should be 0
    # but VTK doesn't generate the text at the exact 0, 0, 0 - the difference is usually
    # very small
    bounds = list(transform_filter.GetBounds())
    left_lower_corner = Point3D(bounds[0], bounds[2], bounds[4])
    offset_vector = plane.o - left_lower_corner
    # make adjustments for text justification
    if not (horizontal_alignment == 0 and vertical_alignment == 2):
        # find the size of the text
        bounds = list(transform_filter.GetBounds())
        bottom_left = Point3D(bounds[0], bounds[2], bounds[4])
        top_right = Point3D(bounds[1], bounds[3], bounds[5])
        bottom_left_2d = plane.xyz_to_xy(bottom_left)
        top_right_2d = plane.xyz_to_xy(top_right)
        x_dist = top_right_2d.x - bottom_left_2d.x
        y_dist = top_right_2d.y - bottom_left_2d.y
        if horizontal_alignment == 0:  # left alignment
            x_dist = 0
        elif horizontal_alignment == 1:  # center alignment
            x_dist = x_dist / 2
        if vertical_alignment == 2:  # bottom alignment
            y_dist = 0
        elif vertical_alignment == 1:  # center alignment
            y_dist = y_dist / 2
        x_vector = plane.x * -1 * x_dist
        y_vector = plane.y * -1 * y_dist
        move_vector = x_vector + y_vector
        plane = plane.move(moving_vec=move_vector)
    plane = plane.move(offset_vector)
    transform_filter = _apply_transformation(
        source, plane, height, offset_multiline=True
    )
    polydata = PolyData()
    polydata.ShallowCopy(transform_filter)
    return polydata 
[docs]
def to_circle(center: Point3D, radius: int = 100, sides: int = 100) -> PolyData:
    """Create a VTK circle from a ladybug Point3D and radius.
    Args:
        center: A ladybug Point3D object.
        radius: The radius of the circle. Defaults to 100 meters.
        sides: The number of sides of the circle. Defaults to 100.
    Returns:
        A Polydata object containing a circle.
    """
    polygonSource = vtk.vtkRegularPolygonSource()
    polygonSource.GeneratePolygonOff()
    polygonSource.SetNumberOfSides(sides)
    polygonSource.SetRadius(radius)
    polygonSource.SetCenter(center.x, center.y, center.z)
    polygonSource.Update()
    polydata = PolyData()
    polydata.ShallowCopy(polygonSource.GetOutput())
    return polydata