# blender CAM pattern.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 *****

import time
import mathutils
from mathutils import *

from cam import simple, chunk, utils
from cam.simple import *
from cam.chunk import *
from cam import polygon_utils_cam
from cam.polygon_utils_cam import *
import shapely
from shapely import geometry as sgeometry
import numpy


def getPathPatternParallel(o, angle):
    zlevel = 1
    pathd = o.dist_between_paths
    pathstep = o.dist_along_paths
    pathchunks = []

    xm = (o.max.x + o.min.x) / 2
    ym = (o.max.y + o.min.y) / 2
    vm = Vector((xm, ym, 0))
    xdim = o.max.x - o.min.x
    ydim = o.max.y - o.min.y
    dim = (xdim + ydim) / 2.0
    e = Euler((0, 0, angle))
    reverse = False
    if bpy.app.debug_value == 0:  # by default off
        # this is the original pattern method, slower, but well tested:
        dirvect = Vector((0, 1, 0))
        dirvect.rotate(e)
        dirvect.normalize()
        dirvect *= pathstep
        for a in range(int(-dim / pathd),
                       int(dim / pathd)):  # this is highly ineffective, computes path2x the area needed...
            chunk = camPathChunkBuilder([])
            v = Vector((a * pathd, int(-dim / pathstep) * pathstep, 0))
            v.rotate(e)
            v += vm  # shifting for the rotation, so pattern rotates around middle...
            for b in range(int(-dim / pathstep), int(dim / pathstep)):
                v += dirvect

                if v.x > o.min.x and v.x < o.max.x and v.y > o.min.y and v.y < o.max.y:
                    chunk.points.append((v.x, v.y, zlevel))
            if (reverse and o.movement.type == 'MEANDER') or (
                    o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CW') or (
                    o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CCW'):
                chunk.points.reverse()

            if len(chunk.points) > 0:
                pathchunks.append(chunk.to_chunk())
            if len(pathchunks) > 1 and reverse and o.movement.parallel_step_back and not o.use_layers:
                # parallel step back - for finishing, best with climb movement, saves cutter life by going into
                # material with climb, while using move back on the surface to improve finish
                # (which would otherwise be a conventional move in the material)

                if o.movement.type == 'CONVENTIONAL' or o.movement.type == 'CLIMB':
                    pathchunks[-2].reverse()
                changechunk = pathchunks[-1]
                pathchunks[-1] = pathchunks[-2]
                pathchunks[-2] = changechunk

            reverse = not reverse
        # print (chunk.points)
    else:  # alternative algorithm with numpy, didn't work as should so blocked now...

        v = Vector((0, 1, 0))
        v.rotate(e)
        e1 = Euler((0, 0, -math.pi / 2))
        v1 = v.copy()
        v1.rotate(e1)

        axis_across_paths = numpy.array((numpy.arange(int(-dim / pathd), int(dim / pathd)) * pathd * v1.x + xm,
                                         numpy.arange(int(-dim / pathd), int(dim / pathd)) * pathd * v1.y + ym,
                                         numpy.arange(int(-dim / pathd), int(dim / pathd)) * 0))

        axis_along_paths = numpy.array((numpy.arange(int(-dim / pathstep), int(dim / pathstep)) * pathstep * v.x,
                                        numpy.arange(int(-dim / pathstep), int(dim / pathstep)) * pathstep * v.y,
                                        numpy.arange(int(-dim / pathstep),
                                                     int(dim / pathstep)) * 0 + zlevel))  # rotate this first
        progress(axis_along_paths)
        chunks = []
        for a in range(0, len(axis_across_paths[0])):
            # progress(chunks[a,...,...].shape)
            # progress(axis_along_paths.shape)
            nax = axis_along_paths.copy()
            # progress(nax.shape)
            nax[0] += axis_across_paths[0][a]
            nax[1] += axis_across_paths[1][a]
            # progress(a)
            # progress(nax.shape)
            # progress(chunks.shape)
            # progress(chunks[...,a,...].shape)
            xfitmin = nax[0] > o.min.x
            xfitmax = nax[0] < o.max.x
            xfit = xfitmin & xfitmax
            # print(xfit,nax)
            nax = numpy.array([nax[0][xfit], nax[1][xfit], nax[2][xfit]])
            yfitmin = nax[1] > o.min.y
            yfitmax = nax[1] < o.max.y
            yfit = yfitmin & yfitmax
            nax = numpy.array([nax[0][yfit], nax[1][yfit], nax[2][yfit]])
            chunks.append(nax.swapaxes(0, 1))
        # chunks
        pathchunks = []
        print("WOO")
        for ch in chunks:
            ch = ch.tolist()
            pathchunks.append(camPathChunk(ch))
        # print (ch)
    return pathchunks


def getPathPattern(operation):
    o = operation
    t = time.time()
    progress('building path pattern')
    minx, miny, minz, maxx, maxy, maxz = o.min.x, o.min.y, o.min.z, o.max.x, o.max.y, o.max.z

    pathchunks = []

    zlevel = 1  # minz#this should do layers...
    if o.strategy == 'PARALLEL':
        pathchunks = getPathPatternParallel(o, o.parallel_angle)
    elif o.strategy == 'CROSS':

        pathchunks.extend(getPathPatternParallel(o, o.parallel_angle))
        pathchunks.extend(getPathPatternParallel(o, o.parallel_angle - math.pi / 2.0))

    elif o.strategy == 'BLOCK':

        pathd = o.dist_between_paths
        pathstep = o.dist_along_paths
        maxxp = maxx
        maxyp = maxy
        minxp = minx
        minyp = miny
        x = 0.0
        y = 0.0
        incx = 1
        incy = 0
        chunk = camPathChunkBuilder([])
        i = 0
        while maxxp - minxp > 0 and maxyp - minyp > 0:

            y = minyp
            for a in range(ceil(minxp / pathstep), ceil(maxxp / pathstep), 1):
                x = a * pathstep
                chunk.points.append((x, y, zlevel))

            if i > 0:
                minxp += pathd
            chunk.points.append((maxxp, minyp, zlevel))

            x = maxxp

            for a in range(ceil(minyp / pathstep), ceil(maxyp / pathstep), 1):
                y = a * pathstep
                chunk.points.append((x, y, zlevel))

            minyp += pathd
            chunk.points.append((maxxp, maxyp, zlevel))

            y = maxyp
            for a in range(floor(maxxp / pathstep), ceil(minxp / pathstep), -1):
                x = a * pathstep
                chunk.points.append((x, y, zlevel))

            maxxp -= pathd
            chunk.points.append((minxp, maxyp, zlevel))

            x = minxp
            for a in range(floor(maxyp / pathstep), ceil(minyp / pathstep), -1):
                y = a * pathstep
                chunk.points.append((x, y, zlevel))
            chunk.points.append((minxp, minyp, zlevel))

            maxyp -= pathd

            i += 1
        if o.movement.insideout == 'INSIDEOUT':
            chunk.points.reverse()
        if (o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CW') or (
                o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CCW'):
            for si in range(0, len(chunk.points)):
                s = chunk.points[si]
                chunk.points[si] = (o.max.x + o.min.x - s[0], s[1], s[2])
        pathchunks = [chunk.to_chunk()]

    elif o.strategy == 'SPIRAL':
        chunk = camPathChunkBuilder([])
        pathd = o.dist_between_paths
        pathstep = o.dist_along_paths
        midx = (o.max.x + o.min.x) / 2
        midy = (o.max.y + o.min.y) / 2
        x = pathd / 4
        y = pathd / 4
        v = Vector((pathd / 4, 0, 0))

        # progress(x,y,midx,midy)
        e = Euler((0, 0, 0))
        pi = math.pi
        chunk.points.append((midx + v.x, midy + v.y, zlevel))
        while midx + v.x > o.min.x or midy + v.y > o.min.y:
            # v.x=x-midx
            # v.y=y-midy
            offset = 2 * v.length * pi
            e.z = 2 * pi * (pathstep / offset)
            v.rotate(e)

            v.length = (v.length + pathd / (offset / pathstep))
            # progress(v.x,v.y)
            if o.max.x > midx + v.x > o.min.x and o.max.y > midy + v.y > o.min.y:
                chunk.points.append((midx + v.x, midy + v.y, zlevel))
            else:
                pathchunks.append(chunk.to_chunk())
                chunk = camPathChunkBuilder([])
        if len(chunk.points) > 0:
            pathchunks.append(chunk.to_chunk())
        if o.movement.insideout == 'OUTSIDEIN':
            pathchunks.reverse()
        for chunk in pathchunks:
            if o.movement.insideout == 'OUTSIDEIN':
                chunk.reverse()

            if (o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CW') or (
                    o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CCW'):
                    ##TODO
                chunk.flipX(o.max.x+o.min.x)
                # for si in range(0, len(chunk.points)):
                #     s = chunk.points[si]
                #     chunk.points[si] = (o.max.x + o.min.x - s[0], s[1], s[2])

    elif o.strategy == 'CIRCLES':

        pathd = o.dist_between_paths
        pathstep = o.dist_along_paths
        midx = (o.max.x + o.min.x) / 2
        midy = (o.max.y + o.min.y) / 2
        rx = o.max.x - o.min.x
        ry = o.max.y - o.min.y
        maxr = math.sqrt(rx * rx + ry * ry)

        # progress(x,y,midx,midy)
        e = Euler((0, 0, 0))
        pi = math.pi
        chunk = camPathChunkBuilder([])
        chunk.points.append((midx, midy, zlevel))
        pathchunks.append(chunk.to_chunk())
        r = 0

        while r < maxr:
            r += pathd
            chunk = camPathChunkBuilder([])
            firstchunk = chunk
            v = Vector((-r, 0, 0))
            steps = 2 * pi * r / pathstep
            e.z = 2 * pi / steps
            laststepchunks = []
            currentstepchunks = []
            for a in range(0, int(steps)):
                laststepchunks = currentstepchunks
                currentstepchunks = []

                if o.max.x > midx + v.x > o.min.x and o.max.y > midy + v.y > o.min.y:
                    chunk.points.append((midx + v.x, midy + v.y, zlevel))
                else:
                    if len(chunk.points) > 0:
                        chunk.closed = False
                        chunk=chunk.to_chunk()
                        pathchunks.append(chunk)
                        currentstepchunks.append(chunk)
                        chunk = camPathChunkBuilder([])
                v.rotate(e)

            if len(chunk.points) > 0:
                chunk.points.append(firstchunk.points[0])
                if chunk == firstchunk:
                    chunk.closed = True
                chunk=chunk.to_chunk()
                pathchunks.append(chunk)
                currentstepchunks.append(chunk)
                chunk = camPathChunkBuilder([])
            for ch in laststepchunks:
                for p in currentstepchunks:
                    parentChildDist(p, ch, o)

        if o.movement.insideout == 'OUTSIDEIN':
            pathchunks.reverse()
        for chunk in pathchunks:
            if o.movement.insideout == 'OUTSIDEIN':
                chunk.reverse()
            if (o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CW') or (
                    o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CCW'):
                chunk.reverse()
    # pathchunks=sortChunks(pathchunks,o)not until they get hierarchy parents!
    elif o.strategy == 'OUTLINEFILL':

        polys = operation.silhouete
        pathchunks = []
        chunks = []
        for p in polys:
            p = p.buffer(-o.dist_between_paths / 10, o.optimisation.circle_detail)
            # first, move a bit inside, because otherwise the border samples go crazy very often changin between
            # hit/non hit and making too many jumps in the path.
            chunks.extend(shapelyToChunks(p, 0))

        pathchunks.extend(chunks)
        lastchunks = chunks
        firstchunks = chunks

        approxn = (min(maxx - minx, maxy - miny) / o.dist_between_paths) / 2
        i = 0

        for porig in polys:
            p = porig
            while not p.is_empty:
                p = p.buffer(-o.dist_between_paths, o.optimisation.circle_detail)
                if not p.is_empty:

                    nchunks = shapelyToChunks(p, zlevel)

                    if o.movement.insideout == 'INSIDEOUT':
                        parentChildDist(lastchunks, nchunks, o)
                    else:
                        parentChildDist(nchunks, lastchunks, o)
                    pathchunks.extend(nchunks)
                    lastchunks = nchunks
                percent = int(i / approxn * 100)
                progress('outlining polygons ', percent)
                i += 1
        pathchunks.reverse()
        if not o.inverse:  # dont do ambient for inverse milling
            lastchunks = firstchunks
            for p in polys:
                d = o.dist_between_paths
                steps = o.ambient_radius / o.dist_between_paths
                for a in range(0, int(steps)):
                    dist = d
                    if a == int(o.cutter_diameter / 2 / o.dist_between_paths):
                        if o.optimisation.use_exact:
                            dist += o.optimisation.pixsize * 0.85
                            # this is here only because silhouette is still done with zbuffer method,
                            # even if we use bullet collisions.
                        else:
                            dist += o.optimisation.pixsize * 2.5
                    p = p.buffer(dist, o.optimisation.circle_detail)
                    if not p.is_empty:
                        nchunks = shapelyToChunks(p, zlevel)
                        if o.movement.insideout == 'INSIDEOUT':
                            parentChildDist(nchunks, lastchunks, o)
                        else:
                            parentChildDist(lastchunks, nchunks, o)
                        pathchunks.extend(nchunks)
                        lastchunks = nchunks

        if o.movement.insideout == 'OUTSIDEIN':
            pathchunks.reverse()

        for chunk in pathchunks:
            if o.movement.insideout == 'OUTSIDEIN':
                chunk.reverse()
            if (o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CW') or (
                    o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CCW'):
                chunk.reverse()

        chunksRefine(pathchunks, o)
    progress(time.time() - t)
    return pathchunks


def getPathPattern4axis(operation):
    o = operation
    t = time.time()
    progress('building path pattern')
    minx, miny, minz, maxx, maxy, maxz = o.min.x, o.min.y, o.min.z, o.max.x, o.max.y, o.max.z
    pathchunks = []
    zlevel = 1  # minz#this should do layers...

    # set axes for various options, Z option is obvious nonsense now.
    if o.rotary_axis_1 == 'X':
        a1 = 0
        a2 = 1
        a3 = 2
    if o.rotary_axis_1 == 'Y':
        a1 = 1
        a2 = 0
        a3 = 2
    if o.rotary_axis_1 == 'Z':
        a1 = 2
        a2 = 0
        a3 = 1

    o.max.z = o.maxz
    # set radius for all types of operation
    radius = max(o.max.z, 0.0001)
    radiusend = o.min.z

    mradius = max(radius, radiusend)
    circlesteps = (mradius * pi * 2) / o.dist_along_paths
    circlesteps = max(4, circlesteps)
    anglestep = 2 * pi / circlesteps
    # generalized rotation
    e = Euler((0, 0, 0))
    e[a1] = anglestep

    # generalized length of the operation
    maxl = o.max[a1]
    minl = o.min[a1]
    steps = (maxl - minl) / o.dist_between_paths

    # set starting positions for cutter e.t.c.
    cutterstart = Vector((0, 0, 0))
    cutterend = Vector((0, 0, 0))  # end point for casting

    if o.strategy4axis == 'PARALLELR':

        for a in range(0, floor(steps) + 1):
            chunk = camPathChunkBuilder([])

            cutterstart[a1] = o.min[a1] + a * o.dist_between_paths
            cutterend[a1] = cutterstart[a1]

            cutterstart[a2] = 0  # radius
            cutterend[a2] = 0  # radiusend

            cutterstart[a3] = radius
            cutterend[a3] = radiusend

            for b in range(0, floor(circlesteps) + 1):
                # print(cutterstart,cutterend)
                chunk.startpoints.append(cutterstart.to_tuple())
                chunk.endpoints.append(cutterend.to_tuple())
                rot = [0, 0, 0]
                rot[a1] = a * 2 * pi + b * anglestep

                chunk.rotations.append(rot)
                cutterstart.rotate(e)
                cutterend.rotate(e)

            chunk.depth = radiusend - radius
            # last point = first
            chunk.startpoints.append(chunk.startpoints[0])
            chunk.endpoints.append(chunk.endpoints[0])
            chunk.rotations.append(chunk.rotations[0])

            pathchunks.append(chunk.to_chunk())

    if o.strategy4axis == 'PARALLEL':
        circlesteps = (mradius * pi * 2) / o.dist_between_paths
        steps = (maxl - minl) / o.dist_along_paths

        anglestep = 2 * pi / circlesteps
        # generalized rotation
        e = Euler((0, 0, 0))
        e[a1] = anglestep

        reverse = False

        for b in range(0, floor(circlesteps) + 1):
            chunk = camPathChunkBuilder([])
            cutterstart[a2] = 0
            cutterstart[a3] = radius

            cutterend[a2] = 0
            cutterend[a3] = radiusend

            e[a1] = anglestep * b

            cutterstart.rotate(e)
            cutterend.rotate(e)

            for a in range(0, floor(steps) + 1):
                cutterstart[a1] = o.min[a1] + a * o.dist_along_paths
                cutterend[a1] = cutterstart[a1]
                chunk.startpoints.append(cutterstart.to_tuple())
                chunk.endpoints.append(cutterend.to_tuple())
                rot = [0, 0, 0]
                rot[a1] = b * anglestep
                chunk.rotations.append(rot)

            chunk = chunk.to_chunk()
            chunk.depth = radiusend - radius
            pathchunks.append(chunk)

            if (reverse and o.movement.type == 'MEANDER') or (
                    o.movement.type == 'CONVENTIONAL' and o.movement.spindle_rotation == 'CW') or (
                    o.movement.type == 'CLIMB' and o.movement.spindle_rotation == 'CCW'):
                chunk.reverse()

            reverse = not reverse

    if o.strategy4axis == 'HELIX':
        print('helix')

        a1step = o.dist_between_paths / circlesteps

        chunk = camPathChunkBuilder([])  # only one chunk, init here

        for a in range(0, floor(steps) + 1):

            cutterstart[a1] = o.min[a1] + a * o.dist_between_paths
            cutterend[a1] = cutterstart[a1]
            cutterstart[a2] = 0
            cutterstart[a3] = radius
            cutterend[a3] = radiusend

            for b in range(0, floor(circlesteps) + 1):
                # print(cutterstart,cutterend)
                cutterstart[a1] += a1step
                cutterend[a1] += a1step
                chunk.startpoints.append(cutterstart.to_tuple())
                chunk.endpoints.append(cutterend.to_tuple())

                rot = [0, 0, 0]
                rot[a1] = a * 2 * pi + b * anglestep
                chunk.rotations.append(rot)

                cutterstart.rotate(e)
                cutterend.rotate(e)

            chunk=chunk.to_chunk()
            chunk.depth = radiusend - radius

        pathchunks.append(chunk)
    # print(chunk.startpoints)
    # print(pathchunks)
    # sprint(len(pathchunks))
    # print(o.strategy4axis)
    return pathchunks
