#!/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,too-many-locals,too-many-branches,too-many-statements
        """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 = []
        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):
                self.base_dirs.append(pos_dir)

        # get windows from sway
        windows = get_windows()
        label_len = 20

        # Icon size
        self.icon_size = 128

        # and create a sizer to manage the layout of child widgets
        x_and_y = int(math.sqrt(len(windows)) + 0.5)
        sizer = wx.GridSizer(x_and_y)  # pylint: disable=no-member
        self.pnl.SetSizer(sizer)
        for window in windows:
            inner_sizer = wx.BoxSizer(orient=wx.VERTICAL)  # pylint: disable=no-member
            try:
                label = window['window_properties']['class']
            except KeyError:
                try:
                    label = window['app_id']
                    if len(label) < label_len:
                        label = label + ':' + window['name'][:label_len -
                                                             len(label)]
                except KeyError:
                    label = window['name']
                if len(label) > label_len:
                    label = label[:label_len]

            # This is setting up an inner sizer with a static text label and an image icon
            label = "ws" + str(window['workspace']) + ": " + label
            winid = window['id']
            size = wx.Window.GetFont(self).GetPointSize() * label_len  # pylint: disable=no-member
            command = get_command(window['pid'])
            desktop_file = self.get_desktop_file(command)
            icon = self.get_icon(desktop_file)  # pylint: disable=no-member
            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(self.get_icon(desktop_file))  # 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
            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()
            sizer.Add(inner_sizer, 0, wx.ALIGN_CENTER)  # pylint: disable=no-member
        sizer.Fit(self)
        sizer.Layout()

    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)
                process = subprocess.Popen(command,
                                           shell=True,
                                           stdout=subprocess.PIPE,
                                           stderr=subprocess.PIPE)
                for data in [i for i in process.communicate() if i]:
                    for result in data.decode().split('\n'):
                        desktop_file = result
                        break
        return desktop_file

    def get_icon(self, desktop_file): # pylint: disable=too-many-branches
        """ Find icon from a desktop file"""
        command = "grep Icon= {}".format(desktop_file)
        process = subprocess.Popen(command,
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
        for data in [i.rstrip() for i in process.communicate() if i]:
            try:
                icon_name = data.decode().split('=')[1]
            except IndexError:
                return ""
        # 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")
        possible_icons = []
        for icon_dir in [directory for directory in icon_dirs if directory]:
            if os.path.exists(icon_dir):
                command = "find {} -name *{}.svg".format(icon_dir, icon_name)
                process = subprocess.Popen(command,
                                           shell=True,
                                           stdout=subprocess.PIPE,
                                           stderr=subprocess.PIPE)
                result = process.communicate()[0].split()
                if not result:
                    command = "find {} -name *{}.png".format(
                        icon_dir, icon_name)
                    process = subprocess.Popen(command,
                                               shell=True,
                                               stdout=subprocess.PIPE,
                                               stderr=subprocess.PIPE)
                    result = process.communicate()[0].split()
                for data in result:
                    icon_cand = data.decode()
                    if icon_cand not in possible_icons:
                        possible_icons.append(icon_cand)
        # 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"'
            subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
            self.Close(True)
        else:
            event.Skip(True)

    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)
        subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
        self.Close(True)


def get_command(pid):
    """Returns a list of all json window objects"""
    command = "ps h c -p {} -o command".format(pid)
    process = subprocess.Popen(command,
                               shell=True,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE)
    result = process.communicate()[0].rstrip().decode()
    return result


def get_windows():
    """Returns a list of all json window objects"""
    command = "swaymsg -t get_tree"
    process = subprocess.Popen(command,
                               shell=True,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE)
    data = json.loads(process.communicate()[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


# 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()
