#!/usr/bin/python3

import json
import os
import re
from functools import lru_cache

HEADER = "# Generated by xdg-retroarch-favourites"
XDG_DIR = "~/.local/share/applications"


def _user_file(path, mode='r'):
    return open(os.path.expanduser(path), mode)


def _user_dir(path):
    return os.path.expanduser(path)


def _find_files(path, ext):
    return (f for f in os.scandir(_user_dir(path)) if f.is_file() and f.name.endswith(ext))


def _pruned(d):
    return {k: v for k, v in d.items() if v}


def _tokenise(console):
    """Convert console name into ;-seperated list"""
    return re.sub(r"[^\w]+", ";", console)


def read_cfg(path):
    """Retroarch CFG format is not compatible with configparser..."""
    split_properties = [line.split("=") for line in _user_file(path)]
    return {key.strip(): value.strip()[1:-1] for key, value in split_properties}


@lru_cache()
def read_lpl(path):
    """Retroarch has two LPL formats..."""
    print("Reading "+path)
    with _user_file(path) as lpl:
        if lpl.readline().startswith('{'):
            lpl.seek(0)
            return json.load(lpl)['items']
        else:
            lpl.seek(0)
            return list(convert_old_lpl_format(lpl))


def convert_old_lpl_format(fd):
    while True:
        entry = {
            'path': fd.readline().strip(),
            'label': fd.readline().strip(),
            'core_path': fd.readline().strip(),
            'core_name': fd.readline().strip(),
            'crc32': fd.readline().strip(),
            'db_name': fd.readline().strip()
        }
        if entry['path']:
            yield entry
        else:
            return


def find_game(game_path):
    """Locate game in playlists (where it has additional labels we want)"""
    for lpl in _find_files(cfg['playlist_directory'], '.lpl'):
        playlist = read_lpl(lpl.path)
        try:
            return next(_pruned(game) for game in playlist if game['path'] == game_path)
        except StopIteration:
            pass


def desktop_entry_for(game):
    library = find_game(game['path'])
    game = {**library, **game}  # Fill in missing detail from library
    print("Matched " + game['label'])

    return f"{game['label']}.desktop", f'''\
{HEADER}
[Desktop Entry]
Version=1.0
Name={game['label']}
Type=Application
Icon={_user_dir(cfg['assets_directory'])}/xmb/retrosystem/png/{game['db_name'][:-4]}.png
Exec=retroarch -L "{game['core_path']}" "{game['path']}"
Terminal=false
StartupNotify=false
Keywords={_tokenise(game['db_name'][:-4])}
Categories=Game;Emulator\
'''


def sync_with_filesystem(retroarch_view):
    """Compare Retroarch favourites to filesystem"""
    for xdg_file in _find_files(XDG_DIR, '.desktop'):
        with open(xdg_file.path, "r+") as h:
            content = h.read()
            if xdg_file.name in retroarch_view:
                if content.startswith(HEADER):
                    if retroarch_view[xdg_file.name] == content:
                        print("Skip   " + xdg_file.path)  # Content identical
                    else:
                        print("Update " + xdg_file.path)  # Content changed
                        h.seek(0)
                        h.write(retroarch_view[xdg_file.name])
                        h.truncate()
                else:
                    print("Exists " + xdg_file.path)  # File exists but not written by us!
                del retroarch_view[xdg_file.name]  # EYECATCHER Remove processed entries from view
            elif content.startswith(HEADER):
                print("Delete " + xdg_file.path)  # No longer in RA favourites
                os.unlink(xdg_file.path)
            else:
                print("Ignore " + xdg_file.path)  # Unrelated desktop entry

    # EYECATCHER Create remaining (unprocessed) entries
    for filename, content in retroarch_view.items():
        with _user_file(f"{XDG_DIR}/{filename}", "w") as f:
            print(f"Create {f.name}")
            f.write(content)


if __name__ == "__main__":
    # Read configs
    cfg = read_cfg('~/.config/retroarch/retroarch.cfg')
    faves = read_lpl(cfg['content_favorites_path'])

    # Retroarch view of expected entries {filename: content}
    retroarch_view = {k: v for k, v in (desktop_entry_for(_pruned(fave)) for fave in faves)}

    sync_with_filesystem(retroarch_view)
