import io
import sys
import types
from collections import deque

PY3 = sys.version_info[0] == 3

if PY3:
    string_types = str,
else:
    string_types = basestring,  # noqa


class cached_property(object):
    def __init__(self, func):
        self.func = func
        self.__doc__ = func.__doc__

    def __get__(self, obj, cls):
        if obj is None:
            return self
        value = obj.__dict__[self.func.__name__] = self.func(obj)
        return value


def rudimentary_repr(obj, maxdepth=5):
    if not maxdepth:
        return '...'
    obj_type = type(obj)
    newdepth = maxdepth - 1

    # specifically handle few of the container builtins that would normally do repr on contained values
    if isinstance(obj, dict):
        if obj_type is not dict:
            return '%s({%s})' % (
                obj_type.__name__,
                ', '.join('%s: %s' % (
                    rudimentary_repr(k, maxdepth),
                    rudimentary_repr(v, newdepth)
                ) for k, v in obj.items()))
        else:
            return '{%s}' % ', '.join('%s: %s' % (
                rudimentary_repr(k, maxdepth),
                rudimentary_repr(v, newdepth)
            ) for k, v in obj.items())
    elif isinstance(obj, list):
        if obj_type is not list:
            return '%s([%s])' % (obj_type.__name__, ', '.join(rudimentary_repr(i, newdepth) for i in obj))
        else:
            return '[%s]' % ', '.join(rudimentary_repr(i, newdepth) for i in obj)
    elif isinstance(obj, tuple):
        if obj_type is not tuple:
            return '%s(%s%s)' % (
                obj_type.__name__,
                ', '.join(rudimentary_repr(i, newdepth) for i in obj),
                ',' if len(obj) == 1 else '')
        else:
            return '(%s%s)' % (', '.join(rudimentary_repr(i, newdepth) for i in obj), ',' if len(obj) == 1 else '')
    elif isinstance(obj, set):
        if obj_type is not set:
            return '%s({%s})' % (obj_type.__name__, ', '.join(rudimentary_repr(i, newdepth) for i in obj))
        else:
            return '{%s}' % ', '.join(rudimentary_repr(i, newdepth) for i in obj)
    elif isinstance(obj, frozenset):
        return '%s({%s})' % (obj_type.__name__, ', '.join(rudimentary_repr(i, newdepth) for i in obj))
    elif isinstance(obj, deque):
        return '%s([%s])' % (obj_type.__name__, ', '.join(rudimentary_repr(i, newdepth) for i in obj))
    elif isinstance(obj, BaseException):
        return '%s(%s)' % (obj_type.__name__, ', '.join(rudimentary_repr(i, newdepth) for i in obj.args))
    elif obj_type in (type, types.ModuleType,
                      types.FunctionType, types.MethodType,
                      types.BuiltinFunctionType, types.BuiltinMethodType,
                      io.IOBase):
        # hardcoded list of safe things. note that isinstance ain't used
        # (we don't trust subclasses to do the right thing in __repr__)
        return repr(obj)
    elif not hasattr(obj, '__dict__'):
        # note that this could be `not hasattr(obj, '__dict__') and not hasattr(obj, '__slots__')`
        # but lots of objects safe to repr (like sockets) have __slots__
        # (I don't want to have the burden of maintaining a huge list of safe to repr types)
        #
        # the assumption is that if you use __slots__ you know what you're doing and you ain't gonna be stupid enough to
        # have a side-effect in __repr__ (I hope ...)
        return repr(obj)
    else:
        # if the object has a __dict__ then it's probably an instance of a pure python class, assume bad things
        #  with side-effects will be going on in __repr__ - use the default instead (object.__repr__)
        return object.__repr__(obj)
