#!/usr/bin/env python
# vim: set fileencoding=utf-8 et sw=4 ts=4 ft=python:

from __future__ import print_function
import datetime
import hashlib
import logging
import os
import pickle
import re
import requests
import sys
import textwrap
import time
import yaml


logging.basicConfig(level=logging.INFO)
logging.getLogger('requests').setLevel(logging.WARNING)
LOG = logging.getLogger('mu-online')


def print_help():
    print(textwrap.dedent('''
    Usage: mu-online [opts/actions]


    Actions:
     *default*:           if no action is specified,
                          list of characters which are online/in-game
                          will be fetched and printed

     --help:              show this help and quit

     --reset <charid>:    reset characters level AND distribute stat points,
                          based on what is in config file,
                          dict key in config has to match given <charid>

     --addstats <charid>: distribute stat points based on config file,
                          always adds the points defined there, regardless
                          what the current character stats are,
                          same as --reset without doing reset first

     --acc <search_term>: search for characters and accounts containing
                          the search_term in their name (case insensitive),
                          print account and all it's characters for all matches

                          Note: as this may cause multiple slow requests
                            going to the servers web site,
                            this always reads just from cache by default,
                            so for reloading the info from server,
                            use *fresh* opt described below.


    General options:
     --srv <id>:     id of server (otherwise value of "default" from config
                     file is used)

    --debug:         enable verbose debugging info


    Options for listing of online accounts:

     --fresh:   ignore local cache and force refresh from server

     --sort:    sort list of online players based on specified key:
                [name, level, reset, class, guild, map,
                 time, exp_speed, eta]

     --auto:    keep running and automatically refreshing online list


    All options may be specified with or without two-dashes prefix.


    Examples:

     mu-online  ... list online players
     mu-online fresh sort map  ... list online ordered by map name,
                                   local cache ignored
     mu-online reset liu  ... reset char liu (see config file below)


    If you don\'t have ~/.muonline.yaml config yet,
     you have to create one!

    Example ~/.muonline.yaml content (for DreamMu.cz):
    ---
    default: dream
    dream:
        info: [Chiana, Someone, Like, Mendri, Or, eRoo]
        warn: [*admin*, Explosion]
        acc: <---YOUR-LOGIN--->
        pass: <---YOUR-PASSWORD--->
        res_stats:
            liu:
                name: LiuKan
                rid: <---optional, find this number in reset character url--->
                str: 30000
                agi: 30000
                vit: 20000
                eng: 30000
    '''))
    sys.exit(1)


def any(where, *what):
    """
    Search 'where' for any of the followup arguments.

    If only one additional argument (after 'where') is
    provided but not matching, it is also iterated over.
    So both forms: any(argv, '--help', '-h', 'help')
    or any(argv, cmd_opts) can be used.
    """
    if len(what) == 1 and what not in where:
        what = what[0]
    try:
        for item in what:
            if item in where:
                return True
    except TypeError:
        pass


def key(key_name):
    """
    Creates getter method for a given key.

    >>> nameof = key('name')
    >>> nameof({'age':99, 'name':'Josh'})
    'Josh'
    >>> map(nameof, [{'a':1, 'name':'A'}, {'b':2, 'name':'B'}])
    ['A', 'B']
    """
    def _fetch(item):
        return item[key_name]
    return _fetch


def tr(struct, key):
    """
    Get value for `key` from `struct` or the `key` itself.

    Just shortcut for dict.get(key, key):
    >>> some = {'a':'no'}
    >>> tr(some, 'x')
    'x'
    >>> tr(some, 'a')
    'no'
    """
    return struct.get(key, key)


def delta_str(timedelta):
    s = timedelta.seconds
    return '%d:%02d' % (
        s / 60,
        s % 60)


REG_HELPERS = {
    'attrs': "[^>]*",
    'in_tag': "[^<]*"
}
CLASS_MARKS = {
    'Dark Knight': 'DK',
    'Blade Knight': 'BK',
    'Blade Master': 'BM',

    'Fairy Elf': 'FE',
    'Muse Elf': 'ME',
    'High Elf': 'HE',

    'Dark Wizard': 'DW',
    'Soul Master': 'SM',
    'Grand Master': 'GM',

    'Magic Gladiator': 'MG',
    'Duel Master': 'DM',

    'Dark Lord': 'DL',
    'Lord Emperor': 'LE',

    'Summoner': 'SU',
    'Bloody Summoner': 'BS',
    'Dimension Master': 'iM',
}
COLOR = {
    'RED': '\033[1;31m',
    'GRAY': '\033[1;30m',
    'YELLOW': '\033[1;33m',
    'BLUE': '\033[1;34m',
    'BLACK': '\033[0m',
}


class RemoteServer(object):
    SID = 'default'
    URL = None
    RESET_LEVEL = 400
    RESET_LEVEL_VIP = 370

    def __init__(self, cfg):
        super(RemoteServer, self).__init__()
        self.cfg = cfg
        self.own_chars = [char['name'] for char
                          in self.cfg.get('res_stats', {}).values()]
        self.no_auth = False
        self.__session = None

    def cache_path(self, content_type):
        if content_type == 'online':
            suff = ''
        else:
            suff = '_%s' % content_type

        cache_path = os.path.expanduser('~/.cache/mu-online')
        os.path.isdir(cache_path) or os.mkdir(cache_path)
        cache_path = '%s/%s%s.pck' % (cache_path, self.SID, suff)
        return cache_path

    @property
    def online_cache_path(self):
        return self.cache_path('online')

    @property
    def account_cache_path(self):
        return self.cache_path('accounts')

    @property
    def session(self):
        if self.__session is None:
            self.__session = requests.Session()
            if not self.no_auth:
                self.login()
        return self.__session

    def get(self, url):
        reply = self.session.get(self.URL % url)
        self.check_err(reply)
        return reply

    def post(self, url, data=None):
        reply = self.session.post(self.URL % url, data=data)
        self.check_err(reply)
        return reply

    def char(self, char_key):
        return self.cfg['res_stats'][char_key]

    def get_reset_level(self, char_name):
        if self.cfg['vip']:
            if char_name in self.own_chars:
                return self.RESET_LEVEL_VIP
        return self.RESET_LEVEL

    def grep_table(self, source, start_reg, stop_reg):
        lines = source.split('\n')
        table_lines = []
        for line in lines:
            if not table_lines:
                if start_reg.match(line):
                    table_lines.append(line)
            else:
                table_lines.append(line)
                if stop_reg.match(line):
                    break
        return ''.join(table_lines)

    def parse_charinfo(self, table, char_reg):
        chars = [char.groupdict()
                 for char
                 in char_reg.finditer(table)]
        for ch in chars:
            for k, v in ch.items():
                v = v.strip()
                if k in ['reset', 'level']:
                    v = int(v)
                ch[k] = v
            self.fill_char_info(ch)
        return chars

    def fill_char_info(self, char, admin=False):
        char.setdefault('acc', '*admin*' if admin else '')
        if 'class' in char:
            char['class_mark'] = tr(CLASS_MARKS, char['class'])
        char.setdefault('guild', '')
        char.setdefault('class', '')
        char.setdefault('class_mark', '')
        char.setdefault('level', 400 if admin else 1)
        char.setdefault('reset', 999 if admin else 0)
        char.setdefault('exp_speed', 0)
        char.setdefault('eta', 0)
        char.setdefault('map', '')
        char.setdefault('time', '')

    def check_err(self, reply):
        reply.encoding = 'UTF-8'
        webpage_err = self.parse_errmsg(reply.text)
        if webpage_err is not None:
            found = webpage_err.groupdict()
            if 'msg' not in found:
                LOG.warning('Servery reply: \n%s', reply.text)
                raise ValueError('Unrecognized server error in reply.')

            LOG.debug(found['msg'].encode('utf-8'))
            raise ValueError(found['msg'].encode('utf-8'))

    def parse_errmsg(self, source):
        pass


class DreamMuServer(RemoteServer):
    SID = 'dream'
    URL = 'http://dreammu.cz/index.php?%s'

    def __init__(self, *args, **kwargs):
        super(DreamMuServer, self).__init__(*args, **kwargs)

    def online_url(self):
        return self.URL % 'page_id=onlinehraci'

    def login(self):
        LOG.debug('Logging in acc %s', self.cfg['acc'])
        data = {
            'uss_id': None,
            'uss_password': None,
            'Submit': 'Prihlásiť',
            'process_login': ''}
        data['uss_id'] = self.cfg['acc']
        data['uss_password'] = self.cfg['pass']
        reply = self.post('page_id=login', data)
        if 'page_id=login' in reply.url:
            raise ValueError('Authentication failed, check if You'
                             ' specified correct account and password!')
        LOG.info('Logged in as %s', self.cfg['acc'])

    def _rid(self, char_info):
        """Fetch special character-request ID"""
        LOG.debug('searching for %s\'s rid ...', char_info['name'])
        if 'rid' in char_info:
            LOG.debug('rid %s defined in config' % char_info['rid'])
            return char_info['rid']
        reply = self.get(self.res_baseurl)
        found = re.search(('<td.*?iR_name".*?>%s</td>.*?<input'
                          ' type="button".*?onclick=".*?rid=([0-9]+)\'"')
                          % char_info['name'],
                          reply.text,
                          re.DOTALL)
        if not found:
            LOG.warning(reply.text)
            raise ValueError(('Failed to find RID for %s,'
                              ' and it was not specified in config.')
                             % char_info['name'])

        LOG.info('rid for %s found: %s' % (char_info, found.group(1)))
        return int(found.group(1))

    def reset(self, char_key):
        char = self.char(char_key)
        LOG.debug('Going to reset char %s ...' % char['name'])
        self.get('%s&rid=%d' % (self.res_baseurl, self._rid(char)))
        LOG.info('Level reset done for character %s.' % char['name'])

    @property
    def res_baseurl(self):
        return ('page_id=user_cp&panel=reset_character%s'
                % ('_vip' if self.cfg['vip'] else ''))

    def add_stats(self, char_key):
        char = self.char(char_key)
        LOG.debug('Adding stats ...')
        LOG.debug(yaml.dump(char))

        sub_url = 'page_id=user_cp&panel=add_points'
        data = {
            'levelup_id': None,
            'token': None,
            'levelup_add': '',
            'submit': "Pridaj body"}
        data.update(char)

        char_form_reg = re.compile((
            '<td{attrs}class="iR_name"{attrs}>'
            + char['name']
            + '<input type="hidden" name="levelup_id" value="(?P<id>[0-9]+)">'
            + '<input type="hidden" name="levelup_add">'
            + '<input type="hidden" name="token" value="(?P<token>[^"]+)">'
            + '</td>').format(**REG_HELPERS))

        reply = self.get(sub_url)
        found = char_form_reg.search(reply.text)
        if not found:
            raise ValueError('Unable to find correct "add_points" form!')

        data['levelup_id'] = str(found.group('id'))
        data['token'] = str(found.group('token'))

        reply = self.post(sub_url, data)

        LOG.info('Stats added.')

    def char_info(self, char_key):
        name = self.char(char_key)['name']

        reply_res = self.get('page_id=user_cp&panel=reset_character')
        found_res = re.search(('<td.*?iR_name".*?>%s</td>'
                               '.*?iR_stats">Level: ([0-9]+)</td>'
                               '.*?iR_stats">Zen: ([0-9,]+)</td>'
                               '.*?iR_stats">Resets: ([0-9]+)</td>')
                              % name,
                              reply_res.text,
                              re.DOTALL)
        return {
            'name': name,
            'level': int(found_res.group(1)),
            'reset': int(found_res.group(3)),
            'zen': int(found_res.group(2)
                       .replace(',', '')
                       .replace(' ', ''))
        }

    def grep_table(self, source):
        return super(DreamMuServer, self).grep_table(
            source,
            re.compile('<table.*class="themain".*'),
            re.compile('</table>'))

    def parse_charinfo(self, source):
        return super(DreamMuServer, self).parse_charinfo(
            source,
            re.compile(
                ('<tr>'
                 '\W*<td{attrs}>{in_tag}</td>'
                 '\W*<td{attrs}>(?P<name>{in_tag})</td>'
                 '\W*<td{attrs}>(?P<level>{in_tag})</td>'
                 '\W*<td{attrs}>(?P<reset>{in_tag})</td>'
                 '\W*<td{attrs}>(?P<class>{in_tag})</td>'
                 '\W*<td{attrs}>(?P<guild>{in_tag})</td>'
                 '\W*<td{attrs}>(?P<map>{in_tag})</td>'
                 '\W*<td{attrs}>(?P<time>{in_tag})</td>'
                 '\W*<td{attrs}>(<img {attrs}>)?</td>'
                 '\W*</tr>').format(**REG_HELPERS)))

    def parse_char_acc_info(self, source):
        acc_char_pattern = re.compile((
            '<tr>'
            '\W*<td{attrs} class="iR_rank">[0-9]+</td>'
            '\W*<td{attrs} class="iR_name" >(?P<name>{in_tag})</td>'
            '\W*<td{attrs}><img{attrs}></td>'
            '\W*<td{attrs} class="iR_stats" >Sila: (?P<stat_str>{in_tag})'
            '</td>'
            '\W*<td{attrs} class="iR_stats" >Vitalita: (?P<stat_vit>{in_tag})'
            '</td>'
            '\W*<td{attrs} class="iR_stats" >Guilda: (?P<guild>{in_tag})</td>'
            '\W*</tr>'
            '\W*<tr>'
            '\W*<td{attrs} class="iR_class">(?P<class>{in_tag})</td>'
            u'\W*<td{attrs} class="iR_stats">Obratnosť: (?P<stat_agi>{in_tag})'
            '</td>'
            '\W*<td{attrs} class="iR_stats">Energia: (?P<stat_ene>{in_tag})'
            '</td>'
            '\W*<td{attrs} class="iR_stats_level">Level (?P<level>{in_tag})'
            '</td>'
            '\W*</tr>'
            '\W*<tr>'
            '\W*<td{attrs} class="iR_status"{attrs}>(?P<accname>{in_tag})'
            '</td>'
            '\W*<td{attrs} class="iR_status"><div{attrs}>'
            u'<a{attrs}get\.php\?aM=(?P<charid>[0-9]+){attrs}>Lokácia</a>'
            '</div></td>'
            '\W*<td{attrs} class="iR_stats">Com: (?P<stat_cmd>{in_tag})</td>'
            u'\W*<td{attrs} class="iR_stats_reset">Počet resetov:'
            ' (?P<reset>{in_tag})</td>'
            '\W*</tr>'
        ).format(**REG_HELPERS))
        chars = super(DreamMuServer, self).parse_charinfo(
            source, acc_char_pattern)

        return chars

    def fetch_admins(self):
        self.no_auth = True
        reply = self.get('page_id=dreammu_team')
        return self.parse_admins(reply.text)

    def parse_admins(self, source):
        admins = []
        patt = re.compile((
            '<tr>'
            '<td{attrs}><span{attrs}>(?P<role>{in_tag})</span></td>'
            '<td{attrs}><span{attrs}> <b>(?P<name>{in_tag})</b> </span></td>'
            '<td{attrs}>(?P<email>{in_tag})</td>'
            '<td{attrs}><span{attrs}>(?P<status>{in_tag})</span></td>'
            '</tr>').format(**REG_HELPERS))
        for admin in patt.finditer(source):
            admin_obj = {
                'name': admin.group('name'),
                'role': admin.group('role'),
                'email': admin.group('email'),
                'online': ('online' == admin.group('status').strip().lower()),
            }
            self.fill_char_info(admin_obj, admin=True)
            admins.append(admin_obj)
        return admins

    def acc_char_list(self, refresh=False):
        acc_chars = {}

        if not refresh:
            try:
                with open(self.account_cache_path) as cache:
                    acc_chars = pickle.Unpickler(cache).load()
            except IOError:
                pass

        if not acc_chars:
            class_types = [  # noqa
                0, 1, 3,  # dw, sm, gm
                16, 17, 19,  # dk, bk, bm
                32, 33, 35,  # elf, me, he
                48, 50,  # mg, dm
                64, 66,  # dl, le
                80, 81, 83,  # su, bs, DimM
            ]

            # http://dreammu.cz/index.php?page_id=rankings&rank=characters&class=0
            rankings_url = 'page_id=rankings&rank=characters&class=%d'
            self.no_auth = True
            for class_type in class_types:
                LOG.info('Loading info for character class %d', class_type)
                url = rankings_url % class_type
                reply = self.get(url)
                src = ''.join(reply.text.split('\n'))

                chars = self.parse_char_acc_info(src)

                for char in chars:
                    acc_chars.setdefault(char['accname'], []).append(
                        char)

            with open(self.account_cache_path, 'w') as cache:
                pickle.Pickler(cache).dump(acc_chars)

        return acc_chars

    def parse_errmsg(self, source):
        return re.compile(
            ('<div class="msg_error"{attrs}>'
             '(?P<msg>(?!Typ VIP{in_tag}){in_tag})'
             '</div>').format(**REG_HELPERS)).search(source)


class DaemonicServer(RemoteServer):
    SID = 'daemu'
    URL = 'http://www.daemu.cz/%s'

    def __init__(self, *args, **kwargs):
        super(DaemonicServer, self).__init__(*args, **kwargs)

    def online_url(self):
        return self.URL % 'online.html'

    def grep_table(self, source):
        return super(DaemonicServer, self).grep_table(
            source,
            re.compile('<h1>Hráči online</h1>'),
            re.compile('<b>Online:</b>.*'))
        # re.compile('<table.*class="themain".*'),
        # re.compile('</table>'))

    def parse_charinfo(self, source):
        return super(DaemonicServer, self).parse_charinfo(
            source,
            re.compile('<tr>'
                       '<td{attrs}>'
                       '<img{attrs}>'
                       '\W*<a{attrs}>(?P<name>{in_tag})</td>'
                       '<td>(?P<map>{in_tag})</td>'
                       '<td>(?P<reset>{in_tag})</td>'
                       '<td>(?P<level>{in_tag})</td>'
                       '<td>(?P<time>{in_tag})</td>'
                       '</tr>'.format(**REG_HELPERS)))

    def login(self):
        LOG.debug('Logging in acc %s', self.cfg['acc'])
        data = {
            'ulogin': None,
            'uheslo': None,
            'trvale': 'None'}
        data['ulogin'] = self.cfg['acc']
        data['uheslo'] = self.cfg['pass']
        reply = self.post('login.html', data)
        if 'page_id=login' in reply.url:
            raise ValueError('Authentication failed, check if You'
                             ' specified correct account and password!')
        LOG.info('Logged in as %s', self.cfg['acc'])

    def reset(self, char_key):
        char = self.char(char_key)
        LOG.debug('Going to reset char %s ...' % char['name'])
        self.get('page_id=user_cp&panel=reset_character&rid=%d'
                 % self._rid(char))
        LOG.info('Level reset done for character %s.' % char['name'])

    def add_stats(self, char_key):
        LOG.warning('add_stats not implemented yet!')

    def char_info(self, char_key):
        LOG.warning('char_info not implemented yet!')
        return {
            'name': '',
            'level': 0,
            'reset': 0,
            'zen': 0
        }


def fetch_page(url, refresh=False):
    content = None
    cache_path = os.path.join(
        '/tmp',
        'muonlinelist_%d_%s' % (os.geteuid(),
                                hashlib.md5(url).hexdigest()))
    max_cache = datetime.timedelta(minutes=2)
    now = datetime.datetime.now()
    if not refresh:
        try:
            last = datetime.datetime.fromtimestamp(
                os.stat(cache_path).st_mtime)
            age = (now - last)
            if age < max_cache:
                with open(cache_path) as cache_file:
                    content = cache_file.read()
                    LOG.warn('%s-- cached %s --%s'
                             % (COLOR['RED'],
                                delta_str(age),
                                COLOR['BLACK']))
        except (IOError, OSError):
            content = None
    if content is None:
        content = requests.get(url).text
        with open(cache_path, 'w') as cache_file:
            cache_file.write(content.encode('utf-8'))
    fetched_at = datetime.datetime.fromtimestamp(
        os.stat(cache_path).st_mtime)
    return (content, fetched_at)


def print_chars(characters, info=None, warn=None):
    props = {
        'ln_n': 1,  # longest_name
        'ln_g': 1,  # longest_guild_name
        'mark': '',
    }

    info = [info_str.lower() for info_str in info]
    warn = [warn_str.lower() for warn_str in warn]

    for ch in characters:
        props['ln_n'] = max(props['ln_n'],
                            len(ch['name']))
        props['ln_g'] = max(props['ln_g'],
                            len(ch['guild']))
    for ch in characters:
        ch.update(props)

        name = ch['name'].lower()
        acc = ch.get('acc', '').lower()
        if name in info or acc in info:
            ch['mark'] = COLOR['BLUE']
            ch['mark_end'] = COLOR['BLACK']
        if name in warn or acc in warn:
            ch['mark'] = COLOR['YELLOW']
            ch['mark_end'] = COLOR['BLACK']
        if 'mark_end' not in ch:
            ch['mark_end'] = ch['mark']
        try:
            print(("{mark} {name:{ln_n}} ({class_mark})"
                   "  {level:<3} {reset:>3}"
                   "  lpm:{exp_speed:.2f} eta_min:{eta:.2f}"
                   "  _{acc}_"
                   "  [{guild:{ln_g}}]"
                   "  in {map:<16}"
                   "  from {time} {mark_end}").format(
                **ch))
        except Exception:
            LOG.exception('Unable to format character')
            for k in ch:
                LOG.warn("%s: %s => %s" % (k, type(ch[k]), ch[k]))


def print_acc_chars(acc_chars):
    for acc_name, chars in acc_chars.items():
        print(acc_name)
        for char in chars:
            print(('  {name} [{guild}]'
                   '  {level} /{reset}'
                   '  {class_mark}'
                   ).format(**char))


def compare_last(srv, chars, lastchanged=None):
    last_chars = {}
    try:
        with open(srv.online_cache_path) as cache:
            last_chars = pickle.Unpickler(cache).load()
    except IOError:
        pass

    for char in chars:
        # add char to history if not there yet
        if char['name'] not in last_chars:
            char['lastchanged'] = lastchanged
            char['exp_speed'] = 0
            last_chars[char['name']] = char

        lchar = last_chars[char['name']]

        if lchar['lastchanged'] == lastchanged:
            char['exp_speed'] = lchar['exp_speed']
        else:
            # time diff
            age = lastchanged - lchar['lastchanged']
            age = max(0.0, age.total_seconds() / 60.0)

            # level diff
            if char['reset'] != lchar['reset']:
                lvldiff = 0
                LOG.debug('%s: reset mismatch: <%s:%s> <%s:%s>',
                          char['name'],
                          type(char['reset']), char['reset'],
                          type(lchar['reset']), lchar['reset'])
            else:
                lvldiff = char['level'] - lchar['level']
                LOG.debug('%s: lvldiff=%s', char['name'], lvldiff)
                lvldiff = max(0, lvldiff)

            LOG.debug(('%s: age=%.4f lvldiff=%d lvl=%d llvl=%d'
                       ' lastchanged=%s beforechanged=%s'),
                      char['name'], age, lvldiff,
                      char['level'], lchar['level'],
                      lastchanged, lchar['lastchanged'])

            # level-per-minute exping speed
            exp_speed = 0
            if age <= 0:
                exp_speed = 0
            else:
                exp_speed = lvldiff / age

            # pass exp speed to current charlist
            char['exp_speed'] = exp_speed
            # store exp_speed, level and lastchanged for next time
            lchar['exp_speed'] = exp_speed
            lchar['level'] = char['level']
            lchar['reset'] = char['reset']
            lchar['lastchanged'] = lastchanged
        if char['exp_speed'] != 0:
            # res_lvl = 300 *
            res_lvl = srv.get_reset_level(char['name'])
            char['eta'] = (res_lvl - char['level']) / char['exp_speed']
        else:
            char['eta'] = 0

    with open(srv.online_cache_path, 'w') as cache:
        pickle.Pickler(cache).dump(last_chars)

    return chars


def add_admins_to_char_list(chars, admins, online_only=True):
    if online_only:
        admins = [admin for admin in admins if admin['online']]

    if admins:
        name_to_idx = dict((
            (char['name'], idx)
            for idx, char
            in enumerate(chars)))
        for admin in admins:
            idx = name_to_idx.get(admin['name'], None)
            if idx is not None:
                # mostly admins/gms ... don't have
                # their account visible online
                # and we have warn marker on *admin* acc by default
                chars[idx]['acc'] = '*admin*'
            else:
                chars.append(admin)


def list_online(srv, args):
    (page, lastchanged) = fetch_page(
        srv.online_url(),
        refresh=args['fresh'])
    table = srv.grep_table(page)
    chars = srv.parse_charinfo(table)
    compare_last(srv, chars, lastchanged)
    admins = srv.fetch_admins()
    print_statusline()

    add_char_acc_info(srv, chars)

    add_admins_to_char_list(chars, admins)

    sort_by = ['name']
    if ',' in args['sort']:
        sort_by += args['sort'].split(',')
    else:
        sort_by.append(args['sort'])
    for sort_by_key in sort_by:
        chars = sorted(chars, key=key(sort_by_key))

    print_chars(chars,
                warn=srv.cfg['warn'],
                info=srv.cfg['info'])


def add_char_acc_info(srv, chars):
    # FIXME: maybe issue if not already cached?
    accounts = srv.acc_char_list(refresh=False)
    char2acc = {}
    for acc, acc_chars in accounts.items():
        for char in acc_chars:
            char2acc[char['name']] = acc
    for idx, char in enumerate(chars):
        chars[idx]['acc'] = char2acc.get(char['name'], '-')


def acc_info(srv, args):
    search_for = args['acc']
    search_for = search_for.lower()
    found = {}

    acc_chars = srv.acc_char_list(refresh=args['fresh'])

    for acc, chars in acc_chars.items():
        if search_for in acc.lower():
            found[acc] = chars
        else:
            for char in chars:
                if search_for in char['name'].lower():
                    found[acc] = chars
                    break
    print_acc_chars(found)


def reset_char(srv, args):
    char_key = args['reset']
    # char = srv.cfg['res_stats'][char_key]

    print_statusline()
    info_before = print_char_info(srv, char_key)
    srv.reset(char_key)
    add_stats(srv, args, char_key)
    info_after = print_char_info(srv, char_key)
    print('Cost was %s zen.' %
          format_zen(info_before['zen'] - info_after['zen']))


def add_stats(srv, args, char_key=None):
    if char_key is None:
        char_key = args['addstats']

    srv.add_stats(char_key)


def print_char_info(srv, char_key):
    info = srv.char_info(char_key)

    if not info:
        LOG.error('Failed to get char lvl/res/zen info!')
    else:
        _info = info.copy()
        _info['zen'] = format_zen(_info['zen'])
        print('\n{name}:  {level} /{reset} {zen} zen\n'.format(**_info))
    return info


def load_cfg(path='~/.muonline.yaml'):
    config = {}
    with open(os.path.expanduser(path)) as cfg_file:
        config = yaml.load(cfg_file.read())

    for srv_id in config.keys():
        if srv_id == 'default':
            continue
        srv = config[srv_id]
        srv['id'] = srv_id
        srv.setdefault('info', [])
        srv.setdefault('warn', [])
        # compile regexps read from config
        # for field in ['start', 'end', 'char', 'err_msg']:
        #     if isinstance(srv[field], list):
        #         srv[field] = ''.join(srv[field])
        #     srv[field] = re.compile(srv[field].format(**REG_HELPERS))

    return config


def format_zen(zen):
    return '{0:,d}'.format(zen)


def parse_args(argv, with_val):
    args = {}
    key = None
    for arg in argv:
        if key is not None:
            args[key] = arg
            key = None
            continue
        arg = arg.lstrip('-')
        if arg in with_val:
            key = arg
            continue
        args[arg] = True
    if key is not None:
        raise ValueError('Missing value for %s' % key)
    return args


def print_statusline():
    print("{0:^60}".format(str(datetime.datetime.now())))


def main():
    servers = {}
    servers[DaemonicServer.SID] = DaemonicServer
    servers[DreamMuServer.SID] = DreamMuServer

    if 'debug' in sys.argv or '--debug' in sys.argv:
        LOG.setLevel(logging.DEBUG)
        logging.getLogger('requests').setLevel(logging.DEBUG)
        print('debugging')
    if 'help' in sys.argv or '--help' in sys.argv:
        print_help()
    args = parse_args(sys.argv, ['sort', 'reset', 'addstats', 'srv', 'acc'])
    args.setdefault('fresh', False)
    args.setdefault('sort', 'name')

    main_cfg = load_cfg()
    srv_id = args.get('srv', main_cfg.get('default'))
    srv_cfg = main_cfg[srv_id]

    srv = servers[srv_id](srv_cfg)

    if 'acc' in args:
        acc_info(srv, args)
        return

    if 'reset' in args:
        reset_char(srv, args)
    elif 'addstats' in args:
        add_stats(srv, args)

    if 'auto' in args:
        wait_min = 3
        wait_sec = wait_min * 60
        wait_step = 10
        try:
            while True:
                list_online(srv, args)
                print('**** ', end='')
                for x in xrange(wait_sec, 0, -wait_step):
                    # for x in xrange(wait_step, wait_sec+1, wait_step):
                    print('%d-' % x, end='')
                    sys.stdout.flush()
                    time.sleep(wait_step)
                print('0 ****')
                print('')
        except KeyboardInterrupt:
            LOG.info('Exiting ...')
    else:
        list_online(srv, args)

if __name__ == '__main__':
    try:
        main()
    except Exception:
        LOG.exception('Exiting ... Something unexpected happened?')
