#!/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
from inspect import getmembers
from pprint import pprint


## 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.potentialId=0
        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( 'terminals' )
        self._terminalConnections = self._getListOfElementsByType( 'terminal' )
        self._wireElements = self._getListOfElementsByType( 'wire' )
        self._prevElements = self._getListOfElementsByType( 'previous_report' )
        self._nextElements = self._getListOfElementsByType( 'next_report' )
        self._masterElements = self._getListOfElementsByType( 'master' )
        self._slaveElements = self._getListOfElementsByType( 'slave' )
        self._simpleElements = self._getListOfElementsByType( 'simple' )


    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
        lib = {}

        # Terminal type
        prefix_term = "term_"
        prefix_term_len = len(prefix_term)

        # Wire type
        prefix_wire = "wire_"
        prefix_wire_len = len(prefix_wire)

        # Cable type
        prefix_cable = "cable_"
        prefix_cable_len = len(prefix_cable)

        # add elements manualy
        if element_type == 'wire':
            ret.append('fil_de_cable.elmt')
            ret.append('filerie.elmt')

        for element in self.qet_project.find('collection').iter('element'):

            # Terminal type by prefix
            if element.attrib['name'][:prefix_term_len] == prefix_term:
                if element_type == 'terminals':
                    ret.append(element.attrib['name'])
            else:

                # Wire type by prefix
                if element.attrib['name'][:prefix_wire_len] == prefix_wire or element.attrib['name'][:prefix_cable_len] == prefix_cable:
                    if element_type == 'wire':
                        ret.append(element.attrib['name'])

                else:
                    # Other types defined by link_type
                    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"""

        ## old version of QET XML diagram doesn't have dynamic text.
        label = comment = location = ''
        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'] == 'comment':
                    comment = t.text
                if t.attrib['name'] == 'location':
                    location = t.text
        
        if label == None:  # attrib returns None if empty.
            label = ''
        if comment == None:
            comment = ''
        if location == None:
            location = ''

        #print("label:"+label)
        #print("comment:"+comment)
        return [label, comment, location]


    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 _isFirstCableWire (self, element_type):
        """ An element is valid if type is 'terminal' and label is like 'X1:1'
        @param element:  element  (XML etree object)
        @return: True / False"""
        
        if "first" in element_type:
            return True
        
        return False

    def _isCableEnd (self, element_type):
        """ An element is valid if type is 'terminal' and label is like 'X1:1'
        @param element:  element  (XML etree object)
        @return: True / False"""
        
        if "ref_next" in element_type:
            return True
        else:
            if "termination" in element_type:
                return True
        
        return False

    def _isCableRefPrev (self, element_type):
        """ An element is valid if type is 'terminal' and label is like 'X1:1'
        @param element:  element  (XML etree object)
        @return: True / False"""
        
        if "ref_previous" in element_type:
            return True
        
        return False

    def _isValidCableWire (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 'type' in element.attrib:  # elements must have a 'type'
            for el in self._wireElements:  # searching type
                if re.search(el + '$', element.attrib['type']):
                    return True
        
        return False


    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('X', self._getElementName(element)[0]):
            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 _getLocation(self, diagram, element):
        
        for d in diagram.find('elementInformations').findall('elementInformation'):
            location = diagram.attrib['locmach']
            if d.attrib['name']=='location':
                location = d.text

        return location
                       

    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_element_by_termId(self, diagram, terminalId):

        for element in diagram.findall('.//element'):  # all elements in diagram
            terminals = element.find('terminals').findall( 'terminal' )
            for t in terminals:
                if t.attrib['id'] == terminalId:  # terminalId to find

                    return element
        return None

    def _seach_connected_cores(self, diagram, element):

        x=element.attrib['x']
        y=element.attrib['y']

        for element in diagram.findall('.//element'):  # all elements in diagram

            if not self._isFirstCableWire(element):
                if self._isValidCableWire(element):

                    terminals = element.find('terminals').findall( 'terminal' )
                    for t in terminals:
                        if t.attrib['orientation'] == 1:   # cable description connection

                            nextTermId = self._get_values_next_wire(diagram, t.attrib['id'])

                            while nextTermId != '':
                                nextElement = self._get_element_by_termId(diagram, nextTermId)
                                self._set_cableVals_to_core(diagram, element, nextElement)
                                nextTermId = self._get_values_next_wire(diagram, nextTermId)

    def get_list_of_wire_elements(self, diagram):

        ret = []

        for element in diagram.findall('.//element'):  # all elements in diagram
            el = {}

            if self._isValidCableWire(element):

                if self._isFirstCableWire(element.attrib['type']):
                    self._seach_connected_cores

                el['elenement_type'] = element.attrib['type']
                el['uuid'] = element.attrib['uuid']
                el['x'] = element.attrib['x']
                el['y'] = element.attrib['y']
                el['label'] = ''
                el['comment'] = ''
                if element.find('elementInformations'):
                    for e in element.find('elementInformations').findall('elementInformation'):
                        if e.attrib['name'] == "label":
                            el['label'] = e.text
                        if e.attrib['name'] == "comment":
                            el['comment'] = e.text

                el['terminals'] = self._get_terminals(element)

                ret.append(el)

        ret = sorted(ret, key=lambda elem: "%s %s" % (elem['x'], elem['y'])) 
        return ret

    def _get_terminals(self, element):
        
        ret = []

        for t in element.find('terminals').findall('terminal'):
            lib= {}
            lib['id'] = t.attrib['id']
            lib['orientation'] = t.attrib['orientation']
            lib['x'] = t.attrib['x']
            lib['y'] = t.attrib['y']

            ret.append(lib)
            
        return ret

    def _get_previous_label(self, uuid):

        ret = ''

        for diagram in self.qet_project.findall('diagram'):  # all diagrams
            for element in diagram.findall('.//element'):  # all elements in diagram
                if element.find('links_uuids'):
                    e = element.find('links_uuids')
                    if e[0].attrib['uuid'] == uuid:
                        terminals = self._get_terminals(element)
                        ret = self._getCableNum(diagram, terminals[0]['id'])
        return ret

    def _get_element_by_uuid(self, uuid):

        for diagram in self.qet_project.findall('diagram'):  # all diagrams
            for element in diagram.findall('.//element'):  # all elements in diagram
                if element.attrib['uuid'] == uuid:
                    return element

        return false


    def get_list_of_cable_names(self):

        ret = []

        for diagram in self.qet_project.findall('diagram'):  # all diagrams
            for element in diagram.findall('.//element'):  # all elements in diagram
                #el = {}
                if self._isValidCableWire(element):
                    if self._isFirstCableWire(element.attrib['type']):
                        if element.find('elementInformations'):
                            for e in element.find('elementInformations').findall('.//elementInformation'):
                                if e.attrib['name'] == "label":
                                    #el['name'] = e.text
                                    #el['uuid'] = element.attrib['uuid']
                                    #ret.append(el)
                                    ret.append(e.text)

        #ret = sorted(ret, key=lambda k: k['name']) 
        ret.sort()
        self.cableNames = ret

    def _set_cable_connection_name(self, diagram, terminalId, label):

        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:
                    cable.attrib['num']=label
                    cable.attrib['displaytext']="0"
        self._qet_tree.write(self.qet_project_file)


    def _make_cable_name(self):
        
        label = 'W' + str(self.cable_count)

        while label in self.cableNames:
            self.cable_count+=1
            label = 'W' + str(self.cable_count)

        self.cableNames.append(label)
        return label
            

    def name_cables(self):

        self.get_list_of_cable_names()

        self.cable_count = 1
        
        naming = []

        for diagram in self.qet_project.findall('diagram'):  # all diagrams

            lib = {}
            cores = self.get_list_of_wire_elements(diagram)

            for cable in cores:
                if self._isFirstCableWire(cable['elenement_type']):
                    
                    # New cable label
                    if cable['label'] == '':

                        # Make new cable lable, if empty
                        lib[cable['y']] = self._make_cable_name()
                        cable['label'] = lib[cable['y']]
                        naming.append(cable)
                    else:

                        # Copy cable lable, if not empty
                        lib[cable['y']] = cable['label']

                    # If there is a cable name connection, take over the cable label to the connection name
                    self._set_cable_connection_name(diagram, cable['terminals'][0]['id'], lib[cable['y']])

                else:
                    if self._isCableRefPrev(cable['elenement_type']):

                        # If the cable is taking its values from another previous cable label
                        lib[cable['y']] = self._get_previous_label(cable['uuid'])

                        # If there is a cable name connection, take over the cable label to the connection name
                        self._set_cable_connection_name(diagram, cable['terminals'][0]['id'], lib[cable['y']])

                    else:
                        if self._isCableEnd(cable['elenement_type']):

                            # End of cable in this Y
                            del lib[cable['y']]
                        else:
                            if cable['y'] in lib:

                                # Take over the cable label into the core element
                                cable['label'] = lib[cable['y']]
                                naming.append(cable)

        self.save_cables(naming)

                    
    def save_cables(self,cables):

        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 cables if x['uuid'] == element.attrib['uuid']]
                if dt:
                    found = False
                    for elinfo in element.iter('elementInformation'):
                        if elinfo.attrib['name'] == 'label':
                            elinfo.text = dt[0]['label']
                            found = True
                        if elinfo.attrib['name'] == 'formula':
                            elinfo.text = ''
                            found = True
                    if not found:  # crete a new child
                        if not element.find('elementInformations'):
                            new = etree.SubElement(element, \
                                'elementInformations')
                        father = element.find('elementInformations')
                        new = etree.SubElement(father, \
                                'elementInformation',
                                name="label", \
                                show="0")
                        new = etree.SubElement(father, \
                                'elementInformation',
                                name="formula", \
                                show="0")
                        new.text = ''
        self._qet_tree.write(self.qet_project_file)


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 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.

    ## Copy cable lables to cores
    qet_project.name_cables()
    



if __name__ == '__main__':
    main()
