
#!/usr/bin/env python

## This program creates Terminal Blocks for QET projects.

## Changelog

# v4.0: 
#   * New sort procedure created (sortTB) that consideres 5 types of data.
#     Terminals names like will be sorted as U1, V1, W1 y U2,V2,W2 instead of U1,U2,V1,V2, W1,W2

# v0.35: fix total width and height in xml definition and draw reservation terminals
#   * In First order terminals strings id ('-0,'+1',...) and later numeric values. Creates RESERVATION
#     terminals for no consecutive numeric terminals.
#   * Calc of total width and height are rounded to the next tenth.


# v0.3: add new functionalities and a GUI selection project
#   * GUI file dialog for select QET project. Save last path at 'recent.his'
#   * System defines the default QET collection path, but for portable instalations, can 
#     set by default at global constant QET_COLLECTION_PATH and is needed to type every time.
#   * Now can chose XRef forlio type. By ID (folio number) or by folio Name

# v0.2: add new functionalities and fix errors
#   * Autodect the path of QET collection dependig the OS. Ask when runs, but a default value is proposed
#   * Creates the collection temp directory for terminal blocks if not exist
#   * Saves recent files at 'recent.his' file for easy select lastest projects
#   * Now respect case. Be careful, now because X1 is different than x1
#   * The form of then name in collection for the terminal blocks created: Project X1(from-to)
#   * Fix xref calc. The top and side grid label has 25px with.
#
# v0.1: 
#   * Terminal elements must be 'terminal' as type in the Element Editor.
#   * If terminal elements has more than one textfield, the first one is considered.
#   * Name of terminals must be in form 'x:a'.  x = Terminal Block name; a = Terminal name.
#   * Creates a Block terminal for every diferents x value. If there ara more 'a' values
#     than the global variable TB_BLOCKS_IN_A_FOLIO, divide the block terminals in parts
#     to put in differents folios
#   * Sorts the 'a' values placing fierts alphanumeric, and later the numeric values.
#   * If there are consecutive terminals if samne number cables, draws a bridge between them.




## IMPORTS
import glob
import os
import platform
import re
import sys
import tkinter
import tkinter.filedialog
import uuid
import xml.etree.ElementTree as etree

########## GLOBAL CONFIG CONSTANTS
VERSION = '0.4'
QET_COLLECTION_PATH = '' #default QET collection path. Leave blank to use default installation
TB_COLLECTION_FOLDER="temp_tb" #folder where save created terminal blocks elements at QET_COLLECTION_PATH
TB_BLOCKS_IN_A_FOLIO = 40
TB_HEAD_WIDTH = 44
TB_HEAD_UNION_HEIGHT = 70
TB_HEAD_UNION_WIDTH = 6
TB_HEIGHT = 120
TB_WIDTH = 20
QET_COL_ROW_SIZE = 25 #pixels offset for elements coordinates
RESERVATION_LABEL = 'RES.'

########## GLOBAL CONSTANTS
READ = 1
SAVE = 2
DEBUG = False


########## getListOfBorneElements
def getListOfBorneElements (proj):
    """Return a list of component in library(collection) that
    are 'terminals' and english name is 'terminal block'.
    Case insensitive
    
    @param QETproject: root of the QET project
    @return [] list with el names of elements that are terminal as 'like_type'"""

    collection = proj.find('collection') #collection root
    ret = [] #return list

    for element in collection.findall ('.//element'):
        definition = element[0]
        if 'link_type' in definition.attrib:
            if definition.attrib['link_type'] == 'terminal':
                ret.append (element.attrib['name'])

    return list(set(ret)) #remove duplicates


########## getListOfUsedElements
def getListOfUsedElements (proj, elementsToFind, sFolioReferenceType):
    """Return a list of all elements in elements 'elementsToFind'
    that are used in diagram pages.
    
    @param QETproject: root of the QET project
    @param elementsToFind: list of elements to find
    @param sFolioReferenceType: 'I' for folio ID (1,2,3,...) or 'N' for folio name ('+PLC',...)
    @return [] list of list where:
        [element_uuid, terminal_xref, terminal block, terminal number, cable_number1, cable_number2] """
    

    ret = [] #return list
    pageOffset = int(proj.attrib[ 'folioSheetQuantity' ]) #check for a index of folios page
    
    for diagram in proj.findall ('diagram'): #all diagrams
        for element in diagram.findall ('.//element'): #all elements in diagram
            if 'type' in element.attrib: #elements must have a 'type'
                for el in elementsToFind: #all elements in collectio that are 'terminal'
                    offset = len(element.attrib['type']) - len(el)
                    if ( element.attrib['type'][offset:] == el): #check if element is one of elements in collection collected
                        text = element.find( 'inputs' ).find( 'input' ).attrib['text'] #text like 'X1:1'
                        if ( ':' in text ) and ( len(text) > 2 ):   #TODO: improve with regex

                            element_data = [] #list for current element
                            if DEBUG: print ("<getListOfUsedElements> searching: %s  \t offset: %i \t %s \t %s" %(el, offset, element.attrib['type'], element.attrib['type'][offset:]))
                            if DEBUG: print ("<getListOfUsedElements> Working with element %s" %element.attrib['uuid'])
                            element_data.append( element.attrib['uuid'] ) #uuid
                            element_data.append( getXRef (diagram, element, pageOffset, sFolioReferenceType) ) #xref
                            element_data.append( text.split( ':' )[0] ) #terminal block name
                            element_data.append( text.split( ':' )[1] )# terminal number
                            terminals = element.find( 'terminals' ).findall( 'terminal' )
                            element_data.append( getCableNum( diagram, terminals[0].attrib['id'] ) ) #cable 1
                            element_data.append( getCableNum( diagram, terminals[1].attrib['id'] ) )#cable 2

                            ret.append (element_data)
                            
    return ret


########## getXRef
def getXRef (diagram, element, iPageOffset, sFolioReferenceType):
    """Return a string with the xreference of the 'element' at page 'diagam'.
    The page number incremented in one if there are a "index" page
    
    @param diagram: diagram(page) XML etree object
    @param element: element XML etree object
    @param iPagesOffset: offset to add at "page_order" references
    @param sFolioReferenceType: 'I' for folio ID (1,2,3,...) or 'N' for folio name ('+PLC',...)
    @return: string like "p-rc" (page - rowLetter colNumber)"""

    
    #get requiered data
    if sFolioReferenceType == 'I':
        page = int( diagram.attrib['order'] ) + iPageOffset
    elif sFolioReferenceType == 'N':
        page = diagram.attrib['folio']
    cols = int( diagram.attrib['cols'] )
    col_size = int( diagram.attrib['colsize'] )
    rows = int( diagram.attrib['rows'] )
    row_size = int( diagram.attrib['rowsize'] )
    element_x = int( element.attrib ['x'] )
    element_y = int( element.attrib ['y'] )
    rows_letters =  [chr(x+65) for x in range(rows)]

    if DEBUG: print ("<getXRef>: Page order: %i\tCol size: %i\tRow size: %i\tX position: %i\tY Position: %i" \
                     %(page, col_size, row_size, element_x, element_y))
    
    row_letter = rows_letters[ int( (element_y - QET_COL_ROW_SIZE) / row_size) -1 + 1 ]  #+1: cal calc. -1 index of lists start 0.
    column =  str( int( (element_x - QET_COL_ROW_SIZE) / col_size ) +1 )
    return str(page) + "-" + row_letter + column 
    

########## getCableNum
def getCableNum (diagram, terminalId):
    """Return the cable number connected at 'terminalId' in the page 'diagram'
    
    @param diagram: diagram(page) XML etree object
    @param terminalId: text with the terminal Id
    @return: string whith cable  number"""

    ret=''
    for cable in diagram.find( 'conductors' ).findall( 'conductor' ):
        for cable_terminal in [x for x in cable.attrib if x[:8]=='terminal']:
            if cable.attrib[cable_terminal] == terminalId:
                ret = cable.attrib['num']
    return ret
       
            
########## createQETCollectionFile
def createQETCollectionFile (sCollectionPath):
    """Delete files o temp collection and create collection QET file.
    @param sCollectionPath end with '/'
    @return: none"""

    # delete contents
    #fs = glob.glob(sCollectionPath + '*')
    #for f in fs:
    #    os.remove(f)

    #create QET directory file
    f = open (sCollectionPath + 'qet_directory', 'w')
    xml = ['<qet-directory>',
            '    <names>',
            '        <name lang="en">TEMP: Terminal Blocks</name>',
            '    </names>',
            '</qet-directory>']
    f.writelines(["%s\n" % line for line in xml])
    f.close()


########## getWeight
def getWeight (s):
    """Return a int value according first char of s:
    * start by letter: index about -300
    * start by '-': index about -200
    * start by '+': index abput -100
    * start by digit: positive value
    This is usefull to sort lists with numbers and letters, but sortering the number values as
    integers and not as strings"""

    
    if s:
        #calc offset
        if s[0] == '-':
            offset = -200
        elif s[0] == '+': offset = -100
        else: offset = 0
        
    try:
        foo = int(s) #provoke a exception?
        return int(s) + offset
        
    except:
        return ord(s[0]) -1000
    
########## sortTB
def sortTB(listTerminals):
    """Sort a list of lists. Every sublist have the info of a terminal like this:
         [element_uuid_in_QET_projecto, terminal_xref, terminal block, terminal number, cable_number1, cable_number2]

    This function, considers that the 'terminal_block' field is the same for all terminals
    The field 'terminal_number' are sorted in this order:
      Type 1: Untyped: GND, R, S, T, M1A, M2A, M3A,... sorted alphabetically (GND, M1A, M2A, M3A, R, S, T)
      Type 2: Letters + Number : R1, V1, U1, U2, GND1, ... and every is sorted alphabetically (GND1,R1,U1,V1,U2)
      Type 3: Negative + Number: -0, -1 : Order numerically
      Type 4: Positive + Number: +0, +1, +2, +1,: Order numerically
      Type 5: Only Numbers: 1, 3, 7, 33,... Order numerically
    Adds 3 temporal fields at every sublist for sort purposes:
    [-3]: Type of terminal
    [-2]: Numeric part of terminal name
    [-1]: No numeric part
    
    @param listTerminals: list of lists
    @return listTerminals sorted."""
    
    TYPE_1 = 1 #untyped
    TYPE_2 = 2 #letters + numbers
    TYPE_3 = 3 #Negative + numbers
    TYPE_4 = 4 #Positive + numbers
    TYPE_5 = 5 #only numbers

    #determine type of terminal name and prepare temp data to sort
    for t in listTerminals:
        sTBName=t[3]
        if re.match('^\d+$',sTBName): #Type 5
            newSortType = TYPE_5
            newSortNum = sTBName
            newSortNotNum = ''
        elif re.match('^(\+)(\d)+$',sTBName): #Type 4
            newSortType = TYPE_4
            foo = re.match('^(\+)(\d)+$', sTBName)
            newSortNum = foo.group(2)
            newSortNotNum = foo.group(1)
        elif re.match('^(\-)(\d)+$',sTBName): #Type 3
            newSortType = TYPE_3
            foo = re.match('^(\-)(\d)+$', sTBName)
            newSortNum = foo.group(2)
            newSortNotNum = foo.group(1)
        elif re.match('^(\D+)(\d+)$',sTBName): #Type 2
            newSortType = TYPE_2
            foo = re.match('^([a-zA-Z]+)(\d+)$', sTBName)
            newSortNum = foo.group(2)
            newSortNotNum = foo.group(1)
        else: # Type 1
            newSortType = TYPE_1
            newSortNum = ''
            newSortNotNum = sTBName

        t.append(newSortType)   #[-3]
        t.append(newSortNum)    #[-2]
        t.append(newSortNotNum) #[-1]
        
    #Sort Type 1
    subData1 = [x for x in listTerminals if x[-3] == TYPE_1]
    subData1.sort( key=lambda x: (x[-1]) )

    #Sort Type 2
    subData2 = [x for x in listTerminals if x[-3] == TYPE_2]
    subData2.sort( key=lambda x: (int(x[-2]), x[-1]) )

    #Sort Type 3
    subData3 = [x for x in listTerminals if x[-3] == TYPE_3]
    subData3.sort( key=lambda x: (x[-2]) )

    #Sort Type 4
    subData4 = [x for x in listTerminals if x[-3] == TYPE_4]
    subData4.sort( key=lambda x: (x[-2]) )
    
    #sort type 5
    subData5 = [x for x in listTerminals if x[-3] == TYPE_5]
    subData5.sort( key=lambda x: int(x[-2]) )

    #return
    return [x[:-3] for x in subData1 + subData2 + subData3 + subData4 + subData5] #delete temp fields
   
    
########## drawTerminalBlock
def drawTerminalBlock (sFilePath, sTerminalBlockID, data, sProjectFileName):
    """Creates a XML file with the terminal block elements. if creates more than one folio
    (a lot of terminals) creates a index (0,1,...) as file name suffix.
    When start the firt terminal numered by a integer, inserts automatic RESERVATION of terminals
    Uses GLOBAL CONSTANTS
    @param sFilePath: path where create collection with end '/'
    @param sTerminalBlockID: id/name of the terminal block.
    @param data: unsortened list of list with terminal data of the corresponding TerminalBlockID.
                 [element_uuid, terminal_xref, terminal block, terminal number, cable_number1, cable_number2]
    @param sProjectFileName: name of QET file project
    @return: none"""

    #Create reservation terminals. Terminals number are string, and can be a number or not ('-0', '+2',...)
    allNumbers = [int(x[3]) for x in data if x[3].isdigit()]
    allNumbers.sort()

    if allNumbers: #if the are digits in terminals numeration
        for i in range(1, int(allNumbers[-1])):
            if i not in allNumbers: data.append ( ['', RESERVATION_LABEL, sTerminalBlockID, str(i), '', ''])
    
    #sort de terminals of the block
    dataSorted = sortTB( data )

    #Fill with reservations
    lastNum = 0
    fFinish = False
    reservation = [] #list of new reservations

    #Definitios to detect brigded terminals
    last_cable_number1 = '' #used to draw a bridge between terminals
    last_cable_number2 = ''
    
    i2 = 0
    for i1 in range ( 0, len(dataSorted) , TB_BLOCKS_IN_A_FOLIO ):
    #process every Term- Block of max length per folio
        
        i2 =  (i2 + TB_BLOCKS_IN_A_FOLIO)
        if i2 > len( dataSorted ): i2 = len( dataSorted )
        subData = dataSorted[ i1:i2 ]

        #Calcs
        iQuantity = len( subData ) #number of terminals

        totalWith = TB_HEAD_WIDTH + TB_HEAD_UNION_WIDTH + ( iQuantity * TB_WIDTH )
        totalWithRoundedUp = totalWith + 1 #+1 to force round the next tenth
        while ( totalWithRoundedUp % 10 ): totalWithRoundedUp += 1 #next tenth

        totalHeight = TB_HEIGHT + 20 + 20 #20 is line to terminal north and south
        totalHeightRoundedUp =  totalHeight + 1 #+1 to force round the next tenth
        while ( totalHeightRoundedUp % 10 ): totalHeightRoundedUp += 1 #next tenth

        sUUID = uuid.uuid1().urn[9:]
        sID = sProjectFileName + ' ' + sTerminalBlockID + '(' + str(i1 + 1) + '-' + str(i2) + ')' #element id in collection
        sFileName = sFilePath + sProjectFileName + '_' + sTerminalBlockID + '_' + str(i1 + 1) + '-' + str(i2) + '.elmt' #file name for the element

        #### Start 'drawing'
        rect_style =  ' antialias="false" style="line-style:normal;line-weight:normal;filling:white;color:black"'
        line_style =  ' antialias="false" style="line-style:normal;line-weight:normal;filling:none;color:black"'


        xml = ['<definition link_type="simple" hotspot_x="5" hotspot_y="24" width="%i" type="element" orientation="dyyy" height="%i" >' \
               %(totalWithRoundedUp, totalHeightRoundedUp),
               '  <uuid uuid="%s"/>' %sUUID,
               '  <names>',
               '    <name lang="en">%s</name>' %sID,
               '  </names>',
               '  <description>']
        x=0
        #head
        xml.append( '    <rect x="%i" y="0" width="%i" height="%i"' \
                    %(x, TB_HEAD_WIDTH, TB_HEIGHT) + rect_style + '/>' )
        x += TB_HEAD_WIDTH
        
        #head union
        xml.append( '    <rect x="%i" y="%i" width="%i" height="%i"' \
                    %(x, (TB_HEIGHT - TB_HEAD_UNION_HEIGHT)/2, TB_HEAD_UNION_WIDTH, TB_HEAD_UNION_HEIGHT) + \
                    rect_style + '/>' )
        x += TB_HEAD_UNION_WIDTH

        #TB ID
        xml.append( '    <input text="%s" tagg="label" rotation="270" size="10" x="%i" y="%i"' \
                    %(sTerminalBlockID, TB_HEAD_WIDTH/2, TB_HEIGHT/2 + (len(sTerminalBlockID)/2)*10) + '/>' )

        #All terminals
        for t in subData:
            halfx = x + TB_WIDTH/2
            xml.append('    <rect x="%i" y="0" width="%i" height="%i"' \
                       %(x, TB_WIDTH, TB_HEIGHT) + rect_style + '/>')
            xml.append('    <text text="%s" rotation="270" size="9" x="%i" y="%i"/>' %(t[3], halfx + 4,  TB_HEIGHT - 35) )#terminal name
            xml.append('    <text text="%s" rotation="270" size="6" x="%i" y="%i"/>' %(t[1], halfx + 4,  TB_HEIGHT - 65) ) #xref
            xml.append('    <input text="%s" tagg="none" rotation="270" size="6" x="%i" y="%i"/>' %(t[4], halfx - 4, -0) )
            xml.append('    <input text="%s" tagg="none" rotation="270" size="6" x="%i" y="%i"/>' %(t[5], halfx - 4, TB_HEIGHT + 25) )

            xml.append('    <line length2="1.5" y1="%i" y2="%i" x1="%i" end1="none" x2="%i" end2="none" length1="1.5"' \
                       %(0, -20, halfx,halfx) + line_style + '/>')
            xml.append('    <line length2="1.5" y1="%i" y2="%i" x1="%i" end1="none" x2="%i" end2="none" length1="1.5"' \
                       %(TB_HEIGHT, TB_HEIGHT + 20, halfx, halfx) + line_style + '/>')

            xml.append('    <terminal orientation="n" x="%i" y="%i"/>' %(halfx, 0 - 20))
            xml.append('    <terminal orientation="s" x="%i" y="%i"/>' %(halfx, TB_HEIGHT + 20))
            xml.append('    <circle x="%i" y="%i" diameter="4"' %(halfx - 2, TB_HEIGHT - 20) + line_style + '/>')

            #bridge
            a= t[1]
            if (t[4] == t[5]) and (last_cable_number1 == last_cable_number2) and \
               (t[4] == last_cable_number1) and (t[1] != RESERVATION_LABEL): #if 4 cables are the same... but not a RESERVE terminal
                xml.append('    <line length2="1.5" y1="%i" y2="%i" x1="%i" end1="none" x2="%i" end2="none" length1="1.5"' \
                       %(TB_HEIGHT - 20 + 2 , TB_HEIGHT - 20 + 2, halfx - TB_WIDTH, halfx) + line_style + '/>')
            last_cable_number1 = t[4]
            last_cable_number2 = t[5]
                
            x += TB_WIDTH
          
        xml.append( '  </description>' )
        xml.append( '</definition>' )

        #write to file
        with open(sFileName, 'w') as f:
            f.writelines(["%s\n" % line for line in xml])


########## getUserInput
def  getUserInput (sMessage, sDefaultInput):
    """get input from user showing a message and a defult input if nothing is entered
    @param sMessage: info message for the user
    @param sDefaultInput: default value
    @return string: text entered"""

    sInput = input ( sMessage + ' [' + sDefaultInput + ']: ' )

    if sInput == '':
        return sDefaultInput
    else:
        return sInput


########## QETDataPath
def  QETDataPath ():
    """If global constant QET_COLLECTION_PATH is blank, return QET Path collection path
    depending the OS. Creates dir if not exist
    @return string: path"""

    path = ''
    
    if QET_COLLECTION_PATH == '': #blank. Uses system depending
        system = platform.system()
        home = os.path.expanduser("~")
        if (system == 'Linux') or (system == 'Darwin'):
            path = home + '/.qet/elements/'
        elif system == 'Windows':
            path = home + '\\AppData\\Roaming\\qet\\elements\\'
    else: #use defined by user
        path = QET_COLLECTION_PATH

    if ( path[-1] != os.sep ): path += os.sep
    fullpath = path +  TB_COLLECTION_FOLDER + os.sep
    if not os.path.exists(fullpath): os.makedirs(fullpath)

    return fullpath


########## QETProjectsHistory
def  QETProjectsHistory (iAction, sPath=''):
    """Manages history of lasts files used.
    @param int iAction
        1: return last path used
        2: save last path used
    @return list"""

    HIS_FILE = 'recent.his'

    #create if not exist
    if not os.path.isfile( HIS_FILE ):
        open( HIS_FILE, 'w' ).close()

    #read the file
    with open(HIS_FILE, 'r+') as f:
        last = f.readline()

    #process
    if iAction == 1: #read
        return last
    elif iAction == 2 and len(sPath): #save
        if sPath != last:
            with open(HIS_FILE, 'w') as f:
                f.write(sPath)
 

############################################### MAIN
if __name__ == '__main__':

    print ("\n<< Terminal Block maker for QET " + VERSION + " - by Raul Roda >>\n")
    print ("Remember the next conditions:")
    print ("  * Terminal elements must be 'terminal' as type in the Element Editor.")
    print ("  * If terminal elements has more than one textfield, the first one is considered.")
    print ("  * Name of terminals must be in form 'x:a'.")
    print ("        x = Terminal Block name")
    print ("        a = Terminal name.\n")


    #define gui
    gui = tkinter.Tk()
    gui.withdraw() #hides root window

    #choose QET project
    sQETProjectFile = tkinter.filedialog.askopenfilename(initialdir = QETProjectsHistory(READ),
                                                         filetypes=[("QET projects","*.qet")])
    QETProjectsHistory(SAVE, os.path.dirname(sQETProjectFile))
    
    #Get QET collection path for the Terminal Blocks
    sTBCollectionPath = getUserInput ('Path to place created QET Terminal Blocks ', QETDataPath() )

    #Get XRef by folio ID or by FolioName
    sTypeXRef = ''
    while sTypeXRef not in ['I','N']:
        sTypeXRef = getUserInput ('XRef to folios by Id or by Name (I / N)', 'I').upper()
    
    #Start the process
    print ("\nCalculating...")

    qet_tree = etree.parse(sQETProjectFile)  
    QETproject = qet_tree.getroot()

    borneElements = getListOfBorneElements (QETproject) #list with 'terminal' elements in collection

    usedBorneElements = getListOfUsedElements (QETproject, borneElements, sTypeXRef) #[element_uuid, terminal_xref, terminal block, terminal number, cable_number1, cable_number2]

    if DEBUG: print ([e for e in usedBorneElements])
    createQETCollectionFile( sTBCollectionPath )

    #Draw every different block terminal name: X1, X2,...
    allTB = list ( set( [x[2] for x in usedBorneElements] ) ) #get unique TerminalBlockID
    for TB in allTB:
        drawTerminalBlock (sTBCollectionPath , TB,
                           [x for x in usedBorneElements if x[2] == TB],
                           os.path.basename (sQETProjectFile)[:-4] )    

    print ("\nDone.")
