#!/usr/bin/env python3
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Gdk", "3.0")
gi.require_version('GtkLayerShell', '0.1')
gi.require_version('Libxfce4windowing', '0.0')
from gi.repository import Gtk, Gio, Gdk, GLib, GtkLayerShell
from gi.repository Libxfce4windowing as Xfw
import dropby_tools as db
import subprocess
import os
import sys
from dbus.mainloop.glib import DBusGMainLoop
import dbus
import dbus.service


"""
DropBy
Author: Jacob Vlijm
Copyright © 2017-2022 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 <https://www.gnu.org/licenses/>.
"""


DBUS_NAME = 'org.ubuntubudgie.dropby'
DBUS_PATH = '/org/ubuntubudgie/dropby'

dropby_css = """
.label {
  padding-bottom: 7px;
  padding-top: 0px;
  font-weight: bold;
}
"""


class DropByDBusService(dbus.service.Object):
    """DBus service for communication with applet"""

    def __init__(self, bus, watch_volumes):
        self.watch_volumes = watch_volumes
        bus_name = dbus.service.BusName(DBUS_NAME, bus=bus)
        dbus.service.Object.__init__(self, bus_name, DBUS_PATH)

    @dbus.service.method(DBUS_NAME)
    def ShowWindow(self):
        """Show the popup window"""
        self.watch_volumes.actonconnect()
        return True

    @dbus.service.method(DBUS_NAME)
    def Quit(self):
        """Quit the application"""
        Gtk.main_quit()
        return True


class WatchVolumes:

    def __init__(self, uuid):
        # Setup DBus
        DBusGMainLoop(set_as_default=True)
        self.bus = dbus.SessionBus()
        self.dbus_service = DropByDBusService(self.bus, self)

        # setup watching connections
        self.uuid = uuid
        self.watchdrives = Gio.VolumeMonitor.get()
        triggers = [
            "volume_added", "volume_removed", "mount_added", "mount_removed",
        ]
        for t in triggers:
            self.watchdrives.connect(t, self.actonconnect, t)
        # workaround to only open nautilus on our own action
        self.act_onmount = False
        # setup css
        self.provider = Gtk.CssProvider.new()
        self.provider.load_from_data(dropby_css.encode())
        self.newwin = None

        self.settings = Gio.Settings.new(
            "org.ubuntubudgie.plugins.budgie-dropby"
        )
        self.settings.connect("changed", self.update_corner)
        self.update_corner()

        app_path = os.path.dirname(os.path.abspath(__file__))
        self.copyscript = os.path.join(app_path, "copy_flash")
        self.tmp_path = os.getenv("XDG_RUNTIME_DIR") \
            if "XDG_RUNTIME_DIR" in os.environ else os.getenv("HOME")

        # Timer for auto-hide
        self.hide_timer = None

        # Get screen info for positioning
        self.xfw_screen = Xfw.Screen.get_default()

        # setup watching applet presence
        self.currpanelsubject_settings = None
        GLib.timeout_add_seconds(1, self.watchout)
        Gtk.main()

    def watchout(self):
        path = "com.solus-project.budgie-panel"
        panelpath_prestring = "/com/solus-project/budgie-panel/panels/"
        panel_settings = Gio.Settings.new(path)
        allpanels_list = panel_settings.get_strv("panels")
        for p in allpanels_list:
            panelpath = panelpath_prestring + "{" + p + "}/"
            self.currpanelsubject_settings = Gio.Settings.new_with_path(
                path + ".panel", panelpath
            )
            applets = self.currpanelsubject_settings.get_strv("applets")
            if self.uuid in applets:
                self.currpanelsubject_settings.connect(
                    "changed", self.check_ifonpanel
                )
        return False

    def check_ifonpanel(self, *args):
        applets = self.currpanelsubject_settings.get_strv("applets")
        if self.uuid not in applets:
            Gtk.main_quit()

    def update_corner(self, *args):
        self.winpos = self.settings.get_int("popup-corner")
        # If window already exists, update its position
        if self.newwin:
            self.setup_layer_shell_position(self.newwin)

    def reset_hide_timer(self):
        """Reset the auto-hide timer"""
        if self.hide_timer:
            GLib.source_remove(self.hide_timer)
        self.hide_timer = GLib.timeout_add_seconds(6, self.getridofwindow)

    def cancel_hide_timer(self):
        """Cancel the auto-hide timer"""
        if self.hide_timer:
            GLib.source_remove(self.hide_timer)
            self.hide_timer = None

    def getridofwindow(self, *args):
        """Hide the popup window"""
        try:
            if self.newwin:
                self.newwin.hide()
                self.newwin = None
        except (AttributeError, TypeError):
            pass
        if self.hide_timer:
            GLib.source_remove(self.hide_timer)
            self.hide_timer = None
        return False

    def busy(self, arg1, arg2):
        """Mouse entered window - cancel auto-hide"""
        self.cancel_hide_timer()

    def outofajob(self, arg1, arg2):
        """Mouse left window - start auto-hide timer"""
        self.reset_hide_timer()

    def create_win(self, subject=None, newvol=None):
        window = Gtk.Window()
        window.connect("enter-notify-event", self.busy)
        window.connect("leave-notify-event", self.outofajob)
        window.set_title("DropBy")
        window.set_decorated(False)

        # Initialize layer shell
        GtkLayerShell.init_for_window(window)

        # Set layer to top
        GtkLayerShell.set_layer(window, GtkLayerShell.Layer.TOP)

        # Set keyboard mode to none (don't steal focus)
        GtkLayerShell.set_keyboard_mode(window,
                                        GtkLayerShell.KeyboardMode.NONE)

        # Position window based on settings
        self.setup_layer_shell_position(window)

        self.maingrid = Gtk.Grid()
        window.add(self.maingrid)
        window.connect("delete-event", lambda w, e: self.getridofwindow())

        # Start auto-hide timer
        self.reset_hide_timer()
        return window

    def setup_layer_shell_position(self, window):
        # Configure GTK Layer Shell anchors and margins based on
        # corner position - Clear all anchors first
        for edge in [GtkLayerShell.Edge.LEFT,
                     GtkLayerShell.Edge.RIGHT,
                     GtkLayerShell.Edge.TOP,
                     GtkLayerShell.Edge.BOTTOM]:
            GtkLayerShell.set_anchor(window, edge, False)

        # Set margins
        margin = 80

        # Configure based on corner setting
        if self.winpos == 1:  # top-left
            GtkLayerShell.set_anchor(window, GtkLayerShell.Edge.TOP, True)
            GtkLayerShell.set_anchor(window, GtkLayerShell.Edge.LEFT, True)
            GtkLayerShell.set_margin(window, GtkLayerShell.Edge.TOP, margin)
            GtkLayerShell.set_margin(window, GtkLayerShell.Edge.LEFT, margin)
        elif self.winpos == 2:  # top-right
            GtkLayerShell.set_anchor(window, GtkLayerShell.Edge.TOP, True)
            GtkLayerShell.set_anchor(window, GtkLayerShell.Edge.RIGHT, True)
            GtkLayerShell.set_margin(window, GtkLayerShell.Edge.TOP, margin)
            GtkLayerShell.set_margin(window, GtkLayerShell.Edge.RIGHT, margin)
        elif self.winpos == 3:  # bottom-left
            GtkLayerShell.set_anchor(window, GtkLayerShell.Edge.BOTTOM, True)
            GtkLayerShell.set_anchor(window, GtkLayerShell.Edge.LEFT, True)
            GtkLayerShell.set_margin(window, GtkLayerShell.Edge.BOTTOM, margin)
            GtkLayerShell.set_margin(window, GtkLayerShell.Edge.LEFT, margin)
        else:  # bottom-right (default)
            GtkLayerShell.set_anchor(window, GtkLayerShell.Edge.BOTTOM, True)
            GtkLayerShell.set_anchor(window, GtkLayerShell.Edge.RIGHT, True)
            GtkLayerShell.set_margin(window, GtkLayerShell.Edge.BOTTOM, margin)
            GtkLayerShell.set_margin(window, GtkLayerShell.Edge.RIGHT, margin)

        # Set monitor (primary monitor)
        monitors = self.xfw_screen.get_monitors()
        primary_monitor = None
        for monitor in monitors:
            if monitor.is_primary():
                primary_monitor = monitor
                break

        # If no primary monitor found, use the first one
        if primary_monitor is None and len(monitors) > 0:
            primary_monitor = monitors[0]

        if primary_monitor:
            # Get the GdkMonitor from Xfw monitor
            gdk_display = Gdk.Display.get_default()
            gdk_monitor = None

            # Match by connector name
            connector = primary_monitor.get_connector()
            for i in range(gdk_display.get_n_monitors()):
                mon = gdk_display.get_monitor(i)
                # Try to match by connector or model
                if hasattr(mon, 'get_connector'):
                    if mon.get_connector() == connector:
                        gdk_monitor = mon
                        break
                if hasattr(mon, 'get_model'):
                    if mon.get_model() == connector:
                        gdk_monitor = mon
                        break

            # Fallback to primary monitor from Gdk
            if gdk_monitor is None:
                gdk_monitor = gdk_display.get_primary_monitor()

            if gdk_monitor:
                GtkLayerShell.set_monitor(window, gdk_monitor)

    def create_label(self, text):
        label = Gtk.Label()
        label.set_text(text)
        label.set_xalign(0)
        label.connect("enter-notify-event", self.busy)
        return label

    def create_button(self, text=None, icon=None):
        button = Gtk.Button()
        if text:
            button.set_label(text)
        if icon:
            button.set_image(icon)
        button.set_relief(Gtk.ReliefStyle.NONE)
        button.connect("enter-notify-event", self.busy)
        return button

    def fill_grid(self, get_relevant, newvol):
        pos = 2
        for d in get_relevant:
            vol_name = d["name"]
            # namebutton
            addition = " *" if d["volume"] == newvol else " "
            namebutton = self.create_button(
                text=vol_name + addition,
                icon=Gtk.Image.new_from_gicon(d["icon"], Gtk.IconSize.MENU),
            )
            namebutton.set_always_show_image(True)
            namebox = Gtk.Box()
            namebox.pack_start(namebutton, False, False, 0)
            self.maingrid.attach(namebox, 2, pos, 1, 1)
            # show free space
            freespace = self.create_label(d["free"])
            self.maingrid.attach(freespace, 3, pos, 1, 1)
            # set gui attributes for mounted volumes
            if d["ismounted"]:
                mount = d["ismounted"]
                vol_path = d["volume_path"]
                if all([mount == newvol, self.act_onmount]):
                    self.open_folder(vol_path)
                    self.act_onmount = False
                eject_button = self.create_button(
                    icon=Gtk.Image.new_from_icon_name(
                        "media-eject-symbolic", Gtk.IconSize.MENU
                    )
                )
                self.maingrid.attach(eject_button, 6, pos, 1, 1)
                if d["flashdrive"]:
                    spacer = self.create_label("\t")
                    self.maingrid.attach(spacer, 4, pos, 1, 1)
                    tooltip = "Eject"
                    eject_button.connect("clicked", self.eject_volume, mount)
                    cp_button = self.create_button(
                        icon=Gtk.Image.new_from_icon_name(
                            "media-floppy-symbolic", Gtk.IconSize.MENU
                        )
                    )
                    cp_button.set_tooltip_text("Make a local copy")
                    cp_button.connect(
                        "clicked", self.copy_flashdrive, vol_path, vol_name,
                    )
                    self.maingrid.attach(cp_button, 5, pos, 1, 1)
                else:
                    tooltip = "Unmount"
                    eject_button.connect("clicked", self.unmount_volume, mount)
                eject_button.set_tooltip_text(tooltip)
                if vol_path is not None:
                    namebutton.set_tooltip_text("Open " + vol_path)
                namebutton.connect("clicked", self.open_folder, vol_path)
            else:
                namebutton.connect("clicked", self.mount_volume, d["volume"])
                tooltip = "Mount and open " + vol_name
                namebutton.set_tooltip_text(tooltip)
            pos = pos + 1
        # create headers
        volume_label = self.create_label("Volume")
        freespace_label = self.create_label("Free\t")
        self.maingrid.attach(volume_label, 1, 1, 2, 1)
        self.maingrid.attach(freespace_label, 3, 1, 1, 1)
        # reserve space for icons
        iconreserved = Gtk.Label()
        iconreserved.set_text("\t" * 3)
        self.maingrid.attach(iconreserved, 4, 1, 10, 1)
        # set style
        for label in [volume_label, freespace_label]:
            label_cont = label.get_style_context()
            label_cont.add_class("label")
            Gtk.StyleContext.add_provider(
                label_cont,
                self.provider,
                Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
            )
        self.set_spacers()

    def set_spacers(self):
        """
        lazy choice to set borders
        """
        spacers = [[0, 0], [0, 100], [100, 100]]
        for sp in spacers:
            self.maingrid.attach(
                self.create_label(text="\t"), sp[0], sp[1], 1, 1
            )
        exitbutton = Gtk.Button()
        exitbutton.set_image(
            Gtk.Image.new_from_icon_name(
                "budgie-dropby-exit-symbolic", Gtk.IconSize.BUTTON,
            )
        )
        exitbutton.set_relief(Gtk.ReliefStyle.NONE)
        self.maingrid.attach(exitbutton, 100, 0, 1, 1)
        exitbutton.connect("clicked", self.getridofwindow)
        self.maingrid.show_all()

    def update_existing(self, newvol):
        # once/if popup exists, populate
        allvols = self.watchdrives.get_volumes()
        get_relevant = db.get_volumes(allvols)
        if get_relevant:
            self.fill_grid(get_relevant, newvol)
        else:
            self.getridofwindow()

    def actonconnect(self, subject=None, newvol=None, trigger=None):
        # check for relevance of changes
        all_usb = db.get_usb()
        try:
            got_uuidfromvolume = newvol.get_volume().get_uuid()
        except Exception:
            got_uuidfromvolume = None
        try:
            got_uuidfromnewvol = newvol.get_uuid()
        except Exception:
            got_uuidfromnewvol = None
        uuid = got_uuidfromvolume or got_uuidfromnewvol
        # act if uuid == None (removed) or in usb list (valid)
        if any([uuid is None, uuid in all_usb, trigger == "volume_removed"]):
            # if popup exists, update info
            if self.newwin:
                self.getridofwindow()
                self.newwin = self.create_win(subject, newvol)
                self.update_existing(newvol)
                # possibly, if no items to show,
                # newwin was destroyed after all ^
                if self.newwin:
                    self.newwin.show_all()
            # only create new popup on mount or connect
            elif trigger in ["volume_added", "mount_added", None]:
                self.newwin = self.create_win(subject, newvol)
                self.update_existing(newvol)
                if self.newwin:
                    self.newwin.show_all()

    def open_folder(self, *args):
        path = list(args)[-1]
        if path is not None:
            subprocess.Popen(["/usr/bin/xdg-open", path])

    def mount_volume(self, button, vol):
        Gio.Volume.mount(
            vol, Gio.MountMountFlags.NONE, Gio.MountOperation(), None,
        )
        self.act_onmount = True

    def unmount_volume(self, button, vol):
        Gio.Mount.unmount(vol, Gio.MountUnmountFlags.NONE, None)

    def eject_volume(self, button, vol):
        Gio.Mount.eject_with_operation(vol, Gio.MountUnmountFlags.NONE, None)

    def copy_flashdrive(self, button, source, name):
        subprocess.Popen([self.copyscript, source, name])


Gtk.init(None)
WatchVolumes(sys.argv[1])
