import contextlib
import pytest
import numpy as np
from numpy.testing import assert_allclose, assert_equal

from scipy.sparse import csr_array, dok_array, SparseEfficiencyWarning
from .test_arithmetic1d import toarray


formats_for_index1d = [csr_array, dok_array]


@contextlib.contextmanager
def check_remains_sorted(X):
    """Checks that sorted indices property is retained through an operation"""
    yield
    if not hasattr(X, 'has_sorted_indices') or not X.has_sorted_indices:
        return
    indices = X.indices.copy()
    X.has_sorted_indices = False
    X.sort_indices()
    assert_equal(indices, X.indices, 'Expected sorted indices, found unsorted')


@pytest.mark.parametrize("spcreator", formats_for_index1d)
class TestGetSet1D:
    def test_None_index(self, spcreator):
        D = np.array([4, 3, 0])
        A = spcreator(D)

        N = D.shape[0]
        for j in range(-N, N):
            assert_equal(A[j, None].toarray(), D[j, None])
            assert_equal(A[None, j].toarray(), D[None, j])
            assert_equal(A[None, None, j].toarray(), D[None, None, j])

    def test_getitem_shape(self, spcreator):
        A = spcreator(np.arange(3 * 4).reshape(3, 4))
        assert A[1, 2].ndim == 0
        assert A[1, 2:3].shape == (1,)
        assert A[None, 1, 2:3].shape == (1, 1)
        assert A[None, 1, 2].shape == (1,)
        assert A[None, 1, 2, None].shape == (1, 1)

        with pytest.raises(IndexError, match='Only 1D or 2D arrays'):
            A[None, 2, 1, None, None]
        with pytest.raises(IndexError, match='Only 1D or 2D arrays'):
            A[None, 0:2, None, 1]
        with pytest.raises(IndexError, match='Only 1D or 2D arrays'):
            A[0:1, 1:, None]
        with pytest.raises(IndexError, match='Only 1D or 2D arrays'):
            A[1:, 1, None, None]

    def test_getelement(self, spcreator):
        D = np.array([4, 3, 0])
        A = spcreator(D)

        N = D.shape[0]
        for j in range(-N, N):
            assert_equal(A[j], D[j])

        for ij in [3, -4]:
            with pytest.raises(IndexError, match='index (.*) out of (range|bounds)'):
                A.__getitem__(ij)

        # single element tuples unwrapped
        assert A[(0,)] == 4

        with pytest.raises(IndexError, match='index (.*) out of (range|bounds)'):
            A.__getitem__((4,))

    def test_setelement(self, spcreator):
        dtype = np.float64
        A = spcreator((12,), dtype=dtype)
        with np.testing.suppress_warnings() as sup:
            sup.filter(
                SparseEfficiencyWarning,
                "Changing the sparsity structure of .* is expensive",
            )
            A[0] = dtype(0)
            A[1] = dtype(3)
            A[8] = dtype(9.0)
            A[-2] = dtype(7)
            A[5] = 9

            A[-9,] = dtype(8)
            A[1,] = dtype(5)  # overwrite using 1-tuple index

            for ij in [13, -14, (13,), (14,)]:
                with pytest.raises(IndexError, match='out of (range|bounds)'):
                    A.__setitem__(ij, 123.0)


@pytest.mark.parametrize("spcreator", formats_for_index1d)
class TestSlicingAndFancy1D:
    #######################
    #  Int-like Array Index
    #######################
    def test_get_array_index(self, spcreator):
        D = np.array([4, 3, 0])
        A = spcreator(D)

        assert_equal(A[()].toarray(), D[()])
        for ij in [(0, 3), (3,)]:
            with pytest.raises(IndexError, match='out of (range|bounds)|many indices'):
                A.__getitem__(ij)

    def test_set_array_index(self, spcreator):
        dtype = np.float64
        A = spcreator((12,), dtype=dtype)
        with np.testing.suppress_warnings() as sup:
            sup.filter(
                SparseEfficiencyWarning,
                "Changing the sparsity structure of .* is expensive",
            )
            A[np.array(6)] = dtype(4.0)  # scalar index
            A[np.array(6)] = dtype(2.0)  # overwrite with scalar index
            assert_equal(A.toarray(), [0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0])

            for ij in [(13,), (-14,)]:
                with pytest.raises(IndexError, match='index .* out of (range|bounds)'):
                    A.__setitem__(ij, 123.0)

            for v in [(), (0, 3), [1, 2, 3], np.array([1, 2, 3])]:
                msg = 'Trying to assign a sequence to an item'
                with pytest.raises(ValueError, match=msg):
                    A.__setitem__(0, v)

    ####################
    #  1d Slice as index
    ####################
    def test_dtype_preservation(self, spcreator):
        assert_equal(spcreator((10,), dtype=np.int16)[1:5].dtype, np.int16)
        assert_equal(spcreator((6,), dtype=np.int32)[0:0:2].dtype, np.int32)
        assert_equal(spcreator((6,), dtype=np.int64)[:].dtype, np.int64)

    def test_get_1d_slice(self, spcreator):
        B = np.arange(50.)
        A = spcreator(B)
        assert_equal(B[:], A[:].toarray())
        assert_equal(B[2:5], A[2:5].toarray())

        C = np.array([4, 0, 6, 0, 0, 0, 0, 0, 1])
        D = spcreator(C)
        assert_equal(C[1:3], D[1:3].toarray())

        # Now test slicing when a row contains only zeros
        E = np.array([0, 0, 0, 0, 0])
        F = spcreator(E)
        assert_equal(E[1:3], F[1:3].toarray())
        assert_equal(E[-2:], F[-2:].toarray())
        assert_equal(E[:], F[:].toarray())
        assert_equal(E[slice(None)], F[slice(None)].toarray())

    def test_slicing_idx_slice(self, spcreator):
        B = np.arange(50)
        A = spcreator(B)

        # [i]
        assert_equal(A[2], B[2])
        assert_equal(A[-1], B[-1])
        assert_equal(A[np.array(-2)], B[-2])

        # [1:2]
        assert_equal(A[:].toarray(), B[:])
        assert_equal(A[5:-2].toarray(), B[5:-2])
        assert_equal(A[5:12:3].toarray(), B[5:12:3])

        # int8 slice
        s = slice(np.int8(2), np.int8(4), None)
        assert_equal(A[s].toarray(), B[2:4])

        # np.s_
        s_ = np.s_
        slices = [s_[:2], s_[1:2], s_[3:], s_[3::2],
                  s_[15:20], s_[3:2],
                  s_[8:3:-1], s_[4::-2], s_[:5:-1],
                  0, 1, s_[:], s_[1:5], -1, -2, -5,
                  np.array(-1), np.int8(-3)]

        for j, a in enumerate(slices):
            x = A[a]
            y = B[a]
            if y.shape == ():
                assert_equal(x, y, repr(a))
            else:
                if x.size == 0 and y.size == 0:
                    pass
                else:
                    assert_equal(x.toarray(), y, repr(a))

    def test_ellipsis_1d_slicing(self, spcreator):
        B = np.arange(50)
        A = spcreator(B)
        assert_equal(A[...].toarray(), B[...])
        assert_equal(A[...,].toarray(), B[...,])

    ##########################
    #  Assignment with Slicing
    ##########################
    def test_slice_scalar_assign(self, spcreator):
        A = spcreator((5,))
        B = np.zeros((5,))
        with np.testing.suppress_warnings() as sup:
            sup.filter(
                SparseEfficiencyWarning,
                "Changing the sparsity structure of .* is expensive",
            )
            for C in [A, B]:
                C[0:1] = 1
                C[2:0] = 4
                C[2:3] = 9
                C[3:] = 1
                C[3::-1] = 9
        assert_equal(A.toarray(), B)

    def test_slice_assign_2(self, spcreator):
        shape = (10,)

        for idx in [slice(3), slice(None, 10, 4), slice(5, -2)]:
            A = spcreator(shape)
            with np.testing.suppress_warnings() as sup:
                sup.filter(
                    SparseEfficiencyWarning,
                    "Changing the sparsity structure of .* is expensive",
                )
                A[idx] = 1
            B = np.zeros(shape)
            B[idx] = 1
            msg = f"idx={idx!r}"
            assert_allclose(A.toarray(), B, err_msg=msg)

    def test_self_self_assignment(self, spcreator):
        # Tests whether a row of one lil_matrix can be assigned to another.
        B = spcreator((5,))
        with np.testing.suppress_warnings() as sup:
            sup.filter(
                SparseEfficiencyWarning,
                "Changing the sparsity structure of .* is expensive",
            )
            B[0] = 2
            B[1] = 0
            B[2] = 3
            B[3] = 10

            A = B / 10
            B[:] = A[:]
            assert_equal(A[:].toarray(), B[:].toarray())

            A = B / 10
            B[:] = A[:1]
            assert_equal(np.zeros((5,)) + A[0], B.toarray())

            A = B / 10
            B[:-1] = A[1:]
            assert_equal(A[1:].toarray(), B[:-1].toarray())

    def test_slice_assignment(self, spcreator):
        B = spcreator((4,))
        expected = np.array([10, 0, 14, 0])
        block = [2, 1]

        with np.testing.suppress_warnings() as sup:
            sup.filter(
                SparseEfficiencyWarning,
                "Changing the sparsity structure of .* is expensive",
            )
            B[0] = 5
            B[2] = 7
            B[:] = B + B
            assert_equal(B.toarray(), expected)

            B[:2] = csr_array(block)
            assert_equal(B.toarray()[:2], block)

    def test_set_slice(self, spcreator):
        A = spcreator((5,))
        B = np.zeros(5, float)
        s_ = np.s_
        slices = [s_[:2], s_[1:2], s_[3:], s_[3::2],
                  s_[8:3:-1], s_[4::-2], s_[:5:-1],
                  0, 1, s_[:], s_[1:5], -1, -2, -5,
                  np.array(-1), np.int8(-3)]

        with np.testing.suppress_warnings() as sup:
            sup.filter(
                SparseEfficiencyWarning,
                "Changing the sparsity structure of .* is expensive",
            )
            for j, a in enumerate(slices):
                A[a] = j
                B[a] = j
                assert_equal(A.toarray(), B, repr(a))

            A[1:10:2] = range(1, 5, 2)
            B[1:10:2] = range(1, 5, 2)
            assert_equal(A.toarray(), B)

        # The next commands should raise exceptions
        toobig = list(range(100))
        with pytest.raises(ValueError, match='Trying to assign a sequence to an item'):
            A.__setitem__(0, toobig)
        with pytest.raises(ValueError, match='could not be broadcast together'):
            A.__setitem__(slice(None), toobig)

    def test_assign_empty(self, spcreator):
        A = spcreator(np.ones(3))
        B = spcreator((2,))
        A[:2] = B
        assert_equal(A.toarray(), [0, 0, 1])

    ####################
    #  1d Fancy Indexing
    ####################
    def test_dtype_preservation_empty_index(self, spcreator):
        A = spcreator((2,), dtype=np.int16)
        assert_equal(A[[False, False]].dtype, np.int16)
        assert_equal(A[[]].dtype, np.int16)

    def test_bad_index(self, spcreator):
        A = spcreator(np.zeros(5))
        with pytest.raises(
            (IndexError, ValueError, TypeError),
            match='Index dimension must be 1 or 2|only integers',
        ):
            A.__getitem__("foo")
        with pytest.raises(
            (IndexError, ValueError, TypeError),
            match='tuple index out of range|only integers',
        ):
            A.__getitem__((2, "foo"))

    def test_fancy_indexing_2darray(self, spcreator):
        B = np.arange(50).reshape((5, 10))
        A = spcreator(B)

        # [i]
        assert_equal(A[[1, 3]].toarray(), B[[1, 3]])

        # [i,[1,2]]
        assert_equal(A[3, [1, 3]].toarray(), B[3, [1, 3]])
        assert_equal(A[-1, [2, -5]].toarray(), B[-1, [2, -5]])
        assert_equal(A[np.array(-1), [2, -5]].toarray(), B[-1, [2, -5]])
        assert_equal(A[-1, np.array([2, -5])].toarray(), B[-1, [2, -5]])
        assert_equal(A[np.array(-1), np.array([2, -5])].toarray(), B[-1, [2, -5]])

        # [1:2,[1,2]]
        assert_equal(A[:, [2, 8, 3, -1]].toarray(), B[:, [2, 8, 3, -1]])
        assert_equal(A[3:4, [9]].toarray(), B[3:4, [9]])
        assert_equal(A[1:4, [-1, -5]].toarray(), B[1:4, [-1, -5]])
        assert_equal(A[1:4, np.array([-1, -5])].toarray(), B[1:4, [-1, -5]])

        # [[1,2],j]
        assert_equal(A[[1, 3], 3].toarray(), B[[1, 3], 3])
        assert_equal(A[[2, -5], -4].toarray(), B[[2, -5], -4])
        assert_equal(A[np.array([2, -5]), -4].toarray(), B[[2, -5], -4])
        assert_equal(A[[2, -5], np.array(-4)].toarray(), B[[2, -5], -4])
        assert_equal(A[np.array([2, -5]), np.array(-4)].toarray(), B[[2, -5], -4])

        # [[1,2],1:2]
        assert_equal(A[[1, 3], :].toarray(), B[[1, 3], :])
        assert_equal(A[[2, -5], 8:-1].toarray(), B[[2, -5], 8:-1])
        assert_equal(A[np.array([2, -5]), 8:-1].toarray(), B[[2, -5], 8:-1])

        # [[1,2],[1,2]]
        assert_equal(toarray(A[[1, 3], [2, 4]]), B[[1, 3], [2, 4]])
        assert_equal(toarray(A[[-1, -3], [2, -4]]), B[[-1, -3], [2, -4]])
        assert_equal(
            toarray(A[np.array([-1, -3]), [2, -4]]), B[[-1, -3], [2, -4]]
        )
        assert_equal(
            toarray(A[[-1, -3], np.array([2, -4])]), B[[-1, -3], [2, -4]]
        )
        assert_equal(
            toarray(A[np.array([-1, -3]), np.array([2, -4])]), B[[-1, -3], [2, -4]]
        )

        # [[[1],[2]],[1,2]]
        assert_equal(A[[[1], [3]], [2, 4]].toarray(), B[[[1], [3]], [2, 4]])
        assert_equal(
            A[[[-1], [-3], [-2]], [2, -4]].toarray(),
            B[[[-1], [-3], [-2]], [2, -4]]
        )
        assert_equal(
            A[np.array([[-1], [-3], [-2]]), [2, -4]].toarray(),
            B[[[-1], [-3], [-2]], [2, -4]]
        )
        assert_equal(
            A[[[-1], [-3], [-2]], np.array([2, -4])].toarray(),
            B[[[-1], [-3], [-2]], [2, -4]]
        )
        assert_equal(
            A[np.array([[-1], [-3], [-2]]), np.array([2, -4])].toarray(),
            B[[[-1], [-3], [-2]], [2, -4]]
        )

        # [[1,2]]
        assert_equal(A[[1, 3]].toarray(), B[[1, 3]])
        assert_equal(A[[-1, -3]].toarray(), B[[-1, -3]])
        assert_equal(A[np.array([-1, -3])].toarray(), B[[-1, -3]])

        # [[1,2],:][:,[1,2]]
        assert_equal(
            A[[1, 3], :][:, [2, 4]].toarray(), B[[1, 3], :][:, [2, 4]]
        )
        assert_equal(
            A[[-1, -3], :][:, [2, -4]].toarray(), B[[-1, -3], :][:, [2, -4]]
        )
        assert_equal(
            A[np.array([-1, -3]), :][:, np.array([2, -4])].toarray(),
            B[[-1, -3], :][:, [2, -4]]
        )

        # [:,[1,2]][[1,2],:]
        assert_equal(
            A[:, [1, 3]][[2, 4], :].toarray(), B[:, [1, 3]][[2, 4], :]
        )
        assert_equal(
            A[:, [-1, -3]][[2, -4], :].toarray(), B[:, [-1, -3]][[2, -4], :]
        )
        assert_equal(
            A[:, np.array([-1, -3])][np.array([2, -4]), :].toarray(),
            B[:, [-1, -3]][[2, -4], :]
        )

    def test_fancy_indexing(self, spcreator):
        B = np.arange(50)
        A = spcreator(B)

        # [i]
        assert_equal(A[[3]].toarray(), B[[3]])

        # [np.array]
        assert_equal(A[[1, 3]].toarray(), B[[1, 3]])
        assert_equal(A[[2, -5]].toarray(), B[[2, -5]])
        assert_equal(A[np.array(-1)], B[-1])
        assert_equal(A[np.array([-1, 2])].toarray(), B[[-1, 2]])
        assert_equal(A[np.array(5)], B[np.array(5)])

        # [[[1],[2]]]
        ind = np.array([[1], [3]])
        assert_equal(A[ind].toarray(), B[ind])
        ind = np.array([[-1], [-3], [-2]])
        assert_equal(A[ind].toarray(), B[ind])

        # [[1, 2]]
        assert_equal(A[[1, 3]].toarray(), B[[1, 3]])
        assert_equal(A[[-1, -3]].toarray(), B[[-1, -3]])
        assert_equal(A[np.array([-1, -3])].toarray(), B[[-1, -3]])

        # [[1, 2]][[1, 2]]
        assert_equal(A[[1, 5, 2, 8]][[1, 3]].toarray(),
                     B[[1, 5, 2, 8]][[1, 3]])
        assert_equal(A[[-1, -5, 2, 8]][[1, -4]].toarray(),
                     B[[-1, -5, 2, 8]][[1, -4]])

    def test_fancy_indexing_boolean(self, spcreator):
        np.random.seed(1234)  # make runs repeatable

        B = np.arange(50)
        A = spcreator(B)

        I = np.array(np.random.randint(0, 2, size=50), dtype=bool)

        assert_equal(toarray(A[I]), B[I])
        assert_equal(toarray(A[B > 9]), B[B > 9])

        Z1 = np.zeros(51, dtype=bool)
        Z2 = np.zeros(51, dtype=bool)
        Z2[-1] = True
        Z3 = np.zeros(51, dtype=bool)
        Z3[0] = True

        msg = 'bool index .* has shape|boolean index did not match'
        with pytest.raises(IndexError, match=msg):
            A.__getitem__(Z1)
        with pytest.raises(IndexError, match=msg):
            A.__getitem__(Z2)
        with pytest.raises(IndexError, match=msg):
            A.__getitem__(Z3)

    def test_fancy_indexing_sparse_boolean(self, spcreator):
        np.random.seed(1234)  # make runs repeatable

        B = np.arange(20)
        A = spcreator(B)

        X = np.array(np.random.randint(0, 2, size=20), dtype=bool)
        Xsp = csr_array(X)

        assert_equal(toarray(A[Xsp]), B[X])
        assert_equal(toarray(A[A > 9]), B[B > 9])

        Y = np.array(np.random.randint(0, 2, size=60), dtype=bool)

        Ysp = csr_array(Y)

        with pytest.raises(IndexError, match='bool index .* has shape|only integers'):
            A.__getitem__(Ysp)
        with pytest.raises(IndexError, match='tuple index out of range|only integers'):
            A.__getitem__((Xsp, 1))

    def test_fancy_indexing_seq_assign(self, spcreator):
        mat = spcreator(np.array([1, 0]))
        with pytest.raises(ValueError, match='Trying to assign a sequence to an item'):
            mat.__setitem__(0, np.array([1, 2]))

    def test_fancy_indexing_empty(self, spcreator):
        B = np.arange(50)
        B[3:9] = 0
        B[30] = 0
        A = spcreator(B)

        K = np.array([False] * 50)
        assert_equal(toarray(A[K]), B[K])
        K = np.array([], dtype=int)
        assert_equal(toarray(A[K]), B[K])
        J = np.array([0, 1, 2, 3, 4], dtype=int)
        assert_equal(toarray(A[J]), B[J])

    ############################
    #  1d Fancy Index Assignment
    ############################
    def test_bad_index_assign(self, spcreator):
        A = spcreator(np.zeros(5))
        msg = 'Index dimension must be 1 or 2|only integers'
        with pytest.raises((IndexError, ValueError, TypeError), match=msg):
            A.__setitem__("foo", 2)

    def test_fancy_indexing_set(self, spcreator):
        M = (5,)

        # [1:2]
        for j in [[2, 3, 4], slice(None, 10, 4), np.arange(3),
                     slice(5, -2), slice(2, 5)]:
            A = spcreator(M)
            B = np.zeros(M)
            with np.testing.suppress_warnings() as sup:
                sup.filter(
                    SparseEfficiencyWarning,
                    "Changing the sparsity structure of .* is expensive",
                )
                B[j] = 1
                with check_remains_sorted(A):
                    A[j] = 1
            assert_allclose(A.toarray(), B)

    def test_sequence_assignment(self, spcreator):
        A = spcreator((4,))
        B = spcreator((3,))

        i0 = [0, 1, 2]
        i1 = (0, 1, 2)
        i2 = np.array(i0)

        with np.testing.suppress_warnings() as sup:
            sup.filter(
                SparseEfficiencyWarning,
                "Changing the sparsity structure of .* is expensive",
            )
            with check_remains_sorted(A):
                A[i0] = B[i0]
                msg = "too many indices for array|tuple index out of range"
                with pytest.raises(IndexError, match=msg):
                    B.__getitem__(i1)
                A[i2] = B[i2]
            assert_equal(A[:3].toarray(), B.toarray())
            assert A.shape == (4,)

            # slice
            A = spcreator((4,))
            with check_remains_sorted(A):
                A[1:3] = [10, 20]
            assert_equal(A.toarray(), [0, 10, 20, 0])

            # array
            A = spcreator((4,))
            B = np.zeros(4)
            with check_remains_sorted(A):
                for C in [A, B]:
                    C[[0, 1, 2]] = [4, 5, 6]
            assert_equal(A.toarray(), B)

    def test_fancy_assign_empty(self, spcreator):
        B = np.arange(50)
        B[2] = 0
        B[[3, 6]] = 0
        A = spcreator(B)

        K = np.array([False] * 50)
        A[K] = 42
        assert_equal(A.toarray(), B)

        K = np.array([], dtype=int)
        A[K] = 42
        assert_equal(A.toarray(), B)
