import collections

import libqtile.hook
from libqtile.backend.base import Static
from libqtile.config import Group, Key, Rule
from libqtile.lazy import lazy
from libqtile.log_utils import logger


def simple_key_binder(mod, keynames=None):
    """Bind keys to mod+group position or to the keys specified as second argument"""

    def func(dgroup):
        # unbind all
        for key in dgroup.keys[:]:
            dgroup.qtile.ungrab_key(key)
            dgroup.qtile.config.keys.remove(key)
            dgroup.keys.remove(key)

        if keynames:
            keys = keynames
        else:
            # keys 1 to 9 and 0
            keys = list(map(str, list(range(1, 10)) + [0]))

        # bind all keys
        for keyname, group in zip(keys, dgroup.qtile.groups):
            name = group.name
            key = Key([mod], keyname, lazy.group[name].toscreen())
            key_s = Key([mod, "shift"], keyname, lazy.window.togroup(name))
            key_c = Key([mod, "control"], keyname, lazy.group.switch_groups(name))
            dgroup.keys.extend([key, key_s, key_c])
            dgroup.qtile.config.keys.extend([key, key_s, key_c])
            dgroup.qtile.grab_key(key)
            dgroup.qtile.grab_key(key_s)
            dgroup.qtile.grab_key(key_c)

    return func


class DGroups:
    """Dynamic Groups"""

    def __init__(self, qtile, dgroups, key_binder=None, delay=1):
        self.qtile = qtile

        self.groups = dgroups
        self.groups_map = {}

        self.rules = []
        self.rules_map = {}
        self.last_rule_id = 0

        for rule in getattr(qtile.config, "dgroups_app_rules", []):
            self.add_rule(rule)

        self.keys = []

        self.key_binder = key_binder

        self._setup_hooks()
        self._setup_groups()

        self.delay = delay

        self.timeout = {}

    def add_rule(self, rule, last=True):
        rule_id = self.last_rule_id
        self.rules_map[rule_id] = rule
        if last:
            self.rules.append(rule)
        else:
            self.rules.insert(0, rule)
        self.last_rule_id += 1
        return rule_id

    def remove_rule(self, rule_id):
        rule = self.rules_map.get(rule_id)
        if rule:
            self.rules.remove(rule)
            del self.rules_map[rule_id]
        else:
            logger.warning('Rule "%s" not found', rule_id)

    def add_dgroup(self, group, start=False):
        self.groups_map[group.name] = group
        rule = Rule(group.matches, group=group.name)
        self.rules.append(rule)
        if start:
            self.qtile.add_group(
                group.name,
                group.layout,
                group.layouts,
                group.label,
                screen_affinity=group.screen_affinity,
            )

    def _setup_groups(self):
        def launch_app(app, group):
            def launcher():
                self.qtile.spawn(app, group=group)

            return launcher

        for group in self.groups:
            self.add_dgroup(group, group.init)

            if group.spawn and not self.qtile.no_spawn:
                if isinstance(group.spawn, str):
                    spawns = [group.spawn]
                else:
                    spawns = group.spawn
                for spawn in spawns:
                    libqtile.hook.subscribe.startup_once(launch_app(spawn, group=group.name))

    def _setup_hooks(self):
        libqtile.hook.subscribe.addgroup(self._addgroup)
        libqtile.hook.subscribe.client_new(self._add)
        libqtile.hook.subscribe.client_killed(self._del)
        if self.key_binder:
            libqtile.hook.subscribe.setgroup(lambda: self.key_binder(self))
            libqtile.hook.subscribe.changegroup(lambda: self.key_binder(self))

    def _addgroup(self, group_name):
        if group_name not in self.groups_map:
            self.add_dgroup(Group(group_name, persist=False))

    def _add(self, client):
        if client in self.timeout:
            logger.debug("Remove dgroup source")
            self.timeout.pop(client).cancel()

        # ignore static windows
        if isinstance(client, Static):
            return

        # ignore windows whose groups is already set (e.g. from another hook or
        # when it was set on state restore)
        if client.group is not None:
            return

        group_set = False
        intrusive = False

        delete_rules = []

        for rule in self.rules:
            # Matching Rules
            if rule.matches(client):
                if rule.group:
                    if rule.group in self.groups_map:
                        layout = self.groups_map[rule.group].layout
                        layouts = self.groups_map[rule.group].layouts
                        label = self.groups_map[rule.group].label
                    else:
                        layout = None
                        layouts = None
                        label = None
                    group_added = self.qtile.add_group(rule.group, layout, layouts, label)
                    client.togroup(rule.group)

                    group_set = True

                    group_obj = self.qtile.groups_map[rule.group]
                    group = self.groups_map.get(rule.group)
                    if group and group_added:
                        for k, v in list(group.layout_opts.items()):
                            if isinstance(v, collections.abc.Callable):
                                v(group_obj.layout)
                            else:
                                setattr(group_obj.layout, k, v)
                        affinity = group.screen_affinity
                        if affinity and len(self.qtile.screens) > affinity:
                            self.qtile.screens[affinity].set_group(group_obj)

                if rule.float:
                    client.enable_floating()

                if rule.intrusive:
                    intrusive = rule.intrusive

                if rule.one_time:
                    delete_rules.append(rule)

                if rule.break_on_match:
                    break

        if delete_rules:
            ids_to_delete = [
                rule_id for rule_id, rule in self.rules_map.items() if rule in delete_rules
            ]
            for rule_id in ids_to_delete:
                self.remove_rule(rule_id)

        # If app doesn't have a group
        if not group_set:
            current_group = self.qtile.current_group.name
            if (
                current_group in self.groups_map
                and self.groups_map[current_group].exclusive
                and not intrusive
            ):
                wm_class = client.get_wm_class()

                if wm_class:
                    if len(wm_class) > 1:
                        wm_class = wm_class[1]
                    else:
                        wm_class = wm_class[0]

                    group_name = wm_class
                else:
                    group_name = client.name or "Unnamed"

                self.add_dgroup(Group(group_name, persist=False), start=True)
                client.togroup(group_name)
        self.sort_groups()

    def sort_groups(self):
        grps = self.qtile.groups
        sorted_grps = sorted(grps, key=lambda g: self.groups_map[g.name].position)
        if grps != sorted_grps:
            self.qtile.groups = sorted_grps
            libqtile.hook.fire("changegroup")

    def _del(self, client):
        # ignore static windows
        if isinstance(client, Static):
            return

        group = client.group

        def delete_client():
            # Delete group if empty and don't persist
            if (
                group
                and group.name in self.groups_map
                and not self.groups_map[group.name].persist
                and len(group.windows) <= 0
            ):
                self.qtile.delete_group(group.name)
                self.sort_groups()
            del self.timeout[client]

        if group is not None and group.persist:
            return

        logger.debug("Deleting %s in %ss", group, self.delay)
        if client not in self.timeout:
            self.timeout[client] = self.qtile.call_later(self.delay, delete_client)
