# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ###########################################################################*/
"""Tests for array_like module"""

__authors__ = ["P. Knobel"]
__license__ = "MIT"
__date__ = "09/01/2017"

try:
    import h5py
except ImportError:
    h5py = None

import numpy
import os
import tempfile
import unittest

from ..array_like import DatasetView, ListOfImages
from ..array_like import get_dtype, get_concatenated_dtype, get_shape,\
    is_array, is_nested_sequence, is_list_of_arrays


@unittest.skipIf(h5py is None,
                 "h5py is needed to test DatasetView")
class TestTransposedDatasetView(unittest.TestCase):

    def setUp(self):
        # dataset attributes
        self.ndim = 3
        self.original_shape = (5, 10, 20)
        self.size = 1
        for dim in self.original_shape:
            self.size *= dim

        self.volume = numpy.arange(self.size).reshape(self.original_shape)

        self.tempdir = tempfile.mkdtemp()
        self.h5_fname = os.path.join(self.tempdir, "tempfile.h5")
        with h5py.File(self.h5_fname, "w") as f:
            f["volume"] = self.volume

        self.h5f = h5py.File(self.h5_fname, "r")

        self.all_permutations = [(0, 1, 2), (0, 2, 1), (1, 0, 2), (1, 2, 0),
                                 (2, 0, 1), (2, 1, 0)]

    def tearDown(self):
        self.h5f.close()
        os.unlink(self.h5_fname)
        os.rmdir(self.tempdir)

    def _testSize(self, obj):
        """These assertions apply to all following test cases"""
        self.assertEqual(obj.ndim, self.ndim)
        self.assertEqual(obj.size, self.size)
        size_from_shape = 1
        for dim in obj.shape:
            size_from_shape *= dim
        self.assertEqual(size_from_shape, self.size)

        for dim in self.original_shape:
            self.assertIn(dim, obj.shape)

    def testNoTransposition(self):
        """no transposition (transposition = (0, 1, 2))"""
        a = DatasetView(self.h5f["volume"])

        self.assertEqual(a.shape, self.original_shape)
        self._testSize(a)

        # reversing the dimensions twice results in no change
        rtrans = list(reversed(range(self.ndim)))
        self.assertTrue(numpy.array_equal(
                a,
                a.transpose(rtrans).transpose(rtrans)))

        for i in range(a.shape[0]):
            for j in range(a.shape[1]):
                for k in range(a.shape[2]):
                    self.assertEqual(self.h5f["volume"][i, j, k],
                                     a[i, j, k])

    def _testTransposition(self, transposition):
        """test transposed dataset

        :param tuple transposition: List of dimensions (0... n-1) sorted
            in the desired order
        """
        a = DatasetView(self.h5f["volume"],
                        transposition=transposition)
        self._testSize(a)

        # sort shape of transposed object, to hopefully find the original shape
        sorted_shape = tuple(dim_size for (_, dim_size) in
                             sorted(zip(transposition, a.shape)))
        self.assertEqual(sorted_shape, self.original_shape)

        a_as_array = numpy.array(self.h5f["volume"]).transpose(transposition)

        # test the __array__ method
        self.assertTrue(numpy.array_equal(
                numpy.array(a),
                a_as_array))

        # test slicing
        for selection in [(2, slice(None), slice(None)),
                          (slice(None), 1, slice(0, 8)),
                          (slice(0, 3), slice(None), 3),
                          (1, 3, slice(None)),
                          (slice(None), 2, 1),
                          (4, slice(1, 9, 2), 2)]:
            self.assertIsInstance(a[selection], numpy.ndarray)
            self.assertTrue(numpy.array_equal(
                    a[selection],
                    a_as_array[selection]))

        # test the DatasetView.__getitem__ for single values
        # (step adjusted to test at least 3 indices in each dimension)
        for i in range(0, a.shape[0], a.shape[0] // 3):
            for j in range(0, a.shape[1], a.shape[1] // 3):
                for k in range(0, a.shape[2], a.shape[2] // 3):
                    sorted_indices = tuple(idx for (_, idx) in
                                           sorted(zip(transposition, [i, j, k])))
                    viewed_value = a[i, j, k]
                    corresponding_original_value = self.h5f["volume"][sorted_indices]
                    self.assertEqual(viewed_value,
                                     corresponding_original_value)

        # reversing the dimensions twice results in no change
        rtrans = list(reversed(range(self.ndim)))
        self.assertTrue(numpy.array_equal(
                a,
                a.transpose(rtrans).transpose(rtrans)))

        # test .T property
        self.assertTrue(numpy.array_equal(
                a.T,
                a.transpose(rtrans)))

    def testTransposition012(self):
        """transposition = (0, 1, 2)
        (should be the same as testNoTransposition)"""
        self._testTransposition((0, 1, 2))

    def testTransposition021(self):
        """transposition = (0, 2, 1)"""
        self._testTransposition((0, 2, 1))

    def testTransposition102(self):
        """transposition = (1, 0, 2)"""
        self._testTransposition((1, 0, 2))

    def testTransposition120(self):
        """transposition = (1, 2, 0)"""
        self._testTransposition((1, 2, 0))

    def testTransposition201(self):
        """transposition = (2, 0, 1)"""
        self._testTransposition((2, 0, 1))

    def testTransposition210(self):
        """transposition = (2, 1, 0)"""
        self._testTransposition((2, 1, 0))

    def testAllDoubleTranspositions(self):
        for trans1 in self.all_permutations:
            for trans2 in self.all_permutations:
                self._testDoubleTransposition(trans1, trans2)

    def _testDoubleTransposition(self, transposition1, transposition2):
        a = DatasetView(self.h5f["volume"],
                        transposition=transposition1).transpose(transposition2)

        b = self.volume.transpose(transposition1).transpose(transposition2)

        self.assertTrue(numpy.array_equal(a, b),
                        "failed with double transposition %s %s" % (transposition1, transposition2))

    def test1DIndex(self):
        a = DatasetView(self.h5f["volume"])
        self.assertTrue(numpy.array_equal(self.volume[1],
                                          a[1]))

        b = DatasetView(self.h5f["volume"], transposition=(1, 0, 2))
        self.assertTrue(numpy.array_equal(self.volume[:, 1, :],
                                          b[1]))


class TestTransposedListOfImages(unittest.TestCase):
    def setUp(self):
        # images attributes
        self.ndim = 3
        self.original_shape = (5, 10, 20)
        self.size = 1
        for dim in self.original_shape:
            self.size *= dim

        volume = numpy.arange(self.size).reshape(self.original_shape)

        self.images = []
        for i in range(self.original_shape[0]):
            self.images.append(
                    volume[i])

        self.images_as_3D_array = numpy.array(self.images)

        self.all_permutations = [(0, 1, 2), (0, 2, 1), (1, 0, 2), (1, 2, 0),
                                 (2, 0, 1), (2, 1, 0)]

    def tearDown(self):
        pass

    def _testSize(self, obj):
        """These assertions apply to all following test cases"""
        self.assertEqual(obj.ndim, self.ndim)
        self.assertEqual(obj.size, self.size)
        size_from_shape = 1
        for dim in obj.shape:
            size_from_shape *= dim
        self.assertEqual(size_from_shape, self.size)

        for dim in self.original_shape:
            self.assertIn(dim, obj.shape)

    def testNoTransposition(self):
        """no transposition (transposition = (0, 1, 2))"""
        a = ListOfImages(self.images)

        self.assertEqual(a.shape, self.original_shape)
        self._testSize(a)

        for i in range(a.shape[0]):
            for j in range(a.shape[1]):
                for k in range(a.shape[2]):
                    self.assertEqual(self.images[i][j, k],
                                     a[i, j, k])

        # reversing the dimensions twice results in no change
        rtrans = list(reversed(range(self.ndim)))
        self.assertTrue(numpy.array_equal(
                a,
                a.transpose(rtrans).transpose(rtrans)))

        # test .T property
        self.assertTrue(numpy.array_equal(
                a.T,
                a.transpose(rtrans)))

    def _testTransposition(self, transposition):
        """test transposed dataset

        :param tuple transposition: List of dimensions (0... n-1) sorted
            in the desired order
        """
        a = ListOfImages(self.images,
                         transposition=transposition)
        self._testSize(a)

        # sort shape of transposed object, to hopefully find the original shape
        sorted_shape = tuple(dim_size for (_, dim_size) in
                             sorted(zip(transposition, a.shape)))
        self.assertEqual(sorted_shape, self.original_shape)

        a_as_array = numpy.array(self.images).transpose(transposition)

        # test the DatasetView.__array__ method
        self.assertTrue(numpy.array_equal(
                numpy.array(a),
                a_as_array))

        # test slicing
        for selection in [(2, slice(None), slice(None)),
                          (slice(None), 1, slice(0, 8)),
                          (slice(0, 3), slice(None), 3),
                          (1, 3, slice(None)),
                          (slice(None), 2, 1),
                          (4, slice(1, 9, 2), 2)]:
            self.assertIsInstance(a[selection], numpy.ndarray)
            self.assertTrue(numpy.array_equal(
                    a[selection],
                    a_as_array[selection]))

        # test the DatasetView.__getitem__ for single values
        # (step adjusted to test at least 3 indices in each dimension)
        for i in range(0, a.shape[0], a.shape[0] // 3):
            for j in range(0, a.shape[1], a.shape[1] // 3):
                for k in range(0, a.shape[2], a.shape[2] // 3):
                    viewed_value = a[i, j, k]
                    sorted_indices = tuple(idx for (_, idx) in
                                           sorted(zip(transposition, [i, j, k])))
                    corresponding_original_value = self.images[sorted_indices[0]][sorted_indices[1:]]
                    self.assertEqual(viewed_value,
                                     corresponding_original_value)

        # reversing the dimensions twice results in no change
        rtrans = list(reversed(range(self.ndim)))
        self.assertTrue(numpy.array_equal(
                a,
                a.transpose(rtrans).transpose(rtrans)))

        # test .T property
        self.assertTrue(numpy.array_equal(
                a.T,
                a.transpose(rtrans)))

    def _testDoubleTransposition(self, transposition1, transposition2):
        a = ListOfImages(self.images,
                         transposition=transposition1).transpose(transposition2)

        b = self.images_as_3D_array.transpose(transposition1).transpose(transposition2)

        self.assertTrue(numpy.array_equal(a, b),
                        "failed with double transposition %s %s" % (transposition1, transposition2))

    def testTransposition012(self):
        """transposition = (0, 1, 2)
        (should be the same as testNoTransposition)"""
        self._testTransposition((0, 1, 2))

    def testTransposition021(self):
        """transposition = (0, 2, 1)"""
        self._testTransposition((0, 2, 1))

    def testTransposition102(self):
        """transposition = (1, 0, 2)"""
        self._testTransposition((1, 0, 2))

    def testTransposition120(self):
        """transposition = (1, 2, 0)"""
        self._testTransposition((1, 2, 0))

    def testTransposition201(self):
        """transposition = (2, 0, 1)"""
        self._testTransposition((2, 0, 1))

    def testTransposition210(self):
        """transposition = (2, 1, 0)"""
        self._testTransposition((2, 1, 0))

    def testAllDoubleTranspositions(self):
        for trans1 in self.all_permutations:
            for trans2 in self.all_permutations:
                self._testDoubleTransposition(trans1, trans2)

    def test1DIndex(self):
        a = ListOfImages(self.images)
        self.assertTrue(numpy.array_equal(self.images[1],
                                          a[1]))

        b = ListOfImages(self.images, transposition=(1, 0, 2))
        self.assertTrue(numpy.array_equal(self.images_as_3D_array[:, 1, :],
                                          b[1]))


class TestFunctions(unittest.TestCase):
    """Test functions to guess the dtype and shape of an array_like
    object"""
    def testListOfLists(self):
        l = [[0, 1, 2], [2, 3, 4]]
        self.assertEqual(get_dtype(l),
                         numpy.dtype(int))
        self.assertEqual(get_shape(l),
                         (2, 3))
        self.assertTrue(is_nested_sequence(l))
        self.assertFalse(is_array(l))
        self.assertFalse(is_list_of_arrays(l))

        l = [[0., 1.], [2., 3.]]
        self.assertEqual(get_dtype(l),
                         numpy.dtype(float))
        self.assertEqual(get_shape(l),
                         (2, 2))
        self.assertTrue(is_nested_sequence(l))
        self.assertFalse(is_array(l))
        self.assertFalse(is_list_of_arrays(l))

        # concatenated dtype of int and float
        l = [numpy.array([[0, 1, 2], [2, 3, 4]]),
             numpy.array([[0., 1., 2.], [2., 3., 4.]])]

        self.assertEqual(get_concatenated_dtype(l),
                         numpy.array(l).dtype)
        self.assertEqual(get_shape(l),
                         (2, 2, 3))
        self.assertFalse(is_nested_sequence(l))
        self.assertFalse(is_array(l))
        self.assertTrue(is_list_of_arrays(l))

    def testNumpyArray(self):
        a = numpy.array([[0, 1], [2, 3]])
        self.assertEqual(get_dtype(a),
                         a.dtype)
        self.assertFalse(is_nested_sequence(a))
        self.assertTrue(is_array(a))
        self.assertFalse(is_list_of_arrays(a))

    @unittest.skipIf(h5py is None,
                     "h5py is needed for this test")
    def testH5pyDataset(self):
        a = numpy.array([[0, 1], [2, 3]])

        tempdir = tempfile.mkdtemp()
        h5_fname = os.path.join(tempdir, "tempfile.h5")
        with h5py.File(h5_fname, "w") as h5f:
            h5f["dataset"] = a
            d = h5f["dataset"]

            self.assertEqual(get_dtype(d),
                             numpy.dtype(int))
            self.assertFalse(is_nested_sequence(d))
            self.assertTrue(is_array(d))
            self.assertFalse(is_list_of_arrays(d))

        os.unlink(h5_fname)
        os.rmdir(tempdir)


def suite():
    test_suite = unittest.TestSuite()
    test_suite.addTest(
        unittest.defaultTestLoader.loadTestsFromTestCase(TestTransposedDatasetView))
    test_suite.addTest(
        unittest.defaultTestLoader.loadTestsFromTestCase(TestTransposedListOfImages))
    test_suite.addTest(
        unittest.defaultTestLoader.loadTestsFromTestCase(TestFunctions))
    return test_suite


if __name__ == '__main__':
    unittest.main(defaultTest='suite')
