# blender CAM curvecamequation.py (c) 2021 Alain Pelletier
#
# ***** 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 3
# 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 bpy
from bpy.props import *

from cam import utils, parametric
import math
from Equation import Expression
import numpy as np


class CamSineCurve(bpy.types.Operator):
    """Object sine """  # by Alain Pelletier april 2021
    bl_idname = "object.sine"
    bl_label = "Create Periodic wave"
    bl_options = {'REGISTER', 'UNDO', 'PRESET'}

    #    zstring: StringProperty(name="Z equation", description="Equation for z=F(u,v)", default="0.05*sin(2*pi*4*t)" )
    axis: bpy.props.EnumProperty(name="displacement axis", items=(
        ('XY', 'Y to displace X axis', 'Y constant; X sine displacement'),
        ('YX', 'X to displace Y axis', 'X constant; Y sine displacement'),
        ('ZX', 'X to displace Z axis', 'X constant; Y sine displacement'),
        ('ZY', 'Y to displace Z axis', 'X constant; Y sine displacement')), default='ZX')
    wave: bpy.props.EnumProperty(name="Wave", items=(
        ('sine', 'Sine Wave', 'Sine Wave'),
        ('triangle', 'Triangle Wave', 'triangle wave'),
        ('cycloid', 'Cycloid', 'Sine wave rectification'),
        ('invcycloid', 'Inverse Cycloid', 'Sine wave rectification')), default='sine')
    amplitude: bpy.props.FloatProperty(name="Amplitude", default=.01, min=0, max=10, precision=4, unit="LENGTH")
    period: bpy.props.FloatProperty(name="Period", default=.5, min=0.001, max=100, precision=4, unit="LENGTH")
    beatperiod: bpy.props.FloatProperty(name="Beat Period offset", default=0.0, min=0.0, max=100, precision=4,
                                        unit="LENGTH")
    shift: bpy.props.FloatProperty(name="phase shift", default=0, min=-360, max=360, precision=4, unit="ROTATION")
    offset: bpy.props.FloatProperty(name="offset", default=0, min=-1.0, max=1, precision=4, unit="LENGTH")
    iteration: bpy.props.IntProperty(name="iteration", default=100, min=50, max=2000)
    maxt: bpy.props.FloatProperty(name="Wave ends at x", default=0.5, min=-3.0, max=3, precision=4, unit="LENGTH")
    mint: bpy.props.FloatProperty(name="Wave starts at x", default=0, min=-3.0, max=3, precision=4, unit="LENGTH")
    wave_distance: bpy.props.FloatProperty(name="distance between multiple waves", default=0.0, min=0.0, max=100,
                                           precision=4, unit="LENGTH")
    wave_angle_offset: bpy.props.FloatProperty(name="angle offset for multiple waves", default=math.pi/2,
                                               min=-200*math.pi, max=200*math.pi, precision=4, unit="ROTATION")
    wave_amount: bpy.props.IntProperty(name="amount of multiple waves", default=1, min=1, max=2000)

    def execute(self, context):

        # z=Asin(B(x+C))+D
        if self.wave == 'sine':
            zstring = ssine(self.amplitude, self.period, dc_offset=self.offset, phase_shift=self.shift)
            if self.beatperiod != 0:
                zstring += "+"+ssine(self.amplitude, self.period+self.beatperiod, dc_offset=self.offset,
                                     phase_shift=self.shift)
        elif self.wave == 'triangle':  # build triangle wave from fourier series
            zstring = str(round(self.offset, 6)) + "+(" + str(triangle(80, self.period, self.amplitude))+")"
            if self.beatperiod != 0:
                zstring += '+' + str(triangle(80, self.period+self.beatperiod, self.amplitude))
        elif self.wave == 'cycloid':
            zstring = "abs("+ssine(self.amplitude, self.period, dc_offset=self.offset, phase_shift=self.shift)+")"
        elif self.wave == 'invcycloid':
            zstring = "-1*abs("+ssine(self.amplitude, self.period, dc_offset=self.offset, phase_shift=self.shift)+")"

        print(zstring)
        e = Expression(zstring, ["t"])  # make equation from string

        # build function to be passed to create parametric curve ()
        def f(t, offset: float = 0.0, angle_offset: float = 0.0):
            if self.axis == "XY":
                c = (e(t+angle_offset)+offset, t, 0)
            elif self.axis == "YX":
                c = (t, e(t+angle_offset)+offset, 0)
            elif self.axis == "ZX":
                c = (t, offset, e(t+angle_offset))
            elif self.axis == "ZY":
                c = (offset, t, e(t+angle_offset))
            return c

        for i in range(self.wave_amount):
            angle_off = self.wave_angle_offset*self.period*i/(2*math.pi)
            parametric.create_parametric_curve(f, offset=self.wave_distance*i, min=self.mint, max=self.maxt,
                                               use_cubic=True, iterations=self.iteration, angle_offset=angle_off)

        return {'FINISHED'}


class CamLissajousCurve(bpy.types.Operator):
    """Lissajous """  # by Alain Pelletier april 2021
    bl_idname = "object.lissajous"
    bl_label = "Create Lissajous figure"
    bl_options = {'REGISTER', 'UNDO', 'PRESET'}

    amplitude_A: bpy.props.FloatProperty(name="Amplitude A", default=.1, min=0, max=100, precision=4, unit="LENGTH")
    waveA: bpy.props.EnumProperty(name="Wave X", items=(
        ('sine', 'Sine Wave', 'Sine Wave'),
        ('triangle', 'Triangle Wave', 'triangle wave')), default='sine')

    amplitude_B: bpy.props.FloatProperty(name="Amplitude B", default=.1, min=0, max=100, precision=4, unit="LENGTH")
    waveB: bpy.props.EnumProperty(name="Wave Y", items=(
        ('sine', 'Sine Wave', 'Sine Wave'),
        ('triangle', 'Triangle Wave', 'triangle wave')), default='sine')
    period_A: bpy.props.FloatProperty(name="Period A", default=1.1, min=0.001, max=100, precision=4, unit="LENGTH")
    period_B: bpy.props.FloatProperty(name="Period B", default=1.0, min=0.001, max=100, precision=4, unit="LENGTH")
    period_Z: bpy.props.FloatProperty(name="Period Z", default=1.0, min=0.001, max=100, precision=4, unit="LENGTH")
    amplitude_Z: bpy.props.FloatProperty(name="Amplitude Z", default=0.0, min=0, max=100, precision=4, unit="LENGTH")
    shift: bpy.props.FloatProperty(name="phase shift", default=0, min=-360, max=360, precision=4, unit="ROTATION")

    iteration: bpy.props.IntProperty(name="iteration", default=500, min=50, max=10000)
    maxt: bpy.props.FloatProperty(name="Wave ends at x", default=11, min=-3.0, max=1000000, precision=4, unit="LENGTH")
    mint: bpy.props.FloatProperty(name="Wave starts at x", default=0, min=-10.0, max=3, precision=4, unit="LENGTH")

    def execute(self, context):
        # x=Asin(at+delta ),y=Bsin(bt)

        if self.waveA == 'sine':
            xstring = ssine(self.amplitude_A, self.period_A, phase_shift=self.shift)
        elif self.waveA == 'triangle':
            xstring = str(triangle(100, self.period_A, self.amplitude_A))

        if self.waveB == 'sine':
            ystring = ssine(self.amplitude_B, self.period_B)

        elif self.waveB == 'triangle':
            ystring = str(triangle(100, self.period_B, self.amplitude_B))

        zstring = ssine(self.amplitude_Z, self.period_Z)

        print("x= " + str(xstring))
        print("y= " + str(ystring))
        x = Expression(xstring, ["t"])  # make equation from string
        y = Expression(ystring, ["t"])  # make equation from string
        z = Expression(zstring, ["t"])

        # build function to be passed to create parametric curve ()
        def f(t, offset: float = 0.0):
            c = (x(t), y(t), z(t))
            return c

        parametric.create_parametric_curve(f, offset=0.0, min=self.mint, max=self.maxt, use_cubic=True,
                                           iterations=self.iteration)

        return {'FINISHED'}


class CamHypotrochoidCurve(bpy.types.Operator):
    """hypotrochoid """  # by Alain Pelletier april 2021
    bl_idname = "object.hypotrochoid"
    bl_label = "Create Spirograph type figure"
    bl_options = {'REGISTER', 'UNDO', 'PRESET'}

    typecurve: bpy.props.EnumProperty(name="type of curve", items=(
        ('hypo', 'Hypotrochoid', 'inside ring'), ('epi', 'Epicycloid', 'outside inner ring')))
    R: bpy.props.FloatProperty(name="Big circle radius", default=0.25, min=0.001, max=100, precision=4, unit="LENGTH")
    r: bpy.props.FloatProperty(name="Small circle radius", default=0.18, min=0.0001, max=100, precision=4,
                               unit="LENGTH")
    d: bpy.props.FloatProperty(name="distance from center of interior circle", default=0.050, min=0, max=100,
                               precision=4, unit="LENGTH")
    dip: bpy.props.FloatProperty(name="variable depth from center", default=0.00, min=-100, max=100, precision=4)

    def execute(self, context):
        r = round(self.r, 6)
        R = round(self.R, 6)
        d = round(self.d, 6)
        Rmr = round(R - r, 6)  # R-r
        Rpr = round(R + r, 6)  # R +r
        Rpror = round(Rpr / r, 6)  # (R+r)/r
        Rmror = round(Rmr / r, 6)  # (R-r)/r
        maxangle = 2 * math.pi * ((np.lcm(round(self.R * 1000), round(self.r * 1000)) / (R * 1000)))

        if self.typecurve == "hypo":
            xstring = str(Rmr) + "*cos(t)+" + str(d) + "*cos(" + str(Rmror) + "*t)"
            ystring = str(Rmr) + "*sin(t)-" + str(d) + "*sin(" + str(Rmror) + "*t)"
        else:
            xstring = str(Rpr) + "*cos(t)-" + str(d) + "*cos(" + str(Rpror) + "*t)"
            ystring = str(Rpr) + "*sin(t)-" + str(d) + "*sin(" + str(Rpror) + "*t)"

        zstring = '(' + str(round(self.dip, 6)) + '*(sqrt(((' + xstring + ')**2)+((' + ystring + ')**2))))'

        print("x= " + str(xstring))
        print("y= " + str(ystring))
        print("z= " + str(zstring))
        print("maxangle " + str(maxangle))

        x = Expression(xstring, ["t"])  # make equation from string
        y = Expression(ystring, ["t"])  # make equation from string
        z = Expression(zstring, ["t"])  # make equation from string
        # build function to be passed to create parametric curve ()

        def f(t, offset: float = 0.0):
            c = (x(t), y(t), z(t))
            return c

        iter = int(maxangle * 10)
        if iter > 10000:  # do not calculate more than 10000 points
            print("limiting calculations to 10000 points")
            iter = 10000
        parametric.create_parametric_curve(f, offset=0.0, min=0, max=maxangle, use_cubic=True, iterations=iter)

        return {'FINISHED'}


class CamCustomCurve(bpy.types.Operator):
    """Object customCurve """  # by Alain Pelletier april 2021
    bl_idname = "object.customcurve"
    bl_label = "Create custom curve"
    bl_options = {'REGISTER', 'UNDO', 'PRESET'}

    xstring: StringProperty(name="X equation", description="Equation x=F(t)", default="t")
    ystring: StringProperty(name="Y equation", description="Equation y=F(t)", default="0")
    zstring: StringProperty(name="Z equation", description="Equation z=F(t)", default="0.05*sin(2*pi*4*t)")

    iteration: bpy.props.IntProperty(name="iteration", default=100, min=50, max=2000)
    maxt: bpy.props.FloatProperty(name="Wave ends at x", default=0.5, min=-3.0, max=10, precision=4, unit="LENGTH")
    mint: bpy.props.FloatProperty(name="Wave starts at x", default=0, min=-3.0, max=3, precision=4, unit="LENGTH")

    def execute(self, context):
        print("x= " + self.xstring)
        print("y= " + self.ystring)
        print("z= " + self.zstring)
        ex = Expression(self.xstring, ["t"])  # make equation from string
        ey = Expression(self.ystring, ["t"])  # make equation from string
        ez = Expression(self.zstring, ["t"])  # make equation from string

        # build function to be passed to create parametric curve ()
        def f(t, offset: float = 0.0):
            c = (ex(t), ey(t), ez(t))
            return c

        parametric.create_parametric_curve(f, offset=0.0, min=self.mint, max=self.maxt, use_cubic=True,
                                           iterations=self.iteration)

        return {'FINISHED'}


def triangle(i, T, A):
    s = str(A*8/(math.pi**2))+'*('
    for n in range(i):
        if n % 2 != 0:
            e = (n-1)/2
            a = round(((-1)**e)/(n**2), 8)
            b = round(n*math.pi/(T/2), 8)
            if n > 1:
                s += '+'
            s += str(a) + "*sin("+str(b)+"*t) "
    s += ')'
    return s


def ssine(A, T, dc_offset=0, phase_shift=0):
    return str(round(dc_offset, 6)) + "+" + str(round(A, 6)) + "*sin((2*pi/" + str(
        round(T, 6)) + ")*(t+" + str(round(phase_shift, 6)) + "))"
