# -*- coding: utf-8 -*-

'''
Created on 27.07.2020

@author: kurz
'''
import os
import sys
import json
from collections import OrderedDict
from datetime import datetime
# python version
PY2  = sys.version_info[0] == 2
PY3  = sys.version_info[0] == 3
PY34 = sys.version_info[0:2] >= (3, 4)
PY37 = sys.version_info[0:2] >= (3, 7)
PY38 = sys.version_info[0:2] >= (3, 8)
if PY2:
    from codecs import open

fse = sys.getfilesystemencoding()       # Filesystem encoding

"""
GEOMETRY

In the dynamic job concept the GEOMETRY is defined as toolpath without any offset-correction nor splitting the toolpath into single cuts, entry / exit path or glue-stop distance.
The GEOMETRY represents 1:1 the designed CAD-shape. 
All floats for length and angles must be rounded to the 6th decimal.
The GEOMETRY definition will be stored in the MACHINING Objects as "M_Geometry" string value or as "M_Geometry_Pocketing" string value.To predefine the entry / exit motion, it is necessary to know the entry / exit element number and store this information into the M_Startpoint... / M_Endpoint... values.
"""
UNIQUA_LANGUAGE = "eng"      # eng, deu, fra, ...
DYNAMIC_JSON, SEQUENTIAL_JSON = 0, 1
UNIQUA_GEOMETRY_HEADER = """//UNIQUA high level ISO V1.1\r\n"""
UNIQUA_SEQUENTIAL_GEOMETRY_HEADER = """// UNIQUA Structured ISO V1.0\r\n"""
MINIMALDBVERSION = ""           # "1.0.2"
# Batch generation strategies
(NOTDEFINED, EARLYMINIMUMTHREADING, EARLYMINIMUMOPERATORTIME, LATE, CUSTOMGENERATIONSTRATEGYIDENTIFIER) = (0, 1, 2, 3, 4)
UNIQUA_STRATEGY_NAMES = [u"Keine", u"Frühe Mindesteinfädelung", u"Frühe Mindestbedienzeit",  u"Spät", u"Benutzerdefinierte Strategie"]
#UNIQUA_STRATEGY_NAMES = [u"Not defined",     u"EarlyMinimumThreading",    u"EarlyMinimumOperatorTime", u"Late", u"custum generation strategy identifier"]

# Workpiece Materials (accepted by simulator/machine)
WPMATERIALS = ['Steel', 'Copper', 'Brass', 'Graphite', 'Hard Metal', 'Alluminium', 'Titanium', 'PCD_CTB010']
# Wire Materials (accepted by simulator/machine)
WIRENAMES = ['AC Cut A 900',  'AC Cut D 800', 'AC Brass 900', 'AC Cut AH', 'AC Cut SP', 'AC Cut D 500', 'AC Brass 500', 'AC Cut Molybdenum', 'AC Cut VS+ 900', 'AC Cut VH', 'AC Cut XS', 'AC Cut XCC', 'AC Cut Micro SP-Z']

_jsonObject = None          # global name of JsonBase class object : MUST SURVIVE WHOLE TIME OF PROCESSING -> AND IS TO BE DESTROYED AT THE END
#
(ROUGH, BRIDGE, TRIM, RESERVE) = (0, 1, 2, 3)   # Extensions for separate files output
_jsonObjectONE  = None
_jsonObjectTWO  = None
_jsonObjectTHREE = None
_jsonObjectFOUR  = None

lOperationList = []
lGeomList      = []
i_debug        = 0
sPrvOperation  = ""
sPrvGeom       = ""

#----------------------------------- Wrapper functions --------------------------------------------------------------------------------------
indentChars = '    {}'      # provides more pretty prints

class UniquaVersion(str):
    """ handles version strings like "0.9.5"
    and provide operators for easy comparing
    """
    def __lt__(self, other):    # lessthan (self.version < other)
        return self.compareVersion(self, other) in [-1]
    def __le__(self, other):    # lessthan or equal (self.version <= other)
        return self.compareVersion(self, other) in [0, -1]
    # def __eq__(self, other):    # equal (self.version == other)
    #     return self.compareVersion(self, other) in [0]
    # def __ne__(self, other):    # not equal (self.version != other)
    #     return self.compareVersion(self, other) not in [0]
    def __gt__(self, other):    # greater than (self.version > other)
        return self.compareVersion(self, other) in [1]
    def __ge__(self, other):    # greater than or equal (self.version >= other)
        return self.compareVersion(self, other) in [0, 1]

    def compareVersion(self, version1, version2):
        versions1 = [int(v) for v in version1.split(".")]
        versions2 = [int(v) for v in version2.split(".")]
        for i in range(max(len(versions1),len(versions2))):
            v1 = versions1[i] if i < len(versions1) else 0
            v2 = versions2[i] if i < len(versions2) else 0
            if v1 > v2:
                return 1    # greater
            elif v1 < v2:
                return -1   # smaller
        return 0            # equal

# GF Version Changelog (file:///C:/GFMS/UniquaV2/Uniqua2build12705pack/Documentation/Uniqua%20JSON%20Post%20Processor%20Changelog.pdf)
UNIQUA_CHANGELOG = """\
0.9.5   ?
0.9.6   ?
0.9.6.1 Require Uniqua 1.1.5 (available on May 2021), build 11423 or upper
0.9.7   Require Uniqua 2.0.0, build 10xxx or upper 
0.9.9   Require Uniqua 2.0.0 Q2 2021 (available on end of june 2021), build 11938 or upper
0.9.10  Require Uniqua 2.0.0 Q2 2021 (available middle of july 2021), build 12398 or upper 
0.9.11  Require Uniqua 2.0.0 Q2 2021 (available end of august 2021), build 12705 or upper 
0.9.12  Require Uniqua 2.?.?
0.9.13  Require Uniqua 2.?.?
1.0.0   Require Uniqua 2.1.0, build 15096 or upper
1.1.0   Require Uniqua ?.?.?
1.2.0   Require Uniqua ?.?.?
1.2.1   Require Uniqua 2.6.0, build 19762 or upper
1.3.0   Require Uniqua 2.7.0, build 20231 or upper
1.3.0   Require Uniqua 2.7.0.2, build 21332 or upper (26.09.2024)
1.3.0   Require Uniqua 2.8.0.0, build 22698 or upper (27.02.2025)
1.5.0   Require Uniqua 2.8.7.0, build 25199 or upper (28.10.2025)
1.5.0   Require Uniqua 2.8.8.0, build 25411 or upper
"""
UNIQUA_VERSION_LIST = ['0.9.5', '0.9.6', '0.9.6.1', '0.9.7', '0.9.9', '0.9.10', '0.9.11', '0.9.12', '0.9.13', '1.0.0', '1.1.0', '1.2.0', '1.2.1' , '1.3.0' , '1.5.0' , '9.9.9']
UNIQUA_VERSION = UniquaVersion(UNIQUA_VERSION_LIST[0])      # oldest
#UNIQUA_VERSION = UniquaVersion(UNIQUA_VERSION_LIST[-1])     # newest
UNIQUA_OPERATION_MODE = DYNAMIC_JSON



class OperationListItem(list):
    def __init__(self, name ):
        self.name = name

    def append(self,item):
        list.append(self, item +r"\n") 
        #append the item to itself (the list) 

class GeomListItem(list):
    def __init__(self, name ):
        self.name = name

    def append(self,item):
        list.append(self, item +r"\n") 
        #append the item to itself (the list)  
  
#  *****  OperationList ******  
def jOperationListAppendLine(OperationName,Line):
    global lOperationList
    global sPrvOperation
    
    if OperationName == "" and sPrvOperation != "":
        OperationName = sPrvOperation
        
    if OperationName == "" :
        print("Append Line In List. No Operation Name given!<" + Line + ">")
        return
    

    for it in lOperationList:
        if it.name == OperationName :
            sPrvOperation = it.name
            it.append(Line)
            break

    else :
        lOperationList.append(OperationListItem (name = OperationName))
        for it in lOperationList:
            if it.name == OperationName:
                sPrvOperation = it.name
                it.append(Line)
                
  
def jOperationList_Debug():
    global lOperationList

    iOperations = 0
    
    print ("")
    print ("------ OPERATION DATA START -------")
    for it in lOperationList:
        if it.name != "" :
            iOperations = iOperations + 1
            print("")
            print("------" + str(iOperations) + '. ' + it.name +"-------")
            for iCounter , iLines in enumerate(it):
                print(str(iCounter+1) + ". ---> " + iLines )
        


    if iOperations==0:
        print("**** No Operation Data ****") 
 

    print("")
    print ("------ OPERATION DATA END -------")   

def jOperationList_Write():
  global lOperationList
  
  Params = {}
  
  for it in lOperationList:
        if it.name != "" :
            Params['SO_Name'] = it.name    

            s_Value = ""
            for iCounter , iLines in enumerate(it):
                s_Value = s_Value + iLines.rstrip().replace('\\n', '\n')

            Params['SO_Code'] = s_Value  
            jaddsequenceoperation(Params, autocount=False)
            
def jOperationList_Destroy():
    global lOperationList
    lOperationList.clear()

def jOperationList_Clear():
    global lOperationList
    lOperationList.clear()            
            
            
# ----------------------  Geom list -------------------------            
        
        
def jGeomListAppendLine(GeomName,Line):
    global lGeomList
    global sPrvGeom
    
    if GeomName == "" and sPrvGeom != "":
        GeomName = sPrvGeom
        
    if GeomName == "" :
        print("Append Line In List. No Geom Name given!<" + GeomName + ">")
        return
    

    for it in lGeomList:
        if it.name == GeomName :
            sPrvGeom = it.name
            it.append(Line)
            break

    else :
        lGeomList.append(GeomListItem (name = GeomName))
        for it in lGeomList:
            if it.name == GeomName:
                sPrvGeom = it.name
                it.append(Line)
        
        
def jGeomListAppendFile(GeomName,FileName):

    if GeomName == "" and sPrvGeom != "":
        GeomName = sPrvGeom
        
    if GeomName == "" :
        print("Append GeomFile. No Geom Name given!<" + GeomName + ">")
        return
        
    for it in lGeomList:
        if it.name == GeomName :
            sPrvGeom = it.name
            break

    else :
        lGeomList.append(GeomListItem (name = GeomName))
        for it in lGeomList:
            if it.name == GeomName:
                sPrvGeom = it.name
        

        
    if os.path.exists(FileName):
        try:
           with open(FileName, 'r', encoding="cp1252" , errors='replace' ) as f:
              file_content = f.readlines()
              
              for Line in file_content:
                  it.append(Line[:-1])
              
           if not file_content:
              print("No Data in file " + FileName)
        except IOError as e:
           print("I/O error({0}): {1}".format(e.errno, e.strerror))
        except: #handle other exceptions such as attribute errors
           print("Unexpected error:", sys.exc_info()[0])
    else :
        print ("!Error:File does not exists:"+ FileName) 
                

        
def jGeomList_Debug():
    global lGeomList

    iGeoms = 0
    
    print ("")
    print ("------ Geom DATA START -------")
    for it in lGeomList:
        if it.name != "" :
            iGeoms = iGeoms + 1
            print("")
            print("------" + str(iGeoms) + '. ' + it.name +"-------")
            for iCounter , iLines in enumerate(it):
                print(str(iCounter+1) + ". ---> " + iLines )
        


    if iGeoms==0:
        print("**** No Geom Data ****") 
 

    print("")
    print ("------ Geom DATA END -------")   
  
    
def jGeomList_Write():
  global lGeomList
  
  Params = {}
  
  for it in lGeomList:
        if it.name != "" :
            Params['SG_Name'] = it.name    

            s_Value = ""
            for iCounter , iLines in enumerate(it):
                s_Value = s_Value + iLines.rstrip().replace('\\n', '\n')

            Params['SG_Code'] = s_Value  
            jaddsequencegeometry(Params, autocount=False)
    
    
    

def jGeomList_Destroy():
    global lGeomList
    lGeomList.clear()

def jGeomList_Clear():
    global lGeomList
    lGeomList.clear()
    
def jSetDebugMode(DebugMode):
    global i_debug
    i_debug = DebugMode
def jGetDebugMode():
    global i_debug
    return i_debug 

# Some test functions :
def jfunc_01(filename='c:\\temp\\test.json'):
    global _jsonObject

    print(indentChars.format('jsonbase.func_01'))
    _jsonObject = JsonBase(filename)
    print(indentChars.format(_jsonObject))
def jfunc_02():
    print(indentChars.format('jsonbase.func_02'))
    pass
def jfunc_03():
    print(indentChars.format('jsonbase.func_03'))
    pass
def jfunc_04():
    print(indentChars.format('jsonbase.func_04'))
    pass
def jfunc_05():
    print(indentChars.format('jsonbase.func_05'))
    print(indentChars.format(_jsonObject))
#---------------------------------------------------
def jsetuniqualanguage(lang):
    global UNIQUA_LANGUAGE
    UNIQUA_LANGUAGE = lang

def jgetuniqualanguage():
    return UNIQUA_LANGUAGE

def jsetuniquaversion(version):
    # sets and returns real actual version
    if isinstance(version, str):     # check if not None
        global UNIQUA_VERSION
        UNIQUA_VERSION = UniquaVersion(version)
    return UNIQUA_VERSION

def jgetuniquaversion():
    return UNIQUA_VERSION

def jgetuniquaversion_oldest():
    return UniquaVersion(UNIQUA_VERSION_LIST[0])

def jgetuniquaversion_newest(last=-1):
    return UniquaVersion(UNIQUA_VERSION_LIST[last])

def jgetuniquaversion_list(last=-1):
    return UNIQUA_VERSION_LIST[:last]

def jsetminimaldbversion(version):
    # sets and returns real actual version
    if isinstance(version, str):     # check if not None
        global MINIMALDBVERSION
        MINIMALDBVERSION = version
    return MINIMALDBVERSION

def jgetminimaldbversion():
    return MINIMALDBVERSION

def jsetoperation_mode(opmode):
    # sets and returns real actual version
    if opmode in [DYNAMIC_JSON, SEQUENTIAL_JSON]:
        global UNIQUA_OPERATION_MODE
        UNIQUA_OPERATION_MODE = opmode
    return UNIQUA_OPERATION_MODE

def jgetoperation_mode():
    return UNIQUA_OPERATION_MODE

# def jsetgeometryheader(header):
#     global UNIQUA_GEOMETRY_HEADER
#     UNIQUA_GEOMETRY_HEADER = header

def jgetgeometryheader():
    return UNIQUA_GEOMETRY_HEADER

# 1. Step : creating new object and assign name
def jcreate(filename='c:\\temp\\test.json', opmode=None, type=None):
    global _jsonObject
    global _jsonObjectONE, _jsonObjectTWO, _jsonObjectTHREE, _jsonObjectFOUR

    jsetoperation_mode(opmode)

    print(indentChars.format('jsonbase.jcreate'))
    #
    # New : Allow multiple json objects for creating separate files output
    if type == ROUGH:
        name, ext = os.path.splitext(filename)
        _jsonObject = _jsonObjectONE = JsonBase( "{}_{}{}".format(name , 1 , ext) )
    elif type == BRIDGE:
        name, ext = os.path.splitext(filename)
        _jsonObject = _jsonObjectTWO = JsonBase( "{}_{}{}".format(name , 2 , ext) )
    elif type == TRIM:
        name, ext = os.path.splitext(filename)
        _jsonObject = _jsonObjectTHREE = JsonBase( "{}_{}{}".format(name , 3 , ext) )
    elif type == RESERVE:
        name, ext = os.path.splitext(filename)
        _jsonObject = _jsonObjectFOUR = JsonBase( "{}_{}{}".format(name , 4 , ext) )
    else:
        if _jsonObject:
            del(_jsonObject)
        _jsonObject = JsonBase(filename)
    #
    return _jsonObject

def jisempty():
    return _jsonObject.isempty()

def jsetfilename(filename='c:\\temp\\test.json'):
    print(indentChars.format('jsonbase.jsetfilename'))
    if _jsonObject:
        _jsonObject.setfilename(filename)
        print(indentChars.format('....set!'))
        return True
    print(indentChars.format('....failed!'))
    return False

def jgetfilename():
    print(indentChars.format('jsonbase.jgetfilename'))
    if _jsonObject:
        ret = _jsonObject.getfilename()
        print(indentChars.format('....get!'))
        return ret
    print(indentChars.format('....failed!'))
    return None

def jswitchtype(type):
    global _jsonObject

    if type == ROUGH:
        _jsonObject = _jsonObjectONE
        print(indentChars.format('jsonbase.jswitchtype to roughcut'))
    elif type == BRIDGE:
        _jsonObject = _jsonObjectTWO
        print(indentChars.format('jsonbase.jswitchtype to brigdecut'))
    elif type == TRIM:
        _jsonObject = _jsonObjectTHREE
        print(indentChars.format('jsonbase.jswitchtype to trimcut'))
    elif type == RESERVE:
        _jsonObject = _jsonObjectFOUR
        print(indentChars.format('jsonbase.jswitchtype to reserved'))
    else:
        print(indentChars.format('....failed (illegal type) !'))
    return _jsonObject

def jgettype():
    if _jsonObject == _jsonObjectONE:
        return ROUGH
    elif _jsonObject == _jsonObjectTWO:
        return BRIDGE
    elif _jsonObject == _jsonObjectTHREE:
        return TRIM
    elif _jsonObject == _jsonObjectFOUR:
        return RESERVE
    else:
        return None

def jisobjectcreated(type=None):
    if type == ROUGH:
        return _jsonObjectONE is not None
    elif type == BRIDGE:
        return _jsonObjectTWO is not None
    elif type == TRIM:
        return _jsonObjectTHREE is not None
    elif type == RESERVE:
        return _jsonObjectFOUR is not None
    else:
        return _jsonObject is not None

def jgetnumberoffiles():
    cnt = 0
    if _jsonObjectONE:
        cnt += 1
    if _jsonObjectTWO:
        cnt += 1
    if _jsonObjectTHREE:
        cnt += 1
    if _jsonObjectFOUR:
        cnt += 1
    return cnt

def jgetnumberofnonemptyfiles():
    cnt = 0
    if _jsonObjectONE and not _jsonObjectONE.isempty():
        cnt += 1
    if _jsonObjectTWO and not _jsonObjectTWO.isempty():
        cnt += 1
    if _jsonObjectTHREE and not _jsonObjectTHREE.isempty():
        cnt += 1
    if _jsonObjectFOUR and not _jsonObjectFOUR.isempty():
        cnt += 1
    return cnt

# 2. Step : creating manifest object
def jaddmanifest(Params):
    if _jsonObject:
        _jsonObject.addmanifest(Params)
        return True
    print(indentChars.format('jsonbase.jaddmanifest'))        
    print(indentChars.format('....failed!'))
    return False

def jupdatemanifest(Params):
    if _jsonObject:
        if _jsonObject.updatemanifest(Params) == True:
            return True
    print(indentChars.format('jsonbase.jupdatemanifest'))            
    print(indentChars.format('....failed!'))
    return False

def jaddsection(Name, Params):
    if _jsonObject:
        _jsonObject.addsection(Name, Params)
        return True
    print(indentChars.format('jsonbase.jaddsection'))        
    print(indentChars.format('....failed!'))
    return False

def jupdatesection(Name, Params):
    if _jsonObject:
        if _jsonObject.updatesection(Name, Params) == True:
            return True
    print(indentChars.format('jsonbase.jupdatesection'))
    print(indentChars.format('....failed!'))
    return False

# 3. Step : creating batch object    (for the first not necessary and not used)
def jaddbatch(Params):
    if _jsonObject:
        _jsonObject.addbatch(Params)
        return True
    print(indentChars.format('jsonbase.jaddbatch'))
    print(indentChars.format('....failed!'))
    return False

def jupdatebatch(Params):
    print(indentChars.format('jsonbase.jupdatebatch'))
    if _jsonObject:
        _jsonObject.updatebatch(Params)
        print(indentChars.format('....updated!'))
        return True
    print(indentChars.format('....failed!'))
    return False

# 3. Step : creating piece object
def jaddpiece(Params, autocount=True):
    """ the piece is automatically placed into the batch section
    if we create one before doing this step
    ...else it's placed into the same section (root) where the manifest is
    """
    if _jsonObject:
        ret = _jsonObject.addpiece(Params, autocount)
        return ret
    print(indentChars.format('jsonbase.jaddpiece'))
    print(indentChars.format('....failed!'))
    return False

# def jaddpiece2batch(Params):
#     print(indentChars.format('jsonbase.jaddpiece2batch'))
#     if _jsonObject:
#         if _jsonObject.addpiece2batch(Params) == True:
#             print(indentChars.format('....added!'))
#             return True
#     print(indentChars.format('....failed!'))
#     return False

def jupdatepiece(Params, autocount=True):
    if _jsonObject:
        if _jsonObject.updatepiece(Params, autocount) == True:
            return True
    return False

def jgetpieceParams(Params=None):
    if _jsonObject:
        ret = _jsonObject.getpieceParams(Params)
        if ret != False:
            return ret
    return False

def jupdatepiece_at(Params, PieceName):
    if _jsonObject:
        if _jsonObject.updatepiece_at(Params, PieceName) == True:
            return True
    return False

# Steps 4 , 4.1, 4.2 normal will be done in a loop (for each machining)
# 4. Step : creating machining object for dynamic mode
def jaddmachining(Params, autocount=True):
    if _jsonObject:
        if _jsonObject.addmachining(Params, autocount) == True:
            return True
    return False

def jupdatemachining(Params, autocount=True):
    if _jsonObject:
        if _jsonObject.updatemachining(Params, autocount) == True:
            return True
    return False

def jgetmachiningParams(Params=None):
    if _jsonObject:
        ret = _jsonObject.getmachiningParams(Params)
        if ret != False:
            return Params
    return False

def jupdatemachining_at(Params, MachiningName, PieceName=None):
    if _jsonObject:
        if _jsonObject.updatemachining_at(Params, MachiningName, PieceName) == True:
            return True
    return False

# 4. Step : creating sequence operation objecta ONLY for sequential mode
#------------------------------------------------------------------------
def jaddvariable(Params, autocount=True):
    if _jsonObject:
        ret = _jsonObject.addvariable(Params, autocount)
        return ret
    return False

def jupdatevariable(Params, autocount=True):
    print(indentChars.format('jsonbase.jupdatevariable not implemented yet'))
def jgetvariable(Params, autocount=True):
    print(indentChars.format('jsonbase.jgetvariable not implemented yet'))
def jupdatevariable_at(Params, autocount=True):
    print(indentChars.format('jsonbase.jupdatevariable_at not implemented yet'))

jaddpiecevariable = jaddvariable
jupdatepiecevariable = jupdatevariable
jgetpiecevariable = jgetvariable
jupdatepiecevariable_at = jupdatevariable_at
#------------------------------------------------------------------------
def jaddcncpoint(Params, autocount=True):
    print(indentChars.format('jsonbase.jaddcncpoint'))
    if _jsonObject:
        ret = _jsonObject.addcncpoint(Params, autocount)
        print(indentChars.format('....added!' if ret else '....failed!'))
        return ret
    print(indentChars.format('....failed!'))
    return False

def jupdatecncpoint(Params, autocount=True):
    print(indentChars.format('jsonbase.jupdatecncpoint not implemented yet'))
def jgetcncpoint(Params, autocount=True):
    print(indentChars.format('jsonbase.jgetcncpoint not implemented yet'))
def jupdatecncpoint_at(Params, autocount=True):
    print(indentChars.format('jsonbase.jupdatecncpoint_at not implemented yet'))

#------------------------------------------------------------------------
def jaddsequenceoperation(Params, autocount=True):
    
    if _jsonObject:
        ret = _jsonObject.addsequenceoperation(Params, autocount)
        return ret
    print(indentChars.format('jsonbase.jaddsequenceoperation'))        
    print(indentChars.format('....failed!'))
    return False

def jupdatesequenceoperation(Params, autocount=True):
    print(indentChars.format('jsonbase.jupdatesequenceoperation'))
    if _jsonObject:
        if _jsonObject.updatesequenceoperation(Params, autocount) == True:
            print(indentChars.format('....updated!'))
            return True
    print(indentChars.format('....failed!'))
    return False

def jgetsequenceoperationParams(Params=None):
    print(indentChars.format('jsonbase.jgetsequenceoperationParams'))
    if _jsonObject:
        ret = _jsonObject.getsequenceoperationParams(Params)
        if ret != False:
            print(indentChars.format('....get!'))
            return Params
    print(indentChars.format('....failed!'))
    return False

def jupdatesequenceoperation_at(Params, OperationName, PieceName=None):
    print(indentChars.format('jsonbase.jupdatesequenceoperation_at'))
    if _jsonObject:
        if _jsonObject.updatesequenceoperation_at(Params, OperationName, PieceName) == True:
            print(indentChars.format('....updated!'))
            return True
    print(indentChars.format('....failed!'))
    return False

#------------------------------------------------------------------------
def jaddsequencegeometry(Params, autocount=True):
    
    if _jsonObject:
        ret = _jsonObject.addsequencegeometry(Params, autocount)
        return ret
    return False

def jupdatesequencegeometry(Params, autocount=True):
    if _jsonObject:
        if _jsonObject.updatesequencegeometry(Params, autocount) == True:
            return True
    return False

def jgetsequencegeometryParams(Params=None):
    if _jsonObject:
        ret = _jsonObject.getsequencegeometryParams(Params)
        if ret != False:
            return Params
    return False

def jupdatesequencegeometry_at(Params, GeometryName, PieceName=None):
    if _jsonObject:
        if _jsonObject.updatesequencegeometry_at(Params, GeometryName, PieceName) == True:
            return True
    return False

#------------------------------------------------------------------------
def jaddsequencetarget(Params, autocount=True):
    if _jsonObject:
        ret = _jsonObject.addsequencetarget(Params, autocount)
        return ret
    return False

def jupdatesequencetarget(Params, autocount=True):                             
    if _jsonObject:
        if _jsonObject.updatesequencetarget(Params, autocount) == True:
            return True
    return False

def jgetsequencetargetParams(Params=None):
    if _jsonObject:
        ret = _jsonObject.getsequencetargetParams(Params)
        if ret != False:
            return Params
    return False

def jupdatesequencetarget_at(Params, TargetName, PieceName=None):
    if _jsonObject:
        if _jsonObject.updatesequencetarget_at(Params, TargetName, PieceName) == True:
            return True
    return False

#------------------------------------------------------------------------
def jaddsequencecriteria(Params, autocount=True):                             # "ST_Technology_Criteria_Array"
    if _jsonObject:
        if _jsonObject.addsequencecriteria(Params) == True:
            return True
    return False

def jupdatesequencecriteria(Params, autocount=True):                             
    if _jsonObject:
        if _jsonObject.updatesequencecriteria(Params) == True:
            return True
    return False

def jgetsequencecriteriaParams(Params=None):
    if _jsonObject:
        ret = _jsonObject.getsequencecriteriaParams(Params)
        if ret != False:
            return Params
    return False

# 4.1. Step : insert geometry data (into machining section)
def jsetgeometry(Name="Geometry_01", NCDataString=None, autocount=True, noHeader=False):
    if _jsonObject:
        if NCDataString is None:
            NCDataString = _jsonObject.SingleSentenceData()     # dummy data - just for testing
            #NCDataString = _jsonObject.DoubleSentenceData()     # dummy data - just for testing
        ret = _jsonObject.setgeometry(Name, NCDataString, autocount, noHeader)
        return ret
    return False

# 4.2. Step : insert pocketing data (into machining section)
def jsetpocketing(Name="Pocketing_01", NCDataString=None, autocount=True, noHeader=False):
    if _jsonObject:
        if NCDataString is None:
            NCDataString = _jsonObject.PocketingData()      # dummy data - just for testing
        ret = _jsonObject.setpocketing(Name, NCDataString, autocount, noHeader)
        return ret
    return False

def jsetstartpoint(Name="Piece_Stp", x=None, y=None, u=None, v=None, autocount=True):
    print(indentChars.format('jsonbase.jsetstartpoint'))
    if _jsonObject:
        ret = _jsonObject.setstartpoint(Name, x, y, u, v, autocount)
        print(indentChars.format('....set!'))
        return ret
    print(indentChars.format('....failed!'))
    return False

def jsetendpoint(Name="Piece_Ep", x=None, y=None, u=None, v=None, autocount=True):
    print(indentChars.format('jsonbase.jsetendpoint'))
    if _jsonObject:
        ret = _jsonObject.setendpoint(Name, x, y, u, v, autocount)
        print(indentChars.format('....set!'))
        return ret
    print(indentChars.format('....failed!'))
    return False

def jaddcriteria(Params):
    print(indentChars.format('jsonbase.jaddcriteria'))
    if _jsonObject:
        ret = _jsonObject.addcriteria(Params)
        print(indentChars.format('....added!' if ret else '....failed!'))
        return ret
    print(indentChars.format('....failed!'))
    return False

def jaddcriteria2(Params):
    print(indentChars.format('jsonbase.jaddcriteria'))
    if _jsonObject:
        ret = _jsonObject.addcriteria2(Params)
        print(indentChars.format('....added!' if ret else '....failed!'))
        return ret
    print(indentChars.format('....failed!'))
    return False

def jupdatecriteria(Params):
    print(indentChars.format('jsonbase.jupdatecriteria'))
    if _jsonObject:
        ret = _jsonObject.updatecriteria(Params)
        print(indentChars.format('....updated!'))
        return ret
    print(indentChars.format('....failed!'))
    return False

def jupdatecriteria2(Params):
    print(indentChars.format('jsonbase.jupdatecriteria'))
    if _jsonObject:
        ret = _jsonObject.updatecriteria2(Params)
        print(indentChars.format('....updated!'))
        return ret
    print(indentChars.format('....failed!'))
    return False

def jsetexpectedtechnology(Params=None):
    if _jsonObject:
        ret = _jsonObject.setexpectedtechnology(Params)
        return ret
    return False

def jaddexpectedcriteria(Params):
    if _jsonObject:
        ret = _jsonObject.addexpectedcriteria(Params)
        return ret
    return False

def jaddexpectedpass(Params=None):
    if _jsonObject:
        ret = _jsonObject.addexpectedpass(Params)
        return ret
    return False

def jaddpasscriteria(Params):
    if _jsonObject:
        ret = _jsonObject.addpasscriteria(Params)
        return ret
    return False

def jaddpoint(Params, autocount=True):
    if _jsonObject:
        ret = _jsonObject.addpoint(Params, autocount)
        return ret
    return False

def jupdatepoint(Params):
    if _jsonObject:
        ret = _jsonObject.updatepoint(Params)
        return ret
    return False

def jaddsector(Params):
    if _jsonObject:
        ret = _jsonObject.addsector(Params)
        return ret
    return False

def jupdatesector(Params):
    if _jsonObject:
        if _jsonObject.updatesector(Params) == True:
            return True
    return False
    
def jmachininggetvalue(*keys):
    if _jsonObject:
        ret = _jsonObject.machininggetkey(*keys)
        return ret
    return False
    
# Last Steps : write to disc
def jwrite(jsonfile=None):
    if _jsonObject:
        _jsonObject.write(jsonfile)
        return True
    return False

# Last Steps : ...and destroy the json-object
def jdestroy(all=False):
    global _jsonObject

    print(indentChars.format('jsonbase.jdestroy'))
    if all:
        global _jsonObjectONE, _jsonObjectTWO, _jsonObjectTHREE, _jsonObjectFOUR

        if _jsonObjectONE:
            del(_jsonObjectONE)
            _jsonObjectONE = None
        if _jsonObjectTWO:
            del(_jsonObjectTWO)
            _jsonObjectTWO = None
        if _jsonObjectTHREE:
            del(_jsonObjectTHREE)
            _jsonObjectTHREE = None
        if _jsonObjectFOUR:
            del(_jsonObjectFOUR)
            _jsonObjectFOUR = None
        
    if _jsonObject:
        del(_jsonObject)
        _jsonObject = None
        print(indentChars.format('....destroyed!'))
        return True
    #print(indentChars.format('....failed!'))
    return False
    
    
# ------ Description Handling Start ------    
def jDescClear():   
    global s_DescriptionList
    s_DescriptionList = ''

def jDescAppend(Line):   
    global s_DescriptionList
    s_DescriptionList = s_DescriptionList + Line 

def jDescWrite():   
    global s_DescriptionList
    Params = {}
    Params['P_Description'] = s_DescriptionList  
    jupdatepiece(Params,autocount=False)
    
def jDescGetList():
    global s_DescriptionList
    return s_DescriptionList
# ------ Description Handling End ------    

    
    
#------------------------------------------------------------------------------


"""
HEADER

ENCODING
UNIQUA is able to encode several GEOMETRY specifications. To identify a dynamic job, the first line must be // UNIQUA high level ISO V1.1. This command
is mandatory and must be defined as a comment (//). In the UNIQUA GEOMETRY previewer this line is not shown.

UNIT
The default length unit is millimeter [mm] G21.
To predefine the GEOMETRY in imperial inch [in] it is mandatory to set G20 before every GEOMETRY.

COORDINATES
By default the movement coordinates are defined in an absolute way with G90. The absolute movement moves to a coordinate based on your MACHINING reference.
It's also possible to define the movement in an incremental way with G91. The incremental movement moves to a coordinate based on your current GEOMETRY position.
"""

"""
INITIALISATION

The mandatory INITIALISATION defines the GEOMETRY beginning, relative to the MACHINING reference. It also defines the plane(s).This is set by G00 Xx Yy Zz.
A 2 axis GEOMETRY is always defined as single sentence. The taper definition is done in the MACHINING objects as M_Taper... .
A 4 axis GEOMETRY is always defined as double sentence. Varible taper definition is also done in double sentences.
"""

"""
GEOMETRY DEFINITION

The GEOMETRY is defined in G01, G02, G03. In case of a 4axis GEOMETRY it is mandatory to use the double sentence style.Other characteristics like taper or offset are
defined in the MACHINING object.
"""


# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Class definitions used for dynamic and sequential (structured) operation mode
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

# Definitions for several data objects
# Based on GF description 'Uniqua JSON Specification 0.9.12.xlsx'

class General(OrderedDict):
    """
    Version 0.9.9

    Line Numbers
    All parameters related to the geometry line number have to be set with the number of the geometry face.
    At importation time, Uniqua will convert the face number to the corresponding geometry line number.
    These number will be displayed on the user interface.
    At exportation time, Uniqua will convert the geometry line number to the corresponding number of geometry face.
    """
    pass

class Manifest(OrderedDict):
    """
    dictionary, holds the Manifest Data
    object variable is self.manifest_dict
    parent is self.main_dict["Manifest"]
    """
    def __init__(self, kwargs):
        # Creation of actual tile stamp
        if PY3:
            timestring = datetime.now().astimezone().isoformat()
        else:
            #timestring = datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%f%z')   # TODO : tzinfo (%z) is empty
            timestring = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%f+00:00')
        self.setdefault("MAN_Version", kwargs.pop("MAN_Version", UNIQUA_VERSION))       # String (Available specification versions)
        self.setdefault("MAN_Scope", kwargs.pop("MAN_Scope", ''))                       # String (Piece, Batch)
        self.setdefault("MAN_Author", kwargs.pop("MAN_Author", ''))                     # String
        self.setdefault("MAN_Description",    kwargs.pop("MAN_Description", ''))        # String
        self.setdefault("MAN_Creator",    kwargs.pop("MAN_Creator", ''))                # String (Name of the tool that created the JSON)
        self.setdefault("MAN_Creator_Version", kwargs.pop("MAN_Creator_Version", ''))   # String (Follow generator standards)
        self.setdefault("MAN_Creator_Data", kwargs.pop("MAN_Creator_Data", ''))         # String ("Additional information for the tool that created the JSON")
        self.setdefault("MAN_Creator_Date", kwargs.pop("MAN_Creator_Date", timestring)) # String
        #
        assert not kwargs, 'invalid keyword args {}'.format( kwargs )

    def __str__(self, *args, **kwargs):
        return "Class: {}".format(self.__class__.__name__)

class MultiPurpose(OrderedDict):
    """
    dictionary, holds the Multipurpose Data
    object variable is self.multi_purpose_dict
    parent is self.main_dict["Manifest"]
    """
    def __init__(self, kwargs):
        for key, value in list(kwargs.items()):
            self.setdefault(key, value)
            
    def __str__(self, *args, **kwargs):
        return "Class: {}".format(self.__class__.__name__)

class Batch(OrderedDict):
    """
    dictionary, holds the Batch Data
    object variable is self.batch_dict
    parent is self.main_dict["Batch"]
    """
    def __init__(self, kwargs):
        self.setdefault("B_Name", kwargs.pop("B_Name", ''))                             # String
        self.setdefault("B_ErpId", kwargs.pop("B_ErpId", ''))                           # String (Customer Batch Id. Uniqua will ignore it.)
        self.setdefault("B_Description", kwargs.pop("B_Description", ''))               # String
        self.setdefault("B_Generation_Strategy_Name", kwargs.pop("B_Generation_Strategy_Name", UNIQUA_STRATEGY_NAMES[EARLYMINIMUMTHREADING]))     # String (EarlyMinimumThreading, EarlyMinimumOperatorTime, Late or a costum generation strategy identifier (name of strategy)
        self.setdefault("B_Sorting_Pallet_By_Pallet", kwargs.pop("B_Sorting_Pallet_By_Pallet", False))     # Boolean
        self.setdefault("B_Sorting_Part_By_Part", kwargs.pop("B_Sorting_Part_By_Part", False))             # Boolean
        self.setdefault("B_Piece_Array", kwargs.pop("B_Piece_Array", []))               # Array of Piece
        #
        assert not kwargs, 'invalid keyword args {}'.format( kwargs )

    def addPiece(self, *args, **kwargs):
        if "B_Piece_Array" in self:
            self["B_Piece_Array"].append(PieceDynamic(*args, **kwargs))

    def __str__(self, *args, **kwargs):
        return "Class: {}, B_Name: {}".format(self.__class__.__name__, self['B_Name'])

class PieceCommon(OrderedDict):
    """
    dictionary, commonly used by dynamic and sequential
    """
    def __init__(self, kwargs):
        self.setdefault("P_Name", kwargs.pop("P_Name", ''))                     # String
        if UNIQUA_VERSION >= "0.9.6":
            self.setdefault("P_ErpId", kwargs.pop("P_ErpId", ''))               # String (Customer Piece Id. Uniqua will ignore it.)
        else:
            kwargs.pop("P_ErpId", '')
            
            
        self.setdefault("P_Description", kwargs.pop("P_Description", ''))       # String
        self.setdefault("P_Material", kwargs.pop("P_Material", ''))             # String (Steel, Copper, Graphite, Hard Metal, Alluminium, Titanium, PCD_CTB010)
        
        if UNIQUA_VERSION >= "0.9.12":
            self.setdefault("P_Operation_Language", kwargs.pop("P_Operation_Language", 'Cmd'))   # String (Cmd, Iso)
        else:
            kwargs.pop("P_Operation_Language", '')
            
            
        self.setdefault("P_Priority", kwargs.pop("P_Priority", 0))   # Int (>= 0)
        
        if UNIQUA_VERSION >= "0.9.10":
            self.setdefault("P_Has_Part_Definition", kwargs.pop("P_Has_Part_Definition", False))    # Boolean (>= 0)
        else:
            kwargs.pop("P_Has_Part_Definition", False)        
        
        if True:    # UNIQUA_OPERATION_MODE == DYNAMIC_JSON:
            self.setdefault("P_Dimensions_Width_X", kwargs.pop("P_Dimensions_Width_X", 0.))         # Double (Required if P_Has_Part_Definition is true)
            self.setdefault("P_Dimensions_Depth_Y", kwargs.pop("P_Dimensions_Depth_Y", 0.))         # Double (Required if P_Has_Part_Definition is true)
            self.setdefault("P_Dimensions_Height_Z", kwargs.pop("P_Dimensions_Height_Z", 0.))       # Double (Required if P_Has_Part_Definition is true)
            self.setdefault("P_Origin_X", kwargs.pop("P_Origin_X", 0.))                             # Double (Required if P_Has_Part_Definition is true)
            self.setdefault("P_Origin_Y", kwargs.pop("P_Origin_Y", 0.))                             # Double (Required if P_Has_Part_Definition is true)
            self.setdefault("P_Origin_Z", kwargs.pop("P_Origin_Z", 0.))                             # Double (Required if P_Has_Part_Definition is true)
            self.setdefault("P_Enable_Measurable_Point", kwargs.pop("P_Enable_Measurable_Point", True))   # Boolean
            self.setdefault("P_Measureable_Point_X", kwargs.pop("P_Measureable_Point_X", 0.))       # Double (Required if P_Enable_Measurable_Point is true)
            self.setdefault("P_Measureable_Point_Y", kwargs.pop("P_Measureable_Point_Y", 0.))       # Double (Required if P_Enable_Measurable_Point is true)
            self.setdefault("P_Measureable_Point_Z", kwargs.pop("P_Measureable_Point_Z", 0.))       # Double (Required if P_Enable_Measurable_Point is true)
            self.setdefault("P_Measureable_Point_Rot_C", kwargs.pop("P_Measureable_Point_Rot_C", 0.))   # Double (Required if P_Enable_Measurable_Point is true)
            if UNIQUA_VERSION >= "0.9.10":
                self.setdefault("P_Correction_X", kwargs.pop("P_Correction_X", 0.))                     # Double (Required if P_Enable_Measurable_Point is true)
                self.setdefault("P_Correction_Y", kwargs.pop("P_Correction_Y", 0.))                     # Double (Required if P_Enable_Measurable_Point is true)
                self.setdefault("P_Correction_Z", kwargs.pop("P_Correction_Z", 0.))                     # Double (Required if P_Enable_Measurable_Point is true)
                self.setdefault("P_Correction_A", kwargs.pop("P_Correction_A", 0.))                     # Double (Required if P_Enable_Measurable_Point is true)
                self.setdefault("P_Correction_B", kwargs.pop("P_Correction_B", 0.))                     # Double (Required if P_Enable_Measurable_Point is true)
                self.setdefault("P_Correction_C", kwargs.pop("P_Correction_C", 0.))                     # Double (Required if P_Enable_Measurable_Point is true)
            else:
                kwargs.pop("P_Correction_X", 0.)
                kwargs.pop("P_Correction_Y", 0.)
                kwargs.pop("P_Correction_Z", 0.)
                kwargs.pop("P_Correction_A", 0.)
                kwargs.pop("P_Correction_B", 0.)
                kwargs.pop("P_Correction_C", 0.)
            
        if UNIQUA_VERSION >= "0.9.10":
            self.setdefault("P_Reference_Aux_A", kwargs.pop("P_Reference_Aux_A", 0.))               # Double (Imported only of the Uniqua instance has the auxiliary axis A configured)
            self.setdefault("P_Reference_Aux_B", kwargs.pop("P_Reference_Aux_B", 0.))               # Double (Imported only of the Uniqua instance has the auxiliary axis A configured)
        else:
            kwargs.pop("P_Reference_Aux_A", 0.)
            kwargs.pop("P_Reference_Aux_B", 0.)            
            
        self.setdefault("P_Creator_Data", kwargs.pop("P_Creator_Data", ''))                     # String (Reserved for Creator  related data. Should not be used for large set of data.)
        self.setdefault("P_Pallet_Name", kwargs.pop("P_Pallet_Name", ''))                       # String ("Empty string or null value means that the piece is not in a pallet)

    def getName(self, name, array, key):
        cnt = 0
        while True:
            cnt += 1
            for a in array:
                if a[key] == "{}_{:03d}".format(name, cnt):
                    break
            else:
                return "{}_{:03d}".format(name, cnt)

class PieceDynamic(PieceCommon):
    """
    dictionary, holds the dynamic piece data
    object variable is self.piece_dict
    parent is self.main_dict["Piece"] or self.batch_dict["B_Piece_Array"]
    """
    machiningnames = set()
    #
    def __init__(self, kwargs):
        PieceCommon.__init__(self, kwargs)
        #
        self.setdefault("P_Security_Level", kwargs.pop("P_Security_Level", 0.05))               # Double
        self.setdefault("P_Return_Level", kwargs.pop("P_Return_Level", 30.))                    # Double
        self.setdefault("P_Macro_Encoding", kwargs.pop("P_Macro_Encoding", 'GfmsCmd'))                              # String (GfmsCmd; Required if a macro is specified)
        self.setdefault("P_Before_First_Pass_Of_Piece", kwargs.pop("P_Before_First_Pass_Of_Piece", 'None'))         # ?
        self.setdefault("P_Before_Each_Pass_Of_Piece", kwargs.pop("P_Before_Each_Pass_Of_Piece", 'None'))           # ?
        self.setdefault("P_After_Each_Pass_Of_Piece", kwargs.pop("P_After_Each_Pass_Of_Piece", 'None'))             # ?
        self.setdefault("P_After_Last_Pass_Of_Piece", kwargs.pop("P_After_Last_Pass_Of_Piece", 'None'))             # ?
        self.setdefault("P_Measurement_Strategy_Macro_Encoding", kwargs.pop("P_Measurement_Strategy_Macro_Encoding", 'GfmsCmd'))    # String (GfmsCmd; Required if a measurement strategy is specified)
        self.setdefault("P_Measurement_Strategy", kwargs.pop("P_Measurement_Strategy", 'None'))        # ?
        if UNIQUA_VERSION > "1.0.0":
            self.setdefault("P_Generation_Strategy_Name", kwargs.pop("P_Generation_Strategy_Name", 'None'))     # String (EarlyMinimumThreading, EarlyMinimumOperatorTime, Late or a costum generation strategy identifier (name of strategy)
        if UNIQUA_VERSION >= "1.5.0":
            self.setdefault("P_Apply_Job_Execution_Strategy", kwargs.pop("P_Apply_Job_Execution_Strategy", False))
            # create default mandatory zone
            mandatory_zone_default = {
                "MZ_Enabled": False,
                "MZ_ReferenceFrame": "Part",
                "MZ_X": 0.0,
                "MZ_Y": 0.0,
                "MZ_C": 0.0,
                "MZ_Width": 0.0,
                "MZ_Length": 0.0
            }
            self.setdefault("P_Mandatory_Zone", kwargs.pop("P_Mandatory_Zone", mandatory_zone_default))
            pass
        self.setdefault("P_Machining_Array", kwargs.pop("P_Machining_Array", []))               # Array of Machining EDM Cut Dynamic"
        #
        if len(self["P_Machining_Array"]) > 0:
            PieceDynamic.machiningnames.add(self["P_Machining_Array"][0].get("M_Name"))
        assert not kwargs, 'invalid keyword args {}'.format( kwargs )

    def addMachining(self, Params, autocount=True):
        if "P_Machining_Array" in self:
            if autocount == True:
                Params['M_Name'] = self.getName(Params['M_Name'], self["P_Machining_Array"], 'M_Name')
            #
            # experimental atm.
            if Params['M_Name'] in PieceDynamic.machiningnames:
                pass
            else:
                PieceDynamic.machiningnames.add(Params['M_Name'])
            #
            self["P_Machining_Array"].append(MachiningDynamic(Params))
            return True
        return False

    def __str__(self, *args, **kwargs):
        return "Class: {}, P_Name: {}".format(self.__class__.__name__, self['P_Name'])

class PieceStructured(PieceCommon):
    """
    dictionary, holds the sequential piece data
    object variable is self.piece_dict
    parent is self.main_dict["Batch"]
    """
    def __init__(self, kwargs):
        PieceCommon.__init__(self, kwargs)
        #
        self.setdefault("P_Variable_Array", kwargs.pop("P_Variable_Array", []))                         # Array of PieceVariable; could be empty
        self.setdefault("P_Cnc_Point_Array", kwargs.pop("P_Cnc_Point_Array", []))                       # Array of CncPoint; could be empty
        self.setdefault("P_Sequence_Operation_Array", kwargs.pop("P_Sequence_Operation_Array", []))     # Array of SequenceOperation
        self.setdefault("P_Sequence_Geometry_Array", kwargs.pop("P_Sequence_Geometry_Array", []))       # Array of SequenceGeometry
        self.setdefault("P_Sequence_Target_Array", kwargs.pop("P_Sequence_Target_Array", []))           # Array of SequenceTarget
        #
        assert not kwargs, 'invalid keyword args {}'.format( kwargs )
        
    def addVariable(self, Params, autocount=True):
        if "P_Variable_Array" in self:
            if autocount == True:
                Params['PV_Name'] = self.getName(Params['PV_Name'], self["P_Variable_Array"], 'PV_Name')
            #
            self["P_Variable_Array"].append(PieceVariable(Params))
            return True
        return False

    def addCncPoint(self, Params, autocount=True):
        if "P_Cnc_Point_Array" in self:
            if autocount == True:
                Params['CP_Name'] = self.getName(Params['CP_Name'], self["P_Cnc_Point_Array"], 'CP_Name')
            #
            self["P_Cnc_Point_Array"].append(CNCPoint(Params))
            return True
        return False

    def addSequenceOperation(self, Params, autocount=True):
        if "P_Sequence_Operation_Array" in self:
            if autocount == True:
                Params['SO_Name'] = self.getName(Params['SO_Name'], self["P_Sequence_Operation_Array"], 'SO_Name')
            #
            self["P_Sequence_Operation_Array"].append(SequenceOperation(Params))
            return True
        return False

    def addSequenceGeometry(self, Params, autocount=True):
        if "P_Sequence_Geometry_Array" in self:
            if autocount == True:
                Params['SG_Name'] = self.getName(Params['SG_Name'], self["P_Sequence_Geometry_Array"], 'SG_Name')
            #
            self["P_Sequence_Geometry_Array"].append(SequenceGeometry(Params))
            return True
        return False

    def addSequenceTarget(self, Params, autocount=True):
        if "P_Sequence_Target_Array" in self:
            if autocount == True:
                Params['ST_Name'] = self.getName(Params['ST_Name'], self["P_Sequence_Target_Array"], 'ST_Name')
            else :
              for it in self["P_Sequence_Target_Array"]:
                  if Params['ST_Name'] == it.get('ST_Name'):
                      return True               
            
            self["P_Sequence_Target_Array"].append(SequenceTarget(Params))
            return True
        return False

    def __str__(self, *args, **kwargs):
        return "Class: {}, P_Name: {}".format(self.__class__.__name__, self['P_Name'])

class MachiningDynamic(OrderedDict):
    """
    dictionary, holds the MachiningDynamic data
    object variable is self.machining_dict
    parent is self.piece_dict["P_Machining_Array"]
    """
    def __init__(self, kwargs):
        self.setdefault("M_Category", kwargs.pop('M_Category', "Standard"))     # String (Standard, Open, Pocketing)
        self.setdefault("M_Name", kwargs.pop('M_Name', "Geo_1"))                # String
        self.setdefault("M_Description", kwargs.pop('M_Description', ""))       # String
        self.setdefault("M_Priority", kwargs.pop('M_Priority', 0))              # Int (>= 0)
        self.setdefault("M_Geometry_Name", kwargs.pop('M_Geometry_Name', ""))   # String ("If empty, each geometry will be generated separately on the job, even if the content is the same of an already existing geometry If the same name is already used in other machining, all of them will share the same instance of the geometry")
        self.setdefault("M_Geometry", kwargs.pop('M_Geometry', ""))             # String
        self.setdefault("M_Geometry_Pocketing_Name", kwargs.pop('M_Geometry_Name', "NotSet"))   # String ("If empty, each geometry will be generated separately on the job, even if the content is the same of an already existing geometry If the same name is already used in other machining, all of them will share the same instance of the geometry")
        self.setdefault("M_Geometry_Pocketing", kwargs.pop('M_Geometry', ""))   # String (Required if M_Category is Pocketing)
        if UNIQUA_VERSION >= "1.2.0":
            self.setdefault("M_Spiral_End_Point_Enabled", kwargs.pop('M_Spiral_End_Point_Enabled', False))       # Boolean
        self.setdefault("M_Reference_X", kwargs.pop('M_Reference_X', 0.))       # Double
        self.setdefault("M_Reference_Y", kwargs.pop('M_Reference_Y', 0.))       # Double
        self.setdefault("M_Reference_Z", kwargs.pop('M_Reference_Z', 0.))       # Double
        self.setdefault("M_Reference_Rot_A", kwargs.pop('M_Reference_Rot_A', 0.))       # Double
        self.setdefault("M_Reference_Rot_B", kwargs.pop('M_Reference_Rot_B', 0.))       # Double
        self.setdefault("M_Reference_Rot_C", kwargs.pop('M_Reference_Rot_C', 0.))       # Double
        
        if UNIQUA_VERSION >= "0.9.12":
            self.setdefault("M_Reference_Aux_A", kwargs.pop('M_Reference_Aux_A', 0.))      # Double (Imported only of the Uniqua instance has the auxiliary axis A configured)
            self.setdefault("M_Reference_Aux_B", kwargs.pop('M_Reference_Aux_B', 0.))      # Double (Imported only of the Uniqua instance has the auxiliary axis B configured)
        else:
            kwargs.pop('M_Reference_Aux_A', 0.)
            kwargs.pop('M_Reference_Aux_B', 0.)
       
        self.setdefault("M_Separationcut_Length", kwargs.pop('M_Separationcut_Length', 0.)) # Double
        self.setdefault("M_Separationcut_CLE", kwargs.pop('M_Separationcut_CLE', 0.))       # Double
        self.setdefault("M_Autofix_Length", kwargs.pop('M_Autofix_Length', 0.))             # Double
        
        if UNIQUA_VERSION >= "0.9.11":
            self.setdefault("M_Clean_Autofix", kwargs.pop('M_Clean_Autofix', False))       # Boolean
        else:
            kwargs.pop('M_Clean_Autofix', False)
        
        self.setdefault("M_Minimal_Radius", kwargs.pop('M_Minimal_Radius', 0.))             # Double
        self.setdefault("M_Invert_CW_CCW", kwargs.pop('M_Invert_CW_CCW', False))            # Boolean
        
        if UNIQUA_VERSION >= "1.5.0":
             self.setdefault("M_Reduce_Params_After_Wire_Break", kwargs.pop('M_Reduce_Params_After_Wire_Break', False))    # Boolean
             self.setdefault("M_Collision_Detection_Mode", kwargs.pop("DoNothing","DoNothing"))          # String (DoNothing)
        
        self.setdefault("M_Forth_And_Back", kwargs.pop('M_Forth_And_Back', "OnlyShorts"))   # String ("None", "OnlyShorts", "ShortsAndLongs", "AllCuts")
        self.setdefault("M_External_Roughing", kwargs.pop('M_External_Roughing', False))    # Boolean
        self.setdefault("M_Short_Trims", kwargs.pop('M_Short_Trims', True))                 # Boolean
        self.setdefault("M_Machining_Type", kwargs.pop('M_Machining_Type', "Die"))          # String (Die, Punch, always Die if M_Category is set to Pocketing)
        self.setdefault("M_Retreat_Strategy", kwargs.pop('M_Retreat_Strategy', "DeltaMain"))  # String (DeltaMain, Perpendicular)
        self.setdefault("M_Offset_Side", kwargs.pop('M_Offset_Side', "Left"))               # String (Right, Left)
        self.setdefault("M_Scale_Factor", kwargs.pop('M_Scale_Factor', 1.0))                # Double
        self.setdefault("M_Taper_Angle", kwargs.pop('M_Taper_Angle', 0.))                   # Double (>= 0)
        self.setdefault("M_Mirror_X", kwargs.pop('M_Mirror_X', False))                      # Boolean
        self.setdefault("M_Mirror_Y", kwargs.pop('M_Mirror_Y', False))                      # Boolean
        self.setdefault("M_Taper_Disposition", kwargs.pop('M_Taper_Disposition', "Conic"))  # String (Conic, Cylinder)
        self.setdefault("M_CLE", kwargs.pop('M_CLE', 0.))                                   # Double
        self.setdefault("M_Taper_Direction", kwargs.pop('M_Taper_Direction', "OpenBottom")) # String (OpenBottom, OpenTop if M_Category is Standard or Pocketing Left, Right if M_Category is Open)
        if UNIQUA_VERSION >= "0.9.11":
            self.setdefault("M_Enable_Auto_Slug", kwargs.pop('M_Enable_Auto_Slug', False))  # Boolean
            self.setdefault("M_Auto_Slug_Detection_Length", kwargs.pop('M_Auto_Slug_Detection_Length', 0.)) # Double
        else:
            kwargs.pop('M_Enable_Auto_Slug', False)
            kwargs.pop('M_Auto_Slug_Detection_Length', 0.)
        self.setdefault("M_Startpoint_Name", kwargs.pop('M_Startpoint_Name', "Piece_Stp"))  # String
        if UNIQUA_VERSION >= "0.9.7":
            self.setdefault("M_Startpoint_Manual_Threading", kwargs.pop('M_Startpoint_Manual_Threading', False))  # Boolean
        else:
            kwargs.pop('M_Startpoint_Manual_Threading', False)
        if UNIQUA_VERSION >= "0.9.10":
            self.setdefault("M_Startpoint_Use_Custom_Threading", kwargs.pop('M_Startpoint_Use_Custom_Threading', False))  # Boolean
            self.setdefault("M_Startpoint_Hole_Diameter", kwargs.pop('M_Startpoint_Hole_Diameter', 2.))                 # Double
            self.setdefault("M_Startpoint_Threading_Empty_Tank", kwargs.pop('M_Startpoint_Threading_Empty_Tank', False))  # Boolean
            self.setdefault("M_Startpoint_Threading_Jet_On", kwargs.pop('M_Startpoint_Threading_Jet_On', True))  # Boolean
            self.setdefault("M_Startpoint_Threading_UV_Movement", kwargs.pop('M_Startpoint_Threading_UV_Movement', True))  # Boolean
            self.setdefault("M_Startpoint_Threading_Z_Movement", kwargs.pop('M_Startpoint_Threading_Z_Movement', False))  # Boolean
        else:
            kwargs.pop('M_Startpoint_Use_Custom_Threading', False)
            kwargs.pop('M_Startpoint_Hole_Diameter', 2.)
            kwargs.pop('M_Startpoint_Threading_Empty_Tank', False)
            kwargs.pop('M_Startpoint_Threading_Jet_On', True)
            kwargs.pop('M_Startpoint_Threading_UV_Movement', True)
        self.setdefault("M_Startpoint_X", kwargs.pop('M_Startpoint_X', 0.))                 # Double
        self.setdefault("M_Startpoint_Y", kwargs.pop('M_Startpoint_Y', 0.))                 # Double
        self.setdefault("M_Startpoint_U", kwargs.pop('M_Startpoint_U', 0.))                 # Double
        self.setdefault("M_Startpoint_V", kwargs.pop('M_Startpoint_V', 0.))                 # Double

        if UNIQUA_VERSION >= "0.9.12":
            self.setdefault("M_Startpoint_A", kwargs.pop('M_Startpoint_A', 0.))                 # Double
            self.setdefault("M_Startpoint_B", kwargs.pop('M_Startpoint_B', 0.))                 # Double
        else:
            kwargs.pop('M_Startpoint_A', 0.)
            kwargs.pop('M_Startpoint_B', 0.)
        self.setdefault("M_Startpoint_Reference", kwargs.pop('M_Startpoint_Reference', "Machining"))  # String (Machining, Part)
        self.setdefault("M_Startpoint_Entry_Line", kwargs.pop('M_Startpoint_Entry_Line', 1))                 # Int
        self.setdefault("M_Startpoint_Entry_Position_Percent", kwargs.pop('M_Startpoint_Entry_Position_Percent', 0.))   # Double  # (Required)
#        self.setdefault("M_Startpoint_Entry_Position_mm", kwargs.pop('M_Startpoint_Entry_Position_mm', 0.))
        self.setdefault("M_Startpoint_Entry_Perpendicular_Zone_Length", kwargs.pop('M_Startpoint_Entry_Perpendicular_Zone_Length', 0.))     # Double
        self.setdefault("M_Startpoint_Entry_Tangent_Zone_ArcRadius", kwargs.pop('M_Startpoint_Entry_Tangent_Zone_ArcRadius', 0.))           # Double
        self.setdefault("M_Startpoint_Entry_Type", kwargs.pop('M_Startpoint_Entry_Type', "Perpendicular"))     # String (Perpendicular, Tangent, Straight)
        self.setdefault("M_Startpoint_Entry_Side", kwargs.pop('M_Startpoint_Entry_Side', "OffsetEdm"))         # String (OffsetEdm, ZoneLength)
        self.setdefault("M_Startpoint_Approach_Type", kwargs.pop('M_Startpoint_Approach_Type', "Straight"))    # String (Triangle, Rectangle, Straight)
        self.setdefault("M_Startpoint_Approach_Width", kwargs.pop('M_Startpoint_Approach_Width', 0.))          # Double
        self.setdefault("M_Startpoint_Approach_Height", kwargs.pop('M_Startpoint_Approach_Height', 0.))        # Double
        self.setdefault("M_Startpoint_Approach_Side", kwargs.pop('M_Startpoint_Approach_Side', "Auto"))        # Auto, Left, Right
        self.setdefault("M_Startpoint_Entry_Length", kwargs.pop('M_Startpoint_Entry_Length', 0.))              # Double
        self.setdefault("M_Startpoint_Exit_Length", kwargs.pop('M_Startpoint_Exit_Length', 0.))                # Double
        self.setdefault("M_Startpoint_Incremental_Entry", kwargs.pop('M_Startpoint_Incremental_Entry', 0.))    # Double
        self.setdefault("M_Endpoint_Name", kwargs.pop('M_Startpoint_Entry_Type', "Unknown"))                   # (Required if M_Category is Open)
        if UNIQUA_VERSION >= "0.9.10":
            self.setdefault("M_Endpoint_Manual_Threading", kwargs.pop('M_Endpoint_Manual_Threading', False))  # Boolean
            self.setdefault("M_Endpoint_Use_Custom_Threading", kwargs.pop('M_Endpoint_Use_Custom_Threading', False))  # Boolean
            self.setdefault("M_Endpoint_Hole_Diameter", kwargs.pop('M_Endpoint_Hole_Diameter', 2.))             # Double
            self.setdefault("M_Endpoint_Threading_Empty_Tank", kwargs.pop('M_Endpoint_Threading_Empty_Tank', False))  # Boolean
            self.setdefault("M_Endpoint_Threading_Jet_On", kwargs.pop('M_Endpoint_Threading_Jet_On', True))  # Boolean
            self.setdefault("M_Endpoint_Threading_UV_Movement", kwargs.pop('M_Endpoint_Threading_UV_Movement', True))  # Boolean
            self.setdefault("M_Endpoint_Threading_Z_Movement", kwargs.pop('M_Endpoint_Threading_Z_Movement', False))  # Boolean
        else:
            kwargs.pop('M_Endpoint_Manual_Threading', False)
            kwargs.pop('M_Endpoint_Use_Custom_Threading', False)
            kwargs.pop('M_Endpoint_Hole_Diameter', 2.)
            kwargs.pop('M_Endpoint_Threading_Empty_Tank', False)
            kwargs.pop('M_Endpoint_Threading_Jet_On', True)
            kwargs.pop('M_Endpoint_Threading_UV_Movement', True)
        self.setdefault("M_Endpoint_X", kwargs.pop('M_Endpoint_X', 0.))                # Double (Required if M_Category is Open)
        self.setdefault("M_Endpoint_Y", kwargs.pop('M_Endpoint_Y', 0.))                # Double (Required if M_Category is Open)
        self.setdefault("M_Endpoint_U", kwargs.pop('M_Endpoint_U', 0.))                # Double (Required if M_Category is Open)
        self.setdefault("M_Endpoint_V", kwargs.pop('M_Endpoint_V', 0.))                # Double (Required if M_Category is Open)
        
        if UNIQUA_VERSION >= "0.9.12":
            self.setdefault("M_Endpoint_A", kwargs.pop('M_Endpoint_A', 0.))                # Double
            self.setdefault("M_Endpoint_B", kwargs.pop('M_Endpoint_B', 0.))                # Double    
        else:
            kwargs.pop('M_Endpoint_A', 0.)
            kwargs.pop('M_Endpoint_B', 0.)
        self.setdefault("M_Endpoint_Reference", kwargs.pop('M_Endpoint_Reference', "Machining"))    # (Required if M_Category is Open)
        self.setdefault("M_Endpoint_Entry_Line", kwargs.pop('M_Endpoint_Entry_Line', 0))                     # Int (Required if M_Category is Open)
        self.setdefault("M_Endpoint_Entry_Side", kwargs.pop('M_Endpoint_Entry_Side', "OffsetEdm"))           # String (OffsetEdm, ZoneLength (Required if M_Category is Open))
        self.setdefault("M_Endpoint_Entry_Position_Percent", kwargs.pop('M_Endpoint_Entry_Position_Percent', 0.))        # (Required if M_Category is Open)
#        self.setdefault("M_Endpoint_Entry_Position_mm", kwargs.pop('M_Endpoint_Entry_Position_mm', 0.))            # M_Endpoint_Enabled
        self.setdefault("M_Endpoint_Entry_Perpendicular_Zone_Length", kwargs.pop('M_Endpoint_Entry_Perpendicular_Zone_Length', 0.))          # Double
        self.setdefault("M_Endpoint_Entry_Tangent_Zone_ArcRadius", kwargs.pop('M_Endpoint_Entry_Tangent_Zone_ArcRadius', 0.))                # Double
        self.setdefault("M_Endpoint_Entry_Type", kwargs.pop('M_Endpoint_Entry_Type', "Perpendicular"))       # String (Perpendicular, Tangent, Straight (Required if M_Category is Open))
        self.setdefault("M_Endpoint_Approach_Type", kwargs.pop('M_Endpoint_Approach_Type', "Straight"))      # String (Triangle, Rectangle, Straight (Required if M_Category is Open))
        self.setdefault("M_Endpoint_Approach_Width", kwargs.pop('M_Endpoint_Approach_Width', 0.))            # Double                   # (Required if M_Category is Open)
        self.setdefault("M_Endpoint_Approach_Height", kwargs.pop('M_Endpoint_Approach_Height', 0.))          # Double                 # (Required if M_Category is Open)
        self.setdefault("M_Endpoint_Approach_Side", kwargs.pop('M_Endpoint_Approach_Side', "Auto"))          # (Required if M_Category is Open)
        self.setdefault("M_Endpoint_Enabled", kwargs.pop('M_Endpoint_Enabled', False))                       # (Required if M_Category is Open)
        self.setdefault("M_Endpoint_Exit_As_Entry", kwargs.pop('M_Endpoint_Exit_As_Entry', False))           # (Required if M_Category is Open)
        self.setdefault("M_Macro_Encoding", kwargs.pop('M_Macro_Encoding', "GfmsCmd"))
        self.setdefault("M_Before_First_Pass", kwargs.pop('M_Before_First_Pass', "None"))
        self.setdefault("M_Before_Each_Passes", kwargs.pop('M_Before_Each_Passes', "None"))
        self.setdefault("M_After_Each_Passes", kwargs.pop('M_After_Each_Passes', "None"))
        self.setdefault("M_After_Last_Pass", kwargs.pop('M_After_Last_Pass', "None"))
        self.setdefault("M_Creator_Data", kwargs.pop('M_Creator_Data', ""))                             # (Reserved for Creator related data. Should not be used for large set of data.)
        self.setdefault("M_Target_Name", kwargs.pop('M_Target_Name', "Edm"))                            # Technologie Name (Could be empty, then each target will be generated separately on the job, even if the values are the same of an already existing target.If the same name is already used in other machining, all of them will share the same instance of the target)
        if UNIQUA_VERSION < "0.9.11":                                        # Obsolete since 0.9.11. They can be replaced by dedicated technology criteria. 
            self.setdefault("M_Wire_Name", kwargs.pop('M_Wire_Name', "AC Cut A 900"))                   # String (See Wire Names sheet)
            self.setdefault("M_Wire_Diameter", kwargs.pop('M_Wire_Diameter', 0.25))                     # Double (unit is [um])
        else: 
            self.setdefault("M_Wire_Name", kwargs.pop('M_Wire_Name', ""))                               # M_Wire_Name and M_Wire_Diameter are obsolete. They can be replaced by dedicated technology criteria. 
            self.setdefault("M_Wire_Diameter", kwargs.pop('M_Wire_Diameter', 0.))
        self.setdefault("M_Technology_Selection", kwargs.pop('M_Technology_Selection', "First"))        # String (First, User, MinimalDb)
        if UNIQUA_VERSION >= "1.0.0":
            self.setdefault("M_Technology_Offset_Zero", kwargs.pop('M_Technology_Offset_Zero', False))  # 
        if UNIQUA_VERSION >= "0.9.11":
            self.setdefault("M_Passes_Status", kwargs.pop('M_Passes_Status', ""))                       # "True;True;False;True;True"  # (since Sept 2021) TODO"List of Boolean values separated by "";"" character. If omitted or set to empty string, all passes will beconsidered enabled"
            if UNIQUA_VERSION >= "1.0.0":
                self.setdefault("M_Passes_Offset_Zero", kwargs.pop('M_Technology_Offset_Zero', ""))     #
            if UNIQUA_VERSION >= "0.9.12":
                self.setdefault("M_Mode_A", kwargs.pop('M_Mode_A', "Lock"))
                self.setdefault("M_Mode_B", kwargs.pop('M_Mode_B', "Lock"))  
            else:
                kwargs.pop('M_Mode_A', "")
                kwargs.pop('M_Mode_B', "")
            self.setdefault("M_Spin_Speed_A", kwargs.pop('M_Spin_Speed_A', ""))  
            self.setdefault("M_Spin_Speed_B", kwargs.pop('M_Spin_Speed_B', ""))
            self.setdefault("M_Spin_Sense_A", kwargs.pop('M_Spin_Sense_A', ""))
            self.setdefault("M_Spin_Sense_B", kwargs.pop('M_Spin_Sense_B', ""))
        else:
            kwargs.pop('M_Passes_Status', "")
            kwargs.pop('M_Mode_A', "")
            kwargs.pop('M_Mode_B', "")
            kwargs.pop('M_Spin_Speed_A', "")
            kwargs.pop('M_Spin_Speed_B', "")
            kwargs.pop('M_Spin_Sense_A', "")
            kwargs.pop('M_Spin_Sense_B', "")
        self.setdefault("M_Technology_Criteria_Array", kwargs.pop('M_Technology_Criteria_Array', []))   # TechnologyCriteriaArray()
        if UNIQUA_VERSION >= "0.9.12":
            self.setdefault("M_Expected_Technology", kwargs.pop('M_Expected_Technology', ExpectedTechnology()))           # Dict of ExpectedTechnology
        else:
            kwargs.pop('M_Expected_Technology', {})

        if UNIQUA_VERSION >= "1.1.0":
            self.setdefault("M_technology_Transformation_Array", kwargs.pop('M_technology_Transformation_Array', []))   # Array of Transformations
        self.setdefault("M_Point_Array", kwargs.pop('M_Point_Array', []))                               # Array of Point
        self.setdefault("M_Sector_Array", kwargs.pop('M_Sector_Array', []))                             # Array of Sector
        #
        assert not kwargs, 'invalid keyword args {}'.format( kwargs )

    def setGeometry(self, Name=None, NCDataString=None, autocount=True, noHeader=False):
        self["M_Geometry_Name"] = Name
        if noHeader:
            self["M_Geometry"] = NCDataString
        else:
            self["M_Geometry"] = UNIQUA_GEOMETRY_HEADER + NCDataString

    def setPocketing(self, Name=None, NCDataString=None, autocount=True, noHeader=False):
        self["M_Geometry_Pocketing_Name"] = Name
        if noHeader:
            self["M_Geometry_Pocketing"] = NCDataString
        else:
            self["M_Geometry_Pocketing"] = UNIQUA_GEOMETRY_HEADER + NCDataString

    def setExpectedTechnology(self, Params):
        self["M_Expected_Technology"] = ExpectedTechnology(Params)

    def addexpectedcriteria(self, Params):
        if "M_Expected_Technology" in self:
            self["M_Expected_Technology"].addexpectedcriteria(Params)

    def addExpectedPass(self, Params):
        if "M_Expected_Technology" in self:
            self["M_Expected_Technology"].addExpectedPass(Params)

    def addPassCriteria(self, Params):
        if "M_Expected_Technology" in self:
            self["M_Expected_Technology"].addPassCriteria(Params)

    def addPoint(self, Params):
        if "M_Point_Array" in self:
            self["M_Point_Array"].append(Point(Params))

    def addSector(self, Params):
        if "M_Sector_Array" in self:
            self["M_Sector_Array"].append(Sector(Params))

    def addTransformation(self, Params):
        if "M_Sector_Array" in self:
            self["M_Sector_Array"].append(Transformation(Params))

    def __str__(self, *args, **kwargs):
        return "Class: {}, M_Name: {}".format(self.__class__.__name__, self['M_Name'])

class Point(OrderedDict):
    """
    dictionary, holds the Point data
    object variable is self.point_dict
    parent is self.machining_dict["M_Point_Array"]
    """
    def __init__(self, kwargs):
        self.setdefault("PT_Name", kwargs.pop("PT_Name", ''))                                   # String
        self.setdefault("PT_Action_Encoding", kwargs.pop("PT_Action_Encoding", 'ActionList'))   # String (ActionList)
        self.setdefault("PT_Action", kwargs.pop("PT_Action", ''))                               # String (See actionList Syntax sheet)
        self.setdefault("PT_Line", kwargs.pop("PT_Line", 0))                                    # Int
        self.setdefault("PT_Position_Percent", kwargs.pop("PT_Position_Percent", 0.))           # Double
        #
        assert not kwargs, 'invalid keyword args {}'.format( kwargs )

class Sector(OrderedDict):
    """
    dictionary, holds the Point data
    object variable is self.sector_dict
    parent is self.machining_dict["M_Sector_Array"]
    """
    def __init__(self, kwargs):
        self.setdefault("S_Name", kwargs.pop("S_Name", ''))                                     # String
        self.setdefault("S_Action_Encoding", kwargs.pop("S_Action_Encoding", 'ActionList'))     # String (ActionList)
        self.setdefault("S_Action", kwargs.pop("S_Action", ''))                                 # String (See actionList Syntax sheet)
        self.setdefault("S_Point_1_Name", kwargs.pop("S_Point_1_Name", ""))                     # String
        self.setdefault("S_Point_1_Line", kwargs.pop("S_Point_1_Line", 0))                      # Int
        self.setdefault("S_Point_1_Position_Percent", kwargs.pop("S_Point_1_Position_Percent", 0.)) # Double
        self.setdefault("S_Point_1_Action", kwargs.pop("S_Point_1_Action", 0.))                 # String (See actionList Syntax sheet)
        self.setdefault("S_Point_2_Name", kwargs.pop("S_Point_2_Name", ""))                     # String
        self.setdefault("S_Point_2_Line", kwargs.pop("S_Point_2_Line", 0))                      # Int
        self.setdefault("S_Point_2_Position_Percent", kwargs.pop("S_Point_2_Position_Percent", 0.)) # Double
        self.setdefault("S_Point_2_Action", kwargs.pop("S_Point_2_Action", 0.))                 # String (See actionList Syntax sheet)
        #
        assert not kwargs, 'invalid keyword args {}'.format( kwargs )

    def __str__(self, *args, **kwargs):
        return "Class: {}, S_Name: {}".format(self.__class__.__name__, self['S_Name'])

class Transformation(OrderedDict):
    """
    dictionary, holds the Transformation data
    object variable is self.transformation_dict
    parent is self.machining_dict["M_technology_Transformation_Array"]
    """
    def __init__(self, kwargs):
        self.setdefault("T_Name", kwargs.pop("T_Name", ''))                                     # String
        #
        assert not kwargs, 'invalid keyword args {}'.format( kwargs )

    def __str__(self, *args, **kwargs):
        return "Class: {}, T_Name: {}".format(self.__class__.__name__, self['T_Name'])

class Criteria(OrderedDict):
    """
    dictionary, holds the Criteria data
    object variable is self.criteria_dict
    parent self.st_dict["ST_Technology_Criteria_Array"]
    """
    def __init__(self, kwargs):
        self.setdefault("C_Name", kwargs.pop("C_Name", ''))                 # String (Material Height Ra Taper Tkm StartPoint WorkingType FlushingCondition Priority StepNumber TechnologyName)
        self.setdefault("C_Value", kwargs.pop("C_Value", ''))               # String (See Technology Criteria sheet for details)
        self.setdefault("C_Operator", kwargs.pop("C_Operator", 'Equal'))    # String (Equal, GreaterThan, LessThan; GreaterThan and LessThan not suported yet)
        #
        assert not kwargs, 'invalid keyword args {}'.format( kwargs )

    def __str__(self, *args, **kwargs):
        return "Class: {}, C_Name: {}".format(self.__class__.__name__, self['C_Name'])

class TechnologyCriteria(OrderedDict):
    """
    dictionary, holds the TechnologyCriteria data
    object variable is self.criteria_dict
    parent self.st_dict["ST_Technology_Criteria_Array"]
    """
    def __init__(self, kwargs):
        self.setdefault("Material", kwargs.pop("Material", 'Steel'))        # String (Steel, Copper, Graphite, Hard Metal, Alluminium, Titanium, PCD_CTB010)
        self.setdefault("TechnologyName", kwargs.pop("TechnologyName", '')) # String (any user technology name; If ThechnologyName is provided, the other criteria will be ignored)
        self.setdefault("Ra", kwargs.pop("Ra", '0.'))                         # Double (micro meter; if omitted, the parameter will be ignored)
        self.setdefault("StepNumber", kwargs.pop("StepNumber", '0'))          # Int (if omitted, the parameter will be ignored)
        self.setdefault("Height", kwargs.pop("Height", '0.'))                 # Double (milli meter; if omitted, the parameter will be ignored)
        self.setdefault("Taper", kwargs.pop("Taper", '0.'))                   # Double (degree; if omitted, the parameter will be ignored)
        self.setdefault("Tkm", kwargs.pop("Tkm", '0.'))                       # Double (micro meter; if omitted, the parameter will be ignored)
        self.setdefault("QualityTf", kwargs.pop("QualityTf", ""))           # String (Hole, Outside)
        self.setdefault("StartPoint", kwargs.pop("StartPoint", 'Hole'))     # String (Hole, Outside; Hole is the default value. Can be omitted)
        self.setdefault("WorkingType", kwargs.pop("WorkingType", 'Open'))   # String (Open, Slot, Pocketing; Open is the default value. Can be omitted)
        self.setdefault("FlushingCondition", kwargs.pop("FlushingCondition", 'SealedNozzles'))  # String (No MinimalDb Query: SealedNozzles, UnsealedNozzles, SpecialConditions, ISPS, MinimalDb Query: VarioCut1, VarioCut2, VarioCut3, VarioCut4, VarioCut5, VarioCut6; SealedNozzles is the default value. Can be omitted)
        self.setdefault("Priority", kwargs.pop("Priority", 'Speed'))        # String (Precision, Speed, TurboTech; If omitted, no priority will be applied)
        self.setdefault("WireName", kwargs.pop("WireName", 'AC Cut A 900')) # String (See Wire Names sheet)
        self.setdefault("WireDiameter", kwargs.pop("WireDiameter", '250.'))   # Double (unit is [um])
### Set this only for twin wire
###        self.setdefault("WireName2", kwargs.pop("WireName2", ''))           # String (See Wire Names sheet)
###        self.setdefault("WireDiameter2", kwargs.pop("WireDiameter2", '250.')) # Double (unit is [um])
        self.setdefault("Options", kwargs.pop("Options", ''))               # String ("List of machine options identifiers separated by "";"" character ""25;30;45""")
        self.setdefault("ShotPeening", kwargs.pop("ShotPeening", 'Yes'))    # String (No, Yes, Irrilevant; Use No or Yes for a better technology selection)
        self.setdefault("Nozzle", kwargs.pop("Nozzle", '1000'))             # String (more values will be supported in the future)
        self.setdefault("MinimalDbVersion", kwargs.pop("MinimalDbVersion", MINIMALDBVERSION))   # String (x.y.z)
        #
        assert not kwargs, 'invalid keyword args {}'.format( kwargs )

class ActionListSyntax(OrderedDict):
    """
    atm. no information provided
    """
    pass

class WireNames(OrderedDict):
    """
    dictionary, used for the WireNames Data (only information purpose)
    """
    def __init__(self, *args, **kwargs):
        self.setdefault("AC Cut A 900", kwargs.pop('AC Cut A 900', ''))  # String
        self.setdefault("AC Cut D 800", kwargs.pop('AC Cut D 800', ''))  # String
        self.setdefault("AC Brass 900", kwargs.pop('AC Brass 900', ''))  # String
        self.setdefault("AC Cut AH",    kwargs.pop('AC Cut AH', ''))  # String
        self.setdefault("AC Cut SP",    kwargs.pop('AC Cut SP', ''))  # String; obsolete
        self.setdefault("AC Cut D 500", kwargs.pop('AC Cut D 500', ''))  # String
        self.setdefault("AC Brass 500", kwargs.pop('AC Brass 500', ''))  # String
        self.setdefault("AC Cut Molybdenum", kwargs.pop('AC Cut Molybdenum', ''))  # String
        self.setdefault("AC Cut VS+ 900", kwargs.pop('AC Cut VS+ 900', ''))  # String
        self.setdefault("AC Cut VH", kwargs.pop('AC Cut VH', ''))  # String
        self.setdefault("AC Cut XS", kwargs.pop('AC Cut XS', ''))  # String
        self.setdefault("AC Cut XCC", kwargs.pop('AC Cut XCC', ''))  # String
        self.setdefault("AC Cut Micro SP-Z", kwargs.pop('AC Cut Micro SP-Z', ''))  # String
        if UNIQUA_VERSION >= "1.0.0":
            self.setdefault("TWS",       kwargs.pop('TWS', ''))  # String
            self.setdefault("AC Cut GV", kwargs.pop('AC Cut GV', ''))  # String
            self.setdefault("AC Cut G",  kwargs.pop('AC Cut G', ''))  # String
        #
        assert not kwargs, 'invalid keyword args {}'.format( kwargs )

    def __str__(self, *args, **kwargs):
        return "Class: {}".format(self.__class__.__name__)

class ExpectedTechnology(OrderedDict):
    """
    dictionary, used for the ST_Expected_Technology
    """
    def __init__(self, kwargs=None):
        if kwargs is not None:
            self.setdefault('ET_Pass_Number', kwargs.pop('ET_Pass_Number', 1))         # Int
            self.setdefault('ET_Priority', kwargs.pop('ET_Priority', "Speed"))         # String (Precision, Speed, TurboTech)
            self.setdefault('ET_Ra', kwargs.pop('ET_Ra', 0.))                          # Double
            self.setdefault('ET_Tf', kwargs.pop('ET_Tf', 0.))                          # Double
            self.setdefault('ET_Tkm', kwargs.pop('ET_Tkm', 0.))                        # Double
            self.setdefault('ET_Speed', kwargs.pop('ET_Speed', 0.))                    # Double
            self.setdefault('ET_Pall', kwargs.pop('ET_Pall', 0))                       # Int
            self.setdefault('ET_Options', kwargs.pop('ET_Options', 0))                 # Int
            self.setdefault('ET_Info', kwargs.pop('ET_Info', ''))                      # String
            self.setdefault('ET_Extended_Info', kwargs.pop('ET_Extended_Info', ''))    # String
            self.setdefault('ET_Pass_Array', kwargs.pop('ET_Pass_Array', []))          # array of Expected Pass
            #
            assert not kwargs, 'invalid keyword args {}'.format( kwargs )
        # else:
        #     self.setdefault('ET_Pass_Array', [])          # array of Expected Pass

    def addexpectedcriteria(self, Params):
        self.update(Params)

    def addExpectedPass(self, Params):
        if "ET_Pass_Array" not in self:
            self.setdefault('ET_Pass_Array', [])          # array of Expected Pass
        
        self["ET_Pass_Array"].append(ExpectedPass(Params))

    def addPassCriteria(self, Params):
        if "ET_Pass_Array" in self:
            EP = self["ET_Pass_Array"][-1]      # use last added dict
            EP.addPassCriteria(Params)

    def __str__(self, *args, **kwargs):
        return "Class: {}".format(self.__class__.__name__)

class ExpectedPass(OrderedDict):
    """
    dictionary, used for the ET_Pass_Array
    """
    def __init__(self, kwargs=None):
        if kwargs is not None:
            self.setdefault('EP_Name', kwargs.pop('EP_Name', "EP_Name"))               # String
            self.setdefault('EP_Tool', kwargs.pop('EP_Tool', 1))                       # Int (1,2) 
            self.setdefault('EP_Speed', kwargs.pop('EP_Speed', 10.))                   # Double
            self.setdefault('EP_Offset', kwargs.pop('EP_Offset', 200))                 # Int (unit is 0.1 um)
            #
            assert not kwargs, 'invalid keyword args {}'.format( kwargs )

    def addPassCriteria(self, Params):
        self.update(Params)

    def __str__(self, *args, **kwargs):
        return "Class: {}, EP_Name: {}".format(self.__class__.__name__, self['EP_Name'])

class ActionsForPoints(OrderedDict):
    """
    dictionary, used for the ActionsForPoints Data
    Action List Samples:
      InstructionMachineProgrammedStop,1
      TaperAngle,0.5
      InclinationLeft
      RadiusTaper:CornerRadius
      MinimumRoundingRadiusOn,1.0,0.4
      AdditionalClearance,1.5:TaperAngle,0.6
    """
    def __init__(self, name="", *args, **kwargs):
        self.setdefault("InstructionMachineProgrammedStop", kwargs.pop('InstructionMachineProgrammedStop', "1"))  # String
        self.setdefault("TaperAngle", kwargs.pop('TaperAngle', 0.5))  # Float
        self.setdefault("InclinationLeft", kwargs.pop('InclinationLeft', ""))  # unknown
        self.setdefault("RadiusTaper", kwargs.pop('RadiusTaper', 1.5))  # Float; RadiusTaper:CornerRadius
        self.setdefault("MinimumRoundingRadiusOn", kwargs.pop('MinimumRoundingRadiusOn', 1.5))  # Float; MinimumRoundingRadiusOn,1.0,0.4
        self.setdefault("AdditionalClearance", kwargs.pop('AdditionalClearance', ""))  # Float; AdditionalClearance,1.5:TaperAngle,0.6
        #self.setdefault("PassNumber", kwargs.pop('PassNumber', 1))  # Int
        #
        assert not kwargs, 'invalid keyword args {}'.format( kwargs )

    def __str__(self, *args, **kwargs):
        return "Class: {}".format(self.__class__.__name__)

class ActionsForSectors(OrderedDict):
    """
    dictionary, used for the ActionsForSectors Data
    """
    def __init__(self, name="", *args, **kwargs):
        self.setdefault("InclinationCancel", kwargs.pop('InclinationCancel', ''))                   # ?
        self.setdefault("InclinationLeft", kwargs.pop('InclinationLeft', ''))                       # ?
        self.setdefault("InclinationRight", kwargs.pop('InclinationRight', ''))                     # ?
        self.setdefault("RadiusTaper", kwargs.pop('RadiusTaper', ''))                               # ?
        self.setdefault("ConicTaper", kwargs.pop('ConicTaper', ''))                                 # ?
        self.setdefault("CornerRadius", kwargs.pop('CornerRadius', ''))                             # ?
        self.setdefault("MinimumConicalAngle", kwargs.pop('MinimumConicalAngle', ''))               # ?
        self.setdefault("MeanConicalAngle", kwargs.pop('MeanConicalAngle', ''))                     # ?
        self.setdefault("MaximumConicalAngle", kwargs.pop('MaximumConicalAngle', ''))               # ?
        self.setdefault("TaperAngle", kwargs.pop('TaperAngle', ''))                                 # ?
        self.setdefault("Angle", kwargs.pop('Angle', 0.))                                           # Double (degree)
        self.setdefault("MinimumRoundingRadiusOn", kwargs.pop('MinimumRoundingRadiusOn', ''))       # ?
        self.setdefault("R", kwargs.pop('R', 0.))                                                   # Double (mm)
        self.setdefault("K", kwargs.pop('K', 0.))                                                   # Double (mm)
        self.setdefault("MinimumRoundingRadiusOff", kwargs.pop('MinimumRoundingRadiusOff', ''))     # ?
        self.setdefault("AdditionalClearance", kwargs.pop('AdditionalClearance', 0.))               # Double (mm)
        self.setdefault("Clearance", kwargs.pop('Clearance', 0.))                                   # Double (mm)
        #
        assert not kwargs, 'invalid keyword args {}'.format( kwargs )

    def __str__(self, *args, **kwargs):
        return "Class: {}".format(self.__class__.__name__)

class SequenceOperation(OrderedDict):
    """
    dictionary, used for the SequenceOperation Data
    object variable is self.so_dict
    parent is self.piece_dict["P_Sequence_Operation_Array"]
    """
    cnt = 0     # static variable
    #
    def __init__(self, kwargs):
        SequenceOperation.cnt += 1
        self.setdefault("SO_Name", kwargs.pop('SO_Name', "Sequence_Operation_{}".format(SequenceOperation.cnt)))    # String
        if UNIQUA_VERSION >= "1.3.0":
            self.setdefault("SO_Language", kwargs.pop('SO_Language', 'Cmd'))                                        # String
        self.setdefault("SO_Code", kwargs.pop('SO_Code', ''))                                                       # String (<NcCode>)
        #
        assert not kwargs, 'invalid keyword args {}'.format( kwargs )

    def __str__(self, *args, **kwargs):
        return "Class: {}, SO_Name: {}".format(self.__class__.__name__, self['SO_Name'])

class SequenceGeometry(OrderedDict):
    """
    dictionary, used for the SequenceGeometry Data
    object variable is self.sg_dict
    parent is self.piece_dict["P_Sequence_Geometry_Array"]
    """
    cnt = 0     # static variable
    #
    def __init__(self, kwargs):
        SequenceGeometry.cnt += 1
        self.setdefault("SG_Name", kwargs.pop('SG_Name', "Sequence_Geometry_{}".format(SequenceGeometry.cnt)))      # String
        self.setdefault("SG_Code", kwargs.pop('SG_Code', ''))                     # String (<NcCode>)
        #
        assert not kwargs, 'invalid keyword args {}'.format( kwargs )

    def __str__(self, *args, **kwargs):
        return "Class: {}, SG_Name: {}".format(self.__class__.__name__, self['SG_Name'])

class SequenceTarget(OrderedDict):
    """
    dictionary, used for the SequenceTarget Data
    object variable is self.st_dict
    parent is self.piece_dict["P_Sequence_Target_Array"]
    """
    cnt = 0     # static variable
    #
    def __init__(self, kwargs):
        SequenceTarget.cnt += 1
        self.setdefault("ST_Name", kwargs.pop('ST_Name', "Sequence_Target_{}".format(SequenceTarget.cnt)))  # String
        self.setdefault("ST_Technology_Selection", kwargs.pop('ST_Technology_Selection', 'MinimalDb'))      # String (First, User, MinimalDb)
        self.setdefault("ST_Technology_Offset_Zero", kwargs.pop('ST_Technology_Offset_Zero', False))        # String (First, User, MinimalDb)
        self.setdefault("ST_Technology_Criteria_Array", kwargs.pop('ST_Technology_Criteria_Array', []))     # array of Criteria
        self.setdefault("ST_technology_Transformation_Array", kwargs.pop('ST_technology_Transformation_Array', []))     # array of Transformations
        self.setdefault("ST_Expected_Technology",  kwargs.pop('ST_Expected_Technology', {}))                # Type of ExpectedTechnology (OrderedDict)
        #
        assert not kwargs, 'invalid keyword args {}'.format( kwargs )

    def addPassCriteria(self, Params):
        if "ST_Expected_Technology" in self:
            self["ST_Expected_Technology"].addPassCriteria(Params)

    def addexpectedcriteria(self, Params):
        if "ST_Expected_Technology" in self:
            self["ST_Expected_Technology"].addexpectedcriteria(Params)

    def addCriteria(self, Params):
        if "ST_Technology_Criteria_Array" in self:
            self["ST_Technology_Criteria_Array"].append(Criteria(Params))

    def setExpectedTechnology(self, Params):
        self["ST_Expected_Technology"] = ExpectedTechnology(Params)

    def addExpectedPass(self, Params):
        if "ST_Expected_Technology" in self:
            if hasattr(self["ST_Expected_Technology"], "addExpectedPass"):
                self["ST_Expected_Technology"].addExpectedPass(Params)

    def __str__(self, *args, **kwargs):
        return "Class: {}, ST_Name: {}".format(self.__class__.__name__, self['ST_Name'])

class PieceVariable(OrderedDict):
    """
    dictionary, used for the ET_Pass_Array
    object variable is self.pv_dict
    parent is self.piece_dict["P_Variable_Array"]
    """
    cnt = 0     # static variable
    #
    def __init__(self, kwargs):
        PieceVariable.cnt += 1
        self.setdefault('PV_Name', kwargs.pop('PV_Name', "Piece_Variable{}".format(PieceVariable.cnt)))     # String (Could be omitted)
        self.setdefault('PV_Number', kwargs.pop('PV_Number', PieceVariable.cnt))                            # Int [100,499]
        self.setdefault('PV_Value', kwargs.pop('PV_Value', ''))                                             # String
        #
        assert not kwargs, 'invalid keyword args {}'.format( kwargs )

    def __str__(self, *args, **kwargs):
        return "Class: {}, PV_Name: {}".format(self.__class__.__name__, self['PV_Name'])

class CNCPoint(OrderedDict):
    """
    dictionary, used for the ET_Pass_Array
    object variable is self.cncp_dict
    parent is self.piece_dict["P_Cnc_Point_Array"]
    """
    cnt = 0     # static variable
    #
    def __init__(self, kwargs):
        CNCPoint.cnt += 1
        self.setdefault("CP_Name", kwargs.pop('CP_Name', "Cnc_Point_{}".format(CNCPoint.cnt)))  # String
        self.setdefault("CP_Number", kwargs.pop('CP_Number', CNCPoint.cnt))                     # Int [1001,2000]
        self.setdefault("CP_Reference", kwargs.pop('CP_Reference', "Machining"))    # String (Guides, Machine, Pallet, Part, Machining, NotDefined)  ### Simulator doesn't accept Part !!
        x = kwargs.pop('CP_Axis_X', None)
        if x is not None:
            self.setdefault("CP_Axis_X", x)       # Double    (If omitted, the axis will not be moved. Not the same to set it to 0.0)
        y = kwargs.pop('CP_Axis_Y', None)
        if y is not None:
            self.setdefault("CP_Axis_Y", y)       # Double    (If omitted, the axis will not be moved. Not the same to set it to 0.0)
        z = kwargs.pop('CP_Axis_Z', None)
        if z is not None:
            self.setdefault("CP_Axis_Z", z)       # Double    (If omitted, the axis will not be moved. Not the same to set it to 0.0)
        a = kwargs.pop('CP_Axis_A', None)
        if a is not None:
            self.setdefault("CP_Axis_A", a)       # Double    (If omitted, the axis will not be moved. Not the same to set it to 0.0)
        b = kwargs.pop('CP_Axis_B', None)
        if b is not None:
            self.setdefault("CP_Axis_B", b)       # Double    (If omitted, the axis will not be moved. Not the same to set it to 0.0)
        c = kwargs.pop('CP_Axis_C', None)
        if c is not None:
            self.setdefault("CP_Axis_C", c)       # Double    (If omitted, the axis will not be moved. Not the same to set it to 0.0)
        w = kwargs.pop('CP_Axis_W', None)
        if w is not None:
            self.setdefault("CP_Axis_W", w)       # Double    (If omitted, the axis will not be moved. Not the same to set it to 0.0)
        u = kwargs.pop('CP_Axis_U', None)
        if u is not None:
            self.setdefault("CP_Axis_U", u)       # Double    (If omitted, the axis will not be moved. Not the same to set it to 0.0)
        v = kwargs.pop('CP_Axis_V', None)
        if v is not None:
            self.setdefault("CP_Axis_V", v)       # Double    (If omitted, the axis will not be moved. Not the same to set it to 0.0)
        #
        assert not kwargs, 'invalid keyword args {}'.format( kwargs )

    def __str__(self, *args, **kwargs):
        return "Class: {}, CP_Name: {}".format(self.__class__.__name__, self['CP_Name'])

# Main class

class JsonBase(object):
    ''' aus: 'https://de.wikipedia.org/wiki/JavaScript_Object_Notation`
    Die JavaScript Object Notation (JSON [ˈdʒeɪsən]) ist ein kompaktes Datenformat in einer einfach lesbaren Textform und dient dem Zweck des Datenaustausches
    zwischen Anwendungen. Jedes gültige JSON-Dokument ist gültiges JavaScript und kann per eval() interpretiert werden. Davon abgesehen ist JSON von der
    Skriptsprache unabhängig. Parser existieren in allen verbreiteten Sprachen.
    
    Beispiel:
    
    {
        "Herausgeber": "Xema",
        "Nummer": "1234-5678-9012-3456",
        "Deckung": 2e+6,
        "Waehrung": "EURO",
        "Inhaber":
        {
            "Name": "Mustermann",
            "Vorname": "Max",
            "maennlich": true,
            "Hobbys": ["Reiten", "Golfen", "Lesen"],
            "Alter": 42,
            "Kinder": [],
            "Partner": null
        }
    }
    
    Vergleich XML:
    
    <Kreditkarte Herausgeber="Xema" Nummer="1234-5678-9012-3456" Deckung="2e+6" Waehrung="EURO">
      <Inhaber Name="Mustermann" Vorname="Max" maennlich="true" Alter="42" Partner="null">
        <Hobbys>
          <Hobby>Reiten</Hobby>
          <Hobby>Golfen</Hobby>
          <Hobby>Lesen</Hobby>
        </Hobbys>
        <Kinder />
      </Inhaber>
    </Kreditkarte>
    
    '''

    def __init__(self, jsonfile=None):
        '''
        In Python, JSON exists as a string. For example ...
        here we support two modi: Piece for normal purpose and Batch for ...don't really know
        '''
        self.jsonfile = jsonfile        # nc output filename (abs path)
        self.json_string  = None        # experimental
        
        # Definitions of essential objects which can be arrays
        self.multi_purpose_dict = None
        self.manifest_dict = None
        self.piece_dict    = None
        self.batch_dict    = None
        
        # additional dicts
        self.pallet_dict   = None
        self.criteria_dict = None
        self.point_dict    = None
        self.sector_dict   = None
        self.transformation_dict   = None

        # for dynamic mode
        self.machining_dict = None  # for actual Machining
        
        # for structured mode
        self.pv_dict   = None       # for actual PieceVariable
        self.cncp_dict = None       # for actual CncPoint
        self.so_dict   = None       # for actual SequenceOperation
        self.sg_dict   = None       # for actual SequenceGeometry
        self.st_dict   = None       # for actual SequenceTarget
        #
        self.main_dict = OrderedDict()       # Root dictionary -> it's important to use ordered dict, otherwise we have another order

    def __str__(self, *args, **kwargs):
        l1 = "None" if self.multi_purpose_dict is None else len(self.multi_purpose_dict)
        l2 = "None" if self.manifest_dict is None else len(self.manifest_dict)
        l3 = "None" if self.piece_dict is None else len(self.piece_dict)
        l4 = "None" if self.batch_dict is None else len(self.batch_dict)
        return "Class: {}, jsonfile: {}  manifest: {}  pieces: {}  batch: {}".format(self.__class__.__name__, os.path.basename(self.jsonfile), l2, l3, l4)
        
    def setfilename(self, jsonfile):
        self.jsonfile = jsonfile        # nc output filename (abs path)

    def getfilename(self):
        return self.jsonfile            # report nc output filename (abs path)
        
    def isempty(self):
        return self.multi_purpose_dict is None and self.manifest_dict is None and self.piece_dict is None and self.batch_dict is None
        
    def json2dict(self, json_string =None):
        # parse a json string to a python dict
        if json_string  is None:
            if self.json_string  is None:
                json_string  = '{"error": "no data"}'
            else:
                json_string  = self.json_string 
        self.main_dict = json.loads(json_string )

    def dict2json(self, python_dict=None):
        # transform a python dict to a json string
        if python_dict is None:
            if self.main_dict is None:
                python_dict = {"error": "no data",}
            else:
                python_dict = self.main_dict
        self.json_string  = json.dumps(python_dict, indent=4, sort_keys=False)

    def write(self, jsonfile=None):
        # write json file
        if jsonfile is None:
            if self.jsonfile is None:
                return
            else:
                jsonfile = self.jsonfile

        with open(jsonfile, 'w', encoding="utf-8") as f:
            json.dump(self.main_dict, f, indent=2, separators=(',', ': '), sort_keys=False, ensure_ascii=False)
    
    def read(self, jsonfile=None):
        # read json file
        if jsonfile is None:
            if self.jsonfile is None:
                return
            else:
                jsonfile = self.jsonfile

        if os.path.exists(jsonfile):
            with open(jsonfile, 'r', encoding="utf-8") as f:
                self.json_string  = json.load(f)

    def SingleSentenceData(self):
        """
        JUST AN Example of single sentence nc data
        """
        single_sentence_string = """G00 X0.0 Y-8.0 Z 20
G01 X-28.0 Y-8.0
G02 X-38.0 Y2.0 I0.0 J10.0
G01 X-38.0 Y35.0
G02 X-32.0 Y41.0 I6.0 J0.0
G01 X-20.0 Y41.0
G02 X-10.0 Y31.0 I0.0 J-10.0
G01 X-10.0 Y25.0
G03 X-4.0 Y19.0 I6.0 J0.0
G01 X18.0 Y19.0
G02 X30.0 Y7.0 I0.0 J-12.0
G01 X30.0 Y-6.0
G02 X28.0 Y-8.0 I-2.0 J0.0
G01 X0.0 Y-8.0"""
        return single_sentence_string
    
    def DoubleSentenceData(self):
        """
        JUST AN Example of double sentence nc data
        """
        double_sentence_string = """G00 X0.0 Y-8.0 Z0.0 G00 X0.0 Y-9.7498 Z20.0
G01 X-28.0 Y-8.0 G01 X-28.0 Y-9.7498
G02 X-38.0 Y2.0 I0.0 J10.0 G02 X-39.7498 Y2.0 I0.0 J11.7498
G01 X-38.0 Y35.0 G01 X-39.7498 Y35.6984
G02 X-32.0 Y41.0 I6.0 J0.0 G02 X-33.7498 Y41.6984 I6.0 J0.0
G01 X-20.0 Y41.0 G01 X-16.4735 Y41.6984
G02 X-10.0 Y31.0 I0.0 J-10.0 G02 X-6.4735 Y31.6984 I0.0 J-10.0
G01 X-10.0 Y25.0 G01 X-6.4735 Y25.0
G03 X-4.0 Y19.0 I6.0 J0.0 G03 X-0.4735 Y19.0 I6.0 J0.0
G01 X18.0 Y19.0 G01 X19.7498 Y19.0
G02 X30.0 Y7.0 I0.0 J-12.0 G02 X31.7498 Y7.0 I0.0 J-12.0
G01 X30.0 Y-6.0 G01 X31.7498 Y-6.0
G02 X28.0 Y-8.0 I-2.0 J0.0 G02 X28.0 Y-9.7498 I-3.7498 J0.0
G01 X0.0 Y-8.0 G01 X0.0 Y-9.7498"""
        return double_sentence_string
        
    def PocketingData(self):
        """
        JUST AN Example of pocketing nc data
        """
        pocketing_string = """G00 X0.0 Y-8.0 Z 20
G01 X-28.0 Y-8.0
G02 X-38.0 Y2.0 I0.0 J10.0
G01 X-38.0 Y35.0
G02 X-32.0 Y41.0 I6.0 J0.0
G01 X-20.0 Y41.0
G02 X-10.0 Y31.0 I0.0 J-10.0
G01 X-10.0 Y25.0
G03 X-4.0 Y19.0 I6.0 J0.0
G01 X18.0 Y19.0
G02 X30.0 Y7.0 I0.0 J-12.0
G01 X30.0 Y-6.0
G02 X28.0 Y-8.0 I-2.0 J0.0
G01 X0.0 Y-8.0"""
        return pocketing_string

    # def Manifest(self, Params={}, set_value_only_if_key_exist=True):
    #     """
    #     The MANIFEST Object will be always at the top level of the JSON structure. The MANIFEST object will be always parallel with the other top element, like the BATCH
    #     object or the PIECE object. The MANIFEST object stores general information about the job, the format version and cares about the machine compatibility.         
    #     """
    #     # Creation of actual tile stamp
    #     if PY3:
    #         timestring = datetime.now().astimezone().isoformat()
    #     else:
    #         #timestring = datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%f%z')   # TODO : tzinfo (%z) is empty
    #         timestring = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%f+00:00')
    #
    #     self.manifest_dict = OrderedDict()
    #     self.manifest_dict["MAN_Version"] = UNIQUA_VERSION
    #     self.manifest_dict["MAN_Scope"] = "Piece"               # Piece or Batch
    #     self.manifest_dict["MAN_Author"] = ""                   # for cust. string; works for dynamic and sequential
    #     self.manifest_dict["MAN_Description"] = "Manifest description"
    #     self.manifest_dict["MAN_Creator"] = "Manifest creator"
    #     self.manifest_dict["MAN_Creator_Version"] = "Python: {}".format(sys.version)
    #     self.manifest_dict["MAN_Creator_Data"] = ""
    #     self.manifest_dict["MAN_Creator_Date"] = timestring
    #
    #     self.checkStringType(Params)
    #     self.manifest_dict.update(Params)
    #     # if set_value_only_if_key_exist:
    #     #     for key, val in self.manifest_dict.items():
    #     #         if key in Params:
    #     #             val = Params[key]
    #     #             self.manifest_dict[key] = val
    #     #
    #     # else:
    #     #     self.manifest_dict.update(Params)
    #     #
    #     return self.manifest_dict
    
    # def Piece(self, Params={}):
    #     """
    #     The PIECE JSON object stores all information for a single dynamic job. Inside the PIECE object can be several MACHINING objects.
    #     Several PIECE objects will be stored togehter into a PIECE Array, into a Batch Object.
    #     """
    #     self.piece_dict = OrderedDict()
    #     self.piece_dict["P_Name"] = "UNIQUA_Piece_01"
    #     if UNIQUA_VERSION >= "0.9.6.1":
    #         self.piece_dict["P_ErpId"] = ""
    #     self.piece_dict["P_Description"] = "Further information about the piece"
    #     self.piece_dict["P_Material"] = "Steel"                  # Steel, Copper, Graphite, Hard Metal, Alluminium, Titanium, PCD_CTB010
    #
    #     if UNIQUA_VERSION >= "0.9.12":
    #         self.piece_dict["P_Operation_Language"] = "Cmd"     # or "Iso"
    #
    #     self.piece_dict["P_Priority"] = 0                        # >=0
    #     if UNIQUA_OPERATION_MODE == DYNAMIC_JSON:
    #         if UNIQUA_VERSION >= "0.9.10":
    #             self.piece_dict["P_Has_Part_Definition"] = True     # (Required) true,false
    #     else:
    #         self.piece_dict["P_Has_Part_Definition"] = False     # (Required) true,false
    #
    #     self.piece_dict["P_Dimensions_Width_X"] = 150.0          # (Required if P_Has_Part_Definition is true)
    #     self.piece_dict["P_Dimensions_Depth_Y"] = 100.0          # (Required if P_Has_Part_Definition is true)
    #     self.piece_dict["P_Dimensions_Height_Z"] = 20.0          # (Required if P_Has_Part_Definition is true)
    #     self.piece_dict["P_Origin_X"] = 0.0                     # >=0
    #     self.piece_dict["P_Origin_Y"] = 0.0                     # >=0
    #     self.piece_dict["P_Origin_Z"] = 0.0                     # >=0
    #     self.piece_dict["P_Enable_Measurable_Point"] = True
    #     self.piece_dict["P_Measureable_Point_X"] = 0.0           # (Required if P_Enable_Measurable_Point is true)
    #     self.piece_dict["P_Measureable_Point_Y"] = 0.0           # (Required if P_Enable_Measurable_Point is true)
    #     self.piece_dict["P_Measureable_Point_Z"] = 0.0           # (Required if P_Enable_Measurable_Point is true)
    #     self.piece_dict["P_Measureable_Point_Rot_C"] = 0.0       # (Required if P_Enable_Measurable_Point is true)
    #     if UNIQUA_OPERATION_MODE == SEQUENTIAL_JSON or UNIQUA_VERSION >= "0.9.10":
    #         self.piece_dict["P_Correction_X"] = 0.0              # (Required if P_Enable_Measurable_Point is true)
    #         self.piece_dict["P_Correction_Y"] = 0.0              # (Required if P_Enable_Measurable_Point is true)
    #         self.piece_dict["P_Correction_Z"] = 0.0              # (Required if P_Enable_Measurable_Point is true)
    #         self.piece_dict["P_Correction_A"] = 0.0              # (Required if P_Enable_Measurable_Point is true)
    #         self.piece_dict["P_Correction_B"] = 0.0              # (Required if P_Enable_Measurable_Point is true)
    #         self.piece_dict["P_Correction_C"] = 0.0              # (Required if P_Enable_Measurable_Point is true)
    #
    #     if UNIQUA_VERSION >= "0.9.12":
    #         self.piece_dict["P_Reference_Aux_A"] = 0.0
    #         self.piece_dict["P_Reference_Aux_B"] = 0.0
    #
    #     if UNIQUA_OPERATION_MODE == DYNAMIC_JSON:        
    #         self.piece_dict["P_Security_Level"] = 0.05
    #         self.piece_dict["P_Return_Level"] = 30.0
    #         self.piece_dict["P_Macro_Encoding"] = "GfmsCmd"          # GfmsCmd (Required if a macro is specified)
    #         self.piece_dict["P_Before_First_Pass_Of_Piece"] = "None"
    #         self.piece_dict["P_Before_Each_Pass_Of_Piece"] = "None"
    #         self.piece_dict["P_After_Each_Pass_Of_Piece"] = "None"
    #         self.piece_dict["P_After_Last_Pass_Of_Piece"] = "None"
    #         self.piece_dict["P_Measurement_Strategy_Macro_Encoding"] = "GfmsCmd"      # GfmsCmd (Required if a measurement strategy is specified)
    #         self.piece_dict["P_Measurement_Strategy"] = "None"
    #
    #     self.piece_dict["P_Creator_data"] = ""
    #     self.piece_dict["P_Pallet_Name"] = ""                   # Empty string or null value, means that the piece is not in a pallet
    #
    #     if UNIQUA_OPERATION_MODE == DYNAMIC_JSON:
    #         self.piece_dict["P_Machining_Array"] = []
    #
    #     # Sequential Only Parameters
    #     if UNIQUA_OPERATION_MODE == SEQUENTIAL_JSON:
    #         self.piece_dict["P_Variable_Array"] = []
    #         # Piece Variable
    #         #self.PieceVariable( {"PV_Name": "PV Variable", "PV_Number", 0, "PV_Value": ""} )
    #         self.piece_dict["P_Cnc_Point_Array"] = []
    #         # CNC Point
    #         #self.CNCPoint( {"CP_Name": "Cnc Point", "CP_Number", 0,} )
    #         self.piece_dict["P_Sequence_Operation_Array"] = []
    #             # self.SequenceOperation({"SO_Name": "Main", "SO_Code": "CCF,PROG_ExCamIso1\r\n"}),
    #             # self.SequenceOperation({"SO_Name": "PROG_ExCamIso1", "SO_Code": "UNIT,MM\r\nMSG,Program PROG_ExCamIso1 started\r\nCCF,PROG_CUT_1_quadr_1\r\nSTP\r\nCCF,PROG_CUT_2_quadr_2\r\nMOV,G,U0,V0,E1\r\nMSG,Program ExCamIso1 ended\r\n"}),
    #             # self.SequenceOperation({"SO_Name": "PROG_CUT_1_quadr_1", "SO_Code": "// Sequence : StA900_25Ra0.22Con0H10\r\n// Steel,   H : 10\r\n// AC Cut A 900 0.25 mm,   NbP : 5\r\nSEP,1001,N{PC1},CP,E1\r\nROT,C,0\r\nCLE,0\r\nSCF,1\r\nMIR,X0,Y0\r\nMSG,LongCut (quadr_1)\r\nBLD,0\r\nGOP,N{PC1},E1\r\nDRS,quadr_1\r\nCUT,quadr_1,StA900_25Ra0.22Con0H10,E1,H10\r\n"}),
    #             # self.SequenceOperation({"SO_Name": "PROG_CUT_2_quadr_2", "SO_Code": "// Sequence : StA900_25Ra0.22Con0H10\r\n// Steel,   H : 10\r\n// AC Cut A 900 0.25 mm,   NbP : 5\r\nSEP,1002,N{PC2},CP,E1\r\nROT,C,0\r\nCLE,0\r\nSCF,1\r\nMIR,X0,Y0\r\nMSG,ShortCut (quadr_2)\r\nBLD,0\r\nGOP,N{PC2},E1\r\nDRS,quadr_2\r\nCUT,quadr_2,StA900_25Ra0.22Con0H10,E1,H10\r\n"}),
    #             # ]
    #         self.piece_dict["P_Sequence_Geometry_Array"] = []
    #             # self.SequenceGeometry({"SG_Name": "quadr_1", "SG_Code": "// UNIQUA Structured ISO V1.0\r\nG90\r\nG92X0.Y0.J0.I10.\r\nS1\r\nG40G50\r\nG0X1.Y2.7\r\nM60\r\nM61\r\nG1Y2.\r\nG41\r\nX2.\r\nY7.\r\nX-2.\r\nY2.\r\nX-1.\r\nG40\r\nG1Y2.25\r\nS2\r\nY2.\r\nG42\r\nX-2.\r\nY7.\r\nX2.\r\nY2.\r\nX1.\r\nG40\r\nG1Y2.7\r\nS3\r\nY2.\r\nG41\r\nX2.\r\nY7.\r\nX-2.\r\nY2.\r\nX-1.\r\nG40\r\nG1Y2.25\r\nS4\r\nY2.\r\nG42\r\nX-2.\r\nY7.\r\nX2.\r\nY2.\r\nX1.\r\nG40\r\nG1Y2.7\r\nS5\r\nY2.\r\nG41\r\nX2.\r\nY7.\r\nX-2.\r\nY2.\r\nX-1.\r\nG40\r\nG1Y2.25\r\nM50\r\nG0X0.Y0.\r\nM2"}),
    #             # self.SequenceGeometry({"SG_Name": "quadr_2", "SG_Code": "// UNIQUA Structured ISO V1.0\r\nG90\r\nG92X0.Y0.J0.I10.\r\nS1\r\nG40G50\r\nG0X1.Y2.7\r\nM60\r\nM61\r\nG1Y2.\r\nG42\r\nX-1.\r\nG40\r\nG1Y2.25\r\nS2\r\nY2.\r\nG41\r\nX1.\r\nG40\r\nG1Y2.7\r\nS3\r\nY2.\r\nG42\r\nX-1.\r\nG40\r\nG1Y2.25\r\nS4\r\nY2.\r\nG41\r\nX1.\r\nG40\r\nG1Y2.7\r\nS5\r\nY2.\r\nG42\r\nX-1.\r\nG40\r\nG1Y2.25\r\nM50\r\nG0X0.Y0.\r\nM2"}),
    #             # ]
    #         self.piece_dict["P_Sequence_Target_Array"] = []
    #             # self.SequenceTarget({"ST_Name": "StA900_25Ra0.22Con0H10", "ST_Technology_Selection": "MinimalDb"}),
    #             # ]
    #         #
    #
    #     self.checkStringType(Params)
    #     self.piece_dict.update(Params)
    #     #
    #     return self.piece_dict
    
    # def SequenceOperation(self, Params={}):
    #     self.so_dict = OrderedDict()
    #     self.so_dict["SO_Name"] = ""
    #     self.so_dict["SO_Code"] = ""
    #
    #     self.so_dict.update(Params)
    #     #
    #     return self.so_dict

    # def SequenceGeometry(self, Params={}):
    #     self.sg_dict = OrderedDict()
    #     self.sg_dict["SG_Name"] = ""
    #     self.sg_dict["SG_Code"] = ""
    #
    #     self.sg_dict.update(Params)
    #     #
    #     return self.sg_dict

    # def PieceVariable(self, Params):
    #     self.pv_dict = OrderedDict()
    #     self.pv_dict["PV_Name"] = ""        # String
    #     self.pv_dict["PV_Number"] = 0       # Int
    #     self.pv_dict["PV_Value"] = ""       # String
    #
    #     self.pv_dict.update(Params)
    #     #
    #     return self.pv_dict

    # def CNCPoint(self, Params):
    #     self.cncp_dict = OrderedDict()
    #     self.cncp_dict["CP_Name"] = ""      # String
    #     self.cncp_dict["CP_Number"] = 0     # Int [1001,2000]
    #     self.cncp_dict["CP_Reference"] = "Machining" # String "Guides, Machine, Pallet, Part, Machining, NotDefined"
    #     self.cncp_dict["CP_Axis_X"] = 0.    # Double
    #     self.cncp_dict["CP_Axis_Y"] = 0.    # Double
    #     self.cncp_dict["CP_Axis_Z"] = 0.    # Double
    #     self.cncp_dict["CP_Axis_A"] = 0.    # Double
    #     self.cncp_dict["CP_Axis_B"] = 0.    # Double
    #     self.cncp_dict["CP_Axis_C"] = 0.    # Double
    #     self.cncp_dict["CP_Axis_W"] = 0.    # Double
    #     self.cncp_dict["CP_Axis_U"] = 0.    # Double
    #     self.cncp_dict["CP_Axis_V"] = 0.    # Double
    #
    #     self.cncp_dict.update(Params)
    #     #
    #     return self.cncp_dict

    # def SequenceTarget(self, Params={}):
    #     self.st_dict = OrderedDict()
    #     self.st_dict["ST_Name"] = ""
    #     self.st_dict["ST_Technology_Selection"] = "MinimalDb"
    #     self.st_dict["ST_Technology_Criteria_Array"] = self.TechnologyCriteriaArray()
    #     self.st_dict["ST_Expected_Technology"] = self.ExpectedTechnology()
    #
    #     self.st_dict.update(Params)
    #     #
    #     return self.st_dict

    # def TechnologyCriteriaArray(self):
    #     return [
    #         self.Criteria({"C_Name": "WireName", "C_Value": "AC Brass 900", "C_Operator": "Equal"}), 
    #         self.Criteria({"C_Name": "WireDiameter", "C_Value": "0.25", "C_Operator": "Equal"}), 
    #         self.Criteria({"C_Name": "Material", "C_Value": "Steel", "C_Operator": "Equal"}),             
    #         self.Criteria({"C_Name": "Height", "C_Value": "19", "C_Operator": "Equal"}), 
    #         self.Criteria({"C_Name": "Ra", "C_Value": "0.22", "C_Operator": "Equal"}), 
    #         self.Criteria({"C_Name": "Taper", "C_Value": "0", "C_Operator": "Equal"}), 
    #         self.Criteria({"C_Name": "Tkm", "C_Value": "1.56", "C_Operator": "Equal"}), 
    #         self.Criteria({"C_Name": "StepNumber", "C_Value": "5", "C_Operator": "Equal"}), 
    #         self.Criteria({"C_Name": "StartPoint", "C_Value": "Hole", "C_Operator": "Equal"}), 
    #         self.Criteria({"C_Name": "WorkingType", "C_Value": "Open", "C_Operator": "Equal"}), 
    #         self.Criteria({"C_Name": "FlushingCondition", "C_Value": "SpecialCondition", "C_Operator": "Equal"}),    # SpecialCondition, (SealedNozzles, UnsealedNozzles, ISPS seem to be obsolete)
    #         self.Criteria({"C_Name": "Priority", "C_Value": "Speed", "C_Operator": "Equal"}), 
    #         self.Criteria({"C_Name": "QualityTf", "C_Value": "7", "C_Operator": "Equal"}), 
    #         self.Criteria({"C_Name": "Options", "C_Value": "0", "C_Operator": "Equal"}),             
    #         self.Criteria({"C_Name": "ShotPeening", "C_Value": "No", "C_Operator": "Equal"}),             
    #         self.Criteria({"C_Name": "Nozzle", "C_Value": "1000", "C_Operator": "Equal"}),             
    #         self.Criteria({"C_Name": "MinimalDbVersion", "C_Value": MINIMALDBVERSION, "C_Operator": "Equal"}),             
    #         ]

    # def ExpectedTechnology(self, Params={}):
    #     self.et_dict = OrderedDict()
    #     self.et_dict["ET_Pass_Number"] = 5
    #     self.et_dict["ET_Priority"] = "Speed"
    #     self.et_dict["ET_Ra"] = 0.24
    #     self.et_dict["ET_Tf"] = 0.0
    #     self.et_dict["ET_Tkm"] = 1.56
    #     self.et_dict["ET_Speed"] = 1.26
    #     self.et_dict["ET_Pall"] = 0
    #     self.et_dict["ET_Options"] = 0
    #     self.et_dict["ET_Info"] = ""
    #     self.et_dict["ET_Extended_Info"] = ""
    #     self.et_dict["ET_Pass_Array"] = [
    #         self.ExpectedPass({"EP_Name": "1", "EP_Tool": 1, "EP_Speed": 7.9, "EP_Offset": 2264}),
    #         self.ExpectedPass({"EP_Name": "2", "EP_Tool": 1, "EP_Speed": 9.1, "EP_Offset": 1655}),
    #         self.ExpectedPass({"EP_Name": "3", "EP_Tool": 1, "EP_Speed": 8.55, "EP_Offset": 1350}),
    #         self.ExpectedPass({"EP_Name": "4", "EP_Tool": 1, "EP_Speed": 7.79, "EP_Offset": 1300}),
    #         self.ExpectedPass({"EP_Name": "5", "EP_Tool": 1, "EP_Speed": 3.2, "EP_Offset": 1290}),
    #         ]
    #
    #     self.et_dict.update(Params)
    #     #
    #     return self.et_dict
        
    # def ExpectedPass(self, Params={}):
    #     self.ep_dict = OrderedDict()
    #     self.ep_dict["EP_Name"] = "1"
    #     self.ep_dict["EP_Tool"] = 1
    #     self.ep_dict["EP_Speed"] = 0
    #     self.ep_dict["EP_Offset"] = 0
    #
    #     self.ep_dict.update(Params)
    #     #
    #     return self.ep_dict


    # def Batch(self, Params={}):
    #     self.batch_dict = OrderedDict()
    #     self.batch_dict["B_Name"] = "batch01"
    #     if UNIQUA_VERSION >= "0.9.6":
    #         self.batch_dict["B_ErpId"] = ""
    #     self.batch_dict["B_Description"] = "Put here the batch description"
    #     # simulator (german version) knows only this terms (-> seems strange : don't know how to handle this depending on language ??
    #     self.batch_dict["B_Generation_Strategy_Name"] = UNIQUA_STRATEGY_NAMES[EARLYMINIMUMTHREADING]
    #     #
    #     if UNIQUA_VERSION >= "0.9.5":       # it seems they changed this : in older example there is "B_Pallet_By_Pallet"
    #         self.batch_dict["B_Sorting_Pallet_By_Pallet"] = False
    #         self.batch_dict["B_Sorting_Part_By_Part"] = False
    #     else:
    #         self.batch_dict["B_Pallet_By_Pallet"] = False
    #         self.batch_dict["B_Part_By_Part"] = False
    #     self.batch_dict["B_Piece_Array"] = []
    #     if UNIQUA_VERSION >= "0.9.6.1":
    #         self.batch_dict["B_Pallet_Array"] = []
    #         self.batch_dict["B_Measure_Array"] = []
    #
    #     self.checkStringType(Params)
    #     self.batch_dict.update(Params)
    #     #
    #     return self.batch_dict

#     def Machining(self, Params={}):
#         """
#         The Machining is the specification for a single shape. It inludes all necessary information to interpret the defined geometry.The GEOMETRY definition will be stored into
#         "M_Geometry" as string value or into "M_Geometry_Pocketing" as string value. 
#         """
#         self.machining_dict = OrderedDict()
#         self.machining_dict["M_Category"] = "Standard"               # Standard, Open, Pocketing
#         self.machining_dict["M_Name"] = "Geo_1"
#         self.machining_dict["M_Description"] = "description"
#         self.machining_dict["M_Priority"] = 0                        # >= 0
#         self.machining_dict["M_Geometry_Name"] = ""
#         self.machining_dict["M_Geometry"] = ""                       # (for M_Category is Standard or Open)
#         self.machining_dict["M_Geometry_Pocketing_Name"] = "NotSet"
#         self.machining_dict["M_Geometry_Pocketing"] = ""             # (Required if M_Category is Pocketing)
#         self.machining_dict["M_Reference_X"] = 0.0
#         self.machining_dict["M_Reference_Y"] = 0.0
#         self.machining_dict["M_Reference_Z"] = 0.0
#         self.machining_dict["M_Reference_Rot_A"] = 0.0
#         self.machining_dict["M_Reference_Rot_B"] = 0.0
#         self.machining_dict["M_Reference_Rot_C"] = 0.0
#
#         if UNIQUA_VERSION >= "0.9.12":
#              self.machining_dict["M_Reference_Aux_A"] = 0.0
#              self.machining_dict["M_Reference_Aux_B"] = 0.0
#
#         self.machining_dict["M_Separationcut_Length"] = 0.0
#         self.machining_dict["M_Separationcut_CLE"] = 0.0
#         self.machining_dict["M_Autofix_Length"] = 0.0
#
#         if UNIQUA_VERSION >= "0.9.12":
#              self.machining_dict["M_Clean_Autofix"] = False
#
#         self.machining_dict["M_Minimal_Radius"] = 0.0
#         self.machining_dict["M_Invert_CW_CCW"] = False
#         self.machining_dict["M_Forth_And_Back"] = "OnlyShorts"      # ("None", "OnlyShorts", "ShortsAndLongs", "AllCuts")
#         self.machining_dict["M_External_Roughing"] = False
#         self.machining_dict["M_Short_Trims"] = True
#         self.machining_dict["M_Machining_Type"] = "Die"              # Die, Punch
#         self.machining_dict["M_Retreat_Strategy"] = "DeltaMain"      # DeltaMain, Perpendicular
#         self.machining_dict["M_Offset_Side"] = "Left"                # Right, Left
#         self.machining_dict["M_Scale_Factor"] = 1.0
#         self.machining_dict["M_Taper_Angle"] = 0.0
#         self.machining_dict["M_Mirror_X"] = False
#         self.machining_dict["M_Mirror_Y"] = False
#         self.machining_dict["M_Taper_Disposition"] = "Conic"         # Conic, Cylinder
#         self.machining_dict["M_CLE"] = 0.0
#         self.machining_dict["M_Taper_Direction"] = "OpenBottom"      # OpenBottom, OpenTop if M_Category is Standard or Pocketing Left, Right if M_Category is Open
#         if UNIQUA_VERSION >= "0.9.11":
#             self.machining_dict["M_Enable_Auto_Slug"] = False        # don't know 
#             self.machining_dict["M_Auto_Slug_Detection_Length"] = 0.0   # don't know 
#         self.machining_dict["M_Startpoint_Name"] = "Piece_Stp"
#         if UNIQUA_VERSION >= "0.9.7":
#             self.machining_dict["M_Startpoint_Manual_Threading"] = False
#         if UNIQUA_VERSION >= "0.9.10":
#             self.machining_dict["M_Startpoint_Use_Custom_Threading"] = False
#             self.machining_dict["M_Startpoint_Hole_Diameter"] = 2.0
#             self.machining_dict["M_Startpoint_Threading_Empty_Tank"] = False
#             self.machining_dict["M_Startpoint_Threading_Jet_On"] = True
#             self.machining_dict["M_Startpoint_Threading_UV_Movement"] = True
#         self.machining_dict["M_Startpoint_X"] = 0.0
#         self.machining_dict["M_Startpoint_Y"] = 0.0
#         self.machining_dict["M_Startpoint_U"] = 0.0
#         self.machining_dict["M_Startpoint_V"] = 0.0
#
#         if UNIQUA_VERSION >= "0.9.12":
#              self.machining_dict["M_Startpoint_A"] = 0.0
#              self.machining_dict["M_Startpoint_B"] = 0.0        
#
#         self.machining_dict["M_Startpoint_Reference"] = "Machining"  # Machining, Part
#         self.machining_dict["M_Startpoint_Entry_Line"] = 1
#         self.machining_dict["M_Startpoint_Entry_Position_Percent"] = 0.0     # (Required)
# #        self.machining_dict["M_Startpoint_Entry_Position_mm"] = 0.0          #
#         self.machining_dict["M_Startpoint_Entry_Perpendicular_Zone_Length"] = 0.0
#         self.machining_dict["M_Startpoint_Entry_Tangent_Zone_ArcRadius"] = 0.0
#         self.machining_dict["M_Startpoint_Entry_Type"] = "Perpendicular"     # Perpendicular, Tangent, Straight
#         self.machining_dict["M_Startpoint_Entry_Side"] = "OffsetEdm"         # OffsetEdm, ZoneLength
#         self.machining_dict["M_Startpoint_Approach_Type"] = "Straight"       # Triangle, Rectangle, Straight
#         self.machining_dict["M_Startpoint_Approach_Width"] = 0.0
#         self.machining_dict["M_Startpoint_Approach_Height"] = 0.0
#         self.machining_dict["M_Startpoint_Approach_Side"] = "Auto"           # Auto, Left, Right
#         self.machining_dict["M_Startpoint_Entry_Length"] = 0.0               # (Required) 
#         self.machining_dict["M_Startpoint_Exit_Length"] = 0.0                # (Required) 
#         self.machining_dict["M_Startpoint_Incremental_Entry"] = 0.0
#         self.machining_dict["M_Endpoint_Name"] = "Unknown"                   # (Required if M_Category is Open)
#         if UNIQUA_VERSION >= "0.9.11":
#             self.machining_dict["M_Endpoint_Manual_Threading"] = False
#             self.machining_dict["M_Endpoint_Use_Custom_Threading"] = False
#             self.machining_dict["M_Endpoint_Hole_Diameter"] = 2.0
#             self.machining_dict["M_Endpoint_Threading_Empty_Tank"] = False
#             self.machining_dict["M_Endpoint_Threading_Jet_On"] = True
#             self.machining_dict["M_Endpoint_Threading_UV_Movement"] = True
#         self.machining_dict["M_Endpoint_X"] = 0.0                            # (Required if M_Category is Open)
#         self.machining_dict["M_Endpoint_Y"] = 0.0                            # (Required if M_Category is Open)
#         self.machining_dict["M_Endpoint_U"] = 0.0                            # (Required if M_Category is Open)
#         self.machining_dict["M_Endpoint_V"] = 0.0                            # (Required if M_Category is Open)
#
#         if UNIQUA_VERSION >= "0.9.12":
#              self.machining_dict["M_Endpoint_A"] = 0.0
#              self.machining_dict["M_Endpoint_B"] = 0.0    
#
#         self.machining_dict["M_Endpoint_Reference"] = "Machining"            # (Required if M_Category is Open)
#         self.machining_dict["M_Endpoint_Entry_Line"] = 0                     # (Required if M_Category is Open)
#         self.machining_dict["M_Endpoint_Entry_Side"] = "OffsetEdm"           # OffsetEdm, ZoneLength (Required if M_Category is Open)
#         self.machining_dict["M_Endpoint_Entry_Position_Percent"] = 0.0        # (Required if M_Category is Open)
# #        self.machining_dict["M_Endpoint_Entry_Position_mm"] = 0.0            # M_Endpoint_Enabled
#         self.machining_dict["M_Endpoint_Entry_Perpendicular_Zone_Length"] = 0.0
#         self.machining_dict["M_Endpoint_Entry_Tangent_Zone_ArcRadius"] = 0.0
#         self.machining_dict["M_Endpoint_Entry_Type"] = "Perpendicular"       # Perpendicular, Tangent, Straight (Required if M_Category is Open)
#         self.machining_dict["M_Endpoint_Approach_Type"] = "Straight"         # Triangle, Rectangle, Straight (Required if M_Category is Open)
#         self.machining_dict["M_Endpoint_Approach_Width"] = 0.0               # (Required if M_Category is Open)
#         self.machining_dict["M_Endpoint_Approach_Height"] = 0.0              # (Required if M_Category is Open)
#         self.machining_dict["M_Endpoint_Approach_Side"] = "Auto"             # (Required if M_Category is Open)
#         self.machining_dict["M_Endpoint_Enabled"] = False                    # (Required if M_Category is Open)
#         self.machining_dict["M_Endpoint_Exit_As_Entry"] = False              # (Required if M_Category is Open)
#         self.machining_dict["M_Macro_Encoding"] = "GfmsCmd"
#         self.machining_dict["M_Before_First_Pass"] = "None"
#         self.machining_dict["M_Before_Each_Passes"] = "None"
#         self.machining_dict["M_After_Each_Passes"] = "None"
#         self.machining_dict["M_After_Last_Pass"] = "None"
#         self.machining_dict["M_Creator_Data"] = ""                           # (Reserved for Creator related data. Should not be used for large set of data.)
#         self.machining_dict["M_Target_Name"] = "Edm"                         # Technologie Name (Could be empty, then each target will be generated separately on the job, even if the values are the same of an already existing target.If the same name is already used in other machining, all of them will share the same instance of the target)
#         if UNIQUA_VERSION < "0.9.11":                                        # Obsolete since 0.9.11. They can be replaced by dedicated technology criteria. 
#             self.machining_dict["M_Wire_Name"] = "AC Cut A 900"                  # (See Wire Names sheet)
#             self.machining_dict["M_Wire_Diameter"] = 0.25                        # (unit is [um])
#         else: 
#             self.machining_dict["M_Wire_Name"] = ""
#             self.machining_dict["M_Wire_Diameter"] = 0.0
#         self.machining_dict["M_Technology_Selection"] = "First"              # First, User, Speed, Surface and (since Sept 2021) MinimalDb
#         if UNIQUA_VERSION >= "0.9.6.1":
#             self.machining_dict["M_Passes_Status"] = ""                     # "True;True;True;True;True"  # (since Sept 2021) TODO
#
#             if UNIQUA_VERSION >= "0.9.12":
#                 self.machining_dict["M_Mode_A"] = "Lock"
#                 self.machining_dict["M_Mode_B"] = "Lock"     
#
#             self.machining_dict["M_Spin_Speed_A"] = ""
#             self.machining_dict["M_Spin_Speed_B"] = ""
#             self.machining_dict["M_Spin_Sense_A"] = ""
#             self.machining_dict["M_Spin_Sense_B"] = ""
#
#         if UNIQUA_OPERATION_MODE == DYNAMIC_JSON:
#             if MINIMALDBVERSION and UNIQUA_VERSION >= "0.9.12":
#                 self.machining_dict["M_Technology_Criteria_Array"] = []     # self.TechnologyCriteriaArray()
#                 self.machining_dict["M_Expected_Technology"] = {}           # TODO
#             else:
#                 self.machining_dict["M_Technology_Criteria_Array"] = []
#
#         self.machining_dict["M_Point_Array"] = []
#         self.machining_dict["M_Sector_Array"] = []
#
#         self.checkStringType(Params)
#         self.machining_dict.update(Params)
#         #
#         return self.machining_dict

    # def Criteria(self, Params={}):
    #     """
    #     The CRITERIA's are stored within in an array within the Machining object. The CRITERIA's are used together with PIECE information like e.g. P_Name to define a new
    #     MACHINING target or call an already existing MACHINING target (Usertechnology) by name.        
    #     """
    #     self.criteria_dict = OrderedDict()
    #     self.criteria_dict["C_Name"] = ""               #  Material, Height, Ra, Taper, Tkm, StartPoint, WorkingType, FlushingCondition, Priority, StepNumber, TechnologyName, QualityTf, Options, ShotPeening, Nozzle, MinimalDbVersion, WireName, WireName2, WireDiameter, WireDiameter2, Other = Defines the name of the Criteria
    #     self.criteria_dict["C_Value"] = ""              # (Ra unit is [um])
    #     self.criteria_dict["C_Operator"] = "Equal"      # Equal, GreaterThan, LessThan
    #
    #     self.checkStringType(Params)
    #     self.criteria_dict.update(Params)
    #     #
    #     return self.criteria_dict
    
    # def Point(self, Params={}):
    #     """
    #     xxx
    #     """
    #     self.point_dict = OrderedDict()
    #     self.point_dict["PT_Name"] = "Point_1"
    #     self.point_dict["PT_Action_Encoding"] = "ActionList"
    #     self.point_dict["PT_Action"] = "InstructionMachineProgrammedStop,0"
    #     self.point_dict["PT_Line"] = 3
    #     self.point_dict["PT_Position_Percent"] = 0.0
    #
    #     self.checkStringType(Params)
    #     self.point_dict.update(Params)
    #     #
    #     return self.point_dict
    
    # def Sector(self, Params={}):
    #     """
    #     xxx
    #     """
    #     self.sector_dict = OrderedDict()
    #     self.sector_dict["S_Name"] = "Sector_1"
    #     self.sector_dict["S_Action_Encoding"] = "ActionList"
    #     self.sector_dict["S_Action"] = "TaperAngle,0.5"
    #     self.sector_dict["S_Point_1_Name"] = "Point_4"
    #     self.sector_dict["S_Point_1_Line"] = 3
    #     self.sector_dict["S_Point_1_Position_Percent"] = 0.0
    #     self.sector_dict["S_Point_1_Action"] = ""
    #     self.sector_dict["S_Point_2_Name"] = "Point_5"
    #     self.sector_dict["S_Point_2_Line"] = 3
    #     self.sector_dict["S_Point_2_Position_Percent"] = 100.0
    #     self.sector_dict["S_Point_2_Action"] = "InstructionMachineProgrammedStop,0"
    #
    #     self.checkStringType(Params)
    #     self.sector_dict.update(Params)
    #     #
    #     return self.sector_dict
    
    def Pallet(self, Params={}):
        """
        The PALLET object is part of the PALLET array inside the BATCH object.
        """
        self.pallet_dict = OrderedDict()
        self.pallet_dict["PA_Name"] = "PalletA"
        self.pallet_dict["PA_Description"] = ""
        self.pallet_dict["PA_Priority"] = 0
        self.pallet_dict["PA_Dimensions_Width_X"] = 0.0
        self.pallet_dict["PA_Dimensions_Depht_Y"] = 0.0
        self.pallet_dict["PA_Dimensions_Height_Z"] = 0.0
        self.pallet_dict["PA_Reference_X"] = 0.0
        self.pallet_dict["PA_Reference_Y"] = 0.0
        self.pallet_dict["PA_Reference_Z"] = 0.0
        self.pallet_dict["PA_Reference_Rot_A"] = 0.0
        self.pallet_dict["PA_Reference_Rot_B"] = 0.0
        self.pallet_dict["PA_Reference_Rot_C"] = 0.0
        self.pallet_dict["PA_RFID"] = ""
        self.pallet_dict["PA_Location"] = ""
        self.pallet_dict["PA_Magazine_Position"] = -1

        self.checkStringType(Params)
        self.pallet_dict.update(Params)
        #
        return self.pallet_dict

    def addmanifest(self, Params):
        if self.main_dict is not None:
            # self.main_dict["Manifest"] = self.Manifest(Params)
            self.main_dict["Manifest"] = self.manifest_dict = Manifest(Params)
            return True
        return False

    def updatemanifest(self, Params):
        if self.manifest_dict is not None:
            self.checkStringType(Params)
            self.manifest_dict.update(Params)
            return True
        return False

    def addsection(self, Name, Params):
        if self.main_dict is not None:
            # self.main_dict[Name] = self.Manifest(Params)
            self.main_dict[Name] = self.multi_purpose_dict = MultiPurpose(Params)
            return True
        return False

    def updatesection(self, Name, Params):
        if self.manifest_dict is not None:
            self.checkStringType(Params)
            if Name in self.main_dict:
                self.main_dict[Name].update(Params)
            return True
        return False

    def addpiece(self, Params, autocount=True):       # developed and tested with joe Fr. 2021/02/19

        # list of keys to sanitize
        keys_to_sanitize = ["P_Name", "P_ErpId"]

        for key in keys_to_sanitize:
            value = Params.get(key)
            # only sanitize if a valid string is present
            if isinstance(value, str) and value:
                sanitized_value = value.replace("\\", "_").replace("/", "_")
                Params[key] = sanitized_value

        if self.main_dict is not None:
            if self.batch_dict is not None and UNIQUA_OPERATION_MODE == DYNAMIC_JSON: # begin with managing names for the first piece too
                if autocount == True:
                    cnt = 1
                    New_Name = P_Name = Params.get('P_Name', 'UNIQUA_Piece_01')
                    while True:
                        for piece in self.batch_dict["B_Piece_Array"]:
                            if piece['P_Name'] == New_Name:
                                cnt += 1
                                New_Name = "{}_{:02d}".format(P_Name, cnt)
                                break
                        else:
                            Params['P_Name'] = New_Name
                            break
                self.piece_dict = PieceDynamic(Params)
                self.batch_dict["B_Piece_Array"].append(self.piece_dict)
                return True
            else:
                if self.manifest_dict is not None:
                    self.manifest_dict["MAN_Scope"] = "Piece"
                # self.main_dict["Piece"] = self.Piece(Params)
                if UNIQUA_OPERATION_MODE == DYNAMIC_JSON:
                    self.main_dict["Piece"] = self.piece_dict = PieceDynamic(Params)
                if UNIQUA_OPERATION_MODE == SEQUENTIAL_JSON:
                    self.main_dict["Piece"] = self.piece_dict = PieceStructured(Params)
                return True
        return False

    def updatepiece(self, Params, autocount=True):
        if self.piece_dict is not None:
            if autocount == True and 'P_Name' in Params:    # check this only when 'P_Name' is in the new Params
                if self.batch_dict is not None: # begin with managing names for the first piece too
                    cnt = 1
                    New_Name = P_Name = Params.get('P_Name', 'UNIQUA_Piece_01')
                    while True:
                        for piece in self.batch_dict["B_Piece_Array"]:
                            if piece['P_Name'] == New_Name:
                                cnt += 1
                                New_Name = "{}_{:02d}".format(P_Name, cnt)
                                break
                        else:
                            Params['P_Name'] = New_Name
                            break
            #
            self.checkStringType(Params)
            self.piece_dict.update(Params)
            return True
        print("Error : No piece exists - create this before calling updatepiece-method!")
        return False
            
    def getpieceParams(self, Params=None):
        if self.piece_dict is not None:
            if Params is None:
                return self.piece_dict
            for key in list(Params.keys()):
                Params[key] = self.piece_dict.get(key)
            return Params
        print("Error : No piece exists - create this before calling getpieceParams-method!")
        return False
            
    def updatepiece_at(self, Params, PieceName=None):
        if self.main_dict is not None:
            if self.batch_dict is not None:     # in batch-mode first we have to search for desired piece
                for piece in self.batch_dict["B_Piece_Array"]:
                    if piece['P_Name'] == PieceName:
                        self.checkStringType(Params)
                        piece.update(Params)
                        return True
            return self.updatepiece(Params)
        return False

    ########## for batch mode ##################
    def addbatch(self, Params):
        if self.main_dict is not None:
            if self.manifest_dict is not None:
                self.manifest_dict["MAN_Scope"] = "Batch"
            self.main_dict["Batch"] = self.batch_dict = Batch(Params)
            return True
        return False

    def updatebatch(self, Params):
        if self.batch_dict is not None:
            self.checkStringType(Params)
            self.batch_dict.update(Params)
            return True
        return False

    def addpallet(self, Params):
        if self.batch_dict is not None:
            self.batch_dict["B_Pallet_Array"].append( self.Pallet(Params) )
            return True
        return False

    def updatepallet(self, Params):
        if self.pallet_dict is not None:
            self.checkStringType(Params)
            self.pallet_dict.update(Params)
            return True
        return False
    ########## for batch (not used from us) ##################

    def machininggetkey(self, *keys):
        if UNIQUA_OPERATION_MODE != DYNAMIC_JSON:
            print("Error : machininggetkey call only allowed in dynamic mode!")
            return False
            
        if self.piece_dict is not None:
            vals = []
            for mach in self.piece_dict["P_Machining_Array"]:
                for key in keys:
                    if key in mach:
                        vals.append(mach[key])
                if vals:
                    break
            return vals
        print("Error : No piece exists - create this before calling machininggetkey-method!")
        return False

    def addmachining(self, Params, autocount=True):
        if hasattr(self.piece_dict, "addMachining"):
            ret = self.piece_dict.addMachining(Params, autocount)
            if len( self.piece_dict["P_Machining_Array"] ) > 0:
                self.machining_dict = self.piece_dict["P_Machining_Array"][-1]       # most recent machining
            return ret
            
        print("Error : No piece exists - only for dynamic mode; create this before calling addmachining-method!")
        return False

    def addmachiningOLD(self, Params, autocount=True):
        if UNIQUA_OPERATION_MODE != DYNAMIC_JSON:
            print("Error : addmachining call only allowed in dynamic mode!")
            return False
            
        if self.piece_dict is not None:
            if "P_Machining_Array" in self.piece_dict:
                if autocount == True:
                    cnt = 0
                    name_in_list = True
                    M_Name = Params.get('M_Name', 'Geo')
                    while name_in_list:
                        cnt += 1
                        for mach in self.piece_dict["P_Machining_Array"]:
                            if mach['M_Name'] == "{}_{:03d}".format(M_Name, cnt):
                                break
                        else:
                            name_in_list = False
                            Params['M_Name'] = "{}_{:03d}".format(M_Name, cnt)
                #
                self.machining_dict = MachiningDynamic(Params)
                self.piece_dict["P_Machining_Array"].append( self.machining_dict )
                return True
            print("Error : No P_Machining_Array key in piece exists - wrong use of addmachining-method!")
            return False
        print("Error : No piece exists - create this before calling addmachining-method!")
        return False

    def updatemachining(self, Params, autocount=True):
        if UNIQUA_OPERATION_MODE != DYNAMIC_JSON:
            print("Error : updatemachining call only allowed in dynamic mode!")
            return False

        if self.machining_dict is not None:
            if autocount == True and 'M_Name' in Params:
                cnt = 0
                name_in_list = True
                M_Name = Params.get('M_Name')
                while name_in_list:
                    cnt += 1
                    for mach in self.piece_dict["P_Machining_Array"]:
                        if mach != self.machining_dict: # do not compare this name with the actual machining entries !!
                            if mach['M_Name'] == "{}_{:03d}".format(M_Name, cnt):
                                break
                    else:
                        name_in_list = False
                        Params['M_Name'] = "{}_{:03d}".format(M_Name, cnt)
            self.checkStringType(Params)
            self.machining_dict.update(Params)
            return True
        return False

    def getmachiningParams(self, Params=None):
        if UNIQUA_OPERATION_MODE != DYNAMIC_JSON:
            print("Error : getmachiningParams call only allowed in dynamic mode!")
            return False

        if self.machining_dict is not None:
            if Params is None:
                return self.machining_dict
            for key in list(Params.keys()):
                Params[key] = self.machining_dict.get(key)
            return Params
        return False

    def updatemachining_at(self, Params, MachiningName, PieceName=None):
        if UNIQUA_OPERATION_MODE != DYNAMIC_JSON:
            print("Error : updatemachining_at call only allowed in dynamic mode!")
            return False

        if self.main_dict is not None:
            if self.batch_dict is not None:     # in batch-mode first we have to search for desired piece
                for piece in self.batch_dict["B_Piece_Array"]:
                    if piece['P_Name'] == PieceName:
                        for machining in piece["P_Machining_Array"]:
                            if machining['M_Name'] == MachiningName:
                                self.checkStringType(Params)
                                machining.update(Params)
                                return True
            elif self.piece_dict is not None:   # in piece-mode we can begin with searching for the desired machining
                for machining in self.piece_dict["P_Machining_Array"]:
                    if machining['M_Name'] == MachiningName:
                        self.checkStringType(Params)
                        machining.update(Params)
                        return True
        return False
    
    def addsequenceoperation(self, Params, autocount=True):
        if hasattr(self.piece_dict, "addSequenceOperation"):
            return self.piece_dict.addSequenceOperation(Params, autocount)
            
        print("Error : No piece exists - create this before calling addsequenceoperation-method!")
        return False

    def addsequenceoperationOLD(self, Params, autocount=True):
        if self.piece_dict and "P_Sequence_Operation_Array" in self.piece_dict:
            if autocount == True:
                cnt = 0
                name_in_list = True
                SO_Name = Params.get('SO_Name', 'Main')
                while name_in_list:
                    cnt += 1
                    for sop in self.piece_dict["P_Sequence_Operation_Array"]:
                        if sop['SO_Name'] == "{}_{:03d}".format(SO_Name, cnt):
                            break
                    else:
                        name_in_list = False
                        Params['SO_Name'] = "{}_{:03d}".format(SO_Name, cnt)
            #
#             self.piece_dict["P_Sequence_Operation_Array"].append( self.SequenceOperation(Params) )
            self.so_dict = SequenceOperation(Params)
            self.piece_dict["P_Sequence_Operation_Array"].append( self.so_dict )
            return True
        print("Error : No piece exists - create this before calling addsequenceoperation-method!")
        return False

    def updatesequenceoperation(self, Params, autocount=True):
        if self.piece_dict and "P_Sequence_Operation_Array" in self.piece_dict:
            so = self.piece_dict.get("P_Sequence_Operation_Array")[-1]     # use the last added operation
            if autocount == True and 'SO_Name' in Params:
                cnt = 0
                name_in_list = True
                SO_Name = Params.get('SO_Name')
                while name_in_list:
                    cnt += 1
                    for sop in self.piece_dict["P_Sequence_Operation_Array"]:
                        if sop != so: # do not compare this name with the actual so entries !!
                            if sop['SO_Name'] == "{}_{:03d}".format(SO_Name, cnt):
                                break
                    else:
                        name_in_list = False
                        Params['SO_Name'] = "{}_{:03d}".format(SO_Name, cnt)
            self.checkStringType(Params)
            so.update(Params)
            return True
        return False

    def getsequenceoperationParams(self, Params=None):
        if self.piece_dict and "P_Sequence_Operation_Array" in self.piece_dict:
            so = self.piece_dict.get("P_Sequence_Operation_Array")[-1]     # use the last added operation
            if Params is None:
                return so
            for key in list(Params.keys()):
                Params[key] = so.get(key)
            return Params
        return False

    def updatesequenceoperation_at(self, Params, OperationName, PieceName=None):
        if self.main_dict is not None:
            if self.piece_dict is not None:   # in piece-mode we can begin with searching for the desired operation
                for operation in self.piece_dict["P_Sequence_Operation_Array"]:
                    if operation['SO_Name'] == OperationName:
                        self.checkStringType(Params)
                        operation.update(Params)
                        return True
        return False

    def addsequencegeometry(self, Params, autocount=True):
        if hasattr(self.piece_dict, "addSequenceGeometry"):
            return self.piece_dict.addSequenceGeometry(Params, autocount)
            
        print("Error : No piece exists - create this before calling addsequencegeometry-method!")
        return False

    def addsequencegeometryOLD(self, Params, autocount=True):
        if self.piece_dict and "P_Sequence_Geometry_Array" in self.piece_dict:
            if autocount == True:
                cnt = 0
                name_in_list = True
                SG_Name = Params.get('SG_Name', 'Geo')
                while name_in_list:
                    cnt += 1
                    for sg in self.piece_dict["P_Sequence_Geometry_Array"]:
                        if sg['SG_Name'] == "{}_{:03d}".format(SG_Name, cnt):
                            break
                    else:
                        name_in_list = False
                        Params['SG_Name'] = "{}_{:03d}".format(SG_Name, cnt)
            #
            # self.piece_dict["P_Sequence_Geometry_Array"].append( self.SequenceGeometry(Params) )
            self.sg_dict = SequenceGeometry(Params)
            self.piece_dict["P_Sequence_Geometry_Array"].append( self.sg_dict )
            return True
        print("Error : No piece exists - create this before calling addsequencegeometry-method!")
        return False

    def updatesequencegeometry(self, Params, autocount=True):
        if self.piece_dict and "P_Sequence_Geometry_Array" in self.piece_dict:
            sg = self.piece_dict.get("P_Sequence_Geometry_Array")[-1]     # use the last added operation
            if autocount == True and 'SG_Name' in Params:
                cnt = 0
                name_in_list = True
                SG_Name = Params.get('SG_Name')
                while name_in_list:
                    cnt += 1
                    for sgm in self.piece_dict["P_Sequence_Geometry_Array"]:
                        if sgm != sg: # do not compare this name with the actual sg entries !!
                            if sgm['SG_Name'] == "{}_{:03d}".format(SG_Name, cnt):
                                break
                    else:
                        name_in_list = False
                        Params['SG_Name'] = "{}_{:03d}".format(SG_Name, cnt)
            self.checkStringType(Params)
            sg.update(Params)
            return True
        return False

    def getsequencegeometryParams(self, Params=None):
        if self.piece_dict is not None and "P_Sequence_Geometry_Array" in self.piece_dict:
            sg = self.piece_dict.get("P_Sequence_Geometry_Array")[-1]     # use the last added operation
            if Params is None:
                return sg
            for key in list(Params.keys()):
                Params[key] = sg.get(key)
            return Params
        return False

    def updatesequencegeometry_at(self, Params, GeometryName, PieceName=None):
        if self.main_dict is not None:
            if self.piece_dict is not None:   # in piece-mode we can begin with searching for the desired geometry
                for geometry in self.piece_dict["P_Sequence_Geometry_Array"]:
                    if geometry['SG_Name'] == GeometryName:
                        self.checkStringType(Params)
                        geometry.update(Params)
                        return True
        return False

    def addvariable(self, Params, autocount=True):
        if hasattr(self.piece_dict, "addVariable"):
            return self.piece_dict.addVariable(Params, autocount)

        print("Error : No piece exists - create this before calling addvariable-method!")
        return False

    def updatevariable(self, Params, autocount=True):
        pass
    def getvariableParams(self, Params=None):
        pass
    def updatevariable_at(self, Params, PVName, PieceName=None):
        pass

    def addcncpoint(self, Params, autocount=True):
        if hasattr(self.piece_dict, "addCncPoint"):
            return self.piece_dict.addCncPoint(Params, autocount)
            
        print("Error : No piece exists - create this before calling addcncpoint-method!")
        return False

    def updatecncpoint(self, Params, autocount=True):
        pass
    def getcncpointParams(self, Params=None):
        pass
    def updatecncpoint_at(self, Params, CPName, PieceName=None):
        pass
    
    def addsequencetarget(self, Params, autocount=True):
        if hasattr(self.piece_dict, "addSequenceTarget"):
            ret = self.piece_dict.addSequenceTarget(Params, autocount)
            if len( self.piece_dict["P_Sequence_Target_Array"] ) > 0:
                self.st_dict = self.piece_dict["P_Sequence_Target_Array"][-1]       # most recent sequence target object
            return ret
            
        print("Error : No piece exists - create this before calling addsequencetarget-method!")
        return False

    def addsequencetargetOLD(self, Params, autocount=True):
        if self.piece_dict is not None and "P_Sequence_Target_Array" in self.piece_dict:
            if autocount == True:
                cnt = 0
                name_in_list = True
                ST_Name = Params.get('ST_Name', 'StA900_25')
                while name_in_list:
                    cnt += 1
                    for st in self.piece_dict["P_Sequence_Target_Array"]:
                        if st['ST_Name'] == "{}_{:03d}".format(ST_Name, cnt):
                            break
                    else:
                        name_in_list = False
                        Params['ST_Name'] = "{}_{:03d}".format(ST_Name, cnt)
            #
            # self.piece_dict["P_Sequence_Target_Array"].append( self.SequenceTarget(Params) )
            self.st_dict = SequenceTarget(Params)
            self.piece_dict["P_Sequence_Target_Array"].append( self.st_dict )
            return True
        print("Error : No piece exists - create this before calling addsequencetarget-method!")
        return False

    def updatesequencetarget(self, Params, autocount=True):
        if self.piece_dict is not None and "P_Sequence_Target_Array" in self.piece_dict:
            if autocount == True and 'ST_Name' in Params:
                cnt = 0
                name_in_list = True
                ST_Name = Params.get('ST_Name')
                while name_in_list:
                    cnt += 1
                    for st in self.piece_dict["P_Sequence_Target_Array"]:
                        if st != self.st_dict: # do not compare this name with the actual st entries !!
                            if st['ST_Name'] == "{}_{:03d}".format(ST_Name, cnt):
                                break
                    else:
                        name_in_list = False
                        Params['ST_Name'] = "{}_{:03d}".format(ST_Name, cnt)
            self.checkStringType(Params)
            self.st_dict.update(Params)
            return True
        return False

    def getsequencetargetParams(self, Params=None):
        if self.piece_dict is not None and "P_Sequence_Target_Array" in self.piece_dict:
            st = self.piece_dict.get("P_Sequence_Target_Array")[-1]     # use the last added entry
            if Params is None:
                return st
            for key in list(Params.keys()):
                Params[key] = st.get(key)
            return Params
        return False

    def updatesequencetarget_at(self, Params, TargetName, PieceName=None):
        if self.main_dict is not None:
            if self.batch_dict is not None:     # in batch-mode first we have to search for desired piece
                for piece in self.batch_dict["B_Piece_Array"]:
                    if piece['P_Name'] == PieceName:
                        for target in piece["P_Sequence_Target_Array"]:
                            if target['ST_Name'] == TargetName:
                                self.checkStringType(Params)
                                target.update(Params)
                                return True
            elif self.piece_dict is not None:   # in piece-mode we can begin with searching for the desired target
                for target in self.piece_dict["P_Sequence_Target_Array"]:
                    if target['ST_Name'] == TargetName:
                        self.checkStringType(Params)
                        target.update(Params)
                        return True
        return False

    def addsequencecriteria(self, Params):
        if self.piece_dict is not None and self.st_dict is not None:
            #
            # self.st_dict["ST_Technology_Criteria_Array"].append( self.Criteria(Params) )
            self.criteria_dict = Criteria(Params)
            self.st_dict["ST_Technology_Criteria_Array"].append( self.criteria_dict )
            return True
        return False
    def addsequencecriteriaNEW(self, Params):
        if hasattr(self.st_dict, "addCriteria"):
            self.st_dict.addCriteria(Params)
            return True
        return False

    def updatesequencecriteria(self, Params):
        if self.piece_dict is not None and self.st_dict is not None and self.criteria_dict is not None:
            #
            self.criteria_dict.update(Params)
            return True
        return False

    def getsequencecriteriaParams(self, Params=None):
        if self.piece_dict is not None and self.st_dict is not None and self.criteria_dict is not None:
            if Params is None:
                return self.criteria_dict
            for key in list(Params.keys()):
                Params[key] = self.criteria_dict.get(key)
            return Params
        return False

    def updatesequencecriteria_at(self, Params, TargetName, CriteriaName):
        if self.piece_dict is not None and self.st_dict is not None and self.criteria_dict is not None:
                for target in self.piece_dict["P_Sequence_Target_Array"]:
                    if target['ST_Name'] == TargetName:
                        for criteria in self.st_dict["ST_Technology_Criteria_Array"]:
                            if criteria['C_Name'] == CriteriaName:
                                self.checkStringType(Params)
                                criteria.update(Params)
                                return True
        return False

    def setgeometry(self, Name=None, NCDataString=None, autocount=True, noHeader=False):
        if self.piece_dict is None:
            print("Error : No piece exists - create this before calling setgeometry-method!")
            return False
        if self.machining_dict is None:
            print("Error : No machining exists - create this before calling setgeometry-method!")
            return False

        cnt = 0
        if Name is not None:
            if autocount == True:
                name_in_list = True
                while name_in_list:
                    cnt += 1
                    for mach in self.piece_dict["P_Machining_Array"]:
                        if mach['M_Geometry_Name'] == "{}_{:03d}".format(Name, cnt):
                            break
                    else:
                        name_in_list = False
                #
                self.machining_dict["M_Geometry_Name"] = "{}_{:03d}".format(Name, cnt)
            else:
                self.machining_dict["M_Geometry_Name"] = Name
        if NCDataString is not None:
            if noHeader:
                self.machining_dict["M_Geometry"] = NCDataString
            else:
                self.machining_dict["M_Geometry"] = UNIQUA_GEOMETRY_HEADER + NCDataString
        return True
    
    def setgeometry_at(self, Name=None, NCDataString=None, MachiningName=None, PieceName=None):
        if self.main_dict is not None:
            if self.batch_dict is not None:     # in batch-mode first we have to search for desired piece
                for piece in self.batch_dict["B_Piece_Array"]:
                    if piece['P_Name'] == PieceName:
                        for machining in piece["P_Machining_Array"]:
                            if machining['M_Name'] == MachiningName:
                                if Name is not None:
                                    machining["M_Geometry_Name"] = Name
                                if NCDataString is not None:
                                    machining["M_Geometry"] = UNIQUA_GEOMETRY_HEADER + NCDataString
                                return True
            elif self.piece_dict is not None:   # in piece-mode we can begin with searching for the desired machining
                for machining in self.piece_dict["P_Machining_Array"]:
                    if machining['M_Name'] == MachiningName:
                        if Name is not None:
                            machining["M_Geometry_Name"] = Name
                        if NCDataString is not None:
                            machining["M_Geometry"] = UNIQUA_GEOMETRY_HEADER + NCDataString
                        return True
        return False
    
    def setpocketing(self, Name=None, NCDataString=None, autocount=True, noHeader=False):
        if self.piece_dict is None:
            print("Error : No piece exists - create this before calling setpocketing-method!")
            return False
        if self.machining_dict is None:
            print("Error : No machining exists - create this before calling setpocketing-method!")
            return False
        
        cnt = 0
        if Name is not None:
            if autocount == True:
                name_in_list = True
                while name_in_list:
                    cnt += 1
                    for mach in self.piece_dict["P_Machining_Array"]:
                        if mach['M_Geometry_Pocketing_Name'] == "{}_{:03d}".format(Name, cnt):
                            break
                    else:
                        name_in_list = False
                #
                self.machining_dict["M_Geometry_Pocketing_Name"] = "{}_{:03d}".format(Name, cnt)
            else:
                self.machining_dict["M_Geometry_Pocketing_Name"] = Name
        if NCDataString is not None:
            if noHeader:
                self.machining_dict["M_Geometry_Pocketing"] = NCDataString
            else:
                self.machining_dict["M_Geometry_Pocketing"] = UNIQUA_GEOMETRY_HEADER + NCDataString
        return cnt
    
    def setstartpoint(self, Name=None, x=None, y=None, u=None, v=None, autocount=True):
        if self.piece_dict is None:
            print("Error : No piece exists - create this before calling setstartpoint-method!")
            return False
        if self.machining_dict is None:
            print("Error : No machining exists - create this before calling setstartpoint-method!")
            return False
    
        cnt = 0
        if Name is not None:
            if autocount == True:
                name_in_list = True
                while name_in_list:
                    cnt += 1
                    for mach in self.piece_dict["P_Machining_Array"]:
                        if mach['M_Startpoint_Name'] == "{}_{:03d}".format(Name, cnt):
                            break
                    else:
                        name_in_list = False
                #
                self.machining_dict["M_Startpoint_Name"] = "{}_{:03d}".format(Name, cnt)
            else:
                self.machining_dict["M_Startpoint_Name"] = Name
        if x is not None:
            self.machining_dict["M_Startpoint_X"] = x
        if y is not None:
            self.machining_dict["M_Startpoint_Y"] = y
        if u is not None:
            self.machining_dict["M_Startpoint_U"] = u
        if v is not None:
            self.machining_dict["M_Startpoint_V"] = v
        return cnt

    def setendpoint(self, Name=None, x=None, y=None, u=None, v=None, autocount=True):
        if self.piece_dict is None:
            print("Error : No piece exists - create this before calling setendpoint-method!")
            return False
        if self.machining_dict is None:
            print("Error : No machining exists - create this before calling setendpoint-method!")
            return False

        cnt = 0
        if Name is not None:
            if autocount == True:
                name_in_list = True
                while name_in_list:
                    cnt += 1
                    for mach in self.piece_dict["P_Machining_Array"]:
                        if mach['M_Endpoint_Name'] == "{}_{:03d}".format(Name, cnt):
                            break
                    else:
                        name_in_list = False
                #
                self.machining_dict["M_Endpoint_Name"] = "{}_{:03d}".format(Name, cnt)
            else:
                self.machining_dict["M_Endpoint_Name"] = Name
        if x is not None:
            self.machining_dict["M_Endpoint_X"] = x
        if y is not None:
            self.machining_dict["M_Endpoint_Y"] = y
        if u is not None:
            self.machining_dict["M_Endpoint_U"] = u
        if v is not None:
            self.machining_dict["M_Endpoint_V"] = v
        return cnt

    def addcriteria(self, Params):
        if self.machining_dict is not None:
            self.criteria_dict = Criteria(Params)
            self.machining_dict["M_Technology_Criteria_Array"].append( self.criteria_dict )
            return True
        return False

    def addcriteria2(self, Params):
        if self.machining_dict is not None:
            if Params.get('C_Name'):
                self.criteria_dict = Criteria(Params)
                self.machining_dict["M_Technology_Criteria_Array"].append( self.criteria_dict )
                return True
        return False

    def updatecriteria(self, Params):
        if self.criteria_dict is not None:
            self.checkStringType(Params)
            self.criteria_dict.update(Params)
            return True
        return False

    def updatecriteria2(self, Params, addIfNotExists=True):
        if self.machining_dict is not None:
            if Params.get('C_Name'):
                for i, crit in enumerate( self.machining_dict["M_Technology_Criteria_Array"] ):
                    if crit.get('C_Name') == Params.get('C_Name'):
                        self.machining_dict["M_Technology_Criteria_Array"][i].update(Params)
                        break
                else:
                    if addIfNotExists:
                        self.addcriteria2(Params)
                return True
        return False
    
    def setexpectedtechnology(self, Params):
        if self.piece_dict is None:
            print("Error : No piece exists - create this before calling setexpectedtechnology-method!")
            return False

        if UNIQUA_OPERATION_MODE == DYNAMIC_JSON and hasattr(self.machining_dict, "setExpectedTechnology"):
            self.machining_dict.setExpectedTechnology(Params)
            return True
        if UNIQUA_OPERATION_MODE == SEQUENTIAL_JSON and hasattr(self.st_dict, "setExpectedTechnology"):
            self.st_dict.setExpectedTechnology(Params)
            return True
        return False
    
    def addexpectedcriteria(self, Params):
        if self.piece_dict is None:
            print("Error : No piece exists - create this before calling addexpectedcriteria-method!")
            return False

        if UNIQUA_OPERATION_MODE == DYNAMIC_JSON and hasattr(self.machining_dict, "addexpectedcriteria"):
            self.machining_dict.addexpectedcriteria(Params)
            return True
        if UNIQUA_OPERATION_MODE == SEQUENTIAL_JSON and hasattr(self.st_dict, "addexpectedcriteria"):
            self.st_dict.addexpectedcriteria(Params)
            return True
        return False
    
    def addexpectedpass(self, Params):
        if self.piece_dict is None:
            print("Error : No piece exists - create this before calling addexpectedpass-method!")
            return False

        if UNIQUA_OPERATION_MODE == DYNAMIC_JSON and hasattr(self.machining_dict, "addExpectedPass"):
            self.machining_dict.addExpectedPass(Params)
            return True
        if UNIQUA_OPERATION_MODE == SEQUENTIAL_JSON and hasattr(self.st_dict, "addExpectedPass"):
            self.st_dict.addExpectedPass(Params)
            return True
        return False
    
    def addpasscriteria(self, Params):
        if self.piece_dict is None:
            print("Error : No piece exists - create this before calling addpasscriteria-method!")
            return False

        if UNIQUA_OPERATION_MODE == DYNAMIC_JSON and hasattr(self.machining_dict, "addPassCriteria"):
            self.machining_dict.addPassCriteria(Params)
            return True
        if UNIQUA_OPERATION_MODE == SEQUENTIAL_JSON and hasattr(self.st_dict, "addPassCriteria"):
            self.st_dict.addPassCriteria(Params)
            return True
        return False
    
    def addpoint(self, Params, autocount=True):
        if self.machining_dict is not None:
            if autocount == True:
                cnt = 0
                name_in_list = True
                PT_Name = Params.get('PT_Name', 'Point')
                while name_in_list:
                    cnt += 1
                    for pkt in self.machining_dict["M_Point_Array"]:
                        if pkt['PT_Name'] == "{}_{:03d}".format(PT_Name, cnt):
                            break
                    else:
                        name_in_list = False
                        Params['PT_Name'] = "{}_{:03d}".format(PT_Name, cnt)
            #
            #self.machining_dict["M_Point_Array"].append( self.Point(Params) )
            self.point_dict = Point(Params)
            self.machining_dict["M_Point_Array"].append( self.point_dict )
            return True
        return False

    def updatepoint(self, Params):
        if self.point_dict is not None:
            self.checkStringType(Params)
            self.point_dict.update(Params)
            return True
        return False
    
    def addsector(self, Params):
        if self.machining_dict is not None:
            self.sector_dict = Sector(Params)
            self.machining_dict["M_Sector_Array"].append( self.sector_dict )
            return True
        return False

    def updatesector(self, Params):
        if self.sector_dict is not None:
            self.checkStringType(Params)
            self.sector_dict.update(Params)
            return True
        return False

    def checkStringType(self, Params):
        """ in python2 we should check for string-type: then transform it into unicode"""
        if PY2:
            for key, val in list(Params.items()):
                if isinstance(val, str):
                    Params[key] = val.decode(fse)


# Implemented some useful utility functions (mainly for getting data from the minDB xml-files)

def jgetminbDBFilesList( rootfolder ):
    #return os.listdir( rootfolder )         # all
    try:
        return ['-'] + next(os.walk( rootfolder ))[1]    # only folders
    except StopIteration:
        return []

def jgetminbDBFileVersion( xmlFolder, xmlFile, mainmatch="MinimalDBVersion" ):
    """ Read org. gf db-file in xml format
    """
    import xml.etree.ElementTree as ET
    global MINIMALDBVERSION
    
    xmlPath = os.path.join(xmlFolder, xmlFile)
    MINIMALDBVERSION = ''
    if os.path.exists(xmlPath):

        on_mainmatch_tag = False

        for event, elem in ET.iterparse(xmlPath, events=("start", )):
            if event == "start":
                if elem.tag == mainmatch:
                    MINIMALDBVERSION = elem.text
                    break
            #
            elem.clear()

    return MINIMALDBVERSION

def jgetminbDBMaterials( xmlFolder, xmlFile, mainmatch="TableMaterial", submatch="Material" ):
    """ Read org. gf db-file in xml format
    """
    import xml.etree.ElementTree as ET
    
    xmlPath = os.path.join(xmlFolder, xmlFile)
    materials = []
    codes = []      # not used atm.
    if os.path.exists(xmlPath):

        on_mainmatch_tag = False
        on_submatch_tag = False
        
        for event, elem in ET.iterparse(xmlPath, events=("start", "end")):
            if event == "start":
                if elem.tag == mainmatch:
                    on_mainmatch_tag = True
                elif on_mainmatch_tag == True and elem.tag == submatch:
                    on_submatch_tag = True
                elif on_submatch_tag == True and elem.tag == "Code":
                    codes.append(elem.text)
                elif on_submatch_tag == True and elem.tag == "PresentationName":
                    materials.append(elem.text)
            #
            elif event == "end":
                if elem.tag == submatch:
                    on_submatch_tag = False
                elif elem.tag == mainmatch:
                    on_mainmatch_tag = False
                    break
            elem.clear()

    return materials or ['Steel']       # provide a default if nothing was found
    
def jgetminbDBWires( xmlFolder, xmlFile, mainmatch="Wires", submatch="TableWire" ):
    """ Read org. gf db-file in xml format
    """
    import xml.etree.ElementTree as ET
    
    xmlPath = os.path.join(xmlFolder, xmlFile)
    wires = []
    keys = []       # not used atm.
    if os.path.exists(xmlPath):

        on_mainmatch_tag = False
        on_submatch_tag = False
        
        for event, elem in ET.iterparse(xmlPath, events=("start", "end")):
            if event == "start":
                if elem.tag == mainmatch:
                    on_mainmatch_tag = True
                elif on_mainmatch_tag == True and elem.tag == submatch:
                    on_submatch_tag = True
                elif on_submatch_tag == True and elem.tag == "KeyName":
                    keys.append(elem.text)
                elif on_submatch_tag == True and elem.tag == "PresentationName":
                    wires.append(elem.text)
                elif on_submatch_tag == True and elem.tag == "Material":
                    dummy = elem.text
                elif on_submatch_tag == True and elem.tag == "MaterialCode":
                    dummy = elem.text
                elif on_submatch_tag == True and elem.tag == "Diameter":
                    dummy = elem.text
            #
            elif event == "end":
                if elem.tag == submatch:
                    on_submatch_tag = False
                elif elem.tag == mainmatch:
                    on_mainmatch_tag = False
                    break
            elem.clear()

    return wires or ['AC Cut A 900']    # provide a default if nothing was found


if __name__ == '__main__':
    #
    # create JsonBase instance (and set nc output filename)
    filePath = "C:/tmp/test1.json"
    filePath = "J:/Kurz/Uniqua/test1.json"
    j = JsonBase(filePath)
    #
    # 1. add manifest object
    Params = {}
    description = "Programm description"    
    Params['MAN_Description'] = description
    Params['MAN_Creator'] = "OPTICAM-TEST"
    j.addmanifest(Params)

    # 1.2. add batch object
    Params = {}     # to be filled with real values...
    Params['B_Name'] = os.path.basename( filePath )  # I think it's more suitable to use for piece same name
    Params['B_Description'] = u"Schöne Batchdatei"
    Params['B_Generation_Strategy_Name'] = u"Spät"    
    j.addbatch(Params)

    # 2. add piece object
    Material = ("Steel", "Copper", "Graphite", "Hard Metal", "Alluminium", "Titanium", "PCD_CTB010")
    Params = {}     # to be filled with real values...
    Params['P_Name'] = os.path.basename( filePath )  # I think it's more suitable to use for piece same name
    Params['P_Description'] = u"Blöde Beschreibung"
    Params['P_Material'] = Material[0]      # ...or another    
    j.addpiece(Params)
    
    # 3. add machining object
    Params = {}     # to be filled with real values...
    # TODO: Technology stuff
    Params['M_Wire_Name'] = "AC Cut A 900"      # AC Cut A 900 TODO : Look for a table
    Params['M_Wire_Diameter'] = 0.25
    Params['M_Technology_Selection'] = "First"  # First, User, Speed, Surface
    
    # ...add geometry (nc data string)
    Params["M_Geometry_Name"] = "Geometrie_1"
    Params["M_Geometry"] = UNIQUA_GEOMETRY_HEADER + j.SingleSentenceData()      # ...just an example for tests
    j.addmachining(Params)
    # another possibility to add nc data (must be called after addmachining mthod)
#     if pocketing:
#         j.setpocketing("Pocketing_1", j.PocketingData())
#     else:
#         j.setgeometry("Geometrie_1", j.SingleSentenceData())
    #
    # not used 
#     j.addcriteria()
#     j.addpoint()
#     j.addsector()

    # ... add more machining objects
#     Params = {}     # to be filled with real values...
#     j.addmachining(Params)
#     j.setgeometry("Geometrie_1", j.SingleSentenceData())
    
    # 4. write json file
    j.write()
    ################ READY #################
    
    # 5. test reading the file
    # j.read(filePath)
