# Copyright (c) 2009-2010 Aldo Cortesi
# Copyright (c) 2010 matt
# Copyright (c) 2010, 2012, 2014 dequis
# Copyright (c) 2010 Philip Kranz
# Copyright (c) 2010-2011 Paul Colomiets
# Copyright (c) 2011 osebelin
# Copyright (c) 2011 Mounier Florian
# Copyright (c) 2011 Kenji_Takahashi
# Copyright (c) 2011 Tzbob
# Copyright (c) 2012, 2014 roger
# Copyright (c) 2012, 2014-2015 Tycho Andersen
# Copyright (c) 2013 Tao Sauvage
# Copyright (c) 2014-2015 Sean Vig
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

"""
    A minimal EWMH-aware OO layer over xcffib. This is NOT intended to be
    complete - it only implements the subset of functionalty needed by qtile.
"""
import functools
import operator
import typing
from collections import OrderedDict
from itertools import chain, repeat

import cairocffi
import cairocffi.pixbuf
import cairocffi.xcb
import xcffib
import xcffib.randr
import xcffib.xinerama
import xcffib.xproto
from xcffib.xfixes import SelectionEventMask
from xcffib.xproto import CW, EventMask, WindowClass

from libqtile import xkeysyms
from libqtile.backend.x11.xcursors import Cursors
from libqtile.log_utils import logger

keysyms = xkeysyms.keysyms


class XCBQError(Exception):
    pass


def rdict(d):
    r = {}
    for k, v in d.items():
        r.setdefault(v, []).append(k)
    return r


rkeysyms = rdict(xkeysyms.keysyms)

# Keyboard modifiers bitmask values from X Protocol
ModMasks = OrderedDict((
    ("shift", 1 << 0),
    ("lock", 1 << 1),
    ("control", 1 << 2),
    ("mod1", 1 << 3),
    ("mod2", 1 << 4),
    ("mod3", 1 << 5),
    ("mod4", 1 << 6),
    ("mod5", 1 << 7),
))

AllButtonsMask = 0b11111 << 8
ButtonMotionMask = 1 << 13
ButtonReleaseMask = 1 << 3

NormalHintsFlags = {
    "USPosition": 1,     # User-specified x, y
    "USSize": 2,         # User-specified width, height
    "PPosition": 4,      # Program-specified position
    "PSize": 8,          # Program-specified size
    "PMinSize": 16,      # Program-specified minimum size
    "PMaxSize": 32,      # Program-specified maximum size
    "PResizeInc": 64,    # Program-specified resize increments
    "PAspect": 128,      # Program-specified min and max aspect ratios
    "PBaseSize": 256,    # Program-specified base size
    "PWinGravity": 512,  # Program-specified window gravity
}

HintsFlags = {
    "InputHint": 1,          # input
    "StateHint": 2,          # initial_state
    "IconPixmapHint": 4,     # icon_pixmap
    "IconWindowHint": 8,     # icon_window
    "IconPositionHint": 16,  # icon_x & icon_y
    "IconMaskHint": 32,      # icon_mask
    "WindowGroupHint": 64,   # window_group
    "MessageHint": 128,      # (this bit is obsolete)
    "UrgencyHint": 256,      # urgency
}

# http://standards.freedesktop.org/wm-spec/latest/ar01s05.html#idm139870830002400
WindowTypes = {
    '_NET_WM_WINDOW_TYPE_DESKTOP': "desktop",
    '_NET_WM_WINDOW_TYPE_DOCK': "dock",
    '_NET_WM_WINDOW_TYPE_TOOLBAR': "toolbar",
    '_NET_WM_WINDOW_TYPE_MENU': "menu",
    '_NET_WM_WINDOW_TYPE_UTILITY': "utility",
    '_NET_WM_WINDOW_TYPE_SPLASH': "splash",
    '_NET_WM_WINDOW_TYPE_DIALOG': "dialog",
    '_NET_WM_WINDOW_TYPE_DROPDOWN_MENU': "dropdown",
    '_NET_WM_WINDOW_TYPE_POPUP_MENU': "menu",
    '_NET_WM_WINDOW_TYPE_TOOLTIP': "tooltip",
    '_NET_WM_WINDOW_TYPE_NOTIFICATION': "notification",
    '_NET_WM_WINDOW_TYPE_COMBO': "combo",
    '_NET_WM_WINDOW_TYPE_DND': "dnd",
    '_NET_WM_WINDOW_TYPE_NORMAL': "normal",
}

# http://standards.freedesktop.org/wm-spec/latest/ar01s05.html#idm139870829988448
net_wm_states = (
    '_NET_WM_STATE_MODAL',
    '_NET_WM_STATE_STICKY',
    '_NET_WM_STATE_MAXIMIZED_VERT',
    '_NET_WM_STATE_MAXIMIZED_HORZ',
    '_NET_WM_STATE_SHADED',
    '_NET_WM_STATE_SKIP_TASKBAR',
    '_NET_WM_STATE_SKIP_PAGER',
    '_NET_WM_STATE_HIDDEN',
    '_NET_WM_STATE_FULLSCREEN',
    '_NET_WM_STATE_ABOVE',
    '_NET_WM_STATE_BELOW',
    '_NET_WM_STATE_DEMANDS_ATTENTION',
    '_NET_WM_STATE_FOCUSED',
)

WindowStates = {
    None: 'normal',
    '_NET_WM_STATE_FULLSCREEN': 'fullscreen',
    '_NET_WM_STATE_DEMANDS_ATTENTION': 'urgent'
}

# Maps property names to types and formats.
PropertyMap = {
    # ewmh properties
    "_NET_DESKTOP_GEOMETRY": ("CARDINAL", 32),
    "_NET_SUPPORTED": ("ATOM", 32),
    "_NET_SUPPORTING_WM_CHECK": ("WINDOW", 32),
    "_NET_WM_NAME": ("UTF8_STRING", 8),
    "_NET_WM_PID": ("CARDINAL", 32),
    "_NET_CLIENT_LIST": ("WINDOW", 32),
    "_NET_CLIENT_LIST_STACKING": ("WINDOW", 32),
    "_NET_NUMBER_OF_DESKTOPS": ("CARDINAL", 32),
    "_NET_CURRENT_DESKTOP": ("CARDINAL", 32),
    "_NET_DESKTOP_NAMES": ("UTF8_STRING", 8),
    "_NET_WORKAREA": ("CARDINAL", 32),
    "_NET_ACTIVE_WINDOW": ("WINDOW", 32),
    "_NET_WM_DESKTOP": ("CARDINAL", 32),
    "_NET_WM_STRUT": ("CARDINAL", 32),
    "_NET_WM_STRUT_PARTIAL": ("CARDINAL", 32),
    "_NET_WM_WINDOW_OPACITY": ("CARDINAL", 32),
    "_NET_WM_WINDOW_TYPE": ("CARDINAL", 32),
    # Net State
    "_NET_WM_STATE": ("ATOM", 32),
    # Xembed
    "_XEMBED_INFO": ("_XEMBED_INFO", 32),
    # ICCCM
    "WM_STATE": ("WM_STATE", 32),
    # Qtile-specific properties
    "QTILE_INTERNAL": ("CARDINAL", 32)
}
for _name in net_wm_states:
    PropertyMap[_name] = ('ATOM', 32)

# TODO add everything required here:
# http://standards.freedesktop.org/wm-spec/latest/ar01s03.html
SUPPORTED_ATOMS = [
    # From http://standards.freedesktop.org/wm-spec/latest/ar01s03.html
    '_NET_SUPPORTED',
    '_NET_CLIENT_LIST',
    '_NET_CLIENT_LIST_STACKING',
    '_NET_CURRENT_DESKTOP',
    '_NET_ACTIVE_WINDOW',
    '_NET_SUPPORTING_WM_CHECK',
    # From http://standards.freedesktop.org/wm-spec/latest/ar01s05.html
    '_NET_WM_NAME',
    '_NET_WM_VISIBLE_NAME',
    '_NET_WM_ICON_NAME',
    '_NET_WM_DESKTOP',
    '_NET_WM_WINDOW_TYPE',
    '_NET_WM_STATE',
    '_NET_WM_STRUT',
    '_NET_WM_STRUT_PARTIAL',
    '_NET_WM_PID',
]
SUPPORTED_ATOMS.extend(WindowTypes.keys())
SUPPORTED_ATOMS.extend(net_wm_states)

XCB_CONN_ERRORS = {
    1: 'XCB_CONN_ERROR',
    2: 'XCB_CONN_CLOSED_EXT_NOTSUPPORTED',
    3: 'XCB_CONN_CLOSED_MEM_INSUFFICIENT',
    4: 'XCB_CONN_CLOSED_REQ_LEN_EXCEED',
    5: 'XCB_CONN_CLOSED_PARSE_ERR',
    6: 'XCB_CONN_CLOSED_INVALID_SCREEN',
    7: 'XCB_CONN_CLOSED_FDPASSING_FAILED',
}


class MaskMap:
    """
        A general utility class that encapsulates the way the bitmask/listofvalue idiom
        works in X protocol. It understands a special attribute _maskvalue on
        objects, which will be used instead of the object value if present.
        This lets us pass in a Font object, rather than Font.fid, for example.
    """
    def __init__(self, obj):
        self.mmap = []
        for i in dir(obj):
            if not i.startswith("_"):
                self.mmap.append((getattr(obj, i), i.lower()))
        self.mmap.sort()

    def __call__(self, **kwargs):
        """
            kwargs: keys should be in the mmap name set

            Returns a (mask, values) tuple.
        """
        mask = 0
        values = []
        for m, s in self.mmap:
            if s in kwargs:
                val = kwargs.get(s)
                if val is not None:
                    mask |= m
                    values.append(getattr(val, "_maskvalue", val))
                del kwargs[s]
        if kwargs:
            raise ValueError("Unknown mask names: %s" % list(kwargs.keys()))
        return mask, values


ConfigureMasks = MaskMap(xcffib.xproto.ConfigWindow)
AttributeMasks = MaskMap(CW)


class AtomCache:
    def __init__(self, conn):
        self.conn = conn
        self.atoms = {}
        self.reverse = {}

        # We can change the pre-loads not to wait for a return
        for name in WindowTypes.keys():
            self.insert(name=name)

        for i in dir(xcffib.xproto.Atom):
            if not i.startswith("_"):
                self.insert(name=i, atom=getattr(xcffib.xproto.Atom, i))

    def insert(self, name=None, atom=None):
        assert name or atom
        if atom is None:
            c = self.conn.conn.core.InternAtom(False, len(name), name)
            atom = c.reply().atom
        if name is None:
            c = self.conn.conn.core.GetAtomName(atom)
            name = c.reply().name.to_string()
        self.atoms[name] = atom
        self.reverse[atom] = name

    def get_name(self, atom):
        if atom not in self.reverse:
            self.insert(atom=atom)
        return self.reverse[atom]

    def __getitem__(self, key):
        if key not in self.atoms:
            self.insert(name=key)
        return self.atoms[key]


class _Wrapper:
    def __init__(self, wrapped):
        self.wrapped = wrapped

    def __getattr__(self, x):
        return getattr(self.wrapped, x)


class Screen(_Wrapper):
    """
        This represents an actual X screen.
    """
    def __init__(self, conn, screen):
        _Wrapper.__init__(self, screen)
        self.default_colormap = Colormap(conn, screen.default_colormap)
        self.root = Window(conn, self.root)


class PseudoScreen:
    """
        This may be a Xinerama screen or a RandR CRTC, both of which are
        rectangular sections of an actual Screen.
    """
    def __init__(self, conn, x, y, width, height):
        self.conn = conn
        self.x = x
        self.y = y
        self.width = width
        self.height = height


class Colormap:
    def __init__(self, conn, cid):
        self.conn = conn
        self.cid = cid

    def alloc_color(self, color):
        """
            Flexible color allocation.
        """
        try:
            return self.conn.conn.core.AllocNamedColor(
                self.cid, len(color), color
            ).reply()
        except xcffib.xproto.NameError:

            def x8to16(i):
                return 0xffff * (i & 0xff) // 0xff
            r = x8to16(int(color[-6] + color[-5], 16))
            g = x8to16(int(color[-4] + color[-3], 16))
            b = x8to16(int(color[-2] + color[-1], 16))
            return self.conn.conn.core.AllocColor(self.cid, r, g, b).reply()


class Xinerama:
    def __init__(self, conn):
        self.ext = conn.conn(xcffib.xinerama.key)

    def query_screens(self):
        r = self.ext.QueryScreens().reply()
        return r.screen_info


class RandR:
    def __init__(self, conn):
        self.ext = conn.conn(xcffib.randr.key)
        self.ext.SelectInput(
            conn.default_screen.root.wid,
            xcffib.randr.NotifyMask.ScreenChange
        )

    def query_crtcs(self, root):
        crtc_list = []
        for crtc in self.ext.GetScreenResources(root).reply().crtcs:
            crtc_info = self.ext.GetCrtcInfo(crtc, xcffib.CurrentTime).reply()
            crtc_dict = {
                "x": crtc_info.x,
                "y": crtc_info.y,
                "width": crtc_info.width,
                "height": crtc_info.height,
            }
            crtc_list.append(crtc_dict)
        return crtc_list


class XFixes:
    selection_mask = SelectionEventMask.SetSelectionOwner | \
        SelectionEventMask.SelectionClientClose | \
        SelectionEventMask.SelectionWindowDestroy

    def __init__(self, conn):
        self.conn = conn
        self.ext = conn.conn(xcffib.xfixes.key)
        self.ext.QueryVersion(xcffib.xfixes.MAJOR_VERSION,
                              xcffib.xfixes.MINOR_VERSION)

    def select_selection_input(self, window, selection="PRIMARY"):
        _selection = self.conn.atoms[selection]
        self.conn.xfixes.ext.SelectSelectionInput(window.wid,
                                                  _selection,
                                                  self.selection_mask)


class NetWmState:
    """NetWmState is a descriptor for _NET_WM_STATE_* properties"""
    def __init__(self, prop_name):
        self.prop_name = prop_name

    def __get__(self, xcbq_win, cls):
        try:
            atom = self.atom
        except AttributeError:
            atom = xcbq_win.conn.atoms[self.prop_name]
            self.atom = atom
        reply = xcbq_win.get_property('_NET_WM_STATE', 'ATOM', unpack=int)
        if atom in reply:
            return True
        return False

    def __set__(self, xcbq_win, value):
        try:
            atom = self.atom
        except AttributeError:
            atom = xcbq_win.conn.atoms[self.prop_name]
            self.atom = atom

        value = bool(value)
        reply = list(xcbq_win.get_property('_NET_WM_STATE', 'ATOM', unpack=int))
        is_set = atom in reply
        if is_set and not value:
            reply.remove(atom)
            xcbq_win.set_property('_NET_WM_STATE', reply)
        elif value and not is_set:
            reply.append(atom)
            xcbq_win.set_property('_NET_WM_STATE', reply)
        return


def _add_net_wm_state(cls):
    for name in net_wm_states:
        lower_name = name.lstrip('_').lower()
        setattr(cls, lower_name, NetWmState(name))
    return cls


@_add_net_wm_state
class Window:
    def __init__(self, conn, wid):
        self.conn = conn
        self.wid = wid

    def _property_string(self, r):
        """Extract a string from a window property reply message"""
        return r.value.to_string()

    def _property_utf8(self, r):
        return r.value.to_utf8()

    def send_event(self, synthevent, mask=EventMask.NoEvent):
        self.conn.conn.core.SendEvent(False, self.wid, mask, synthevent.pack())

    def kill_client(self):
        self.conn.conn.core.KillClient(self.wid)

    def set_input_focus(self):
        self.conn.conn.core.SetInputFocus(
            xcffib.xproto.InputFocus.PointerRoot,
            self.wid,
            xcffib.xproto.Time.CurrentTime
        )

    def warp_pointer(self, x, y):
        """Warps the pointer to the location `x`, `y` on the window"""
        self.conn.conn.core.WarpPointer(
            0, self.wid,  # src_window, dst_window
            0, 0,         # src_x, src_y
            0, 0,         # src_width, src_height
            x, y          # dest_x, dest_y
        )

    def get_name(self):
        """Tries to retrieve a canonical window name.

        We test the following properties in order of preference:
            - _NET_WM_VISIBLE_NAME
            - _NET_WM_NAME
            - WM_NAME.
        """
        r = self.get_property("_NET_WM_VISIBLE_NAME", "UTF8_STRING")
        if r:
            return self._property_utf8(r)

        r = self.get_property("_NET_WM_NAME", "UTF8_STRING")
        if r:
            return self._property_utf8(r)

        r = self.get_property(xcffib.xproto.Atom.WM_NAME, "UTF8_STRING")
        if r:
            return self._property_utf8(r)

        r = self.get_property(
            xcffib.xproto.Atom.WM_NAME,
            xcffib.xproto.GetPropertyType.Any
        )
        if r:
            return self._property_string(r)

    def get_wm_hints(self):
        wm_hints = self.get_property("WM_HINTS", xcffib.xproto.GetPropertyType.Any)
        if wm_hints:
            atoms_list = wm_hints.value.to_atoms()
            flags = {k for k, v in HintsFlags.items() if atoms_list[0] & v}
            return {
                "flags": flags,
                "input": atoms_list[1] if "InputHint" in flags else None,
                "initial_state": atoms_list[2] if "StateHing" in flags else None,
                "icon_pixmap": atoms_list[3] if "IconPixmapHint" in flags else None,
                "icon_window": atoms_list[4] if "IconWindowHint" in flags else None,
                "icon_x": atoms_list[5] if "IconPositionHint" in flags else None,
                "icon_y": atoms_list[6] if "IconPositionHint" in flags else None,
                "icon_mask": atoms_list[7] if "IconMaskHint" in flags else None,
                "window_group": atoms_list[8] if 'WindowGroupHint' in flags else None,
            }

    def get_wm_normal_hints(self):
        wm_normal_hints = self.get_property(
            "WM_NORMAL_HINTS",
            xcffib.xproto.GetPropertyType.Any
        )
        if wm_normal_hints:
            atom_list = wm_normal_hints.value.to_atoms()
            flags = {k for k, v in NormalHintsFlags.items() if atom_list[0] & v}
            return {
                "flags": flags,
                "min_width": atom_list[1 + 4],
                "min_height": atom_list[2 + 4],
                "max_width": atom_list[3 + 4],
                "max_height": atom_list[4 + 4],
                "width_inc": atom_list[5 + 4],
                "height_inc": atom_list[6 + 4],
                "min_aspect": atom_list[7 + 4],
                "max_aspect": atom_list[8 + 4],
                "base_width": atom_list[9 + 4],
                "base_height": atom_list[9 + 4],
                "win_gravity": atom_list[9 + 4],
            }

    def get_wm_protocols(self):
        wm_protocols = self.get_property("WM_PROTOCOLS", "ATOM", unpack=int)
        if wm_protocols is not None:
            return {self.conn.atoms.get_name(wm_protocol) for wm_protocol in wm_protocols}
        return set()

    def get_wm_state(self):
        return self.get_property("WM_STATE", xcffib.xproto.GetPropertyType.Any, unpack=int)

    def get_wm_class(self):
        """Return an (instance, class) tuple if WM_CLASS exists, or None"""
        r = self.get_property("WM_CLASS", "STRING")
        if r:
            s = self._property_string(r)
            return tuple(s.strip("\0").split("\0"))
        return tuple()

    def get_wm_window_role(self):
        r = self.get_property("WM_WINDOW_ROLE", "STRING")
        if r:
            return self._property_string(r)

    def get_wm_transient_for(self):
        r = self.get_property("WM_TRANSIENT_FOR", "WINDOW", unpack=int)

        if r:
            return r[0]

    def get_wm_icon_name(self):
        r = self.get_property("_NET_WM_ICON_NAME", "UTF8_STRING")
        if r:
            return self._property_utf8(r)

        r = self.get_property("WM_ICON_NAME", "STRING")
        if r:
            return self._property_utf8(r)

    def get_wm_client_machine(self):
        r = self.get_property("WM_CLIENT_MACHINE", "STRING")
        if r:
            return self._property_utf8(r)

    def get_geometry(self):
        q = self.conn.conn.core.GetGeometry(self.wid)
        return q.reply()

    def get_wm_desktop(self):
        r = self.get_property("_NET_WM_DESKTOP", "CARDINAL", unpack=int)

        if r:
            return r[0]

    def get_wm_type(self):
        """
        http://standards.freedesktop.org/wm-spec/wm-spec-latest.html#id2551529
        """
        r = self.get_property('_NET_WM_WINDOW_TYPE', "ATOM", unpack=int)
        if r:
            name = self.conn.atoms.get_name(r[0])
            return WindowTypes.get(name, name)

    def get_net_wm_state(self):
        r = self.get_property('_NET_WM_STATE', "ATOM", unpack=int)
        if r:
            names = [self.conn.atoms.get_name(p) for p in r]
            return [WindowStates.get(n, n) for n in names]
        return []

    def get_net_wm_pid(self):
        r = self.get_property("_NET_WM_PID", unpack=int)
        if r:
            return r[0]

    def configure(self, **kwargs):
        """
        Arguments can be: x, y, width, height, border, sibling, stackmode
        """
        mask, values = ConfigureMasks(**kwargs)
        # older versions of xcb pack everything into unsigned ints "=I"
        # since 1.12, uses switches to pack things sensibly
        if float(xcffib.__xcb_proto_version__) < 1.12:
            values = [i & 0xffffffff for i in values]
        return self.conn.conn.core.ConfigureWindow(self.wid, mask, values)

    def set_attribute(self, **kwargs):
        mask, values = AttributeMasks(**kwargs)
        self.conn.conn.core.ChangeWindowAttributesChecked(
            self.wid, mask, values
        )

    def set_cursor(self, name):
        cursor_id = self.conn.cursors[name]
        mask, values = AttributeMasks(cursor=cursor_id)
        self.conn.conn.core.ChangeWindowAttributesChecked(
            self.wid, mask, values
        )

    def set_property(self, name, value, type=None, format=None):
        """
        Parameters
        ==========
        name : String Atom name
        type : String Atom name
        format : 8, 16, 32
        """
        if name in PropertyMap:
            if type or format:
                raise ValueError(
                    "Over-riding default type or format for property."
                )
            type, format = PropertyMap[name]
        else:
            if None in (type, format):
                raise ValueError(
                    "Must specify type and format for unknown property."
                )

        try:
            if isinstance(value, str):
                # xcffib will pack the bytes, but we should encode them properly
                value = value.encode()
            else:
                # if this runs without error, the value is already a list, don't wrap it
                next(iter(value))
        except StopIteration:
            # The value was an iterable, just empty
            value = []
        except TypeError:
            # the value wasn't an iterable and wasn't a string, so let's
            # wrap it.
            value = [value]

        try:
            self.conn.conn.core.ChangePropertyChecked(
                xcffib.xproto.PropMode.Replace,
                self.wid,
                self.conn.atoms[name],
                self.conn.atoms[type],
                format,  # Format - 8, 16, 32
                len(value),
                value
            ).check()
        except xcffib.xproto.WindowError:
            logger.debug(
                'X error in SetProperty (wid=%r, prop=%r), ignoring',
                self.wid, name)

    def get_property(self, prop, type=None, unpack=None):
        """Return the contents of a property as a GetPropertyReply

        If unpack is specified, a tuple of values is returned.  The type to
        unpack, either `str` or `int` must be specified.
        """
        if type is None:
            if prop not in PropertyMap:
                raise ValueError(
                    "Must specify type for unknown property."
                )
            else:
                type, _ = PropertyMap[prop]

        try:
            r = self.conn.conn.core.GetProperty(
                False, self.wid,
                self.conn.atoms[prop]
                if isinstance(prop, str)
                else prop,
                self.conn.atoms[type]
                if isinstance(type, str)
                else type,
                0, (2 ** 32) - 1
            ).reply()
        except (xcffib.xproto.WindowError, xcffib.xproto.AccessError):
            logger.debug(
                'X error in GetProperty (wid=%r, prop=%r), ignoring',
                self.wid, prop)
            if unpack:
                return []
            return None

        if not r.value_len:
            if unpack:
                return []
            return None
        elif unpack:
            # Should we allow more options for unpacking?
            if unpack is int:
                return r.value.to_atoms()
            elif unpack is str:
                return r.value.to_string()
        else:
            return r

    def list_properties(self):
        r = self.conn.conn.core.ListProperties(self.wid).reply()
        return [self.conn.atoms.get_name(i) for i in r.atoms]

    def map(self):
        self.conn.conn.core.MapWindow(self.wid)

    def unmap(self):
        self.conn.conn.core.UnmapWindowUnchecked(self.wid)

    def get_attributes(self):
        return self.conn.conn.core.GetWindowAttributes(self.wid).reply()

    def query_tree(self):
        q = self.conn.conn.core.QueryTree(self.wid).reply()
        root = None
        parent = None
        if q.root:
            root = Window(self.conn, q.root)
        if q.parent:
            parent = Window(self.conn, q.parent)
        return root, parent, [Window(self.conn, i) for i in q.children]


class Font:
    def __init__(self, conn, fid):
        self.conn = conn
        self.fid = fid

    @property
    def _maskvalue(self):
        return self.fid

    def text_extents(self, s):
        s += "aaa"
        x = self.conn.conn.core.QueryTextExtents(self.fid, len(s), s).reply()
        return x


class Connection:
    _extmap = {
        "xinerama": Xinerama,
        "randr": RandR,
        "xfixes": XFixes,
    }

    def __init__(self, display):
        self.conn = xcffib.connect(display=display)
        self._connected = True
        self.cursors = Cursors(self)
        self.setup = self.conn.get_setup()
        extensions = self.extensions()
        self.screens = [Screen(self, i) for i in self.setup.roots]
        self.default_screen = self.screens[self.conn.pref_screen]

        for i in extensions:
            if i in self._extmap:
                setattr(self, i, self._extmap[i](self))

        self.pseudoscreens = []
        if "xinerama" in extensions:
            for i, s in enumerate(self.xinerama.query_screens()):
                scr = PseudoScreen(
                    self,
                    s.x_org,
                    s.y_org,
                    s.width,
                    s.height,
                )
                self.pseudoscreens.append(scr)
        elif "randr" in extensions:
            for i in self.randr.query_crtcs(self.screens[0].root.wid):
                scr = PseudoScreen(
                    self,
                    i["x"],
                    i["y"],
                    i["width"],
                    i["height"],
                )
                self.pseudoscreens.append(scr)

        self.atoms = AtomCache(self)

        self.code_to_syms = {}
        self.first_sym_to_code = None
        self.refresh_keymap()

        self.modmap = None
        self.refresh_modmap()

    def finalize(self):
        self.cursors.finalize()
        self.disconnect()

    def refresh_keymap(self, first=None, count=None):
        if first is None:
            first = self.setup.min_keycode
            count = self.setup.max_keycode - self.setup.min_keycode + 1
        q = self.conn.core.GetKeyboardMapping(first, count).reply()

        assert len(q.keysyms) % q.keysyms_per_keycode == 0
        for i in range(len(q.keysyms) // q.keysyms_per_keycode):
            self.code_to_syms[first + i] = \
                q.keysyms[i * q.keysyms_per_keycode:(i + 1) * q.keysyms_per_keycode]

        first_sym_to_code = {}
        for k, s in self.code_to_syms.items():
            if s[0] and not s[0] in first_sym_to_code:
                first_sym_to_code[s[0]] = k

        self.first_sym_to_code = first_sym_to_code

    def refresh_modmap(self):
        reply = self.conn.core.GetModifierMapping().reply()
        modmap = {}
        names = (repeat(name, reply.keycodes_per_modifier) for name in ModMasks)
        for name, keycode in zip(chain.from_iterable(names), reply.keycodes):
            value = modmap.setdefault(name, [])
            value.append(keycode)
        self.modmap = modmap

    def get_modifier(self, keycode):
        """Return the modifier matching keycode"""
        for n, l in self.modmap.items():
            if keycode in l:
                return n
        return None

    def keysym_to_keycode(self, keysym):
        return self.first_sym_to_code.get(keysym, 0)

    def keycode_to_keysym(self, keycode, modifier):
        if keycode >= len(self.code_to_syms) or \
                modifier >= len(self.code_to_syms[keycode]):
            return 0
        return self.code_to_syms[keycode][modifier]

    def create_window(self, x, y, width, height):
        wid = self.conn.generate_id()
        self.conn.core.CreateWindow(
            self.default_screen.root_depth,
            wid,
            self.default_screen.root.wid,
            x, y, width, height, 0,
            WindowClass.InputOutput,
            self.default_screen.root_visual,
            CW.BackPixel | CW.EventMask,
            [
                self.default_screen.black_pixel,
                EventMask.StructureNotify | EventMask.Exposure
            ]
        )
        return Window(self, wid)

    def disconnect(self):
        try:
            self.conn.disconnect()
        except xcffib.ConnectionException:
            logger.error("Failed to disconnect, connection already failed?")
        self._connected = False

    def flush(self):
        if self._connected:
            return self.conn.flush()

    def xsync(self):
        # The idea here is that pushing an innocuous request through the queue
        # and waiting for a response "syncs" the connection, since requests are
        # serviced in order.
        self.conn.core.GetInputFocus().reply()

    def grab_server(self):
        return self.conn.core.GrabServer()

    def get_setup(self):
        return self.conn.get_setup()

    def open_font(self, name):
        fid = self.conn.generate_id()
        self.conn.core.OpenFont(fid, len(name), name)
        return Font(self, fid)

    def extensions(self):
        return set(
            i.name.to_string().lower()
            for i in self.conn.core.ListExtensions().reply().names
        )

    def fixup_focus(self):
        """
        If the X11 focus is set to None, all keypress events are discarded,
        which makes our hotkeys not work. This fixes up the focus so it is not
        None.
        """
        window = self.conn.core.GetInputFocus().reply().focus
        if window == xcffib.xproto.InputFocus._None:
            self.conn.core.SetInputFocus(
                xcffib.xproto.InputFocus.PointerRoot,
                xcffib.xproto.InputFocus.PointerRoot,
                xcffib.xproto.Time.CurrentTime,
            )


class Painter:
    def __init__(self, display):
        self.conn = xcffib.connect(display=display)
        self.setup = self.conn.get_setup()
        self.screens = [Screen(self, i) for i in self.setup.roots]
        self.default_screen = self.screens[self.conn.pref_screen]
        self.conn.core.SetCloseDownMode(xcffib.xproto.CloseDown.RetainPermanent)
        self.atoms = AtomCache(self)

    def paint(self, screen, image_path, mode=None):
        try:
            with open(image_path, 'rb') as f:
                image, _ = cairocffi.pixbuf.decode_to_image_surface(f.read())
        except IOError as e:
            logger.error('Wallpaper: %s' % e)
            return

        root_pixmap = self.default_screen.root.get_property(
            '_XROOTPMAP_ID', xcffib.xproto.Atom.PIXMAP, int
        )
        if not root_pixmap:
            root_pixmap = self.default_screen.root.get_property(
                'ESETROOT_PMAP_ID', xcffib.xproto.Atom.PIXMAP, int
            )
        if root_pixmap:
            root_pixmap = root_pixmap[0]
        else:
            root_pixmap = self.conn.generate_id()
            self.conn.core.CreatePixmap(
                self.default_screen.root_depth,
                root_pixmap,
                self.default_screen.root.wid,
                self.default_screen.width_in_pixels,
                self.default_screen.height_in_pixels,
            )

        for depth in self.default_screen.allowed_depths:
            for visual in depth.visuals:
                if visual.visual_id == self.default_screen.root_visual:
                    root_visual = visual
                    break
        surface = cairocffi.xcb.XCBSurface(
            self.conn, root_pixmap, root_visual,
            self.default_screen.width_in_pixels,
            self.default_screen.height_in_pixels,
        )

        context = cairocffi.Context(surface)
        with context:
            context.translate(screen.x, screen.y)
            if mode == 'fill':
                context.rectangle(0, 0, screen.width, screen.height)
                context.clip()
                image_w = image.get_width()
                image_h = image.get_height()
                width_ratio = screen.width / image_w
                if width_ratio * image_h >= screen.height:
                    context.scale(width_ratio)
                else:
                    height_ratio = screen.height / image_h
                    context.translate(
                        - (image_w * height_ratio - screen.width) // 2, 0
                    )
                    context.scale(height_ratio)
            elif mode == 'stretch':
                context.scale(
                    sx=screen.width / image.get_width(),
                    sy=screen.height / image.get_height(),
                )
            context.set_source_surface(image)
            context.paint()

        self.conn.core.ChangeProperty(
            xcffib.xproto.PropMode.Replace,
            self.default_screen.root.wid,
            self.atoms['_XROOTPMAP_ID'],
            xcffib.xproto.Atom.PIXMAP,
            32, 1, [root_pixmap]
        )
        self.conn.core.ChangeProperty(
            xcffib.xproto.PropMode.Replace,
            self.default_screen.root.wid,
            self.atoms['ESETROOT_PMAP_ID'],
            xcffib.xproto.Atom.PIXMAP,
            32, 1, [root_pixmap]
        )
        self.conn.core.ChangeWindowAttributes(
            self.default_screen.root.wid,
            xcffib.xproto.CW.BackPixmap, [root_pixmap]
        )
        self.conn.core.ClearArea(
            0, self.default_screen.root.wid, 0, 0,
            self.default_screen.width_in_pixels,
            self.default_screen.height_in_pixels
        )
        self.conn.flush()

    def __del__(self):
        self.conn.disconnect()


def get_keysym(key: str) -> int:
    keysym = keysyms.get(key)
    if not keysym:
        raise XCBQError("Unknown key: %s" % key)
    return keysym


def translate_modifiers(mask: int) -> typing.List[str]:
    r = []
    for k, v in ModMasks.items():
        if mask & v:
            r.append(k)
    return r


def translate_masks(modifiers: typing.List[str]) -> int:
    """
    Translate a modifier mask specified as a list of strings into an or-ed
    bit representation.
    """
    masks = []
    for i in modifiers:
        try:
            masks.append(ModMasks[i])
        except KeyError as e:
            raise XCBQError("Unknown modifier: %s" % i) from e
    if masks:
        return functools.reduce(operator.or_, masks)
    else:
        return 0
