#!/usr/bin/env python3
# encoding: utf-8

#---------|---------|---------|---------|---------|---------|---------|---------|
# Copyright (C) 2018 Raul Roda <raulroda@yahoo.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#---------|---------|---------|---------|---------|---------|---------|---------|


# TODO
# muchos borneros
# long. max de un bornero


## Imports
from collections import OrderedDict
from functools import cmp_to_key
from operator import itemgetter as i
import re
import sys
import tkinter as tk
from tkinter import filedialog 
from tkinter import messagebox
from tkinter import ttk
import xml.etree.ElementTree as etree


## Globals (allows access from callbacks events)
frmFoot = None  # Has the auto-fill info
qet_project = None  # class to manage QET_Projects
data = None  # Has the terminals info of the project.


# CONSTANTS
DEBUG = True
STRIP_LONG = 30
txt = {
    0: "- Right click to auto-fill.\n- Left click to edit.\n- <Return> after edit updates auto-fill"
}
VERSION = '1.0.2.alpha'
SAMPLE_DATA = [ \
        ['-', '-X1', '1', '2-B4', 'E0.1', 'STANDARD', 'W2', '3'], \
        ['-', '-X1', 'GND', '2-B6', 'GND', 'GROUND', 'W2', 'GND'], \
        ['-', '-X1', '+2', '1-A2', '+2', 'STANDARD', 'W2', '1'], \
        ['-', '-X1', '-0', '1-A3', '-0', 'STANDARD', 'W2', '2'], \
        ['-', '-X1', 'GND', '2-B5', 'T', 'STANDARD', 'W1', 'GND'], \
        ['-', '-X1', 'R', '2-B5', 'R', 'STANDARD', 'W1', 'Black'], \
        ['-', '-X1', 'S', '2-B5', 'S', 'STANDARD', 'W1', 'Brown'], \
        ['-', '-X1', 'T', '2-B5', 'T', 'STANDARD', 'W1', 'Gray'], \
        ['-', '-X1', '+1', '1-A1', '+1', 'FUSE', '', ''], \
    ]

# GENERAL CONFIG
#font_grid_caption = Font(family="Courier", size=11, weight='bold')

#!/usr/bin/env python3
# encoding: utf-8

import xml.etree.ElementTree as etree  # python3-lxml

class QETProject:
    """This class works with the XML source file of a QET Project"""

    # class attributes
    XREF_DEFAULT = 'D'
    XREF_AUTO = 'A'
    QET_COL_ROW_SIZE = 25  # pixels offset for elements coord
    DEBUG = False


    def __init__(self, project_file, fromPage='', \
            toPage = '', searchImplicitsConnections = False):
        """class initializer. Parses the QET XML file.
        @param project_file: file of the QET project
        @param folio_reference_type: how to calc XRefs when recover project info:
           'A' auto. Same xref as configured in the QET diagram project.
           'D' default (%f-%l%c) i.e. 15-F4
        @param fromPage: first page in range to be processed
        @param toPage: last page in range to be processed
        @param searchImplicitsConnections: True for search implicit connections in TB creation"""

        self._qet_tree = etree.parse(project_file)

        self.qet_project_file = project_file
        self.qet_project = self._qet_tree.getroot()

        # determine xref format to use or default

        self.folio_reference_type = self.qet_project.find('.//newdiagrams'). \
                find('report').attrib['label']

        # XML version
        self.xml_version = self.qet_project.attrib['version']

        # pageOffset for folio numbers
        self.pageOffset = int (self.qet_project.attrib
                ['folioSheetQuantity'])  # offset table of contents

        # general project info
        self._totalPages = len (self.qet_project.findall('.//diagram')) + \
                self.pageOffset

        # elements type of terminal
        self._terminalElements = self._getListOfElementsByType( 'terminal' )


    def _getListOfElementsByType(self, element_type):
        """Return a list of component in library(collection) that
        have 'link_type' as element_type parameter.

        @return [] list with el names of elements that
                   are terminal as 'like_type'"""

        ret = []  # return list

        for element in self.qet_project.find('collection').iter('element'):
            definition = element[0]
            if 'link_type' in definition.attrib:
                if definition.attrib['link_type'] == element_type:
                    ret.append(element.attrib['name'])

        return list(set(ret))  # remove duplicates


    def _getElementName (self, element):
        """Returns the name of a terminal element.
        The name comes from 'dynamic_text' section.
        If not exists, the name is specified in elementInformation/label or 
        elementInformation/formula. 
        return: name of terminal"""

        for d in element.find('dynamic_texts').findall('dynamic_elmt_text'):
            if d.attrib['text_from'] == 'ElementInfo':
                return d.findtext('text')

        ## old version of QET XML diagram doesn't have dynamic text.
        label = formula = ''
        elinfos = element.find('elementInformations')
        if elinfos:
            #~ print ('---------------------------------------- 1')
            #~ print_debug_xml(elinfos)

            for t in elinfos.findall('elementInformation'):
                if t.attrib['name'] == 'label':
                    label = t.text
                if t.attrib['name'] == 'formula':
                    formula = t.text
        
        if label == None:  # attrib returns None if empty.
            label = ''
        if formula == None:
            formula = ''

        return [label, formula][label == '']


    def _getElementMetadata (self, element):
        """Returns the metadata of the terminal element.
        Al the info is Function field under 'elementInformation'
        return: {} keys: terminal_pos, terminal_type, cable, cable_conductor"""

        meta = ''
        ret = {'terminal_pos':'-', 'terminal_type':'STANDARD', 'cable':'', 'cable_cond':''}
    
        ## Get meta string
        for t in element.find('elementInformations').findall('elementInformation'):
            if t.attrib['name'] == 'function':
                meta = t.text
        
        ## Getting data
        foo  = re.search('%p(\d+)(%|$)', meta)
        if foo:
            ret['terminal_pos'] = foo.group(1)

        foo  = re.search('%t([^%]*)(%|$)', meta)
        if foo:
            ret['terminal_type'] = foo.group(1)
    
        foo  = re.search('%c([^%]*)(%|$)', meta)
        if foo:
            ret['cable'] = foo.group(1)
        
        foo  = re.search('%n([^%]*)(%|$)', meta)
        if foo:
            ret['cable_cond'] = foo.group(1)
        
        return ret


    def _isValidTerminal (self, element):
        """ An element is valid if type is 'terminal' and label is like 'X1:1'
        @param element:  element  (XML etree object)
        @return: True / False"""
        
        regex_name = '\w+:\W?\w+$'
        
        if re.search(':', self._getElementName(element)):
            if 'type' in element.attrib:  # elements must have a 'type'
                for el in self._terminalElements:  # searching type
                    if re.search(el + '$', element.attrib['type']):
                        return True
        
        return False


    def _getCableNum(self, 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

    
    def _getXRef(self, diagram, element, offset_x = 0, offset_y = 0):
        """Return a string with the xreference.

        The element is specified by '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 offset_x: correction of the coord x.
               Useful for Xref for the terminal of an element
        @param offset_y: correction of the coord y
        @return: string like "p-rc" (page - rowLetter colNumber)"""
        ret = self.folio_reference_type

        # get coord
        element_x = int(float(element.attrib['x'])) + int(float(offset_x))
        element_y = int(float(element.attrib['y'])) + int(float(offset_y))
        row, col = self._getXRefByCoord (diagram, element_x, element_y)
        diagram_page = str(int(diagram.attrib['order']) + self.pageOffset)

        # Change tags to real value
        if '%f' in ret:
            ret = ret.replace('%f', diagram_page)
        if '%F' in ret:
            # %F could include extra tags
            folio_label = diagram.attrib['folio']
            if '%id' in folio_label:
                folio_label = folio_label.replace('%id', diagram_page)
            if '%total' in folio_label:
                folio_label = folio_label.replace('%total', str(self._totalPages))
            if '%autonum' in folio_label:
                folio_label = folio_label.replace('%autonum', diagram_page)
            ret = ret.replace('%F', folio_label)
        if '%M' in ret:
            ret = ret.replace('%M', \
                self.qet_project.find('.//newdiagrams'). \
                find('inset').attrib['machine'])
        if '%LM' in ret:
            ret = ret.replace('%LM', \
                self.qet_project.find('.//newdiagrams'). \
                find('inset').attrib['locmach'])
        if '%l' in ret:
            ret = ret.replace('%l', row)
        if '%c' in ret:
            ret = ret.replace('%c', col)

        return ret


    def _getXRefByCoord(self, diagram, x, y):
        """Return a string with the xreference for the coordinates at page 'diagam'
        The page number incremented in one if there are a "index" page

        @param diagram: diagram(page) XML etree object
        @param x,y: coordinates
        @return: string like "p-rc" (page - rowLetter colNumber)"""

        # get requiered data
        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(x)
        element_y = int(y)
        rows_letters = [chr(x + 65) for x in range(rows)]

        if self.DEBUG:
            print( '<getXRef>: Page order: {}\tCol size: {}\tRow size: {}\tX position: {}\tY Position: {}'. \
                format (page, col_size, row_size, element_x, element_y))

        row_letter = rows_letters[ int(
                (element_y - QETProject.QET_COL_ROW_SIZE) / row_size) - 1 + 1]
                # +1: cal calc. -1 index of lists start 0.
        column = str(int((element_x - QETProject.QET_COL_ROW_SIZE) / col_size) + 1)
        return (row_letter, column)


    def get_list_of_used_terminals(self):
        """Return a list of all terminal elements used in the qet project.
        @return [] where every element:
            {uuid, block_name, strip, terminal_name, terminal_pos, 
            terminal_xref, terminal_type, conductor_name, cable, cable_cond} 
            
            strip: a long terminal strip is splited into a smaller parts.
        """
        ret = []  # return dict

        # first search for elements of type 'terminal' and its conductors.
        for diagram in self.qet_project.findall('diagram'):  # all diagrams
            for element in diagram.findall('.//element'):  # all elements in diagram
                el = {}

                if self._isValidTerminal(element):

                    terminalName = self._getElementName (element)
                    meta_data = self._getElementMetadata (element)
                    
                    terminals = element.find('terminals').findall( 'terminal' )
                    terminalId = terminals[0].attrib['id']
                    cableNum = self._getCableNum(diagram, terminalId)
                    terminalId2 = terminals[1].attrib['id']
                    cableNum2 = self._getCableNum(diagram, terminalId2)
                    if cableNum == '': cableNum = cableNum2
                    
                    el['uuid'] = element.attrib['uuid']
                    el['block_name'] = terminalName.split(':')[0]
                    el['segment'] = ''
                    el['terminal_name'] = terminalName.split(':')[1]
                    el['terminal_pos'] = meta_data['terminal_pos']
                    el['terminal_xref'] = self._getXRef(diagram, element)
                    el['terminal_type'] = meta_data['terminal_type']
                    el['conductor_name'] = cableNum
                    el['cable'] = meta_data['cable']
                    el['cable_cond'] = meta_data['cable_cond']
                if el: ret.append(el)

        return ret


    def save_tb(self, data, full_filename):
        """Save the array 'data' to the XML file"""
        for diagram in self.qet_project.findall('diagram'):  # all diagrams
            for element in diagram.iter('element'):  # all elements in diagram
                dt = [x for x in data if x['uuid'] == element.attrib['uuid']]
                if dt:
                    found = False
                    value = '%p{}%t{}%c{}%n{}'.format(
                            dt[0]['terminal_pos'], \
                            dt[0]['terminal_type'], \
                            dt[0]['cable'], \
                            dt[0]['cable_cond'])
                    for elinfo in element.iter('elementInformation'):
                        if elinfo.attrib['name'] == 'function':
                            elinfo.text = value
                            found = True
                    if not found:  # crete a new child
                        #~ print ('----------------------- {}'.format(element.attrib['uuid']))
                        father = element.find('elementInformations')
                        new = etree.SubElement(father, \
                                'elementInformation',
                                name="function", \
                                show="0")
                        new.text = value

        self._qet_tree.write(full_filename)


class TerminalBlock:
    """This class represents a Terminal Block for a QET project."""

    DEBUG = False

    # Constants to clarify list access
    _IDX_TERM_NAME_ = 2


    def __init__(self, tb_id, qet_project_title, \
             tb_folder_name, config, reservation, implicit):
        """initializer.
        @param string tb_id: id for this terminal block (e.g. X1)
        @param string qet_project_title: label to give name to the
                     element at collection
        @param tb_folder_name: name of the folder to crete at personal
             collection dir (ends with /)
        @param config: dict with config info. This allows diferent config for every
            block terminal
        @param reservation: true/false for create terminals reservation
        @param implicit: true if tb was generated searching implicits connections.
            If true show xref for conductor in every terminal
            If false show xref for the terminal itself """

        self.tb_id = tb_id
        self.qet_project_title = qet_project_title
        self.tb_folder_name =  tb_folder_name
        self.config = config
        self.reservation = reservation
        self.implicit = implicit

        self.element_dir_name = self.config['collection'] + tb_folder_name + os.sep
        self.terminals = [] #terminals belongs this block
        self._create_collection_index_file(self.element_dir_name)


    def addTerminals (self, terminals):
        """Add terminals to the block, but only accept terminals that haves
        the same 'terminal block' as the defined on constructor.
        @param terminals: dict of lists with terminal info. The format is:
                {id: [element_uuid, terminal_block_name,
                      terminal_name, terminal1_id, terminal2_id
                NORTH_cable_num, NORTH_xref, SOUTH_cable_num, SOUTH_xref ] }"""
        for b in terminals.keys():
            if b.split( SEPARATOR )[0] == self.tb_id:
                self.terminals.append(terminals[b])


    def _create_collection_index_file(self, sCollectionPath):
        """Collection index file. Create directory if not exists
        @param sCollectionPath
        @return: none"""

        # creates if not exist
        if (sCollectionPath[-1] != os.sep): sCollectionPath += os.sep
        if not os.path.exists(sCollectionPath): os.makedirs(sCollectionPath)

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


    def _sort_tb(self, listTerminals):
        """Sort a dict of lists. Every sublist have the info of a terminal
        like this:
                [element_uuid, terminal_block_name, terminal_name,
                terminal1_id, terminal2_id, NORTH_cable_num, NORTH_xref,
                SOUTH_cable_num, SOUTH_xref ]

        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

        The last criteria to sort is ther number in NORTH XREF.

        @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

        _IDX_TERMINAL_NUM = 2
        _IDX_NORTH_XREF = 6
        _IDX_NEW_SORT_TYPE = -3
        _IDX_NEW_SORT_NUM = -2
        _IDX_NEW_SORT_NOT_NUM = -1


        # determine type of terminal name and prepare temp data to sort
        for t in listTerminals:
            sTBName = t[ _IDX_TERMINAL_NUM ]
            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: untyped
        subData1 = [x for x in listTerminals if x[ _IDX_NEW_SORT_TYPE ] == TYPE_1]
        subData1.sort(key=lambda x:
                (
                    x[ _IDX_NEW_SORT_NOT_NUM ],
                    self._getNum( x[ _IDX_NORTH_XREF ] )
                 ))

        # Sort Type 2: letters + numbers
        subData2 = [x for x in listTerminals if x[ _IDX_NEW_SORT_TYPE ] == TYPE_2]
        subData2.sort(key=lambda x:
                (
                    int(x[ _IDX_NEW_SORT_NUM ]),
                    x[ _IDX_NEW_SORT_NOT_NUM ],
                    self._getNum( x[ _IDX_NORTH_XREF ] )
                 ))

        # Sort Type 3: Negative + numbers
        subData3 = [x for x in listTerminals if x[ _IDX_NEW_SORT_TYPE ] == TYPE_3]
        subData3.sort( key=lambda x:
                (   x[ _IDX_NEW_SORT_NUM ],
                    self._getNum( x[ _IDX_NORTH_XREF ] )
                ))

        # Sort Type 4: Positive + numbers
        subData4 = [x for x in listTerminals if x[ _IDX_NEW_SORT_TYPE ] == TYPE_4]
        subData4.sort( key=lambda x:
                (
                    x[ _IDX_NEW_SORT_NUM ],
                    self._getNum( x[ _IDX_NORTH_XREF ] )
                ))

        # sort type 5: only numbers
        subData5 = [x for x in listTerminals if x[ _IDX_NEW_SORT_TYPE ] == TYPE_5]
        subData5.sort(key=lambda x:
                (
                    int(x[ _IDX_NEW_SORT_NUM ]),
                    self._getNum( x[ _IDX_NORTH_XREF ] )
                ))

        # return
        return [x[:-3] for x in subData1 + subData2 + subData3 + subData4 + subData5]  # delete temp fields


    def _getNum(self, x):
        """ Returns the page part as integer of a XREF. Is there isn't digits,
        return 9999. Usefull for sort reasons.
        e.g. '12-B8' """

        foo = x.split('-')[0]
        if foo.isdigit():
            return int(foo)
        else:
            return 9999


    def _get_empty_terminal(self, terminal_name=''):
        """Returns a list corresponding a new empty terminalself.

        The new terminal haves the same teminal_block_name.

        @param terminal_name: name/number for the terminal block
        @return: valid list format for a terminal.
        """
        # [element_uuid, terminal_block_name, terminal_name/number, terminal_xref,
        # NORTH cable id side 1, N.cable id side 2, N.cable num, N. cable destination xref,
        # SOUTH cable id side 1, S.cable id side 2, S.cable num, S. cable destination xref]
        return ['', self.tb_id, str(terminal_name), '', \
                '', '', self.config['reservation_label'], '', \
                '', '', self.config['reservation_label'], '']


    def _generate_reservation_numbers(self):
        """Creates new terminals ID for gaps if exist.

        Only check gaps for numerical ID's (not for +1, -0,...).
        The list of terminal_numbers comes from a unique block terminal,
        i.e. X1, X12,...

        NOTE: Modify self.terminals
        @return list with gaps filled and sorted.
        """

        only_numbers = [int(x[self._IDX_TERM_NAME_])
            for x in self.terminals if x[self._IDX_TERM_NAME_].isdigit()]
        only_numbers.sort()
        if self.DEBUG:
            print ("<drawTerminalBlock> Reservation - {}". \
                format(only_numbers))

        print ('{}'.format(self.terminals[0]))

        if only_numbers:  # if the are digits in terminals numeration
            for i in range(1, int(only_numbers[-1])):
                if i not in only_numbers:
                    self.terminals.append( self._get_empty_terminal(i))


    def drawTerminalBlock(self):
        """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 format:
            [element_uuid, terminal_block_name, terminal_name, terminal1_id,
             terminal2_id, NORTH_cable_num, NORTH_xref, SOUTH_cable_num,
             SOUTH_xref ]

        coord (0,0) al corner upper-left

        @(param) self.terminals
        @return: none"""


        # Create reservation terminals. Terminals number are string,
        # and can be a number or not ('-0', '+2',...)
        if self.reservation:
            self._generate_reservation_numbers()  # changes self.terminals

        # sort the terminals of the block
        dataSorted = self._sort_tb(self.terminals)

        # 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 = '*'
        last_terminal_name = '*'

        # Vertical coord depend if TB was generated searching implitic or not connections
        if self.implicit:
            y_coord_num = 40
            y_coord_bridge = 0
            y_coord_north_xref = 85
            y_coord_south_xref = 3
        else:
            y_coord_num = 20
            y_coord_bridge = 20
            y_coord_xref = 65

        i2 = 0
        for i1 in range(0, len(dataSorted), self.config['max_page']):
            # process every Term- Block of max length per folio

            i2 = (i2 + self.config['max_page'])
            if i2 > len(dataSorted): i2 = len(dataSorted)
            subData = dataSorted[i1:i2]

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

            totalWith = self.config['head_width'] + self.config['union_width'] + (iQuantity * self.config['terminal_width'])
            totalWithRoundedUp = totalWith + 1  # +1 to force round the next tenth
            while (totalWithRoundedUp % 10): totalWithRoundedUp += 1  # next tenth

            totalHeight = self.config['head_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 = self.qet_project_title + ' ' + self.tb_id + '(' + str(i1 + 1) + '-' + str(
                i2) + ')'  # element id in collection
            file_name = self.element_dir_name + self.qet_project_title + \
                        '_' + self.tb_id + '_' + 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>',
                '  <informations></informations>',
                '  <description>']
            x = 0
            # head
            xml.append('    <rect x="%i" y="0" width="%i" height="%i"' \
                       % (x, self.config['head_width'], self.config['head_height']) + rect_style + '/>')
            x += self.config['head_width']

            # head union
            xml.append('    <rect x="%i" y="%i" width="%i" height="%i"' \
                       % (x, (self.config['head_height'] - self.config['union_height']) / 2, self.config['union_width'], self.config['union_height']) + \
                       rect_style + '/>')
            x += self.config['union_width']

            # TB ID
            xml.append('    <input text="%s" tagg="label" rotation="270" size="10" x="%i" y="%i"' \
                       % (self.tb_id, self.config['head_width'] / 2, self.config['head_height'] / 2 + (len(self.tb_id) / 2) * 10) + '/>')
            # All terminals
            for t in subData:
                halfx = x + self.config['terminal_width'] / 2
                # terminal rectangle
                xml.append('    <rect x="%i" y="0" width="%i" height="%i"' \
                           % (x, self.config['terminal_width'], self.config['head_height']) + rect_style + '/>')
                # terminal name
                xml.append('    <text text="%s" rotation="270" size="9" x="%i" y="%i"/>' % (
                t[ IDX_TER_NAME ], halfx + 4, self.config['head_height'] - y_coord_num))

                if self.implicit:  # configured for searching implicit
                    # terminal NORTH xref
                    xml.append('    <text text="%s" rotation="270" size="6" x="%i" y="%i"/>' % (
                    t[ IDX_NORTH_XREF ], halfx + 4, self.config['head_height'] - y_coord_north_xref))

                    #terminal SOUTH xref
                    xml.append('    <text text="%s" rotation="270" size="6" x="%i" y="%i"/>' % (
                    t[ IDX_SOUTH_XREF ], halfx + 4, self.config['head_height'] - y_coord_south_xref))

                else:
                    # terminal xref
                    xml.append('    <text text="%s" rotation="270" size="7" x="%i" y="%i"/>' % (
                    t[ IDX_TER_XREF ], halfx + 4, self.config['head_height'] - y_coord_xref))

                #north cable num
                xml.append(
                    '    <input text="%s" tagg="none" rotation="270" size="6" x="%i" y="%i"/>' % (
                    t[ IDX_NORTH_CABLE_NUM ], halfx - 4, -0))

                #south cable num
                xml.append('    <input text="%s" tagg="none" rotation="270" size="6" x="%i" y="%i"/>' % (
                    t[ IDX_SOUTH_CABLE_NUM ], halfx - 4, self.config['head_height'] + 40))
                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"' \
                    % (self.config['head_height'], self.config['head_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, self.config['head_height'] + 20))
                center = ( self.config['head_height'] / 2 ) - 16
                xml.append('    <circle x="%i" y="%i" diameter="4"' % (halfx - 2, center + y_coord_bridge) + line_style + '/>')
                # bridge

                if self._brigeNeeded( last_terminal_name, last_cable_number1, last_cable_number2,
                                      t[ IDX_TER_NAME ],
                                      t[ IDX_NORTH_CABLE_NUM ],
                                      t[ IDX_SOUTH_CABLE_NUM ] ):
                    xml.append(
                        '    <line length2="1.5" y1="%i" y2="%i" x1="%i" end1="none" x2="%i" end2="none" length1="1.5"' \
                        % (center + 2 + y_coord_bridge, center + 2 + y_coord_bridge, halfx - self.config['terminal_width'], halfx) + line_style + '/>')
                last_cable_number1 = t[ IDX_NORTH_CABLE_NUM ]
                last_cable_number2 = t[ IDX_SOUTH_CABLE_NUM ]
                last_terminal_name = t[ IDX_TER_NAME ]

                x += self.config['terminal_width']

            xml.append('  </description>')
            xml.append('</definition>')

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



def createGUI(root):
    """Create the main window
    @param root: main window"""
    # define main window

            
def right_on_field(wdg_clicked, wdg_autofill):
    """establecemos el texto del campo auto-fill"""
    val = wdg_autofill['text']
   
    wdg_clicked.delete(0, "end")
    wdg_clicked.insert(0, val)
    if val.isdigit():
        wdg_autofill['text'] = str( int(wdg_autofill['text']) + 1)


def return_on_field(wdg_clicked, wdg_autofill):
    """actualizamos auto-fill tras edicion"""
    val = wdg_clicked.get()
    if val.isdigit():
        wdg_autofill['text'] = str( int(val) + 1)
    else:
        wdg_autofill['text'] = str(val)


def right_on_pos(event):
    right_on_field ( event.widget, frmFoot.winfo_children()[0] )


def right_on_type(event):
    right_on_field ( event.widget, frmFoot.winfo_children()[5] )


def right_on_cable(event):
    right_on_field ( event.widget, frmFoot.winfo_children()[6] )


def right_on_cond(event):
    right_on_field ( event.widget, frmFoot.winfo_children()[7] )

            
def return_on_pos(event):
    return_on_field( event.widget, frmFoot.winfo_children()[0])


def return_on_type(event):
    return_on_field( event.widget, frmFoot.winfo_children()[5])


def return_on_cable(event):
    return_on_field( event.widget, frmFoot.winfo_children()[6])


def return_on_cond(event):
    return_on_field( event.widget, frmFoot.winfo_children()[7])


def click_on_save(event):
    global wdg_data
    
    ## transform control variables
    full_filename = qet_project.qet_project_file + '.qet'
    qet_project.save_tb(convert_from_control_var(wdg_data), full_filename)
    messagebox.showinfo("QET", "Document saved as  {}".format(full_filename))


def click_on_draw(event):
    """Slot; button Create terminal block
    Launch Terminal Block creation..."""
    
    messagebox.showinfo("QET", \
            "This will generate the Terminal Blocks\n as new element!!")

    #~ qet_file = self.txt_project_path.text()

    #~ qet_project = QETProject(
            #~ qet_file,
            #~ ref_type,
            #~ self.txtFromPage.text(),
            #~ self.txtToPage.text(),
            #~ self.chkImplicit.isChecked()) #analize QET xml code
    #~ terminal_elements_in_project = qet_project.get_list_of_used_terminals()

    #~ # For every diferent block terminal name: X1, X2,...
    #~ # {TB:term/index : [element_uuid, terminal_block_name, terminal_name, terminal_xref
    #~ # north_id1, north_id2, north_cable_num, north_xref,
    #~ # south_id1, south_id2, south_cable_num, south_xref ] }
    #~ all_tb_names = \
            #~ list( set ( [x.split( SEPARATOR )[0] \
            #~ for x in terminal_elements_in_project.keys() ] \
            #~ ) )  # get unique TerminalBlockID

    #~ full_xref = not (self.chkXref.isEnabled() and self.chkXref.isChecked())
    #~ for block_name in all_tb_names:
        #~ block = TerminalBlock(block_name,
                #~ os.path.basename(qet_file[:-4]),
                #~ 'temp_tb',
                #~ self.config,
                #~ self.chkReservation.isChecked(),
                #~ full_xref )
        #~ block.addTerminals(terminal_elements_in_project)
        #~ block.drawTerminalBlock()  #creates XML element file

    #~ #  show info
    #~ if len(all_tb_names) == 1:
        #~ self.lbl_info.setText('1 terminals strip succesfully created !!')
    #~ else:
        #~ self.lbl_info.setText(
                #~ '%i terminals strips succesfully created !!' \
                #~ %len(all_tb_names))

def print_tb(data):
    if DEBUG:
        print ('{}\t{}\t{}\t{}\t{}\t{}'.format( \
            'Segm.', \
            'Blk', \
            'Ter.Pos', \
            'Cable', \
            'Conduct', \
            'Ter.Nam'))
        for it in data:
            print ('{}\t{}\t{}\t{}\t{}\t{}'.format( \
                    it['segment'], \
                    it['block_name'], \
                    it['terminal_pos'], \
                    it['cable'], \
                    it['cable_cond'], \
                    it['terminal_name']))
    
            
def click_on_reorder(event):
    messagebox.showinfo("QET", \
            "This will redraw the table according the POS. column values.")
  

def sort_terminals(data):
    """Terminals are sorter by: block_name, terminal_pos, cable, 
    cable_cond, terminal_name.
    Later, if a terminal block is longer than STRIP_LONG, will be split
    using 'strip' field.
    @param data: lista de diccionarios con los datos de los terminales.
    """
    ret = []
    
    # convert values to int
    data = convert_numerical(data)
    
    # spliting if needed
    terms = list(OrderedDict.fromkeys([x['block_name'] for x in data])) # no dupl.
    for t in terms:
        tb = [x for x in data if x['block_name'] == t]
        tb_sorted = multikeysort(tb, \
                ['terminal_pos', 'cable', 'cable_cond', 'terminal_name'])
        segment_label = 0
        for i in range(0, len(tb), STRIP_LONG):
            segment_label += 1  # segment label
            segment = tb_sorted[i:i+STRIP_LONG]
            
            # Labeling strip
            for item in segment:
                item['segment'] = '{}_{}'.format(t, segment_label)
            
            ret += segment
    return ret


def to_int(data):
    try: 
        int(data)
        return int(data)
    except ValueError:
        return data


def convert_numerical(data):
    """Convert string values to integers. This allows 'more intelligent'
    sorting"""
    for ter in data:
        ter['terminal_name'] = to_int( ter['terminal_name'] )
        ter['terminal_pos'] = to_int( ter['terminal_pos'] )
        ter['cable'] = to_int( ter['cable'] )
        ter['cable_cond'] = to_int( ter['cable_cond'] )
    return data

def cmp(a, b):
    return (a > b) - (a < b)
    
    
def multikeysort(items, columns):
    """ Sort list of dict using diferent keys. From
    https://stackoverflow.com/questions/1143671/
    python-sorting-list-of-dictionaries-by-multiple-keys
    @param items: dict to sort
    @param colums: list with keys names of dict
    """
    comparers = [
        ((i(col[1:].strip()), -1) if col.startswith('-') else (i(col.strip()), 1))
        for col in columns
    ]
    def comparer(left, right):
        comparer_iter = (
            cmp(fn(left), fn(right)) * mult
            for fn, mult in comparers
        )
        return next((result for result in comparer_iter if result), 0)
    return sorted(items, key=cmp_to_key(comparer))


def initUI(root, data):
    """Creates the UI
    param root: root frame
    param data: list of a dictionary for every terminal
      {uuid, block_name, terminal_name, terminal_pos, 
            terminal_xref, terminal_type, conductor_name, cable, cable_cond}
    """
    global frmFoot
    global wdg_data

    GRID_CONFIG = [
    {'TITLE':'POS.', 'WIDTH': 8, 'AUTOFILL': '1', 'dicc_index':'terminal_pos', \
        'right':right_on_pos,'return':return_on_pos}, \
    {'TITLE':'TERM.BLOCK', 'WIDTH': 13, 'AUTOFILL': '', 'dicc_index':'block_name', \
        'right':'','return':''}, \
    {'TITLE':'ID', 'WIDTH': 8, 'AUTOFILL': '', 'dicc_index':'terminal_name', \
        'right':'','return':''}, \
    {'TITLE':'XREF', 'WIDTH': 8, 'AUTOFILL': '', 'dicc_index':'terminal_xref', \
        'right':'','return':''}, \
    {'TITLE':'CONDUCTOR', 'WIDTH': 12, 'AUTOFILL': '', 'dicc_index':'conductor_name', \
        'right':'','return':''}, \
    {'TITLE':'TYPE', 'WIDTH': 10, 'AUTOFILL': 'GROUND', 'dicc_index':'terminal_type', \
        'right':right_on_type,'return':return_on_type}, \
    {'TITLE':'CABLE', 'WIDTH': 10, 'AUTOFILL': '-W1', 'dicc_index':'cable', \
        'right':right_on_cable,'return':return_on_cable}, \
    {'TITLE':'CABLE COND.', 'WIDTH': 13, 'AUTOFILL': '1', 'dicc_index':'cable_cond', \
        'right':right_on_cond,'return':return_on_cond}, \
    ]
    #~ {'TITLE':'UUID', 'WIDTH': 13, 'AUTOFILL': '', 'dicc_index':'uuid', \
        #~ 'right':'','return':''} \
 
    ## config root window
    root.title("QET - Terminal block generator - v{}".format(VERSION))
    root.resizable(True, True)  # width, height
    #~ img = tk.PhotoImage(file='qet.png')
    #~ root.tk.call('wm', 'iconphoto', root._w, img)
    #~ root.iconbitmap("qet.ico")  # only b/n ico for linux
    
    ## frame header
    frmHeader = tk.Frame(root, borderwidth=1, relief='solid')
    tk.Label(frmHeader, text=txt[0], justify='left').pack(side='left')
    
    b1 = tk.Button(frmHeader,text='SAVE')
    b1.pack(side='right')
    b1.bind("<Button-1>", click_on_save)
    
    b2 = tk.Button(frmHeader,text='DRAW')
    b2.pack(side='right')
    b2.bind("<Button-1>", click_on_draw)
    
    b3 = tk.Button(frmHeader,text='REFRESH')
    b3.pack(side='right')
    b3.bind("<Button-1>", click_on_reorder)
    
    ## TABS
    tabs = ttk.Notebook(root)  # populates 'segment' field

    segments = list(OrderedDict.fromkeys([x['segment'] for x in data])) # no dupl.
    wdg_data_index = 0
    for seg in segments:
        ## frame data
        frmData = tk.Frame(tabs)
        
        ## caption
        for c in range(len(GRID_CONFIG)):
            tk.Label(frmData, text=GRID_CONFIG[c]['TITLE'], \
                     width = GRID_CONFIG[c]['WIDTH'], \
                     font=('Helvetica', 10, 'bold underline')).grid(row=0, column=c)
        offset = 1
        terminals = [x for x in data if x['segment'] == seg]  # choose a segment
        tab_row = 0
        for ter in terminals:
            for c in range(len(GRID_CONFIG)):
                lbl = tk.Entry(frmData, borderwidth=0, \
                        width = GRID_CONFIG[c]['WIDTH']-1, \
                        justify = 'center', fg='blue', bg='gray85', \
                        textvariable = wdg_data[wdg_data_index][GRID_CONFIG[c]['dicc_index']])
                lbl.grid(row=tab_row+offset,column=c)
                
                if GRID_CONFIG[c]['AUTOFILL'] == '':
                    lbl.config(state='disabled', disabledforeground = 'black', \
                            disabledbackground='gray85')
                    
                if GRID_CONFIG[c]['right'] != '':
                    lbl.bind("<Button-3>",GRID_CONFIG[c]['right'])
                    lbl.bind("<Return>",GRID_CONFIG[c]['return'])
            tab_row += 1
            wdg_data_index += 1
        tabs.add(frmData, text=seg)

    ## Config tab
    frmConfig = tk.Frame(tabs)
    tk.Label(frmConfig, text='This is the config tab').pack()
    tabs.add(frmConfig, text='Config')

    ## frame foot
    frmFoot = tk.Frame(root, borderwidth=1, relief='solid')
    for c in range(len(GRID_CONFIG)):
        t = GRID_CONFIG[c]['AUTOFILL']
        tk.Label(frmFoot, text=t, padx=2, \
                 width = GRID_CONFIG[c]['WIDTH'], \
                 font=('Helvetica', 10, 'bold'), fg='gray').grid(row=0, column=c)
    
    ## Packing
    frmHeader.pack(anchor='n', fill='x', expand='True', pady=6, padx=6)
    tabs.pack(fill='x', expand='True',pady=6, padx=6)
    frmFoot.pack(anchor='s', fill='x', expand='True', pady=6, padx=6)


def get_QET_path(wdg_root):
    """Returns the QET project file from command line or file dialog"""
    
    ## Get QET path from command line or show dialog to choose one.
    if len(sys.argv) == 1:  # first data is the prg name
        ftypes = [('QET', '*.qet'), ('All files', '*')]
        dlg = filedialog.Open(wdg_root, filetypes = ftypes)
        fl = dlg.show()

        if fl != '':
            return fl
    else:
        return sys.argv[1]


def convert_to_control_var(data_array):
    """Converts an array of lists to a control variables array to easy
    access to/from UI widgets"""
    ret = []

    for r in range(len(data_array)):
        a_row = {}
        for key in data_array[r].keys():
            a_row[key] = tk.StringVar(value=data_array[r][key])
        ret.append(a_row)

    return ret
    
    
def convert_from_control_var(control_data_array):
    """Converts an array of lists of control variables to its values.
    """
    ret = []
    for r in range(len(control_data_array)):
        a_row = {}
        for key in control_data_array[r].keys():
            a_row[key] = control_data_array[r][key].get()
        ret.append(a_row)
    return ret
    
    
def print_debug_xml(element):
    if DEBUG:
        print (etree.tostring(element).decode())
    
     
def main():
    global qet_project
    global wdg_data  
    
    ## UI Root
    wdg_root = tk.Tk(  )
    
    ## Get QET path from command line or show dialog to choose one.
    qet_file = get_QET_path(wdg_root)
    
    ## QET Project
    qet_project = QETProject(qet_file)  # allow working with a QET XML file.
    qet_termials_used = qet_project.get_list_of_used_terminals()
    
    ## Converting to number, sorting  and converting to a widgets dicc
    qet_termials_used = sort_terminals(qet_termials_used)
    wdg_data = convert_to_control_var(qet_termials_used)
    
    ## UI
    initUI(wdg_root, qet_termials_used)
    
    ## main loop
    wdg_root.mainloop(  )


if __name__ == '__main__':
    main()
