# -*- coding: utf-8 -*-
"""

  `Vital Debugging Tools`
   Simple tools to debug and time your scripts & structures
--·--·--·--·--·--·--·--·--·--·--·--·--·--·--·--·--·--·--·--·--·--·--·--·--·--·--
   The MIT License (MIT) (c) 2014 Jared Lunde

"""
import os
import re
import gc
import sys
import time
import datetime
import types
import string
import random
import inspect
import warnings
import importlib
_random = random.SystemRandom()
try:
    import numpy as np
except ImportError:
    try:
        import statistics as np
        np.sum = sum
        np.max = max
        np.min = min
        np.std = np.pstdev
    except ImportError:
        from vital.debug.stats import mean, median, pstdev, np
        np.sum = sum
        np.max = max
        np.min = min
        np.std = pstdev
        np.mean = mean
        np.median = median
import subprocess
from io import StringIO
from math import floor
from pydoc import locate
from codecs import getencoder
from functools import wraps
from collections import *

from vital.tools import getattr_in
from vital.tools.encoding import stdout_encode
from . import colors, tlds


__all__ = (
  "get_terminal_width",
  "line",
  'banner',
  "flag",
  "padd",
  "colorize",
  "uncolorize",
  "bold",
  "cut",
  "table_mapping",
  "gen_rand_str",
  "rand_readable",
  "prepr",
  "preprX",
  "RandData",
  "Look",
  "Logg",
  "logg",
  "ProgressBar",
  "Timer",
  "Compare",
  "get_parent_name",
  "get_parent_obj",
  "get_obj_name",
  "format_obj_name",
)


def get_terminal_width(default=80):
    """ -> #int width of the terminal window """
    # http://www.brandonrubin.me/2014/03/18/python-snippet-get-terminal-width/
    return default
    '''command = ['tput', 'cols']
    try:
        width = int(subprocess.check_output(command))
    except OSError as e:
        print(
            "Invalid Command '{0}': exit status ({1})".format(
                command[0], e.errno))
        return default
    except subprocess.CalledProcessError as e:
        print(
            "'{0}' returned non-zero exit status: ({1})".format(
                command, e.returncode))
        return default
    else:
        return width'''


def line(separator="-·-", color=None, padding=None, num=1):
    """ Prints a line separator the full width of the terminal.

        @separator: the #str chars to create the line from
        @color: line color from :mod:vital.debug.colors
        @padding: adds extra lines to either the top, bottom or both
            of the line via :func:padd
        @num: #int number of lines to print
        ..
            from vital.debug import line
            line("__")
            ____________________________________________________________________
        ..
    """
    for x in range(num):
        columns = get_terminal_width()
        separator = "".join(
            separator for x in
            range(floor(columns/len(separator))))
        print(padd(colorize(separator.strip(), color), padding))


def padd(text, padding="top", size=1):
    """ Adds extra new lines to the top, bottom or both of a String

        @text: #str text to pad
        @padding: #str 'top', 'bottom' or 'all'
        @size: #int number of new lines

        -> #str padded @text
        ..
            from vital.debug import *

            padd("Hello world")
            # -> '\\nHello world'

            padd("Hello world", size=5, padding="all")
            # -> '\\n\\n\\n\\n\\nHello world\\n\\n\\n\\n\\n'
        ..
    """
    if padding:
        padding = padding.lower()
        pad_all = padding == 'all'
        padding_top = ""
        if padding and (padding == 'top' or pad_all):
            padding_top = "".join("\n" for x in range(size))
        padding_bottom = ""
        if padding and (padding == 'bottom' or pad_all):
            padding_bottom = "".join("\n" for x in range(size))
        return "{}{}{}".format(padding_top, text, padding_bottom)
    return text


def colorize(text, color="BLUE", close=True):
    """ Colorizes text for terminal outputs

        @text: #str to colorize
        @color: #str color from :mod:colors
        @close: #bool whether or not to reset the color

        -> #str colorized @text
        ..
            from vital.debug import colorize

            colorize("Hello world", "blue")
            # -> '\x1b[0;34mHello world\x1b[1;m'

            colorize("Hello world", "blue", close=False)
            # -> '\x1b[0;34mHello world'
        ..
    """
    if color:
        color = getattr(colors, color.upper())
        return color + uncolorize(str(text)) + (colors.RESET if close else "")
    return text


_find_colors = re.compile(r"""(\033\[([\d;]*)m?)""")


def uncolorize(text):
    """ Attempts to remove color and reset flags from text via regex pattern

        @text: #str text to uncolorize

        -> #str uncolorized @text
        ..
            from vital.debug import uncolorize

            uncolorize('\x1b[0;34mHello world\x1b[1;m')
            # -> 'Hello world'
        ..
    """
    return _find_colors.sub("", text)


def bold(text, close=True):
    """ Bolds text for terminal outputs

        @text: #str to bold
        @close: #bool whether or not to reset the bold flag

        -> #str bolded @text
        ..
            from vital.debug import bold

            bold("Hello world")
            # -> '\x1b[1mHello world\x1b[1;m'

            bold("Hello world", close=False)
            # -> '\x1b[1mHello world'
        ..
    """
    return getattr(colors, "BOLD") + str(text) + \
        (colors.RESET if close else "")


def banner(banner, separator="-·-", color='bold'):
    line(separator)
    print(colorize(banner, color))
    line(separator)


def cut(text, length=50, replace_with="…"):
    """ Shortens text to @length, appends @replace_with to end of string
        if the string length is > @length

        @text: #str text to shortens
        @length: #int max length of string
        @replace_with: #str to replace chars beyond @length with
        ..
            from vital.debug import cut

            cut("Hello world", 8)
            # -> 'Hello w…'

            cut("Hello world", 15)
            # -> 'Hello world'
        ..
    """
    text_len = len(uncolorize(text))
    if text_len > length:
        replace_len = len(replace_with)
        color_spans = [
            _colors.span() for _colors in _find_colors.finditer(text)]
        chars = 0
        _length = length+1 - replace_len
        for i, c in enumerate(text):
            broken = False
            for span in color_spans:
                if span[0] <= i < span[1]:
                    broken = True
                    break
            if broken:
                continue
            chars += 1
            if chars <= _length:
                cutoff = i
            else:
                break
        if color_spans:
            return text[:cutoff] + replace_with + colors.RESET
        else:
            return text[:cutoff] + replace_with
    return text


def flag(text=None, color=None, padding=None, show=True):
    """ Wraps @text in parentheses (), optionally colors and pads and
        prints the text.

        @text: #str text to (flag)
        @color: #str color to :func:colorize the text within
        @padding: #str location of padding from :func:padd
        @show: #bool whether or not to print the text in addition to returning
            it

        -> #str (flagged) text
        ..
            from vital.debug import flag

            flag("Hello world", "blue")
            # -> (Hello world)
            #    '(\x1b[0;34mHello world\x1b[1;m)'

            flag("Hello world", "blue", show=False)
            # -> '(\x1b[0;34mHello world\x1b[1;m)'

            flag("Hello world", color="blue", padding="all")
            # ->
            #    (Hello world)
            #
            #    '\\n(\x1b[0;34mHello world\x1b[1;m)\\n'
        ..
    """
    _flag = None
    if text:
        _flag = padd("({})".format(colorize(text, color)), padding)
        if not show:
            return _flag
        else:
            print(_flag)
    return _flag or text


def table_mapping(data, padding=1, separator=" "):
    """ Pretty prints a one-dimensional key: value mapping

        @data: #dict data to pretty print
        @padding: #int number of spaces to pad the left side of the key with
        @separator: #str chars to separate the key and value pair with

        -> #str pretty one dimensional table
        ..
            from vital.debug import table_mapping

            print(table_mapping({"key1": "val1", "key2": "val2"}))
            # -> \x1b[1m  key1\x1b[1;m val1
            #    \x1b[1m  key2\x1b[1;m val2

            print(table_mapping({"key1": "val1", "key2": "val2"}, padding=4))
            # ->    \x1b[1m     key1\x1b[1;m val1
            #       \x1b[1m     key2\x1b[1;m val2

            print(table_mapping(
                {"key1": "val1", "key2": "val2"}, padding=4, separator=": "))
            # ->    \x1b[1m     key1\x1b[1;m: val1
            #       \x1b[1m     key2\x1b[1;m: val2
        ..
    """
    if data:
        ml = max(len(k) for k in data.keys())+1
        return "\n".join("{}{}{}".format(
            bold(k.rjust(ml+padding, " ")), separator, v)
            for k, v in data.items())
    return ""


def gen_rand_str(*size, use=None, keyspace=None):
    """ Generates a random string using random module specified in @use within
        the @keyspace

        @*size: #int size range for the length of the string
        @use: the random module to use
        @keyspace: #str chars allowed in the random string
        ..
            from vital.debug import gen_rand_str

            gen_rand_str()
            # -> 'PRCpAq'

            gen_rand_str(1, 2)
            # -> 'Y'

            gen_rand_str(12, keyspace="abcdefg")
            # -> 'gaaacffbedf'
        ..
    """
    keyspace = keyspace or (string.ascii_letters + string.digits)
    keyspace = [char for char in keyspace]
    use = use or _random
    use.seed()
    if size:
        size = size if len(size) == 2 else (size[0], size[0])
    else:
        size = (10, 12)
    return ''.join(
        use.choice(keyspace)
        for _ in range(use.randint(*size)))


def rand_readable(*size, use=None, density=6):
    """ Generates a random string with readable characters using
        random module specified in @use

        @*size: #int size range for the length of the string
        @use: the random module to use
        @density: how often to include a vowel, you can expect a vowel about
            once every (density) nth character
        ..
            from vital.debug import rand_readable

            rand_readable()
            # -> 'hyiaqk'

            rand_readable(15, 20)
            # -> 'oqspyywvhifsaikiaoi'

            rand_readable(15, 20, density=1)
            # -> 'oeuiueioieeioeeeue'

            rand_readable(15, 20, density=15)
            # -> 'ktgjabwdqhgeanh'
        ..

    """
    use = use or _random
    keyspace = [c for c in string.ascii_lowercase if c != "l"]
    vowels = ("a", "e", "i", "o", "u")

    def use_vowel(density): not use.randint(0, density)
    if size:
        size = size if len(size) == 2 else (size[0], size[0])
    else:
        size = (6, 7)
    return ''.join(
        use.choice(vowels if use_vowel(density) else keyspace)
        for _ in range(use.randint(*size)))


lambda_sub = re.compile(r"""([\w\d=\s]{0,}?)lambda([\s\w\d]{0,})([:]?)\s+""")\
    .sub
whitespace_sub = re.compile(r"""\s{2,}""").sub


def get_parent_name(obj):
    """ Gets the name of the object containing @obj and returns as a string

        @obj: any python object

        -> #str parent object name or None
        ..
            from vital.debug import get_parent_name

            get_parent_name(get_parent_name)
            # -> 'vital.debug'

            get_parent_name(vital.debug)
            # -> 'vital'

            get_parent_name(str)
            # -> 'builtins'
        ..
    """
    parent_obj = get_parent_obj(obj)
    parent_name = get_obj_name(parent_obj) if parent_obj else None
    n = 0
    while parent_obj and n < 2500:
        parent_obj = get_parent_obj(parent_obj)
        if parent_obj:
            parent_name = "{}.{}".format(get_obj_name(parent_obj), parent_name)
            n += 1
    if not parent_name or not len(parent_name):
        parent_name = None
        objname = get_obj_name(obj)
        if objname and len(objname.split(".")) > 1:
            return ".".join(objname.split(".")[:-1])
        return None
    return parent_name


def get_class_that_defined_method(meth):
    """ Gets the class object which defined a given method

        @meth: a class method

        -> owner class object
    """
    if inspect.ismethod(meth):
        for cls in inspect.getmro(meth.__self__.__class__):
            if cls.__dict__.get(meth.__name__) is meth:
                return cls
        meth = meth.__func__  # fallback to __qualname__ parsing
    if inspect.isfunction(meth):
        cls = getattr(
            inspect.getmodule(meth),
            meth.__qualname__.split('.<locals>', 1)[0].rsplit('.', 1)[0])
        if isinstance(cls, type):
            return cls
    return None


def get_parent_obj(obj):
    """ Gets the name of the object containing @obj and returns as a string

        @obj: any python object

        -> #str parent object name or None
        ..
            from vital.debug import get_parent_obj

            get_parent_obj(get_parent_obj)
            # -> <module 'vital.debug' from>
        ..
    """
    try:
        cls = get_class_that_defined_method(obj)
        if cls and cls != obj:
            return cls
    except AttributeError:
        pass
    if hasattr(obj, '__module__') and obj.__module__:
        try:
            module = locate(obj.__module__)
            assert module is not obj
            return module
        except Exception:
            try:
                module = module.__module__.split('.')[:-1]
                if len(module):
                    return locate(module)
            except Exception:
                pass
    elif hasattr(obj, '__objclass__') and obj.__objclass__:
        return obj.__objclass__
    try:
        assert hasattr(obj, '__qualname__') or hasattr(obj, '__name__')
        objname = obj.__qualname__ if hasattr(obj, '__qualname__') \
            else obj.__name__
        objname = objname.split(".")
        assert len(objname) > 1
        return locate(".".join(objname[:-1]))
    except Exception:
        try:
            module = importlib.import_module(".".join(objname[:-1]))
            return module
        except Exception:
            pass
    return None


def get_obj_name(obj, full=True):
    """ Gets the #str name of @obj

        @obj: any python object
        -> #str object name
        ..
            from vital.debug import get_obj_name

            get_obj_name(get_obj_name)
            # -> 'get_obj_name'

            get_obj_name(vital.debug.Timer)
            # -> 'Timer'
        ..
    """
    if hasattr(obj, '__qualname__') and obj.__qualname__:
        return obj.__qualname__.split(".")[-1]
    has_name_attr = hasattr(obj, '__name__')
    if has_name_attr and obj.__name__ == "<lambda>":
        try:
            src = whitespace_sub("", inspect.getsource(obj))\
                .replace("\n", "; ").strip(" <>")
        except OSError:
            src = obj.__name__
        return lambda_sub("", src)
    elif has_name_attr and obj.__name__:
        return obj.__name__.split(".")[-1]
    elif hasattr(obj, '__class__'):
        return obj.__class__.__name__.strip("<>")
    else:
        return obj.__repr__()


def format_obj_name(obj, delim="<>"):
    """ Formats the object name in a pretty way

        @obj: any python object
        @delim: the characters to wrap a parent object name in

        -> #str formatted name
        ..
            from vital.debug import format_obj_name

            format_obj_name(vital.debug.Timer)
            # -> 'Timer<vital.debug>'

            format_obj_name(vital.debug)
            # -> 'debug<vital>'

            format_obj_name(vital.debug.Timer.time)
            # -> 'time<vital.debug.Timer>'
        ..
    """
    pname = ""
    parent_name = get_parent_name(obj)
    if parent_name:
        pname = "{}{}{}".format(delim[0], get_parent_name(obj), delim[1])
    return "{}{}".format(get_obj_name(obj), pname)


def preprX(*attributes, address=True, full_name=False,
           pretty=False, keyless=False, **kwargs):
    """ `Creates prettier object representations`

        @*attributes: (#str) instance attributes within the object you
            wish to display. Attributes can be recursive
            e.g. |one.two.three| for access to |self.one.two.three|
        @address: (#bool) |True| to include the memory address
        @full_name: (#bool) |True| to include the full path to the
            object vs. the qualified name
        @pretty: (#bool) |True| to allow bolding and coloring
        @keyless: (#bool) |True| to display the values of @attributes
            withotu their attribute names
        ..
            class Foo(object):

                def __init__(self, bar, baz=None):
                    self.bar = bar
                    self.baz = baz

                __repr__ = prepr('bar', 'baz', address=False)

            foo = Foo('foobar')
            repr(foo)
        ..
        |<Foo:bar=`foobar`, baz=None>|
    """
    def _format(obj, attribute):
        try:
            if keyless:
                val = getattr_in(obj, attribute)
                if val is not None:
                    return repr(val)
            else:
                return '%s=%s' % (attribute,
                                  repr(getattr_in(obj, attribute)))
        except AttributeError:
            return None

    def prep(obj, address=address, full_name=full_name, pretty=pretty,
             keyless=keyless, **kwargs):
        if address:
            address = ":%s" % hex(id(obj))
        else:
            address = ""
        data = list(filter(lambda x: x is not None,
                           map(lambda a: _format(obj, a), attributes)))
        if data:
            data = ':%s' % ', '.join(data)
        else:
            data = ''
        return stdout_encode("<%s%s%s>" % (get_obj_name(obj), data, address))
    return prep


class prepr(UserString):
    __slots__ = (
        'obj', 'data', 'options', 'outputs', 'line_break',
        'supplemental', 'hex', 'pretty', '_no_keys')

    def __init__(self, *attrs, _self=None, _break=False, _doc=False,
                 _address=True, _pretty=False, _full_name=False,
                 _no_keys=False, **kwattrs):
        self.obj = _self
        self.line_break = _break
        self.data = ""
        self.doc = _doc
        self.address = _address
        self.attrs = OrderedDict()
        self.supplemental = None
        self.pretty = _pretty
        self.full_name = _full_name
        self._no_keys = _no_keys
        self.add_attrs(*attrs, **kwattrs)

    def __str__(self):
        return str(self.format())

    __repr__ = __str__

    def __call__(self, obj):
        @wraps(obj)
        def wrapper(*args, **kwargs):
            self.obj = args[0]
            supp = obj(args[0])
            self.supplemental = str(supp) if supp else None
            return str(self.format())
        return wrapper

    def __len__(self):
        return len(uncolorize(self.data))

    def add_attrs(self, *args, _order=[], **kwargs):
        """ Adds attributes to the __repr__ string
            @order: optional #list containing order to display kwargs
        """
        for arg in args:
            if isinstance(arg, (tuple, list)):
                key, color = arg
                self.attrs[key] = (None, color)
            else:
                self.attrs[arg] = (None, None)
        if not _order:
            for key, value in kwargs.items():
                self.attrs[key] = (value, None)
        else:
            for key in _order:
                self.attrs[key] = (kwargs[key], None)

    def _getattrs(self, func, obj, attrs):
        for attr in attrs:
            try:
                obj = func(obj, attr)
            except (AttributeError, TypeError):
                raise AttributeError("`{} not in class".format(attr))
        return obj

    def _format_attrs(self):
        """ Formats the self.attrs #OrderedDict """
        _bold = bold
        _colorize = colorize
        if not self.pretty:
            _bold = lambda x: x
            _colorize = lambda x, c: x
        attrs = []
        add_attr = attrs.append
        if self.doc and hasattr(self.obj, "__doc__"):
            # Optionally attaches documentation
            if self.obj.__doc__:
                add_attr("`{}`".format(self.obj.__doc__.strip()))
        if self.attrs:
            # Attach request attributes
            for key, value in self.attrs.items():
                value, color = value
                try:
                    value = value or \
                        self._getattrs(getattr, self.obj, key.split("."))
                except AttributeError:
                    pass
                value = _colorize(value, color) if color else value
                v = None
                if value is not None:
                    value = "`{}`".format(value) \
                        if isinstance(value, Look.str_) else value
                    k, v = _bold(key), value
                else:
                    k, v = _bold(key), str(value)
                if v:
                    k = '{}='.format(k) if not self._no_keys else ''
                    add_attr("{}{}".format(k, v))
        if len(attrs):
            breaker = "\n    " if self.line_break and len(attrs) > 1 else ""
            return breaker + ((", "+breaker).join(attrs)) + breaker.strip(" ")
        else:
            return ""

    def format(self):
        """ Formats the __repr__ string
            -> #str containing __repr__ output
        """
        _bold = bold
        _break = "\n    "
        if not self.pretty:
            _bold = lambda x: x
        # Attach memory address and return
        _attrs = self._format_attrs()
        parent_name = get_parent_name(self.obj) if self.full_name else None
        self.data = "<{}{}:{}{}>{}".format(
            parent_name + "." if parent_name else "",
            get_obj_name(self.obj),
            _attrs,
            ":{}".format(hex(id(self.obj))) if self.address else "",
            _break+self.supplemental if self.supplemental else "")
        return stdout_encode(self.data)


class RandData(object):
    """ ..
            from vital.debug import RandData

            rd = RandData(str)

            rd.list(10)
            # -> [
            #   'nmeFFu', '4Ouonu5', 'HEb6OaujYL', 'C2KaV', 'CfYioZy',
            #   'i5PKqZ', 'nA', 'Upzyt', 'Wc', '252V']

            #: Changes the values to <int>
            rd.datatype = int

            rd.dict(10)
            # -> {
            #   'Lnhy6tcwY': 16440106327475681, 'fvKgj': 94196830283393128,
            #   'JP8l8': 78674425573407922, 'Gd22cjsKL': 58888721526521112,
            #   'H8dNss': 5741955182682407, 'WVs': 18667502416672836,
            #   'epW': 34582433811443162, 'AE': 70332483677259590,
            #   'Amfh81vB': 69006761006741551, 's1afta8tCg': 95049217390404053
            # }

            #: Generates random data for all sequence-like structures
            rd.sequence(list, 10)
            # -> [
            #    11012231953494111, 16269794115998776, 65722985758809749,
            #    77868898572532507, 98520928111804919, 80994560862734842,
            #    13185719941276731, 85143540729944251, 20857604948101100,
            #    71388371195677723]

            #: Generates random data for all mapping-like structures
            rd.mapping(dict, 10)
            # -> {
            #    '8CH8G5': 85696434945877667, '3WF': 45209983369688209,
            #    'YXdniVzf7v': 77685036519618428, 'ke2MPk': 14167846091729017,
            #    'sT3A': 76501719321948408, 'zaK9YYsup2': 70344281084943701,
            #    'YAz0': 35778426682427301, 'zd5oj': 95145008229111099,
            #    '0x20mL6': 57137525121733914, '2dzvKZQf': 71981907451087745
            # }
        ..
    """
    __slots__ = (
        'datatype', 'keyspace', 'str_keyspace', 'digits_keyspace',
        'random', 'tlds', 'typemap')
    randomType = "random"
    urlType = "url"
    generatorType = "generator"
    emailType = "email"
    hashType = "hash"

    def __init__(self, datatype=int, use=False):
        """ Fills data structures with random data

            @datatype: #builtin to use as the values for the random data.
                Supported types are in :prop:typemap
            @use: random module, :class:random.SystemRandom used by default
        """
        self.datatype = datatype
        self.keyspace = string.ascii_letters+string.digits
        self.str_keyspace = string.ascii_letters
        self.digits_keyspace = string.digits
        self.random = use or random
        self.tlds = []
        self.typemap = {
          int: lambda: self.randint,
          float: lambda: self.randfloat,
          list: lambda: self.randlist,
          dict: lambda: self.randdict,
          tuple: lambda: self.randtuple,
          set: lambda: self.randset,
          str: lambda: self.randstr,
          bytes: lambda: self.randbytes,
          deque: lambda: self.randdeque,
          self.generatorType: lambda: self.randgenerator,
          self.urlType: lambda: self.randurl,
          self.emailType: lambda: self.randemail,
          self.hashType: lambda: self.randhash,
        }

    @prepr('random', 'datatype')
    def __repr__(self): return

    @property
    def randstr(self):
        """ -> #str result of :func:gen_rand_str """
        return gen_rand_str(
            4, 10, use=self.random, keyspace=list(string.ascii_letters))

    @property
    def randbytes(self):
        """ -> #bytes result of bytes-encoded :func:gen_rand_str """
        return gen_rand_str(
            10, 30, use=self.random, keyspace=list(self.keyspace)
        ).encode("utf-8")

    @property
    def randdomain(self):
        """ -> a randomized domain-like name """
        return '.'.join(
            rand_readable(3, 6, use=self.random, density=3)
            for _ in range(self.random.randint(1, 2))
        ).lower()

    @property
    def randpath(self):
        """ -> a random URI-like #str path """
        return '/'.join(
            gen_rand_str(3, 10, use=self.random, keyspace=list(self.keyspace))
            for _ in range(self.random.randint(0, 3)))

    @property
    def randtld(self):
        """ -> a random #str tld via :mod:tlds """
        self.tlds = tuple(tlds.tlds) if not self.tlds else self.tlds
        return self.random.choice(self.tlds)

    @property
    def randurl(self):
        """ -> a random url-like #str via :prop:randdomain, :prop:randtld,
                and :prop:randpath
        """
        return "{}://{}.{}/{}".format(
            self.random.choice(("http", "https")),
            self.randdomain, self.randtld, self.randpath)

    @property
    def randemail(self):
        """ -> a random email address-like #str via :prop:randdomain and
                :prop:randtld
        """
        return "{}@{}.{}".format(
            self.randdomain, self.randdomain, self.randtld)

    @property
    def randint(self):
        """ -> a random #int """
        return self.random.randint(1, 1000000000)

    @property
    def randfloat(self):
        """ -> a random #float """
        return self.random.random()

    @property
    def randlist(self):
        """ -> a #list of random #int """
        return [
            self.randint
            for x in range(0, self.random.randint(3, 10))]

    @property
    def randgenerator(self):
        """ -> a #generator of random #int """
        return (
            self.randint
            for x in range(0, self.random.randint(3, 10)))

    @property
    def randtuple(self):
        """ -> a #tuple of random #int """
        return tuple(
            self.randint
            for x in range(0, self.random.randint(3, 10)))

    @property
    def randdeque(self):
        """ -> a :class:collections.deque of random #int """
        return deque(
            self.randint
            for x in range(0, self.random.randint(3, 10)))

    @property
    def randdict(self):
        """ -> a #dict of |{random_string: random_int}| """
        return {
            self.randstr: self._map_type(int)
            for x in range(self.random.randint(3, 10))}

    @property
    def randset(self):
        """ -> a #set of random integers """
        return {
            self._map_type(int)
            for x in range(self.random.randint(3, 10))}

    @property
    def randhash(self):
        """ -> a random #hash using :func:os.urandom """
        _hash = os.urandom(24)
        return getencoder("hex")(_hash)[0]

    def _to_tuple(self, _list):
        """ Recursively converts lists to tuples """
        result = list()
        for l in _list:
            if isinstance(l, list):
                result.append(tuple(self._to_tuple(l)))
            else:
                result.append(l)
        return tuple(result)

    def _map_type(self, _type=None):
        _type = _type or self.datatype
        if _type == self.randomType:
            _type = random.choice(list(self.typemap.keys()))
        return self.typemap.get(_type, lambda: self.randint)()

    def dict(self, key_depth=1000, tree_depth=1):
        """ Creates a random #dict

            @key_depth: #int number of keys per @tree_depth to generate random
                values for
            @tree_depth: #int dict tree dimensions size, i.e.
                1=|{key: value}|
                2=|{key: {key: value}, key2: {key2: value2}}|

            -> random #dict
        """
        if not tree_depth:
            return self._map_type()
        return {
            self.randstr: self.dict(key_depth, tree_depth-1)
            for x in range(key_depth)}

    def defaultdict(self, key_depth=1000, tree_depth=1):
        """ Creates a random :class:collections.defaultdict

            @key_depth: #int number of keys per @tree_depth to generate random
                values for
            @tree_depth: #int dict tree dimensions size, i.e.
                1=|{key: value}|
                2=|{key: {key: value}, key2: {key2: value2}}|

            -> random :class:collections.defaultdict
        """
        if not tree_depth:
            return self._map_type()
        _dict = defaultdict()
        _dict.update({
            self.randstr: self.defaultdict(key_depth, tree_depth-1)
            for x in range(key_depth)})
        return _dict

    def tuple(self, size=1000, tree_depth=1):
        """ Creates a random #tuple

            @size: #int number of random values to include in each @tree_depth
            @tree_depth: #int dict tree dimensions size, i.e.
                1=|(value1, value2)|
                2=|((value1, value2), (value1, value2))|

            -> random #tuple
        """
        if not tree_depth:
            return self._map_type()
        return tuple(self.tuple(size, tree_depth-1) for x in range(size))

    def set(self, size=1000):
        """ Creates a random #set

            @size: #int number of random values to include in the set

            -> random #set
        """
        return set(lambda: self._map_type() for x in range(size))

    def list(self, size=1000, tree_depth=1):
        """ Creates a random #list

            @size: #int number of random values to include in each @tree_depth
            @tree_depth: #int dict tree dimensions size, i.e.
                1=|[value1, value2]|
                2=|[[value1, value2], [value1, value2]]|

            -> random #list
        """
        if not tree_depth:
            return self._map_type()
        return list(self.list(size, tree_depth-1) for x in range(size))

    def deque(self, size=1000, tree_depth=1):
        """ Creates a random :class:collections.deque

            @size: #int number of random values to include in each @tree_depth
            @tree_depth: #int dict tree dimensions size, i.e.
                1=|deque([value1, value2])|
                2=|deque([[value1, value2], [value1, value2]])|

            -> random :class:collections.deque
        """
        if not tree_depth:
            return self._map_type()
        return deque([self.deque(size, tree_depth-1) for x in range(size)])

    def generator(self, size=1000, tree_depth=1):
        """ Creates a random #generator

            @size: #int number of random values to include in each @tree_depth
            @tree_depth: #int dict tree dimensions size, i.e.
                1=|(value1, value2)|
                2=|((value1, value2), (value1, value2))|

            -> random :class:collections.deque
        """
        if not tree_depth:
            return self._map_type()
        return (self.generator(size, tree_depth-1) for x in range(size))

    def sequence(self, struct, size=1000, tree_depth=1, append_callable=None):
        """ Generates random values for sequence-like objects

            @struct: the sequence-like structure you want to fill with random
                data
            @size: #int number of random values to include in each @tree_depth
            @tree_depth: #int dict tree dimensions size, i.e.
                1=|(value1, value2)|
                2=|((value1, value2), (value1, value2))|
            @append_callable: #callable method which appends/adds data to your
                sequence-like structure - e.g. :meth:list.append

            -> random @struct
            ..
                from collections import UserList
                from vital.debug import RandData

                class MySequence(UserList):
                    pass

                rd = RandData(int)

                my_seq = MySequence()
                rd.sequence(my_seq, 3, 1, my_seq.append)
                # -> [88508293836062443, 49097807561770961, 55043550817099444]
            ..
        """
        if not tree_depth:
            return self._map_type()
        _struct = struct()
        add_struct = _struct.append if not append_callable \
            else getattr(_struct, append_callable)
        for x in range(size):
            add_struct(self.sequence(
                struct, size, tree_depth-1, append_callable))
        return _struct

    def mapping(self, struct, key_depth=1000, tree_depth=1,
                update_callable=None):
        """ Generates random values for dict-like objects

            @struct: the dict-like structure you want to fill with random data
            @size: #int number of random values to include in each @tree_depth
            @tree_depth: #int dict tree dimensions size, i.e.
                1=|{key: value}|
                2=|{key: {key: value}, key2: {key2: value2}}|
            @update_callable: #callable method which updates data in your
                dict-like structure - e.g. :meth:builtins.dict.update

            -> random @struct
            ..
                from collections import UserDict
                from vital.debug import RandData

                class MyDict(UserDict):
                    pass

                rd = RandData(int)

                my_dict = MyDict()
                rd.dict(my_dict, 3, 1, my_dict.update)
                # -> {
                #   'SE0ZNy0F6O': 42078648993195761,
                #   'pbK': 70822820981335987,
                #   '0A5Aa7': 17503122029338459}
            ..
        """
        if not tree_depth:
            return self._map_type()
        _struct = struct()
        add_struct = _struct.update if not update_callable \
            else getattr(_struct, update_callable)
        for x in range(key_depth):
            add_struct({
                self.randstr: self.mapping(
                    struct, key_depth, tree_depth-1, update_callable)
            })
        return _struct


class Look(object):
    """ ..
            from vital.debug import Look

            _dict = RandData(RandData.hashType).dict(3, 1)
            look = Look(_dict)
            look()
            '''
            qpqF8mrY: b'45670c323f72d67b49663c300c58249649fe0ed91495f1ca'
                  QN: b'047a9b1a27e4ac824091667c18cf5f75d4f0919d376bd41a'
             YLHoDKt: b'405dbbbcff75adde2b2d8a9bec13eaa8dc83bda2615ae70b'
            '''

            look = Look(["for", "the", "love", "of", ("the", "game")])
            look()
            '''
            0] for
            1] the
            2] love
            3] of
            4] 0) the
               1) game
            '''

            look(set(("penguin",)))
            # -> 1… penguin
        ..
    """
    __slots__ = ('obj', '_depth', '_justify', '_key_maxlen', '_dicts')
    str_ = (str, UserString)
    dict_ = (dict, defaultdict, OrderedDict, UserDict, Counter)
    list_ = (list, UserList)
    set_ = (set, frozenset)
    numeric_ = (int, float, complex)

    def __init__(self, obj=None):
        """ Formats/prettifies your data structures

            @obj: the object you wish to prettify
        """
        self.obj = obj
        self._depth = 0
        self._justify = 0
        self._key_maxlen = get_terminal_width()//2
        self._dicts = {}

    @prepr('_key_maxlen')
    def __repr__(self): return

    def __call__(self, obj=None):
        self.pretty_print(obj or self.obj)

    def _dict_prefix(self, key, value, i, dj=0, color=None, separator=":"):
        just = self._justify if i > 0 else dj
        key = cut(str(key), self._key_maxlen).rjust(just)
        key = colorize(key, color=color)
        pref = "{}{} {}".format(key, separator, value)
        """pref = "{}{} {}".format(colorize(str(key)[:self._key_maxlen]\
            .rjust(just), color=color), separator, value)"""
        return pref

    def _numeric_prefix(self, i, item, just=0, color=None, separator="]"):
        just = self._justify if i > 0 or self._depth == 1 else just
        return "{}{} {}".format(
            str(i).rjust(just),
            colorize(separator, color=color),
            item)

    def _prefix(self, i, item, just=0, color=None, separator="-"):
        just = self._justify if i > 0 else just
        return " {} {}".format(
            colorize(str(separator).rjust(just), color=color),
            item)

    def _incr_just_size(self, size):
        self._depth += 1
        self._justify += size

    def _decr_just_size(self, size):
        self._depth -= 1
        self._justify -= size

    def _format_numeric_sequence(self, _sequence, separator="."):
        """ Length of the highest index in chars = justification size """
        if not _sequence:
            return colorize(_sequence, "purple")
        _sequence = _sequence if _sequence is not None else self.obj
        minus = (2 if self._depth > 0 else 0)
        just_size = len(str(len(_sequence)))
        out = []
        add_out = out.append
        for i, item in enumerate(_sequence):
            self._incr_just_size(just_size+minus)
            add_out(self._numeric_prefix(
                i, self.pretty(item, display=False),
                just=just_size, color="blue", separator=separator))
            self._decr_just_size(just_size+minus)
        if not self._depth:
            return padd("\n".join(out) if out else str(out), padding="top")
        else:
            return "\n".join(out) if out else str(out)

    def _format_other_sequence(self, _sequence, separator="&"):
        """ Length of the separator = justification size """
        if not _sequence:
            return colorize(_sequence, "purple")
        minus = (2 if self._depth > 0 else 0)
        just_size = len(separator)
        _sequence = _sequence if _sequence is not None else self.obj
        out = []
        add_out = out.append
        for i, item in enumerate(_sequence):
            self._incr_just_size(just_size+minus)
            add_out(self._prefix(
                i, self.pretty(item, display=False), just=just_size,
                color="blue", separator=separator))
            self._decr_just_size(just_size + minus)
        if not self._depth:
            return padd("\n".join(out) if out else str(out), padding="top")
        else:
            return "\n".join(out) if out else str(out)

    def _recursion_sucks_sometimes(self, _dict):
        _dict_id = id(_dict)
        if _dict_id in self._dicts and self._dicts[_dict_id] < self._depth:
            """ Prevent perpetual recursion """
            return self.object(_dict)
        else:
            self._dicts[_dict_id] = self._depth
            return None

    def dict(self, _dict=None):
        #: (dict, defaultdict, OrderedDict, UserDict)
        _dict = _dict if _dict is not None else self.obj
        if not _dict:
            return colorize(_dict, "purple")
        stop = self._recursion_sucks_sometimes(_dict)
        if stop:
            return stop
        minus = (2 if self._depth > 0 else 0)
        just_size = max(len(uncolorize(str(k))) for k in _dict.keys())+minus
        just_size = just_size if just_size <= self._key_maxlen \
            else self._key_maxlen
        out = []
        add_out = out.append
        for i, (k, item) in enumerate(_dict.items()):
            self._incr_just_size(just_size)
            add_out(self._dict_prefix(
                k, self.pretty(item, display=False), i,
                dj=(just_size-minus), color="bold"))
            self._decr_just_size(just_size)
        if not self._depth:
            return padd("\n".join(out) if out else str(out), padding="top")
        else:
            return "\n".join(out) if out else str(out)

    def list(self, _list=None):
        #: (list, UserList)
        return self._format_numeric_sequence(_list, separator="]")

    def tuple(self, _tuple=None):
        #: (tuple)
        return self._format_numeric_sequence(_tuple, separator=")")

    def deque(self, _deque=None):
        #: (deque)
        return self._format_numeric_sequence(_deque, separator="|")

    def generator(self, _generator=None):
        #: (generator)
        return self._format_other_sequence(_generator, separator="*")

    def set(self, _set=None):
        #: (set, frozenset)
        return self._format_other_sequence(_set, separator="…")

    def sequence(self, _sequence=None):
        #: (set, frozenset)
        return self._format_numeric_sequence(_sequence, separator=".")

    def object(self, _obj=None):
        #: anything else
        _obj = str(_obj) + " "
        return _obj

    def bytes(self, _bytes=None):
        return colorize(str(_bytes) + " ", "purple")

    def number(self, _number=None):
        return colorize(str(_number) + " ", "purple")

    def objname(self, obj=None):
        """ Formats object names in a pretty fashion """
        obj = obj or self.obj
        _objname = self.pretty_objname(obj, color=None)
        _objname = "'{}'".format(colorize(_objname, "blue"))
        return _objname

    def pretty(self, obj=None, display=True):
        """ Formats @obj or :prop:obj

            @obj: the object you'd like to prettify

            -> #str pretty object
        """
        ret = self._format_obj(obj if obj is not None else self.obj)
        if display:
            print(ret)
        else:
            return ret

    def _format_obj(self, item=None):
        """ Determines the type of the object and maps it to the correct
            formatter
        """
        # Order here matters, odd behavior with tuples
        if item is None:
            return getattr(self, 'number')(item)
        elif isinstance(item, self.str_):
            #: String
            return item + " "
        elif isinstance(item, bytes):
            #: Bytes
            return getattr(self, 'bytes')(item)
        elif isinstance(item, self.numeric_):
            #: Float, int, etc.
            return getattr(self, 'number')(item)
        elif isinstance(item, self.dict_):
            #: Dict
            return getattr(self, 'dict')(item)
        elif isinstance(item, self.list_):
            #: List
            return getattr(self, 'list')(item)
        elif isinstance(item, tuple):
            #: Tuple
            return getattr(self, 'tuple')(item)
        elif isinstance(item, types.GeneratorType):
            #: Generator
            return getattr(self, 'generator')(item)
        elif isinstance(item, self.set_):
            #: Set
            return getattr(self, 'set')(item)
        elif isinstance(item, deque):
            #: Deque
            return getattr(self, 'deque')(item)
        elif isinstance(item, Sequence):
            #: Sequence
            return getattr(self, 'sequence')(item)
        #: Any other object
        return getattr(self, 'object')(item)

    @classmethod
    def pretty_objname(self, obj=None, maxlen=50, color="boldcyan"):
        """ Pretty prints object name

            @obj: the object whose name you want to pretty print
            @maxlen: #int maximum length of an object name to print
            @color: your choice of :mod:colors or |None|

            -> #str pretty object name
            ..
                from vital.debug import Look
                print(Look.pretty_objname(dict))
                # -> 'dict\x1b[1;36m<builtins>\x1b[1;m'
            ..
        """
        parent_name = lambda_sub("", get_parent_name(obj) or "")
        objname = get_obj_name(obj)
        if color:
            objname += colorize("<{}>".format(parent_name), color, close=False)
        else:
            objname += "<{}>".format(parent_name)
        objname = objname if len(objname) < maxlen else \
            objname[:(maxlen-1)]+"…>"
        if color:
            objname += colors.RESET
        return objname


class Logg(object):
    """ ..
            from vital.debug import logg

            logg.warning()
            # (Warning)

            logg.notice("Hello world")
            # (Hello world)

            logg("world").warning("Hello")
            # (Hello) world

            logg([780, 779, 778, 777]).success("Found IDs")
            # (Found IDs)
            # 0] 780
            # 1] 779
            # 2] 778
            # 3] 777

            logg("15 wasn't an", int, 12, 13, 14, "15").error()
            # (TypeError) '15 wasn't an integer' 12 13 14 '15'

            #: Changes the loglevel to 'notice'
            logg.set_level("n")

            logg({"hello": "world"}).error()
            # (Error)
            # hello: world

            logg.notice("Hello world")
            # (Hello world)

            logg.log("Hello world")
            #
        ..
    """
    __slots__ = ('message', 'levelmap', 'loglevel', 'pretty', 'include_time')
    SUCCESS = 0
    ERROR = 1
    WARNING = 2
    NOTICE = 3
    LOG = 4
    TIMING = 5
    COUNT = 6
    COMPLETE = 7
    levels = {
        # Verbose, print all
        'v': {SUCCESS, WARNING, NOTICE, LOG, TIMING, COUNT, ERROR, COMPLETE},
        # Print all log, notice, warning, timing, count and error types
        'l': {LOG, NOTICE, WARNING, ERROR, TIMING, COUNT},
        # Print all notices, warnings and errors
        'n': {NOTICE, WARNING, ERROR},
        # Print all warnings and errors
        'w': {WARNING, ERROR},
        # Diagnotics, print timing/count
        'd': {TIMING, COUNT},
        # Print all errors
        'e': {ERROR},
        # Print all successes
        's': {SUCCESS, COMPLETE},
        # Print all timings
        't': {TIMING},
        # Print all counts
        'c': {COUNT},
        # Prints no messages
        'x': set(),
    }

    def __init__(self, *messages, loglevel=None, include_time=False,
                 pretty=False):
        """ A logger that also pretty prints

            @messages: you can pass any python object as an argument here
            @loglevel: #str or iterable combination of any of :attr:levels keys
            @include_time: #bool whether or not to include the current time in
                the log message
            @pretty: #bool whether or not to format @messages with :class:Look

            ``Log levels``
            - |v| Verbose, prints all messages
            - |l| Log, prints all log, notice, warning, error, timing and
                    count type messages
            - |n| Notice, prints all notice, warning, and error type messages
            - |w| Warning, prints all warning and error type messages
            - |d| Diagnostic, prints all timing and count type messages
            - |e| Error, prints all error type messages
            - |s| Success, prints all success and complete type messages
            - |t| Timing, prints all timing type messages
            - |c| Count, prints all count type messages
            - |x| Off, ignores all messages

            These can be used together, a loglevel of "ts" for example
            would allow for both timing and success type messages.

            You may add your own level spec with :meth:add_level
        """
        self.message = list(messages)
        self.levelmap = {}
        self.loglevel = self.set_level(loglevel or "v")
        self.pretty = pretty
        self.include_time = include_time

    @prepr('loglevel')
    def __repr__(self): return

    def __call__(self, *messages, **kwargs):
        """ :see::meth:Logg.__init__

            -> self
        """
        return self.__class__(*messages, **kwargs)

    def add(self, *messages):
        """ Adds a message or messages to the log messages (:attr:message)
            stack
            @messages: log messages

            -> self
        """
        self.message.extend(messages)
        return self

    def add_level(self, name, *allowed_types):
        """ Adds a custom loglevel to :attr:levels

            @name: #str single-char name of the loglevel
            @allowed_types: #int one or more types,
                e.g. :attr:vital.debug.Logg.SUCCESS
        """
        self.levels[name] = set(allowed_types)

    def set_level(self, level):
        """ Sets :attr:loglevel to @level

            @level: #str one or several :attr:levels
        """
        if not level:
            return None
        self.levelmap = set()
        for char in level:
            self.levelmap = self.levelmap.union(self.levels[char])
        self.loglevel = level
        return self.loglevel

    def should_log(self, type):
        """ Decides whether or not something should be logged
            @type: #int log type

            -> #bool True if @type is in :prop:levelmap
        """
        return type in self.levelmap

    def log(self, flag_message=None, padding=None, color=None, force=False):
        """ Log Level: :attr:LOG

            @flag_message: #str flags the message with the given text
                using :func:flag
            @padding: #str 'top', 'bottom' or 'all', adds a new line to the
                specified area with :func:padd
            @color: #str colorizes @flag_message using :func:colorize
            @force: #bool whether or not to force the message to log in spite
                of the assigned log level

            ..
                from vital.debug import Logg
                logg = Logg(loglevel="v")

                logg("World").log("Hello")
                # (Hello) World

                logg("Hello world").log()
                # Hello world
            ..
        """
        if self.should_log(self.LOG) or force:
            self._print_message(
                flag_message=flag_message, color=color or colors.bold,
                padding=padding)

    def success(self, flag_message="Success", padding=None, force=False):
        """ Log Level: :attr:SUCCESS

            @flag_message: #str flags the message with the given text
                using :func:flag
            @padding: #str 'top', 'bottom' or 'all', adds a new line to the
                specified area with :func:padd
            @color: #str colorizes @flag_message using :func:colorize
            @force: #bool whether or not to force the message to log in spite
                of the assigned log level

            ..
                from vital.debug import Logg
                logg = Logg(loglevel="v")

                logg("World").success("Hello")
                # (Hello) World

                logg("Hello world").success()
                # (Success) Hello world
            ..
        """
        if self.should_log(self.SUCCESS) or force:
            self._print_message(
                flag_message=flag_message, color=colors.success_color,
                padding=padding)

    def complete(self, flag_message="Complete", padding=None, force=False):
        """ Log Level: :attr:COMPLETE

            @flag_message: #str flags the message with the given text
                using :func:flag
            @padding: #str 'top', 'bottom' or 'all', adds a new line to the
                specified area with :func:padd
            @color: #str colorizes @flag_message using :func:colorize
            @force: #bool whether or not to force the message to log in spite
                of the assigned log level

            ..
                from vital.debug import Logg
                logg = Logg(loglevel="v")

                logg("World").complete("Hello")
                # (Hello) World

                logg("Hello world").complete()
                # (Complete) Hello world
            ..
        """
        if self.should_log(self.COMPLETE) or force:
            self._print_message(
                flag_message=flag_message, color=colors.complete_color,
                padding=padding)

    def notice(self, flag_message="Notice", padding=None, force=False):
        """ Log Level: :attr:NOTICE

            @flag_message: #str flags the message with the given text
                using :func:flag
            @padding: #str 'top', 'bottom' or 'all', adds a new line to the
                specified area with :func:padd
            @color: #str colorizes @flag_message using :func:colorize
            @force: #bool whether or not to force the message to log in spite
                of the assigned log level

            ..
                from vital.debug import Logg
                logg = Logg(loglevel="v")

                logg("World").notice("Hello")
                # (Hello) World

                logg("Hello world").notice()
                # (Notice) Hello world
            ..
        """
        if self.should_log(self.NOTICE) or force:
            self._print_message(
                flag_message=flag_message, color=colors.notice_color,
                padding=padding)

    def warning(self, flag_message="Warning", padding=None, force=False):
        """ Log Level: :attr:WARNING

            @flag_message: #str flags the message with the given text
                using :func:flag
            @padding: #str 'top', 'bottom' or 'all', adds a new line to the
                specified area with :func:padd
            @color: #str colorizes @flag_message using :func:colorize
            @force: #bool whether or not to force the message to log in spite
                of the assigned log level

            ..
                from vital.debug import Logg
                logg = Logg(loglevel="v")

                logg("World").warning("Hello")
                # (Hello) World

                logg("Hello world").warning()
                # (Warning) Hello world
            ..
        """
        if self.should_log(self.WARNING) or force:
            self._print_message(
                flag_message=flag_message, color=colors.warning_color,
                padding=padding)

    def error(self, flag_message="Error", padding=None, force=False):
        """ Log Level: :attr:ERROR

            @flag_message: #str flags the message with the given text
                using :func:flag
            @padding: #str 'top', 'bottom' or 'all', adds a new line to the
                specified area with :func:padd
            @color: #str colorizes @flag_message using :func:colorize
            @force: #bool whether or not to force the message to log in spite
                of the assigned log level

            ..
                from vital.debug import Logg
                logg = Logg(loglevel="v")

                logg("World").error("Hello")
                # (Hello) World

                logg("Hello world").error()
                # (Error) Hello world
            ..
        """
        if self.should_log(self.ERROR) or force:
            self._print_message(
                flag_message=flag_message, color=colors.error_color,
                padding=padding)

    def timing(self, flag_message, padding=None, force=False):
        """ Log Level: :attr:TIMING

            @flag_message: time-like #float
            @padding: #str 'top', 'bottom' or 'all', adds a new line to the
                specified area with :func:padd
            @force: #bool whether or not to force the message to log in spite
                of the assigned log level

            ..
                from vital.debug import Logg
                logg = Logg(loglevel="v")

                logg("Finished in").timing(0.908)
                # Finished in (908.0ms)

                logg().timing(0.908)
                # (908.0ms)
            ..
        """
        if self.should_log(self.TIMING) or force:
            self._print_message(
                flag_message=Timer.format_time(flag_message), padding=padding,
                reverse=True, color=colors.timing_color)

    def count(self, flag_message, padding=None, force=False):
        """ Log Level: :attr:COUNT

            @flag_message: time-like #float
            @padding: #str 'top', 'bottom' or 'all', adds a new line to the
                specified area with :func:padd
            @force: #bool whether or not to force the message to log in spite
                of the assigned log level

            ..
                from vital.debug import Logg
                logg = Logg(loglevel="v")

                logg("Total apps").count(3)
                # Total apps (3)

                logg().count([0, 1, 2, 3])
                # (4)
            ..
        """
        if self.should_log(self.COUNT) or force:
            flag_message = flag_message \
                if isinstance(flag_message, (int, float)) else \
                str(len(flag_message))
            self._print_message(
                flag_message=flag_message, padding=padding, reverse=True,
                color=colors.timing_color)

    #: Formatting
    def format_message(self, message):
        """ Formats a message with :class:Look """
        look = Look(message)
        return look.pretty(display=False)

    def format_messages(self, messages):
        """ Formats several messages with :class:Look, encodes them
            with :func:vital.tools.encoding.stdout_encode """
        mess = ""
        for message in self.message:
            if self.pretty:
                mess = "{}{}".format(mess, self.format_message(message))
            else:
                mess += str(message)
        if self.include_time:
            return ": {} : {}".format(
                datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), mess)
        return stdout_encode(mess)

    #: Message printing
    def _print_message(self, flag_message=None, color=None, padding=None,
                       reverse=False):
        """ Outputs the message to the terminal """
        if flag_message:
            flag_message = stdout_encode(flag(flag_message,
                                              color=color,
                                              show=False))
            if not reverse:
                print(padd(flag_message, padding),
                      self.format_messages(self.message))
            else:
                print(self.format_messages(self.message),
                      padd(flag_message, padding))
        else:
            print(self.format_messages(self.message))
        self.message = []


logg = Logg(pretty=True)


class ProgressBar(object):
    """ ..
            from vital.debug import ProgressBar

            progress = ProgressBar()
            for x in progress(['hello', 0, 1]):
                print(x)
            # 'hello'
            # 0
            # 1
            # Loading =‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒↦       ☉ (80%)

            for x in progress(500):
                p = ProgressBar(progress)
                for y in p(500):
                    p.update()
            # Loading [405/5000] =‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒↦         ☉ (71%)
            # Loading [405/5000] =                                ↦ӿ (100%)
        ..
    """
    __slots__ = (
        'size', 'progress', 'terminal_width',
        '_barsize', 'parent_bar', 'visible', '_mod')

    def __init__(self, parent_bar=None, visible=True):
        """ |Loading [405/5000] =‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒↦         ☉ (71%)|
            A fun looking, performant progress bar for monitoring the progress
            of loops and tasks

            @parent_bar: another :class:ProgressBar which this progress bar
                is a child of
            @barsize: #int
            @visible: #bool whether or not to print the progress bar to the
                terminal
        """
        self.size = 0
        self._mod = 6
        self.progress = 0
        self._barsize = 33
        self.visible = visible
        self.parent_bar = parent_bar

    @prepr('parent_bar', 'formatter')
    def __repr__(self): return

    def __call__(self, size):
        """ @size: #int or iterable object with __len__ property

            -> #iter if @size is iterable or #range if @size is an #int
        """
        if isinstance(size, (int, float, complex)):
            self.size = size
            self._mod = floor(self.size*0.025) if size > 500 else 2
            for x in range(int(size)):
                yield
                self.update()
        else:
            self.size = len(size)
            self._mod = floor(self.size*0.025) if self.size > 500 else 2
            for x in size:
                yield x
                self.update()

    def format_parent_bar(self):
        """ Formats the parent progress bar """
        return " [{}/{}]".format(
            self.parent_bar.progress+1,
            self.parent_bar.size)

    def format_bar(self):
        """ Builds the progress bar """
        pct = floor(round(self.progress/self.size, 2)*100)
        pr = floor(pct*.33)
        bar = "".join(
            ["‒" for x in range(pr)] + ["↦"] +
            [" " for o in range(self._barsize-pr-1)])
        subprogress = self.format_parent_bar() if self.parent_bar else ""
        message = "Loading{} ={}{} ({}%)".format(subprogress, bar, "☉", pct)
        return message.ljust(len(message)+5)

    def finish(self):
        """ Resets the progress bar and clears it from the terminal """
        pct = floor(round(self.progress/self.size, 2)*100)
        pr = floor(pct*.33)
        bar = "".join([" " for x in range(pr-1)] + ["↦"])
        subprogress = self.format_parent_bar() if self.parent_bar else ""
        fin = "Loading{} ={}{} ({}%)".format(subprogress, bar, "ӿ", pct)
        print(fin.ljust(len(fin)+5), end="\r")
        time.sleep(0.10)
        print("\033[K\033[1A")
        self.progress = 0

    def update(self, progress=0):
        """ Updates the progress bar with @progress if given, otherwise
            increments :prop:progress by 1. Also prints the progress bar.

            @progress: #int to assign to :prop:progress
        """
        self.progress += (progress or 1)
        if self.visible:
            if self.progress % self._mod == 1 or\
               self.progress == self.size - 1:
                print(self.format_bar(), end="\r")
            if self.progress == (self.size):
                self.finish()


class NullIO(StringIO):
    def write(self, txt): pass


class Timer(object):
    """ ..
            from vital.debug import Timer, RandData
            import json

            t1 = Timer()
            t1.start()

            # Generates a random dictionary with str values
            rd = RandData(str).dict(10, 2)

            # Passes rd to test_dumps inside Timer
            t2 = Timer(json.dumps, rd)
            t2.time(100)
            '''
                (dumps<json>)
                Intervals: 100
                     Mean: 80.48µs
                      Min: 72.0µs
                   Median: 79.0µs
                      Max: 187.0µs
                 St. Dev.: 12.75µs
                    Total: 8.05ms
            '''

            # Passes keyword arg sort_keys to json_dumps
            t2.time(100, sort_keys=True)
            '''
                (dumps<json>)
                Intervals: 100
                     Mean: 121.96µs
                      Min: 110.0µs
                   Median: 117.0µs
                      Max: 331.0µs
                 St. Dev.: 24.44µs
                    Total: 12.2ms
            '''

            print("Script execution time:", t1.stop())
            # Script execution time: 354.5ms
        ..
    """
    __slots__ = (
        '_start', '_callableargs', '_first_start', '_stop', 'intervals',
        '_callable', '_array', '_array_len', 'precision', 'progress',
        '_intervals_len', 'allocated_memory')

    def __init__(self, callable=None, *args, _precision=8, _start=0.0,
                 _parent_progressbar=None, **kwargs):
        """ `Timer`

            A memory-efficient, performant and accurate timer for analyzing the
            performance of your python structures and scripts. Timer utilizes
            time.perf_counter() for the most accurate reading of time available
            within python scripts.

            @callable: the object you wish to time
            @*args: arguments you wish to pass to @callable
            @**kwargs: keyword arguments you wish to past to @callable
            @_precision: #int number of decimal places to round execution
                time to
            @_start: #float optional start time in time since epoch
            @_parent_progressbar: optional parent :class:ProgressBar
        """
        self._start = _start
        self._first_start = _start
        self._stop = time.perf_counter()
        self._callable = callable
        self._callableargs = (args, kwargs)
        self.intervals = []
        self._intervals_len = 0
        self.precision = _precision
        self.progress = ProgressBar(parent_bar=_parent_progressbar)

    @prepr('precision')
    def __repr__(self): return

    @property
    def array(self):
        """ Returns :prop:intervals as a numpy array, caches

            -> :class:numpy.array
        """
        return self.intervals

    @property
    def latest(self):
        if len(self.intervals):
            return self.intervals[-1]

    def start(self):
        """ Starts the timer """
        if not self._start:
            self._first_start = time.perf_counter()
            self._start = self._first_start
        else:
            self._start = time.perf_counter()

    def stop(self, precision=0):
        """ Stops the timer, adds it as an interval to :prop:intervals
            @precision: #int number of decimal places to round to

            -> #str formatted interval time
        """
        self._stop = time.perf_counter()
        return self.add_interval(precision)

    @classmethod
    def format_time(self, sec):
        """ Pretty-formats a given time in a readable manner
            @sec: #int or #float seconds

            -> #str formatted time
        """
        # µsec
        if sec < 0.001:
            return "{}{}".format(
                colorize(round(sec*1000000, 2), "purple"), bold("µs"))
        # ms
        elif sec < 1.0:
            return "{}{}".format(
                colorize(round(sec*1000, 2), "purple"), bold("ms"))
        # s
        elif sec < 60.0:
            return "{}{}".format(
                colorize(round(sec, 2), "purple"), bold("s"))
        else:
            floored = floor(sec/60)
            return "{}{} {}{}".format(
                colorize(floored, "purple"),
                bold("m"),
                colorize(floor(sec-(floored*60)), "purple"),
                bold("s"))

    def format_size(self, bytes):
        """ Pretty-formats given bytes size in a readable manner
            @bytes: #int or #float bytes

            -> #str formatted bytes
        """
        # b
        if bytes < 1024:
            return "{}{}".format(colorize(round(
                bytes, 2), "purple"),
                bold("bytes"))
        # kb
        elif bytes < (1024*1000):
            return "{}{}".format(colorize(round(
                bytes/1024, 2), "purple"),
                bold("kB"))
        # mb
        elif bytes < (1024*1024):
            return "{}{}".format(colorize(round(
                bytes/1024, 2), "purple"),
                bold("MB"))

    def add_interval(self, precision=0):
        """ Adds an interval to :prop:intervals
            -> #str formatted time
        """
        precision = precision or self.precision
        interval = round((self._stop - self._start), precision)
        self.intervals.append(interval)
        self._intervals_len += 1
        self._start = time.perf_counter()
        return self.format_time(interval)

    def time(self, intervals=1, *args, _show_progress=True, _print=True,
             _collect_garbage=True, _quiet=True, **kwargs):
        """ Measures the execution time of :prop:_callable for @intervals

            @intervals: #int number of intervals to measure the execution time
                of the function for
            @*args: arguments to pass to the callable being timed
            @**kwargs: arguments to pass to the callable being timed
            @_show_progress: #bool whether or not to print a progress bar
            @_print: #bool whether or not to print the results of the timing
            @_collect_garbage: #bool whether or not to garbage collect
                while timing
            @_quiet: #bool whether or not to disable the print() function's
                ability to output to terminal during the timing

            -> :class:collections.OrderedDict of stats about the timing
        """
        self.reset()
        args = list(args) + list(self._callableargs[0])
        _kwargs = self._callableargs[1]
        _kwargs.update(kwargs)
        kwargs = _kwargs
        if not _collect_garbage:
            gc.disable()  # Garbage collection setting
        gc.collect()
        self.allocated_memory = 0
        for x in self.progress(intervals):
            if _quiet:  # Quiets print()s in the tested function
                sys.stdout = NullIO()
            try:
                self.start()  # Starts the timer
                self._callable(*args, **kwargs)
                self.stop()  # Stops the timer
            except Exception as e:
                if _quiet:  # Unquiets prints()
                    sys.stdout = sys.__stdout__
                raise e
            if _quiet:  # Unquiets prints()
                sys.stdout = sys.__stdout__
        if not _collect_garbage:
            gc.enable()  # Garbage collection setting
        if _print:
            self.info()

    @property
    def mean(self):
        """ -> #float :func:numpy.mean of the timing intervals """
        return round(np.mean(self.array), self.precision)\
            if len(self.array) else None

    @property
    def median(self):
        """ -> #float :func:numpy.median of the timing intervals """
        return round(float(np.median(self.array)), self.precision)\
            if len(self.array) else None

    @property
    def max(self):
        """ -> #float :func:numpy.max of the timing intervals """
        return round(np.max(self.array), self.precision)\
            if len(self.array) else None

    @property
    def min(self):
        """ -> #float :func:numpy.min of the timing intervals """
        return round(np.min(self.array), self.precision)\
            if len(self.array) else None

    @property
    def stdev(self):
        """ -> #float :func:numpy.std of the timing intervals """
        return round(np.std(self.array), self.precision)\
            if len(self.array) else None

    @property
    def exectime(self):
        """ -> #float :func:numpy.sum of the timing intervals """
        return round(np.sum(self.array), self.precision)

    @property
    def runtime(self):
        """ -> #float total time between the first start() and now """
        return round(time.perf_counter()-self._first_start, self.precision)

    @property
    def stats(self):
        """ -> :class:collections.OrderedDict of stats about the time intervals
        """
        return OrderedDict([
            ("Intervals", len(self.array)),
            ("Mean", self.format_time(self.mean or 0)),
            ("Min", self.format_time(self.min or 0)),
            ("Median", self.format_time(self.median or 0)),
            ("Max", self.format_time(self.max or 0)),
            ("St. Dev.", self.format_time(self.stdev or 0)),
            ("Total", self.format_time(self.exectime or 0)),
        ])

    def info(self):
        logg(self.stats, pretty=True).log(
            Look.pretty_objname(self._callable), force=True)

    def reset(self):
        """ Resets the time intervals """
        self._start = 0
        self._first_start = 0
        self._stop = time.perf_counter()
        self.intervals = []
        self._intervals_len = 0


class Compare(object):
    """ ..
            from vital.debug import Compare, RandData
            import json
            import ujson

            # Generates a random dictionary with str values
            rd = RandData(str).dict(10, 2)

            c = Compare(ujson.dumps, json.dumps)
            c.time(100, rd)
            '''
            (Results after 100 intervals)
            ‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒
            #1 ¦  55.58µs            'dumps<ujson>'
            #2 ¦  78.28µs    -40.84% 'dumps<json>'
            ‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒
            '''

            c = Compare(ujson.dumps, json.dumps)
            c.time(100, rd, indent=4)
            '''
            (Results after 100 intervals)
            ‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒
            #1 ¦   49.76µs            'dumps<ujson>'
            #2 ¦  294.94µs   -492.73% 'dumps<json>'
            ‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒
            '''

            c = Compare(ujson.loads, json.loads)
            c.time(100, ujson.dumps(rd))
            '''
            (Results after 100 intervals)
            ‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒
            #1 ¦  62.89µs            'loads<ujson>'
            #2 ¦  86.01µs    -36.76% 'loads<json>'
            ‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒
            '''
        ..
    """
    __slots__ = (
        '_callables', '_callable_results', 'precision', 'verbose',
        'progress', 'name', 'num_intervals')

    def __init__(self, *callables, precision=8, name=None, verbose=False):
        """ A memory-efficient, performant and accurate tool to compare the
            execution times of given @callables

            @callables: one or several #callable objects
            @precision: #int number of decimals to round time intervals to
            @verbose: prints the results with individual :prop:Timer.stats
                included.
            @name: (#str) test name

            |‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒|
            | (dumps<ujson>)                                  |
            | Intervals: 100                                  |
            |      Mean: 46.1µs                               |
            |       Min: 38.59µs                              |
            |    Median: 50.01µs                              |
            |       Max: 104.23µs                             |
            |  St. Dev.: 8.3µs                                |
            |     Total: 4.61ms                               |
            |-·--·--·--·--·--·--·--·--·--·--·--·--·--·--·--·--|
            | (dumps<json>)                                   |
            | Intervals: 100                                  |
            |      Mean: 82.22µs                              |
            |       Min: 72.65µs                              |
            |    Median: 85.63µs                              |
            |       Max: 184.47µs                             |
            |  St. Dev.: 12.63µs                              |
            |     Total: 8.22ms                               |
            |-·--·--·--·--·--·--·--·--·--·--·--·--·--·--·--·--|
            | #1 ¦   46.1µs            'dumps<ujson>'         |
            | #2 ¦  82.22µs    -78.37% 'dumps<json>'          |
            |‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒‒|
        """
        self._callables = []
        self._callable_results = []
        self.precision = precision
        self.verbose = verbose
        self.num_intervals = 0
        self.name = name
        self.progress = ProgressBar(visible=False)
        self.add(*callables)

    @prepr('precision')
    def __repr__(self): return

    def add(self, *callables):
        """ Adds one or several @callables to the next timing """
        self._callables.extend(callables)

    def time(self, intervals=1, *args, _show_progress=True, _print=True,
             _collect_garbage=False, **kwargs):
        """ Measures the execution time of :prop:_callables for @intervals

            @intervals: #int number of intervals to measure the execution time
                of the function for
            @*args: arguments to pass to the callable being timed
            @**kwargs: arguments to pass to the callable being timed
            @_show_progress: #bool whether or not to print a progress bar
            @_print: #bool whether or not to print the results of the timing
            @_collect_garbage: #bool whether or not to garbage collect
                while timing
            @_quiet: #bool whether or not to disable the print() function's
                ability to output to terminal during the timing

            -> #tuple of :class:Timer :prop:results of timing
        """
        self.reset()
        self.num_intervals = intervals
        for func in self.progress(self._callables):
            try:
                #: Don't ruin all timings if just one doesn't work
                t = Timer(
                    func, _precision=self.precision,
                    _parent_progressbar=self.progress)
                t.time(
                    intervals, *args, _print=False,
                    _show_progress=_show_progress,
                    _collect_garbage=_collect_garbage,
                    **kwargs)
            except Exception as e:
                warnings.warn(
                    "{} with {}".format(colorize(
                        "{} failed".format(Look.pretty_objname(
                            func, color="yellow")), "yellow"), repr(e)))
            self._callable_results.append(t)
        if _print:
            self.info()
        return self.results

    def _pct_diff(self, best, other):
        """ Calculates and colorizes the percent difference between @best
            and @other
        """
        return colorize("{}%".format(
            round(((best-other)/best)*100, 2)).rjust(10), "red")

    def info(self, verbose=None):
        """ Prints and formats the results of the timing
            @_print: #bool whether or not to print out to terminal
            @verbose: #bool True if you'd like to print the individual timing
                results in additions to the comparison results
        """
        if self.name:
            flag(bold(self.name))
        flag("Results after {} intervals".format(
            bold(self.num_intervals, close=False)),
            colors.notice_color, padding="top")
        line("‒")
        verbose = verbose if verbose is not None else self.verbose
        if verbose:
            for result in self._callable_results:
                result.info()
                line()
        diffs = [
            (i, result.mean)
            for i, result in enumerate(self._callable_results)
            if result.mean]
        ranking = [
            (i, self._callable_results[i].format_time(r))
            for i, r in sorted(diffs, key=lambda x: x[1])]
        max_rlen = len(str(len(ranking)))+2
        max_rlen2 = max(len(r) for i, r in ranking)+1
        best = self._callable_results[ranking[0][0]].mean
        for idx, (i, rank) in enumerate(ranking, 1):
            _obj_name = Look(self._callables[i]).objname()
            pct = "".rjust(10) if idx == 1 else \
                self._pct_diff(best, self._callable_results[i].mean)
            print(
                ("#"+str(idx)+" ¦").rjust(max_rlen), rank.rjust(max_rlen2),
                pct, "{}".format(_obj_name))
        line("‒", padding="bottom")


    @property
    def results(self):
        """ -> #tuple of :class:Timer objects for each :prop:_callables """
        return tuple(self._callable_results)

    def reset(self):
        """ Resets the intervals """
        self.num_intervals = 0
        self._callable_results = []
