import ctypes

import py

from rpython.rlib.rfloat import NAN, INFINITY
from rpython.rlib.entrypoint import entrypoint
from rpython.rlib.unroll import unrolling_iterable
from rpython.rlib.rarithmetic import r_longlong, r_ulonglong, r_uint, intmask
from rpython.rlib.objectmodel import specialize
from rpython.rtyper.lltypesystem import lltype
from rpython.rtyper.lltypesystem.lltype import *
from rpython.rtyper.lltypesystem.rstr import STR
from rpython.tool.nullpath import NullPyPathLocal
from rpython.translator.c import genc
from rpython.translator.interactive import Translation
from rpython.translator.translator import TranslationContext, graphof

signed_ffffffff = r_longlong(0xffffffff)
unsigned_ffffffff = r_ulonglong(0xffffffff)

def llrepr_in(v):
    if r_uint is not r_ulonglong and isinstance(v, r_ulonglong):
        return "%d:%d" % (intmask(v >> 32), intmask(v & unsigned_ffffffff))
    elif isinstance(v, r_longlong):
        return "%d:%d" % (intmask(v >> 32), intmask(v & signed_ffffffff))
    elif isinstance(v, float):
        return repr(v)    # extra precision than str(v)
    elif isinstance(v, str):
        if v.isalnum():
            return v
        else:   # escape the string
            return '/' + ','.join([str(ord(c)) for c in v])
    return str(v)

@specialize.argtype(0)
def llrepr_out(v):
    if isinstance(v, float):
        from rpython.rlib.rfloat import formatd, DTSF_ADD_DOT_0
        return formatd(v, 'r', 0, DTSF_ADD_DOT_0)
    return str(v)   # always return a string, to get consistent types

def parse_longlong(a):
    p0, p1 = a.split(":")
    return (r_longlong(int(p0)) << 32) + (r_longlong(int(p1)) &
                                          signed_ffffffff)

def parse_ulonglong(a):
    p0, p1 = a.split(":")
    return (r_ulonglong(int(p0)) << 32) + (r_ulonglong(int(p1)) &
                                           unsigned_ffffffff)

def compile(fn, argtypes, view=False, gcpolicy="none", backendopt=True,
            annotatorpolicy=None, thread=False, **kwds):
    argtypes_unroll = unrolling_iterable(enumerate(argtypes))

    for argtype in argtypes:
        if argtype not in [int, float, str, bool, r_ulonglong, r_longlong,
                           r_uint]:
            raise Exception("Unsupported argtype, %r" % (argtype,))

    def entry_point(argv):
        args = ()
        for i, argtype in argtypes_unroll:
            a = argv[i + 1]
            if argtype is int:
                args += (int(a),)
            elif argtype is r_uint:
                args += (r_uint(int(a)),)
            elif argtype is r_longlong:
                args += (parse_longlong(a),)
            elif argtype is r_ulonglong:
                args += (parse_ulonglong(a),)
            elif argtype is bool:
                if a == 'True':
                    args += (True,)
                else:
                    assert a == 'False'
                    args += (False,)
            elif argtype is float:
                if a == 'inf':
                    args += (INFINITY,)
                elif a == '-inf':
                    args += (-INFINITY,)
                elif a == 'nan':
                    args += (NAN,)
                else:
                    args += (float(a),)
            else:
                if a.startswith('/'):     # escaped string
                    if len(a) == 1:
                        a = ''
                    else:
                        l = a[1:].split(',')
                        a = ''.join([chr(int(x)) for x in l])
                args += (a,)
        res = fn(*args)
        print "THE RESULT IS:", llrepr_out(res), ";"
        return 0

    t = Translation(entry_point, None, gc=gcpolicy, backend="c",
                    policy=annotatorpolicy, thread=thread, **kwds)
    if not backendopt:
        t.disable(["backendopt_lltype"])
    t.driver.config.translation.countmallocs = True
    t.annotate()
    try:
        if py.test.config.option.view:
            t.view()
    except AttributeError:
        pass
    t.rtype()
    if backendopt:
        t.backendopt()
    try:
        if py.test.config.option.view:
            t.view()
    except AttributeError:
        pass
    t.compile_c()
    ll_res = graphof(t.context, fn).getreturnvar().concretetype

    def output(stdout):
        for line in stdout.splitlines(False):
            if len(repr(line)) == len(line) + 2:   # no escaped char
                print line
            else:
                print 'REPR:', repr(line)

    def f(*args, **kwds):
        expected_extra_mallocs = kwds.pop('expected_extra_mallocs', 0)
        expected_exception_name = kwds.pop('expected_exception_name', None)
        assert not kwds
        assert len(args) == len(argtypes)
        for arg, argtype in zip(args, argtypes):
            assert isinstance(arg, argtype)

        stdout = t.driver.cbuilder.cmdexec(
            " ".join([llrepr_in(arg) for arg in args]),
            expect_crash=(expected_exception_name is not None))
        #
        if expected_exception_name is not None:
            stdout, stderr = stdout
            print '--- stdout ---'
            output(stdout)
            print '--- stderr ---'
            output(stderr)
            print '--------------'
            stderr, prevline, lastline, empty = stderr.rsplit('\n', 3)
            assert empty == ''
            expected = 'Fatal RPython error: ' + expected_exception_name
            assert lastline == expected or prevline == expected
            return None

        output(stdout)
        stdout, lastline, empty = stdout.rsplit('\n', 2)
        assert empty == ''
        assert lastline.startswith('MALLOC COUNTERS: ')
        mallocs, frees = map(int, lastline.split()[2:])
        assert stdout.endswith(' ;')
        pos = stdout.rindex('THE RESULT IS: ')
        res = stdout[pos + len('THE RESULT IS: '):-2]
        #
        if isinstance(expected_extra_mallocs, int):
            assert mallocs - frees == expected_extra_mallocs
        else:
            assert mallocs - frees in expected_extra_mallocs
        #
        if ll_res in [lltype.Signed, lltype.Unsigned, lltype.SignedLongLong,
                      lltype.UnsignedLongLong]:
            return int(res)
        elif ll_res == lltype.Bool:
            return bool(int(res))
        elif ll_res == lltype.Char:
            assert len(res) == 1
            return res
        elif ll_res == lltype.Float:
            return float(res)
        elif ll_res == lltype.Ptr(STR):
            return res
        elif ll_res == lltype.Void:
            return None
        raise NotImplementedError("parsing %s" % (ll_res,))

    class CompilationResult(object):
        def __repr__(self):
            return 'CompilationResult(%s)' % (fn.__name__,)
        def __call__(self, *args, **kwds):
            return f(*args, **kwds)

    cr = CompilationResult()
    cr.t = t
    cr.builder = t.driver.cbuilder
    return cr


def test_simple():
    def f(x):
        return x * 2

    f1 = compile(f, [int])

    assert f1(5) == 10
    assert f1(-123) == -246

    py.test.raises(Exception, f1, "world")  # check that it's really typed


def test_int_becomes_float():
    # used to crash "very often": the long chain of mangle() calls end
    # up converting the return value of f() from an int to a float, but
    # if blocks are followed in random order by the annotator, it will
    # very likely first follow the call to llrepr_out() done after the
    # call to f(), getting an int first (and a float only later).
    @specialize.arg(1)
    def mangle(x, chain):
        if chain:
            return mangle(x, chain[1:])
        return x - 0.5
    def f(x):
        if x > 10:
            x = mangle(x, (1,1,1,1,1,1,1,1,1,1))
        return x + 1

    f1 = compile(f, [int])

    assert f1(5) == 6
    assert f1(12) == 12.5


def test_string_arg():
    def f(s):
        total = 0
        for c in s:
            total += ord(c)
        return total + len(s)

    f1 = compile(f, [str])

    for check in ['x', '', '\x00', '\x01', '\n', '\x7f', '\xff',
                  '\x00\x00', '\x00\x01']:
        assert f1(check) == len(check) + sum(map(ord, check))


def test_dont_write_source_files():
    def f(x):
        return x*2
    t = TranslationContext()
    t.buildannotator().build_types(f, [int])
    t.buildrtyper().specialize()

    t.config.translation.countmallocs = True
    t.config.translation.dont_write_c_files = True
    builder = genc.CStandaloneBuilder(t, f, config=t.config)
    builder.generate_source()
    assert isinstance(builder.targetdir, NullPyPathLocal)
    for f in builder.targetdir.listdir():
        assert not str(f).endswith('.c')


def test_rlist():
    def f(x):
        l = [x]
        l.append(x+1)
        return l[0] * l[-1]
    f1 = compile(f, [int])
    assert f1(5) == 30
    #assert f1(x=5) == 30


def test_rptr():
    S = GcStruct('testing', ('x', Signed), ('y', Signed))
    def f(i):
        if i < 0:
            p = nullptr(S)
        else:
            p = malloc(S)
            p.x = i*2
        if i > 0:
            return p.x
        else:
            return -42
    f1 = compile(f, [int])
    assert f1(5) == 10
    #assert f1(i=5) == 10
    assert f1(1) == 2
    assert f1(0) == -42
    assert f1(-1) == -42
    assert f1(-5) == -42


def test_empty_string():
    A = Array(Char, hints={'nolength': True})
    p = malloc(A, 1, immortal=True)
    def f():
        return p[0]
    f1 = compile(f, [])
    assert f1() == '\x00'


def test_rstr():
    def fn(i):
        return "hello"[i]
    f1 = compile(fn, [int])
    res = f1(1)
    assert res == 'e'


def test_recursive_struct():
    # B has an A as its super field, and A has a pointer to B.
    class A:
        pass
    class B(A):
        pass
    def fn(i):
        a = A()
        b = B()
        a.b = b
        b.i = i
        return a.b.i
    f1 = compile(fn, [int])
    res = f1(42)
    assert res == 42

def test_recursive_struct_2():
    class L:
        def __init__(self, target):
            self.target = target
    class RL(L):
        pass
    class SL(L):
        pass
    class B:
        def __init__(self, exits):
            self.exits = exits
    def fn(i):
        rl = RL(None)
        b = B([rl])
        sl = SL(b)
    f1 = compile(fn, [int])
    f1(42)

def test_infinite_float():
    x = 1.0
    while x != x / 2:
        x *= 3.1416
    def fn():
        return x
    f1 = compile(fn, [])
    res = f1()
    assert res > 0 and res == res / 2
    def fn():
        return -x
    f1 = compile(fn, [])
    res = f1()
    assert res < 0 and res == res / 2
    class Box:

        def __init__(self, d):
            self.d = d
    b1 = Box(x)
    b2 = Box(-x)
    b3 = Box(1.5)

    def f(i):
        if i==0:
            b = b1
        elif i==1:
            b = b2
        else:
            b = b3
        return b.d

    f1 = compile(f, [int])
    res = f1(0)
    assert res > 0 and res == res / 2
    res = f1(1)
    assert res < 0 and res == res / 2
    res = f1(3)
    assert res == 1.5

def test_infinite_float_in_array():
    from rpython.rlib.rfloat import INFINITY, NAN, isnan
    lst = [INFINITY, -INFINITY, NAN]
    def fn(i):
        return lst[i]
    f1 = compile(fn, [int])
    res = f1(0)
    assert res == INFINITY
    res = f1(1)
    assert res == -INFINITY
    res = f1(2)
    assert isnan(res)

def test_nan_and_special_values():
    from rpython.rlib.rfloat import isnan, isinf, isfinite, copysign
    inf = 1e300 * 1e300
    assert isinf(inf)
    nan = inf/inf
    assert isnan(nan)

    for value, checker in [
            (inf,   lambda x: isinf(x) and x > 0.0),
            (-inf,  lambda x: isinf(x) and x < 0.0),
            (nan,   isnan),
            (42.0,  isfinite),
            (0.0,   lambda x: not x and copysign(1., x) == 1.),
            (-0.0,  lambda x: not x and copysign(1., x) == -1.),
            ]:
        def f():
            return value
        f1 = compile(f, [])
        res = f1()
        assert checker(res)

        l = [value]
        def g(x):
            return l[x]
        g2 = compile(g, [int])
        res = g2(0)
        assert checker(res)

        l2 = [(-value, -value), (value, value)]
        def h(x):
            return l2[x][1]
        h3 = compile(h, [int])
        res = h3(1)
        assert checker(res)

def test_prebuilt_instance_with_dict():
    class A:
        pass
    a = A()
    a.d = {}
    a.d['hey'] = 42
    def t():
        a.d['hey'] = 2
        return a.d['hey']
    f = compile(t, [])
    assert f() == 2

def test_long_strings():
    s1 = 'hello'
    s2 = ''.join([chr(i) for i in range(256)])
    s3 = 'abcd'*17
    s4 = open(__file__, 'rb').read(2049)
    choices = [s1, s2, s3, s4]
    def f(i, j):
        return choices[i][j]
    f1 = compile(f, [int, int])
    for i, s in enumerate(choices):
        j = 0
        while j < len(s):
            c = s[j]
            assert f1(i, j) == c
            j += 1
            if j > 100:
                j += 10

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

    f1 = compile(f, [])
    assert f1() == 1

def test_print():
    def f():
        for i in range(10):
            print "xxx"

    fn = compile(f, [])
    fn()

def test_name():
    def f():
        return 3

    f.c_name = 'pypy_xyz_f'

    t = Translation(f, [], backend="c")
    t.annotate()
    t.compile_c()
    if py.test.config.option.view:
        t.view()
    assert hasattr(ctypes.CDLL(str(t.driver.c_entryp)), 'pypy_xyz_f')

def test_entrypoints():
    def f():
        return 3

    key = "test_entrypoints42"
    @entrypoint(key, [int], "foobar")
    def g(x):
        return x + 42

    t = Translation(f, [], backend="c", secondaryentrypoints="test_entrypoints42")
    t.annotate()
    t.compile_c()
    if py.test.config.option.view:
        t.view()
    assert hasattr(ctypes.CDLL(str(t.driver.c_entryp)), 'foobar')

def test_exportstruct():
    from rpython.translator.tool.cbuild import ExternalCompilationInfo
    from rpython.rlib.exports import export_struct
    def f():
        return 42
    FOO = Struct("FOO", ("field1", Signed))
    foo = malloc(FOO, flavor="raw")
    foo.field1 = 43
    # maybe export_struct should add the struct name to eci automatically?
    # https://bugs.pypy.org/issue1361
    foo._obj._compilation_info = ExternalCompilationInfo(export_symbols=['BarStruct'])
    export_struct("BarStruct", foo._obj)
    t = Translation(f, [], backend="c")
    t.annotate()
    t.compile_c()
    if py.test.config.option.view:
        t.view()
    assert hasattr(ctypes.CDLL(str(t.driver.c_entryp)), 'BarStruct')
    free(foo, flavor="raw")

def test_recursive_llhelper():
    from rpython.rtyper.annlowlevel import llhelper
    from rpython.rtyper.lltypesystem import lltype
    from rpython.rlib.objectmodel import specialize
    FT = lltype.ForwardReference()
    FTPTR = lltype.Ptr(FT)
    STRUCT = lltype.Struct("foo", ("bar", FTPTR))
    FT.become(lltype.FuncType([lltype.Ptr(STRUCT)], lltype.Signed))

    class A:
        def __init__(self, func, name):
            self.func = func
            self.name = name
        def _freeze_(self):
            return True
        @specialize.memo()
        def make_func(self):
            f = getattr(self, "_f", None)
            if f is not None:
                return f
            f = lambda *args: self.func(*args)
            f.c_name = self.name
            f.relax_sig_check = True
            f.__name__ = "WRAP%s" % (self.name, )
            self._f = f
            return f
        def get_llhelper(self):
            return llhelper(FTPTR, self.make_func())
    def f(s):
        if s.bar == t.bar:
            lltype.free(s, flavor="raw")
            return 1
        lltype.free(s, flavor="raw")
        return 0
    def g(x):
        return 42
    def chooser(x):
        s = lltype.malloc(STRUCT, flavor="raw")
        if x:
            s.bar = llhelper(FTPTR, a_f.make_func())
        else:
            s.bar = llhelper(FTPTR, a_g.make_func())
        return f(s)
    a_f = A(f, "f")
    a_g = A(g, "g")
    t = lltype.malloc(STRUCT, flavor="raw", immortal=True)
    t.bar = llhelper(FTPTR, a_f.make_func())
    fn = compile(chooser, [bool])
    assert fn(True)

def test_ordered_dict():
    try:
        from collections import OrderedDict
    except ImportError:
        py.test.skip("Please update to Python 2.7")

    expected = [('ea', 1), ('bb', 2), ('c', 3), ('d', 4), ('e', 5),
                ('ef', 6)]
    d = OrderedDict(expected)

    def f():
        assert d.items() == expected

    fn = compile(f, [])
    fn()

def test_inhibit_tail_call():
    def foobar_fn(n):
        return 42
    foobar_fn._dont_inline_ = True
    def main(n):
        return foobar_fn(n)
    #
    t = Translation(main, [int], backend="c")
    t.rtype()
    t.context._graphof(foobar_fn).inhibit_tail_call = True
    t.source_c()
    lines = t.driver.cbuilder.c_source_filename.join('..',
                              'rpython_translator_c_test_test_genc.c').readlines()
    for i, line in enumerate(lines):
        if '= pypy_g_foobar_fn' in line:
            break
    else:
        assert 0, "the call was not found in the C source"
    assert 'PYPY_INHIBIT_TAIL_CALL();' in lines[i+1]
