import ctypes
from java import (cast, jarray, jboolean, jbyte, jchar, jclass, jdouble, jfloat, jint,
                  jlong, jshort)
from java.lang import String

from .test_utils import FilterWarningsCase


class TestArray(FilterWarningsCase):

    def test_basic(self):
        array_C = jarray(jchar)("hello")
        self.assertEqual(5, len(array_C))
        self.assertEqual(repr(array_C), "jarray('C')(['h', 'e', 'l', 'l', 'o'])")

        self.assertTrue(isinstance(array_C, jclass("java.lang.Object")))
        self.assertTrue(isinstance(array_C, jclass("java.lang.Cloneable")))
        self.assertTrue(isinstance(array_C, jclass("java.io.Serializable")))
        self.assertFalse(isinstance(array_C, jclass("java.io.Closeable")))
        self.assertRegexpMatches(array_C.toString(), r"^\[C")

    def test_type_klass(self):
        self.assertIs(jarray(String), jarray(String.getClass()))

    def test_type_jni(self):
        self.assertIs(jarray(String), jarray("Ljava/lang/String;"))

        with self.assertRaisesRegex(jclass("java.lang.NoClassDefFoundError"),
                                    "java.lang.Nonexistent"):
            jarray("Ljava/lang/Nonexistent;")

        with self.assertRaisesRegex(ValueError, "Invalid JNI signature: 'java.lang.String'"):
            jarray("java.lang.String")

    def test_type_invalid(self):
        for element_type in [0, True, None, int, str]:
            with self.subTest(element_type=element_type):
                with self.assertRaisesRegex(TypeError, f"{element_type!r} is not a Java type"):
                    jarray(element_type)

    def test_blank(self):
        array_Z = jarray(jboolean)(2)
        self.assertEqual(2, len(array_Z))
        self.assertEqual([False, False], array_Z)

        array_C = jarray(jchar)(3)
        self.assertEqual(3, len(array_C))
        self.assertEqual(["\u0000", "\u0000", "\u0000"], array_C)

        array_I = jarray(jint)(2)
        self.assertEqual(2, len(array_I))
        self.assertEqual([0, 0], array_I)

        from java.lang import Object
        array_Object = jarray(Object)(3)
        self.assertEqual(3, len(array_Object))
        self.assertEqual([None, None, None], array_Object)

        array_empty = jarray(Object)(0)
        self.assertEqual(0, len(array_empty))
        self.assertEqual([], array_empty)

    # More conversion tests in test_conversion.py
    def test_conversion(self):
        Object = jclass("java.lang.Object")
        Integer = jclass("java.lang.Integer")
        TestArray = jclass('com.chaquo.python.TestArray')
        # All object arrays, primitive arrays, and Python iterables are assignable to Object,
        # Cloneable and Serializable
        for array in [jarray(Object)(["hello", 42]), jarray(Integer)([11, 22]),
                      jarray(jboolean)([False, True]), [False, True]]:
            for field in ["object", "cloneable", "serializable"]:
                setattr(TestArray, field, array)
                self.assertEqual(array, getattr(TestArray, field))
                with self.assertRaisesRegexp(TypeError, "Cannot convert"):
                    setattr(TestArray, "closeable", array)

    def test_cast(self):
        Object = jclass("java.lang.Object")
        Boolean = jclass("java.lang.Boolean")

        Boolean_array = jarray(Boolean)([True, False])
        Boolean_array_Object_array = cast(jarray(Object), Boolean_array)
        self.assertIsNot(Boolean_array, Boolean_array_Object_array)
        self.assertEqual(Boolean_array, Boolean_array_Object_array)
        self.assertIs(Boolean_array_Object_array, cast(jarray(Object), Boolean_array))
        self.assertIs(Boolean_array, cast(jarray(Boolean), Boolean_array_Object_array))

        Boolean_array_Object = cast(Object, Boolean_array)
        self.assertIsNot(Boolean_array, Boolean_array_Object)
        self.assertEqual(Boolean_array, Boolean_array_Object)
        self.assertIs(Boolean_array_Object, cast(Object, Boolean_array))
        self.assertIs(Boolean_array, cast(jarray(Boolean), Boolean_array_Object))

        with self.assertRaisesRegexp(TypeError, r"cannot create boolean\[\] proxy from "
                                     r"java.lang.Boolean\[\] instance"):
            cast(jarray(jboolean), Boolean_array)

        with self.assertRaisesRegexp(TypeError, r"cannot create java.lang.Object\[\] proxy from "
                                     "java.lang.Object instance"):
            cast(jarray(Object), Object())

        Object_array = jarray(Object)([])
        with self.assertRaisesRegexp(TypeError, r"cannot create java.lang.Boolean proxy from "
                                     r"java.lang.Object\[\] instance"):
            cast(Boolean, Object_array)
        with self.assertRaisesRegexp(TypeError, r"cannot create java.lang.Boolean\[\] proxy from "
                                     r"java.lang.Object\[\] instance"):
            cast(jarray(Boolean), Object_array)

        Z_array = jarray(jboolean)([True, False])
        with self.assertRaisesRegexp(TypeError, r"cannot create java.lang.Boolean\[\] proxy from "
                                     r"boolean\[\] instance"):
            cast(jarray(Boolean), Z_array)
        with self.assertRaisesRegexp(TypeError, r"cannot create java.lang.Object\[\] proxy from "
                                     r"boolean\[\] instance"):
            cast(jarray(Object), Z_array)

    def test_output_arg(self):
        string = String('\u1156\u2278\u3390\u44AB')
        for btarray in ([0] * 4,
                        (0,) * 4,
                        jarray(jbyte)([0] * 4)):
            # This version of getBytes returns the 8 low-order of each Unicode character.
            string.getBytes(0, 4, btarray, 0)
            if not isinstance(btarray, tuple):
                self.assertEquals(btarray, [ctypes.c_int8(x).value
                                            for x in [0x56, 0x78, 0x90, 0xAB]])

    def test_multiple_dimensions(self):
        Arrays = jclass('com.chaquo.python.TestArray')
        matrix = [[1, 2, 3],
                  [4, 5, 6],
                  [7, 8, 9]]
        self.assertEquals(Arrays.methodParamsMatrixI(matrix), True)
        self.assertEquals(Arrays.methodReturnMatrixI(), matrix)

    # Most of the positive tests are in test_conversion, but here are some error tests.
    def test_modify(self):
        Object = jclass("java.lang.Object")
        array_Z = jarray(jboolean)([True, False])
        with self.assertRaisesRegexp(TypeError, "Cannot convert int object to boolean"):
            array_Z[0] = 1
        with self.assertRaisesRegexp(TypeError, "Cannot convert Object object to boolean"):
            array_Z[0] = Object()

        Boolean = jclass("java.lang.Boolean")
        array_Boolean = jarray(Boolean)([True, False])
        with self.assertRaisesRegexp(TypeError, "Cannot convert int object to java.lang.Boolean"):
            array_Boolean[0] = 1
        with self.assertRaisesRegexp(TypeError, "Cannot convert int object to java.lang.Boolean"):
            cast(jarray(Object), array_Boolean)[0] = 1

        array_Object = jarray(Object)([True, False])
        array_Object[0] = 1
        self.assertEqual([1, False], array_Object)

    def test_str_repr(self):
        Object = jclass("java.lang.Object")
        for func in [str, repr]:
            self.assertEqual("cast('[Z', None)", func(cast(jarray(jboolean), None)))
            self.assertEqual("cast('[[Z', None)", func(cast(jarray(jarray(jboolean)), None)))
            self.assertEqual("cast('[Ljava/lang/Object;', None)",
                             func(cast(jarray(Object), None)))
            self.assertEqual("cast('[[Ljava/lang/Object;', None)",
                             func(cast(jarray(jarray(Object)), None)))

            self.assertEqual("jarray('Z')([])", func(jarray(jboolean)([])))
            self.assertEqual("jarray('Ljava/lang/Object;')([])", func(jarray(Object)([])))

            self.assertEqual("jarray('Z')([True])", func(jarray(jboolean)([True])))
            self.assertEqual("jarray('Z')([True])", func(jarray(jboolean)((True,))))
            self.assertEqual("jarray('Z')([True, False])", func(jarray(jboolean)([True, False])))
            self.assertEqual("jarray('[Z')([[True], [False, True]])",
                             func(jarray(jarray(jboolean))([[True], [False, True]])))

    def test_bytes(self):
        # Java byte arrays (signed) and Python bytes (unsigned) can be converted both ways.
        self.verify_bytes([], b"")
        self.verify_bytes([-128, -127, -2, -1, 0, 1, 126, 127],
                          b"\x80\x81\xFE\xFF\x00\x01\x7E\x7F")

        # Java primitive arrays (except char[]) can be converted to their raw data bytes.
        # Expected values are little-endian.
        for element_type, values, expected in \
            [(jboolean, [False, True], b"\x00\x01"),
             (jbyte, [0, 123], bytes(1) + b"\x7b"),
             (jshort, [0, 12345], bytes(2) + b"\x39\x30"),
             (jint, [0, 123456789], bytes(4) + b"\x15\xcd\x5b\x07"),
             (jlong, [0, 1234567890123456789], bytes(8) + b"\x15\x81\xe9\x7d\xf4\x10\x22\x11"),
             (jfloat, [0, 1.99], bytes(4) + b"\x52\xb8\xfe\x3f"),
             (jdouble, [0, 1.99], bytes(8) + b"\xd7\xa3\x70\x3d\x0a\xd7\xff\x3f")]:
            java_type = jarray(element_type)
            for python_type in [bytes, bytearray]:
                with self.subTest(element_type=element_type, python_type=python_type):
                    self.assertEqual(b"", python_type(java_type([])))
                    self.assertEqual(expected, python_type(java_type(values)))

        # Integer arrays containing values 0 to 255 can be converted element-wise using `list`.
        for element_type in [jshort, jint, jlong]:
            with self.subTest(element_type=element_type):
                self.assertEqual(
                    b"\x00\x01\x7E\x7F\x80\x81\xFE\xFF",
                    bytes(list(jarray(element_type)([0, 1, 126, 127, 128, 129, 254, 255]))))

        # Other array types (including multi-dimensional arrays) will be treated as an iterable
        # of integers, so converting them to bytes will fail unless the array is empty.
        for element_type, values in \
            [(jchar, "hello"),
             (jarray(jbyte), [b"hello", b"world"]),
             (String, ["hello"])]:
            java_type = jarray(element_type)
            for python_type in [bytes, bytearray]:
                with self.subTest(element_type=element_type, python_type=python_type):
                    self.assertEqual(b"", python_type(java_type([])))
                    with self.assertRaisesRegex(TypeError, "cannot be interpreted as an integer"):
                        python_type(java_type(values))

    def verify_bytes(self, jbyte_values, b):
        arrayB_from_list = jarray(jbyte)(jbyte_values)
        self.assertEqual(jbyte_values, arrayB_from_list)

        array_bytes = bytes(arrayB_from_list)
        self.assertEqual(b, array_bytes)
        array_bytearray = bytearray(arrayB_from_list)
        self.assertEqual(b, array_bytearray)

        arrayB_from_bytes = jarray(jbyte)(b)
        self.assertEqual(jbyte_values, arrayB_from_bytes)
        arrayB_from_bytearray = jarray(jbyte)(bytearray(b))
        self.assertEqual(jbyte_values, arrayB_from_bytearray)

    BUFFER_TESTS = [
        (jboolean, "?", 1, [True, False]),
        (jbyte, "b", 1, [-100, 0, 100]),
        (jshort, "h", 2, [-10_000, 0, 10_000]),
        (jint, "i", 4, [-1_000_000_000, 0, 1_000_000_000]),
        (jlong, "q", 8, [-1_000_000_000_000, 0, 1_000_000_000_000]),
        (jfloat, "f", 4, [-float("inf"), -1.5, 0, 1.5, float("inf")]),
        (jdouble, "d", 8, [-float("inf"), -1e300, 0, 1e300, float("inf")])]

    def test_buffer_j2p(self):
        for element_type, format, itemsize, values in self.BUFFER_TESTS:
            for input in [[], values]:
                with self.subTest(element_type=element_type, input=input):
                    ja = jarray(element_type)(input)
                    m = memoryview(ja)
                    self.assertEqual(input, m.tolist())
                    self.assertEqual(len(input) * itemsize, m.nbytes)
                    self.assertFalse(m.readonly)
                    self.assertEqual(format, m.format)
                    self.assertEqual(itemsize, m.itemsize)
                    self.assertEqual(1, m.ndim)
                    self.assertEqual((len(input),), m.shape)
                    self.assertEqual((itemsize,), m.strides)
                    self.assertEqual((), m.suboffsets)
                    self.assertTrue(m.c_contiguous)

                    # Because the JVM may return a copy, we can't guarantee that updates to the
                    # view will be written to the array until the view is released, and we
                    # can't guarantee that updates to the array are ever visible to the view.
                    if ja:
                        self.assertNotEqual(ja[0], ja[1])
                        m[0] = m[1]
                        self.assertEqual(m[0], m[1])
                        m.release()
                        self.assertEqual(ja[0], ja[1])

        for element_type in [jchar, jarray(jbyte), String]:
            with self.assertRaisesRegex(TypeError, "a bytes-like object is required"):
                memoryview(jarray(element_type)([]))

    def test_buffer_p2j(self):
        import array
        sizeof_long = array.array("l").itemsize  # May be 4 or 8.

        for element_type, format, itemsize, values in self.BUFFER_TESTS:
            if element_type is jboolean:
                # array.array doesn't support this format, but it's covered by the NumPy tests.
                continue
            for input in [[], values]:
                with self.subTest(element_type=element_type, input=input):
                    self.check_buffer_p2j(element_type, format, input)
                    if (format in "iq") and (itemsize == sizeof_long):
                        self.check_buffer_p2j(element_type, "l", input)

    def check_buffer_p2j(self, element_type, format, input):
        import array
        aa = array.array(format, input)
        ja = jarray(element_type)(aa)
        self.assertEqual(ja, aa)

    def test_abc(self):
        from collections import abc
        for element_type_1d in [jboolean, jbyte, jshort, jint, jlong, jfloat, jdouble, jchar,
                                String]:
            for element_type in [element_type_1d, jarray(element_type_1d)]:
                with self.subTest(element_type=element_type):
                    cls = jarray(element_type)
                    self.assertTrue(issubclass(cls, abc.MutableSequence))
                    self.assertTrue(isinstance(cls([]), abc.MutableSequence))

    def test_truth(self):
        self.assertFalse(jarray(jboolean)([]))
        self.assertTrue(jarray(jboolean)([False]))
        self.assertTrue(jarray(jboolean)([True]))

    def test_eq(self):
        tf = jarray(jboolean)([True, False])
        self.verify_equal(tf, tf)
        self.verify_equal(tf, jarray(jboolean)([True, False]))
        self.verify_equal(tf, [True, False])
        self.verify_equal(tf, [1, 0])

        self.verify_not_equal(tf, [True, True])
        self.verify_not_equal(tf, [True, False, True])

        single = jarray(jboolean)([True])
        self.verify_not_equal(single, True)

        empty = jarray(jboolean)([])
        self.verify_equal(empty, empty)
        self.verify_equal(empty, [])
        self.verify_not_equal(empty, single)
        self.verify_not_equal(empty, False)

    def verify_equal(self, a, b):
        self.assertEqual(a, b)
        self.assertEqual(b, a)
        self.assertFalse(a != b)
        self.assertFalse(b != a)

    def verify_not_equal(self, a, b):
        self.assertNotEqual(a, b)
        self.assertNotEqual(b, a)
        self.assertFalse(a == b)
        self.assertFalse(b == a)

    def test_hash(self):
        with self.assertRaisesRegexp(TypeError, "unhashable type"):
            hash(jarray(jboolean)([]))

    def test_add(self):
        tf = jarray(jboolean)([True, False])
        self.assertEqual(tf, tf + [])
        self.assertEqual(tf, [] + tf)
        self.assertEqual(tf, tf + jarray(jboolean)([]))
        self.assertEqual([True, False, True, False], tf + tf)
        self.assertEqual([True, False, True], tf + [True])
        self.assertEqual([True, True, False], [True] + tf)
        with self.assertRaises(TypeError):
            tf + True
        with self.assertRaises(TypeError):
            tf + None

        hw = jarray(String)(["hello", "world"])
        self.assertEqual([True, False, "hello", "world"], tf + hw)

    def test_iter(self):
        a = jarray(jint)([1, 2, 3])
        self.assertEqual([1, 2, 3], [x for x in a])

    def test_in(self):
        a = jarray(jint)([1, 2])
        self.assertTrue(1 in a)
        self.assertTrue(2 in a)
        self.assertFalse(3 in a)

    def test_attributes(self):
        a = jarray(jint)([1, 2, 3])
        with self.assertRaises(AttributeError):
            a.length
        with self.assertRaises(AttributeError):
            a.foo = 99
