#!/usr/bin/python3

import os
import sys
import xml.etree.ElementTree as ET
import argparse
import re
import logging

# Changelog:
# 0.0: Initial release
# 0.1:  - Changed signal lookup to avoid catching child of child
#       - added sorting object types so that statics decl and init are always same
#       - added replacing GtkCheckButton by GtkToggleButton in output code
#       - changed lookup_widget to gtk_builder_get_object so that any buildable is correctly init

skippedobj = ['class', 'packing', 'style', 'requires', 'attributes', 'attribute',
              'columns', 'column', 'placeholder', 'pattern', 'patterns', 'mime-type', 'mime-types']
levelup = ['child', 'items', 'object']
noprint = ['child', 'items', 'action-widgets', 'interface']
space = '  '  # visual formatting of levels
replacements = {'GtkCheckButton' : 'GtkToggleButton'}

level = 0

# used to replace some classes with another (sup or sub) which has most of the functions required

def replace_type(s):
    if s in replacements.keys():
        return replacements[s]
    return s

# used to cast widgets to subclass during statics declarations based on their class name
# example transform_string('GtkBox') returns GTK_BOX

def transform_string(s):
    transformed_string = re.sub(r'(?<!^)([A-Z])', r'_\1', s)
    return transformed_string.upper()

# used to write to both terminal and output file


class Logger(object):
    """Class to log both to terminal and to .yml file

    :param object: object
    :type object: object
    """

    def __init__(self, name='uiparser'):
        """Class constructor with default name

        :param caller: the suffix that will be added before the date-time the log was crated, defaults to 'gess_'
        :type caller: str, optional
        """
        self.terminal = sys.stdout
        self.dbgfile = name + '.yml'
        self.log = open(self.dbgfile, "w", encoding='utf-8')

    def write(self, message):
        self.terminal.write(message)
        ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]')
        self.log.write(ansi_escape.sub('', message))

    def flush(self):
        pass

# formatter based on node tag
# Uses ANSI escape
# In Windows, should run in Windows Terminal for proper display


def format_print(n):
    global level
    if n.tag == 'object':
        if 'id' in n.attrib:
            print('{:s}[{:s}] \033[32m{:s}\033[0m'.format(
                level * space, n.attrib['class'], n.attrib['id']))
        else:
            print('{:s}[{:s}]'.format(level * space, n.attrib['class']))
    elif n.tag == 'signal':
        print('\033[34m{:s}{:s}\033[0m >> \033[1m\033[3m{:s}\033[0m'.format(
            level * space, n.attrib['name'], n.attrib['handler']))
    elif n.tag == 'item':
        print('{:s}\033[3m\033[35m* {:s}\033[0m'.format(level * space, n.text))
    elif n.tag == 'accelerator':
        print('{:s}\033[31m{:s} =>\033[34m {:s}\033[0m'.format(
            level * space,  n.attrib['key'], n.attrib['signal']))
    elif n.tag == 'property':
        print('{:s}action >>> \033[33m{:s}\033[0m'.format(
            level * space,  n.text))
    elif n.tag == 'action-widget':
        print(
            '{:s}action-widget >>> \033[33m{:s}\033[0m'.format(level * space,  n.text))
    else:
        print(level * space, n.tag, ' : ', n.attrib, ' - ', n.text)

# recursively explore the tree


def get_node(n):
    global level, signals, statics
    if n.tag and not (n.tag in skippedobj):
        # otherwise we miss the actions, this is the only 'property' child which we need
        if not (n.tag == 'property' and not n.attrib['name'] == 'action-name'):
            if not n.tag in noprint:
                format_print(n)
            if n.tag in levelup:
                level += 1
            for m in n:
                yield from get_node(m)
            if n.tag in levelup:
                level -= 1

# main function


def Run(filename=None, rootspec=None, dolog=False):

    if filename is None:
        import tkinter as tk
        from tkinter import filedialog
        parent = tk.Tk()
        parent.attributes('-topmost', True)
        parent.withdraw()
        filename = filedialog.askopenfilename(title='Choose UI file', filetypes=[
                                              ('UI file', '*.ui')], defaultextension="ui")
        parent.destroy()

    if len(filename) == 0:
        sys.exit()

    filename = os.path.abspath(filename)
    logname = os.path.splitext(filename)[0]
    try:
        tree = ET.parse(filename)
    except Exception as error:
        logging.error(error)
        sys.exit(1)

    root = tree.getroot()
    if rootspec is not None:
        try:
            root = root.find(rootspec)
        except:
            print('Could not find the root at {:s}'.format(rootspec))
            sys.exit(1)

    saveout = sys.stdout
    if dolog:
        sys.stdout = Logger(logname)
    failed = False
    try:
        list(get_node(root))
    except Exception as error:
        print('\n')
        failed = True
        logging.error(error)
        pass
    finally:
        sys.stdout = saveout

    if failed:
        sys.exit(1)

    print('\n\n// Statics declarations\n')
    ids = root.findall('.//object[@id]')
    itemdict = {}
    for i in ids:
        itemdict[i.attrib['id']] = replace_type(i.attrib['class'])

    # sort the set alphabetically so that the static init function is always the same if nothing has changed
    types = sorted(list(set([v for _, v in itemdict.items()])))

    for t in types:
        l = [' *{:s} = NULL'.format(k) for k, v in itemdict.items() if v == t]
        print('{:s}{:s};'.format(t, ','.join(l)))

    print('\n\n// Statics init\n')
    f = os.path.splitext(os.path.split(filename)[1])[0]
    print('void {:s}_init_statics() {:s}'.format(f, '{'))
    print('\tif ({:s} == NULL) {:s}'.format(next(iter(itemdict)), '{'))
    for t in types:
        print('\t\t// {:s}'.format(t))
        l = [k for k, v in itemdict.items() if v == t]
        for k in l:
            print('\t\t{:s} = {:s}(gtk_builder_get_object(gui.builder, \"{:s}\"));'.format(
                k, transform_string(t), k))
    print('\t{:s}\n{:s}'.format('}', '}'))

    print('\n\n// Listing all callbacks\n')
    ids = root.findall('.//signal/..')
    for i in ids:
        sig = i.findall('./signal')
        for s in sig:
            print('Object {:s} - {:s}::{:s} ==> {:s}'.format(
                i.attrib['id'], i.attrib['class'], s.attrib['name'], s.attrib['handler']))


if __name__ == "__main__":

    parser = argparse.ArgumentParser(description='This script parses a Siril UI file, writes its tree in a more compact way and produces statics declaration (for items which have an id) and callbacks list',
                                     formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument(
        '-f', '--filename', action='store', default=None,
        help="Path to the file being parsed. If blank, will open a dialog to select the file",
        type=str
    )
    parser.add_argument(
        '-r', '--root', action='store', default=None,
        help="Modifier to move the root to a specific child, e.g \".//*[@class='GtkScrolledWindow'][@id='conversion_tab']\" "
        " places the root at the first child whose class is GtkScrolledWindow and whose id is conversion_tab."
        " See https://docs.python.org/3/library/xml.etree.elementtree.html#supported-xpath-syntax for syntax",
        type=str, dest="rootspec"
    )
    parser.add_argument(
        '-l', '--log', action='store_true',
        help="Flag to enable saving the tree to a .yml file alongside the .ui file defined in filename",
        dest="dolog"
    )

    args = parser.parse_args()
    Run(**vars(args))
