import types, operator
import traceback, sys

############# Error Handling ############
class InterfaceError(TypeError):
    def __init__(self, name, val, type):
        self.name, self.val, self.type = name, val, type
        self.err = repr(self)
        TypeError.__init__(self, self.err)
    def __repr__(self):
        if self.name=="_RETURNS":
            name = "Return code"
        else:
            name = "Parameter '%s'" % self.name
        return "%s expected %s.\nInstead it got '%s' (%s)" % \
                           (name, self.type.dump(),  \
                type(self.val).__name__, repr(self.val))

############# Basic types ##############

# Rather than defining interface abstractions for these types, we will
# use the type objects directly. In the future we may swap in my
# abstract definitions that would allow (e.g.) integers to pass for
# floats or that might convert between types automatically.

Complex = type(1j)
Int = type(1)
Float = type(1.0)
Long = type(1L)

#INumber: pass
# 
# Python numbers do not actually have that much in common right now.
# Floats can be passed to some functions expecting integers but not
# others. Integers can be passed to some functions expecting floats
# but not others. Until this is cleaned up, it is safest not to
# pretend that there is a proper "number" behavior.

########### Interface base class ##############

class Interface:
    """The shared behavior of interface objects"""
    def __init__(self, name, docstring, checkfunc, bases=(), 
                                *args, **kwargs):
        self.__name__ = name
        self.__doc__= docstring
        if bases==():
            bases = [Object]
        self._bases = bases
        self._checkfunc = checkfunc
        self.args = args
        for arg in args:
            assert arg!=None
        self.kwargs = kwargs

    # this allows interfaces to look like object names but be called
    # like functions when necessary
    def __call__(self, *args, **kwargs):
        "For parameterized types -- make a new object with the params"
        return Interface(self.__name__, self.__doc__, self._checkfunc, 
                        self._bases, *args, **kwargs)
        
    def __check__(self, obj):
        for base in self._bases:
            if not base.__check__(obj):
                return 0
        if self._checkfunc is not None:
            return self._checkfunc(obj, *self.args, **self.kwargs)
        return 1

    def dump(self):
        if self.args:
            args = [arg.dump() for arg in self.args]
            argstr = "(%s)" + ",".join(args)
        else:
            args = ""
        return "%s%s" % (self.__name__, args)
        

    def __repr__(self):
        return "<interface %s>" % self.dump()

############# Machinery ###############

        #everything's an object!
Object = Interface("Object", "Any Python object", None, [])

InterfaceInterface = Interface("Interface", "Interface interface", 
            lambda obj: hasattr(obj, "__check__"))

def _unionCheck(obj, *bases):
    for base in bases:
        if checkType(obj, base):
            return 1
    return 0

def Union(*args):
    comma = ", "
    names = []
    for base in args:
        if base==None:
            names.append("None")
        else:
            names.append(base.__name__)
    names = comma.join(names)
    return Interface(None, "Union of %s" % names,
                             lambda obj,args=args: _unionCheck(obj, *args))

def _tupleCheck(obj, *types):
    try:
        if len(types)!=len(obj):
            return 0
    except IndexError:
        return 0

    for aval, aniface in zip(obj, types):
        if not checkType(aval, aniface):
            return 0
    return 1

Tuple = Interface(None, "Tuple", _tupleCheck)

########### Strings ########################
String=Interface("String", 
    "Unicode or 8-bit string",
    lambda obj: type(obj) in (types.StringType, types.UnicodeType))

########### Ancient Protocols #################
Undefined = "_____________undefined______________"

def _isseq(obj, valuetype=Undefined):
    if type(obj)==types.InstanceType:
        if not hasattr( obj, "__getitem__"):
            return 0
    elif not operator.isSequenceType(obj):
        return 0

    if valuetype is not Undefined:
        try:
            # check first and last -- that's an approximation!
            if not checkType(obj[1], valuetype): return 0
            if not checkType(obj[-1], valuetype): return 0
        except IndexError:
            pass # empty list is considered good enough

    return 1 # passed all tests!

Sequence = Interface("Sequence", "Some kind of sequence", _isseq)

def _ismapping(obj, KeyType = None, ValueType = None):
    if type(obj)==types.InstanceType: 
        return (hasattr(obj, "__getitem__")
                and hasattr(obj, "items"))
    else:
        return operator.isMappingType(obj)

Mapping = Interface("Mapping", "Mapping from keys to values", _ismapping)

Callable=Interface("ICallable", "Object that can be called like function", callable)
        # built-in callable is a little smarter than
        # types.isSequenceType, etc.

StrictBoolean = Interface("StrictBoolean", 
                        "0 or 1 value", 
                        lambda obj: obj in (0, 1))        
        
ReadStream= Interface("ReadStream", 
                "object that can be read from like a file",
                lambda obj: hasattr(obj, "read"))

WriteStream= Interface("ReadStream", 
                "object that can be read from like a file",
                lambda obj: hasattr(obj, "read"))

def _findInterface(obj, iface):
    """Helper function that figures out if an interface is in an objects
     __interfaces__ list. Note that objects with a single interface
     may not bother to wrap it in a list. That's why this function is
     not trivial."""
    ifaces = getattr(obj, "__interfaces__", None)
    
    if type(ifaces) not in (types.ListType, types.TupleType):
        ifaces = [ifaces] #normalize the single interface shortcut

    if iface in ifaces:
        return 1

    # now look for all of the interface's base interfaces in the
    # object.
    for base in iface._bases:
        if _findInterface(obj, base):
            return 1
    # give up
    return 0

def checkType(val, iface):
    "Checks that an object adheres to a type"

    if iface == None == iface:
        return 1

    # if the iface is a tuple or list, we do unpacking on the value
    if type(iface) in (types.TupleType, types.ListType):
        return _tupleCheck(val, iface)

    # if the iface is just a class or type, we just use isinstance
    elif type(iface) in (types.ClassType, types.TypeType):
        return isinstance(val, iface)

    # if the object CLAIMS to support an interface, we believe it
    elif _findInterface(val, iface):
        return 1    

    # if the object LOOKS like it supports an interface,  good 'nuff
    else:
        return iface.__check__(val)

####### Strictness (exception throwing or warning) ############
_g_strict = None

def setstrict(strict):
    global _g_strict 
    _g_strict = strict

######## Magic runtime functions ############

def __paramcheck__(localsdict=None, typesdict=None):
    """Magic function to check all params in a localsdict against
       type declarations in a typesdict"""
    # XXX one day we should make the params required and dump this
    # code.
    for descr, typ in typesdict.items():
        name=descr.split(":")[0]
        if name in ("_RETURNS", "self", "__types__"):
            continue
        obj = localsdict[name]
        if not checkType(obj, typ):
            raise InterfaceError(name, obj, typ)

def __rccheck__(val, localsdict, typesdict):
    "Like paramcheck for locals"
    for descr, typ in typesdict.items():
        name=descr.split(":")[0]
        if name=="_RETURNS" and not checkType(val, typ):
            raise InterfaceError(name, val, typ)
    return val

def __typewarn__(exception):
    # XXX use Python 2.1 warning features when available
    if _g_strict is None:
        import os
        setstrict( os.getenv("PYTHON_STRICT_TYPECHECK"))

    if _g_strict:
        raise exception
    else:
        # of course this should use the Python 2.1 warning
        # framework eventually!
        print "Type error:", exception

def __declare__(*args, **args): 
    "dummy function for declaration recognition purposes"

############ test code ##################

def _test1(module, integer, func, stream):
    __types__ = {"module: A module object": types.ModuleType,
                  "integer: An integer object": Int,
                  "func: A callable object": Callable,
                  "stream: A readable stream": ReadStream,
                 "_RETURNS: Return value": Int}
    __paramcheck__(locals(), __types__)
    
    return __rccheck__(5, locals(), __types__)

def _test2(tuple, string, interface, interface2):
    __types__ = {"tuple: A sequence": Sequence,
                  "string: A string": String,
                  "interface: An interface obj": Interface,
                  "interface2: An interface obj": Interface}
    __paramcheck__(locals(), __types__)

def _test3(object, object2, object3):
    __types__ = {"object: Any old object": Object,
                    "object2: Any old object": Object,
                    "object3: Any old object": Object,
                    "_RETURNS: A pair of objects": (Object, Object) }
    __paramcheck__(locals(), __types__)
    return __rccheck__((object, object2), locals(), __types__)

if __name__=="__main__":
    # use doctest eventually!
    import os
    _test1(os, 5, _test2, os.popen("blah"))
    _test2((), "abc", String, String)
    _test3((), "abc", None)
    
    try:
            _test1(1, 2, 3, 4)
            assert 0, "Should not have got to this point!"
    except InterfaceError:
        pass # expect this exception
