#!/usr/bin/env python3

"""
Based on 
- https://github.com/TimeTravel-0/ofsplot

ToDo's
- break apart combined paths
- option to handle groups 

Author: Mario Voigt / FabLab Chemnitz
Mail: mario.voigt@stadtfabrikanten.org
Last Patch: 10.06.2021
License: GNU GPL v3

"""

import inkex
import math
from inkex.paths import CubicSuperPath
import re
import copy
import pyclipper

class OffsetPaths(inkex.EffectExtension):
    
    def add_arguments(self, pars):
        pars.add_argument('--tab')
        pars.add_argument('--unit')
        pars.add_argument("--offset_count", type=int, default=1, help="Number of offset paths")
        pars.add_argument("--offset", type=float, default=1.000, help="Offset amount")
        pars.add_argument("--init_offset", type=float, default=0.000, help="Initial Offset Amount")
        pars.add_argument("--offset_increase", type=float, default=0.000, help="Offset increase between iterations")
        pars.add_argument("--jointype", default="2", help="Join type")
        pars.add_argument("--endtype", default="3", help="End type")
        pars.add_argument("--miterlimit", type=float, default=3.0, help="Miter limit")
        pars.add_argument("--clipperscale", type=int, default=1024, help="Scaling factor. Should be a multiplicator of 2, like 2^4=16 or 2^10=1024. The higher the scale factor the higher the quality.")
        pars.add_argument("--copy_org", type=inkex.Boolean, default=True, help="copy original path")
        pars.add_argument("--individual", type=inkex.Boolean, default=True, help="Separate into individual paths")
        pars.add_argument("--path_types", default="both", help="Process open, closed or all paths!")
        
        
    def effect(self):
        unit_factor = 1.0 / self.svg.uutounit(1.0, self.options.unit)
        pathElements = self.svg.selection.filter(inkex.PathElement).values()
        count = sum(1 for pathElement in pathElements)
        pathElements = self.svg.selection.filter(inkex.PathElement).values() #we need to call this twice because the sum function consumes the generator
        if count == 0:
            inkex.errormsg("No paths selected.")
            exit()
        for pathElement in pathElements:
            csp = CubicSuperPath(pathElement.get('d'))
            
            '''
            check for closed or open paths
            '''
            isClosed = False
            raw = pathElement.path.to_arrays()
            if raw[-1][0] == 'Z' or \
                (raw[-1][0] == 'L' and raw[0][1] == raw[-1][1]) or \
                (raw[-1][0] == 'C' and raw[0][1] == [raw[-1][1][-2], raw[-1][1][-1]]) \
                :  #if first is last point the path is also closed. The "Z" command is not required
                isClosed = True
            if self.options.path_types == "open_paths" and isClosed is True:
                continue #skip this loop iteration
            elif self.options.path_types == "closed_paths" and isClosed is False:
                continue #skip this loop iteration
                        
            scale_factor = self.options.clipperscale # 2 ** 32 = 1024 - see also https://github.com/fonttools/pyclipper/wiki/Deprecating-SCALING_FACTOR
            pco = pyclipper.PyclipperOffset(self.options.miterlimit)
            
            JT = None #join types
            if self.options.jointype == "0":
                JT = pyclipper.JT_SQUARE
            elif self.options.jointype == "1":
                JT = pyclipper.JT_ROUND
            elif self.options.jointype == "2":
                JT = pyclipper.JT_MITER
                
            ET = None #end types
            if self.options.endtype == "0":
                ET = pyclipper.ET_CLOSEDPOLYGON
            elif self.options.endtype == "1":
                ET = pyclipper.ET_CLOSEDLINE
            elif self.options.endtype == "2":
                ET = pyclipper.ET_OPENBUTT
            elif self.options.endtype == "3":
                ET = pyclipper.ET_OPENSQUARE
            elif self.options.endtype == "4":                 
                ET = pyclipper.ET_OPENROUND
            
            newPaths = []

            # load in initial paths
            for subPath in csp:
                sub_simple = []
                for item in subPath:
                    itemx = [float(z) * scale_factor for z in item[1]]
                    sub_simple.append(itemx)
                pco.AddPath(sub_simple, JT, ET)

            # calculate offset paths for different offset amounts
            offset_list = []
            offset_list.append(self.options.init_offset * unit_factor)
            for i in range(0, self.options.offset_count):
                ofs_increase = +math.pow(float(i) * self.options.offset_increase * unit_factor, 2)
                if self.options.offset_increase < 0:
                    ofs_increase = -ofs_increase
                offset_list.append(offset_list[0] + float(i) * self.options.offset * unit_factor + ofs_increase * unit_factor)

            solutions = []
            for offset in offset_list:
                solution = pco.Execute(offset * scale_factor)
                solutions.append(solution)
                if len(solution) <= 0:
                    continue # no more loops to go, will provide no results.

            # re-arrange solutions to fit expected format & add to array
            for solution in solutions:
                for sol in solution:
                    solx = [[float(s[0]) / scale_factor, float(s[1]) / scale_factor] for s in sol]
                    sol_p = [[a, a, a] for a in solx]
                    sol_p.append(sol_p[0][:])
                    if sol_p not in newPaths:
                        newPaths.append(sol_p)

            if self.options.individual is True:
                parentGroup = pathElement.getparent().add(inkex.Group(id="g-offset-{}".format(pathElement.attrib["id"])))
                parent = pathElement.getparent()
                idx = parent.index(pathElement) + 1
                idSuffix = 0
                for newPath in newPaths:
                    copyElement = copy.copy(pathElement)
                    elementId = copyElement.get('id')
                    copyElement.path = CubicSuperPath(newPath)
                    copyElement.set('id', elementId + str(idSuffix))
                    parentGroup.append(copyElement)
                    idSuffix += 1
                parent.insert(idx, parentGroup)
                if self.options.copy_org is False:
                    pathElement.delete()
            else:
                if self.options.copy_org is True:
                    for subPath in csp:
                        newPaths.append(subPath)
                pathElement.set('d', CubicSuperPath(newPaths))

if __name__ == '__main__':
    OffsetPaths().run()