#!/usr/bin/python3

"""
QET Terminal Block maker v0.5
Creates new Terminal blocks elements
corresponding to terminals used in a QET project

Requires QT5
"""

# Imports
from frmMain_ui import Ui_frmMain
import json
import os
import platform
from PyQt5 import QtWidgets, QtCore # QtGui  # Optional: QtCore
import re
import sys
import uuid
import xml.etree.ElementTree as etree

# GLOBAL CONFIG CONSTANTS
VERSION = 'v0.5'


class MainWindow(QtWidgets.QMainWindow, Ui_frmMain):

    CONFIG_FILE = 'QET_TB_maker.ini'

    def __init__(self):
        """ constructor """
        super(MainWindow, self).__init__()

        # Read config from file
        self.config = {} #initizalize config
        self.data_path = self._read_config() #changes self.config

        # Customize ui
        self.setupUi(self)  # setup the user interface from Designer
        MainWindow.setWindowTitle(self, MainWindow.windowTitle(self) + ' - ' + VERSION)
        MainWindow.move(self, 100, 100)
        self.txtDataPath.setText(str(self.config['collection']))
        self.txtSize_h.setText(str(self.config['head_height']))
        self.txtSize_w.setText(str(self.config['head_width']))
        self.txtSize_h1.setText(str(self.config['union_height']))
        self.txtSize_w1.setText(str(self.config['union_width']))
        self.txtSize_w2.setText(str(self.config['terminal_width']))
        self.txtRes.setText(str(self.config['reservation_label']))
        self.txtMaxPage.setText(str(self.config['max_page']))
        self.sldMaxPage.setValue(int(self.config['max_page']))
        self.lbl_info.setText('')

        # Fill recent projects list
        qet_settings = QtCore.QSettings('QElectroTech','QElectroTech')
        for i in range(10):
            foo = qet_settings.value('projects-recentfiles/file' + str(i))
            if foo: self.lst_recent.addItem(foo)

    def choose_project(self):
        """Slot: button select qet project
        Show dialog to select QET project"""
        file = QtWidgets.QFileDialog.getOpenFileName(self, caption="Choose user collection path",
                                                         filter="*.qet")
        if file:
            self.txt_project_path.setText(file[0])
            self.lbl_info.setText('')  # clear info label

    def recent_picked(self):
        """Slot: for recent projects list"""
        self.txt_project_path.setText(self.lst_recent.selectedItems()[0].text())
        self.lbl_info.setText('')  # clear info label

    def save_config(self):
        """Slot: buton save pressed"""
        # Updates attributes and save
        self.config['collection'] = self.txtDataPath.text()
        self.config['head_height'] = int(self.txtSize_h.text())
        self.config['head_width'] = int(self.txtSize_w.text())
        self.config['union_height'] = int(self.txtSize_h1.text())
        self.config['union_width'] = int(self.txtSize_w1.text())
        self.config['terminal_width'] = int(self.txtSize_w2.text())
        self.config['reservation_label'] = self.txtRes.text()
        self.config['max_page'] = int(self.txtMaxPage.text())
        self.sldMaxPage.setValue(int(self.config['max_page']))

        self._save_config()

    def sld_changed(self):
        """Slot: slider value changed"""
        self.txtMaxPage.setText(str(self.sldMaxPage.value()))

    def choosePath(self):
        """Slot,button select data path
        Show dialog to select user"""
        dir = QtWidgets.QFileDialog.getExistingDirectory(self,
                caption="Choose user collection path", directory=self.data_path)
        if dir:
            if not dir.endswith(os.sep): dir += os.sep
            self.txtProjecDataPath.setText(dir)
            self.save_config()

    def create_tb(self):
        """Slot; button Create terminal block
        Launch Terminal Block creation..."""
        qet_file = self.txt_project_path.text()

        if not os.path.exists(qet_file): #not exist
            self.lbl_info.setText('File not exists !!')
        else:
            self.lbl_info.setText('Creating...')

            if self.rad_id.isChecked():  # determine type of reference to use
                ref_type = QETProject.XREF_ID
            else:
                ref_type = QETProject.XREF_NUMBER
            print (ref_type)
            qet_project = QETProject(qet_file, ref_type) #analize QET xml code
            terminal_elements_in_project = qet_project.get_list_of_used_elements()

            # For every diferent block terminal name: X1, X2,...
            all_tb_names = list(set([x[2] for x in terminal_elements_in_project]))  # get unique TerminalBlockID
            for block_name in all_tb_names:
                block = TerminalBlock(block_name, os.path.basename(qet_file[:-4]),
                                      'temp_tb', self.config)
                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 block succesfully created !!')
            else:
                self.lbl_info.setText('%i terminals blocks succesfully created !!' %len(all_tb_names))

    def salir(self):
            exit()

    def _read_config(self):
        """Read the config file and assign to config attribute
        If not exists, it will create and saves the default params
        @return none"""
        if not os.path.isfile(MainWindow.CONFIG_FILE):  # create if not exist
            self._save_config(defaults=True)
        with open(MainWindow.CONFIG_FILE, 'r') as f:
            self.config = json.load(f)
    
    def _save_config(self, defaults = False):
        """Save to config file the current user collection path.
        @param defaults: if true, save the defaults parameters.
        @return none"""
        default_config = {'collection': self._get_platforma_data_path(),
                          'head_height': 120,
                          'head_width': 44,
                          'union_height': 70,
                          'union_width': 6,
                          'terminal_width': 20,
                          'reservation_label': 'RES.',
                          'max_page': 50}
        with open(MainWindow.CONFIG_FILE, 'w') as f:
            if defaults:
                foo = default_config
            else:
                foo = self.config
            json.dump(foo, f)

    def _get_platforma_data_path(self):
        """Return QET Path collection path depending the OS.
        @return string: path with ending /"""
        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 = ''

        return path


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

    # class attributes
    XREF_NUMBER = 'N'
    XREF_ID = 'I'
    QET_COL_ROW_SIZE = 25  # pixels offset for elements coordinates
    DEBUG = False

    def __init__(self, project_file, folio_reference_type):
        """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:
           'I' for folio ID (1,2,3,...) or
           'N' for folio name ('+PLC',...)"""

        qet_tree = etree.parse(project_file)

        self.qet_project_file = project_file
        self.folio_reference_type = folio_reference_type
        self.qet_project = qet_tree.getroot()

    def _get_list_of_borne_elements(self):
        """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 = self.qet_project.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

    def _getXRef(self, diagram, element, iPageOffset):
        """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
        @return: string like "p-rc" (page - rowLetter colNumber)"""

        # get requiered data
        if self.folio_reference_type == QETProject.XREF_ID:
            page = int(diagram.attrib['order']) + iPageOffset
        elif self.folio_reference_type == QETProject.XREF_NUMBER:
            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 self.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 - 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 str(page) + "-" + row_letter + column

    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 get_list_of_used_elements(self):
        """Return a list of all terminal elements used in the qet project.
        @return [] list of lists where:
            [element_uuid, terminal_xref, terminal block, terminal number,
             cable_number1, cable_number2] """

        elementsToFind = self._get_list_of_borne_elements()

        ret = []  # return list
        pageOffset = int(self.qet_project.attrib['folioSheetQuantity'])  # check for a index of folios page

        for diagram in self.qet_project.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: use regex
                                element_data = []  # list for current element
                                if self.DEBUG:
                                    print("<get_list_of_used_elements> searching: %s  \t offset: \
                                     %i \t %s \t %s" % (el, offset, element.attrib['type'], \
                                                        element.attrib['type'][offset:]))
                                if self.DEBUG:
                                    print("<get_list_of_used_elements> Working with \
                                    element %s" % element.attrib['uuid'])

                                element_data.append(element.attrib['uuid'])  # uuid
                                element_data.append(self._getXRef(diagram, element, pageOffset))  # 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(self._getCableNum(diagram, terminals[0].attrib['id']))  # cable 1
                                element_data.append(self._getCableNum(diagram, terminals[1].attrib['id']))  # cable 2

                                ret.append(element_data)
        return ret


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

    def __init__(self, tb_id, qet_project_title, tb_folder_name, config):
        """initializer.
        @param string tb_id: id for this terminal block
        @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"""
        self.tb_id = tb_id
        self.qet_project_title = qet_project_title
        self.tb_folder_name =  tb_folder_name
        self.config = config

        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. The format is:
        @param list: list of lists with terminal info:
            [element_uuid_in_QET_project, terminal_xref, terminal block id,
             terminal number, cable_number1, cable_number2]"""
        for b in terminals:
            if b[2] == self.tb_id: self.terminals.append(b)

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

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

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

    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
        @(param) self.terminals
        @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 self.terminals 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: self.terminals.append(['', self.config['reservation_label'], self.tb_id, str(i), '', ''])

        # sort de 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 = ''

        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>',
                '  <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
                xml.append('    <rect x="%i" y="0" width="%i" height="%i"' \
                           % (x, self.config['terminal_width'], self.config['head_height']) + rect_style + '/>')
                xml.append('    <text text="%s" rotation="270" size="9" x="%i" y="%i"/>' % (
                t[3], halfx + 4, self.config['head_height'] - 35))  # terminal name
                xml.append('    <text text="%s" rotation="270" size="6" x="%i" y="%i"/>' % (
                t[1], halfx + 4, self.config['head_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, self.config['head_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"' \
                    % (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))
                xml.append('    <circle x="%i" y="%i" diameter="4"' % (halfx - 2, self.config['head_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] != self.config['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"' \
                        % (self.config['head_height'] - 20 + 2, self.config['head_height'] - 20 + 2, halfx - self.config['terminal_width'], halfx) + line_style + '/>')
                last_cable_number1 = t[4]
                last_cable_number2 = t[5]

                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])

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    mainWindow = MainWindow()
    mainWindow.show()
    sys.exit(app.exec_())

