#!/usr/bin/env python3
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Gdk", "3.0")
from gi.repository import Gtk, Gdk, GObject
import cairo
import wprviews_tools as pv
import os
import subprocess
import sys
from threading import Thread
import time


"""
Budgie WindowPreviews
Author: Jacob Vlijm
Copyright=Copyright © 2017-2018 Ubuntu Budgie Developers
Website=https://ubuntubudgie.org
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 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. You
should have received a copy of the GNU General Public License along with this
program.  If not, see <http://www.gnu.org/licenses/>.
"""


css_data = """
.windowbutton {
  border-width: 4px;
  border-color: #979987;
  background-color: #505050;
  padding: 4px;
  border-radius: 3px;
  -gtk-icon-effect: none;
  border-style: solid;
}
.windowbutton:hover {
  border-color: white;
  background-color: #505050;
  border-width: 2px;
  padding: 6px;
  border-radius: 3px;
  -gtk-icon-effect: highlight;
  border-style: solid;
}
.windowbutton:focus {
  border-color: white;
  background-color: #505050;
  border-width: 5px;
  padding: 3px;
}
.wsbutton {
  border-color: white;
  border-width: 0px;
  padding-left: 2px;
  padding-right: 2px;
}
.closebutton {
   border-width: 0px;
   padding: 3px;
}
.closebutton:hover {
   border-radius: 3px;
   padding: 3px;
    background-color: transparent;
}
.label {
  color: white;
  padding-bottom: 0px;
}
.labelbottom {
  color: white;
  padding: 0px;
}
"""


# paths
pics_path = pv.previews
area = pv.get_area()
maxrow = int(area / 380)
show_allwins = True
plugin_path = pv.plugin_path
button_images = os.path.join(plugin_path, "pics")
button_images = button_images.replace("lib", "share")
trigger = os.path.join("/tmp", pv.user + "_wprviews_trigger")


try:
    os.remove(trigger)
except FileNotFoundError:
    pass


"""
Gtk alignment isn't fully according the set margins The deviation below is
the result of trial & error, fix for 3 columns on small screens.
"""


args = sys.argv[1:]
# current viewport
currws = pv.get_ws()
# fix spacing for different n-rows
deviation = 5 if maxrow == 3 else 0
# read all windows from the previews folder
wins = [w.split(".") for w in os.listdir(pics_path)]
# latest check on recen situation, filter out obsolete wins
win_update = pv.get(["wmctrl", "-l"])
if win_update:
    wins = [w for w in wins if w[0] in win_update]
# see if only windows of the current wm-class should be picked out
if "current" in args:
    show_allwins = False
    curr = pv.get_activeclass()
    wins = [w for w in wins if pv.show_wmclass(w[0]) == curr]


# check how the windows are spread over the workspaces
spread = sorted([int(n) for n in [w[-2] for w in wins]])
# pick only windows of the current workspace. the ws is part of the name
wins = [w[:-1] for w in wins if w[-2] == currws]
# check for previously focussed
temp_history = pv.temp_history


class PreviewsWin(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self, title="PrVflash_prvwin")
        self.shapers = []
        # window props, bindings
        self.provider = Gtk.CssProvider.new()
        self.provider.load_from_data(css_data.encode())
        self.set_decorated(False)
        self.nokeys = "nokeys" in args
        ###
        self.connect("focus-out-event", self.on_focus_out)
        ###
        self.connect("key-release-event", self.get_key)
        self.connect("destroy", Gtk.main_quit)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.set_skip_taskbar_hint(True)
        self.props.border_width = 30
        self.set_focus()
        # book keeping
        self.buttondata = []
        self.curr_focus = 0
        self.age = 0
        self.button_index = 0
        # transparency
        screen = self.get_screen()
        visual = screen.get_rgba_visual()
        if all([visual, screen.is_composited()]):
            self.set_visual(visual)
        self.set_app_paintable(True)
        self.connect("draw", self.area_draw)
        # main grid
        self.maingrid = Gtk.Grid()
        self.add(self.maingrid)
        # distance between rows
        self.maingrid.set_row_spacing(0)
        # window-previews
        width = 0
        row = 0
        rowlist = []
        # create first row
        self.curr_row = Gtk.Grid()
        self.curr_row.set_column_spacing(20)
        self.maingrid.attach(self.curr_row, 0, 0, 1, 1)
        # get possible previously active window
        active = self.get_curractive()
        self.previous = self.get_previous(active)
        try:
            open(temp_history, "wt").write(active)
        except TypeError:
            pass
        active = self.previous or active
        self.focussed = None
        # create rows
        for i in range(n_wins):
            # next row if the previous one is filled
            if width >= maxrow:
                row = row + 1
                self.curr_row = Gtk.Grid()
                self.curr_row.set_column_spacing(20)
                self.maingrid.attach(self.curr_row, 0, row, 1, 1)
                width = 0
            # button + styling
            win = wins[i]
            w_id = win[0]
            # first see if the window still exists:
            nm = pv.get_wmname(w_id)
            if nm:
                button = self.create_button(win, w_id, active)
                win_name = self.create_label(nm)
                self.bgrid = self.create_buttonset(button, win_name, w_id)
                self.curr_row.attach(self.bgrid, i + 1, 0, 1, 1)
                width = width + 1
            last_row = width
        # if there are no windows, make sure last_row exists/
        # fill up the empty space
        if n_wins == 0:
            last_row = 5  # random value, make sure we can add the spacers
            v_space = self.v_spacer(163)
            self.maingrid.attach(v_space, 0, 0, 1, 1)
            h_space = self.h_spacer(280)
            self.maingrid.attach(h_space, 0, 1, 1, 1)
            # add space on the left of the last row if it isn't a full one
        if all([row != 0, last_row != maxrow]):
            add = ((maxrow - last_row) * 290 / 2) - (last_row * 5) - deviation
            add = self.h_spacer(add)
            self.curr_row.attach(add, 0, 0, 1, 1)
        # set the navigation section
        if show_allwins:
            try:
                wsbuttons = self.set_crosswspaces()
                self.maingrid.attach(wsbuttons, 0, row + 1, 1, 1)
            except TypeError:
                pass
        # set focus on the (possibly) currently active window
        try:
            self.focussed.grab_focus()
        except AttributeError:
            pass
        self.timer = Thread(target=self.wait)
        self.timer.setDaemon(True)
        self.timer.start()
        self.maingrid.show_all()
        self.show_all()
        self.set_keep_above(True)

    def on_focus_out(self, *args):
        if self.nokeys:
            self.stop()
        else:
            self.always_above()

    def always_above(self, *args):
        try:
            subprocess.Popen(["wmctrl", "-a", "PrVflash_prvwin"])
        except subprocess.CalledProcessError:
            pass

    def get_previous(self, active):
        try:
            prev = open(temp_history).read().strip()
        except FileNotFoundError:
            pass
        else:
            return prev if all([
                prev in [w[0] for w in wins],
                prev != active,
            ]) else None

    def setfocus(self, nf=False):
        self.curr_focus = self.curr_focus + 1
        try:
            newfocus = self.buttondata[self.curr_focus]
        except IndexError:
            self.curr_focus = 0
            try:
                newfocus = self.buttondata[0]
            except IndexError:
                pass
        try:
            GObject.idle_add(
                self.set_newfocus, newfocus,
                priority=GObject.PRIORITY_DEFAULT
            )
            if nf:
                return newfocus
        except UnboundLocalError:
            pass

    def get_curractive(self):
        # get the active window on startup of the overview
        active = pv.get(["xdotool", "getactivewindow"])
        return pv.get_hex(active) if active else None

    def wait(self):
        """
        wait loop; see if trigger file pops up, or we need to quit on immediate
        key release
        """
        newfocus = self.setfocus(nf=True)
        first = True if not self.nokeys else False
        while True:
            time.sleep(0.1)
            if first:
                if not self.key_checker():
                    # try/except, in case no windows on workspace
                    try:
                        self.activate(newfocus[1])
                    except TypeError:
                        pass
                    self.stop()
                else:
                    first = False
            self.age = self.age + 1
            if os.path.exists(trigger):
                self.setfocus()
                os.remove(trigger)

    def set_newfocus(self, button):
        button[0].grab_focus()
        self.maingrid.show_all()

    def get_key(self, button, val):
        # keybinding to close the previews
        key = "Escape" if self.nokeys else "Alt_L"
        if Gdk.keyval_name(val.keyval) == key:
            if not self.nokeys:
                try:
                    w = self.buttondata[self.curr_focus][1]
                    self.activate(w)
                except IndexError:
                    pass
            self.stop()

    def change_onenter(self, button, event, target, image):
        new_image = Gtk.Image.new_from_file(image)
        target.set_image(new_image)

    def change_onleave(self, button, event, target, image):
        # this one should be combined with the function above,
        # but need to find out how to identify a Gdk.EventCrossing object
        new_image = Gtk.Image.new_from_file(image)
        target.set_image(new_image)

    def show_other(self, button, nxt):
        # call the external script to move to the other workspace
        add = "nokeys" if self.nokeys else ""
        subprocess.Popen(
            [os.path.join(plugin_path, "moveto.sh"), str(nxt), add]
        )
        self.stop()

    def set_crosswspaces(self):
        # create the navigation section
        try:
            ws_data = pv.get(["wmctrl", "-d"]).splitlines()
        except AttributeError:
            pass
        else:
            wsb_subgrid = Gtk.Grid()
            n_ws = len(ws_data)
            for i in range(n_ws):
                index_str = str(i)
                # create the label, displaying the number of windows
                # on workspace [i]
                wslabel = self.create_wslabel(i)
                wsb_subgrid.attach(wslabel, i, 1, 1, 1)
                # workspace switch- button
                button = self.create_wsbutton(index_str, currws)
                wsb_subgrid.attach(button, i, 0, 1, 1)
                # add functionality to the button
                if index_str != currws:
                    button.connect("clicked", self.show_other, i)
                else:
                    self.set_hover(button)
            wsbuttons = Gtk.ButtonBox()
            wsbuttons.pack_end(wsb_subgrid, False, False, 0)
            return wsbuttons

    def set_hover(self, button):
        # change image to replace the white icon on hover
        # on current workspace' button
        new_image = os.path.join(
            button_images,
            "prv_grey.png",
        )
        target_button = button
        button.connect(
            "enter-notify-event",
            self.change_onenter,
            target_button, new_image,
        )
        orig_image = os.path.join(
            button_images,
            "prv_white.png",
        )
        target_button = button
        button.connect(
            "leave-notify-event",
            self.change_onleave,
            target_button, orig_image,
        )
        return button

    def create_button(self, win, w_id, active):
        # create the preview button
        button = Gtk.Button()
        button.connect("clicked", self.activate, w_id)
        button.set_size_request(280, 180)
        img = os.path.join(pics_path, ".".join(win) + ".jpg")
        win_img = Gtk.Image.new_from_file(img)
        button.set_image(win_img)
        st_cont = button.get_style_context()
        st_cont.add_class("windowbutton")
        st_cont.remove_class("image-button")
        Gtk.StyleContext.add_provider(
            st_cont,
            self.provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
        )
        if w_id == active:
            self.focussed = button
            add = -1 if self.previous else 0
            self.curr_focus = self.button_index + add
        self.button_index = self.button_index + 1
        self.buttondata.append([button, w_id])
        return button

    def create_wsbutton(self, index_str, currws):
        # create the navigation button
        button = Gtk.Button()
        wsbutton_cont = button.get_style_context()
        wsbutton_cont.add_class("wsbutton")
        Gtk.StyleContext.add_provider(
            wsbutton_cont,
            self.provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
        )
        im_name = "prv_white" if index_str == currws else "prv_grey"
        im = Gtk.Image.new_from_file(
            os.path.join(button_images, im_name + ".png")
        )
        button.set_image(im)
        button.set_can_focus(False)
        button.set_relief(Gtk.ReliefStyle.NONE)
        return button

    def create_label(self, nm):
        # create window title button
        nm = nm if len(nm) < 30 else nm[:27] + "..."
        win_name = Gtk.Label(nm)
        win_name.set_xalign(0)
        label_cont = win_name.get_style_context()
        label_cont.add_class("label")
        Gtk.StyleContext.add_provider(
            label_cont,
            self.provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
        )
        return win_name

    def create_wslabel(self, index):
        # create label to show n-workspaces (navigation bar)
        l_text = spread.count(index)
        l_text = str(l_text) if l_text != 0 else ""
        wslabel = Gtk.Label(l_text)
        label_cont = wslabel.get_style_context()
        label_cont.add_class("labelbottom")
        Gtk.StyleContext.add_provider(
            label_cont,
            self.provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
        )
        return wslabel

    def create_buttonset(self, button, label, w_id):
        # create the subset per navigation button, closebutton, label
        self.bgrid = Gtk.Grid()
        # closebutton
        closebutton = Gtk.Button()
        st_cont = closebutton.get_style_context()
        st_cont.add_class("closebutton")
        Gtk.StyleContext.add_provider(
            st_cont,
            self.provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
        )
        closeb_img = Gtk.Image.new_from_file(
            os.path.join(button_images, "grey_x.png")
        )
        closebutton.set_image(closeb_img)
        closebutton.connect(
            "enter-notify-event",
            self.change_onenter,
            closebutton,
            os.path.join(button_images, "white2_x.png"),
        )
        closebutton.connect(
            "leave-notify-event",
            self.change_onenter,
            closebutton,
            os.path.join(button_images, "grey_x.png")
        )
        closebutton.set_relief(Gtk.ReliefStyle.NONE)
        closebutton.set_can_focus(False)
        closebutton.connect(
            "clicked",
            self.remove_fromwin, self.bgrid, w_id,
        )
        bholder = Gtk.Box()
        bholder.pack_end(closebutton, False, False, 0)
        self.bgrid.attach(bholder, 1, 0, 1, 1)
        # make sure the window representation keeps the size after removing
        element_hsizer = self.h_spacer(280)
        element_vsizer = self.v_spacer(180)
        self.bgrid.attach(element_hsizer, 0, 3, 2, 1)
        self.bgrid.attach(element_vsizer, 3, 0, 1, 2)
        for el in [element_vsizer, element_hsizer]:
            self.shapers.append(el)
        # add the window representation, label
        self.bgrid.attach(button, 0, 1, 2, 1)
        self.bgrid.attach(label, 0, 0, 1, 1)
        # change icon of closebutton on enter window button
        new_image = os.path.join(button_images, "white_x.png")
        target_button = closebutton
        button.connect(
            "enter-notify-event",
            self.change_onenter,
            target_button, new_image,
        )
        orig_image = os.path.join(button_images, "grey_x.png")
        button.connect(
            "leave-notify-event",
            self.change_onleave,
            target_button,
            orig_image,
        )
        return self.bgrid

    def remove_fromwin(self, widget, bgrid, w_id):
        children = bgrid.get_children()
        for c in children:
            if c not in self.shapers:
                bgrid.remove(c)
                if "Button" in str(type(c)):
                    self.buttondata = [
                        bd for bd in self.buttondata if not bd[0] == c
                    ]
        subprocess.Popen(["wmctrl", "-ic", w_id])

    def activate(self, arg1, arg2=None):
        # activate the selected window, close preview window
        w = arg2 or arg1
        # subprocess.Popen(["xdotool", "windowactivate", w])
        subprocess.Popen(["wmctrl", "-ia", w])
        self.stop()

    def h_spacer(self, addwidth):
        # artificial (calculated) stuffing on the left side of the row
        spacegrid = Gtk.Grid()
        if addwidth:
            label1 = Gtk.Label()
            label2 = Gtk.Label()
            spacegrid.attach(label1, 0, 0, 1, 1)
            spacegrid.attach(label2, 1, 0, 1, 1)
            spacegrid.set_column_spacing(addwidth)
        return spacegrid

    def v_spacer(self, addheight):
        spacegrid = Gtk.Grid()
        label1 = Gtk.Label()
        label2 = Gtk.Label()
        spacegrid.attach(label1, 0, 0, 1, 1)
        spacegrid.attach(label2, 0, 1, 1, 1)
        spacegrid.set_row_spacing(addheight)
        return spacegrid

    def stop(self, *args):
        if self.age < 2:
            time.sleep((2 - self.age) * 0.1)
        Gtk.main_quit()

    def area_draw(self, widget, cr):
        # set transparent color
        cr.set_source_rgba(0.2, 0.2, 0.2, 0.8)
        cr.set_operator(cairo.OPERATOR_SOURCE)
        cr.paint()
        cr.set_operator(cairo.OPERATOR_OVER)

    def key_checker(self):
        # check if keys are in a pressed state
        exclude = ["Button", "Virtual", "pointer"]
        keyboards = [
            k for k in pv.get(["xinput", "--list"]).splitlines()
            if not any([s in k for s in exclude])
        ]
        dev_ids = [[
            s.split("=")[1] for s in k.split() if "id=" in s
        ][0] for k in keyboards]
        pressed = False
        for d in dev_ids:
            if "down" in pv.get(["xinput", "--query-state", d]):
                pressed = True
                break
        return pressed


def run_previews():
    n_wins = len(wins)
    PreviewsWin()
    GObject.threads_init()
    Gtk.main()


# see if we should go at all
muted = os.path.exists(os.path.join(pv.settings_dir, "muted"))
keyexists = pv.getkey()


if all([wins is not None, keyexists, not muted]):
    n_wins = len(wins)
    if show_allwins:
        run_previews()
    else:
        if wins:
            run_previews()
