from collections import OrderedDict
import py
from rpython.rlib.objectmodel import (
    r_dict, UnboxedValue, Symbolic, compute_hash, compute_identity_hash,
    compute_unique_id, current_object_addr_as_int, we_are_translated,
    prepare_dict_update, reversed_dict, specialize, enforceargs, newlist_hint,
    resizelist_hint, is_annotation_constant, always_inline, NOT_CONSTANT,
    iterkeys_with_hash, iteritems_with_hash, contains_with_hash,
    setitem_with_hash, getitem_with_hash, delitem_with_hash, import_from_mixin,
    fetch_translated_config)
from rpython.translator.translator import TranslationContext, graphof
from rpython.rtyper.test.tool import BaseRtypingTest
from rpython.rtyper.test.test_llinterp import interpret
from rpython.conftest import option

def strange_key_eq(key1, key2):
    return key1[0] == key2[0]   # only the 1st character is relevant
def strange_key_hash(key):
    return ord(key[0])

def play_with_r_dict(d):
    d['hello'] = 41
    d['hello'] = 42
    assert d['hi there'] == 42
    try:
        unexpected = d["dumb"]
    except KeyError:
        pass
    else:
        assert False, "should have raised, got %s" % unexpected
    assert len(d) == 1
    assert 'oops' not in d

    count = 0
    for x in d:
        assert x == 'hello'
        count += 1
    assert count == 1

    assert d.get('hola', -1) == 42
    assert d.get('salut', -1) == -1
    d1 = d.copy()
    del d['hu!']
    assert len(d) == 0
    assert d1.keys() == ['hello']
    d.update(d1)
    assert d.values() == [42]
    lst = d.items()
    assert len(lst) == 1 and len(lst[0]) == 2
    assert lst[0][0] == 'hello' and lst[0][1] == 42

    count = 0
    for x in d.iterkeys():
        assert x == 'hello'
        count += 1
    assert count == 1

    count = 0
    for x in d.itervalues():
        assert x == 42
        count += 1
    assert count == 1

    count = 0
    for x in d.iteritems():
        assert len(x) == 2 and x[0] == 'hello' and x[1] == 42
        count += 1
    assert count == 1

    d.clear()
    assert d.keys() == []
    return True   # for the tests below


def test_recursive_r_dict_repr():
    import operator
    rdic = r_dict(operator.eq, hash)
    rdic['x'] = rdic
    assert str(rdic) == "r_dict({'x': r_dict({...})})"
    assert repr(rdic) == "r_dict({'x': r_dict({...})})"

def func_r_dict():
    # NB. this test function is also annotated/rtyped by the next tests
    d = r_dict(strange_key_eq, strange_key_hash)
    return play_with_r_dict(d)

class Strange:
    def key_eq(strange, key1, key2):
        return key1[0] == key2[0]   # only the 1st character is relevant

    def key_hash(strange, key):
        return ord(key[0])

def func_r_dict_bm():
    # NB. this test function is also annotated by the next tests
    strange = Strange()
    d = r_dict(strange.key_eq, strange.key_hash)
    return play_with_r_dict(d)

def test_annotate_r_dict():
    t = TranslationContext()
    a = t.buildannotator()
    a.build_types(func_r_dict, [])
    #t.view()
    graph = graphof(t, strange_key_eq)
    assert a.binding(graph.getargs()[0]).knowntype == str
    assert a.binding(graph.getargs()[1]).knowntype == str
    graph = graphof(t, strange_key_hash)
    assert a.binding(graph.getargs()[0]).knowntype == str

def test_annotate_r_dict_bm():
    t = TranslationContext()
    a = t.buildannotator()
    a.build_types(func_r_dict_bm, [])
    #t.view()
    strange_key_eq = Strange.key_eq.im_func
    strange_key_hash = Strange.key_hash.im_func

    Strange_def = a.bookkeeper.getuniqueclassdef(Strange)

    graph = graphof(t, strange_key_eq)
    assert a.binding(graph.getargs()[0]).classdef == Strange_def
    assert a.binding(graph.getargs()[1]).knowntype == str
    assert a.binding(graph.getargs()[2]).knowntype == str
    graph = graphof(t, strange_key_hash)
    assert a.binding(graph.getargs()[0]).classdef == Strange_def
    assert a.binding(graph.getargs()[1]).knowntype == str


def test_unboxed_value():
    class Base(object):
        __slots__ = ()

    class C(Base, UnboxedValue):
        __slots__ = 'smallint'

    assert C(17).smallint == 17
    assert C(17).get_untagged_value() == 17

    class A(UnboxedValue):
        __slots__ = ['value']

    assert A(12098).value == 12098
    assert A(12098).get_untagged_value() == 12098

def test_symbolic():
    py.test.skip("xxx no test here")

def test_symbolic_raises():
    s1 = Symbolic()
    s2 = Symbolic()
    py.test.raises(TypeError, "s1 < s2")
    py.test.raises(TypeError, "hash(s1)")

def test_compute_hash():
    from rpython.rlib.objectmodel import _hash_string, _hash_float, _hash_tuple
    assert compute_hash("Hello") == _hash_string("Hello")
    assert compute_hash(7) == 7
    assert compute_hash(-3.5) == _hash_float(-3.5)
    assert compute_hash(None) == 0
    assert compute_hash(("world", None, 7)) == _hash_tuple(("world", None, 7))
    #
    class Foo(object):
        def __hash__(self):
            return 42
    foo = Foo()
    h = compute_hash(foo)
    assert h == object.__hash__(foo)
    assert h == getattr(foo, '__precomputed_identity_hash')
    assert compute_hash(None) == 0

def test_compute_hash_float():
    from rpython.rlib.rfloat import INFINITY, NAN
    assert compute_hash(INFINITY) == 314159
    assert compute_hash(-INFINITY) == -271828
    assert compute_hash(NAN) == 0

def test_compute_identity_hash():
    class Foo(object):
        def __hash__(self):
            return 42
    foo = Foo()
    h = compute_identity_hash(foo)
    assert h == object.__hash__(foo)
    assert h == getattr(foo, '__precomputed_identity_hash')

def test_compute_unique_id():
    from rpython.rlib.rarithmetic import intmask
    class Foo(object):
        pass
    foo = Foo()
    x = compute_unique_id(foo)
    assert type(x) is int
    assert x == intmask(id(foo))

def test_current_object_addr_as_int():
    from rpython.rlib.rarithmetic import intmask
    class Foo(object):
        pass
    foo = Foo()
    assert current_object_addr_as_int(foo) == intmask(id(foo))

class TestObjectModel(BaseRtypingTest):

    def test_we_are_translated(self):
        assert we_are_translated() is False

        def fn():
            return we_are_translated()
        res = self.interpret(fn, [])
        assert res is True

    def test_rtype_r_dict(self):
        res = self.interpret(func_r_dict, [])
        assert res is True

    def test_rtype_r_dict_bm(self):
        res = self.interpret(func_r_dict_bm, [])
        assert res is True

    def test_rtype_constant_r_dicts(self):
        d1 = r_dict(strange_key_eq, strange_key_hash)
        d1['hello'] = 666
        d2 = r_dict(strange_key_eq, strange_key_hash)
        d2['hello'] = 777
        d2['world'] = 888
        def fn(i):
            if i == 1:
                d = d1
            else:
                d = d2
            return len(d)
        res = self.interpret(fn, [1])
        assert res == 1
        res = self.interpret(fn, [2])
        assert res == 2

    def test_rtype_r_dict_singlefrozen_func(self):
        class FreezingClass(Strange):
            def _freeze_(self):
                return True
        obj = FreezingClass()
        def fn():
            d = r_dict(obj.key_eq, obj.key_hash)
            return play_with_r_dict(d)
        assert self.interpret(fn, []) is True

    def test_rtype_r_dict_singlefrozen_func_pbc(self):
        class FreezingClass(Strange):
            def _freeze_(self):
                return True
        obj = FreezingClass()
        pbc_d = r_dict(obj.key_eq, obj.key_hash)
        def fn():
            return play_with_r_dict(pbc_d)
        assert self.interpret(fn, []) is True

    def test_rtype_r_dict_exceptions(self):
        def raising_hash(obj):
            if obj.startswith("bla"):
                raise TypeError
            return 1
        def eq(obj1, obj2):
            return obj1 is obj2
        def f():
            d1 = r_dict(eq, raising_hash)
            d1['xxx'] = 1
            try:
                x = d1["blabla"]
            except Exception:
                return 42
            return x
        res = self.interpret(f, [])
        assert res == 42

        def f():
            d1 = r_dict(eq, raising_hash)
            d1['xxx'] = 1
            try:
                x = d1["blabla"]
            except TypeError:
                return 42
            return x
        res = self.interpret(f, [])
        assert res == 42

        def f():
            d1 = r_dict(eq, raising_hash)
            d1['xxx'] = 1
            try:
                d1["blabla"] = 2
            except TypeError:
                return 42
            return 0
        res = self.interpret(f, [])
        assert res == 42

    def test_access_in_try(self):
        h = lambda x: 1
        eq = lambda x, y: x == y

        def f(d):
            try:
                return d[2]
            except ZeroDivisionError:
                return 42
            return -1

        def g(n):
            d = r_dict(eq, h)
            d[1] = n
            d[2] = 2 * n
            return f(d)

        res = self.interpret(g, [3])
        assert res == 6

    def test_access_in_try_set(self):
        h = lambda x: 1
        eq = lambda x, y: x == y
        def f(d):
            try:
                d[2] = 77
            except ZeroDivisionError:
                return 42
            return -1
        def g(n):
            d = r_dict(eq, h)
            d[1] = n
            f(d)
            return d[2]
        res = self.interpret(g, [3])
        assert res == 77

    def test_prepare_dict_update(self):
        def g(n):
            d = {}
            prepare_dict_update(d, n)
            return 42
        res = self.interpret(g, [3])
        assert res == 42     # "did not crash"

    def test_prepare_dict_update_2(self):
        def g(n):
            d = OrderedDict()
            prepare_dict_update(d, n)
            return 42
        res = self.interpret(g, [3])
        assert res == 42     # "did not crash"

    def test_reversed_dict(self):
        d1 = {2: 3, 4: 5, 6: 7}
        def g():
            n1 = 0
            for key in d1:
                n1 = n1 * 10 + key
            n2 = 0
            for key in reversed_dict(d1):
                n2 = n2 * 10 + key
            return n1 * 10000 + n2
        got = str(g())
        assert len(got) == 7 and got[3] == '0' and got[:3] == got[6:3:-1]
        got = str(self.interpret(g, []))
        assert len(got) == 7 and got[3] == '0' and got[:3] == got[6:3:-1]

    def test_compute_hash(self):
        class Foo(object):
            pass
        def f(i):
            assert compute_hash(i) == compute_hash(42)
            assert compute_hash(i + 1.0) == compute_hash(43.0)
            assert compute_hash("Hello" + str(i)) == compute_hash("Hello42")
            if i == 42:
                p = None
            else:
                p = Foo()
            assert compute_hash(p) == compute_hash(None)
            assert (compute_hash(("world", None, i, 7.5)) ==
                    compute_hash(("world", None, 42, 7.5)))
            q = Foo()
            assert compute_hash(q) == compute_identity_hash(q)
            from rpython.rlib.rfloat import INFINITY, NAN
            assert compute_hash(INFINITY) == 314159
            assert compute_hash(-INFINITY) == -271828
            assert compute_hash(NAN) == 0
            return i * 2
        res = self.interpret(f, [42])
        assert res == 84

    def test_isconstant(self):
        @specialize.arg_or_var(0)
        def f(arg):
            if is_annotation_constant(arg):
                return 1
            return 10

        def fn(arg):
            return f(arg) + f(3)

        assert self.interpret(fn, [15]) == 11

    def test_rtype_keepalive(self):
        from rpython.rlib import objectmodel
        def f():
            x = [1]
            y = ['b']
            objectmodel.keepalive_until_here(x, y)
            return 1

        res = self.interpret(f, [])
        assert res == 1

    def test_compute_hash_across_translation(self):
        class Foo(object):
            pass
        q = Foo()

        def f(i):
            assert compute_hash(None) == 0
            assert compute_hash(i) == h_42
            assert compute_hash(i + 1.0) == h_43_dot_0
            assert compute_hash((i + 3) / 6.0) == h_7_dot_5
            assert compute_hash("Hello" + str(i)) == h_Hello42
            if i == 42:
                p = None
            else:
                p = Foo()
            assert compute_hash(p) == h_None
            assert compute_hash(("world", None, i, 7.5)) == h_tuple
            assert compute_hash(q) == h_q
            return i * 2
        h_42 = compute_hash(42)
        h_43_dot_0 = compute_hash(43.0)
        h_7_dot_5 = compute_hash(7.5)
        h_Hello42 = compute_hash("Hello42")
        h_None = compute_hash(None)
        h_tuple = compute_hash(("world", None, 42, 7.5))
        h_q = compute_hash(q)

        res = self.interpret(f, [42])
        assert res == 84

    def test_fetch_translated_config(self):
        assert fetch_translated_config() is None
        def f():
            return fetch_translated_config().translation.continuation
        res = self.interpret(f, [])
        assert res is False


def test_specialize_decorator():
    def f():
        pass

    specialize.memo()(f)

    assert f._annspecialcase_ == 'specialize:memo'

    specialize.arg(0)(f)

    assert f._annspecialcase_ == 'specialize:arg(0)'

    specialize.arg(1)(f)

    assert f._annspecialcase_ == 'specialize:arg(1)'

def test_enforceargs_decorator():
    @enforceargs(int, str, None)
    def f(a, b, c):
        return a, b, c
    f.foo = 'foo'
    assert f._annenforceargs_ == (int, str, None)
    assert f.func_name == 'f'
    assert f.foo == 'foo'
    assert f(1, 'hello', 42) == (1, 'hello', 42)
    exc = py.test.raises(TypeError, "f(1, 2, 3)")
    assert exc.value.message == "f argument 'b' must be of type <type 'str'>"
    py.test.raises(TypeError, "f('hello', 'world', 3)")

def test_always_inline():
    @always_inline
    def f(a, b, c):
        return a, b, c
    assert f._always_inline_ is True

def test_enforceargs_defaults():
    @enforceargs(int, int)
    def f(a, b=40):
        return a + b
    assert f(2) == 42

def test_enforceargs_keywords():
    @enforceargs(b=int)
    def f(a, b, c):
        return a + b
    assert f._annenforceargs_ == (None, int, None)

def test_enforceargs_int_float_promotion():
    @enforceargs(float)
    def f(x):
        return x
    # in RPython there is an implicit int->float promotion
    assert f(42) == 42

def test_enforceargs_None_string():
    @enforceargs(str, unicode)
    def f(a, b):
        return a, b
    assert f(None, None) == (None, None)

def test_enforceargs_complex_types():
    @enforceargs([int], {str: int})
    def f(a, b):
        return a, b
    x = [0, 1, 2]
    y = {'a': 1, 'b': 2}
    assert f(x, y) == (x, y)
    assert f([], {}) == ([], {})
    assert f(None, None) == (None, None)
    py.test.raises(TypeError, "f(['hello'], y)")
    py.test.raises(TypeError, "f(x, {'a': 'hello'})")
    py.test.raises(TypeError, "f(x, {0: 42})")

def test_enforceargs_no_typecheck():
    @enforceargs(int, str, None, typecheck=False)
    def f(a, b, c):
        return a, b, c
    assert f._annenforceargs_ == (int, str, None)
    assert f(1, 2, 3) == (1, 2, 3)  # no typecheck

def test_enforceargs_translates():
    from rpython.rtyper.lltypesystem import lltype
    @enforceargs(int, float)
    def f(a, b):
        return a, b
    graph = getgraph(f, [int, int])
    TYPES = [v.concretetype for v in graph.getargs()]
    assert TYPES == [lltype.Signed, lltype.Float]

def test_enforceargs_not_constant():
    from rpython.translator.translator import TranslationContext, graphof
    @enforceargs(NOT_CONSTANT)
    def f(a):
        return a
    def f42():
        return f(42)
    t = TranslationContext()
    a = t.buildannotator()
    s = a.build_types(f42, [])
    assert not hasattr(s, 'const')


def getgraph(f, argtypes):
    from rpython.translator.translator import TranslationContext, graphof
    from rpython.translator.backendopt.all import backend_optimizations
    t = TranslationContext()
    a = t.buildannotator()
    typer = t.buildrtyper()
    a.build_types(f, argtypes)
    typer.specialize()
    backend_optimizations(t)
    graph = graphof(t, f)
    if option.view:
        graph.show()
    return graph


def test_newlist():
    def f(z):
        x = newlist_hint(sizehint=38)
        if z < 0:
            x.append(1)
        return len(x)

    graph = getgraph(f, [int])
    for llop in graph.startblock.operations:
        if llop.opname == 'malloc_varsize':
            break
    assert llop.args[2].value == 38

def test_newlist_nonconst():
    def f(z):
        x = newlist_hint(sizehint=z)
        return len(x)

    graph = getgraph(f, [int])
    for llop in graph.startblock.operations:
        if llop.opname == 'malloc_varsize':
            break
    assert llop.args[2] is graph.startblock.inputargs[0]

def test_resizelist_hint():
    def f(z):
        x = []
        resizelist_hint(x, 39)
        return len(x)

    graph = getgraph(f, [int])
    for _, op in graph.iterblockops():
        if op.opname == 'direct_call':
            break
    call_name = op.args[0].value._obj.graph.name
    assert call_name.startswith('_ll_list_resize_hint')
    call_arg2 = op.args[2].value
    assert call_arg2 == 39

def test_resizelist_hint_len():
    def f(i):
        l = [44]
        resizelist_hint(l, i)
        return len(l)

    r = interpret(f, [29])
    assert r == 1

def test_iterkeys_with_hash():
    def f(i):
        d = {i + .0: 5, i + .5: 6}
        total = 0
        for k, h in iterkeys_with_hash(d):
            total += k * h
        total -= (i + 0.0) * compute_hash(i + 0.0)
        total -= (i + 0.5) * compute_hash(i + 0.5)
        return total

    assert f(29) == 0.0
    r = interpret(f, [29])
    assert r == 0.0

def test_iteritems_with_hash():
    def f(i):
        d = {i + .0: 5, i + .5: 6}
        total = 0
        for k, v, h in iteritems_with_hash(d):
            total += k * h * v
        total -= (i + 0.0) * compute_hash(i + 0.0) * 5
        total -= (i + 0.5) * compute_hash(i + 0.5) * 6
        return total

    assert f(29) == 0.0
    r = interpret(f, [29])
    assert r == 0.0

def test_contains_with_hash():
    def f(i):
        d = {i + .5: 5}
        assert contains_with_hash(d, i + .5, compute_hash(i + .5))
        assert not contains_with_hash(d, i + .3, compute_hash(i + .3))
        return 0

    f(29)
    interpret(f, [29])

def test_setitem_with_hash():
    def f(i):
        d = {}
        setitem_with_hash(d, i + .5, compute_hash(i + .5), 42)
        setitem_with_hash(d, i + .6, compute_hash(i + .6), -612)
        return d[i + .5]

    assert f(29) == 42
    res = interpret(f, [27])
    assert res == 42

def test_getitem_with_hash():
    def f(i):
        d = {i + .5: 42, i + .6: -612}
        return getitem_with_hash(d, i + .5, compute_hash(i + .5))

    assert f(29) == 42
    res = interpret(f, [27])
    assert res == 42

def test_delitem_with_hash():
    def f(i):
        d = {i + .5: 42, i + .6: -612}
        delitem_with_hash(d, i + .5, compute_hash(i + .5))
        try:
            delitem_with_hash(d, i + .5, compute_hash(i + .5))
        except KeyError:
            pass
        else:
            raise AssertionError
        return 0

    f(29)
    interpret(f, [27])

def test_rdict_with_hash():
    def f(i):
        d = r_dict(strange_key_eq, strange_key_hash)
        h = strange_key_hash("abc")
        assert h == strange_key_hash("aXX") and strange_key_eq("abc", "aXX")
        setitem_with_hash(d, "abc", h, i)
        assert getitem_with_hash(d, "aXX", h) == i
        try:
            getitem_with_hash(d, "bYY", strange_key_hash("bYY"))
        except KeyError:
            pass
        else:
            raise AssertionError
        return 0

    assert f(29) == 0
    interpret(f, [27])

def test_import_from_mixin():
    class M:    # old-style
        def f(self):
            pass
    class A:    # old-style
        import_from_mixin(M)
    assert A.f.im_func is not M.f.im_func

    class M(object):
        def f(self):
            pass
    class A:    # old-style
        import_from_mixin(M)
    assert A.f.im_func is not M.f.im_func

    class M:    # old-style
        def f(self):
            pass
    class A(object):
        import_from_mixin(M)
    assert A.f.im_func is not M.f.im_func

    class M(object):
        def f(self):
            pass
    class A(object):
        import_from_mixin(M)
    assert A.f.im_func is not M.f.im_func

    class MBase(object):
        a = 42
        b = 43
        c = 1000
        def f(self):
            return "hi"
        def g(self):
            return self.c - 1

    class M(MBase):
        a = 84
        def f(self):
            return "there"

    class A(object):
        import_from_mixin(M)
        c = 88

    assert A.f.im_func is not M.f.im_func
    assert A.f.im_func is not MBase.f.im_func
    assert A.g.im_func is not MBase.g.im_func
    assert A().f() == "there"
    assert A.a == 84
    assert A.b == 43
    assert A.c == 88
    assert A().g() == 87

    try:
        class B(object):
            a = 63
            import_from_mixin(M)
    except Exception as e:
        assert ("would overwrite the value already defined locally for 'a'"
                in str(e))
    else:
        raise AssertionError("failed to detect overwritten attribute")

    class M(object):
        def __str__(self):
            return "m!"
    class A(object):
        import_from_mixin(M)
    class B(object):
        import_from_mixin(M, special_methods=['__str__'])
    assert str(A()).startswith('<')
    assert str(B()) == "m!"

    class M(object):
        pass
    class A(object):
        def __init__(self):
            self.foo = 42
    class B(A):
        import_from_mixin(M)
    assert B().foo == 42

    d = dict(__name__='foo')
    exec """class M(object):
                @staticmethod
                def f(): pass
    """ in d
    M = d['M']
    class A(object):
        import_from_mixin(M)
    assert A.f is not M.f
    assert A.f.__module__ != M.f.__module__


def test_import_from_mixin_immutable_fields():
    class A(object):
        _immutable_fields_ = ['a']

    class B(object):
        _immutable_fields_ = ['b']
        import_from_mixin(A)

    assert B._immutable_fields_ == ['b', 'a']
    assert A._immutable_fields_ == ['a']

    class B(object):
        import_from_mixin(A)

    assert B._immutable_fields_ == ['a']

    class C(A):
        _immutable_fields_ = ['c']

    class B(object):
        import_from_mixin(C)

    assert B._immutable_fields_ == ['c', 'a']

    class B(object):
        _immutable_fields_ = ['b']
        import_from_mixin(C)

    assert B._immutable_fields_ == ['b', 'c', 'a']

    class B(object):
        _immutable_fields_ = ['b']

    class BA(B):
        import_from_mixin(C)

    assert BA._immutable_fields_ == ['c', 'a']
