try:
    from . import generic as g
except BaseException:
    import generic as g


def test_edges():
    """
    Test edges_to_polygon
    """
    m = g.get_mesh("featuretype.STL")

    # get a polygon for the second largest facet
    index = m.facets_area.argsort()[-2]
    normal = m.facets_normal[index]
    origin = m._cache["facets_origin"][index]
    T = g.trimesh.geometry.plane_transform(origin, normal)
    vertices = g.trimesh.transform_points(m.vertices, T)[:, :2]

    # find boundary edges for the facet
    edges = m.edges_sorted.reshape((-1, 6))[m.facets[index]].reshape((-1, 2))
    group = g.trimesh.grouping.group_rows(edges, require_count=1)

    # run the polygon conversion
    polygon = g.trimesh.path.polygons.edges_to_polygons(
        edges=edges[group], vertices=vertices
    )

    assert len(polygon) == 1
    assert g.np.isclose(polygon[0].area, m.facets_area[index])

    # try transforming the polygon around
    M = g.np.eye(3)
    M[0][2] = 10.0
    P2 = g.trimesh.path.polygons.transform_polygon(polygon[0], M)
    distance = (
        g.np.array(P2.centroid.coords)[0] - g.np.array(polygon[0].centroid.coords)[0]
    )
    assert g.np.allclose(distance, [10.0, 0])


def test_random_polygon():
    """
    Test creation of random polygons
    """
    p = g.trimesh.path.polygons.random_polygon()
    assert p.area > 0.0
    assert p.is_valid


def test_polygon_sample():
    """
    Test random sampling of polygons
    """

    p = g.Point([0, 0]).buffer(1.0)
    count = 100

    s = g.trimesh.path.polygons.sample(p, count=count)
    assert len(s) <= count
    assert s.shape[1] == 2

    radius = (s**2).sum(axis=1).max()
    assert radius < (1.0 + 1e-8)

    # test Path2D sample wiring
    path = g.trimesh.load_path(p)
    s = path.sample(count=count)
    assert len(s) <= count
    assert s.shape[1] == 2
    radius = (s**2).sum(axis=1).max()
    assert radius < (1.0 + 1e-8)

    # try getting OBB of samples
    _T, extents = g.trimesh.path.polygons.polygon_obb(s)
    # OBB of samples should be less than diameter of circle
    diameter = g.np.ptp(g.np.reshape(p.bounds, (2, 2)), axis=0).max()
    assert (extents <= diameter).all()

    # test sampling with multiple bodies
    for i in range(3):
        assert g.np.isclose(path.area, p.area * (i + 1))
        path = path + g.trimesh.load_path(g.Point([(i + 2) * 2, 0]).buffer(1.0))
        s = path.sample(count=count)
        assert s.shape[1] == 2

    # try a polygon with a low area/aabb-area ratio to
    # check the iteration loop.
    p = g.Polygon([[0, 0], [1, 1], [0.5 - 1e-3, 0.5], [0, 0]])
    s = g.trimesh.path.polygons.sample(polygon=p, count=100)


def test_project():
    m = g.trimesh.creation.icosphere(subdivisions=4)

    p = [g.trimesh.path.polygons.projected(m, normal=n) for n in g.random((100, 3))]

    # sphere projection should never have interiors
    assert all(len(i.interiors) == 0 for i in p)
    # sphere projected area should always be close to pi
    assert g.np.allclose([i.area for i in p], g.np.pi, atol=0.05)


def test_project_backface():
    m = g.trimesh.Trimesh(vertices=[[0, 0, 0], [0, 1, 0], [1, 0, 0]], faces=[[0, 1, 2]])

    # check ignore_sign argument
    front = m.projected(m.face_normals[0], ignore_sign=False)
    assert len(front.entities) == 1
    front = m.projected(m.face_normals[0], ignore_sign=True)
    assert len(front.entities) == 1

    back = m.projected(-m.face_normals[0], ignore_sign=False)
    assert len(back.entities) == 0
    back = m.projected(-m.face_normals[0], ignore_sign=True)
    assert len(back.entities) == 1


def test_project_multi():
    for ignore_sign, precise in g.itertools.combinations([True, False] * 2, 2):
        mesh = g.trimesh.creation.box() + g.trimesh.creation.box().apply_translation(
            [3, 0, 0]
        )
        proj = mesh.projected(normal=[0, 0, 1], ignore_sign=ignore_sign, precise=precise)

        assert mesh.body_count == 2
        assert len(proj.root) == 2
        assert g.np.isclose(proj.area, 2.0)


def test_second_moment():
    def rectangle(extents):
        # return the boundary of an origin-centered
        # rectangle as a numpy array
        a = g.np.abs(g.np.array(extents) / 2.0)
        lower, upper = -a, a
        return g.np.array([lower, [upper[0], lower[1]], upper, [lower[0], upper[1]]])

    def poly(bh, bhi=None):
        # return a rectangle centered at the origin
        # as a shapely Polygon
        shell = rectangle(bh)
        if bhi is not None:
            holes = [rectangle(bhi)]
        else:
            holes = []
        return Polygon(shell=shell, holes=holes)

    def poly_corner(bh):
        # return a rectangle with one corner
        # at the origin and the rest in positive space
        shell = rectangle(bh)
        shell += shell.min(axis=0)
        return Polygon(shell=shell)

    def poly_doublecorner(bh):
        # returns two equal sized rectangles as one polygon
        # Same as poly_corner(), but the rectangle gets
        # mirrored by 180 deg at the origin
        # This puts the centroid in the origin with Ixy != 0
        shell_1 = rectangle(bh)
        shell_1 += shell_1.min(axis=0)
        shell_2 = -shell_1
        shell = g.np.concatenate(
            (shell_1[2:, :], shell_1[:2, :], shell_2[2:, :], shell_2[:2, :]), axis=0
        )
        return Polygon(shell=shell)

    def truth(bh, bhi=None):
        # return the analytical second moment of area
        # for a rectangle with centroid at the origin
        # and width-height of `bh` and an interior
        # rectangle width-height of `bhi`
        if bhi is None:
            bhi = g.np.zeros(2)
        b, h = bh
        bi, hi = bhi
        return (
            g.np.array(
                [b * h**3 - bi * hi**3, h * b**3 - hi * bi**3, 0.0],
                dtype=g.np.float64,
            )
            / 12
        )

    def truth_corner(bh):
        # check a rectangle with one corner
        # at the origin and the rest in positive space
        b, h = bh
        return g.np.array(
            [b * h**3 / 3.0, h * b**3 / 3.0, 0.5 * b**2 * 0.5 * h**2],
            dtype=g.np.float64,
        )

    from shapely.geometry import Polygon

    from trimesh.path.polygons import second_moments, transform_polygon

    heights = g.np.array([[0.01, 0.01], [1, 1], [10, 2], [3, 21]])
    for bh in heights:
        # check the second moment of a rectangle
        # as polygon is already centered, centered doesn't have any effect
        O_moments, O_principal_moments, O_alpha, O_transform = second_moments(
            poly(bh), return_centered=True
        )
        # check against wikipedia
        t = truth(bh)
        # for a centered rectangle, the principal axis are already aligned
        # with the frame axis
        assert g.np.allclose(O_moments, t)
        assert g.np.any(g.np.isclose(O_moments, O_principal_moments[0]))
        assert g.np.isclose(O_moments[2], 0)  # Ixy = 0
        assert g.np.isclose(O_alpha, 0)
        assert g.np.allclose(O_transform, g.np.eye(3))

        # now check a rectangle with the corner, so Ixy != 0

        # First we test with centering. The results should be same as
        # with the initially centered rectangles
        C_moments, C_principal_moments, C_alpha, C_transform = second_moments(
            poly_corner(bh), return_centered=True
        )
        assert g.np.allclose(O_moments, C_moments)
        assert g.np.allclose(O_principal_moments, C_principal_moments)
        assert g.np.isclose(O_alpha, C_alpha)
        assert g.np.allclose(O_transform[:, :2], C_transform[:, :2])

        # Now without centering
        moments = second_moments(poly_corner(bh), return_centered=False)
        t = truth_corner(bh)
        assert g.np.allclose(moments, t)

        # Now we will get the transform for a double rectangle. Then we will apply
        # the transform and test if Ixy == 0, alpha == 0 etc.
        C_moments, C_principal_moments, C_alpha, C_transform = second_moments(
            poly_doublecorner(bh), return_centered=True
        )
        # apply the outputted transform to the polygon
        T_polygon = transform_polygon(poly_doublecorner(bh), C_transform)
        # call the function on the transformed polygon
        T_moments, T_principal_moments, T_alpha, T_transform = second_moments(
            T_polygon, return_centered=True
        )
        assert g.np.any(g.np.isclose(T_moments, C_principal_moments[0]))
        assert g.np.allclose(C_principal_moments, T_principal_moments)
        assert g.np.isclose(T_alpha, 0, atol=1e-7)
        assert g.np.allclose(T_transform, g.np.eye(3))

        # check polygons with interior
        for bhi in heights:
            # only check if interior is smaller than exterior
            if not (bhi < bh).all():
                continue

            # check a rectangle with interiors
            c = second_moments(poly(bh, bhi), return_centered=False)
            t = truth(bh, bhi)
            assert g.np.allclose(c, t)


def test_native_centroid():
    # check our native implementation of the shoelace algorithm
    # against the results from shapely which we use for native
    # checks of counter-clockwise 2D coordinate loops

    # get some polygons without interiors
    polygons = [g.Polygon(i) for i in g.data["nestable"]]
    # same data as (n, 2) float arrays
    coords = [g.np.array(i) for i in g.data["nestable"]]

    for p, c in zip(polygons, coords):
        # area will be signed with respect to counter-clockwise
        _ccw, area, centroid = g.trimesh.util.is_ccw(c, return_all=True)
        assert g.np.allclose(centroid, g.np.array(p.centroid.coords)[0])
        assert g.np.isclose(abs(area), p.area)


if __name__ == "__main__":
    g.trimesh.util.attach_to_log()
    test_polygon_sample()
