#!/usr/bin/env python3
"""A simple windowswitcher for sway"""
# switch_window, get_windows and extract_nodes_iterative is derived from
# https://github.com/tobiaspc/wofi-scripts Copyright (c) 2020 Tobi which
# is covered by the MIT License. However this program is licensed under
# the GPLv3+ Copyright (c) 2020 Micke Nordin <hej@mic.ke>.
# See LICENSE file for details.

import json
import math
import os
import subprocess
from io import BytesIO

import cairosvg
import wx


class SwaySwitch(wx.Frame):  # pylint: disable=no-member
    """Frame for the swayswitcher"""
    def __init__(self, *args, **kw):  # pylint: disable=unused-argument
        """Constructor"""
        wx.Frame.__init__(self, None, title="", style=wx.STAY_ON_TOP)  # pylint: disable=no-member
        # create a panel in the frame
        self.pnl = wx.Panel(self)  # pylint: disable=no-member

        # Some xdg data
        self.home = os.environ.get('HOME')
        self.base_dirs = self.get_base_dirs()
        # get windows from sway
        windows = get_windows()

        # Get config
        self.label_len = 20
        self.icon_size = 128

        self.icon_dirs = self.get_icon_dirs()

        # and create a sizer to manage the layout of child widgets
        x_and_y = int(math.sqrt(len(windows)) + 0.5)
        self.sizer = wx.GridSizer(x_and_y)  # pylint: disable=no-member
        self.pnl.SetSizer(self.sizer)
        self.set_buttons(windows)
        self.sizer.Fit(self)
        self.sizer.Layout()

    def get_base_dirs(self):
        """We want to follow the XDG standard if possible"""
        base_dirs = []
        xdg_dirs = os.environ.get('XDG_DATA_DIRS')
        pos_dirs = []
        if xdg_dirs:
            pos_dirs = xdg_dirs.split(":")
        if not pos_dirs:
            pos_dirs = [
                "/usr/share", "/usr/local/share", self.home + "/.local/share",
                self.home + "/.local/share/flatpak/exports/share",
                "/var/lib/flatpak/exports/share"
            ]
        for pos_dir in pos_dirs:
            if os.path.exists(pos_dir):
                base_dirs.append(pos_dir)
        return base_dirs

    def get_desktop_file(self, command):
        """From here we return first dektopfile"""
        desktop_file = None
        for base_dir in self.base_dirs:
            directory = base_dir + "/applications"
            if os.path.exists(directory):
                command = "grep -s -l {} {}/*.desktop".format(
                    command, directory)
                for data in [i for i in run_command(command) if i]:
                    for result in data.decode().split('\n'):
                        desktop_file = result
                        break
        return desktop_file

    def get_icon_bitmap(self, icon):
        """Create a bitmap with right size from svg or png"""
        if icon:
            if icon.endswith(".svg"):
                svgpng = cairosvg.svg2png(
                    bytestring=open(icon).read().encode('utf-8'))
                image = wx.Image(BytesIO(svgpng), wx.BITMAP_TYPE_PNG)  # pylint: disable=no-member
            else:
                unscaled_bitmap = wx.Bitmap(icon)  # pylint: disable=no-member
                image = unscaled_bitmap.ConvertToImage()
            image = image.Scale(self.icon_size, self.icon_size,
                                wx.IMAGE_QUALITY_HIGH)  # pylint: disable=no-member
            bitmap = wx.Bitmap(image)  # pylint: disable=no-member
        else:
            bitmap = wx.Bitmap()  # pylint: disable=no-member
        return bitmap

    def get_icon_path(self, desktop_file):
        """Glue function to get icon path"""
        icon_name = get_icon_name(desktop_file)
        pos_icons = self.get_pos_icons(icon_name)
        icon_path = self.get_right_size_icon(pos_icons)
        return icon_path

    def get_icon_dirs(self):
        """Find icon dirs"""
        icon_dirs = []
        for base_dir in [
                directory for directory in self.base_dirs if directory
        ]:
            directory = base_dir + "/icons"
            icon_dirs.append(directory)
        icon_dirs.append(self.home + "/.icons")
        icon_dirs.append("/usr/share/pixmaps")

        return icon_dirs

    def get_label(self, window):
        """Construct the label for the window"""
        try:
            label = window['window_properties']['class']
        except KeyError:
            try:
                label = window['app_id']
                if len(label) < self.label_len:
                    label = label + ':' + window['name'][:self.label_len -
                                                         len(label)]
            except KeyError:
                label = window['name']
            if len(label) > self.label_len:
                label = label[:self.label_len]

        label = "ws" + str(window['workspace']) + ": " + label
        return label

    def get_pos_icons(self, icon_name):
        """Collect possible icons, prefer svg"""
        possible_icons = []
        for icon_dir in [
                directory for directory in self.icon_dirs if directory
        ]:
            if os.path.exists(icon_dir):
                command = "find {} -name *{}.svg".format(icon_dir, icon_name)
                result = run_command(command)[0].split()
                if not result:
                    command = "find {} -name *{}.png".format(
                        icon_dir, icon_name)
                    result = run_command(command)[0].split()
                for data in result:
                    icon_cand = data.decode()
                    if icon_cand not in possible_icons:
                        possible_icons.append(icon_cand)
        return possible_icons

    def get_right_size_icon(self, possible_icons):
        """Try to find prefered size"""
        icon = None
        for pos_icon in possible_icons:
            if pos_icon.endswith(".svg"):
                icon = pos_icon
                break
            if not icon and (str(self.icon_size) + "x" + str(self.icon_size)
                             in pos_icon):
                icon = pos_icon
        if not icon:
            try:
                icon = sorted(possible_icons)[0]
            except IndexError:
                icon = ""
        return icon

    def on_key_press(self, event):
        """Intercept esc key press"""
        keycode = event.GetUnicodeKey()
        if keycode == wx.WXK_ESCAPE:  # pylint: disable=no-member
            # enter normal mode and exit
            command = 'swaymsg mode "default"'
            run_command(command)
            self.Close(True)
        else:
            event.Skip(True)

    def set_buttons(self, windows):
        """Loop over windows and create buttons for them"""
        for window in windows:
            # This is setting up an inner sizer with a static text label and an image icon
            inner_sizer = wx.BoxSizer(orient=wx.VERTICAL)  # pylint: disable=no-member
            label = self.get_label(window)
            winid = window['id']
            size = wx.Window.GetFont(self).GetPointSize() * self.label_len  # pylint: disable=no-member
            command = get_command(window['pid'])
            desktop_file = self.get_desktop_file(command)
            icon_path = self.get_icon_path(desktop_file)
            bitmap = self.get_icon_bitmap(icon_path)
            btn = wx.BitmapButton(  # pylint: disable=no-member
                self.pnl,
                id=winid,
                bitmap=bitmap,
                size=wx.Size(size, size))  # pylint: disable=no-member
            btn.Bind(
                wx.EVT_BUTTON,
                lambda event, mwinid=winid: self.switch_window(event, mwinid))
            # Set up esc keybinding
            self.Bind(wx.EVT_CHAR_HOOK, lambda event: self.on_key_press(event))  # pylint: disable=unnecessary-lambda
            statictext = wx.StaticText(self.pnl, -1, label)  # pylint: disable=no-member
            inner_sizer.Add(statictext, 0, wx.ALIGN_CENTER)  # pylint: disable=no-member
            inner_sizer.Add(btn, 0, wx.ALIGN_CENTER)  # pylint: disable=no-member
            inner_sizer.Fit(self)
            inner_sizer.Layout()
            self.sizer.Add(inner_sizer, 0, wx.ALIGN_CENTER)  # pylint: disable=no-member

    def switch_window(self, event, winid):  # pylint: disable=unused-argument
        """Switches the focus to the given id and enter normalmode"""
        command = 'swaymsg [con_id={}] focus, mode "default"'.format(winid)
        run_command(command)
        self.Close(True)


def get_command(pid):
    """Returns a list of all json window objects"""
    command = "ps h c -p {} -o command".format(pid)
    result = run_command(command)[0].rstrip().decode()
    return result


def get_icon_name(desktop_file):
    """ Find icon from a desktop file"""
    command = "grep Icon= {}".format(desktop_file)
    for data in [i.rstrip() for i in run_command(command) if i]:
        try:
            icon_name = data.decode().split('=')[1]
            return icon_name
        except IndexError:
            return ""


def get_windows():
    """Returns a list of all json window objects"""
    command = "swaymsg -t get_tree"
    data = json.loads(run_command(command)[0])

    # Select outputs that are active
    windows = []
    for output in data['nodes']:

        # The scratchpad (under __i3) is not supported
        if output.get('name') != '__i3' and output.get('type') == 'output':
            workspaces = output.get('nodes')
            for workspace in workspaces:
                if workspace.get('type') == 'workspace':
                    windows += extract_nodes_iterative(workspace)
    return windows


def extract_nodes_iterative(workspace):
    """Extracts all windows from a sway workspace json object"""
    all_nodes = []

    floating_nodes = workspace.get('floating_nodes')

    for floating_node in floating_nodes:
        floating_node['workspace'] = workspace['num']
        all_nodes.append(floating_node)

    nodes = workspace.get('nodes')

    for node in nodes:

        # Leaf node
        if len(node.get('nodes')) == 0:
            node['workspace'] = workspace['num']
            all_nodes.append(node)
        # Nested node, handled iterative
        else:
            for inner_node in node.get('nodes'):
                inner_node['workspace'] = workspace['num']
                nodes.append(inner_node)

    return all_nodes


def run_command(command):
    """Run a command on system and capture result"""
    process = subprocess.Popen(command,
                               shell=True,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE)
    result = process.communicate()
    return result


# Entry point
if __name__ == "__main__":
    # When this module is run (not imported) then create the app, the
    # frame, show it, and start the event loop.
    app = wx.App()
    frm = SwaySwitch(None, title="")
    frm.Show()
    app.MainLoop()
