
#!/usr/bin/env python

## This program creates Terminal Blocks for QET projects.

## Changelog

# 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.
#   * 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.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 sys
import tkinter
import tkinter.filedialog
import uuid
import xml.etree.ElementTree as etree

########## GLOBAL CONFIG CONSTANTS
VERSION = '0.3'
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 = 50
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

########## 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 ret


########## 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: 
                    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()


########## getOrder
def getOrder (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
    

    
########## 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.
    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"""
 
    data.sort(key=lambda x: getOrder(x[3])) #first sort by alphanimeric order

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

        iQuantity = len( subData )
        totalWith = TB_HEAD_WIDTH + iQuantity * TB_WIDTH
        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

        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 hotspot_x="4" hotspot_y="4" width="%i" height="%i" orientation="dyyy" link_type="simple" type="element">' \
               %(totalWith, max( TB_HEIGHT, TB_HEIGHT )),
               '  <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_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 - 38) )#terminal name
            xml.append('    <text text="%s" rotation="270" size="6" x="%i" y="%i"/>' %(t[1], halfx + 4,  TB_HEIGHT - 62) ) #xref
            xml.append('    <input text="%s" tagg="none" rotation="270" size="6" x="%i" y="%i"/>' %(t[4], halfx - 4, -10) )
            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
            if (t[4] == t[5]) and (last_cable_number1 == last_cable_number2) and (t[4] == last_cable_number1):
                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]', 'N').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.")
