from gettext import gettext as _
from typing import Optional

from gi.repository import Gdk, Gtk  # type: ignore

from lutris.exceptions import UnavailableRunnerError
from lutris.game import Game
from lutris.gui import dialogs
from lutris.gui.dialogs.download import DownloadDialog
from lutris.services import SERVICES
from lutris.util.downloader import Downloader


class Delegate:
    def get_service(self, service_id):
        """Returns a new service object by its id. This seems dumb, but it is a work-around
        for Python's circular import limitations."""
        return SERVICES[service_id]()


class LaunchUIDelegate(Delegate):
    """These objects provide UI for the game while it is being launched;
    one provided to the launch() method.

    The default implementation provides no UI and makes default choices for
    the user, but DialogLaunchUIDelegate implements this to show dialogs and ask the
    user questions. Windows then inherit from DialogLaunchUIDelegate.

    If these methods throw any errors are reported via tha game-error signal;
    that is not part of this delegate because errors can be report outside of
    the launch() method, where no delegate is available.
    """

    def check_game_launchable(self, game):
        """See if the game can be launched. If there are adverse conditions,
        this can warn the user and ask whether to launch. If this returs
        False, the launch is cancelled. The default is to return True with no
        actual checks.
        """
        if not game.runner.is_installed():
            raise UnavailableRunnerError(_("The required runner '%s' is not installed.") % game.runner.name)
        return True

    def select_game_launch_config(self, game):
        """Prompt the user for which launch config to use. Returns None
        if the user cancelled, an empty dict for the primary game configuration
        and the launch_config as a dict if one is selected.

        The default is the select the primary game.
        """
        return {}  # primary game


class InstallUIDelegate(Delegate):
    """These objects provide UI for a runner as it is installing itself.
    One of these must be provided to the install() method.

    The default implementation provides no UI and makes default choices for
    the user, but DialogInstallUIDelegate implements this to show dialogs and
    ask the user questions. Windows then inherit from DialogLaunchUIDelegate.
    """

    def show_install_yesno_inquiry(self, question: str, title: str) -> bool:
        """Called to ask the user a yes/no question.

        The default is 'yes'."""
        return True

    def show_install_file_inquiry(self, question: str, title: str, message: str) -> Optional[str]:
        """Called to ask the user for a file.

        Lutris first asks the user the question given (showing the title);
        if the user answers 'Yes', it asks for the file using the message.

        Returns None if the user answers 'No' or cancels out. Returns the
        file path if the user selected one.

        The default is to return None always.
        """
        return None

    def download_install_file(self, url: str, destination: str) -> bool:
        """Downloads a file from a URL to a destination, overwriting any
        file at that path.

        Returns True if sucessful, and False if the user cancels.

        The default is to download with no UI, and no option to cancel.
        """
        downloader = Downloader(url, destination, overwrite=True)
        downloader.start()
        return downloader.join()


class CommandLineUIDelegate(LaunchUIDelegate):
    """This delegate can provide user selections that were provided on the command line."""

    def __init__(self, launch_config_name):
        self.launch_config_name = launch_config_name

    def select_game_launch_config(self, game):
        if not self.launch_config_name:
            return {}

        game_config = game.config.game_level.get("game", {})
        configs = game_config.get("launch_configs")

        for config in configs:
            if config.get("name") == self.launch_config_name:
                return config

        raise RuntimeError("The launch configuration '%s' could not be found." % self.launch_config_name)


class DialogInstallUIDelegate(InstallUIDelegate):
    """This provides UI for runner installation via dialogs."""

    def show_install_yesno_inquiry(self, question, title):
        dialog = dialogs.QuestionDialog(
            {
                "parent": self,
                "question": question,
                "title": title,
            }
        )
        return Gtk.ResponseType.YES == dialog.result

    def show_install_file_inquiry(self, question, title, message):
        dlg = dialogs.QuestionDialog(
            {
                "parent": self,
                "question": question,
                "title": title,
            }
        )
        if dlg.result == dlg.YES:
            dlg = dialogs.FileDialog(message)
            return dlg.filename

    def download_install_file(self, url: str, destination: str) -> bool:
        dialog = DownloadDialog(url, destination, parent=self)
        dialog.run()
        return dialog.downloader.state == Downloader.COMPLETED


class DialogLaunchUIDelegate(LaunchUIDelegate):
    """This provides UI for game launch via dialogs."""

    def check_game_launchable(self, game):
        if not game.runner.is_installed():
            installed = game.runner.install_dialog(self)
            if not installed:
                return False

        return True

    def select_game_launch_config(self, game):
        game_config = game.config.game_level.get("game", {})
        configs = game_config.get("launch_configs")

        def get_preferred_config_index():
            # Validate that the settings are still valid; we need the index to
            # cope when two configs have the same name but we insist on a name
            # match. Returns None if it can't find a match, and then the user
            # must decide.
            preferred_name = game_config.get("preferred_launch_config_name")
            preferred_index = game_config.get("preferred_launch_config_index")

            if preferred_index == 0 or preferred_name == Game.PRIMARY_LAUNCH_CONFIG_NAME:
                return 0

            if preferred_name:
                if preferred_index:
                    try:
                        if configs[preferred_index - 1].get("name") == preferred_name:
                            return preferred_index
                    except IndexError:
                        pass

                for index, config in enumerate(configs):
                    if config.get("name") == preferred_name:
                        return index + 1

            return None

        def save_preferred_config(index):
            name = configs[index - 1].get("name") if index > 0 else Game.PRIMARY_LAUNCH_CONFIG_NAME
            game_config["preferred_launch_config_index"] = index
            game_config["preferred_launch_config_name"] = name
            game.config.save()

        def reset_preferred_config():
            game_config.pop("preferred_launch_config_index", None)
            game_config.pop("preferred_launch_config_name", None)
            game.config.save()

        if not configs:
            return {}  # use primary configuration

        keymap = Gdk.Keymap.get_default()
        if keymap.get_modifier_state() & Gdk.ModifierType.SHIFT_MASK:
            config_index = None
        else:
            config_index = get_preferred_config_index()

        if config_index is None:
            dlg = dialogs.LaunchConfigSelectDialog(game, configs, title=_("Select game to launch"), parent=self)
            if not dlg.confirmed:
                return None  # no error here- the user cancelled out

            config_index = dlg.config_index
            if dlg.dont_show_again:
                save_preferred_config(config_index)
            else:
                reset_preferred_config()

        return configs[config_index - 1] if config_index > 0 else {}
