#!/usr/bin/env python
#
# Copyright (C) 2006-2008 Async Open Source
#                         Henrique Romano <henrique@async.com.br>
#                         Johan Dahlin <jdahlin@async.com.br>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public License
# as published by the Free Software Foundation; either version 2
# 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 Lesser General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# TODO:
#  Toolbars

"""Usage: gtk-builder-convert [OPTION] [INPUT] [OUTPUT]
Converts Glade files into XML files which can be loaded with GtkBuilder.
The [INPUT] file is

  -w, --skip-windows     Convert everything but GtkWindow subclasses.
  -r, --root             Convert only widget named root and its children
  -h, --help             display this help and exit

When OUTPUT is -, write to standard output.

Examples:
  gtk-builder-convert preference.glade preferences.ui

Report bugs to https://gitlab.gnome.org/GNOME/gtk/issues/new
"""

from __future__ import print_function
import getopt
import os
import sys

from xml.dom import minidom, Node

DIALOGS = ['GtkDialog',
           'GtkFileChooserDialog',
           'GtkMessageDialog']
WINDOWS = ['GtkWindow'] + DIALOGS

import subprocess

def get_child_nodes(node):
    assert node.tagName == 'object'
    nodes = []
    for child in node.childNodes:
        if child.nodeType != Node.ELEMENT_NODE:
            continue
        if child.tagName != 'child':
            continue
        nodes.append(child)
    return nodes

def get_properties(node):
    assert node.tagName == 'object'
    properties = {}
    for child in node.childNodes:
        if child.nodeType != Node.ELEMENT_NODE:
            continue
        if child.tagName != 'property':
            continue
        value = child.childNodes[0].data
        properties[child.getAttribute('name')] = value
    return properties

def get_property(node, property_name):
    assert node.tagName == 'object'
    properties = get_properties(node)
    return properties.get(property_name)

def get_property_node(node, property_name):
    assert node.tagName == 'object'
    properties = {}
    for child in node.childNodes:
        if child.nodeType != Node.ELEMENT_NODE:
            continue
        if child.tagName != 'property':
            continue
        if child.getAttribute('name') == property_name:
            return child

def get_signal_nodes(node):
    assert node.tagName == 'object'
    signals = []
    for child in node.childNodes:
        if child.nodeType != Node.ELEMENT_NODE:
            continue
        if child.tagName == 'signal':
            signals.append(child)
    return signals

def get_property_nodes(node):
    assert node.tagName == 'object'
    properties = []
    for child in node.childNodes:
        if child.nodeType != Node.ELEMENT_NODE:
            continue
        # FIXME: handle comments
        if child.tagName == 'property':
            properties.append(child)
    return properties

def get_accelerator_nodes(node):
    assert node.tagName == 'object'
    accelerators = []
    for child in node.childNodes:
        if child.nodeType != Node.ELEMENT_NODE:
            continue
        if child.tagName == 'accelerator':
            accelerators.append(child)
    return accelerators

def get_object_node(child_node):
    assert child_node.tagName == 'child', child_node
    nodes = []
    for node in child_node.childNodes:
        if node.nodeType != Node.ELEMENT_NODE:
            continue
        if node.tagName == 'object':
            nodes.append(node)
    assert len(nodes) == 1, nodes
    return nodes[0]

def copy_properties(node, props, prop_dict):
    assert node.tagName == 'object'
    for prop_name in props:
        child = get_property_node(node, prop_name)
        if child is not None:
            prop_dict[prop_name] = child

    return node

class GtkBuilderConverter(object):

    def __init__(self, skip_windows, target_version, root):
        self.skip_windows = skip_windows
        self.target_version = target_version
        self.root = root
        self.root_objects = []
        self.objects = {}

    #
    # Public API
    #

    def parse_file(self, file):
        self._dom = minidom.parse(file)
        self._parse()

    def parse_buffer(self, buffer):
        self._dom = minidom.parseString(buffer)
        self._parse()

    def to_xml(self):
        xml = self._dom.toprettyxml("", "")
        return xml.encode('utf-8')

    #
    # Private
    #

    def _get_object(self, name):
        return self.objects.get(name)

    def _get_objects_by_attr(self, attribute, value):
        return [w for w in self._dom.getElementsByTagName("object")
                      if w.getAttribute(attribute) == value]

    def _create_object(self, obj_class, obj_id, template=None, properties=None):
        """
        Creates a new <object> tag.
        Optionally a name template can be provided which will be used
        to avoid naming collisions.
        The properties dictionary can either contain string values or Node
        values. If a node is provided the name of the node will be overridden
        by the dictionary key.

        @param obj_class: class of the object (class tag)
        @param obj_id: identifier of the object (id tag)
        @param template: name template to use, for example 'button'
        @param properties: dictionary of properties
        @type properties: string or Node.
        @returns: Newly created node of the object
        """
        if template is not None:
            count = 1
            while True:
                obj_id = template + str(count)
                widget = self._get_object(obj_id)
                if widget is None:
                    break

                count += 1

        obj = self._dom.createElement('object')
        obj.setAttribute('class', obj_class)
        obj.setAttribute('id', obj_id)
        if properties:
            for name, value in properties.items():
                if isinstance(value, Node):
                    # Reuse the node, so translatable and context still will be
                    # set when converting nodes. See also #509153
                    prop = value
                else:
                    prop = self._dom.createElement('property')
                    prop.appendChild(self._dom.createTextNode(value))

                prop.setAttribute('name', str(name))
                obj.appendChild(prop)
        self.objects[obj_id] = obj
        return obj

    def _create_root_object(self, obj_class, template, properties=None):
        obj = self._create_object(obj_class, None, template, properties)
        self.root_objects.append(obj)
        return obj

    def _parse(self):
        glade_iface = self._dom.getElementsByTagName("glade-interface")
        assert glade_iface, ("Badly formed XML, there is "
                             "no <glade-interface> tag.")
        # Rename glade-interface to interface
        glade_iface[0].tagName = 'interface'
        self._interface = glade_iface[0]

        # Remove glade-interface doc type
        for node in self._dom.childNodes:
            if node.nodeType == Node.DOCUMENT_TYPE_NODE:
                if node.name == 'glade-interface':
                    self._dom.removeChild(node)

        # Strip unsupported tags
        for tag in ['requires', 'requires-version']:
            for child in self._dom.getElementsByTagName(tag):
                child.parentNode.removeChild(child)

        if self.root:
            self._strip_root(self.root)

        # Rename widget to object
        objects = self._dom.getElementsByTagName("widget")
        for node in objects:
            node.tagName = "object"

        for node in objects:
            self._convert(node.getAttribute("class"), node)
            if self._get_object(node.getAttribute('id')) is not None:
                print("WARNING: duplicate id \"" + node.getAttribute('id') + "\"")
            self.objects[node.getAttribute('id')] = node

        # Convert Gazpachos UI tag
        for node in self._dom.getElementsByTagName("ui"):
            self._convert_ui(node)

        # Convert accessibility tag
        for node in self._dom.getElementsByTagName("accessibility"):
            self._convert_accessibility(node)

        root_objects = self.root_objects[:]
        root_objects.sort(key=lambda a: a.getAttribute('id'), reverse=True)
        for obj in root_objects:
            self._interface.childNodes.insert(0, obj)

    def _convert(self, klass, node):
        if klass == 'GtkNotebook':
            self._packing_prop_to_child_attr(node, "type", "tab")
        elif klass in ['GtkExpander', 'GtkFrame']:
            self._packing_prop_to_child_attr(
                node, "type", "label_item", "label")
        elif klass == "GtkMenuBar":
            self._convert_menu(node)
        elif klass == "GtkMenu":
            # Only convert toplevel popups
            if node.parentNode == self._interface:
                self._convert_menu(node, popup=True)
        elif klass in WINDOWS and self.skip_windows:
            self._remove_window(node)

        if self.target_version == "3.0":
            if klass == "GtkComboBoxEntry":
                node.setAttribute("class","GtkComboBox")
                prop = self._dom.createElement("property")
                prop.setAttribute("name", "has-entry")
                prop.appendChild(self._dom.createTextNode("True"))
                node.appendChild(prop)
            elif klass == "GtkDialog":
                for child in node.childNodes:
                    if child.nodeType != Node.ELEMENT_NODE:
                        continue
                    if child.tagName != 'property':
                        continue
                    if (child.getAttribute("name") not in ("has-separator", "has_separator")):
                        continue;
                    node.removeChild(child)
                    break

        self._default_widget_converter(node)

    def _default_widget_converter(self, node):
        klass = node.getAttribute("class")
        for prop in get_property_nodes(node):
            prop_name = prop.getAttribute("name")
            if prop_name == "sizegroup":
                self._convert_sizegroup(node, prop)
            elif prop_name == "tooltip" and klass != "GtkAction":
                prop.setAttribute("name", "tooltip-text")
            elif prop_name in ["response_id", 'response-id']:
                # It does not make sense to convert responses when
                # we're not going to output dialogs
                if self.skip_windows:
                    continue
                object_id = node.getAttribute('id')
                response = prop.childNodes[0].data
                self._convert_dialog_response(node, object_id, response)
                prop.parentNode.removeChild(prop)
            elif prop_name == "adjustment":
                self._convert_adjustment(prop)
            elif prop_name == "items" and klass in ['GtkComboBox',
                                                    'GtkComboBoxEntry']:
                self._convert_combobox_items(node, prop)
            elif prop_name == "text" and klass == 'GtkTextView':
                self._convert_textview_text(prop)

    def _remove_window(self, node):
        object_node = get_object_node(get_child_nodes(node)[0])
        parent = node.parentNode
        parent.removeChild(node)
        parent.appendChild(object_node)

    def _convert_menu(self, node, popup=False):
        if node.hasAttribute('constructor'):
            return

        uimgr = self._create_root_object('GtkUIManager',
                                         template='uimanager')

        if popup:
            name = 'popup'
        else:
            name = 'menubar'

        menu = self._dom.createElement(name)
        menu.setAttribute('name', node.getAttribute('id'))
        node.setAttribute('constructor', uimgr.getAttribute('id'))

        for child in get_child_nodes(node):
            obj_node = get_object_node(child)
            item = self._convert_menuitem(uimgr, obj_node)
            menu.appendChild(item)
            child.removeChild(obj_node)
            child.parentNode.removeChild(child)

        ui = self._dom.createElement('ui')
        uimgr.appendChild(ui)

        ui.appendChild(menu)

    def _convert_menuitem(self, uimgr, obj_node):
        children = get_child_nodes(obj_node)
        name = 'menuitem'
        if children:
            child_node = children[0]
            menu_node = get_object_node(child_node)
            # Can be GtkImage, which will take care of later.
            if menu_node.getAttribute('class') == 'GtkMenu':
                name = 'menu'

        object_class = obj_node.getAttribute('class')
        if object_class in ['GtkMenuItem',
                            'GtkImageMenuItem',
                            'GtkCheckMenuItem',
                            'GtkRadioMenuItem']:
            menu = self._dom.createElement(name)
        elif object_class == 'GtkSeparatorMenuItem':
            return self._dom.createElement('separator')
        else:
            raise NotImplementedError(object_class)

        menu.setAttribute('action', obj_node.getAttribute('id'))
        self._add_action_from_menuitem(uimgr, obj_node)
        if children:
            for child in get_child_nodes(menu_node):
                obj_node = get_object_node(child)
                item = self._convert_menuitem(uimgr, obj_node)
                menu.appendChild(item)
                child.removeChild(obj_node)
                child.parentNode.removeChild(child)
        return menu

    def _menuitem_to_action(self, node, properties):
        copy_properties(node, ['label', 'tooltip'], properties)

    def _togglemenuitem_to_action(self, node, properties):
        self._menuitem_to_action(node, properties)
        copy_properties(node, ['active'], properties)

    def _radiomenuitem_to_action(self, node, properties):
        self._togglemenuitem_to_action(node, properties)
        copy_properties(node, ['group'], properties)

    def _add_action_from_menuitem(self, uimgr, node):
        properties = {}
        object_class = node.getAttribute('class')
        object_id = node.getAttribute('id')
        if object_class == 'GtkMenuItem':
            name = 'GtkAction'
            self._menuitem_to_action(node, properties)
        elif object_class == 'GtkCheckMenuItem':
            name = 'GtkToggleAction'
            self._togglemenuitem_to_action(node, properties)
        elif object_class == 'GtkRadioMenuItem':
            name = 'GtkRadioAction'
            self._radiomenuitem_to_action(node, properties)
        elif object_class == 'GtkImageMenuItem':
            name = 'GtkAction'
            children = get_child_nodes(node)
            if (children and
                children[0].getAttribute('internal-child') == 'image'):
                image = get_object_node(children[0])
                child = get_property_node(image, 'stock')
                if child is not None:
                    properties['stock_id'] = child
            self._menuitem_to_action(node, properties)
        elif object_class == 'GtkSeparatorMenuItem':
            return
        else:
            raise NotImplementedError(object_class)

        if get_property(node, 'use_stock') == 'True':
            if 'label' in properties:
                properties['stock_id'] = properties['label']
                del properties['label']

        properties['name'] = object_id
        action = self._create_object(name,
                                     object_id,
                                     properties=properties)
        for signal in get_signal_nodes(node):
            signal_name = signal.getAttribute('name')
            if signal_name in ['activate', 'toggled']:
                action.appendChild(signal)
            else:
                print('Unhandled signal %s::%s' % (node.getAttribute('class'),
                                                   signal_name))

        if not uimgr.childNodes:
            child = self._dom.createElement('child')
            uimgr.appendChild(child)

            group = self._create_object('GtkActionGroup', None,
                                        template='actiongroup')
            child.appendChild(group)
        else:
            group = uimgr.childNodes[0].childNodes[0]

        child = self._dom.createElement('child')
        group.appendChild(child)
        child.appendChild(action)

        for accelerator in get_accelerator_nodes(node):
            signal_name = accelerator.getAttribute('signal')
            if signal_name != 'activate':
                print('Unhandled accelerator signal for %s::%s' % (
                    node.getAttribute('class'), signal_name))
                continue
            accelerator.removeAttribute('signal')
            child.appendChild(accelerator)

    def _convert_sizegroup(self, node, prop):
        # This is Gazpacho only
        node.removeChild(prop)
        obj = self._get_object(prop.childNodes[0].data)
        if obj is None:
            widgets = self._get_objects_by_attr("class", "GtkSizeGroup")
            if widgets:
                obj = widgets[-1]
            else:
                obj = self._create_root_object('GtkSizeGroup',
                                               template='sizegroup')

        widgets = obj.getElementsByTagName("widgets")
        if widgets:
            assert len(widgets) == 1
            widgets = widgets[0]
        else:
            widgets = self._dom.createElement("widgets")
            obj.appendChild(widgets)

        member = self._dom.createElement("widget")
        member.setAttribute("name", node.getAttribute("id"))
        widgets.appendChild(member)

    def _convert_dialog_response(self, node, object_name, response):
        # 1) Get parent dialog node
        while True:
            # If we can't find the parent dialog, give up
            if node == self._dom:
                return

            if (node.tagName == 'object' and
                node.getAttribute('class') in DIALOGS):
                dialog = node
                break
            node = node.parentNode
            assert node

        # 2) Get dialogs action-widgets tag, create if not found
        for child in dialog.childNodes:
            if child.nodeType != Node.ELEMENT_NODE:
                continue
            if child.tagName == 'action-widgets':
                actions = child
                break
        else:
            actions = self._dom.createElement("action-widgets")
            dialog.appendChild(actions)

        # 3) Add action-widget tag for the response
        action = self._dom.createElement("action-widget")
        action.setAttribute("response", response)
        action.appendChild(self._dom.createTextNode(object_name))
        actions.appendChild(action)

    def _convert_adjustment(self, prop):
        properties = {}
        if prop.childNodes:
            data = prop.childNodes[0].data
            value, lower, upper, step, page, page_size = data.split(' ')
            properties.update(value=value,
                              lower=lower,
                              upper=upper,
                              step_increment=step,
                              page_increment=page,
                              page_size=page_size)
        else:
            prop.appendChild(self._dom.createTextNode(""))

        adj = self._create_root_object("GtkAdjustment",
                                       template='adjustment',
                                       properties=properties)
        prop.childNodes[0].data = adj.getAttribute('id')

    def _convert_combobox_items(self, node, prop):
        parent = prop.parentNode
        if not prop.childNodes:
            parent.removeChild(prop)
            return

        translatable_attr = prop.attributes.get('translatable')
        translatable = translatable_attr is not None and translatable_attr.value == 'yes'
        has_context_attr = prop.attributes.get('context')
        has_context = has_context_attr is not None and has_context_attr.value == 'yes'
        comments_attr = prop.attributes.get('comments')
        comments = comments_attr is not None and comments_attr.value or None

        value = prop.childNodes[0].data
        model = self._create_root_object("GtkListStore",
                                         template="model")

        columns = self._dom.createElement('columns')
        model.appendChild(columns)

        column = self._dom.createElement('column')
        column.setAttribute('type', 'gchararray')
        columns.appendChild(column)

        data = self._dom.createElement('data')
        model.appendChild(data)

        if value.endswith('\n'):
            value = value[:-1]
        for item in value.split('\n'):
            row = self._dom.createElement('row')
            data.appendChild(row)

            col = self._dom.createElement('col')
            col.setAttribute('id', '0')
            if translatable:
                col.setAttribute('translatable', 'yes')
            if has_context:
                splitting = item.split('|', 1)
                if len(splitting) == 2:
                    context, item = splitting
                    col.setAttribute('context', context)
            if comments is not None:
                col.setAttribute('comments', comments)
            col.appendChild(self._dom.createTextNode(item))
            row.appendChild(col)

        model_prop = self._dom.createElement('property')
        model_prop.setAttribute('name', 'model')
        model_prop.appendChild(
            self._dom.createTextNode(model.getAttribute('id')))
        parent.appendChild(model_prop)

        parent.removeChild(prop)

        child = self._dom.createElement('child')
        node.appendChild(child)
        cell_renderer = self._create_object('GtkCellRendererText', None,
                                            template='renderer')
        child.appendChild(cell_renderer)

        attributes = self._dom.createElement('attributes')
        child.appendChild(attributes)

        attribute = self._dom.createElement('attribute')
        attributes.appendChild(attribute)
        attribute.setAttribute('name', 'text')
        attribute.appendChild(self._dom.createTextNode('0'))

    def _convert_textview_text(self, prop):
        if not prop.childNodes:
            prop.parentNode.removeChild(prop)
            return

        data = prop.childNodes[0].data
        if prop.hasAttribute('translatable'):
            prop.removeAttribute('translatable')
        tbuffer = self._create_root_object("GtkTextBuffer",
                                           template='textbuffer',
                                           properties=dict(text=data))
        prop.childNodes[0].data = tbuffer.getAttribute('id')
        prop.setAttribute('name', 'buffer')

    def _packing_prop_to_child_attr(self, node, prop_name, prop_val,
                                   attr_val=None):
        for child in get_child_nodes(node):
            packing_props = [p for p in child.childNodes if p.nodeName == "packing"]
            if not packing_props:
                continue
            assert len(packing_props) == 1
            packing_prop = packing_props[0]
            properties = packing_prop.getElementsByTagName("property")
            for prop in properties:
                if (prop.getAttribute("name") != prop_name or
                    prop.childNodes[0].data != prop_val):
                    continue
                packing_prop.removeChild(prop)
                child.setAttribute(prop_name, attr_val or prop_val)
            if len(properties) == 1:
                child.removeChild(packing_prop)

    def _convert_ui(self, node):
        cdata = node.childNodes[0]
        data = cdata.toxml().strip()
        if not data.startswith("<![CDATA[") or not data.endswith("]]>"):
            return
        data = data[9:-3]
        child = minidom.parseString(data).childNodes[0]
        nodes = child.childNodes[:]
        for child_node in nodes:
            node.appendChild(child_node)
        node.removeChild(cdata)
        if not node.hasAttribute("id"):
            return

        # Updating references made by widgets
        parent_id = node.parentNode.getAttribute("id")
        for widget in self._get_objects_by_attr("constructor",
                                                node.getAttribute("id")):
            widget.getAttributeNode("constructor").value = parent_id
        node.removeAttribute("id")

    def _convert_accessibility(self, node):
        objectNode = node.parentNode
        parent_id = objectNode.getAttribute("id")

        properties = {}
        for node in node.childNodes:
            if node.nodeName == 'atkproperty':
                node.tagName = 'property'
                properties[node.getAttribute('name')] = node
                node.parentNode.removeChild(node)
            elif node.nodeName == 'atkrelation':
                node.tagName = 'relation'
                relation_type = node.getAttribute('type')
                relation_type = relation_type.replace('_', '-')
                node.setAttribute('type', relation_type)
            elif node.nodeName == 'atkaction':
                node.tagName = 'action'

        if properties:
            child = self._dom.createElement('child')
            child.setAttribute("internal-child", "accessible")

            atkobject = self._create_object(
                "AtkObject", None,
                template='a11y-%s' % (parent_id,),
                properties=properties)
            child.appendChild(atkobject)
            objectNode.appendChild(child)

    def _strip_root(self, root_name):
        for widget in self._dom.getElementsByTagName("widget"):
            if widget.getAttribute('id') == root_name:
                break
        else:
            raise SystemExit("Could not find an object called `%s'" % (
                root_name))

        for child in self._interface.childNodes[:]:
            if child.nodeType != Node.ELEMENT_NODE:
                continue
            child.parentNode.removeChild(child)

        self._interface.appendChild(widget)


def _indent(output):
    if not subprocess:
        return output

    for directory in os.environ['PATH'].split(os.pathsep):
        filename = os.path.join(directory, 'xmllint')
        if os.path.exists(filename):
            break
    else:
        return output

    s = subprocess.Popen([filename, '--format', '-'],
                         stdin=subprocess.PIPE,
                         stdout=subprocess.PIPE)
    s.stdin.write(output)
    s.stdin.close()
    return s.stdout.read()

def usage():
    print(__doc__)

def main(args):
    try:
        opts, args = getopt.getopt(args[1:], "hwr:",
                                   ["help",
                                    "skip-windows",
                                    "target-version=",
                                    "root="])
    except getopt.GetoptError:
        usage()
        return 2

    if len(args) != 2:
        usage()
        return 2

    input_filename, output_filename = args

    skip_windows = False
    split = False
    root = None
    target_version = "2.0"
    for o, a in opts:
        if o in ("-h", "--help"):
            usage()
            sys.exit()
        elif o in ("-r", "--root"):
            root = a
        elif o in ("-w", "--skip-windows"):
            skip_windows = True
        elif o in ("-t", "--target-version"):
            target_version = a

    conv = GtkBuilderConverter(skip_windows=skip_windows,
                               target_version=target_version,
                               root=root)
    conv.parse_file(input_filename)

    xml = _indent(conv.to_xml())
    if output_filename == "-":
        if isinstance(xml, str):
            print(xml)
        else:
            print(xml.decode(sys.stdout.encoding))
    else:
        open(output_filename, 'wb').write(xml)
        print("Wrote", output_filename)

    return 0

if __name__ == "__main__":
    sys.exit(main(sys.argv))
