# program.py
#
# Copyright 2022 brombinmirko <send@mirko.pm>
#
# 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, in version 3 of the License.
#
# 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/>.
#

import webbrowser
from gettext import gettext as _

from gi.repository import Gtk, Adw

from bottles.backend.managers.library import LibraryManager
from bottles.backend.managers.steam import SteamManager
from bottles.backend.models.result import Result
from bottles.backend.utils.manager import ManagerUtils
from bottles.backend.utils.threading import RunAsync
from bottles.backend.wine.executor import WineExecutor
from bottles.backend.wine.uninstaller import Uninstaller
from bottles.backend.wine.winedbg import WineDbg
from bottles.frontend.utils.gtk import GtkUtils
from bottles.frontend.windows.launchoptions import LaunchOptionsDialog
from bottles.frontend.windows.rename import RenameDialog


# noinspection PyUnusedLocal
@Gtk.Template(resource_path="/com/usebottles/bottles/program-entry.ui")
class ProgramEntry(Adw.ActionRow):
    __gtype_name__ = "ProgramEntry"

    # region Widgets
    btn_menu = Gtk.Template.Child()
    btn_run = Gtk.Template.Child()
    btn_stop = Gtk.Template.Child()
    btn_launch_options = Gtk.Template.Child()
    btn_launch_steam = Gtk.Template.Child()
    btn_uninstall = Gtk.Template.Child()
    btn_remove = Gtk.Template.Child()
    btn_hide = Gtk.Template.Child()
    btn_unhide = Gtk.Template.Child()
    btn_rename = Gtk.Template.Child()
    btn_browse = Gtk.Template.Child()
    btn_add_steam = Gtk.Template.Child()
    btn_add_entry = Gtk.Template.Child()
    btn_add_library = Gtk.Template.Child()
    btn_launch_terminal = Gtk.Template.Child()
    pop_actions = Gtk.Template.Child()

    # endregion

    def __init__(
        self, window, config, program, is_steam=False, check_boot=True, **kwargs
    ):
        super().__init__(**kwargs)

        # common variables and references
        self.window = window
        self.view_bottle = window.page_details.view_bottle
        self.manager = window.manager
        self.config = config
        self.program = program

        self.set_title(self.program["name"])

        if is_steam:
            self.set_subtitle("Steam")
            for w in [self.btn_run, self.btn_stop, self.btn_menu]:
                w.set_visible(False)
                w.set_sensitive(False)
            self.btn_launch_steam.set_visible(True)
            self.btn_launch_steam.set_sensitive(True)
            self.set_activatable_widget(self.btn_launch_steam)
        else:
            self.executable = program.get("executable", "")

        if program.get("removed"):
            self.add_css_class("removed")

        if program.get("auto_discovered"):
            self.btn_remove.set_visible(False)

        self.btn_hide.set_visible(not program.get("removed"))
        self.btn_unhide.set_visible(program.get("removed"))

        if self.manager.steam_manager.is_steam_supported:
            self.btn_add_steam.set_visible(True)

        library_manager = LibraryManager()
        for _uuid, entry in library_manager.get_library().items():
            if entry.get("id") == program.get("id"):
                self.btn_add_library.set_visible(False)

        external_programs = []
        for v in self.config.External_Programs.values():
            external_programs.append(v["name"])

        """Signal connections"""
        self.btn_run.connect("clicked", self.run_executable)
        self.btn_launch_steam.connect("clicked", self.run_steam)
        self.btn_launch_terminal.connect("clicked", self.run_executable, True)
        self.btn_stop.connect("clicked", self.stop_process)
        self.btn_launch_options.connect("clicked", self.show_launch_options_view)
        self.btn_uninstall.connect("clicked", self.uninstall_program)
        self.btn_hide.connect("clicked", self.hide_program)
        self.btn_unhide.connect("clicked", self.hide_program)
        self.btn_rename.connect("clicked", self.rename_program)
        self.btn_browse.connect("clicked", self.browse_program_folder)
        self.btn_add_entry.connect("clicked", self.add_entry)
        self.btn_add_library.connect("clicked", self.add_to_library)
        self.btn_add_steam.connect("clicked", self.add_to_steam)
        self.btn_remove.connect("clicked", self.remove_program)

        if not program.get("removed") and not is_steam and check_boot:
            self.__is_alive()

    def show_launch_options_view(self, _widget=False):
        def update(_widget, config):
            self.config = config
            self.update_programs()

        dialog = LaunchOptionsDialog(self, self.config, self.program)
        dialog.present()
        dialog.connect("options-saved", update)

    @GtkUtils.run_in_main_loop
    def __reset_buttons(self, result: bool | Result = False, _error=False):
        status = False
        if isinstance(result, Result):
            status = result.status
        elif isinstance(result, bool):
            status = result
            if not isinstance(result, bool):
                status = result.status
        else:
            raise NotImplementedError(
                "Invalid data type, expect bool or Result, but it was %s" % type(result)
            )

        self.btn_run.set_visible(status)
        self.btn_stop.set_visible(not status)
        self.btn_run.set_sensitive(status)
        self.btn_stop.set_sensitive(not status)

    def __is_alive(self):
        winedbg = WineDbg(self.config)

        @GtkUtils.run_in_main_loop
        def set_watcher(_result=False, _error=False):
            nonlocal winedbg
            self.__reset_buttons()

            RunAsync(
                winedbg.wait_for_process,
                callback=self.__reset_buttons,
                name=self.executable,
                timeout=5,
            )

        RunAsync(winedbg.is_process_alive, callback=set_watcher, name=self.executable)

    def run_executable(self, _widget, with_terminal=False):
        self.pop_actions.popdown()  # workaround #1640

        def _run():
            WineExecutor.run_program(self.config, self.program, with_terminal)
            self.pop_actions.popdown()  # workaround #1640
            return True

        self.window.show_toast(_('Launching "{0}"…').format(self.program["name"]))
        RunAsync(_run, callback=self.__reset_buttons)
        self.__reset_buttons()

    def run_steam(self, _widget):
        self.manager.steam_manager.launch_app(self.config.CompatData)
        self.window.show_toast(
            _('Launching "{0}" with Steam…').format(self.program["name"])
        )
        self.pop_actions.popdown()  # workaround #1640

    def stop_process(self, widget):
        self.window.show_toast(_('Stopping "{0}"…').format(self.program["name"]))
        winedbg = WineDbg(self.config)
        widget.set_sensitive(False)
        winedbg.kill_process(self.executable)
        self.__reset_buttons(True)

    @GtkUtils.run_in_main_loop
    def update_programs(self, _result=False, _error=False):
        self.view_bottle.update_programs(config=self.config)

    def uninstall_program(self, _widget):
        uninstaller = Uninstaller(self.config)
        RunAsync(
            task_func=uninstaller.from_name,
            callback=self.update_programs,
            name=self.program["name"],
        )

    def hide_program(self, _widget=None, update=True):
        status = not self.program.get("removed")
        msg = _('"{0}" hidden').format(self.program["name"])
        if not status:
            msg = _('"{0}" showed').format(self.program["name"])

        self.program["removed"] = status
        self.save_program()
        self.btn_hide.set_visible(not status)
        self.btn_unhide.set_visible(status)
        self.window.show_toast(msg)
        if update:
            self.update_programs()

    def save_program(self):
        return self.manager.update_config(
            config=self.config,
            key=self.program["id"],
            value=self.program,
            scope="External_Programs",
        ).data["config"]

    def remove_program(self, _widget=None):
        self.config = self.manager.update_config(
            config=self.config,
            key=self.program["id"],
            scope="External_Programs",
            value=None,
            remove=True,
        ).data["config"]
        self.window.show_toast(_('"{0}" removed').format(self.program["name"]))
        self.update_programs()

    def rename_program(self, _widget):
        def func(new_name):
            if new_name == self.program["name"]:
                return
            old_name = self.program["name"]
            self.program["name"] = new_name
            self.manager.update_config(
                config=self.config,
                key=self.program["id"],
                value=self.program,
                scope="External_Programs",
            )

            def async_work():
                library_manager = LibraryManager()
                entries = library_manager.get_library()

                for uuid, entry in entries.items():
                    if entry.get("id") == self.program["id"]:
                        entries[uuid]["name"] = new_name
                        library_manager.download_thumbnail(uuid, self.config)
                        break

                library_manager.__library = entries
                library_manager.save_library()

            @GtkUtils.run_in_main_loop
            def ui_update(_result, _error):
                self.window.page_library.update()
                self.window.show_toast(
                    _('"{0}" renamed to "{1}"').format(old_name, new_name)
                )
                self.update_programs()

            RunAsync(async_work, callback=ui_update)

        dialog = RenameDialog(self.window, on_save=func, name=self.program["name"])
        dialog.present()

    def browse_program_folder(self, _widget):
        ManagerUtils.open_filemanager(
            config=self.config, path_type="custom", custom_path=self.program["folder"]
        )
        self.pop_actions.popdown()  # workaround #1640

    def add_entry(self, _widget):
        @GtkUtils.run_in_main_loop
        def update(result, _error=False):
            if not result:
                webbrowser.open("https://docs.usebottles.com/bottles/programs#flatpak")
                return

            self.window.show_toast(
                _('Desktop Entry created for "{0}"').format(self.program["name"])
            )

        RunAsync(
            ManagerUtils.create_desktop_entry,
            callback=update,
            config=self.config,
            program={
                "name": self.program["name"],
                "executable": self.program["executable"],
                "path": self.program["path"],
            },
        )

    def add_to_library(self, _widget):
        def update(_result, _error=False):
            self.window.update_library()
            self.window.show_toast(
                _('"{0}" added to your library').format(self.program["name"])
            )

        def add_to_library():
            self.save_program()  # we need to store it in the bottle configuration to keep the reference
            library_manager = LibraryManager()
            library_manager.add_to_library(
                {
                    "bottle": {"name": self.config.Name, "path": self.config.Path},
                    "name": self.program["name"],
                    "id": str(self.program["id"]),
                    "icon": ManagerUtils.extract_icon(
                        self.config, self.program["name"], self.program["path"]
                    ),
                },
                self.config,
            )

        self.btn_add_library.set_visible(False)
        RunAsync(add_to_library, update)

    def add_to_steam(self, _widget):
        def update(result, _error=False):
            if result.ok:
                self.window.show_toast(
                    _('"{0}" added to your Steam library').format(self.program["name"])
                )

        steam_manager = SteamManager(self.config)
        RunAsync(
            steam_manager.add_shortcut,
            update,
            program_name=self.program["name"],
            program_path=self.program["path"],
        )
