#!/usr/bin/python3
# encoding=utf-8
#
# Copyright © 2014-2016 Simon McVittie <smcv@debian.org>
# Copyright © 2015-2016 Alexandre Detiste <alexandre@detiste.be>
# SPDX-License-Identifier: GPL-2.0-or-later

import argparse
import logging
import os
import sys
import textwrap
import time
import zipfile

from .config import (read_config)
from .data import (ProgressCallback)
from .game import (load_games)
from .packaging import (get_packaging_system)
from .paths import (DATADIR)
from .util import (human_size)
from .version import (FORMAT, DISTRO)

logger = logging.getLogger(__name__)

class TerminalProgress(ProgressCallback):
    def __init__(self, interval=0.2, info=None):
        """Constructor.

        Progress will update at most once per @interval seconds, unless we
        are at a "checkpoint".
        """
        self.pad = ' '
        self.interval = interval
        self.ts = time.time()
        self.info = info

    def __call__(self, done=None, total=None, checkpoint=False):
        ts = time.time()

        if done is None or (ts < self.ts + self.interval and not checkpoint):
            return

        if self.info is not None:
            print(self.info, file=sys.stderr)
            self.info = None

        if done is None or total == 0:
            s = ''
        elif total is None or total == 0:
            s = human_size(done)
        else:
            s = '%.0f%% %s/%s' % (100 * done / total,
                    human_size(done),
                    human_size(total))

        self.ts = ts

        if len(self.pad) <= len(s):
            self.pad = ' ' * len(s)

        print(' %s \r %s\r' % (self.pad, s), end='', file=sys.stderr)

    def __enter__(self):
        return self

    def __exit__(self, et=None, ev=None, tb=None):
        self()

def run_command_line():
    logger.debug('Arguments: %r', sys.argv)

    # Don't set any defaults on these base parsers, because that interferes
    # with the ability to recognise the same argument either before or
    # after the game name. Set them on the Namespace instead.
    base_parser = argparse.ArgumentParser(prog='game-data-packager',
            description='Package game files.',
            add_help=False,
            argument_default=argparse.SUPPRESS)

    group = base_parser.add_mutually_exclusive_group()
    group.add_argument('--verbose', action='store_true',
            help='show output from external tools')
    group.add_argument('--no-verbose', action='store_false',
            dest='verbose', help='hide output from external '
             'tools (default)')

    class DebugAction(argparse.Action):
        def __call__(self, parser, namespace, values, option_string=None):
            logging.getLogger().setLevel(logging.DEBUG)

    base_parser.add_argument('--debug', action=DebugAction, nargs=0,
            help='show debug messages')

    game_parser = argparse.ArgumentParser(prog='game-data-packager',
            description='Package game files.',
            add_help=False,
            argument_default=argparse.SUPPRESS,
            parents=(base_parser,))

    game_parser.add_argument('--everything', action='store_true',
            help='Download all possible expansions')
    game_parser.add_argument('--package', '-p', action='append',
            dest='packages', metavar='PACKAGE',
            help='Produce this data package (may be repeated)')

    game_parser.add_argument('--target-format',
            help='Produce packages for this packaging system',
            choices='arch deb rpm'.split())
    game_parser.add_argument('--target-distro',
            help='Produce packages suitable for this distro')
    game_parser.add_argument(
        '--target-architecture',
        help=(
            'Produce packages suitable for this architecture (named '
            'according to either the --target-distro or dpkg conventions)'
        ),
    )

    game_parser.add_argument('--install-method', metavar='METHOD',
            dest='install_method',
            help='Use METHOD (apt, dpkg, gdebi, gdebi-gtk, gdebi-kde) ' +
                'to install packages')

    game_parser.add_argument('--gain-root-command', metavar='METHOD',
            dest='gain_root_command',
            help='Use METHOD (su, sudo, pkexec) to gain root if needed')

    # This is about ability to audit, not freeness. We don't have an
    # option to restrict g-d-p to dealing with Free Software, because
    # that would rule out the vast majority of its packages: if a game's
    # data is Free Software, we could put it in main or contrib and not need
    # g-d-p at all.
    game_parser.add_argument('--binary-executables', action='store_true',
            help='allow installation of executable code that was not built ' +
                'from public source code')

    # Misc options
    group = game_parser.add_mutually_exclusive_group()
    group.add_argument('-i', '--install', action='store_true',
            help='install the generated package')
    group.add_argument('-n', '--no-install', action='store_false',
            dest='install',
            help='do not install the generated package (requires -d, default)')
    game_parser.add_argument('-d', '--destination', metavar='OUTDIR',
            help='write the generated .%s(s) to OUTDIR' % FORMAT)

    group = game_parser.add_mutually_exclusive_group()
    group.add_argument('-z', '--compress', action='store_true',
            help='compress generated .%s (default if -d is used)' % FORMAT)
    group.add_argument('--no-compress', action='store_false',
            dest='compress',
            help='do not compress generated .%s (default without -d)' % FORMAT)

    group = game_parser.add_mutually_exclusive_group()
    group.add_argument('--download', action='store_true',
            help='automatically download necessary files if possible ' +
                '(default)')
    group.add_argument('--no-download', action='store_false',
            dest='download', help='do not download anything')
    game_parser.add_argument('--save-downloads', metavar='DIR',
            help='save downloaded files to DIR, and look for files there')

    group = game_parser.add_mutually_exclusive_group()
    group.add_argument('--search', action='store_true', default=True,
        help='look for installed files in Steam and other likely places ' +
            '(default)')
    group.add_argument('--no-search', action='store_false',
        dest='search',
        help='only look in paths provided on the command line')

    class DumbParser(argparse.ArgumentParser):
        def error(self, message):
            pass

    dumb_parser = DumbParser(parents=(game_parser,),add_help=False)
    dumb_parser.add_argument('game', type=str, nargs='?')
    dumb_parser.add_argument('paths', type=str, nargs='*')
    dumb_parser.add_argument('-h', '--help', action='store_true', dest='h')
    try:
        g = dumb_parser.parse_args().game
    except TypeError:
        g = None
    zip = os.path.join(DATADIR, 'vfs.zip')
    if g is None:
        games = load_games()
    elif '-h' in sys.argv or '--help' in sys.argv:
        games = load_games()
    elif os.path.isfile(os.path.join(DATADIR, '%s.json' % g)):
        games = load_games(game=g)
    elif not os.path.isfile(zip):
        games = load_games()
    else:
        with zipfile.ZipFile(zip, 'r') as zf:
            if '%s.json' % g in zf.namelist():
                games = load_games(game=g)
            else:
                games = load_games()

    parser = argparse.ArgumentParser(prog='game-data-packager',
            description='Package game files.', parents=(game_parser,),
            epilog='Run "game-data-packager GAME --help" to see ' +
                'game-specific arguments.')

    game_parsers = parser.add_subparsers(dest='shortname',
            title='supported games', metavar='GAME')

    for g in sorted(games.keys()):
        games[g].add_parser(game_parsers, game_parser)

    # GOG meta-mode
    gog_parser = game_parsers.add_parser('gog',
        help='Package all your GOG.com games at once',
        description='Automatically package all your GOG.com games',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        parents=(game_parser,))
    group = gog_parser.add_mutually_exclusive_group()
    group.add_argument('--all', action='store_true', default=False,
                       help='show all GOG.com games')
    group.add_argument('--new', action='store_true', default=False,
                       help='show all new GOG.com games')

    # Steam meta-mode
    steam_parser = game_parsers.add_parser('steam',
        help='Package all Steam games at once',
        description='Automatically package all your Steam games',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        parents=(game_parser,))
    group = steam_parser.add_mutually_exclusive_group()
    group.add_argument('--all', action='store_true', default=False,
                       help='package all Steam games')
    group.add_argument('--new', action='store_true', default=False,
                       help='package all new Steam games')

    mt_parser = game_parsers.add_parser('make-template',
        help='Capture details of a new game or update an existing one',
        description=textwrap.dedent('''\
        This command collects details of game files in a directory or archive,
        so that they can be used for a newly supported game or added to an
        existing game. Please send its output to the Debian bug tracking system
        as a bug with 'wishlist' severity.\
        '''),
        formatter_class=argparse.RawDescriptionHelpFormatter,
        parents=(base_parser,))
    mt_parser.add_argument('args', nargs='*', metavar='DEB|DIRECTORY|FILE')
    mt_parser.add_argument(
        '--strip-path', action='append', dest='strip_paths', default=[],
        help='Strip a prefix from all filenames (may be repeated)')
    mt_parser.add_argument(
        '--base', default=None, help='Base the template on an existing game')
    mt_parser.add_argument(
        '--template', action='append', dest='templates', default=[],
        help='Load pre-generated templates')
    mt_parser.add_argument(
        '-l', '--lower', action='store_true', dest='lower',
        help='make all files lowercase')
    mt_parser.add_argument(
        '-e', '--execute', action='store_true', dest='execute',
        help='run this game through strace and see which files from '
             '/usr/share/games or /usr/local/games are needed')
    mt_parser.add_argument(
        '-f', '--flacsums', action='store_true', dest='flacsums',
        help='compute "flacsums" from .wav files')

    config = read_config()
    parsed = argparse.Namespace(
            binary_executables=False,
            compress=None,
            destination=None,
            download=True,
            everything=False,
            verbose=False,
            install=False,
            install_method='',
            gain_root_command='',
            packages=[],
            save_downloads=None,
            shortname=None,
            target_architecture=None,
            target_format=FORMAT,
            target_distro=DISTRO,
    )
    if config['install']:
        logger.debug('obeying INSTALL=yes in configuration')
        parsed.install = True
    if config['preserve']:
        logger.debug('obeying PRESERVE=yes in configuration')
        parsed.destination = '.'
    if config['verbose']:
        logger.debug('obeying VERBOSE=yes in configuration')
        parsed.verbose = True
    if config['install_method']:
        logger.debug('obeying INSTALL_METHOD=%r in configuration',
                config['install_method'])
        parsed.install_method = config['install_method']
    if config['gain_root_command']:
        logger.debug('obeying GAIN_ROOT_COMMAND=%r in configuration',
                config['gain_root_command'])
        parsed.gain_root_command = config['gain_root_command']

    parser.parse_args(namespace=parsed)
    logger.debug('parsed command-line arguments into: %r', parsed)

    if (parsed.destination is None and not parsed.install and
            parsed.shortname != 'make-template'):
        logger.error('At least one of --install or --destination is required')
        sys.exit(2)

    if parsed.shortname is None:
        parser.print_help()
        sys.exit(0)

    for arg, path in (('--save-downloads', parsed.save_downloads),
                      ('--destination', parsed.destination)):
        if path is None:
            continue
        elif not os.path.isdir(path):
            logger.error('argument "%s" to %s does not exist', path, arg)
            sys.exit(2)
        elif not os.access(path, os.W_OK | os.X_OK):
            logger.error('argument "%s" to %s is not writable', path, arg)
            sys.exit(2)
        elif path == '.':
            try:
                os.getcwd()
            except FileNotFoundError:
                logger.error('argument "%s" to %s has been deleted', path, arg)
                sys.exit(2)

    if parsed.shortname == 'steam':
        from .steam import (run_steam_meta_mode)
        run_steam_meta_mode(parsed, games)
        return
    elif parsed.shortname == 'gog':
        from .gog import (run_gog_meta_mode)
        run_gog_meta_mode(parsed, games)
        return
    elif parsed.shortname == 'make-template':
        import game_data_packager.make_template
        game_data_packager.make_template.main(parsed, games)
        return
    elif parsed.shortname in games:
        game = games[parsed.shortname]
    else:
        parsed.package = parsed.shortname
        for game in games.values():
            if parsed.shortname in game.packages:
                break
            if parsed.shortname in game.aliases:
                break
        else:
            raise AssertionError('could not find %s' % parsed.shortname)

    with game.construct_task(
        packaging=get_packaging_system(
            parsed.target_format,
            parsed.target_distro,
            parsed.target_architecture,
        ),
    ) as task:
        if sys.stderr.isatty():
            task.progress_factory = TerminalProgress

        task.run_command_line(parsed)

if __name__ == '__main__':
    run_command_line()
