# blender CAM simple.py (c) 2012 Vilem Novak
#
# ***** 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 LICENCE BLOCK *****

# Solves: No module named 'shapely' (even if it is installed)
#help('modules')

import math
import sys
import os
import string
import time
import bpy
import mathutils
from mathutils import *
from math import *
from shapely.geometry import Point, LineString, Polygon, MultiLineString


def tuple_add(t, t1):  # add two tuples as Vectors
    return t[0] + t1[0], t[1] + t1[1], t[2] + t1[2]


def tuple_sub(t, t1):  # sub two tuples as Vectors
    return t[0] - t1[0], t[1] - t1[1], t[2] - t1[2]


def tuple_mul(t, c):  # multiply two tuples with a number
    return t[0] * c, t[1] * c, t[2] * c


def tuple_length(t):  # get length of vector, but passed in as tuple.
    return Vector(t).length


# timing functions for optimisation purposes...
def timinginit():
    return [0, 0]


def timingstart(tinf):
    t = time.time()
    tinf[1] = t


def timingadd(tinf):
    t = time.time()
    tinf[0] += t - tinf[1]


def timingprint(tinf):
    print('time ' + str(tinf[0]) + 'seconds')


def progress(text, n=None):
    """function for reporting during the script, works for background operations in the header."""
    text = str(text)
    if n is None:
        n = ''
    else:
        n = ' ' + str(int(n * 1000) / 1000) + '%'
    sys.stdout.write('progress{%s%s}\n' % (text, n))
    sys.stdout.flush()


def activate(o):
    """makes an object active, used many times in blender"""
    s = bpy.context.scene
    bpy.ops.object.select_all(action='DESELECT')
    o.select_set(state=True)
    s.objects[o.name].select_set(state=True)
    bpy.context.view_layer.objects.active = o


def dist2d(v1, v2):
    """distance between two points in 2d"""
    return math.hypot((v1[0] - v2[0]), (v1[1] - v2[1]))


def delob(ob):
    """object deletion for multiple uses"""
    activate(ob)
    bpy.ops.object.delete(use_global=False)


def dupliob(o, pos):
    """helper function for visualising cutter positions in bullet simulation"""
    activate(o)
    bpy.ops.object.duplicate()
    s = 1.0 / BULLET_SCALE
    bpy.ops.transform.resize(value=(s, s, s), constraint_axis=(False, False, False), orient_type='GLOBAL',
                             mirror=False, use_proportional_edit=False, proportional_edit_falloff='SMOOTH',
                             proportional_size=1)
    o = bpy.context.active_object
    bpy.ops.rigidbody.object_remove()
    o.location = pos


def addToGroup(ob, groupname):
    activate(ob)
    if bpy.data.groups.get(groupname) is None:
        bpy.ops.group.create(name=groupname)
    else:
        bpy.ops.object.group_link(group=groupname)


def compare(v1, v2, vmiddle, e):
    """comparison for optimisation of paths"""
    # e=0.0001
    v1 = Vector(v1)
    v2 = Vector(v2)
    vmiddle = Vector(vmiddle)
    vect1 = v2 - v1
    vect2 = vmiddle - v1
    vect1.normalize()
    vect1 *= vect2.length
    v = vect2 - vect1
    if v.length < e:
        return True
    return False


def isVerticalLimit(v1, v2, limit):
    """test path segment on verticality threshold, for protect_vertical option"""
    z = abs(v1[2] - v2[2])
    # verticality=0.05
    # this will be better.
    #
    # print(a)
    if z > 0:
        v2d = Vector((0, 0, -1))
        v3d = Vector((v1[0] - v2[0], v1[1] - v2[1], v1[2] - v2[2]))
        a = v3d.angle(v2d)
        if a > pi / 2:
            a = abs(a - pi)
        # print(a)
        if a < limit:
            # print(abs(v1[0]-v2[0])/z)
            # print(abs(v1[1]-v2[1])/z)
            if v1[2] > v2[2]:
                v1 = (v2[0], v2[1], v1[2])
                return v1, v2
            else:
                v2 = (v1[0], v1[1], v2[2])
                return v1, v2
    return v1, v2


def getCachePath(o):
    fn = bpy.data.filepath
    l = len(bpy.path.basename(fn))
    bn = bpy.path.basename(fn)[:-6]
    print('fn-l:', fn[:-l])
    print('bn:', bn)

    iname = fn[:-l] + 'temp_cam' + os.sep + bn + '_' + o.name
    return iname


def getSimulationPath():
    fn = bpy.data.filepath
    l = len(bpy.path.basename(fn))
    iname = fn[:-l] + 'temp_cam' + os.sep
    return iname


def safeFileName(name):  # for export gcode
    valid_chars = "-_.()%s%s" % (string.ascii_letters, string.digits)
    filename = ''.join(c for c in name if c in valid_chars)
    return filename


def strInUnits(x, precision=5):
    if bpy.context.scene.unit_settings.system == 'METRIC':
        return str(round(x * 1000, precision)) + ' mm '
    elif bpy.context.scene.unit_settings.system == 'IMPERIAL':
        return str(round(x * 1000 / 25.4, precision)) + "'' "
    else:
        return str(x)


# select multiple object starting with name
def select_multiple(name):
    scene = bpy.context.scene
    bpy.ops.object.select_all(action='DESELECT')
    for ob in scene.objects:  # join pocket curve calculations
        if ob.name.startswith(name):
            ob.select_set(True)
        else:
            ob.select_set(False)


# join multiple objects starting with 'name' renaming final object as 'name'
def join_multiple(name):
    select_multiple(name)
    bpy.ops.object.join()
    bpy.context.active_object.name = name  # rename object


# remove multiple objects starting with 'name'.... useful for fixed name operation
def remove_multiple(name):
    scene = bpy.context.scene
    bpy.ops.object.select_all(action='DESELECT')
    for ob in scene.objects:
        if ob.name.startswith(name):
            ob.select_set(True)
            bpy.ops.object.delete()


def deselect():
    bpy.ops.object.select_all(action='DESELECT')


# makes the object with the name active
def make_active(name):
    ob = bpy.context.scene.objects[name]
    bpy.ops.object.select_all(action='DESELECT')
    bpy.context.view_layer.objects.active = ob
    ob.select_set(True)


# change the name of the active object
def active_name(name):
    bpy.context.active_object.name = name


# renames and makes active name and makes it active
def rename(name, name2):
    make_active(name)
    bpy.context.active_object.name = name2


# boolean union of objects starting with name result is object name.
# all objects starting with name will be deleted and the result will be name
def union(name):
    select_multiple(name)
    bpy.ops.object.curve_boolean(boolean_type='UNION')
    active_name('unionboolean')
    remove_multiple(name)
    rename('unionboolean', name)

def intersect(name):
    select_multiple(name)
    bpy.ops.object.curve_boolean(boolean_type='INTERSECT')
    active_name('intersection')

# boolean difference of objects starting with name result is object from basename.
# all objects starting with name will be deleted and the result will be basename
def difference(name, basename):
    #   name is the series to select
    #   basename is what the base you want to cut including name
    select_multiple(name)
    bpy.context.view_layer.objects.active = bpy.data.objects[basename]
    bpy.ops.object.curve_boolean(boolean_type='DIFFERENCE')
    active_name('booleandifference')
    remove_multiple(name)
    rename('booleandifference', basename)


# duplicate active object or duplicate move
# if x or y not the default, duplicate move will be executed
def duplicate(x=0, y=0):
    if x == 0 and y == 0:
        bpy.ops.object.duplicate()
    else:
        bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked": False, "mode": 'TRANSLATION'},
                                      TRANSFORM_OT_translate={"value": (x, y, 0.0)})


# Mirror active object along the x axis
def mirrorx():
    bpy.ops.transform.mirror(orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)),
                             orient_matrix_type='GLOBAL', constraint_axis=(True, False, False))


# mirror active object along y axis
def mirrory():
    bpy.ops.transform.mirror(orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)),
                             orient_matrix_type='GLOBAL', constraint_axis=(False, True, False))


# move active object and apply translation
def move(x=0.0, y=0.0):
    bpy.ops.transform.translate(value=(x, y, 0.0))
    bpy.ops.object.transform_apply(location=True)


# Rotate active object and apply rotation
def rotate(angle):
    bpy.context.object.rotation_euler[2] = angle
    bpy.ops.object.transform_apply(rotation=True)


# remove doubles
def remove_doubles():
    bpy.ops.object.curve_remove_doubles()


# Add overcut to active object
def add_overcut(diametre, overcut=True):
    if overcut:
        name = bpy.context.active_object.name
        bpy.ops.object.curve_overcuts(diameter=diametre, threshold=math.pi/2.05)
        overcut_name = bpy.context.active_object.name
        make_active(name)
        bpy.ops.object.delete()
        rename(overcut_name, name)
        remove_doubles()


# add bounding rectangtle to curve
def add_bound_rectangle(xmin, ymin, xmax, ymax, name='bounds_rectangle'):
    # xmin = minimum corner x value
    # ymin = minimum corner y value
    # xmax = maximum corner x value
    # ymax = maximum corner y value
    # name = name of the resulting object
    xsize = xmax - xmin
    ysize = ymax - ymin

    bpy.ops.curve.simple(align='WORLD', location=(xmin + xsize/2, ymin + ysize/2, 0), rotation=(0, 0, 0),
                         Simple_Type='Rectangle',
                         Simple_width=xsize, Simple_length=ysize, use_cyclic_u=True, edit_mode=False, shape='3D')
    bpy.ops.object.transform_apply(location=True)
    active_name(name)


def add_rectangle(width, height, center_x=True, center_y=True):
    x_offset = width / 2
    y_offset = height / 2

    if center_x:
        x_offset = 0
    if center_y:
        y_offset = 0

    bpy.ops.curve.simple(align='WORLD', location=(x_offset, y_offset, 0), rotation=(0, 0, 0),
                         Simple_Type='Rectangle',
                         Simple_width=width, Simple_length=height, use_cyclic_u=True, edit_mode=False, shape='3D')
    bpy.ops.object.transform_apply(location=True)
    active_name('simple_rectangle')


#  Returns coords from active object
def active_to_coords():
    bpy.ops.object.duplicate()
    obj = bpy.context.active_object
    bpy.ops.object.convert(target='MESH')
    active_name("_tmp_mesh")

    coords = []
    for v in obj.data.vertices:  # extract X,Y coordinates from the vertices data
        coords.append((v.co.x, v.co.y))
    remove_multiple('_tmp_mesh')
    return coords


# returns shapely polygon from active object
def active_to_shapely_poly():
    return Polygon(active_to_coords())  # convert coordinates to shapely Polygon datastructure
