#!/usr/bin/python3

import os

from functools import partial
from operator import contains

import click
import psutil

from acdsc.daemon import ACDaemon
from acdsc.parser import (
    convert_ini_key, get_next_section, make_ac_parser, to_ini)
from acdsc.settings import (
    server_settings, entry_list_settings, weather_settings, session_settings)
from acdsc.steam import has_acds, has_steam, install_steam
from acdsc.steamcmd import (
    assert_working_steamcmd, has_steamcmd, install_steamcmd, update_acds)
from acdsc.validators import SteamPath


## Utils
def echo_section(section, options):
    click.secho('[{}]'.format(section), fg='green')
    # ??
    # max_width = max(map(len, options.keys()))
    max_width = 24
    for key, value in options.items():
        click.echo('{:<{}}: {}'.format(key, max_width, value))
    click.echo()


def dynamic_options(options):
    from click.decorators import option

    def decorator(f):
        for key, value in reversed(options.items()):
            help_msg = '{}  [default: {}]'.format(
                    value['description'], value['default'])
            f = option('--{}'.format(convert_ini_key(key)),
                       type=value['validator'],
                       help=help_msg)(f)
        return f
    return decorator


def all_none(iterable):
    def is_none(item):
        return item is None

    return all(map(is_none, iterable))


def write_config_file(parser, config_file):
    click.secho('Writing new configuration to {}'.format(config_file))
    # TODO: write actual file
    parser.write(open('new.cfg', 'w'))
    # parser.write(open(config_file, 'w'))


def del_section(ctx, parser, config_file, prefix, idx):
    sections = parser.prefix_sections(prefix)
    section = '{}_{}'.format(prefix, idx)
    if section not in sections:
        ctx.fail('{} does not exist!'.format(section))
    parser.remove_section(section)
    write_config_file(parser, config_file)


def list_sections(parser, prefix):
    sections = parser.prefix_sections(prefix)
    for section in sections:
        echo_section(section, dict(parser.items(section)))


def add_section(parser, config_file, name, prefix, settings, **kwargs):
    if all_none(kwargs.values()):
        click.secho('WARNING: no {} options given!'.format(name), fg='yellow')
        click.confirm('Do you really want to add default {}?'.format(name),
                      abort=True)
    sections = parser.prefix_sections(prefix)
    next_section = get_next_section(prefix, sections)
    parser.add_section(next_section)
    for key, value in map(to_ini, kwargs.items()):
        if value is None:
            value = settings[key]['default']
            click.secho(
                'Setting non-given {} to default value {}'.format(key, value),
                fg='yellow')
        else:
            click.echo('Setting {} to {}'.format(key, value))
        parser.set(next_section, key, value)
    click.secho('Added {}'.format(name), fg='green')
    echo_section(next_section, dict(parser.items(next_section)))
    write_config_file(parser, config_file)


def set_in_section(ctx,
                   parser,
                   config_file,
                   idx,
                   name,
                   prefix,
                   settings,
                   **kwargs):
    sections = parser.prefix_sections(prefix)
    section = '{}_{}'.format(prefix, idx)
    if section not in sections:
        ctx.fail('{} does not exist!'.format(section))
    if all_none(kwargs.values()):
        click.secho('WARNING: no {} options given!'.format(name), fg='yellow')
        click.confirm(
            'Do you really want to reset to default {}?'.format(name),
            abort=True)
    for key, value in map(to_ini, kwargs.items()):
        if value is not None:
            click.echo('Setting {} to {}'.format(key, value))
            parser.set(section, key, value)
    write_config_file(parser, config_file)
    echo_section(section, dict(parser.items(section)))
    click.secho('{} values saved!'.format(section), fg='green')


## Click group

@click.group(context_settings={'help_option_names': ['-h', '--help']})
@click.option('--steam-path',
              default=os.path.expanduser('~/Steam'),
              show_default=True,
              type=click.Path(file_okay=False, dir_okay=True, exists=False),
              help='Path to Steam installation')
@click.option('--server-path',
              default='steamapps/common/Assetto Corsa Dedicated Server/',
              show_default=True,
              type=SteamPath(file_okay=False, dir_okay=True, exists=False),
              help='Relative path to --steam-path containing server '
                   'configuration .ini-file')
@click.option('--steamcmd-path',
              default=os.path.expanduser('~/steamcmd'),
              show_default=True,
              type=click.Path(file_okay=False, dir_okay=True, exists=False),
              help='Path to steamcmd installation (will be downloaded '
                   'automatically if it does not exist)')
@click.option('--pidfile',
              default=os.path.expanduser('~/.acds.pid'),
              show_default=True,
              type=click.Path(file_okay=True, dir_okay=False, exists=False),
              help='Path to server PID file')
@click.pass_context
def cli(ctx, steam_path, server_path, steamcmd_path, pidfile):
    # Steam
    if not has_steam():
        install_steam()

    # Steamcmd
    steamcmd = os.path.join(steamcmd_path, 'steamcmd.sh')
    if not has_steamcmd(steamcmd_path):
        install_steamcmd(steamcmd_path)
    assert_working_steamcmd(steamcmd)

    # Assetto Corsa Dedicated Server
    if not has_acds(steam_path, server_path):
        update_acds(steamcmd)

    config_file = os.path.join(
        steam_path, server_path, 'cfg', 'server_cfg.ini')
    entry_list = os.path.join(
        steam_path, server_path, 'cfg', 'entry_list.ini')
    parser = make_ac_parser(config_file)
    entry_parser = make_ac_parser(entry_list)
    ctx.obj = {
        'parser': parser,
        'entry-parser': entry_parser,
        'pidfile': pidfile,
        'config-file': config_file,
        'entry-list': entry_list,
        'server-path': server_path,
        'steamcmd': steamcmd
    }


## Server configuration

@cli.group()
def server():
    pass


## print out current server settings
# acdsc server show
@server.command('current')
@click.pass_obj
def server_list(config):
    click.secho('Current settings:', fg='green')
    for key, value in config['parser'].items('SERVER'):
        click.echo('{:<30}: {}'.format(key, value))


## set setting values
# acdsc server set [--with-defaults] --name "my ac server #001"
# --time-of-day-mult "2" ...
@server.command('set')
@click.option('-w', '--with-defaults',
              is_flag=True,
              default=False,
              show_default=True,
              help='Write ALL default values as well as given values')
@dynamic_options(server_settings)
@click.pass_context
def server_set(ctx, with_defaults, **kwargs):
    parser = ctx.obj['parser']
    # Set configuration values when given
    if not all_none(kwargs.values()):
        for key, value in map(to_ini, kwargs.items()):
            if value is None and not parser.has_option('SERVER', key):
                value = server_settings.get(key, {}).get('default', '')
                click.secho('Setting missing {} to default value {}'.format(
                            key, value))
            else:
                old_value = parser.get('SERVER', key)
                click.secho(
                    'Setting {} from {} => {}'.format(key, old_value, value),
                    fg='yellow')
            parser.set('SERVER', key, value)
        if with_defaults:
            for key, value in server_settings.items():
                click.secho('Setting non-given {} to default value {}'.format(
                            key, value['default']))
                parser.set('SERVER', key, value['default'])
        write_config_file(ctx.obj['parser'], ctx.obj['config-file'])
    ctx.invoke(server_list)


## print out current sessions
def read_sessions(parser):
    valid_sessions = session_settings.keys()
    return sorted(filter(
        partial(contains, valid_sessions), parser.sections()))


# acdsc server session-list
@server.command('session-list')
@click.pass_obj
def session_list(config):
    parser = config['parser']
    for session in read_sessions(parser):
        options = dict(parser.items(session))
        reversed_grid_race = int(parser.get(
                'SERVER', 'REVERSED_GRID_RACE_POSITIONS'))
        if session == 'RACE' and reversed_grid_race != 0:
            session += 'x2'
        echo_section(session, options)


## add a session
# acdsc server add-session
@server.group('add-session')
def add_session():
    pass


# this dude is a distant relative of black adder ^_^
def make_session_adder(session_type, options):
    @add_session.command(session_type)
    @click.option('-f', '--force',
                  is_flag=True,
                  default=False,
                  show_default=True,
                  help='Remove existing session without prompting')
    @dynamic_options(options)
    @click.pass_context
    def session_adder(ctx, force=False, **kwargs):
        parser = ctx.obj['parser']
        if parser.has_section(session_type):
            if not force:
                click.secho(
                    'ERROR: {} session already exists!'.format(session_type),
                    fg='red')
                echo_section(session_type, dict(parser.items(session_type)))
                click.confirm('Do you wish to replace existing session?',
                              abort=True)
            ctx.invoke(del_session, session_type)
        click.secho('Adding {} session:'.format(session_type), fg='yellow')
        section_key = parser.add_section(session_type)
        for key, value in map(to_ini, kwargs.items()):
            if value is None:
                value = options[key]['default']
                click.secho('Setting non-given {} to default value {}'.format(
                            key, value))
            else:
                click.secho('Setting {} to {}'.format(key, value))
            parser.set(section_key, key, value)
        write_config_file(ctx.obj['parser'], ctx.obj['config-file'])
    return session_adder


# dynamically create acdsc server add-session commands for session types
for session_type, options in session_settings.items():
    defname = 'add_{}_session'.format(session_type)
    globals()[defname] = make_session_adder(session_type, options)


## remove a session
# acdsc server del-session
@server.command('del-session')
@click.argument('session-type',
                type=click.Choice(session_settings.keys()))
@click.pass_context
def del_session(ctx, session_type):
    parser = ctx.obj['parser']
    if not parser.has_section(session_type):
        ctx.fail('{} session does not exist!'.format(session_type))
    parser.remove_section(session_type)
    write_config_file(ctx.obj['parser'], ctx.obj['config-file'])


## Weather
@cli.group('weather')
def weather():
    pass


## print out current weathers
# acdsc weather list
@weather.command('list')
@click.pass_obj
def weather_list(config):
    parser = config['parser']
    list_sections(parser, 'WEATHER')


## add a weather
# acdsc weather add
@weather.command('add')
@dynamic_options(weather_settings)
@click.pass_obj
def add_weather(config, **kwargs):
    parser = config['parser']
    config_file = config['config-file']
    add_section(
        parser, config_file, 'weather', 'WEATHER', weather_settings, **kwargs)


## remove a weather
# acdsc weather del
@weather.command('del')
@click.argument('weather-id',
                type=click.IntRange(0),
                required=True)
@click.pass_context
def del_weather(ctx, weather_id):
    del_section(
        ctx, ctx.obj['parser'], ctx.obj['config-file'], 'WEATHER', weather_id)


## set weather
# acdsc weather set
@weather.command('set')
@click.argument('weather-id',
                type=click.IntRange(0),
                required=True)
@dynamic_options(weather_settings)
@click.pass_context
def set_weather(ctx, weather_id, **kwargs):
    set_in_section(ctx,
                   ctx.obj['parser'],
                   ctx.obj['config-file'],
                   weather_id,
                   'weather',
                   'WEATHER',
                   weather_settings,
                   **kwargs)


## Entry list
@cli.group()
def entries():
    pass


## list current entries
# acdsc entries list
@entries.command('list')
@click.pass_obj
def list_entries(config):
    parser = config['entry-parser']
    list_sections(parser, 'CAR')


## add new entry to current entry list
# acdsc entries add
@entries.command('add')
@dynamic_options(entry_list_settings)
@click.pass_context
def add_entry(ctx, **kwargs):
    # TODO: Compare number of entries to current track (also, compare current
    # number of entries to track when changing from server-side settings)
    parser = ctx.obj['entry-parser']
    config_file = ctx.obj['entry-list']
    add_section(
        parser, config_file, 'entry', 'CAR', entry_list_settings, **kwargs)


## remove existing entry from current entry list
# acdsc entries del
@entries.command('del')
@click.argument('entry-id', type=click.IntRange(0), required=True)
@click.pass_context
def del_entry(ctx, entry_id):
    del_section(
        ctx, ctx.obj['entry-parser'], ctx.obj['entry-list'], 'CAR', entry_id)


## set driver details in entry list
# acdsc set-entry --car 0 --drivername "foobar" --ballast 50 ...
@entries.command('set')
@click.argument('entry-id', type=click.IntRange(0), required=True)
@dynamic_options(entry_list_settings)
@click.pass_context
def set_entry(ctx, entry_id, **kwargs):
    set_in_section(ctx,
                   ctx.obj['entry-parser'],
                   ctx.obj['entry-list'],
                   entry_id,
                   'entry',
                   'CAR',
                   entry_list_settings,
                   **kwargs)


## generate anonymous entries to entry list
# acdsc gen-entries car1 car2 car3
@cli.command('gen-entries')
def gen_entries():
    pass


## Server state

## run AC Dedicated Server
# acdsc start -f|-b
@cli.command('start')
@click.option('--autoupdate',
              is_flag=True,
              default=False,
              show_default=True,
              help='Automatically update AC Dedicated Server before launching')
@click.pass_context
def start_server(ctx, autoupdate):
    config = ctx.obj
    if autoupdate:
        update_acds(config['steamcmd'])
    daemon = ACDaemon(config)
    try:
        pid_exists = psutil.pid_exists(daemon.getpid())
    except IOError:
        pid_exists = False
    if pid_exists:
        ctx.fail('Server is already running!')
    daemon.start()
    click.secho('Server is starting', fg='green')
    ctx.invoke(server_status)


## check status (and logs [TODO])
# acdsc status
@cli.command('status')
@click.pass_obj
def server_status(config):
    # TODO: exit codes?
    daemon = ACDaemon(config)
    try:
        pid_exists = psutil.pid_exists(daemon.getpid())
    except IOError:
        pid_exists = False
    if pid_exists:
        click.secho('Server is running', fg='green')
    else:
        click.secho('Server is NOT running', fg='red')


## stop AC Dedicated Server
# acdsc stop
@cli.command('stop')
@click.pass_context
def stop_server(ctx):
    config = ctx.obj
    daemon = ACDaemon(config)
    daemon.stop()
    ctx.invoke(server_status)


## update AC Dedicated Server (you can also use --autoupdate -flag with start)
# acdsc update
@cli.command('update')
@click.pass_obj
def update_server(config):
    update_acds(config['steamcmd'])


if __name__ == '__main__':
    cli()
