from __future__ import print_function
from __future__ import absolute_import
from __future__ import division

from .smoothing import mesh_smooth_area
from .operations.collapse import trimesh_collapse_edge
from .operations.swap import trimesh_swap_edge
from .operations.split import trimesh_split_edge


def trimesh_remesh(
    mesh,
    target,
    kmax=100,
    tol=0.1,
    divergence=0.01,
    verbose=False,
    allow_boundary_split=False,
    allow_boundary_swap=False,
    allow_boundary_collapse=False,
    smooth=True,
    fixed=None,
    callback=None,
    callback_args=None,
):
    """Remesh until all edges have a specified target length.

    Parameters
    ----------
    mesh : :class:`~compas.datastructures.Mesh`
        A triangle mesh.
    target : float
        The target length for the mesh edges.
    kmax : int, optional
        The number of iterations.
    tol : float, optional
        Length deviation tolerance.
    divergence : float, optional
        ??
    verbose : bool, optional
        Print feedback messages.
    allow_boundary_split : bool, optional
        Allow boundary edges to be split.
    allow_boundary_swap : bool, optional
        Allow boundary edges or edges connected to the boundary to be swapped.
    allow_boundary_collapse : bool, optional
        Allow boundary edges or edges connected to the boundary to be collapsed.
    smooth : bool, optional
        Apply smoothing at every iteration.
    fixed : list[int], optional
        A list of vertices that have to stay fixed.
    callback : callable, optional
        A user-defined function that is called after every iteration.
    callback_args : list[Any], optional
        A list of additional parameters to be passed to the callback function.

    Returns
    -------
    None
        The mesh is modified in place.

    Notes
    -----
    This algorithm not only changes the geometry of the mesh, but also its
    topology as needed to achieve the specified target lengths.
    Topological changes are made such that vertex valencies are well-balanced
    and close to six. This involves three operations:

        * split edges that are longer than a maximum length,
        * collapse edges that are shorter than a minimum length,
        * swap edges if this improves the valency error.

    The minimum and maximum lengths are calculated based on a desired target
    length.

    For more info, see [1]_.

    References
    ----------
    .. [1] Botsch, M. & Kobbelt, L., 2004. *A remeshing approach to multiresolution modeling*.
           Proceedings of the 2004 Eurographics/ACM SIGGRAPH symposium on Geometry processing - SGP '04, p.185.
           Available at: http://portal.acm.org/citation.cfm?doid=1057432.1057457.

    """
    if verbose:
        print(target)

    lmin = (1 - tol) * (4.0 / 5.0) * target
    lmax = (1 + tol) * (4.0 / 3.0) * target

    edge_lengths = [mesh.edge_length(edge) for edge in mesh.edges()]
    target_start = max(edge_lengths) / 2.0

    fac = target_start / target

    boundary = set(mesh.vertices_on_boundary())

    fixed = fixed or []
    fixed = set(fixed)

    count = 0

    kmax_start = kmax / 2.0

    for k in range(kmax):
        if k <= kmax_start:
            scale = fac * (1.0 - k / kmax_start)
            dlmin = lmin * scale
            dlmax = lmax * scale
        else:
            dlmin = 0
            dlmax = 0

        if verbose:
            print(k)

        count += 1

        if k % 20 == 0:
            num_vertices_1 = mesh.number_of_vertices()

        # split
        if count == 1:
            visited = set()

            for u, v in list(mesh.edges()):
                if u in fixed and v in fixed:
                    continue

                if u in visited or v in visited:
                    continue
                if mesh.edge_length((u, v)) <= lmax + dlmax:
                    continue

                if verbose:
                    print("split edge: {0} - {1}".format(u, v))

                trimesh_split_edge(mesh, (u, v), allow_boundary=allow_boundary_split)

                visited.add(u)
                visited.add(v)

        # collapse
        elif count == 2:
            visited = set()

            for u, v in list(mesh.edges()):
                if u in fixed and v in fixed:
                    continue

                if u in visited or v in visited:
                    continue
                if mesh.edge_length((u, v)) >= lmin - dlmin:
                    continue
                if verbose:
                    print("collapse edge: {0} - {1}".format(u, v))

                trimesh_collapse_edge(mesh, (u, v), allow_boundary=allow_boundary_collapse, fixed=fixed)

                visited.add(u)
                visited.add(v)

                visited.update(mesh.halfedge[u])

        # swap
        elif count == 3:
            visited = set()

            for u, v in list(mesh.edges()):
                if u in fixed and v in fixed:
                    continue

                if u in visited or v in visited:
                    continue

                f1 = mesh.halfedge[u][v]
                f2 = mesh.halfedge[v][u]

                if f1 is None or f2 is None:
                    continue

                face1 = mesh.face[f1]
                face2 = mesh.face[f2]

                v1 = face1[face1.index(u) - 1]
                v2 = face2[face2.index(v) - 1]

                valency1 = mesh.vertex_degree(u)
                valency2 = mesh.vertex_degree(v)
                valency3 = mesh.vertex_degree(v1)
                valency4 = mesh.vertex_degree(v2)

                if u in boundary:
                    valency1 += 2
                if v in boundary:
                    valency2 += 2
                if v1 in boundary:
                    valency3 += 2
                if v2 in boundary:
                    valency4 += 2

                current_error = abs(valency1 - 6) + abs(valency2 - 6) + abs(valency3 - 6) + abs(valency4 - 6)
                flipped_error = abs(valency1 - 7) + abs(valency2 - 7) + abs(valency3 - 5) + abs(valency4 - 5)

                if current_error <= flipped_error:
                    continue

                if verbose:
                    print("swap edge: {0} - {1}".format(u, v))

                trimesh_swap_edge(mesh, (u, v), allow_boundary=allow_boundary_swap)

                visited.add(u)
                visited.add(v)
        # count
        else:
            count = 0

        if (k - 10) % 20 == 0:
            num_vertices_2 = mesh.number_of_vertices()

            if abs(1 - num_vertices_1 / num_vertices_2) < divergence and k > kmax_start:
                break

        # smoothen
        if smooth:
            if allow_boundary_split:
                boundary = set(mesh.vertices_on_boundary())

            mesh_smooth_area(mesh, fixed=fixed.union(boundary), kmax=1)

        # callback
        if callback:
            callback(mesh, k, callback_args)
