# -*- coding:utf-8 -*-

# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

bl_info = {
    "name": "Delaunay Voronoi",
    "description": "Points cloud Delaunay triangulation in 2.5D "
                   "(suitable for terrain modelling) or Voronoi diagram in 2D",
    "author": "Domlysz, Oscurart",
    "version": (1, 3),
    "blender": (2, 70, 0),
    "location": "3D View > Toolshelf > Create > Delaunay Voronoi",
    "warning": "",
    "wiki_url": "https://github.com/domlysz/BlenderGIS/wiki",
    "category": "Add Mesh"
    }

import bpy
from .DelaunayVoronoi import (
        computeVoronoiDiagram,
        computeDelaunayTriangulation,
        )
from bpy.types import (
        Operator,
        Panel,
        )
from bpy.props import EnumProperty

try:
    from scipy.spatial import Delaunay
    import bmesh
    import numpy as np
    HAS_SCIPY = True
except:
    HAS_SCIPY = False
    pass


# Globals
# set to True to enable debug_prints
DEBUG = False


def debug_prints(text=""):
    global DEBUG
    if DEBUG and text:
        print(text)


class Point:
    def __init__(self, x, y, z):
        self.x, self.y, self.z = x, y, z


def unique(L):
    """Return a list of unhashable elements in s, but without duplicates.
    [[1, 2], [2, 3], [1, 2]] >>> [[1, 2], [2, 3]]"""
    # For unhashable objects, you can sort the sequence and
    # then scan from the end of the list, deleting duplicates as you go
    nDupli = 0
    nZcolinear = 0
    # sort() brings the equal elements together; then duplicates
    # are easy to weed out in a single pass
    L.sort()
    last = L[-1]
    for i in range(len(L) - 2, -1, -1):
        if last[:2] == L[i][:2]:    # XY coordinates compararison
            if last[2] == L[i][2]:  # Z coordinates compararison
                nDupli += 1         # duplicates vertices
            else:  # Z colinear
                nZcolinear += 1
            del L[i]
        else:
            last = L[i]
    # list data type is mutable, input list will automatically update
    # and doesn't need to be returned
    return (nDupli, nZcolinear)


def checkEqual(lst):
    return lst[1:] == lst[:-1]


class ToolsPanelDelaunay(Panel):
    bl_category = "Create"
    bl_label = "Delaunay Voronoi"
    bl_space_type = "VIEW_3D"
    bl_context = "objectmode"
    bl_region_type = "TOOLS"
    bl_options = {"DEFAULT_CLOSED"}

    def draw(self, context):
        layout = self.layout

        box = layout.box()
        col = box.column(align=True)
        col.label(text="Point Cloud:")
        col.operator("delaunay.triangulation")
        col.operator("voronoi.tesselation")


class OBJECT_OT_TriangulateButton(Operator):
    bl_idname = "delaunay.triangulation"
    bl_label = "Triangulation"
    bl_description = ("Terrain points cloud Delaunay triangulation in 2.5D\n"
                      "Needs an existing Active Mesh Object")
    bl_options = {"REGISTER", "UNDO"}

    @classmethod
    def poll(cls, context):
        obj = context.active_object
        return (obj is not None and obj.type == "MESH")

    def execute(self, context):
        # move the check into the poll
        obj = context.active_object

        if HAS_SCIPY:
            # Use scipy when present (~18 x faster)
            bpy.ops.object.mode_set(mode='EDIT')
            bm = bmesh.from_edit_mesh(obj.data)
            points_3D = [list(v.co) for v in bm.verts]
            points_2D = np.array([[v[0], v[1]] for v in points_3D])
            print("Triangulate " + str(len(points_3D)) + " points...")
            # Triangulate
            tri = Delaunay(points_2D)
            faces = tri.simplices.tolist()
            # Create new mesh structure
            print("Create mesh...")
            bpy.ops.object.mode_set(mode='OBJECT')
            mesh = bpy.data.meshes.new("TIN")
            mesh.from_pydata(points_3D, [], faces)
            mesh.update(calc_edges=True)
            my = bpy.data.objects.new("TIN", mesh)
            context.collection.objects.link(my)
            my.matrix_world = obj.matrix_world.copy()
            obj.select_set(False)
            my.select_set(True)
            context.view_layer.objects.active = my
            self.report({'INFO'}, "Mesh created (" + str(len(faces)) + " triangles)")
            print("Total :%s faces  %s verts" % (len(faces), len(points_3D)))
            return {'FINISHED'}

        # Get points coordinates
        r = obj.rotation_euler
        s = obj.scale
        mesh = obj.data
        vertsPts = [vertex.co for vertex in mesh.vertices]

        # Remove duplicate
        verts = [[vert.x, vert.y, vert.z] for vert in vertsPts]
        nDupli, nZcolinear = unique(verts)
        nVerts = len(verts)

        debug_prints(text=str(nDupli) + " duplicate points ignored")
        debug_prints(str(nZcolinear) + " z colinear points excluded")

        if nVerts < 3:
            self.report({"WARNING"},
                        "Not enough points to continue. Operation Cancelled")

            return {"CANCELLED"}

        # Check colinear
        xValues = [pt[0] for pt in verts]
        yValues = [pt[1] for pt in verts]

        if checkEqual(xValues) or checkEqual(yValues):
            self.report({'ERROR'}, "Points are colinear")
            return {'FINISHED'}

        # Triangulate
        debug_prints(text="Triangulate " + str(nVerts) + " points...")

        vertsPts = [Point(vert[0], vert[1], vert[2]) for vert in verts]
        triangles = computeDelaunayTriangulation(vertsPts)
        # reverse point order --> if all triangles are specified anticlockwise then all faces up
        triangles = [tuple(reversed(tri)) for tri in triangles]

        debug_prints(text=str(len(triangles)) + " triangles")

        # Create new mesh structure
        debug_prints(text="Create mesh...")
        tinMesh = bpy.data.meshes.new("TIN")        # create a new mesh
        tinMesh.from_pydata(verts, [], triangles)   # Fill the mesh with triangles
        tinMesh.update(calc_edges=True)             # Update mesh with new data

        # Create an object with that mesh
        tinObj = bpy.data.objects.new("TIN", tinMesh)

        # Place object
        tinObj.location = obj.location.copy()
        tinObj.rotation_euler = r
        tinObj.scale = s

        # Update scene
        bpy.context.collection.objects.link(tinObj)  # Link object to collection 
        bpy.context.view_layer.objects.active = tinObj
        tinObj.select_set(True)
        obj.select_set(False)

        self.report({"INFO"},
                     "Mesh created (" + str(len(triangles)) + " triangles)")

        return {'FINISHED'}


class OBJECT_OT_VoronoiButton(Operator):
    bl_idname = "voronoi.tesselation"
    bl_label = "Diagram"
    bl_description = ("Points cloud Voronoi diagram in 2D\n"
                      "Needs an existing Active Mesh Object")
    bl_options = {"REGISTER", "UNDO"}

    meshType: EnumProperty(
            items=[('Edges', "Edges", "Edges Only - do not fill Faces"),
                   ('Faces', "Faces", "Fill Faces in the new Object")],
            name="Mesh type",
            description="Type of geometry to generate"
            )

    @classmethod
    def poll(cls, context):
        obj = context.active_object
        return (obj is not None and obj.type == "MESH")

    def execute(self, context):
        # move the check into the poll
        obj = context.active_object

        # Get points coordinates
        r = obj.rotation_euler
        s = obj.scale
        mesh = obj.data
        vertsPts = [vertex.co for vertex in mesh.vertices]

        # Remove duplicate
        verts = [[vert.x, vert.y, vert.z] for vert in vertsPts]
        nDupli, nZcolinear = unique(verts)
        nVerts = len(verts)

        debug_prints(text=str(nDupli) + " duplicates points ignored")
        debug_prints(text=str(nZcolinear) + " z colinear points excluded")

        if nVerts < 3:
            self.report({"WARNING"},
                        "Not enough points to continue. Operation Cancelled")

            return {"CANCELLED"}

        # Check colinear
        xValues = [pt[0] for pt in verts]
        yValues = [pt[1] for pt in verts]

        if checkEqual(xValues) or checkEqual(yValues):
            self.report({"WARNING"},
                        "Points are colinear. Operation Cancelled")

            return {"CANCELLED"}

        # Create diagram
        debug_prints(text="Tesselation... (" + str(nVerts) + " points)")

        xbuff, ybuff = 5, 5
        zPosition = 0
        vertsPts = [Point(vert[0], vert[1], vert[2]) for vert in verts]

        if self.meshType == "Edges":
            pts, edgesIdx = computeVoronoiDiagram(
                                vertsPts, xbuff, ybuff,
                                polygonsOutput=False, formatOutput=True
                                )
        else:
            pts, polyIdx = computeVoronoiDiagram(
                                vertsPts, xbuff, ybuff, polygonsOutput=True,
                                formatOutput=True, closePoly=False
                                )

        pts = [[pt[0], pt[1], zPosition] for pt in pts]

        # Create new mesh structure
        voronoiDiagram = bpy.data.meshes.new("VoronoiDiagram")  # create a new mesh

        if self.meshType == "Edges":
            # Fill the mesh with triangles
            voronoiDiagram.from_pydata(pts, edgesIdx, [])
        else:
            # Fill the mesh with triangles
            voronoiDiagram.from_pydata(pts, [], list(polyIdx.values()))

        voronoiDiagram.update(calc_edges=True)  # Update mesh with new data
        # create an object with that mesh
        voronoiObj = bpy.data.objects.new("VoronoiDiagram", voronoiDiagram)
        # place object
        voronoiObj.location = obj.location.copy()
        voronoiObj.rotation_euler = r
        voronoiObj.scale = s

        # update scene
        bpy.context.collection.objects.link(voronoiObj)  # Link object to collection
        bpy.context.view_layer.objects.active = voronoiObj
        voronoiObj.select_set(True)
        obj.select_set(False)

        # Report
        if self.meshType == "Edges":
            self.report({"INFO"}, "Mesh created (" + str(len(edgesIdx)) + " edges)")
        else:
            self.report({"INFO"}, "Mesh created (" + str(len(polyIdx)) + " polygons)")

        return {'FINISHED'}


# Register
def register():
    bpy.utils.register_class(OBJECT_OT_VoronoiButton)
    bpy.utils.register_class(OBJECT_OT_TriangulateButton)
    bpy.utils.register_class(ToolsPanelDelaunay)


def unregister():
    bpy.utils.unregister_class(OBJECT_OT_VoronoiButton)
    bpy.utils.unregister_class(OBJECT_OT_TriangulateButton)
    bpy.utils.unregister_class(ToolsPanelDelaunay)


if __name__ == "__main__":
    register()
