import re
import threading
import unittest

import pytest

import cupy
from cupy import cuda
from cupy import testing


class TestDeviceComparison(unittest.TestCase):

    def check_eq(self, result, obj1, obj2):
        if result:
            assert obj1 == obj2
            assert obj2 == obj1
            assert not (obj1 != obj2)
            assert not (obj2 != obj1)
        else:
            assert obj1 != obj2
            assert obj2 != obj1
            assert not (obj1 == obj2)
            assert not (obj2 == obj1)

    def test_equality(self):
        self.check_eq(True, cuda.Device(0), cuda.Device(0))
        self.check_eq(True, cuda.Device(1), cuda.Device(1))
        self.check_eq(False, cuda.Device(0), cuda.Device(1))
        self.check_eq(False, cuda.Device(0), 0)
        self.check_eq(False, cuda.Device(0), None)
        self.check_eq(False, cuda.Device(0), object())

    def test_lt_device(self):
        assert cuda.Device(0) < cuda.Device(1)
        assert not (cuda.Device(0) < cuda.Device(0))
        assert not (cuda.Device(1) < cuda.Device(0))

    def test_le_device(self):
        assert cuda.Device(0) <= cuda.Device(1)
        assert cuda.Device(0) <= cuda.Device(0)
        assert not (cuda.Device(1) <= cuda.Device(0))

    def test_gt_device(self):
        assert not (cuda.Device(0) > cuda.Device(0))
        assert not (cuda.Device(0) > cuda.Device(0))
        assert cuda.Device(1) > cuda.Device(0)

    def test_ge_device(self):
        assert not (cuda.Device(0) >= cuda.Device(1))
        assert cuda.Device(0) >= cuda.Device(0)
        assert cuda.Device(1) >= cuda.Device(0)

    def check_comparison_other_type(self, obj1, obj2):
        with pytest.raises(TypeError):
            obj1 < obj2
        with pytest.raises(TypeError):
            obj1 <= obj2
        with pytest.raises(TypeError):
            obj1 > obj2
        with pytest.raises(TypeError):
            obj1 >= obj2
        with pytest.raises(TypeError):
            obj2 < obj1
        with pytest.raises(TypeError):
            obj2 <= obj1
        with pytest.raises(TypeError):
            obj2 > obj1
        with pytest.raises(TypeError):
            obj2 >= obj1

    def test_comparison_other_type(self):
        self.check_comparison_other_type(cuda.Device(0), 0)
        self.check_comparison_other_type(cuda.Device(0), 1)
        self.check_comparison_other_type(cuda.Device(1), 0)
        self.check_comparison_other_type(cuda.Device(1), None)
        self.check_comparison_other_type(cuda.Device(1), object())


class TestDeviceAttributes(unittest.TestCase):

    def test_device_attributes(self):
        d = cuda.Device()
        attributes = d.attributes
        assert isinstance(attributes, dict)
        assert all(isinstance(a, int) for a in attributes.values())
        # test a specific attribute that would be present on any supported GPU
        assert 'MaxThreadsPerBlock' in attributes

    def test_device_attributes_error(self):
        with pytest.raises(cuda.runtime.CUDARuntimeError):
            # try to retrieve attributes from a non-existent device
            cuda.device.Device(cuda.runtime.getDeviceCount()).attributes


class TestDevicePCIBusId(unittest.TestCase):
    def test_device_get_pci_bus_id(self):
        d = cuda.Device()
        pci_bus_id = d.pci_bus_id
        assert re.match(
            '^[a-fA-F0-9]{4}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}.[a-fA-F0-9]',
            pci_bus_id
        )

    def test_device_by_pci_bus_id(self):
        d1 = cuda.Device()
        d2 = cuda.Device.from_pci_bus_id(d1.pci_bus_id)
        assert d1 == d2
        d3 = cuda.Device(d2)
        assert d2 == d3

        with pytest.raises(cuda.runtime.CUDARuntimeError) as excinfo:
            cuda.Device.from_pci_bus_id('fake:id')
            assert excinfo == 'cudaErrorInvalidValue: invalid argument'

        with pytest.raises(cuda.runtime.CUDARuntimeError) as excinfo:
            cuda.Device.from_pci_bus_id('FFFF:FF:FF.F')
            assert excinfo == 'cudaErrorInvalidDevice: invalid device ordinal'


class TestDeviceHandles(unittest.TestCase):
    def _check_handle(self, func):
        handles = [func(), None, None]

        def _subthread():
            cupy.cuda.Device().use()
            handles[1] = func()
            handles[2] = func()

        t = threading.Thread(target=_subthread)
        t.start()
        t.join()
        assert handles[0] is not None
        assert handles[0] != handles[1]
        assert handles[1] == handles[2]

    def test_cublas_handle(self):
        self._check_handle(cuda.get_cublas_handle)

    def test_cusolver_handle(self):
        self._check_handle(cuda.device.get_cusolver_handle)

    def test_cusolver_sp_handle(self):
        self._check_handle(cuda.device.get_cublas_handle)

    def test_cusparse_handle(self):
        self._check_handle(cuda.device.get_cusparse_handle)


class TestDeviceFromPointer(unittest.TestCase):
    def test_from_pointer(self):
        assert cuda.device.from_pointer(cupy.empty(1).data.ptr).id == 0


@testing.multi_gpu(2)
class TestDeviceSwitch(unittest.TestCase):

    def setUp(self):
        self._prev_device = cuda.Device()

    def tearDown(self):
        self._prev_device.use()

    def test_use(self):
        dev0 = cuda.Device(0)
        dev1 = cuda.Device(1)

        assert dev1.use() is dev1
        assert 1 == cuda.Device().id
        assert dev0.use() is dev0
        assert 0 == cuda.Device().id

    def test_context(self):
        dev0 = cuda.Device(0)
        dev1 = cuda.Device(1)

        with dev0:
            assert 0 == cuda.Device().id
            with dev1:
                assert 1 == cuda.Device().id
            assert 0 == cuda.Device().id

    def test_context_and_use(self):
        dev0 = cuda.Device(0)
        dev1 = cuda.Device(1)

        dev1.use()
        with dev0:
            assert 0 == cuda.Device().id
            dev1.use()
            with dev1:
                assert 1 == cuda.Device().id
            assert 1 == cuda.Device().id
        assert 1 == cuda.Device().id

    def test_thread_safe(self):
        dev0 = cuda.Device(0)
        dev1 = cuda.Device(1)

        t0_setup = threading.Event()
        t1_setup = threading.Event()
        t0_first_exit = threading.Event()

        t0_exit_device = []
        t1_exit_device = []

        def t0_seq():
            with dev0:
                with dev0:
                    t0_setup.set()
                    t1_setup.wait()
                t0_exit_device.append(cuda.Device().id)
                t0_first_exit.set()

        def t1_seq():
            t0_setup.wait()
            with dev1:
                with dev0:
                    t1_setup.set()
                    t0_first_exit.wait()
                t1_exit_device.append(cuda.Device().id)

        t1 = threading.Thread(target=t1_seq)
        t1.start()
        t0_seq()
        t1.join()
        assert t0_exit_device[0] == 0
        assert t1_exit_device[0] == 1

    def test_invalid(self):
        d = cuda.Device(100)
        with pytest.raises(cuda.runtime.CUDARuntimeError):
            d.use()
        with pytest.raises(cuda.runtime.CUDARuntimeError):
            with d:
                pass
        with cuda.Device(0):
            pass
