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

import ast
import compas_rhino

from compas.geometry import add_vectors

import Rhino
import clr

clr.AddReference('Rhino.UI')
import Rhino.UI  # noqa: E402
from Rhino.Geometry import Point3d  # noqa: E402

try:
    from compas_rhino.forms import PropertyListForm
except ImportError:
    from Rhino.UI.Dialogs import ShowPropertyListBox


__all__ = [
    'network_update_attributes',
    'network_update_node_attributes',
    'network_update_edge_attributes',

    'network_move_node',

    'mesh_update_attributes',
    'mesh_update_vertex_attributes',
    'mesh_update_face_attributes',
    'mesh_update_edge_attributes',

    'mesh_move_vertex',
    'mesh_move_vertices',
    'mesh_move_face',
]


def _update_named_values(names, values, message='', title='Update named values'):
    try:
        dialog = PropertyListForm(names, values)
    except Exception:
        values = ShowPropertyListBox(message, title, names, values)
    else:
        if dialog.ShowModal(Rhino.UI.RhinoEtoApp.MainWindow):
            values = dialog.values
        else:
            values = None
    return values


def network_update_attributes(network):
    """Update the attributes of a network.

    Parameters
    ----------
    network : :class:`compas.datastructures.Network`
        A network object.

    Returns
    -------
    bool
        True if the update was successful.
        False otherwise.
    """
    names = sorted(network.attributes.keys())
    values = [str(network.attributes[name]) for name in names]
    values = _update_named_values(names, values)
    if values:
        for name, value in zip(names, values):
            try:
                network.attributes[name] = ast.literal_eval(value)
            except (ValueError, TypeError):
                network.attributes[name] = value
        return True
    return False


def network_update_node_attributes(network, nodes, names=None):
    """Update the attributes of the nodes of a network.

    Parameters
    ----------
    network : :class:`compas.datastructures.Network`
        A network object.
    nodes : list
        The identifiers of the nodes to update.
    names : list, optional
        The names of the atrtibutes to update.
        Default is to update all attributes.

    Returns
    -------
    bool
        True if the update was successful.
        False otherwise.

    """
    names = names or network.default_node_attributes.keys()
    names = sorted(names)
    values = network.node_attributes(nodes[0], names)
    if len(nodes) > 1:
        for i, name in enumerate(names):
            for node in nodes[1:]:
                if values[i] != network.node_attribute(node, name):
                    values[i] = '-'
                    break
    values = map(str, values)
    values = _update_named_values(names, values)
    if values:
        for name, value in zip(names, values):
            if value == '-':
                continue
            for node in nodes:
                try:
                    network.node_attribute(node, name, ast.literal_eval(value))
                except (ValueError, TypeError):
                    network.node_attribute(node, name, value)
        return True
    return False


def network_update_edge_attributes(network, edges, names=None):
    """Update the attributes of the edges of a network.

    Parameters
    ----------
    network : :class:`compas.datastructures.Network`
        A network object.
    edges : list
        The identifiers of the edges to update.
    names : list, optional
        The names of the atrtibutes to update.
        Default is to update all attributes.

    Returns
    -------
    bool
        True if the update was successful.
        False otherwise.

    """
    names = names or network.default_edge_attributes.keys()
    names = sorted(names)
    edge = edges[0]
    values = network.edge_attributes(edge, names)
    if len(edges) > 1:
        for i, name in enumerate(names):
            for edge in edges[1:]:
                if values[i] != network.edge_attribute(edge, name):
                    values[i] = '-'
                    break
    values = map(str, values)
    values = _update_named_values(names, values)
    if values:
        for name, value in zip(names, values):
            if value == '-':
                continue
            for edge in edges:
                try:
                    value = ast.literal_eval(value)
                except (SyntaxError, ValueError, TypeError):
                    pass
                network.edge_attribute(edge, name, value)
        return True
    return False


def network_move_node(network, node, constraint=None, allow_off=False):
    """Move on node of the network.

    Parameters
    ----------
    network : :class:`compas.datastructures.Network`
        A network object.
    node : hashable
        The identifier of the node to move.
    constraint : Rhino.Geometry, optional
        A Rhino geometry object to constrain the movement to.
        By default the movement is unconstrained.
    allow_off : bool, optional (False)
        Allow the node to move off the constraint.

    """
    def OnDynamicDraw(sender, e):
        for ep in nbrs:
            sp = e.CurrentPoint
            e.Display.DrawDottedLine(sp, ep, color)

    color = Rhino.ApplicationSettings.AppearanceSettings.FeedbackColor
    nbrs = [network.node_coordinates(nbr) for nbr in network.node_neighbors(node)]
    nbrs = [Point3d(*xyz) for xyz in nbrs]

    gp = Rhino.Input.Custom.GetPoint()

    gp.SetCommandPrompt('Point to move to?')
    gp.DynamicDraw += OnDynamicDraw
    if constraint:
        gp.Constrain(constraint, allow_off)

    gp.Get()
    if gp.CommandResult() != Rhino.Commands.Result.Success:
        return False

    network.node_attributes(node, 'xyz', list(gp.Point()))
    return True


def mesh_update_attributes(mesh):
    """Update the attributes of a mesh.

    Parameters
    ----------
    mesh : :class:`compas.datastructures.Mesh`
        A mesh object.

    Returns
    -------
    bool
        True if the update was successful.
        False otherwise.

    """
    names = sorted(mesh.attributes.keys())
    values = [str(mesh.attributes[name]) for name in names]
    values = compas_rhino.update_named_values(names, values)
    if values:
        for name, value in zip(names, values):
            try:
                mesh.attributes[name] = ast.literal_eval(value)
            except (ValueError, TypeError):
                mesh.attributes[name] = value
        return True
    return False


def mesh_update_vertex_attributes(mesh, vertices, names=None):
    """Update the attributes of selected vertices of a given datastructure.

    Parameters
    ----------
    mesh : compas.datastructures.Datastructure
        The data structure.
    vertices : list
        The vertices of the vertices of which the attributes should be updated.
    names : list, optional
        The names of the attributes that should be updated.
        Default is to update all available attributes.

    Returns
    -------
    bool
        True if the attributes were successfully updated.
        False otherwise.

    """
    names = names or mesh.default_vertex_attributes.keys()
    names = sorted(names)
    values = mesh.vertex_attributes(vertices[0], names)
    if len(vertices) > 1:
        for i, name in enumerate(names):
            for vertex in vertices[1:]:
                if values[i] != mesh.vertex_attribute(vertex, name):
                    values[i] = '-'
                    break
    values = map(str, values)
    values = _update_named_values(names, values)
    if values:
        for name, value in zip(names, values):
            if value == '-':
                continue
            for vertex in vertices:
                try:
                    mesh.vertex_attribute(vertex, name, ast.literal_eval(value))
                except (ValueError, TypeError):
                    mesh.vertex_attribute(vertex, name, value)
        return True
    return False


# rename to modify
def mesh_update_face_attributes(mesh, faces, names=None):
    """Update the attributes of the faces of a mesh.

    Parameters
    ----------
    mesh : :class:`compas.datastructures.Mesh`
    faces : list of int
    names : list, optional
        The names of the atrtibutes to update.
        Default is to update all attributes.

    Returns
    -------
    bool
        True if the update was successful.
        False otherwise.

    """
    names = names or mesh.default_face_attributes.keys()
    names = sorted(names)
    values = mesh.face_attributes(faces[0], names)
    if len(faces) > 1:
        for i, name in enumerate(names):
            for face in faces[1:]:
                if values[i] != mesh.face_attribute(face, name):
                    values[i] = '-'
                    break
    values = map(str, values)
    values = _update_named_values(names, values)
    if values:
        for name, value in zip(names, values):
            if value == '-':
                continue
            for face in faces:
                try:
                    mesh.face_attribute(face, name, ast.literal_eval(value))
                except (ValueError, TypeError):
                    mesh.face_attribute(face, name, value)
        return True
    return False


def mesh_update_edge_attributes(mesh, edges, names=None):
    """Update the attributes of the edges of a mesh.

    Parameters
    ----------
    mesh : :class:`compas.datastructures.Mesh`
        A mesh object.
    edges : list
        The edges to update.
    names : list, optional
        The names of the atrtibutes to update.
        Default is to update all attributes.

    Returns
    -------
    bool
        True if the update was successful.
        False otherwise.

    """
    names = names or mesh.default_edge_attributes.keys()
    names = sorted(names)
    edge = edges[0]
    values = mesh.edge_attributes(edge, names)
    if len(edges) > 1:
        for i, name in enumerate(names):
            for edge in edges[1:]:
                if values[i] != mesh.edge_attribute(edge, name):
                    values[i] = '-'
                    break
    values = map(str, values)
    values = _update_named_values(names, values)
    if values:
        for name, value in zip(names, values):
            if value == '-':
                continue
            for edge in edges:
                try:
                    value = ast.literal_eval(value)
                except (SyntaxError, ValueError, TypeError):
                    pass
                mesh.edge_attribute(edge, name, value)
        return True
    return False


# def mesh_move(mesh):
#     """"""
#     color = Rhino.ApplicationSettings.AppearanceSettings.FeedbackColor

#     vertex_xyz0 = {key: mesh.vertex_coordinates(key) for key in mesh.mesh.vertices()}
#     vertex_xyz = {key: mesh.vertex_coordinates(key) for key in mesh.mesh.vertices()}

#     edges = list(mesh.edges())

#     start = compas_rhino.pick_point('Point to move from?')
#     if not start:
#         return False

#     def OnDynamicDraw(sender, e):
#         current = list(e.CurrentPoint)
#         vector = subtract_vectors(current, start)
#         for vertex in vertex_xyz:
#             vertex_xyz[vertex] = add_vectors(vertex_xyz0[vertex], vector)
#         for u, v in iter(edges):
#             sp = vertex[u]
#             ep = vertex[v]
#             sp = Point3d(*sp)
#             ep = Point3d(*ep)
#             e.Display.DrawDottedLine(sp, ep, color)

#     gp = Rhino.Input.Custom.GetPoint()
#     gp.SetCommandPrompt('Point to move to?')
#     gp.DynamicDraw += OnDynamicDraw
#     gp.Get()

#     if gp.CommandResult() == Rhino.Commands.Result.Success:
#         end = list(gp.Point())
#         vector = subtract_vectors(end, start)
#         for vertex, attr in mesh.vertices(True):
#             attr['x'] += vector[0]
#             attr['y'] += vector[1]
#             attr['z'] += vector[2]
#         return True
#     return False


def mesh_move_vertex(mesh, vertex, constraint=None, allow_off=True):
    """Move on vertex of the mesh.

    Parameters
    ----------
    mesh : :class:`compas.datastructures.Mesh`
    vertex : int
    constraint : :rhino:`Rhino.Geometry`, optional
        A Rhino geometry object to constrain the movement to.
        By default the movement is unconstrained.
    allow_off : bool, optional (True)
        Allow the vertex to move off the constraint.

    """
    color = Rhino.ApplicationSettings.AppearanceSettings.FeedbackColor
    nbrs = [mesh.vertex_coordinates(nbr) for nbr in mesh.vertex_neighbors(vertex)]
    nbrs = [Point3d(*xyz) for xyz in nbrs]

    def OnDynamicDraw(sender, e):
        for ep in nbrs:
            sp = e.CurrentPoint
            e.Display.DrawDottedLine(sp, ep, color)

    gp = Rhino.Input.Custom.GetPoint()

    gp.SetCommandPrompt('Point to move to?')
    gp.DynamicDraw += OnDynamicDraw
    if constraint:
        gp.Constrain(constraint, allow_off)

    gp.Get()
    if gp.CommandResult() != Rhino.Commands.Result.Success:
        return False

    mesh.vertex_attributes(vertex, 'xyz', list(gp.Point()))
    return True


def mesh_move_vertices(mesh, vertices):
    """Move on vertices of the mesh.

    Parameters
    ----------
    mesh : compas.datastructures.Mesh
        A mesh object.
    keys : list
        The vertices to move.
    constraint : Rhino.Geometry (None)
        A Rhino geometry object to constrain the movement to.
        By default the movement is unconstrained.
    allow_off : bool (False)
        Allow the vertex to move off the constraint.

    """
    color = Rhino.ApplicationSettings.AppearanceSettings.FeedbackColor
    lines = []
    connectors = []

    for vertex in vertices:
        a = mesh.vertex_coordinates(vertex)
        nbrs = mesh.vertex_neighbors(vertex)
        for nbr in nbrs:
            b = mesh.vertex_coordinates(nbr)
            line = [Point3d(* a), Point3d(* b)]
            if nbr in vertices:
                lines.append(line)
            else:
                connectors.append(line)

    gp = Rhino.Input.Custom.GetPoint()

    gp.SetCommandPrompt('Point to move from?')
    gp.Get()
    if gp.CommandResult() != Rhino.Commands.Result.Success:
        return False

    start = gp.Point()

    def OnDynamicDraw(sender, e):
        end = e.CurrentPoint
        vector = end - start
        for a, b in lines:
            a = a + vector
            b = b + vector
            e.Display.DrawDottedLine(a, b, color)
        for a, b in connectors:
            a = a + vector
            e.Display.DrawDottedLine(a, b, color)

    gp.SetCommandPrompt('Point to move to?')
    gp.SetBasePoint(start, False)
    gp.DrawLineFromPoint(start, True)
    gp.DynamicDraw += OnDynamicDraw
    gp.Get()
    if gp.CommandResult() != Rhino.Commands.Result.Success:
        return False

    end = gp.Point()
    vector = list(end - start)

    for vertex in vertices:
        xyz = mesh.vertex_attributes(vertex, 'xyz')
        mesh.vertex_attributes(vertex, 'xyz', add_vectors(xyz, vector))
    return True


def mesh_move_face(mesh, face, constraint=None, allow_off=True):
    """Move a face of the mesh to a different location and update the data structure.

    Parameters
    ----------
    mesh : :class:`compas.datastructures.Mesh`
    face : int

    Returns
    -------
    bool
        True if the update was successful.
        False otherwise.

    """
    def OnDynamicDraw(sender, e):
        for ep in nbrs:
            sp = e.CurrentPoint
            e.Display.DrawDottedLine(sp, ep, color)

    color = Rhino.ApplicationSettings.AppearanceSettings.FeedbackColor
    nbrs = [mesh.face_coordinates(nbr) for nbr in mesh.face_neighbors(face)]
    nbrs = [Point3d(*xyz) for xyz in nbrs]

    gp = Rhino.Input.Custom.GetPoint()

    gp.SetCommandPrompt('Point to move to?')
    gp.DynamicDraw += OnDynamicDraw
    if constraint:
        gp.Constrain(constraint, allow_off)

    gp.Get()

    if gp.CommandResult() == Rhino.Commands.Result.Success:
        mesh.face_attributes(face, 'xyz', list(gp.Point()))
        return True

    return False
