# -*- coding: utf-8 -*-

# Copyright © 2008-2009 Chris Jones <tortoise@tortuga>
# Copyright © 2010 Francesco Fumanti <francesco.fumanti@gmx.net>
# Copyright © 2011-2016 marmuta <marmvta@gmail.com>
#
# This file is part of Onboard.
#
# Onboard 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.
#
# Onboard 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/>.

from __future__ import division, print_function, unicode_literals

### Logging ###
import logging
_logger = logging.getLogger("LayoutLoaderSVG")
###############

import os
import re
import sys
import shutil
from xml.dom import minidom

from Onboard                 import Exceptions
from Onboard                 import KeyCommon
from Onboard.KeyCommon       import StickyBehavior, ImageSlot, \
                                    KeyPath, KeyGeometry
from Onboard.Layout          import LayoutRoot, LayoutBox, LayoutPanel
from Onboard.utils           import modifiers, Rect, \
                                    toprettyxml, Version, open_utf8, \
                                    permute_mask, LABEL_MODIFIERS, \
                                    unicode_str, XDGDirs

# Layout items that can be created dynamically via the 'class' XML attribute.
from Onboard.WordSuggestions import WordListPanel
from Onboard.KeyGtk          import RectKey, WordlistKey, BarKey, \
                                    WordKey, InputlineKey

### Config Singleton ###
from Onboard.Config import Config
config = Config()
########################


class LayoutLoaderSVG:
    """
    Keyboard layout loaded from an SVG file.
    """
    # onboard <= 0.95
    LAYOUT_FORMAT_LEGACY      = Version(1, 0)

    # onboard 0.96, initial layout-tree
    LAYOUT_FORMAT_LAYOUT_TREE = Version(2, 0)

    # onboard 0.97, scanner overhaul, no more scan columns,
    # new attributes scannable, scan_priority
    LAYOUT_FORMAT_SCANNER     = Version(2, 1)

    # onboard 0.99, prerelease on Nexus 7,
    # new attributes key.action, key.sticky_behavior.
    # allow (i.e. have by default) keycodes for modifiers.
    LAYOUT_FORMAT_2_2         = Version(2, 2)

    # onboard 0.99, key_templates in key_def.xml and include tags.
    LAYOUT_FORMAT_3_0         = Version(3, 0)

    # sub-layouts for popups, various new key attributes,
    # label_margin, theme_id, popup_id
    LAYOUT_FORMAT_3_1         = Version(3, 1)

    # new key attributes show_active
    LAYOUT_FORMAT_3_2         = Version(3, 2)

    # current format
    LAYOUT_FORMAT             = LAYOUT_FORMAT_3_2

    # precalc mask permutations
    _label_modifier_masks = permute_mask(LABEL_MODIFIERS)

    def __init__(self):
        self._vk = None
        self._svg_cache = {}
        self._format = None   # format of the currently loading layout
        self._layout_filename = ""
        self._color_scheme = None
        self._root_layout_dir = ""  # path to svg files
        self._layout_regex = re.compile("([^\(]+) (?: \( ([^\)]*) \) )?",
                                        re.VERBOSE)

    def load(self, vk, layout_filename, color_scheme):
        """ Load layout root file. """
        self._system_layout, self._system_variant = \
                                      self._get_system_keyboard_layout(vk)
        _logger.info("current system keyboard layout(variant): '{}'" \
                     .format(self._get_system_layout_string()))

        layout = self._load(vk, layout_filename, color_scheme,
                            os.path.dirname(layout_filename))
        if layout:
            # purge attributes only used during loading
            for item in layout.iter_items():
                if not item.templates is None:
                    item.templates = None
                if not item.keysym_rules is None:
                    item.keysym_rules = None

            # enable caching
            layout = LayoutRoot(layout)

        return layout


    def _load(self, vk, layout_filename, color_scheme, root_layout_dir, parent_item = None):
        """ Load or include layout file at any depth level. """
        self._vk = vk
        self._layout_filename = layout_filename
        self._color_scheme = color_scheme
        self._root_layout_dir = root_layout_dir
        return self._load_layout(layout_filename, parent_item)

    def _load_layout(self, layout_filename, parent_item = None):
        self._svg_cache = {}
        layout = None

        try:
            f = open_utf8(layout_filename)
        except FileNotFoundError as ex:
            _logger.warning("Failed to open '{}': {}"
                            .format(layout_filename, unicode_str(ex)))
            return None

        # make sure unlink is called
        with minidom.parse(f).documentElement as dom:

            # check layout format, no format version means legacy layout
            format = self.LAYOUT_FORMAT_LEGACY
            if dom.hasAttribute("format"):
               format = Version.from_string(dom.attributes["format"].value)
            self._format = format

            root = LayoutPanel() # root, representing the 'keyboard' tag
            root.set_id("__root__") # id for debug prints

            # Init included root with the parent item's svg filename.
            # -> Allows to skip specifying svg filenames in includes.
            if parent_item:
                root.filename = parent_item.filename

            if format >= self.LAYOUT_FORMAT_LAYOUT_TREE:
                self._parse_dom_node(dom, root)
                layout = root
            else:
                _logger.warning(_format("Loading legacy layout, format '{}'. "
                            "Please consider upgrading to current format '{}'",
                            format, self.LAYOUT_FORMAT))
                items = self._parse_legacy_layout(dom)
                if items:
                    root.set_items(items)
                    layout = root

        f.close()

        self._svg_cache = {} # Free the memory
        return layout

    def _parse_dom_node(self, dom_node, parent_item):
        """ Recursively traverse the dom nodes of the layout tree. """
        loaded_ids = set()

        for child in dom_node.childNodes:
            if child.nodeType == minidom.Node.ELEMENT_NODE:

                # Skip over items with non-matching keyboard layout string.
                # Items with the same id are processed from top to bottom,
                # the first match wins. If no item matches we fall back to
                # the item without layout string.
                # This is used to select between alternative key definitions
                # depending on the current system layout.
                can_load = False
                if not child.hasAttribute("id"):
                    can_load = True
                else:
                    id = child.attributes["id"].value
                    if not id in loaded_ids:
                        if child.hasAttribute("layout"):
                            layout = child.attributes["layout"].value
                            can_load = self._has_matching_layout(layout)

                            # don't look at items with this id again
                            if can_load:
                                loaded_ids.add(id)
                        else:
                            can_load = True


                if can_load:
                    tag = child.tagName

                    # rule and control tags
                    if tag == "include":
                        self._parse_include(child, parent_item)
                    elif tag == "key_template":
                        self._parse_key_template(child, parent_item)
                    elif tag == "keysym_rule":
                        self._parse_keysym_rule(child, parent_item)
                    elif tag == "layout":
                        item = self._parse_sublayout(child, parent_item)
                        parent_item.append_sublayout(item)
                        self._parse_dom_node(child, item)
                    else:
                        # actual items that make up the layout tree
                        if tag == "box":
                            item = self._parse_box(child)
                        elif tag == "panel":
                            item = self._parse_panel(child)
                        elif tag == "key":
                            item = self._parse_key(child, parent_item)
                        else:
                            item = None

                        if item:
                            parent_item.append_item(item)
                            self._parse_dom_node(child, item)

    def _parse_include(self, node, parent):
        if node.hasAttribute("file"):
            filename = node.attributes["file"].value
            filepath = config.find_layout_filename(filename, "layout include")
            _logger.info("Including layout '{}'".format(filename))
            incl_root = LayoutLoaderSVG()._load(self._vk,
                                                filepath,
                                                self._color_scheme,
                                                self._root_layout_dir,
                                                parent)
            if incl_root:
                parent.append_items(incl_root.items)
                parent.update_keysym_rules(incl_root.keysym_rules)
                parent.update_templates(incl_root.templates)
                incl_root.items = None # help garbage collector
                incl_root.keysym_rules = None
                incl_root.templates = None

    def _parse_key_template(self, node, parent):
        """
        Templates are partially define layout items. Later non-template
        items inherit attributes of templates with matching id.
        """
        attributes = dict(list(node.attributes.items()))
        id = attributes.get("id")
        if not id:
            raise Exceptions.LayoutFileError(
                "'id' attribute required for template '{} {}' "
                "in layout '{}'" \
                .format(tag,
                        str(list(attributes.values())),
                        self._layout_filename))

        parent.update_templates({(id, RectKey) : attributes})

    def _parse_keysym_rule(self, node, parent):
        """
        Keysym rules link attributes like "label", "image"
        to certain keysyms.
        """
        attributes = dict(list(node.attributes.items()))
        keysym = attributes.get("keysym")
        if keysym:
            del attributes["keysym"]
            if keysym.startswith("0x"):
                keysym = int(keysym, 16)
            else:
                # translate symbolic keysym name
                keysym = 0

            if keysym:
                parent.update_keysym_rules({keysym : attributes})

    def _init_item(self, attributes, item_class):
        """ Parses attributes common to all LayoutItems """

        # allow to override the item's default class
        if "class" in attributes:
            class_name = attributes["class"]
            try:
                item_class = globals()[class_name]
            except KeyError:
                pass

        # create the item
        item = item_class()

        value = attributes.get("id")
        if not value is None:
            item.id = value

        value = attributes.get("group")
        if not value is None:
            item.group = value

        value = attributes.get("layer")
        if not value is None:
            item.layer_id = value

        value = attributes.get("filename")
        if not value is None:
            item.filename = value

        value = attributes.get("visible")
        if not value is None:
            item.visible = value == "true"

        value = attributes.get("sensitive")
        if not value is None:
            item.sensitive = value == "true"

        value = attributes.get("border")
        if not value is None:
            item.border = float(value)

        value = attributes.get("expand")
        if not value is None:
            item.expand = value == "true"

        value = attributes.get("unlatch_layer")
        if not value is None:
            item.unlatch_layer = value == "true"

        value = attributes.get("scannable")
        if value and value.lower() == 'false':
            item.scannable = False

        value = attributes.get("scan_priority")
        if not value is None:
            item.scan_priority = int(value)

        return item

    def _parse_sublayout(self, node, parent):
        attributes = dict(node.attributes.items())
        item = self._init_item(attributes, LayoutPanel)
        item.sublayout_parent = parent # make templates accessible in the subl.
        return item

    def _parse_box(self, node):
        attributes = dict(node.attributes.items())
        item = self._init_item(attributes, LayoutBox)
        if node.hasAttribute("orientation"):
            item.horizontal = \
                node.attributes["orientation"].value.lower() == "horizontal"
        if node.hasAttribute("spacing"):
            item.spacing = float(node.attributes["spacing"].value)
        if node.hasAttribute("compact"):
            item.compact = node.attributes["compact"].value == "true"
        return item

    def _parse_panel(self, node):
        attributes = dict(node.attributes.items())
        item = self._init_item(attributes, LayoutPanel)
        if node.hasAttribute("compact"):
            item.compact = node.attributes["compact"].value == "true"
        return item

    def _parse_key(self, node, parent):
        result = None

        id = node.attributes["id"].value
        if id == "inputline":
            item_class = InputlineKey
        else:
            item_class = RectKey

        # find template attributes
        attributes = {}
        if node.hasAttribute("id"):
            theme_id, id = RectKey.parse_id(node.attributes["id"].value)
            attributes.update(self.find_template(parent, RectKey, [id]))

        # let current node override any preceding templates
        attributes.update(dict(node.attributes.items()))

        # handle common layout-item attributes
        key = self._init_item(attributes, item_class)
        key.parent = parent  # assign early to have get_filename() work

        # handle key-specific attributes
        self._init_key(key, attributes)

        # get key geometry from the closest svg file
        filename = key.get_filename()
        if not filename:
            if not attributes.get("group") == "wsbutton":
                _logger.warning(_format("Ignoring key '{}'."
                                        " No svg filename defined.",
                                        key.id))
        else:
            svg_nodes = self._get_svg_keys(filename)
            if svg_nodes:
                # try svg_id first, if there is one
                if key.svg_id != key.id:
                    svg_node = svg_nodes.get(key.svg_id)
                else:
                    # then the regular id
                    svg_node = svg_nodes.get(key.id)

                if svg_node:
                    r, geometry = svg_node.extract_key_params()
                    key.set_initial_border_rect(r.copy())
                    key.set_border_rect(r.copy())
                    key.geometry = geometry
                    result = key
                else:
                    _logger.info("Ignoring key '{}'."
                                 " No svg object found for '{}'." \
                                 .format(key.id, key.svg_id))

        return result  # ignore keys not found in an svg file

    def _init_key(self, key, attributes):
        # Re-parse the id to distinguish between the short key_id
        # and the optional longer theme_id.
        full_id = attributes["id"]
        theme_id = attributes.get("theme_id")
        svg_id = attributes.get("svg_id")
        key.set_id(full_id, theme_id, svg_id)

        if "_" in key.get_id():
            _logger.warning("underscore in key id '{}', please use dashes" \
                            .format(key.get_id()))

        value = attributes.get("modifier")
        if value:
            try:
                key.modifier = modifiers[value]
            except KeyError as ex:
                (strerror) = ex
                raise Exceptions.LayoutFileError("Unrecognized modifier %s in" \
                    "definition of %s" (strerror, full_id))

        value = attributes.get("action")
        if value:
            try:
                key.action = KeyCommon.actions[value]
            except KeyError as ex:
                (strerror) = ex
                raise Exceptions.LayoutFileError("Unrecognized key action {} in" \
                    "definition of {}".format(strerror, full_id))

        if "char" in attributes:
            key.code = attributes["char"]
            key.type = KeyCommon.CHAR_TYPE
        elif "keysym" in attributes:
            value = attributes["keysym"]
            key.type = KeyCommon.KEYSYM_TYPE
            if value[1] == "x":#Deals for when keysym is hex
                key.code = int(value,16)
            else:
                key.code = int(value,10)
        elif "keypress_name" in attributes:
            key.code = attributes["keypress_name"]
            key.type = KeyCommon.KEYPRESS_NAME_TYPE
        elif "macro" in attributes:
            key.code = attributes["macro"]
            key.type = KeyCommon.MACRO_TYPE
        elif "script" in attributes:
            key.code = attributes["script"]
            key.type = KeyCommon.SCRIPT_TYPE
        elif "keycode" in attributes:
            key.code = int(attributes["keycode"])
            key.type = KeyCommon.KEYCODE_TYPE
        elif "button" in attributes:
            key.code = key.id[:]
            key.type = KeyCommon.BUTTON_TYPE
        elif key.modifier:
            key.code = None
            key.type = KeyCommon.LEGACY_MODIFIER_TYPE
        else:
            # key without action: just draw it, do nothing on click
            key.action = None
            key.action_type = None

        # get the size group of the key
        if "group" in attributes:
            group_name = attributes["group"]
        else:
            group_name = "_default"

        # get the optional image filename
        if "image" in attributes:
            if not key.image_filenames: key.image_filenames = {}
            key.image_filenames[ImageSlot.NORMAL] = attributes["image"].split(";")[0]
        if "image_active" in attributes:
            if not key.image_filenames: key.image_filenames = {}
            key.image_filenames[ImageSlot.ACTIVE] = attributes["image_active"]

        # get labels
        labels = self._parse_key_labels(attributes, key)

        # Replace label and size group with overrides from
        # theme and/or system defaults.
        label_overrides = config.theme_settings.key_label_overrides
        override = label_overrides.get(key.id)
        if override:
            olabel, ogroup = override
            if olabel:
                labels = {0 : olabel[:]}
                if ogroup:
                    group_name = ogroup[:]

        key.labels = labels
        key.group = group_name

        # optionally  override the theme's default key_style
        if "key_style" in attributes:
            key.style = attributes["key_style"]

        # select what gets drawn, different from "visible" flag as this
        # doesn't affect the layout.
        if "show" in attributes:
            if attributes["show"].lower() == 'false':
                key.show_face = False
                key.show_border = False
        if "show_face" in attributes:
            if attributes["show_face"].lower() == 'false':
                key.show_face = False
        if "show_border" in attributes:
            if attributes["show_border"].lower() == 'false':
                key.show_border = False

        # show_active: allow to display key in latched or locked state
        # legacy show_active behavior was hard-coded for layer0
        if self._format < LayoutLoaderSVG.LAYOUT_FORMAT_3_2:
            if key.id == "layer0":
                key.show_active = False
        if "show_active" in attributes:
            if attributes["show_active"].lower() == 'false':
                key.show_active = False

        if "label_x_align" in attributes:
            key.label_x_align = float(attributes["label_x_align"])
        if "label_y_align" in attributes:
            key.label_y_align = float(attributes["label_y_align"])

        if "label_margin" in attributes:
            values = attributes["label_margin"].replace(" ","").split(",")
            margin = [float(x) if x else key.label_margin[i] \
                      for i, x in enumerate(values[:2])]
            margin += margin[:1]*(2 - len(margin))
            if margin:
                key.label_margin = margin

        if "sticky" in attributes:
            sticky = attributes["sticky"].lower()
            if sticky == "true":
                key.sticky = True
            elif sticky == "false":
                key.sticky = False
            else:
                raise Exceptions.LayoutFileError(
                    "Invalid value '{}' for 'sticky' attribute of key '{}'" \
                    .format(sticky, key.id))
        else:
            key.sticky = False

        # legacy sticky key behavior was hard-coded for CAPS
        if self._format < LayoutLoaderSVG.LAYOUT_FORMAT_2_2:
            if key.id == "CAPS":
                key.sticky_behavior = StickyBehavior.LOCK_ONLY

        value = attributes.get("sticky_behavior")
        if value:
            try:
                key.sticky_behavior = StickyBehavior.from_string(value)
            except KeyError as ex:
                (strerror) = ex
                raise Exceptions.LayoutFileError("Unrecognized sticky behavior {} in" \
                    "definition of {}".format(strerror, full_id))

        if "tooltip" in attributes:
            key.tooltip = attributes["tooltip"]

        if "popup_id" in attributes:
            key.popup_id = attributes["popup_id"]

        if "chamfer_size" in attributes:
            key.chamfer_size = float(attributes["chamfer_size"])

        key.color_scheme = self._color_scheme

    def _parse_key_labels(self, attributes, key):
        labels = {}   # {modifier_mask : label, ...}

        # Get labels from keyboard mapping first.
        if key.type == KeyCommon.KEYCODE_TYPE and \
           not key.id in ["BKSP"]:
            if self._vk: # xkb keyboard found?
                vkmodmasks = self._label_modifier_masks
                if sys.version_info.major == 2:
                    vkmodmasks = [long(m) for m in vkmodmasks]
                vklabels = self._vk.labels_from_keycode(key.code, vkmodmasks)
                if sys.version_info.major == 2:
                    vklabels = [x.decode("UTF-8") for x in vklabels]
                labels = {m : l for m, l in zip(vkmodmasks, vklabels)}
            else:
                if key.id.upper() == "SPCE":
                    labels[0] = "No X keyboard found, retrying..."
                else:
                    labels[0] = "?"

        # If key is a macro (snippet) generate label from its number.
        elif key.type == KeyCommon.MACRO_TYPE:
            label, text = config.snippets.get(int(key.code), \
                                                       (None, None))
            tooltip = _format("Snippet {}", key.code)
            if not label:
                labels[0] = "     --     "
                # i18n: full string is "Snippet n, unassigned"
                tooltip += _(", unassigned")
            else:
                labels[0] = label.replace("\\n", "\n")
            key.tooltip = tooltip

        # get labels from the key/template definition in the layout
        layout_labels = self._parse_layout_labels(attributes)
        if layout_labels:
            labels = layout_labels

        # override with per-keysym labels
        keysym_rules = self._get_keysym_rules(key)
        if key.type == KeyCommon.KEYCODE_TYPE:
            if self._vk: # xkb keyboard found?
                vkmodmasks = self._label_modifier_masks
                try:
                    if sys.version_info.major == 2:
                        vkmodmasks = [long(m) for m in vkmodmasks]
                    vkkeysyms  = self._vk.keysyms_from_keycode(key.code,
                                                               vkmodmasks)
                except AttributeError:
                    # virtkey until 0.61.0 didn't have that method.
                    vkkeysyms = []

                # replace all labels whith keysyms matching a keysym rule
                for i, keysym in enumerate(vkkeysyms):
                    attributes = keysym_rules.get(keysym)
                    if attributes:
                        label = attributes.get("label")
                        if not label is None:
                            mask = vkmodmasks[i]
                            labels[mask] = label

        # Translate labels - Gettext behaves oddly when translating
        # empty strings
        return { mask : lab and _(lab) or None
                 for mask, lab in labels.items()}

    def _parse_layout_labels(self, attributes):
        """ Deprecated label definitions up to v0.98.x """
        labels = {}
        # modifier masks were hard-coded in python-virtkey
        if "label" in attributes:
            labels[0] = attributes["label"]
            if "cap_label" in attributes:
                labels[1] = attributes["cap_label"]
            if "shift_label" in attributes:
                labels[2] = attributes["shift_label"]
            if "altgr_label" in attributes:
                labels[128] = attributes["altgr_label"]
            if "altgrNshift_label" in attributes:
                labels[129] = attributes["altgrNshift_label"]
            if "_label" in attributes:
                labels[129] = attributes["altgrNshift_label"]
        return labels

    def _get_svg_keys(self, filename):
        svg_nodes = self._svg_cache.get(filename)
        if svg_nodes is None:
            svg_nodes = self._load_svg_keys(filename)
            self._svg_cache[filename] = svg_nodes

        return svg_nodes

    def _load_svg_keys(self, filename):
        filename = os.path.join(self._root_layout_dir, filename)
        try:
            with open_utf8(filename) as svg_file:
                svg_dom = minidom.parse(svg_file).documentElement
                svg_nodes = self._parse_svg(svg_dom)
                svg_nodes = {node.id : node for node in svg_nodes}
        except Exceptions.LayoutFileError as ex:
            raise Exceptions.LayoutFileError(
                "error loading '{}'".format(filename),
                chained_exception = (ex))
        return svg_nodes

    def _parse_svg(self, node):
        svg_nodes = []
        for child in node.childNodes:
            if child.nodeType == minidom.Node.ELEMENT_NODE:
                tag = child.tagName
                if tag in ("rect", "path", "g"):
                    svg_node = SVGNode()
                    id = child.attributes["id"].value
                    svg_node.id = id

                    if tag == "rect":
                        svg_node.bounds = \
                                   Rect(float(child.attributes['x'].value),
                                        float(child.attributes['y'].value),
                                        float(child.attributes['width'].value),
                                        float(child.attributes['height'].value))

                    elif tag == "path":
                        data = child.attributes['d'].value

                        try:
                            svg_node.path = KeyPath.from_svg_path(data)
                        except ValueError as ex:
                            raise Exceptions.LayoutFileError(
                                  "while reading geometry with id '{}'".format(id),
                                  chained_exception = (ex))

                        svg_node.bounds = svg_node.path.get_bounds()

                    elif tag == "g":  # group
                        svg_node.children = self._parse_svg(child)

                    svg_nodes.append(svg_node)

                svg_nodes.extend(self._parse_svg(child))

        return svg_nodes

    def find_template(self, scope_item, classinfo, ids):
        """
        Look for a template definition upwards from item until the root.
        """
        for item in scope_item.iter_to_global_root():
            templates = item.templates
            if templates:
                for id in ids:
                    match = templates.get((id, classinfo))
                    if not match is None:
                        return match
        return {}

    def _get_keysym_rules(self, scope_item):
        """
        Collect and merge keysym_rule from the root to item.
        Rules in nested items overwrite their parents'.
        """
        keysym_rules = {}
        for item in reversed(list(scope_item.iter_to_root())):
            if not item.keysym_rules is None:
                keysym_rules.update(item.keysym_rules)

        return keysym_rules

    def _get_system_keyboard_layout(self, vk):
        """ get names of the currently active layout group and variant """

        if vk: # xkb keyboard found?
            group = vk.get_current_group()
            names = vk.get_rules_names()
        else:
            group = 0
            names = ""

        if not names:
            names = ("base", "pc105", "us", "", "")
        layouts  = names[2].split(",")
        variants = names[3].split(",")

        if group >= 0 and group < len(layouts):
            layout = layouts[group]
        else:
            layout = ""
        if group >= 0 and group < len(variants):
            variant = variants[group]
        else:
            variant = ""

        return layout, variant

    def _get_system_layout_string(self):
        s = self._system_layout
        if self._system_variant:
            s += "(" + self._system_variant + ")"
        return s

    def _has_matching_layout(self, layout_str):
        """
        Check if one ot the given layout strings matches
        system keyboard layout and variant.

        Doctests:
        >>> l = LayoutLoaderSVG()
        >>> l._system_layout = "ch"
        >>> l._system_variant = "fr"
        >>> l._has_matching_layout("ch(x), us, de")
        False
        >>> l._has_matching_layout("abc, ch(fr)")
        True
        >>> l._system_variant = ""
        >>> l._has_matching_layout("ch(x), us, de")
        False
        >>> l._has_matching_layout("ch, us, de")
        True
        """
        layouts = layout_str.split(",")  # comma separated layout specifiers
        sys_layout = self._system_layout
        sys_variant = self._system_variant
        for value in layouts:
            layout, variant = self._layout_regex.search(value.strip()).groups()
            if layout == sys_layout and \
               (not variant or sys_variant.startswith(variant)):
                return True
        return False


    # --------------------------------------------------------------------------
    # Legacy pane layout support
    # --------------------------------------------------------------------------
    def _parse_legacy_layout(self, dom_node):

        # parse panes
        panes = []
        is_scan = False
        for i, pane_node in enumerate(dom_node.getElementsByTagName("pane")):
            item = LayoutPanel()
            item.layer_id = "layer {}".format(i)

            item.id       = pane_node.attributes["id"].value
            item.filename = pane_node.attributes["filename"].value

            # parse keys
            keys = []
            for node in pane_node.getElementsByTagName("key"):
                key = self._parse_key(node, item)
                if key:
                    # some keys have changed since Onboard 0.95
                    if key.id == "middleClick":
                        key.set_id("middleclick")
                        key.type = KeyCommon.BUTTON_TYPE
                    if key.id == "secondaryClick":
                        key.set_id("secondaryclick")
                        key.type = KeyCommon.BUTTON_TYPE

                    keys.append(key)

            item.set_items(keys)

            # check for scan columns
            if pane_node.getElementsByTagName("column"):
                is_scan = True

            panes.append(item)

        layer_area = LayoutPanel()
        layer_area.id = "layer_area"
        layer_area.set_items(panes)

        # find the most frequent key width
        histogram = {}
        for key in layer_area.iter_keys():
            w = key.get_border_rect().w
            histogram[w] = histogram.get(w, 0) + 1
        most_frequent_width = max(list(zip(list(histogram.values()), list(histogram.keys()))))[1] \
                              if histogram else 18

        # Legacy onboard had automatic tab-keys for pane switching.
        # Simulate this by generating layer buttons from scratch.
        keys = []
        group = "__layer_buttons__"
        widen = 1.4 if not is_scan else 1.0
        rect = Rect(0, 0, most_frequent_width * widen, 20)

        key = RectKey()
        attributes = {}
        attributes["id"]     = "hide"
        attributes["group"]  = group
        attributes["image"]  = "close.svg"
        attributes["button"] = "true"
        attributes["scannable"] = "false"
        self._init_key(key, attributes)
        key.set_border_rect(rect.copy())
        keys.append(key)

        key = RectKey()
        attributes = {}
        attributes["id"]     = "move"
        attributes["group"]  = group
        attributes["image"]  = "move.svg"
        attributes["button"] = "true"
        attributes["scannable"] = "false"
        self._init_key(key, attributes)
        key.set_border_rect(rect.copy())
        keys.append(key)

        if len(panes) > 1:
            for i, pane in enumerate(panes):
                key = RectKey()
                attributes = {}
                attributes["id"]     = "layer{}".format(i)
                attributes["group"]  = group
                attributes["label"]  = pane.id
                attributes["button"] = "true"
                self._init_key(key, attributes)
                key.set_border_rect(rect.copy())
                keys.append(key)

        layer_switch_column = LayoutBox()
        layer_switch_column.horizontal = False
        layer_switch_column.set_items(keys)

        layout = LayoutBox()
        layout.border = 1
        layout.spacing = 2
        layout.set_items([layer_area, layer_switch_column])

        return [layout]

    @staticmethod
    def copy_layout(src_filename, dst_filename):
        src_dir = os.path.dirname(src_filename)
        dst_dir, name_ext = os.path.split(dst_filename)
        dst_basename, ext = os.path.splitext(name_ext)
        _logger.info(_format("copying layout '{}' to '{}'",
                             src_filename, dst_filename))

        domdoc = None
        svg_filenames = {}
        fallback_layers = {}

        try:
            with open_utf8(src_filename) as f:
                domdoc = minidom.parse(f)
                keyboard_node = domdoc.documentElement

                # check layout format
                format = LayoutLoaderSVG.LAYOUT_FORMAT_LEGACY
                if keyboard_node.hasAttribute("format"):
                   format = Version.from_string(keyboard_node.attributes["format"].value)
                keyboard_node.attributes["id"] = dst_basename

                if format < LayoutLoaderSVG.LAYOUT_FORMAT_LAYOUT_TREE:
                    raise Exceptions.LayoutFileError( \
                        _format("copy_layouts failed, unsupported layout format '{}'.",
                                format))
                else:
                    # replace the basename of all svg filenames
                    for node in LayoutLoaderSVG._iter_dom_nodes(keyboard_node):
                        if LayoutLoaderSVG.is_layout_node(node):
                            if node.hasAttribute("filename"):
                                filename = node.attributes["filename"].value

                                # Create a replacement layer name for the unlikely
                                # case  that the svg-filename doesn't contain a
                                # layer section (as in path/basename-layer.ext).
                                fallback_layer_name = fallback_layers.get(filename,
                                             "Layer" + str(len(fallback_layers)))
                                fallback_layers[filename] = fallback_layer_name

                                # replace the basename of this filename
                                new_filename = LayoutLoaderSVG._replace_basename( \
                                     filename, dst_basename, fallback_layer_name)

                                node.attributes["filename"].value = new_filename
                                svg_filenames[filename] = new_filename

            if domdoc:
                XDGDirs.assure_user_dir_exists(config.get_user_layout_dir())

                # write the new layout file
                with open_utf8(dst_filename, "w") as f:
                    xml = toprettyxml(domdoc)
                    if sys.version_info.major == 2:  # python 2?
                        xml = xml.encode("UTF-8")
                    f.write(xml)

                    # copy the svg files
                    for src, dst in list(svg_filenames.items()):

                        dir, name = os.path.split(src)
                        if not dir:
                            src = os.path.join(src_dir, name)
                        dir, name = os.path.split(dst)
                        if not dir:
                            dst = os.path.join(dst_dir, name)

                        _logger.info(_format("copying svg file '{}' to '{}'", \
                                     src, dst))
                        shutil.copyfile(src, dst)
        except OSError as ex:
            _logger.error("copy_layout failed: " + \
                          unicode_str(ex))
        except Exceptions.LayoutFileError as ex:
            _logger.error(unicode_str(ex))


    @staticmethod
    def remove_layout(filename):
        for fn in LayoutLoaderSVG.get_layout_svg_filenames(filename):
            os.remove(fn)
        os.remove(filename)

    @staticmethod
    def get_layout_svg_filenames(filename):
        results = []
        domdoc = None
        with open_utf8(filename) as f:
            domdoc = minidom.parse(f).documentElement

        if domdoc:
            filenames = {}
            for node in LayoutLoaderSVG._iter_dom_nodes(domdoc):
                if LayoutLoaderSVG.is_layout_node(node):
                    if node.hasAttribute("filename"):
                        fn = node.attributes["filename"].value
                        filenames[fn] = fn

            layout_dir, name = os.path.split(filename)
            results = []
            for fn in list(filenames.keys()):
                dir, name = os.path.split(fn)
                results.append(os.path.join(layout_dir, name))

        return results

    @staticmethod
    def _replace_basename(filename, new_basename, fallback_layer_name):
        """
        Doctests:
        # Basename has to be replaced with new_basename.
        >>> test = LayoutLoaderSVG._replace_basename
        >>> test("/home/usr/.local/share/onboard/Base-Alpha.svg",
        ... "NewBase","Fallback")
        'NewBase-Alpha.svg'

        # Dashes in front are allowed, but the layer name must not have any.
        >>> test("/home/usr/.local/share/onboard/a-b-c-Alpha.svg",
        ... "d-e-f","g-h")
        'd-e-f-Alpha.svg'
        """
        dir, name_ext = os.path.split(filename)
        name, ext = os.path.splitext(name_ext)
        if name:
            index = name.rfind("-")
            if index >= 0:
                layer = name[index+1:]
            else:
                layer = fallback_layer_name
            return "{}-{}{}".format(new_basename, layer, ext)
        return ""

    @staticmethod
    def is_layout_node(dom_node):
        return dom_node.tagName in ["include", "key_template", "keysym_rule",
                                    "box", "panel", "key", "layout"]

    @staticmethod
    def _iter_dom_nodes(dom_node):
        """ Recursive generator function to traverse aa dom tree """
        yield dom_node

        for child in dom_node.childNodes:
            if child.nodeType == minidom.Node.ELEMENT_NODE:
                for node in LayoutLoaderSVG._iter_dom_nodes(child):
                    yield node


class SVGNode:
    """
    Cache of SVG provided key attributes.
    """
    id = None             # svg_id
    bounds = None         # logical bounding rect, aka border rect
    path = None           # optional path for arbitrary shapes

    def __init__(self):
        self.children = []

    def extract_key_params(self):
        if self.children:
            nodes = self.children[:2]
        else:
            nodes = [self]
        bounds = nodes[0].bounds
        paths = [node.path for node in nodes if node.path]
        if paths:
            geometry = KeyGeometry.from_paths(paths)
        else:
            geometry = None
        return bounds, geometry


