"""
Taken from axc2 pretty much as is
"""
import collections
import json

# colors from the os theme for python:
import os
import re
import shutil
import string
import subprocess
import sys
import time
import types
from copy import deepcopy
from fnmatch import fnmatch

# from jsondiff import diff as js_diff # TODO
from functools import partial
from pprint import pformat
from threading import current_thread

from absl import flags

# --------------------------------------------------------------------------------- tty
have_tty = lambda: sys.stdin.isatty() and sys.stdout.isatty()

try:
    # Setting the default for the cli - flag, i.e. this rules w/o the flag:
    term_fixed_width_env = int(os.environ.get('term_width'))
except Exception:
    term_fixed_width_env = 0


def walk_dir(directory, crit=None):
    crit = (lambda *a: True) if crit is None else crit
    files = []
    j = os.path.join
    for (dirpath, dirnames, filenames) in os.walk(directory):
        files += [j(dirpath, file) for file in filenames if crit(dirpath, file)]
    return files


class Pytest:
    """Helpers specifically for pytest"""

    this_test = lambda: os.environ['PYTEST_CURRENT_TEST']

    def parse_test_infos(into=None):
        m = {} if into is None else into
        t = Pytest.this_test()
        f = t.split('::')
        m['class'] = f[1]
        m['test'] = f[-1].split('(')[0].strip()  # testname
        m['file'] = _ = f[0]
        m['fn_mod'] = _ = _.rsplit('/', 1)[-1].rsplit('.py', 1)[0]
        mod = sys.modules[_]
        m['path'] = mod.__file__
        return m

    this_test_func_name = lambda: Pytest.parse_test_infos()['test']

    def set_sys_argv(done=[0]):
        """
        remove any pytest cli args, would break FLGs:
        coverage:pytest.py, else pytest. and yes, its shit.
        TODO: create a conf + fixture, avoiding this:
        """
        if done[0]:
            return
        done[0] = True

        if 'pytest' not in sys.argv[0].rsplit('/', 1)[-1]:
            return
        while len(sys.argv) > 1:
            sys.argv.pop()

        e = os.environ

        dflts = {
            'environ_flags': True,
            'log_level': 10,
            'log_time_fmt': 'dt',
            'log_add_thread_name': True,
            'log_dev_fmt_coljson': 'pipes,cfg,payload',
            'plot_depth': 3,
            'plot_id_short_chars': 8,
        }
        if sys.stdout.isatty():
            dflts['plot_before_build'] = True

        for k, v in dflts.items():
            v = e.get(k, v)
            print('pytest EnvFlag: %s->%s' % (k, v))
            if v:
                e[k] = str(v)

    def init():
        Pytest.set_sys_argv()
        FLG(sys.argv)  # parsing the flags


class Flags:
    class term_fixed_width:
        n = 'Sets terminal width to this. 0: auto.'
        d = term_fixed_width_env

    class term_auto_width_recalc_every:
        n = 'When term with is auto we run stty size maximum every so many millisecs'
        d = 2000

    class help_output_fmt:
        n = 'Format of help output'
        t = [
            'plain',
            'simple',
            'github',
            'grid',
            'fancy_grid',
            'pipe',
            'orgtbl',
            'jira',
            'presto',
            'pretty',
            'psql',
            'rst',
            'mediawiki',
            'moinmoin',
            'youtrack',
            'html',
            'latex',
            'latex_raw',
            'latex_booktabs',
            'terminal',
            'textile',
        ]
        d = 'terminal'


def termwidth(c=[-1, 0]):
    """
    somewhat expensive, we run max every 2 seconds, i.e. potentially
    redraw delays when term resizing
    Right now we have to foreground term app anyway but maybe dev log rearranging
    """
    try:
        # when called before flag parseing, e.g. at -hf
        flg_fixed, flg_dt_recalc = (
            FLG.term_fixed_width,
            FLG.term_auto_width_recalc_every,
        )
    except Exception as ex:
        flg_fixed, flg_dt_recalc = term_fixed_width_env or None, None
    if flg_dt_recalc:
        if c[0] > -1 and now() - c[1] < FLG.term_auto_width_recalc_every:
            return c[0]

    def _tw():
        tfw = flg_fixed
        if tfw is not None and tfw > 0:
            return tfw
        try:
            # This seems to be working in the most situations - i.e. with a shell
            # better than os.popen('tput cols 2>/dev/null')
            os.system('stty size > /tmp/sttysize 2>/dev/null')
            with open('/tmp/sttysize') as fd:
                s = fd.read()
            i = int(s.strip().split(' ', 1)[1])
            if not i:
                raise Exception('x')
            return i
        except Exception:
            return 80

    c[0], c[1] = _tw(), now()
    return c[0]


# -------------------------------------------------------------------------- data utils
def to_list(o):
    o = [] if o is None else o
    t = type(o)
    return o if t == list else list(o) if t == tuple else [o]


def matched(d, match, prefix=()):
    """Returns parts of data where keys match match string - mainly for debugging (P)"""
    if isinstance(d, (tuple, list)):
        v = [matched(l, match) for l in d]
        if not any([k for k in v if k is not None]):
            return None
        # remove structs wich don't match but leave data:
        r, rd = [], []
        for o in v:
            if isinstance(o, (dict, list, tuple)):
                rd.append(o)
            r.append(o)
        return rd  # best
    if not isinstance(d, dict):
        return
    match = '*%s*' % match
    r = {}
    for k, v in d.items():
        k = str(k)
        np = prefix + (k,)
        if fnmatch(k, match):
            r['.'.join(np)] = v
        vv = matched(v, match, prefix=np)
        if vv:
            if isinstance(v, dict):
                if vv:
                    r.update(vv)
            elif isinstance(v, (tuple, list)):
                r['.'.join(np)] = vv

    return r


def headerize(dicts_list, mark=None):
    '''mark e.g. "dicts"'''
    l = dicts_list
    if not isinstance(l, list) or not l or not isinstance(l[0], dict):
        return l

    r = [list(l[0].keys())]
    r.extend([[m.get(h) for h in r[0]] for m in l])
    if mark:
        r = {mark: r}
    return r


def recurse_data(data, key_callbacks):
    if isinstance(data, dict):
        for k, v in data.items():
            cb = key_callbacks.get(k)
            if cb:
                data[k] = v = cb(v)
            if isinstance(v, (list, dict, tuple)):
                data[k] = v = recurse_data(v, key_callbacks)
    elif isinstance(data, list):
        data = [recurse_data(d, key_callbacks) for d in data]
    elif isinstance(data, tuple):
        data = tuple([recurse_data(d, key_callbacks) for d in data])
    return data


termcols = shutil.get_terminal_size((80, 20)).columns


def P(data, depth=None, match=None, headered=None, out=True, **kw):
    """
    Pretty Printer for large Dicts. In globals(). For debugging.
    P(data, 0, 'wifi') -> deep subtree match

    P(data, depth=2, match=None, [headered|h]='all')

    headered='all' -> all values for key 'all', if a list of dicts, will be printed as lists with headers
    h='all': alias for headered


    kw: pformat kws: indent=1, width=80, depth=None, stream=None, *, compact=False, sort_dicts=True
    """
    # P(data, 'match') also:
    kw['width'] = kw.get('width') or termcols
    headered = kw.pop('h', headered)
    if headered:
        data = deepcopy(data)
        kw['compact'] = kw.get('compact', True)

    if isinstance(depth, str):
        match = depth
        depth = None
    depth = depth or None  # 0 is None
    if match:
        data = matched(data, match)
    h = to_list(headered)
    if h:
        h = {k: partial(headerize, mark='@DL') for k in h}
        data = recurse_data(data, h)
    p = pformat(data, depth=depth, **kw)
    if out:
        print(p)
    else:
        return p


class DictTree(dict):
    __is_tree__ = True

    @property
    def __class__(self):
        # isinstance is dict:
        return dict

    def __getattr__(self, name):
        v = self[name]
        if isinstance(v, dict):
            return v if hasattr(v, '__is_tree__') else DictTree(v)
        return v


class DictTreeMatched(dict):
    __is_tree__ = True

    @property
    def __class__(self):
        # isinstance is dict:
        return dict

    def __getattr__(self, name, nil=b'\x04'):
        v = self.get(name, nil)
        if v == nil:
            for k in self.keys():
                if k.startswith(name):
                    return getattr(self, k)
            self[name]

        if isinstance(v, dict):
            return v if hasattr(v, '__is_tree__') else DictTreeMatched(v)
        return v


# debugging tool: P('data', 1) in pdb sessions
if isinstance(__builtins__, dict):
    __builtins__['P'] = P
    __builtins__['DT'] = DictTree
    __builtins__['T'] = DictTreeMatched
else:
    __builtins__.P = P
    __builtins__.DT = DictTree
    __builtins__.T = DictTreeMatched


def get_deep(key, data, sep='.', create=False, dflt=None):
    """
    Client can, via the dflt. decide how to handle problems, like collisions with values
    """
    # key maybe already path tuple - or string with sep as seperator:
    if not key:
        return data
    d = data
    parts = key.split(sep) if isinstance(key, str) else list(key)
    while parts:
        part = parts.pop(0)
        try:
            data = data[part]
        except TypeError as ex:
            # a list was in the original data:
            try:
                data = data[int(part)]
            except Exception:
                # this happens when we are already at a value, like an int
                # client wants to go deeper, not possible, but we can't delete the int
                # -> leave the client to decide:
                if dflt is None:
                    raise KeyError(key)
                return dflt
        except KeyError as ex:
            if not create:
                if dflt is None:
                    raise
                return dflt
            data = data.setdefault(part, {})

    return data


def deep_update(orig_dict, new_dict, maptyp=collections.abc.Mapping):
    for key, val in new_dict.items():
        if isinstance(val, maptyp):
            tmp = deep_update(orig_dict.get(key, {}), val)
            orig_dict[key] = tmp
        elif isinstance(val, list):
            orig_dict[key] = orig_dict.get(key, []) + val
        else:
            orig_dict[key] = new_dict[key]
    return orig_dict


def in_gevent():
    try:
        import gevent

        return gevent.sleep == time.sleep
    except Exception:
        return False


has_tty = lambda: sys.stdin.isatty() and sys.stdout.isatty()


def confirm(msg):
    if not has_tty():
        raise Exception('Require confirmation for: %s' % msg)
    print(msg)
    r = input('Confirmed [y|N]? ')
    if r.lower() != 'y':
        from devapp.app import app

        app.die('Unconfirmed')


now = lambda: int(time.time() * 1000)
is_ = isinstance


def jdiff(d1, d2):
    try:
        # Got exception when we have L(list) in test_share.py
        return jsondiff(d1, d2, marshal=True)
    except Exception as ex:
        # TODO: convert tuples recursively into strings
        try:
            d1 = json.loads(json.dumps(d1, default=str))
            d2 = json.loads(json.dumps(d2, default=str))
            return jsondiff(d1, d2, marshal=True)
        except Exception as ex:
            return {'err': 'cannot diff', 'exc': str(ex)}


def dict_merge(source, destination):
    """dest wins"""

    for key, value in source.items():
        if isinstance(value, dict):
            # get node or create one
            node = destination.setdefault(key, {})
            dict_merge(value, node)
        else:
            destination[key] = value

    return destination


tn = lambda: current_thread().name


pass_ = lambda *a, **kw: None
exists = os.path.exists
dirname = os.path.dirname
abspath = os.path.abspath
env = os.environ

# here = dir_of(__file__)
def dir_of(fn, up=0):
    d = abspath(dirname(fn))
    return dir_of(d, up - 1) if up else d


def to_url(shorthand):
    s = shorthand
    if not s.startswith('http'):
        s = 'http://' + s
    schema, rest = s.split('://', 1)
    if not '/' in rest:
        rest = rest + '/'
    hp, path = rest.split('/', 1)
    if hp.startswith(':'):
        hp = '127.0.0.1' + hp
    elif hp.isdigit():
        hp = '127.0.0.1:%s' % hp
    hp = hp.replace('*', '0.0.0.0')
    return schema + '://' + hp + '/' + path


def host_port(shorthand):
    u = to_url(shorthand).split('//', 1)[1]
    h, p = u.split(':')
    return h, int(p.split('/', 1)[0])


def write_config_cls_key(k, v):
    fn = env['DA_DIR'] + '/config.cls'
    s = read_file(fn).splitlines()
    s = [l for l in s if not l.startswith('%s=' % k)]
    s.append('%s="%s"' % (k, v))
    s.sort()
    write_file(fn, '\n'.join(s) + '\n')


archives = {'.tar.gz': 'tar xfvz "%(frm)s"', '.tar': 'tar xfv "%(frm)s"'}


def download_file(url, local_filename, auto_extract=True):
    """auto extract works for exe types like hugo.tar.gz -> contains in same dir the hugo binary.
    if the .tar.gz is containing a bin dir then the user should rather not say type
    is 'exe' but type is archive or so
    """
    import requests
    from devapp.app import app

    app.info('Downloading', url=url, to=local_filename)
    r = requests.get(url, stream=True)
    arch, fn = None, local_filename
    if auto_extract:
        for k in archives:
            if url.endswith(k):
                arch = k
                fn = local_filename + k
                break

    with open(fn, 'wb') as f:
        shutil.copyfileobj(r.raw, f)
    if arch:
        m = {'dir': fn.rsplit('/', 1)[0], 'cmd': archives[arch], 'frm': fn}
        m['cmd'] = m['cmd'] % m
        cmd = 'cd "%(dir)s" && %(cmd)s' % m
        app.info('Uncharchiving', cmd=cmd)
        os.system(cmd)
    return local_filename


_is_root = [None]  # True, False. None: undefined
_sudo_pw = [None]  # None: undef


def sudo_pw():
    fns = env.get('DA_FILE_SUDO_PASSWORD') or env['HOME'] + '/.sudo_password'
    return env.get('SUDO_PASSWORD') or read_file(fns, '').strip()


def sp_call(*args, as_root='false', get_all=False):
    """run command - as root if 'true' or True"""
    # todo: as_root = 'auto'
    sp = subprocess
    sudo = str(as_root).lower() == 'true'
    if sudo:
        if _is_root[0] == None:
            _is_root[0] = os.system('touch /etc/hosts 2>/dev/null') == 0
        if _is_root[0]:
            sudo = False
    if sudo:
        args = ('sudo', '-S') + tuple(args)
        if _sudo_pw[0] == None:
            _sudo_pw[0] = sudo_pw()
        pw = sp.Popen(('echo', _sudo_pw[0]), stdout=sp.PIPE)
        stdin = pw.stdout
    else:
        stdin = sys.stdin
    proc = sp.Popen(list(args), shell=False, stdin=stdin, stderr=sp.PIPE)
    out, err = proc.communicate()
    if not get_all:
        return err
    return {'exit_status': int(proc.wait()), 'stderr': err, 'stdout': '<term>'}


def exec_file(fn, ctx=None, get_src=None):
    ctx = ctx if ctx is not None else {}
    # allows tracebacks with file pos:
    ctx['__file__'] = fn
    src = read_file(fn)
    exec(compile(src, fn, 'exec'), ctx)
    if get_src:
        ctx['source'] = src
    return ctx


def funcname(f):
    while True:
        n = getattr(f, '__name__', None)
        if n:
            return n
        f = f.func


env = os.environ

clean_env_key = lambda s: ''.join(
    [c for c in s if c in string.digits + string.ascii_letters]
)


def restart_unshared(name):
    """
    Manual user start of daemons.
    Since we assemble filesystems we dont' want those hang around globally,
    but be gone with session end.
    Systemd does this as well.
    must be called at beginning of the process - sys.argv must be original
    """

    def set_linked_python_as_executable():
        """Systemd starts us with sourceing env then ../run/$DA_CLS -m devapp.run start
        sys.argv then: ['$DA_MAX_DIR/../python/devapp/run.py', 'start']
        When a user copies this from the unit file then a unshare is done.
        That calls the start args again after unshare -- ...
        WE have /bin/env python in hashbang - resulting in differences in the ps ax
        -> we replace:
        """
        if sys.argv[0].endswith('/run.py'):
            sys.argv.pop(0)
            for k in 'devapp.run', '-m', '%(DA_DIR)s/run/%(DA_CLS)s' % env:
                sys.argv.insert(0, k)

    n = name
    if sys.argv[-1] == 'unshared':
        sys.argv.pop()
        env['da_unshared'] = n
    if env.get('da_unshared') != n:
        sys.argv.append('unshared')
        print(
            'Restarting unshared. To prevent: export da_unshared="%s"' % n,
            file=sys.stderr,
        )
        c = ['unshare', '-rm', '--']
        set_linked_python_as_executable()
        c.extend(sys.argv)
        sys.exit(subprocess.call(c))


env, envget, exists = os.environ, os.environ.get, os.path.exists
ctx = {}

str8 = partial(str, encoding='utf-8')
is_str = lambda s: isinstance(s, str)

# not matches classes which are callable:
is_func = lambda f: isinstance(
    f, (types.FunctionType, types.BuiltinFunctionType, partial)
)

json_diff = lambda old, new: json.loads(js_diff(old, new, syntax='explicit', dump=True))


def into(d, k, v):
    """the return is important for e.g. rx"""
    d[k] = v
    return d


def setitem(m, k, v):  # for list comprehensions
    m[k] = v


def deep(m, dflt, *pth):
    """Access to props in nested dicts / Creating subdicts by *pth

    Switch to create mode a bit crazy, but had no sig possibilit in PY2:
    # thats a py3 feature I miss, have to fix like that:
    if add is set (m, True) we create the path in m

    Example:
    deep(res, True), [], 'post', 'a', 'b').append(r['fid'])
        creates in res dict: {'post': {'a': {'b': []}}}  (i.e. as list)
        and appends r['fid'] to it in one go
        create because init True was set

    """
    m, add = m if isinstance(m, tuple) else (m, False)
    keys = list(pth)
    while True:
        k = keys.pop(0)
        get = m.setdefault if add else m.get
        v = dflt if not keys else {}
        m = get(k, v)
        if not keys:
            return m


def start_of(s, chars=100):
    """ to not spam output we often do "foo {a': 'b'...."""
    s = str8(s)
    return s[:100] + '...' if len(s) > 100 else ''


cast_list = lambda v, sep=',': (
    [] if v == '[]' else [s.strip() for s in v.split(sep)] if is_str(v) else v
)


dt_precision = '%.2fs'


def dt_human(ts_start, ts_end=None):
    dt = time.time() - ts_start if ts_end is None else ts_end - ts_start
    if dt > 7200:
        return '%.2fh' % (dt / 3600.0)
    return dt_precision % dt


class AllStatic(type):
    'turn all methods of this class into static methods'
    new = None

    def __new__(cls, name, bases, local):
        for k in local:
            if k.startswith('_'):
                continue
            if is_func(local[k]):
                local[k] = staticmethod(local[k])
        # customization hook:
        if cls.new:
            cls.new(cls, name, bases, local)
        return type.__new__(cls, name, bases, local)


class _ctx:
    def __enter__(self, *a, **kw):
        pass

    __exit__ = __enter__


def parse_host(url, no_port=None):
    if not url.startswith('http'):
        raise Exception(url + ' is no url')
    host = '/'.join(url.split('/', 3)[:3])
    if no_port:
        host = ':'.join(host.split(':')[:2])
    return host


class LazyDict(dict):
    def get(self, key, thunk=None):
        return self[key] if key in self else thunk() if callable(thunk) else thunk

    def set_lazy(self, key, thunk=None, *a, **kw):
        return (
            self[key]
            if key in self
            else dict.setdefault(
                self, key, thunk(*a, **kw) if callable(thunk) else thunk
            )
        )


osenv = os.environ.get
HCOLS = {'M': '#8bd124', 'R': '#FF0000', 'L': '#333399', 'I': '#44FFFF'}

shl = 'shell'


def camel_to_snake(string):
    groups = re.findall('([A-z0-9][a-z]*)', string)
    return '_'.join([i.lower() for i in groups])


def url_to_dir(url):
    # url = url.replace('file://', '') # NO, leave
    for ft in '://', '?', ':':
        url = url.replace(ft, '_')
    return url


def html(s, col):
    return '<font color="%s">%s</font>' % (HCOLS[col], s)


def color(s, col, mode=shl):
    if not osenv(col):
        return str(s)
    # for col term
    if mode == 'html':
        return html(s, col)
    else:
        return str(s)


# faster than calling color func:
sm = '%s%%s\x1B[0m' % ('\x1B%s' % osenv('M', '')[2:])
si = '%s%%s\x1B[0m' % ('\x1B%s' % osenv('I', '')[2:])
sl = '%s%%s\x1B[0m' % ('\x1B%s' % osenv('L', '')[2:])
sr = '%s%%s\x1B[0m' % ('\x1B%s' % osenv('R', '')[2:])
sgr = '%s%%s\x1B[0m' % '\x1B[1;38;5;154m'


def M(s, mode=shl):
    if mode == shl:
        return sm % s
    return color(s, 'M', mode)


def I(s, mode=shl):
    if mode == shl:
        return si % s
    return color(s, 'I', mode)


def L(s, mode=shl):
    if mode == shl:
        return sl % s
    return color(s, 'L', mode)


def R(s, mode=shl):
    if mode == shl:
        return sr % s
    return color(s, 'R', mode)


def GR(s):
    return sgr % s


def check_start_env(req_env):
    """ can we run ? """

    def die(msg):
        print(msg)
        sys.exit(1)

    for k in req_env:
        rd = envget(k)
        if not rd:
            die('$%s required' % k)
    return 'Passed requirements check'


def read_file(fn, dflt=None, bytes=-1, strip_comments=False):
    """
    API function.
    read a file - return a default if it does not exist"""
    if not exists(fn):
        if dflt is not None:
            return dflt
        raise Exception(fn, 'does not exist')
    with open(fn) as fd:
        # no idea why but __etc__hostname always contains a linesep at end
        # not present in source => rstrip(), hope this does not break templs
        res = fd.read(bytes)
        res = res if not res.endswith('\n') else res[:-1]
        if strip_comments:
            lines = res.splitlines()
            res = '\n'.join([l for l in lines if not l.startswith('#')])
        return res


def write_file(fn, s, log=0, mkdir=0, chmod=None):
    'API: Write a file. chmod e.g. 0o755 (as octal integer)'
    from devapp.app import app

    fn = os.path.abspath(fn)

    if log > 0:
        app.info('Writing file', fn=fn)

    if isinstance(s, (list, tuple)):
        s = '\n'.join(s)
    if log > 1:
        sep = '\n----------------------\n'
        ps = (
            s
            if not 'key' in fn and not 'assw' in fn and not 'ecret' in fn
            else '<hidden>'
        )
        app.debug('Content', content=sep + ps + sep[:-1])
    e = None
    for i in 1, 2:
        try:
            with open(fn, 'w') as fd:
                fd.write(s)
            if chmod:
                if not isinstance(chmod, (list, tuple)):
                    chmod = [int(chmod)]
                for s in chmod:
                    os.chmod(fn, s)
            return fn
        except IOError as ex:
            if mkdir:
                d = os.path.dirname(fn)
                os.makedirs(d)
                continue
            e = ex
        except Exception as ex:
            e = ex
        raise Exception('Could not write file: %s %s' % (fn, e))


def failsafe(meth, *a, **kw):
    """Spotted log errors at aggressively reconnecting clients, running in greenlets
    in the pytest progs. This allows to wrap these methods.
    """
    # spotted log
    try:
        meth(*a, **kw)
    except Exception as ex:
        print('Failed with %s: %s(%s %s)' % (str(ex), meth, str(a), str(kw)))


# ------------------------------------------------------------------------------- flags


FLG = flags.FLAGS


def set_flag(k, v):
    setattr(FLG, k, type(getattr(FLG, k))(v))


def set_flag_vals_from_env():
    """allows so ref $foo as default var a flag"""
    # hmm. pytest -> nrtesting -> run_app in greenlet. How to pass flags, e.g. to drawflow
    ef = FLG.environ_flags or os.environ.get(
        'environ_flags'
    )  # if set we check all from environ
    for f in dir(FLG):
        if ef:
            v = os.environ.get(f)
            if v:
                set_flag(f, v)
                continue
        # wanted always from environ by developer - then he gave a default with $:
        v = getattr(FLG, f)
        if isinstance(v, str) and v.startswith('$'):
            setattr(FLG, f, os.environ.get(v[1:], v))


def shorten(key, prefix, maxlen, all_shorts=None, take=1):
    take, sold = 1, None
    while True:
        s = autoshort(key, prefix, maxlen, take)
        if not s in all_shorts:
            all_shorts.add(s)
            return s
        if s == sold:
            raise Exception('Unresolvable collision in autoshort names: %s' % key)
        sold, take, maxlen = s, take + 1, maxlen + 0


def autoshort(key, prefix, maxlen, take=1):
    ml = maxlen - len(prefix)
    parts = key.split('_')
    if prefix and not any([c for c in prefix if not c in parts[0]]):
        parts.pop(0)
    r = prefix + ''.join([parts.pop(0)[0:take].lower() for i in range(ml) if parts])
    return r[:maxlen]


# fmt:off
_ = [
    [type(None), 'boolean'],
    [str,        'string' ],
    [int,        'integer'],
    [float,      'float'  ],
    [list,       'list'   ],
    [bool,       'boolean'],
]
_flag_makers = dict({k: getattr(flags, 'DEFINE_' + v) for k, v in _})
# fmt:on


def mk_enum(*a, vals, **kw):
    a = list(a)
    a.insert(2, vals)
    return flags.DEFINE_enum(*a, **kw)


def flag_makers(t, m=_flag_makers):
    # the actual absl.flags.DEFINE_integer(..),... methods
    return _flag_makers[t] if not isinstance(t, list) else partial(mk_enum, vals=t)


all_flag_shorts = set()


def make_flag(c, module, autoshort, default, g, sub=False, **kw):
    key = c.__name__
    if sub:
        key = sub + '_' + key
    d = g(c, 'd', default)  # when d is not given, what to do. Dictates the flag type.
    ml = kw.get('short_maxlen', 5)
    mkw = {'module_name': module, 'short_name': g(c, 's', None)}
    s = g(c, 's', None)
    if s is None and autoshort is not False:
        s = shorten(key, prefix=autoshort, maxlen=ml, all_shorts=all_flag_shorts)
    mkw['short_name'] = s
    m = flag_makers(g(c, 't', type(d)))
    txt = g(c, 'n', human(key))
    if c.__doc__:
        ls = c.__doc__.splitlines()
        txt += ' Details: %s' % ('\n'.join([l.strip() for l in ls]).strip())
    m(key, d, txt, **mkw)


human = lambda key: ' '.join([k.capitalize() for k in key.split(' ')])


# this allows a module to "steal" the flags class of another module. e.g. lc client:
skip_flag_defines = []


def define_flags(Class, sub=False):
    """
    Pretty classes to flag defs. See nrclient, devapp.app or structlogging.sl how to use
    All config in the top class
    """
    # TODO: Register docstrings here (markdown?), supplying even more detailed help
    g = getattr
    module = g(Class, 'module', Class.__module__)
    if module in skip_flag_defines:
        return
    autoshort = g(Class, 'autoshort', sub[0] if sub else False)
    # passed to make_flag as default for default
    default = getattr(Class, 'default', '')
    l = dict(locals())
    cs, cnos = [], []
    for k in dir(Class):
        if not k.startswith('_'):
            c = getattr(Class, k)
            if isinstance(c, type):
                if not hasattr(c, 'n') and not hasattr(c, 'd'):
                    # prbably group but no n, no d is allowed as well
                    subs = [getattr(c, s) for s in dir(c) if not s.startswith('_')]
                    subs = [i for i in subs if isinstance(i, type)]
                    if subs:
                        r = define_flags(c, sub=c.__name__)
                        continue

                cs.append(c) if g(c, 's', None) else cnos.append(c)
    # do the ones with shorts first, to collide only on autoshorts:
    [make_flag(c, **l) for k in [cs, cnos] for c in k]


define_flags(Flags)  # ours


def FlagsDict(f):
    """
    Flags to Dicts
    allows:
    FLG.foo = 'bar' -> '%(foo)s' % FlagsDict(Flags) = 'bar'
    i.e. Allow values of Flags to reference values of other flags.
    """

    # class F:
    #     __init__ = lambda self, f: setattr(self, 'f', f)
    #     __getitem__ = lambda self, k: getattr(self.f, k)

    # return F(f)
    return FLG.flag_values_dict()


# --------------------------------------------------------------------- Not importable
def unavail(missing, req=None, warn=True):
    """We don't need everything always"""
    # TODO:Add a few pip install options
    def f(*a, _miss=missing, _req=req, _warn=warn, **kw):
        print('Function from package "%s" not available in your installation!' % _miss)
        if _req:
            print('Please run: ', _req)
        raise Exception('Cannot continue - missing %s' % _miss)

    if warn:
        print('Cannot import: %s' % missing)
    return f


try:
    from jsondiff import diff as jsondiff
except Exception as ex:
    jsondiff = unavail('jsondiff', req='pip install jsondiff', warn=False)


# ------------------------------------------------------------ decorators


# https://github.com/micheles/decorator/blob/master/docs/documentation.md
# preserve the signature of decorated functions in a consistent way across Python releases

try:
    from decorator import decorate, decorator
except ImportError as ex:
    decorate = decorator = unavail('decorator', req='pip install decorator', warn=False)

try:
    from tabulate import tabulate
except ImportError as ex:
    tabulate = unavail('tabulate', req='pip install tabulate', warn=False)


def _memoize(func, *args, **kw):
    if kw:  # frozenset is used to ensure hashability
        key = args, frozenset(list(kw.items()))
    else:
        key = args
    cache = func.cache  # public cache attribute added by memoize
    if key not in cache:
        cache[key] = func(*args, **kw)
    return cache[key]


def memoize(f):
    """
    A simple memoize implementation. It works by adding a .cache diction
    to the decorated function. The cache will grow indefinitely, so it i
    your responsibility to clear it, if needed.
    """
    f.cache = {}
    return decorate(f, _memoize)


if __name__ == '__main__':
    pass
    # r = P(
    #     {'a': [{'all': [{'a': 23, 'b': 23}, {'a': 2322, 'b': 232323}]}]},
    #     out=None,
    #     headered='all',
    # )
    # assert r == "{'a': [{'all': [['a', 'b'], 23, 23, 2322, 232323]}]}"
