import unittest

import numpy
import pytest

import cupy
from cupy import testing
import cupyx
from cupyx import cusolver, lapack


@testing.parameterize(*testing.product({
    'dtype': [numpy.float32, numpy.float64, numpy.complex64, numpy.complex128],
    'n': [3],
    'nrhs': [None, 1, 4],
    'order': ['C', 'F'],
}))
class TestGesv(unittest.TestCase):
    _tol = {'f': 1e-5, 'd': 1e-12}

    def _make_array(self, shape, alpha, beta):
        a = testing.shaped_random(shape, cupy, dtype=self.dtype.char.lower(),
                                  order=self.order, scale=alpha) + beta
        return a

    def _make_matrix(self, shape, alpha, beta):
        a = self._make_array(shape, alpha, beta)
        if self.dtype.char in 'FD':
            a = a + 1j * self._make_array(shape, alpha, beta)
        return a

    def setUp(self):
        self.dtype = numpy.dtype(self.dtype)
        n = self.n
        nrhs = 1 if self.nrhs is None else self.nrhs
        # Diagonally dominant matrix is used as it is stable
        alpha = 2.0 / n
        a = self._make_matrix((n, n), alpha, -alpha / 2)
        diag = cupy.diag(cupy.ones((n,), dtype=self.dtype.char.lower()))
        a[diag > 0] = 0
        a += diag
        x = self._make_matrix((n, nrhs), 0.2, 0.9)
        b = cupy.matmul(a, x)
        b_shape = [n]
        if self.nrhs is not None:
            b_shape.append(nrhs)
        b = b.reshape(b_shape)
        self.a = a
        if self.nrhs is None or self.nrhs == 1:
            self.b = b.copy(order=self.order)
        else:
            self.b = b.copy(order='F')
        self.x_ref = x.reshape(b_shape)
        self.tol = self._tol[self.dtype.char.lower()]

    def test_gesv(self):
        lapack.gesv(self.a, self.b)
        cupy.testing.assert_allclose(self.b, self.x_ref,
                                     rtol=self.tol, atol=self.tol)

    def test_invalid_cases(self):
        if self.nrhs is None or self.nrhs == 1:
            raise unittest.SkipTest()
        ng_a = self.a.reshape(1, self.n, self.n)
        with pytest.raises(ValueError):
            lapack.gesv(ng_a, self.b)
        ng_b = self.b.reshape(1, self.n, self.nrhs)
        with pytest.raises(ValueError):
            lapack.gesv(self.a, ng_b)
        ng_a = cupy.ones((self.n, self.n+1), dtype=self.dtype)
        with pytest.raises(ValueError):
            lapack.gesv(ng_a, self.b)
        ng_a = cupy.ones((self.n+1, self.n+1), dtype=self.dtype)
        with pytest.raises(ValueError):
            lapack.gesv(ng_a, self.b)
        ng_a = cupy.ones(self.a.shape, dtype='i')
        with pytest.raises(TypeError):
            lapack.gesv(ng_a, self.b)
        ng_a = cupy.ones((2, self.n, self.n), dtype=self.dtype, order='F')[0]
        with pytest.raises(ValueError):
            lapack.gesv(ng_a, self.b)
        ng_b = self.b.copy(order='C')
        with pytest.raises(ValueError):
            lapack.gesv(self.a, ng_b)


@testing.parameterize(*testing.product({
    'shape': [(4, 4), (5, 4), (4, 5)],
    'nrhs': [None, 1, 4],
}))
class TestGels(unittest.TestCase):
    _tol = {'f': 1e-5, 'd': 1e-12}

    @testing.for_dtypes('fdFD')
    def test_gels(self, dtype):
        b_shape = [self.shape[0]]
        if self.nrhs is not None:
            b_shape.append(self.nrhs)
        a = testing.shaped_random(self.shape, numpy, dtype=dtype)
        b = testing.shaped_random(b_shape, numpy, dtype=dtype)
        tol = self._tol[numpy.dtype(dtype).char.lower()]
        x_lstsq = numpy.linalg.lstsq(a, b, rcond=None)[0]
        x_gels = lapack.gels(cupy.array(a), cupy.array(b))
        cupy.testing.assert_allclose(x_gels, x_lstsq, rtol=tol, atol=tol)


@testing.parameterize(*testing.product({
    'shape': [(3, 4, 2, 2), (5, 3, 3), (7, 7)],
    'nrhs': [None, 1, 4]
}))
class TestPosv(unittest.TestCase):

    def setUp(self):
        if not cusolver.check_availability('potrsBatched'):
            pytest.skip('potrsBatched is not available')

    @testing.for_dtypes('fdFD')
    @testing.numpy_cupy_allclose(atol=1e-5)
    def test_posv(self, xp, dtype):
        # TODO: cusolver does not support nrhs > 1 for potrsBatched
        if len(self.shape) > 2 and self.nrhs and self.nrhs > 1:
            pytest.skip('cusolver does not support nrhs > 1 for potrsBatched')

        a = self._create_posdef_matrix(xp, self.shape, dtype)
        b_shape = list(self.shape[:-1])
        if self.nrhs is not None:
            b_shape.append(self.nrhs)
        b = testing.shaped_random(b_shape, xp, dtype=dtype)

        if xp == cupy:
            return lapack.posv(a, b)
        else:
            return numpy.linalg.solve(a, b)

    def _create_posdef_matrix(self, xp, shape, dtype):
        n = shape[-1]
        a = testing.shaped_random(shape, xp, dtype, scale=1)
        a = a @ a.swapaxes(-2, -1).conjugate()
        a = a + n * xp.eye(n)
        return a


# TODO: cusolver does not support nrhs > 1 for potrsBatched
@testing.parameterize(*testing.product({
    'shape': [(2, 3, 3)],
    'dtype': [numpy.float32, numpy.float64, numpy.complex64, numpy.complex128],
}))
class TestXFailBatchedPosv(unittest.TestCase):

    def test_posv(self):
        if not cusolver.check_availability('potrsBatched'):
            pytest.skip('potrsBatched is not available')
        a = self._create_posdef_matrix(cupy, self.shape, self.dtype)
        n = a.shape[-1]
        identity_matrix = cupy.eye(n, dtype=a.dtype)
        b = cupy.empty(a.shape, a.dtype)
        b[...] = identity_matrix
        with cupyx.errstate(linalg='ignore'):
            with pytest.raises(cupy.cuda.cusolver.CUSOLVERError):
                lapack.posv(a, b)

    def _create_posdef_matrix(self, xp, shape, dtype):
        n = shape[-1]
        a = testing.shaped_random(shape, xp, dtype, scale=1)
        a = a @ a.swapaxes(-2, -1).conjugate()
        a = a + n * xp.eye(n)
        return a
