#!/usr/bin/env python3
# *
# * Source based on xfce4-panel-profiles
# * https://gitlab.xfce.org/apps/xfce4-panel-profiles/-/blob/master/xfce4-panel-profiles/panelconfig.py
# *
# * Copyright 2013 Alistair Buxton <a.j.buxton@gmail.com>
# * Copyright 2019 Daniel Ruiz de Alegría <daniel@drasite.com>
# *
# * License: This program 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. 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.

import gi
gi.require_version("Gdk", "3.0")

from gi.repository import Gio, GLib, Gdk
import tarfile
import io
import time
import os
import re

# yes, python 3.2 has exist_ok, but it will still fail if the mode is different


def mkdir_p(path):
    try:
        os.makedirs(path, exist_ok=True)
    except FileExistsError:
        pass


def add_to_tar(t, bytes, arcname):
    ti = tarfile.TarInfo(name=arcname)
    ti.size = len(bytes)
    ti.mtime = time.time()
    f = io.BytesIO(bytes)
    t.addfile(ti, fileobj=f)


class DesktopConfig(object):

    def __init__(self):
        self.desktops = []
        self.channels = ['xfce4-panel', 'xfce4-desktop', 'xfce4-notifyd', 'xfwm4', 'xsettings']
        self.properties = {}
        self.source = None

    @classmethod
    def from_xfconf(cls, xfconf):
        dc = DesktopConfig()

        for channel in dc.channels:
            dc.properties[channel] = dc.read_xfconf_properties(xfconf, channel)

        dc.remove_panel_orphans()
        dc.find_desktops()

        dc.source = None

        return dc

    @classmethod
    def from_file(cls, filename):
        dc = DesktopConfig()

        dc.source = tarfile.open(filename, mode='r')

        for channel in dc.channels:
            file_name = channel.replace('xfce4-', '') + '.txt'
            dc.properties[channel] = dc.read_file_properties(file_name)

        dc.remove_panel_orphans()
        dc.find_desktops()

        return dc

    def read_xfconf_properties(self, xfconf, channel):
        result = xfconf.call_sync(
            'GetAllProperties',
            GLib.Variant('(ss)', (channel, '')), 0, -1, None)

        props = result.get_child_value(0)
        properties = {}

        for n in range(props.n_children()):
            p = props.get_child_value(n)
            pp = p.get_child_value(0).get_string()
            pv = p.get_child_value(1).get_variant()

            pn = GLib.Variant.parse(None, str(pv), None, None)
            assert(pv == pn)

            properties[pp] = pv

        return properties

    def read_file_properties(self, filename):
        properties = {}
        
        try:
            config = self.source.extractfile(filename)
            for line in config:
                try:
                    x = line.decode('utf-8').strip().split(' ', 1)
                    properties[x[0]] = GLib.Variant.parse(None, x[1], None, None)
                except:
                    pass
        except KeyError:
            pass

        return properties

    def remove_panel_orphans(self):
        plugin_ids = set()
        rem_keys = []

        for pp, pv in self.properties['xfce4-panel'].items():
            path = pp.split('/')
            if len(path) == 4 and path[0] == '' and path[1] == 'panels' and \
                    path[2].startswith('panel-') and path[3] == 'plugin-ids':
                plugin_ids.update(pv)

        for pp, pv in self.properties['xfce4-panel'].items():
            path = pp.split('/')
            if len(path) == 3 and path[0] == '' and path[1] == 'plugins' and \
                    path[2].startswith('plugin-'):
                number = path[2].split('-')[1]
                try:
                    if int(number) not in plugin_ids:
                        rem_keys.append('/plugins/plugin-' + number)
                except ValueError:
                    pass

        self.remove_keys(rem_keys)

    def check_desktop(self, path):
        try:
            f = self.get_desktop_source_file(path)
            bytes = f.read()
            f.close()
        except (KeyError, FileNotFoundError):
            return False

        # Check if binary exists
        keyfile = GLib.KeyFile.new()
        decoded = bytes.decode()
        if keyfile.load_from_data(decoded, len(decoded),
                                  GLib.KeyFileFlags.NONE):
            try:
                exec_str = keyfile.get_string("Desktop Entry", "Exec")
                if self.check_exec(exec_str):
                    return True
            except GLib.Error:  # pylint: disable=E0712
                pass #  https://bugzilla.xfce.org/show_bug.cgi?id=14597

        return False

    def find_desktops(self):
        rem_keys = []

        for pp, pv in self.properties['xfce4-panel'].items():
            path = pp.split('/')
            if len(path) == 3 and path[0] == '' and path[1] == 'plugins' and \
                    path[2].startswith('plugin-'):
                number = path[2].split('-')[1]
                if pv.get_type_string() == 's' and \
                        pv.get_string() == 'launcher':
                    for d in self.properties['xfce4-panel']['/plugins/plugin-' + number +
                                             '/items'].unpack():
                        desktop_path = 'launcher-' + number + '/' + d
                        if self.check_desktop(desktop_path):
                            self.desktops.append(desktop_path)
                        else:
                            rem_keys.append('/plugins/plugin-' + number)

        self.remove_keys(rem_keys)

    def remove_keys(self, rem_keys):
        keys = list(self.properties['xfce4-panel'].keys())
        for param in keys:
            for bad_plugin in rem_keys:
                if param == bad_plugin or param.startswith(bad_plugin+'/'):
                    try:
                        del self.properties['xfce4-panel'][param]
                    except KeyError:
                        pass #  https://bugzilla.xfce.org/show_bug.cgi?id=14934

    def get_desktop_source_file(self, desktop):
        if getattr(self, 'source', None) is None:
            path = os.path.join(
                GLib.get_home_dir(), '.config/xfce4/panel/', desktop)
            return open(path, 'rb')
        else:
            return self.source.extractfile(desktop)

    def to_file(self, filename):
        if filename.endswith('.gz'):
            mode = 'w:gz'
        elif filename.endswith('.bz2'):
            mode = 'w:bz2'
        else:
            mode = 'w'
        t = tarfile.open(name=filename, mode=mode)
        for channel in self.channels:
            text = self.get_properties_text(self.properties[channel])
            file_name = channel.replace('xfce4-', '') + '.txt'
            add_to_tar(t, '\n'.join(text).encode('utf8'), file_name)

        for d in self.desktops:
            bytes = self.get_desktop_source_file(d).read()
            add_to_tar(t, bytes, d)

        t.close()

    def get_properties_text(self, properties):
        props_tmp = []
        for (pp, pv) in sorted(properties.items()):
            props_tmp.append(str(pp) + ' ' + str(pv))
        return props_tmp

    def check_exec(self, program):
        program = program.strip()
        if len(program) == 0:
            return False

        params = list(GLib.shell_parse_argv(program)[1])
        executable = params[0]

        if os.path.exists(executable):
            return True

        path = GLib.find_program_in_path(executable)
        if path is not None:
            return True

        return False

    def to_xfconf(self, xfconf):
        self.configure_desktop_monitors(xfconf)

        for channel in self.channels:
            self.set_properties(xfconf, channel)

        session_bus = Gio.BusType.SESSION
        conn = Gio.bus_get_sync(session_bus, None)
        destination = 'org.xfce.Panel'
        path = '/org/xfce/Panel'
        interface = destination
        dbus_proxy = Gio.DBusProxy.new_sync(conn, 0, None, destination, path, interface, None)

        if dbus_proxy is not None:
            panel_path = os.path.join(
                GLib.get_home_dir(), '.config/xfce4/panel/')
            for d in self.desktops:
                bytes = self.get_desktop_source_file(d).read()
                mkdir_p(panel_path + os.path.dirname(d))
                f = open(panel_path + d, 'wb')
                f.write(bytes)
                f.close()

            try:
                dbus_proxy.call_sync('Terminate', GLib.Variant('(b)', ('xfce4-panel',)), 0, -1, None)
            except GLib.GError:  # pylint: disable=E0712
                pass

    def configure_desktop_monitors(self, xfconf):
        fallback_monitor = '/backdrop/screen0/monitor0/'
        fallback_monitor_properties = {}
        prev_desktop_properties = self.read_xfconf_properties(xfconf, 'xfce4-desktop')

        for pp in self.properties['xfce4-desktop']:
            if re.match('^' + fallback_monitor + r'[^/\s]+$', pp):
                fallback_monitor_properties[pp.replace(fallback_monitor, '')] = \
                        self.properties['xfce4-desktop'][pp]

        for pp in prev_desktop_properties:
            w = re.search(r'^/backdrop/screen\d/monitor[^/]+/workspace\d/', pp)
            if w:
                for fpp in fallback_monitor_properties:
                    new_pp = w.group(0) + fpp
                    if new_pp not in self.properties['xfce4-desktop']:
                        self.properties['xfce4-desktop'][new_pp] = \
                                fallback_monitor_properties[fpp]

        # Fix desktop not changing if wallpaper never modified
        display = Gdk.Display.get_default()

        for d in range(display.get_n_monitors()):
            monitor = display.get_monitor(d).get_model()
            if monitor:
                for fpp in fallback_monitor_properties:
                    new_pp = f'/backdrop/screen0/monitor{monitor}/workspace0/' + fpp
                    if new_pp not in self.properties['xfce4-desktop']:
                        self.properties['xfce4-desktop'][new_pp] = \
                                fallback_monitor_properties[fpp]

    def set_properties(self, xfconf, channel):
        # Reset all properties to make sure old settings are invalidated
        try:
            xfconf.call_sync('ResetProperty', GLib.Variant(
                '(ssb)', (channel, '/', True)), 0, -1, None)
        except GLib.Error:  # pylint: disable=E0712
            pass

        for (pp, pv) in sorted(self.properties[channel].items()):
            xfconf.call_sync('SetProperty', GLib.Variant(
                '(ssv)', (channel, pp, pv)), 0, -1, None)


