#!/usr/bin/python3

import random
import signal
import os, locale
from xml.etree import ElementTree
from setproctitle import setproctitle

from gi.repository import Gio, GLib

SLIDESHOW_DBUS_NAME = "org.Cinnamon.Slideshow"
SLIDESHOW_DBUS_PATH = "/org/Cinnamon/Slideshow"

BACKGROUND_COLLECTION_TYPE_DIRECTORY = "directory"
BACKGROUND_COLLECTION_TYPE_XML = "xml"

# D-Bus interface XML definition
DBUS_INTERFACE_XML = '''
<node>
    <interface name="org.Cinnamon.Slideshow">
        <method name="begin" />
        <method name="end" />
        <method name="getNextImage" />
    </interface>
</node>
'''

class CinnamonSlideshowApplication(Gio.Application):
    def __init__(self):
        super().__init__(
            application_id=SLIDESHOW_DBUS_NAME,
            flags=Gio.ApplicationFlags.IS_SERVICE
        )

        self.slideshow_settings = Gio.Settings(schema="org.cinnamon.desktop.background.slideshow")
        self.background_settings = Gio.Settings(schema="org.cinnamon.desktop.background")

        if self.slideshow_settings.get_boolean("slideshow-paused"):
            self.slideshow_settings.set_boolean("slideshow-paused", False)

        self.image_playlist = []
        self.used_image_playlist = []
        self.images_ready = False
        self.update_in_progress = False
        self.starting_image = self.background_settings.get_string("picture-uri")
        self.current_image = self.starting_image

        self.update_id = 0
        self.loop_counter = self.slideshow_settings.get_int("delay")

        self.folder_monitor = None
        self.folder_monitor_id = 0

        self.connection = None
        self.registration_id = 0
        GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self.end)

    def do_startup(self):
        Gio.Application.do_startup(self)
        self.hold()

    def do_dbus_register(self, connection, object_path):
        try:
            self.connection = connection
            iface_info = Gio.DBusNodeInfo.new_for_xml(DBUS_INTERFACE_XML)
            self.registration_id = connection.register_object(
                SLIDESHOW_DBUS_PATH,
                iface_info.interfaces[0],
                self.handle_method_call,
                None,  # get_property
                None   # set_property
            )
        except Exception as e:
            print(f"Failed to export slideshow service: {e}")
            return False

        return Gio.Application.do_dbus_register(self, connection, object_path)

    def do_dbus_unregister(self, connection, path):
        if self.registration_id > 0:
            connection.unregister_object(self.registration_id)
            self.registration_id = 0

        Gio.Application.do_dbus_unregister(self, connection, path)

    def do_activate(self):
        self.setup_slideshow()

    def handle_method_call(self, connection, sender, object_path, interface_name, method_name, parameters, invocation):
        try:
            if method_name == "begin":
                self.begin()
                invocation.return_value(None)
            elif method_name == "end":
                self.end()
                invocation.return_value(None)
            elif method_name == "getNextImage":
                self.get_next_image()
                invocation.return_value(None)
            else:
                invocation.return_error_literal(
                    Gio.dbus_error_quark(),
                    Gio.DBusError.UNKNOWN_METHOD,
                    f"Unknown method: {method_name}"
                )
        except Exception as e:
            invocation.return_error_literal(
                Gio.dbus_error_quark(),
                Gio.DBusError.FAILED,
                str(e)
            )

    def begin(self):
        self.setup_slideshow()

    def end(self):
        if self.update_id > 0:
            GLib.source_remove(self.update_id)
            self.update_id = 0

        self.disconnect_folder_monitor()
        self.quit()

    def get_next_image(self):
        if self.update_id > 0:
            GLib.source_remove(self.update_id)
            self.update_id = 0

        self.loop_counter = self.slideshow_settings.get_int("delay")
        self.start_mainloop()

    def setup_slideshow(self):
        self.load_settings()
        self.connect_signals()
        self.gather_images()
        if self.collection_type == BACKGROUND_COLLECTION_TYPE_DIRECTORY:
            self.connect_folder_monitor()
        self.start_mainloop()

    def format_source(self, type, path):
        # returns 'type://path'
        return "%s://%s" % (type, path)

    def load_settings(self):
        self.random_order = self.slideshow_settings.get_boolean("random-order")
        self.collection = self.slideshow_settings.get_string("image-source")
        self.collection_path = ""
        self.collection_type = None
        if self.collection != "" and "://" in self.collection:
            (self.collection_type, self.collection_path) = self.collection.split("://")
            self.collection_path = os.path.expanduser(self.collection_path)

    def connect_signals(self):
        self.slideshow_settings.connect("changed::image-source", self.on_slideshow_source_changed)
        self.slideshow_settings.connect("changed::random-order", self.on_random_order_changed)
        self.background_settings.connect("changed::picture-uri", self.on_picture_uri_changed)

    def connect_folder_monitor(self):
        folder_path = Gio.file_new_for_path(self.collection_path)
        self.folder_monitor = folder_path.monitor_directory(0, None)
        self.folder_monitor_id = self.folder_monitor.connect("changed", self.on_monitored_folder_changed)

    def disconnect_folder_monitor(self):
        if self.folder_monitor_id > 0:
            self.folder_monitor.disconnect(self.folder_monitor_id)
            self.folder_monitor_id = 0

    def gather_images(self):
        if self.collection_type == BACKGROUND_COLLECTION_TYPE_DIRECTORY:
            folder_at_path = Gio.file_new_for_path(self.collection_path)

            if folder_at_path.query_exists(None):
                folder_at_path.enumerate_children_async("standard::name,standard::type,standard::content-type",
                                                        Gio.FileQueryInfoFlags.NONE,
                                                        GLib.PRIORITY_LOW,
                                                        None,
                                                        self.gather_images_cb,
                                                        None)

        elif self.collection_type == BACKGROUND_COLLECTION_TYPE_XML:
            pictures = self.parse_xml_backgrounds_list(self.collection_path)
            for picture in pictures:
                filename = picture["filename"]
                self.add_image_to_playlist(filename)

    def gather_images_cb(self, obj, res, user_data):
        all_files = []
        enumerator = obj.enumerate_children_finish(res)
        def on_next_file_complete(obj, res, user_data=all_files):
            files = obj.next_files_finish(res)
            file_list = all_files
            if len(files) != 0:
                file_list = file_list.extend(files)
                enumerator.next_files_async(100, GLib.PRIORITY_LOW, None, on_next_file_complete, None)
            else:
                enumerator.close(None)
                self.ensure_file_is_image(file_list)

        enumerator.next_files_async(100, GLib.PRIORITY_LOW, None, on_next_file_complete, all_files)

    def ensure_file_is_image(self, file_list):
        for item in file_list:
            file_type = item.get_file_type()
            if file_type is not Gio.FileType.DIRECTORY:
                file_contents = item.get_content_type()
                if file_contents.startswith("image"):
                    self.add_image_to_playlist(self.collection_path + "/" + item.get_name())

    def add_image_to_playlist(self, file_path):
        image = Gio.file_new_for_path(file_path)
        image_uri = image.get_uri()
        self.image_playlist.append(image_uri)
        if self.collection_type == BACKGROUND_COLLECTION_TYPE_DIRECTORY:
            self.image_playlist.sort()
        self.images_ready = True

    def on_slideshow_source_changed(self, settings, key):
        if self.update_id > 0:
            GLib.source_remove(self.update_id)
            self.update_id = 0
        self.disconnect_folder_monitor()
        self.image_playlist = []
        self.used_image_playlist = []
        self.images_ready = False
        self.collection = self.slideshow_settings.get_string("image-source")
        self.collection_path = ""
        self.collection_type = None
        if self.collection != "" and "://" in self.collection:
            (self.collection_type, self.collection_path) = self.collection.split("://")
            self.collection_path = os.path.expanduser(self.collection_path)
        if self.collection_type == BACKGROUND_COLLECTION_TYPE_DIRECTORY:
            self.connect_folder_monitor()
        self.gather_images()
        self.loop_counter = self.slideshow_settings.get_int("delay")
        self.start_mainloop()

    def on_monitored_folder_changed(self, monitor, file1, file2, event_type):
        try:
            if event_type == Gio.FileMonitorEvent.DELETED:
                file_uri = file1.get_uri()
                if self.image_playlist.count(file_uri) > 0:
                    index_to_remove = self.image_playlist.index(file_uri)
                    del self.image_playlist[index_to_remove]
                elif self.used_image_playlist.count(file_uri) > 0:
                    index_to_remove = self.used_image_playlist.index(file_uri)
                    del self.used_image_playlist[index_to_remove]

            if event_type == Gio.FileMonitorEvent.CREATED:
                file_path = file1.get_path()
                file_info = file1.query_info("standard::type,standard::content-type", Gio.FileQueryInfoFlags.NONE, None)
                file_type = file_info.get_file_type()
                if file_type is not Gio.FileType.DIRECTORY:
                    file_contents = file_info.get_content_type()
                    if file_contents.startswith("image"):
                        self.add_image_to_playlist(file_path)
        except:
            pass

    def on_random_order_changed(self, settings, key):
        self.random_order = self.slideshow_settings.get_boolean("random-order")

    def on_picture_uri_changed(self, settings, key):
        if self.update_in_progress:
            return
        else:
            if self.background_settings.get_string("picture-uri") != self.current_image:
                self.slideshow_settings.set_boolean("slideshow-enabled", False)

    def start_mainloop(self):
        if self.update_id > 0:
            GLib.source_remove(self.update_id)
            self.update_id = 0

        if not self.images_ready:
            self.update_id = GLib.timeout_add_seconds(1, self.start_mainloop)
        else:
            if self.loop_counter >= self.slideshow_settings.get_int("delay") and not self.slideshow_settings.get_boolean("slideshow-paused"):
                self.loop_counter = 1
                self.update_background()
                self.update_id = GLib.timeout_add_seconds(60, self.start_mainloop)
            else:
                self.loop_counter = self.loop_counter + 1
                self.update_id = GLib.timeout_add_seconds(60, self.start_mainloop)

    def update_background(self):
        if self.update_in_progress:
            return

        self.update_in_progress = True

        if len(self.image_playlist) == 0:
            self.move_used_images_to_original_playlist()

        next_image = self.get_next_image_from_list()
        if next_image is not None:
            self.background_settings.set_string("picture-uri", next_image)
            self.current_image = next_image

        self.update_in_progress = False

    def get_next_image_from_list(self):
        if self.random_order:
            index = random.randint(0, len(self.image_playlist) - 1)
            image = self.image_playlist[index]
        else:
            self.maybe_skip_past_last_image()
            index = 0
            image = self.image_playlist[index]

        self.move_image_to_used_playlist(index, image)

        return image

    def maybe_skip_past_last_image(self):
        if self.starting_image is None:
            return
        # Check if the starting image is in our list (we've rebooted or otherwise a new process)
        if self.starting_image in self.image_playlist:
            # Make sure it's *not* the last image in our list. (We want to start over anyhow, if it is)
            if self.image_playlist[-1] != self.starting_image:
                # Move all images leading up to, and including this one, to the 'used' bin.
                while self.image_playlist[0] != self.starting_image:
                    self.move_image_to_used_playlist(0, self.image_playlist[0])
                self.move_image_to_used_playlist(0, self.image_playlist[0])

        self.starting_image = None

    def move_image_to_used_playlist(self, index, image):
        self.image_playlist.pop(index)
        self.used_image_playlist.append(image)

    def move_used_images_to_original_playlist(self):
        self.image_playlist = self.used_image_playlist
        if self.collection_type == BACKGROUND_COLLECTION_TYPE_DIRECTORY:
            self.image_playlist.sort()
        self.used_image_playlist = []


########### TAKEN FROM CS_BACKGROUND
    def splitLocaleCode(self, localeCode):
        loc = localeCode.partition("_")
        loc = (loc[0], loc[2])
        return loc

    def getLocalWallpaperName(self, names, loc):
        result = ""
        mainLocFound = False
        for wp in names:
            wpLoc = wp[0]
            wpName = wp[1]
            if wpLoc == ("", ""):
                if not mainLocFound:
                    result = wpName
            elif wpLoc[0] == loc[0]:
                if wpLoc[1] == loc[1]:
                    return wpName
                elif wpLoc[1] == "":
                    result = wpName
                    mainLocFound = True
        return result

    def parse_xml_backgrounds_list(self, filename):
        try:
            locAttrName = "{http://www.w3.org/XML/1998/namespace}lang"
            loc = self.splitLocaleCode(locale.getlocale()[0])
            res = []
            f = open(filename)
            rootNode = ElementTree.fromstring(f.read())
            f.close()
            if rootNode.tag == "wallpapers":
                for wallpaperNode in rootNode:
                    if wallpaperNode.tag == "wallpaper" and wallpaperNode.get("deleted") != "true":
                        wallpaperData = {"metadataFile": filename}
                        names = []
                        for prop in wallpaperNode:
                            if type(prop.tag) == str:
                                if prop.tag != "name":
                                    wallpaperData[prop.tag] = prop.text
                                else:
                                    propAttr = prop.attrib
                                    wpName = prop.text
                                    locName = self.splitLocaleCode(propAttr.get(locAttrName)) if locAttrName in propAttr else ("", "")
                                    names.append((locName, wpName))
                        wallpaperData["name"] = self.getLocalWallpaperName(names, loc)

                        if "filename" in wallpaperData and wallpaperData["filename"] != "" and os.path.exists(wallpaperData["filename"]) and os.access(wallpaperData["filename"], os.R_OK):
                            if wallpaperData["name"] == "":
                                wallpaperData["name"] = os.path.basename(wallpaperData["filename"])
                            res.append(wallpaperData)
            return res
        except Exception as detail:
            print(detail)
            return []

if __name__ == "__main__":
    setproctitle("cinnamon-slideshow")

    app = CinnamonSlideshowApplication()
    app.run()