import numpy as np

from .. import transformations, util
from ..geometry import plane_transform
from . import arc
from .entities import Arc, Line


def circle_pattern(
    pattern_radius, circle_radius, count, center=None, angle=None, **kwargs
):
    """
    Create a Path2D representing a circle pattern.

    Parameters
    ------------
    pattern_radius : float
      Radius of circle centers
    circle_radius : float
      The radius of each circle
    count : int
      Number of circles in the pattern
    center : (2,) float
      Center of pattern
    angle :  float
      If defined pattern will span this angle
      If None, pattern will be evenly spaced

    Returns
    -------------
    pattern : trimesh.path.Path2D
      Path containing circular pattern
    """
    from .path import Path2D

    if angle is None:
        angles = np.linspace(0.0, np.pi * 2.0, count + 1)[:-1]
    elif isinstance(angle, float) or isinstance(angle, int):
        angles = np.linspace(0.0, angle, count)
    else:
        raise ValueError("angle must be float or int!")

    if center is None:
        center = [0.0, 0.0]

    # centers of circles
    centers = np.column_stack((np.cos(angles), np.sin(angles))) * pattern_radius

    vert = []
    ents = []
    for circle_center in centers:
        # (3,3) center points of arc
        three = arc.to_threepoint(
            angles=[0, np.pi], center=circle_center, radius=circle_radius
        )
        # add a single circle entity
        ents.append(Arc(points=np.arange(3) + len(vert), closed=True))
        # keep flat array by extend instead of append
        vert.extend(three)

    # translate vertices to pattern center
    vert = np.array(vert) + center
    pattern = Path2D(entities=ents, vertices=vert, **kwargs)
    return pattern


def circle(radius, center=None, **kwargs):
    """
    Create a Path2D containing circle with the specified
    radius.

    Parameters
    --------------
    radius : float
      The radius of the circle
    center : None or (2,) float
      Center of the circle, origin by default
    ** kwargs : dict
      Passed to trimesh.path.Path2D constructor

    Returns
    -------------
    circle : Path2D
      Path containing specified circle
    """
    from .path import Path2D

    if center is None:
        center = [0.0, 0.0]
    else:
        center = np.asanyarray(center, dtype=np.float64)
    # make sure radius is a float
    radius = float(radius)

    # (3, 2) float, points on arc
    three = arc.to_threepoint(angles=[0, np.pi], center=center, radius=radius)
    # generate the path object
    result = Path2D(
        entities=[Arc(points=np.arange(3), closed=True)], vertices=three, **kwargs
    )

    return result


def rectangle(bounds, **kwargs):
    """
    Create a Path2D containing a single or multiple rectangles
    with the specified bounds.

    Parameters
    --------------
    bounds : (2, 2) float, or (m, 2, 2) float
      Minimum XY, Maximum XY

    Returns
    -------------
    rect : Path2D
      Path containing specified rectangles
    """
    from .path import Path2D

    # data should be float
    bounds = np.asanyarray(bounds, dtype=np.float64)

    # bounds are extents, re- shape to origin- centered rectangle
    if bounds.shape == (2,):
        half = np.abs(bounds) / 2.0
        bounds = np.array([-half, half])

    # should have one bounds or multiple bounds
    if not (util.is_shape(bounds, (2, 2)) or util.is_shape(bounds, (-1, 2, 2))):
        raise ValueError("bounds must be (m, 2, 2) or (2, 2)")

    # hold Line objects
    lines = []
    # hold (n, 2) cartesian points
    vertices = []

    # loop through each rectangle
    for lower, upper in bounds.reshape((-1, 2, 2)):
        lines.append(Line((np.arange(5) % 4) + len(vertices)))
        vertices.extend([lower, [upper[0], lower[1]], upper, [lower[0], upper[1]]])

    # create the Path2D with specified rectangles
    rect = Path2D(entities=lines, vertices=vertices, **kwargs)

    return rect


def box_outline(extents=None, transform=None, **kwargs):
    """
    Return a cuboid.

    Parameters
    ------------
    extents : float, or (3,) float
      Edge lengths
    transform: (4, 4) float
      Transformation matrix
    **kwargs:
        passed to Trimesh to create box

    Returns
    ------------
    geometry : trimesh.Path3D
      Path outline of a cuboid geometry
    """
    from .exchange.load import load_path

    # create vertices for the box
    vertices = [0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1]
    vertices = np.array(vertices, order="C", dtype=np.float64).reshape((-1, 3))
    vertices -= 0.5

    # resize the vertices based on passed size
    if extents is not None:
        extents = np.asanyarray(extents, dtype=np.float64)
        if extents.shape != (3,):
            raise ValueError("Extents must be (3,)!")
        vertices *= extents

    # apply transform if passed
    if transform is not None:
        vertices = transformations.transform_points(vertices, transform)

    # vertex indices
    indices = [0, 1, 3, 2, 0, 4, 5, 7, 6, 4, 0, 2, 6, 7, 3, 1, 5]
    outline = load_path(vertices[indices])

    return outline


def grid(
    side,
    count=5,
    transform=None,
    plane_origin=None,
    plane_normal=None,
    include_circle=True,
    sections_circle=32,
):
    """
    Create a Path3D for a grid visualization of a plane.

    Parameters
    -----------
    side : float
      Length of half of a grid side
    count : int
      Number of grid lines per grid half
    transform : None or (4, 4) float
      Transformation matrix to move grid location.
      Takes precedence over plane_origin if both are passed.
    plane_origin : None or (3,) float
      Plane origin
    plane_normal : None or (3,) float
      Unit normal vector
    include_circle : bool
      Include a circular pattern inside the grid
    sections_circle : int
      How many sections should the smallest circle have

    Returns
    ----------
    grid : trimesh.path.Path3D
      Path containing grid plane visualization
    """
    from .path import Path3D

    # change full side length to half-side
    side = float(side)
    # make sure count is an integer
    count = int(count)
    # get a spaced sequence of radius
    radii = np.linspace(0.0, side, count + 1)[1:]
    # what's the maximum radius
    rmax = radii[-1]

    # keep a count of the current vertex count
    current = 0
    # collect vertices and entities
    vertices = []
    entities = []
    for r in radii:
        if include_circle:
            # scale the section count by radius
            circle_res = int((r / radii[0]) * sections_circle)
            # generate a circule pattern
            theta = np.linspace(0.0, np.pi * 2, circle_res)
            circle = np.column_stack((np.cos(theta), np.sin(theta))) * r
            # append the circle pattern
            vertices.append(circle)
            entities.append(Line(points=np.arange(len(circle)) + current))
            # keep the vertex count correct
            current += len(circle)
        # generate a series of grid lines
        vertices.append(
            [
                [-rmax, r],
                [rmax, r],
                [-rmax, -r],
                [rmax, -r],
                [r, -rmax],
                [r, rmax],
                [-r, -rmax],
                [-r, rmax],
            ]
        )
        # append an entity per grid line
        for i in [0, 2, 4, 6]:
            entities.append(Line(points=np.arange(2) + current + i))
        current += len(vertices[-1])

    # add the middle lines which were skipped
    vertices.append([[0, rmax], [0, -rmax], [-rmax, 0], [rmax, 0]])
    entities.append(Line(points=np.arange(2) + current))
    entities.append(Line(points=np.arange(2) + current + 2))
    # stack vertices into clean (n, 3) float
    vertices = np.vstack(vertices)

    # if plane was passed instead of transform create the matrix here
    if transform is None and plane_origin is not None and plane_normal is not None:
        transform = np.linalg.inv(
            plane_transform(origin=plane_origin, normal=plane_normal)
        )

    # stack vertices to 3D
    vertices = np.column_stack((vertices, np.zeros(len(vertices))))
    # apply transform if passed
    if transform is not None:
        vertices = transformations.transform_points(vertices, matrix=transform)
    # combine result into a Path3D object
    grid_path = Path3D(entities=entities, vertices=vertices)
    return grid_path
