"""Keep track of game executables' presence"""

import json
import os
import time

from lutris import settings
from lutris.database.games import get_games
from lutris.game import Game
from lutris.gui.widgets import NotificationSource
from lutris.util import cache_single
from lutris.util.jobs import AsyncCall
from lutris.util.log import logger

GAME_PATH_CACHE_PATH = os.path.join(settings.CACHE_DIR, "game-paths.json")


def get_game_paths():
    game_paths = {}
    all_games = get_games(filters={"installed": 1})
    for db_game in all_games:
        if db_game.get("runner") in ("steam", "web"):
            continue
        game = Game(db_game["id"])
        path = game.get_path_from_config()
        if not path:
            continue
        game_paths[db_game["id"]] = path
    return game_paths


def build_path_cache(recreate=False):
    """Generate a new cache path"""
    if os.path.exists(GAME_PATH_CACHE_PATH) and not recreate:
        return
    start_time = time.time()
    with open(GAME_PATH_CACHE_PATH, "w", encoding="utf-8") as cache_file:
        game_paths = get_game_paths()
        json.dump(game_paths, cache_file, indent=2)
    end_time = time.time()
    get_path_cache.cache_clear()
    logger.debug("Game path cache built in %0.2f seconds", end_time - start_time)


def add_to_path_cache(game):
    """Add or update the path of a game in the cache"""
    logger.debug("Adding %s to path cache", game)
    path = game.get_path_from_config()
    if not path:
        logger.warning("No path for %s", game)
        return
    current_cache = read_path_cache()
    current_cache[game.id] = path
    with open(GAME_PATH_CACHE_PATH, "w", encoding="utf-8") as cache_file:
        json.dump(current_cache, cache_file, indent=2)
    get_path_cache.cache_clear()


@cache_single
def get_path_cache():
    """Return the contents of the path cache file; this
    dict is cached, so do not modify it."""
    return read_path_cache()


def read_path_cache():
    """Read the contents of the path cache file, and does not cache it."""
    with open(GAME_PATH_CACHE_PATH, encoding="utf-8") as cache_file:
        try:
            return json.load(cache_file)
        except json.JSONDecodeError:
            return {}


def remove_from_path_cache(game):
    logger.debug("Removing %s from path cache", game)
    current_cache = read_path_cache()
    if game.id not in current_cache:
        logger.warning("Game %s (id=%s) not in cache path", game, game.id)
        return
    del current_cache[game.id]
    with open(GAME_PATH_CACHE_PATH, "w", encoding="utf-8") as cache_file:
        json.dump(current_cache, cache_file, indent=2)
    get_path_cache.cache_clear()


class MissingGames:
    """This class is a singleton that holds a set of game-ids for games whose directories
    are missing. It is updated on a background thread, but there's a NotificationSource ('updated')
    that fires when that thread has made changes and exited, so that the UI cab update then."""

    def __init__(self):
        self.updated = NotificationSource()
        self.missing_game_ids = set()
        self._update_running = None

    @property
    def is_initialized(self):
        """True if the missing games have ever been updated."""
        return self._update_running is not None

    def update_all_missing(self) -> None:
        """This starts the check for all games; the actual list of game-ids will be obtained
        on the worker thread, and this method will start it."""

        if not self._update_running:
            self._update_running = True
            AsyncCall(self._update_missing_games, self._update_missing_games_cb)

    def _update_missing_games(self):
        """This is the method that runs on the worker thread; it checks each game given
        and returns True if any changes to missing_game_ids was made."""

        logger.debug("Checking for missing games")

        changed = False

        for game_id, path in get_path_cache().items():
            if path:
                old_status = game_id in self.missing_game_ids
                new_status = not os.path.exists(path)
                if old_status != new_status:
                    if new_status:
                        self.missing_game_ids.add(game_id)
                    else:
                        self.missing_game_ids.discard(game_id)
                    changed = True
        return changed

    def _update_missing_games_cb(self, changed, error):
        self._update_running = False

        if error:
            logger.exception("Unable to detect missing games: %s", error)
        elif changed:
            self.updated.fire()


MISSING_GAMES = MissingGames()
