"""
convex.py

Deal with creating and checking convex objects in 2, 3 and N dimensions.

Convex is defined as:
1) "Convex, meaning "curving out" or "extending outward" (compare to concave)
2) having an outline or surface curved like the exterior of a circle or sphere.
3) (of a polygon) having only interior angles measuring less than 180
"""

import numpy as np

from .constants import tol

from . import util
from . import triangles


try:
    from scipy import spatial
except ImportError as E:
    from .exceptions import ExceptionModule
    spatial = ExceptionModule(E)


def convex_hull(obj, qhull_options='QbB Pp Qt'):
    """
    Get a new Trimesh object representing the convex hull of the
    current mesh attempting to return a watertight mesh with correct
    normals.

    Details on qhull options:
      http://www.qhull.org/html/qh-quick.htm#options

    Arguments
    --------
    obj : Trimesh, or (n,3) float
      Mesh or cartesian points
    qhull_options : str
      Options to pass to qhull.

    Returns
    --------
    convex : Trimesh
      Mesh of convex hull
    """
    from .base import Trimesh

    if isinstance(obj, Trimesh):
        points = obj.vertices.view(np.ndarray)
    else:
        # will remove subclassing
        points = np.asarray(obj, dtype=np.float64)
        if not util.is_shape(points, (-1, 3)):
            raise ValueError('Object must be Trimesh or (n,3) points!')

    hull = spatial.ConvexHull(points,
                              qhull_options=qhull_options)

    # hull object doesn't remove unreferenced vertices
    # create a mask to re- index faces for only referenced vertices
    vid = np.sort(hull.vertices)
    mask = np.zeros(len(hull.points), dtype=np.int64)
    mask[vid] = np.arange(len(vid))
    # remove unreferenced vertices here
    faces = mask[hull.simplices].copy()

    # rescale vertices back to original size
    vertices = hull.points[vid].copy()

    # qhull returns faces with random winding
    # calculate the returned normal of each face
    crosses = triangles.cross(vertices[faces])

    # qhull returns zero magnitude faces like an asshole
    normals, valid = util.unitize(crosses, check_valid=True)

    # remove zero magnitude faces
    faces = faces[valid]
    crosses = crosses[valid]

    # each triangle area and mean center
    triangles_area = triangles.area(crosses=crosses, sum=False)
    triangles_center = vertices[faces].mean(axis=1)

    # since the convex hull is (hopefully) convex, the vector from
    # the centroid to the center of each face
    # should have a positive dot product with the normal of that face
    # if it doesn't it is probably backwards
    # note that this sometimes gets screwed up by precision issues
    centroid = np.average(triangles_center,
                          weights=triangles_area,
                          axis=0)
    # a vector from the centroid to a point on each face
    test_vector = triangles_center - centroid
    # check the projection against face normals
    backwards = util.diagonal_dot(normals,
                                  test_vector) < 0.0

    # flip the winding outward facing
    faces[backwards] = np.fliplr(faces[backwards])
    # flip the normal
    normals[backwards] *= -1.0

    # save the work we did to the cache so it doesn't have to be recomputed
    initial_cache = {'triangles_cross': crosses,
                     'triangles_center': triangles_center,
                     'area_faces': triangles_area,
                     'centroid': centroid}

    # create the Trimesh object for the convex hull
    convex = Trimesh(vertices=vertices,
                     faces=faces,
                     face_normals=normals,
                     initial_cache=initial_cache,
                     process=True,
                     validate=False)

    # we did the gross case above, but sometimes precision issues
    # leave some faces backwards anyway
    # this call will exit early if the winding is consistent
    # and if not will fix it by traversing the adjacency graph
    convex.fix_normals(multibody=False)

    # sometimes the QbB option will cause precision issues
    # so try the hull again without it and
    # check for qhull_options is None to avoid infinite recursion
    if (qhull_options is not None and
            not convex.is_winding_consistent):
        return convex_hull(convex, qhull_options=None)

    return convex


def adjacency_projections(mesh):
    """
    Test if a mesh is convex by projecting the vertices of
    a triangle onto the normal of its adjacent face.

    Parameters
    ----------
    mesh : Trimesh
      Input geometry

    Returns
    ----------
    projection : (len(mesh.face_adjacency),) float
      Distance of projection of adjacent vertex onto plane
    """
    # normals and origins from the first column of face adjacency
    normals = mesh.face_normals[mesh.face_adjacency[:, 0]]
    # one of the vertices on the shared edge
    origins = mesh.vertices[mesh.face_adjacency_edges[:, 0]]

    # faces from the second column of face adjacency
    vid_other = mesh.face_adjacency_unshared[:, 1]
    vector_other = mesh.vertices[vid_other] - origins

    # get the projection with a dot product
    dots = util.diagonal_dot(vector_other, normals)

    return dots


def is_convex(mesh):
    """
    Check if a mesh is convex.

    Parameters
    -----------
    mesh : Trimesh
      Input geometry

    Returns
    -----------
    convex : bool
      Was passed mesh convex or not
    """
    # non-watertight meshes are not convex
    if not mesh.is_watertight:
        return False

    # don't consider zero- area faces
    nonzero = mesh.area_faces > tol.merge
    # adjacencies with two nonzero faces
    adj_ok = nonzero[mesh.face_adjacency].all(axis=1)
    # make threshold of convexity scale- relative
    threshold = tol.planar * mesh.scale
    # if projections of vertex onto plane of adjacent
    # face is negative, it means the face pair is locally
    # convex, and if that is true for all faces the mesh is convex
    convex = bool(mesh.face_adjacency_projections[adj_ok].max() < threshold)

    return convex


def hull_points(obj, qhull_options='QbB Pp'):
    """
    Try to extract a convex set of points from multiple input formats.

    Parameters
    ---------
    obj: Trimesh object
         (n,d) points
         (m,) Trimesh objects

    Returns
    --------
    points: (o,d) convex set of points
    """
    if hasattr(obj, 'convex_hull'):
        return obj.convex_hull.vertices

    initial = np.asanyarray(obj, dtype=np.float64)
    if len(initial.shape) != 2:
        raise ValueError('points must be (n, dimension)!')

    hull = spatial.ConvexHull(initial, qhull_options=qhull_options)
    points = hull.points[hull.vertices]

    return points
