# Copyright (C) 2024-2025, Quinton Wiebe
# License: Sustainable Use License 1.0
import ezdxf
from ezdxf import units
from math import pi
from prettytable import PrettyTable
import matplotlib.pyplot as plt
from datetime import datetime
import numpy as np
import json

SOFTWARE_NAME = 'TubeCutterDxf' 
VERSION = '0.1.2'

PRECISION = 0.000001
ROUND = 6

MM = 1
M = 1000
INCH = 25.4
FEET = INCH * 12

def overflow(val, max=2*pi):
    temp = val % max
    
    return temp if temp >= 0 else temp + max

class CutSpiral:
    def __init__(self, OD, offsetX, offsetA, cutLength, unCutLength, pitch, instances, variableCutLength=False, cutIncrease=0, unCutIncrease=0, continuous=False, CW=True):
        self.type = 'CutSpiral'
        self.OD = OD
        self.offsetX = offsetX
        self.offsetA = offsetA
        self.cutLength = cutLength
        self.unCutLength = unCutLength
        self.variableCutLength = variableCutLength
        self.cutIncrease = cutIncrease
        self.unCutIncrease = unCutIncrease
        self.pitch = pitch
        self.instances = instances
        self.continuous = continuous

        self.lines = list()

        # Normalized from 0 to 1
        # offsetA_n = round((overflow(self.offsetA, 360) if not continuous else self.offsetA) / 360, ROUND)
        offsetA_n = round(self.offsetA / 360, ROUND)
        cutLength_n = round(self.cutLength / 360, ROUND)
        cutSpace_n = round(self.unCutLength / 360, ROUND)
        cutIncrease_n = round(self.cutIncrease / 360, ROUND)
        cutSpaceIncrease_n = round(self.unCutIncrease / 360, ROUND)

        # Normalized from 0 to c (the circumference of the tube)
        self.c = round(self.OD * pi, ROUND)
        offsetA_c = round(self.c * offsetA_n, ROUND)
        cutLength_c = round(self.c * cutLength_n, ROUND)
        cutSpace_c = round(self.c * cutSpace_n, ROUND)
        cutIncrease_c = round(self.c * cutIncrease_n, ROUND)
        cutSpaceIncrease_c = round(self.c * cutSpaceIncrease_n, ROUND)

        x_d = (1 if CW else -1)
        y_d = (1 if pitch > 0 else -1) * x_d

        x_start = round(offsetX, ROUND)
        x_end = round(x_start + cutLength_n * pitch, ROUND)
        y_start = round(offsetA_c, ROUND)
        # y_end = (y_start + cutLength_c) % self.c

        y_end = y_start + cutLength_c * y_d
        if not continuous:
            y_start = overflow(y_start, self.c)
            y_end = overflow(y_end, self.c)

        # y_end = round((y_start + cutLength_c * d) % (self.c if not continuous else 1), ROUND)

        for i in range(0, instances):
            '''
            Use Right Hand rule with thumb pointing distally (-x direction; or to the left)
            / == Clockwise (CW)
            \ == Counterclockwise (CCW, CW ==)
            S / E - y_s < y_e & x_s < x_e --- wrap around: y_s > y_e & x_s < x_e
            E / S - y_s > y_e & x_s > x_e --- wrap around: y_s < y_e & x_s > x_e
            S \ E - y_s > y_e & x_s < x_e --- wrap around: y_s < y_e & x_s < x_e
            E \ S - y_s < y_e & x_s > x_e --- wrap around: y_s > y_e & x_s > x_e

            Y = y_s < y_e
            X = x_s < x_e
            WA = wrap around necessary

            TYPE  | CW | Y | X | WA | - [(X XOR Y) XOR CW]
            S / E   T    T   T   F    F      F      T
            E / S   T    T   F   T    T      T      F
            S / E   T    F   T   T    T      T      F
            E / S   T    F   F   F    F      F      T
            S \ E   F    T   T   T    T      F      F
            E \ S   F    T   F   F    F      T      T
            S \ E   F    F   T   F    F      T      T
            E \ S   F    F   F   T    T      F      F
            
            '''
            # if ((y_start > y_end) and not continuous):
            if (not (((y_start < y_end) != (x_start < x_end)) != CW)):
                if y_d == 1:
                    percentOver = y_end / cutLength_c
                    y_end_temp = self.c
                    y_start_temp = 0
                elif y_d == -1:
                    percentOver = (self.c - y_end) / cutLength_c
                    y_end_temp = 0
                    y_start_temp = self.c

                x_end_temp = x_end - (x_end - x_start) * percentOver

                if (abs(y_end - y_start) > PRECISION):
                    # if y_end > 3.14:
                    #     print(i)
                    self.lines.append(((x_start, y_start), (x_end_temp, y_end_temp)))
            
                x_start = x_end_temp
                y_start = y_start_temp
            
            if (abs(y_end - y_start) > PRECISION):
                # if y_end > 3.14:
                #     print(i)
                self.lines.append(((x_start, y_start), (x_end, y_end)))

            if (self.variableCutLength):
                cutLength_n += cutIncrease_n
                cutLength_c = self.c * cutLength_n

                cutSpace_n += cutSpaceIncrease_n
                cutSpace_c = self.c * cutSpace_n

            x_start = x_end + cutSpace_n * pitch
            x_end = x_start + cutLength_n * pitch
            y_start = y_end + cutSpace_c * y_d
            y_end = y_start + cutLength_c * y_d

            if (not continuous):
                y_start = round(overflow(y_start, self.c), ROUND)
                y_end = round(overflow(y_end, self.c), ROUND)
                # y_start = round(y_start % self.c, ROUND)
                # y_end = round(y_end % self.c, ROUND)
        
        self.xNext = x_start
        self.yNext = y_start
    
    def dumpConfig(self):
        return {
            'type': self.type,
            'OD': self.OD,
            'offsetX': self.offsetX,
            'offsetA': self.offsetA,
            'cutLength': self.cutLength,
            'unCutLength': self.unCutLength,
            'variableCutLength': self.variableCutLength,
            'cutIncrease': self.cutIncrease,
            'unCutIncrease': self.unCutIncrease,
            'pitch': self.pitch,
            'instances': self.instances,
            'continuous': self.continuous
        }
    
    def getLines(self):
        return self.lines

    def plot(self):
        color = np.random.rand(3)
        for start, end in self.lines:
            plt.plot([start[0], end[0]], [start[1], end[1]], color=color)

    def draw(self, doc):
        msp = doc.modelspace()

        for start, end in self.lines:
            msp.add_line(start, end, dxfattribs={"layer": "MyLayer"})
            # print("start: ", start)
            # print("end ", end, "\n")
        
        return len(self.lines)

class CutBrick:
    def __init__(self, OD, offsetX, offsetA, cutLength, numRadialCuts, spacingA, pitch, instances, variableCutLength=False, cutIncrease=0, variablePitch=False, pitchIncrease=0, continuous=False):
        self.type = 'CutBrick'
        self.OD = OD
        self.offsetX = offsetX
        self.offsetA = offsetA
        self.cutLength = cutLength
        self.variableCutLength = variableCutLength
        self.cutIncrease = cutIncrease
        self.numRadialCuts = numRadialCuts
        self.spacingA = spacingA
        self.pitch = pitch
        self.variablePitch = variablePitch
        self.pitchIncrease = pitchIncrease
        self.instances = instances
        self.continuous = continuous

        self.cutSpace = (360 / self.numRadialCuts) - self.cutLength

        self.c = self.OD * pi
        self.offsetA_c = self.offsetA / 360 * self.c
        self.cutLength_c = self.cutLength / 360 * self.c
        self.cutSpace_c = self.cutSpace / 360 * self.c
        self.cutIncrease_c = self.cutIncrease / 360 * self.c
        self.spacingA_c = self.spacingA / 360 * self.c
        
        pitch = self.pitch

        self.lines = []

        x = self.offsetX

        y_start = self.offsetA_c
        # y_end = (y_start + self.cutLength_c) % self.c
        y_end = y_start + self.cutLength_c

        if (not continuous):
            y_end = y_end % self.c

        for i in range(0, self.instances):
            for j in range(0, self.numRadialCuts):

                if y_start > y_end:
                    self.lines.append(((x, y_start), (x, self.c)))
                    y_start = 0
                
                self.lines.append(((x, y_start), (x, y_end)))

                y_start = y_end + self.cutSpace_c
                y_end = y_start + self.cutLength_c

                if (not self.continuous):
                    y_start = y_start % self.c
                    y_end = y_end % self.c
                
            if (self.variableCutLength):
                # self.cutLength += self.cutIncrease
                # self.cutLength_c = self.cutLength_c = self.cutLength / 360 * self.c

                # self.cutSpace = (360 / self.numRadialCuts) - self.cutLength
                # self.cutSpace_c = self.cutSpace_c = self.cutSpace / 360 * self.c
                self.cutLength_c += self.cutIncrease_c
                self.cutSpace_c = (self.c / self.numRadialCuts) - self.cutLength_c
            
            x += pitch

            y_start = y_start + self.spacingA_c
            y_end = y_start + self.cutLength_c

            if (not self.continuous):
                y_start = y_start % self.c
                y_end = y_end % self.c
            
            if (self.variablePitch):
                pitch += self.pitchIncrease
        
        self.xNext = x
        self.yNext = y_start
    
    def dumpConfig(self):
        return {
            'type': self.type,
            'OD': self.OD,
            'offsetX': self.offsetX,
            'offsetA': self.offsetA,
            'cutLength': self.cutLength,
            'variableCutLength': self.variableCutLength,
            'cutIncrease': self.cutIncrease,
            'numRadialCuts': self.numRadialCuts,
            'spacingA': self.spacingA,
            'pitch': self.pitch,
            'variablePitch': self.variablePitch,
            'instances': self.instances,
            'continuous': self.continuous,
            'xNext': self.xNext,
            'yNext': self.yNext,
        }
    
    def getLines(self):
        return self.lines

    def plot(self):
        for start, end in self.lines:
            plt.plot([start[0], end[0]], [start[1], end[1]])

    def draw(self, doc):
        msp = doc.modelspace()

        for start, end in self.lines:
            msp.add_line(start, end, dxfattribs={"layer": "MyLayer"})
            # print("start: ", start)
            # print("end ", end, "\n")
        
        return len(self.lines)
    
class CutPartline:
    def __init__(self, OD, offsetX = 0):
        self.type = 'CutPartline'
        self.OD = OD
        self.c = OD * pi
        self.offsetX = offsetX
        self.lines = [((self.offsetX, 0), (self.offsetX, self.c))]
    
    def dumpConfig(self):
        return {
            'type': self.type,
            'OD': self.OD,
            'offsetX': self.offsetX,
        }
    
    def getLines(self):
        return self.lines

    def plot(self):
        for start, end in self.lines:
            plt.plot([start[0], end[0]], [start[1], end[1]])

    def draw(self, doc):
        msp = doc.modelspace()

        for start, end in self.lines:
            msp.add_line(start, end, dxfattribs={"layer": "MyLayer"})

        return len(self.lines)

class CutPattern:
    def __init__(self):
        self._cuts = []
        self._dxf = ezdxf.new()
        self._dxf.units = units.MM

    def add(self, cut):
        self._cuts.append(cut)

    def remove(self, cutIdx):
        self._cuts.pop(cutIdx)

    def draw(self):
        linecount = 0
        for cut in self._cuts:
            linecount += cut.draw(self._dxf)
        
        print(f'Drew {linecount} lines')
        return linecount

    def save(self, filename):
        # Save DXF
        try:
            self._dxf.saveas(f'./output/{filename}.dxf')
        except Exception as e:
            print(e)
            return

        # Save Config File
        header = {SOFTWARE_NAME: {'Version': VERSION, 'Date': str(datetime.now())}}
        data = {}
        i = 0
        for cut in self._cuts:
            data[cut.type + str(i)] = cut.dumpConfig()
            i += 1
        
        header['config'] = data

        with open(f'./output/{filename}.json', 'w') as f:
            json.dump(header, f)
        
        print(f'Saved {filename}')

    def getCuts(self):
        return self._cuts
    
    def preview(self):
        self.plot()
        plt.show()

    def plot(self):
        for cut in self._cuts:
            cut.plot()

    def printCutTable(self):
        table = PrettyTable(['Cut #', 'Start X', 'Instances', 'Pitch'])
        table.align['Start X'] = 'l'
        table.align['Instances'] = 'r'
        table.align['Pitch'] = 'r'

        i = 1
        for cut in self.getCuts():
            offsetX = cut.offsetX or ''
            instances = cut.instances or ''
            pitch = cut.pitch or ''

            table.add_row([i, offsetX, instances, pitch])
            i += 1

        print(table)