#!/opt/dosis2/bin/python3-venv
__author__ = 'tlanclos'

import os
import sys

try:
    from xml.dom.minidom import parse, Document
    from xml.dom import DOMException
    import psycopg2
    import syslog
    import copy
    from getopt import getopt, GetoptError
    import time
    import pwd
except ImportError as err:
    print(err)
    sys.exit(1)

DOSIS_DB_CREDS = dict()

DEFAULT_PATH = '/opt/dosis2/1.8/etc/'

DAFE_ID = None
VERBOSE = False

def status(message, shell=False, status=None):
    syslog.syslog('__XML_PY: %s' % message)
    if shell:
        print(message)
    if status is not None:
        sys.exit(int(status))


class xml_rules:
    RULESHEET = {'axis': '', 'db_function_get': '', 'db_function_update': '', 'db_tag': '',
                 'description': '', 'name': '', 'type': '', 'unit_type': ''}

    AXIS_TYPES = ['AXIS_', 'NULL']
    UNIT_TYPES = ['IN', 'EN', 'BOOL', 'INT_BOOL', 'STRING', 'INT']

    RULE_TYPE = {'import_type': 'equipment.xml_update_setting_type', 'export_type': 'equipment.xml_get_setting_type',
                 'import_setting': 'equipment.xml_update_axis_setting', 'export_setting': 'equipment.xml_get_axis_setting',
                 'import_position': 'equipment.xml_update_position', 'export_position': 'equipment.xml_get_position'}

    @staticmethod
    def get_sql_command(rule, value=None, to_database=True):
        if rule['unit_type'] not in xml_rules.UNIT_TYPES:
            status('<RULES> %s is not a valid unit type!' % rule['unit_type'], shell=True, status=1)
            return None

        if to_database:
            if rule['db_function_update'] == xml_rules.RULE_TYPE['import_type']:
                t = (rule['db_function_update'], DAFE_ID, rule['db_tag'], value)
                return 'SELECT * FROM %s(%i, \'%s\', \'%s\')' % t

            elif rule['db_function_update'] == xml_rules.RULE_TYPE['import_setting']:
                t = (rule['db_function_update'], DAFE_ID, rule['axis'], rule['db_tag'], rule['unit_type'], value)
                return 'SELECT * FROM %s(%i, \'%s\', \'%s\', \'%s\', \'%s\')' % t

            elif rule['db_function_update'] == xml_rules.RULE_TYPE['import_position']:
                t = (rule['db_function_update'], DAFE_ID, rule['axis'], rule['db_tag'], rule['unit_type'], float(value))
                return 'SELECT * FROM %s(%i, \'%s\', \'%s\', \'%s\', %f)' % t


        else:
            if rule['db_function_get'] == xml_rules.RULE_TYPE['export_type']:
                t = (rule['db_function_get'], DAFE_ID, rule['db_tag'])
                return 'SELECT %s(%i, \'%s\')' % t

            elif rule['db_function_get'] == xml_rules.RULE_TYPE['export_setting']:
                t = (rule['db_function_get'], DAFE_ID, rule['axis'], rule['db_tag'], rule['unit_type'])
                return 'SELECT %s(%i, \'%s\', \'%s\', \'%s\')' % t

            elif rule['db_function_get'] == xml_rules.RULE_TYPE['export_position']:
                t = (rule['db_function_get'], DAFE_ID, rule['axis'], rule['db_tag'], rule['unit_type'])
                return 'SELECT %s(%i, \'%s\', \'%s\', \'%s\')' % t

        return None

    def __init__(self, filepath):
        try:
            self.rules = parse(filepath)
        except (DOMException, IOError):
            status('<RULES> failed to read rules file %s!' % filepath, shell=True, status=1)

    def read_all(self):
        status('<RULES> reading all rules...', shell=True)
        ret_rules = []
        for rule in self.rules.getElementsByTagName('property'):
            temp_rule = self.read(rule.attributes['name'].value)
            if temp_rule is not None:
                ret_rules.append(temp_rule)
        if len(ret_rules) == 0:
            status('<RULES> there are no rules in this rules file!', shell=True, status=1)
            return None
        status('<RULES> successfully read all rules!', shell=True)
        return ret_rules

    def get_config_name(self):
        return self.rules.getElementsByTagName('config')[0].attributes['name'].value

    def get_sections_with_properties(self):
        swp = []
        sections = self.rules.getElementsByTagName('section')
        for section in sections:
            properties = section.getElementsByTagName('property')
            temp = {'SECTION': section, 'PROPERTIES': properties}
            swp.append(temp)

        return swp


    def read(self, p_name):
        try:
            properties = self.rules.getElementsByTagName('property')
            prop_element = [x for x in properties if x.attributes['name'].value == p_name][0]
            status('<RULES> successfully found property: %s' % p_name)

            rule = copy.deepcopy(xml_rules.RULESHEET)

            for key in list(rule.keys()):
                rule[key] = prop_element.attributes[key].value

            return rule
        except KeyError:
            status('<RULES> bad key: possibly not a valid xml file.', shell=True)
            return None
        except IndexError:
            status('<RULES> bad name: either the section or property specified doesn\'t exist', shell=True)
            return None


class xml_smash:
    FAIL_ON_EMPTY_FILE = False
    FILE_ATTRIBUTES = ['description', 'type', 'value']

    def __init__(self, xml_filepath1, xml_filepath2):
        self.use_xml2 = True
        self.c_name = None
        try:
            self.xml1 = parse(xml_filepath1)
            self.xml2 = parse(xml_filepath2)
            status('<SMASHER> files read successfully.')

            c1_name = self.xml1.getElementsByTagName('config')[0].attributes['name'].value
            c2_name = self.xml2.getElementsByTagName('config')[0].attributes['name'].value

            if c1_name != c2_name:
                status('<SMASHER> bad combo: the two files do not contain the same config name', shell=True, status=1)

            self.c_name = c1_name

        except KeyError:
            status('<SMASHER> bad key: possibly not a valid xml file or both header configs don\'t have the same name tag.', shell=True)
        except IndexError:
            if xml_smash.FAIL_ON_EMPTY_FILE:
                status('<SMASHER> bad file: file does not contain any \'config\' directive.', shell=True, status=1)
            else:
                status('<SMASHER> bad file: file does not contain any \'config\' directive.', shell=True)
                self.use_xml2 = False
        except DOMException:
            status('<SMASHER> bad file: DOM was not able to read it.', shell=True, status=1)

    def smash(self):
        status('<SMASHER> smashing some files together...', shell=True)
        if self.use_xml2:
            xml1_names = [p.attributes['name'].value for p in self.xml1.getElementsByTagName('property')]
            xml2_properties = self.xml2.getElementsByTagName('property')
            for current in xml2_properties:
                current_name = current.attributes['name'].value

                if current_name not in xml1_names:
                    parent_names = [s.attributes['name'].value for s in self.xml1.getElementsByTagName('section')]
                    current_parent_name = current.parentNode.attributes['name'].value
                    parent = None

                    if current_parent_name not in parent_names:
                        section = self.xml1.createElement('section')
                        section.setAttribute('name', current_parent_name)
                        self.xml1.getElementsByTagName('config')[0].appendChild(section)
                        parent = section
                    else:
                        parent = [s for s in self.xml1.getElementsByTagName('section')
                                  if s.attributes['name'].value == current_parent_name][0]

                    try:
                        parent = [p for p in parent.getElementsByTagName('property')
                                  if p.attributes['name'].value == current_name][0]
                    except IndexError:
                        prop = self.xml1.createElement('property')
                        prop.setAttribute('name', current_name)
                        parent.appendChild(prop)
                        parent = prop

                    for tag in ['description', 'type', 'value']:
                        try:
                            tag_value = current.getElementsByTagName(tag)[0].firstChild.nodeValue
                        except IndexError:
                            tag_value = None

                        if tag_value is not None:
                            try:
                                prop.getElementsByTagName(tag)[0].firstChild.replaceWholeText(tag_value)
                            except IndexError:
                                temp_node = self.xml1.createElement(tag)
                                temp_text_node = self.xml1.createTextNode(tag_value)
                                temp_node.appendChild(temp_text_node)
                                prop.appendChild(temp_node)
                else:
                    prop = [p for p in self.xml1.getElementsByTagName('property')
                            if p.attributes['name'].value == current_name][0]
                    for tag in ['description', 'type', 'value']:
                        try:
                            tag_value = current.getElementsByTagName(tag)[0].firstChild.nodeValue
                        except AttributeError:
                            tag_value = None

                        if tag_value is not None:
                            try:
                                pr = prop.getElementsByTagName(tag)[0]
                                pr.firstChild.replaceWholeText(tag_value)
                            except IndexError:
                                temp_node = self.xml1.createElement(tag)
                                temp_text_node = self.xml1.createTextNode(tag_value)
                                temp_node.appendChild(temp_text_node)
                                prop.appendChild(temp_node)
                            except AttributeError:
                                prop.removeChild(pr)
                                temp_node = self.xml1.createElement(tag)
                                temp_text_node = self.xml1.createTextNode(tag_value)
                                temp_node.appendChild(temp_text_node)
                                prop.appendChild(temp_node)

            status('<SMASHER> files successfully smashed!', shell=True)
            return self.xml1
        else:
            status('<SMASHER> xml2 did not contain a \'config\' directive; smashing not needed', shell=True)
            return self.xml1


class xml_import:
    def __init__(self, xml_filepath):
        _name = pwd.getpwuid(os.getuid()).pw_name
        if _name != 'root':
            status('<IMPORT> You must run as \'sudo\' to import to the database...', shell=True, status=1)

        try:
            os.listdir(xml_filepath)
            _isdir = True
        except Exception as e:
            _isdir = False

        if _isdir:
            self.path = xml_filepath
        else:
            global DAFE_ID
            while DAFE_ID is None:
                try:
                    DAFE_ID = eval(input("Enter Dafe ID: "))
                except (KeyboardInterrupt, SystemExit):
                    status('canceled by user', shell=True, status=2)
                except:
                    DAFE_ID = None
            rules_filename, basename, default_path, custom_path = self.__find_all__(xml_filepath)
            status('<IMPORT> opening rules file...', shell=True)
            self.rules = xml_rules(rules_filename)
            self.smashed_xml = xml_smash('%s/%s' % (default_path, basename), '%s/%s' % (custom_path, basename)).smash()
            self.connect_to_database()

    def __find_all__(self, file):
        basename = os.path.basename(file)
        path = file.replace(basename, '')
        rules_filename = basename.replace('.xml', '_rules.xml')

        if '/custom/' in file:
            custom_path = os.path.abspath(path)
            default_path = os.path.abspath('%s/..' % custom_path)
        else:
            custom_path = os.path.abspath('%s/custom/' % path)
            default_path = os.path.abspath(path)

        return rules_filename, basename, default_path, custom_path

    def connect_to_database(self):
        status('<IMPORT> connecting to database...', shell=True)
        try:
            self.connection = psycopg2.connect(host=DOSIS_DB_CREDS['HOST'], database=DOSIS_DB_CREDS['DATABASE'],
                                               user=DOSIS_DB_CREDS['USER'], port=DOSIS_DB_CREDS['PORT'],
                                               password=DOSIS_DB_CREDS['PASS'])
            self.cursor = self.connection.cursor()
            status('<IMPORT> successfully connected to database!', shell=True)
        except psycopg2.Error:
            status('<IMPORT> failed to connected to database!', shell=True, status=1)

    def import_all(self):
        start = time.time()  # timing
        error_count = 0

        status('<IMPORT> importing xml to database...', shell=True)
        temp_rules = self.rules.read_all()
        for rule in temp_rules:
            if not self.import_single(rule):
                error_count += 1
        self.connection.commit()
        if error_count > 0:
            status('<IMPORT> finished with %i errors!' % error_count, shell=True)
        else:
            status('<IMPORT> successfully imported xml to database!', shell=True)

        print('execution time: ', time.time() - start)  # timing

    def import_path(self, type, serial):
        global DAFE_ID
        files = set(['blisterfeeder.xml', 'labelsealer.xml', 'motorControl.xml'])
        os_files = set([os.path.basename(f) for f in os.listdir(self.path)])
        all_files = files.intersection(os_files)

        self.connect_to_database()

        try:
            if type.upper() == 'L60':
                self.cursor.execute('SELECT equipment.add_dafe_l60(%s);' % serial)
           # elif type.upper() == 'C60':
           #     self.cursor.execute('SELECT equipment.add_dafe_c60(%s);' % serial)
            elif type.upper() == 'U60':
                self.cursor.execute('SELECT equipment.add_dafe_u60(%s);' % serial)
            else:
                status('Dafe type must be L60 or U60', shell=True)
                os.abort()
            DAFE_ID = self.cursor.fetchone()[0]
        except Exception as e:
            status('Something bad happened, database may be down', shell=True)
            status(e, shell=True)
            return

        for f in all_files:
            xml_filepath = '%s/%s' % (self.path, f)
            status('<IMPORT> Working on %s' % xml_filepath, shell=True)
            rules_filename, basename, default_path, custom_path = self.__find_all__(xml_filepath)

            status('<IMPORT> opening rules file %s...' % rules_filename, shell=True)
            self.rules = xml_rules(rules_filename)
            self.smashed_xml = xml_smash('%s/%s' % (default_path, basename), '%s/%s' % (custom_path, basename)).smash()
            self.import_all()

        try:
            self.cursor.execute('SELECT equipment.dafe_init_all();')
            self.cursor.execute('''
                UPDATE equipment.dafe_axis_settings
                SET value = '16,0,0'
                WHERE axis_id IN (SELECT id FROM  equipment.dafe_axis WHERE tag = 'AXIS_F')
                AND axis_setting_type_id IN (SELECT id FROM equipment.dafe_axis_setting_types WHERE tag = 'IO_3');
            ''')

            self.connection.commit()
        except psycopg2.Error as e:
            status('Something bad happened running equipment.dafe_init_all(), database may be down', shell=True)
            return

    def import_single(self, rule):
        try:
            prop = [p for p in self.smashed_xml.getElementsByTagName('property')
                    if p.attributes['name'].value == rule['name']][0]
            value = prop.getElementsByTagName('value')[0].firstChild.nodeValue
        except IndexError:
            status('<IMPORT> value not found for>>> \033[31m%s\033[0m <<<did not try to import!' % rule['name'], shell=True)
            return

        command = xml_rules.get_sql_command(rule, value=value, to_database=True)

        if VERBOSE:
            print(command)

        try:
            self.cursor.execute(command)
            ret = self.cursor.fetchone()
            stat = ret[0]
            if bool(stat) is False:
                status('<IMPORT> failed to import %s>>> \033[31m%s\033[0m!' % (rule['name'], ret[2]), shell=True)
                return False
            return True
        except (psycopg2.ProgrammingError, psycopg2.InternalError, psycopg2.DataError) as err:
            status('<IMPORT> %s' % str(err).strip(), shell=True)


class xml_export:
    def __init__(self, xml_filepath):
        global DAFE_ID
        while DAFE_ID is None:
            try:
                DAFE_ID = eval(input("Enter Dafe ID: "))
            except (KeyboardInterrupt, SystemExit):
                status('canceled by user', shell=True, status=2)
            except:
                DAFE_ID = None

        basename = os.path.basename(xml_filepath)
        rules_filename = basename.replace('.xml', '_rules.xml')
        self.xml_filename = basename

        status('<EXPORT> opening rules file...', shell=True)
        self.rules = xml_rules(rules_filename)

        self.output_document = self.create_basic_structure()

        self.connect_to_database()

    def connect_to_database(self):
        status('<EXPORT> connecting to database...', shell=True)
        try:
            self.connection = psycopg2.connect(host=DOSIS_DB_CREDS['HOST'], database=DOSIS_DB_CREDS['DATABASE'],
                                               user=DOSIS_DB_CREDS['USER'], port=DOSIS_DB_CREDS['PORT'])
            self.cursor = self.connection.cursor()
            status('<EXPORT> successfully connected to database!', shell=True)
        except psycopg2.Error:
            status('<EXPORT> failed to connected to database!', shell=True, status=1)

    def create_basic_structure(self):
        status('<EXPORT> creating temporary document...', shell=True)
        output_document = Document()
        temp_node = output_document.createElement('config')
        temp_node.setAttribute('name', self.rules.get_config_name())
        output_document.appendChild(temp_node)
        parent = temp_node

        sections_with_properties = self.rules.get_sections_with_properties()
        for swp in sections_with_properties:
            s_name = swp['SECTION'].attributes['name'].value
            temp_node = output_document.createElement('section')
            temp_node.setAttribute('name', s_name)
            parent2 = temp_node

            for p in swp['PROPERTIES']:
                p_name = p.attributes['name'].value
                temp_node2 = output_document.createElement('property')
                temp_node2.setAttribute('name', p_name)
                parent2.appendChild(temp_node2)

            parent.appendChild(parent2)

        status('<EXPORT> successfully created temporary document!', shell=True)
        return output_document

    def export_all(self):
        start = time.time() # timing

        status('<EXPORT> exporting xml from database...', shell=True)
        temp_rules = self.rules.read_all()
        for rule in temp_rules:
            self.export_single(rule)
        self.connection.commit()
        status('<EXPORT> successfully exported xml from database!', shell=True)

        file = open(self.xml_filename, 'w')
        file.write(self.output_document.toprettyxml(indent='  ', newl='\n', encoding='UTF-8'))
        file.close()

        print('execution time: ', time.time() - start) # timing

    def export_single(self, rule):
        command = xml_rules.get_sql_command(rule, to_database=False)

        if VERBOSE:
            print(command)

        try:
            self.cursor.execute(command)
            value = '%s' % self.cursor.fetchone()
        except (psycopg2.ProgrammingError, psycopg2.InternalError, psycopg2.DataError) as err:
            status('<EXPORT> %s' % str(err).strip(), shell=True)

        if value in (None, 'None'):
            status('<EXPORT> no value in database for %s!' % rule['name'], shell=True)
        else:
            p_elements = self.output_document.getElementsByTagName('property')

            try:
                p_temp = [x for x in p_elements if x.attributes['name'].value == rule['name']][0]

                for tag, v in zip(['description', 'type', 'value'], [rule['description'], rule['type'], value]):
                    temp_node = self.output_document.createElement(tag)
                    temp_text_node = self.output_document.createTextNode(v)
                    temp_node.appendChild(temp_text_node)
                    p_temp.appendChild(temp_node)

            except IndexError:
                status('<EXPORT> cannot find %s in output document!' % rule['name'], shell=True)
                pass


class xml_manage:
    def __init__(self):
        self.getargs()

    def usage(self):
        print()
        print('To import anything to the database, you must run as super user (sudo)')
        print()
        print('sudo python3 xml_manager.py -s SERIAL -I /PATH/TO/ETC [-v]')
        print()
        print('-h      or --help            - display this help information')
        print('-v      or --verbose         - show what the script is doing verbosely (i.e. functions ran on database)')
        #print '-d x    or --dafe=x          - specify a dafe id (i.e. 2)'
        #print '-i xxxx or --import=xxxx     - import a file to the database (from /opt/dosis2/1.8/etc/.)'
        print('-I xxxx or --Import=xxxx     - import blisterfeeder, labelsealer, and motorControl from path')
        print('-s xxxx or --serial=xxxx     - serial number to use for this dafe')
        print('-t L60|U60 or --type=L60|U60 - which type of machine to load')
        #print 'Feature (export) has been removed as requested...'
        print()
        print('Examples:')
        print('   sudo python3 xml_manager.py -s 200 -I /opt/dosis2/1.8/etc -t L60')
        print()
        print('For Debugging:')
        print('   sudo python3 xml_manager.py -s 200 -I /opt/dosis2/1.8/etc -t U60 -v')
        print()

    def getargs(self):
        try:
            opts, args = getopt(sys.argv[1:], 'hvd:i:I:s:e:t:', ['help', 'verbose', 'dafe', 'import', 'Import', 'serial', 'export', 'type'])

            global DAFE_ID
            global VERBOSE
            DAFE_ID = None

            dafe_type = None
            for o, a in opts:
                if o in ('-h', '--help'):
                    self.usage()
                    status('', status=0)
                elif o in ('-v', '--verbose'):
                    VERBOSE = True
                elif o in ('-d', '--dafe'):
                    if a.isdigit():
                        DAFE_ID = eval(a)
                    else:
                        self.usage()
                        status('<MANAGE> dafe id must be a number...', shell=True, status=1)
                elif o in ('-t', '--type'):
                    dafe_type = a.strip()

            import_path = None
            serial = None

            for o, a in opts:
                if o in ('-i', '--import'):
                    if a not in ('', None):
                        xml_import('%s' % a).import_all()

                elif o in ('-I', '--Import'):
                    if a not in ('', None):
                        import_path = a

                elif o in ('-s', '--serial'):
                    if a not in ('', None):
                        try:
                            serial = int(a.strip())
                            if serial <= 0:
                                status('<MANAGER> serial number must be greater than 0')
                                sys.exit(1)
                        except Exception as e:
                            status('<MANAGER> serial number must be a number')
                            sys.exit(1)

                elif o in ('-e', '--export'):
                    if a not in ('', None):
                        self.status('<MANAGER> the export feature has been removed as requested...', shell=True)
                        self.status('<MANAGER> %s will be ignored and not exported from the database...', shell=True)
#                        xml_export(a).export_all()

            if import_path is not None and serial is not None:
                if dafe_type is None:
                    status('You must specify a -t or --type option', shell=True)
                else:
                    xml_import(import_path).import_path(dafe_type, serial)

        except GetoptError as err:
            self.usage()
            status('<MANAGE> %s...' % err, shell=True, status=3)

if __name__ == "__main__":
    file1 = '/opt/dosis2/etc/dosisConfig.xml'
    file2 = '/opt/dosis2/etc/custom/dosisConfig.xml'
    if os.path.isfile(file1) and os.path.isfile(file2):
        credentialFinder = xml_smash(file1, file2).smash()
        section = [x for x in credentialFinder.getElementsByTagName('section') if x.attributes['name'].value == 'general'][0]
        for key, property in zip(['HOST', 'DATABASE', 'USER', 'PASS', 'PORT'], ['DBServer', 'DBName', 'DBUsername', 'DBPassword', 'DBPort']):
            tmp = [x for x in section.getElementsByTagName('property') if x.attributes['name'].value == property][0].getElementsByTagName('value')[0].firstChild
            if tmp is None:
                DOSIS_DB_CREDS[key] = ''
            else:
                DOSIS_DB_CREDS[key] = str(tmp.nodeValue)

        xml_manage()
    else:
        status('%s or %s not found...' % (file1, file2))
