from __future__ import print_function, division, absolute_import

import time
import warnings
import sys
# unittest only added in 3.4 self.subTest()
if sys.version_info[0] < 3 or sys.version_info[1] < 4:
    import unittest2 as unittest
else:
    import unittest
# unittest.mock is not available in 2.7 (though unittest2 might contain it?)
try:
    import unittest.mock as mock
except ImportError:
    import mock

import matplotlib
matplotlib.use('Agg')  # fix execution of tests involving matplotlib on travis
import numpy as np
import six.moves as sm

import imgaug as ia


# TODO add tests for:
#      hooks is_activated
#      hooks is_propagating
#      hooks preprocess
#      hooks postprocess
#      HeatmapsOnImage.__init__()
#      HeatmapsOnImage.get_arr()
#      HeatmapsOnImage.to_uint8()
#      HeatmapsOnImage.from_0to1()
#      HeatmapsOnImage.copy()
#      HeatmapsOnImage.deepcopy()


class TestHeatmapsOnImage_draw(unittest.TestCase):
    def test_basic_functionality(self):
        heatmaps_arr = np.float32([
            [0.5, 0.0, 0.0, 0.5],
            [0.0, 1.0, 1.0, 0.0],
            [0.0, 1.0, 1.0, 0.0],
            [0.5, 0.0, 0.0, 0.5],
        ])
        heatmaps = ia.HeatmapsOnImage(heatmaps_arr, shape=(4, 4, 3))

        heatmaps_drawn = heatmaps.draw()[0]
        assert heatmaps_drawn.shape == (4, 4, 3)
        v1 = heatmaps_drawn[0, 1]
        v2 = heatmaps_drawn[0, 0]
        v3 = heatmaps_drawn[1, 1]

        v1_coords = [(0, 1), (0, 2), (1, 0), (1, 3), (2, 0), (2, 3), (3, 1),
                     (3, 2)]
        v2_coords = [(0, 0), (0, 3), (3, 0), (3, 3)]
        v3_coords = [(1, 1), (1, 2), (2, 1), (2, 2)]

        for y, x in v1_coords:
            assert np.allclose(heatmaps_drawn[y, x], v1)

        for y, x in v2_coords:
            assert np.allclose(heatmaps_drawn[y, x], v2)

        for y, x in v3_coords:
            assert np.allclose(heatmaps_drawn[y, x], v3)

    def test_use_size_arg_with_different_shape_than_heatmap_arr_shape(self):
        # size differs from heatmap array size
        heatmaps_arr = np.float32([
            [0.0, 1.0],
            [0.0, 1.0]
        ])
        heatmaps = ia.HeatmapsOnImage(heatmaps_arr, shape=(2, 2, 3))

        heatmaps_drawn = heatmaps.draw(size=(4, 4))[0]
        assert heatmaps_drawn.shape == (4, 4, 3)
        v1 = heatmaps_drawn[0, 0]
        v2 = heatmaps_drawn[0, -1]

        for y in sm.xrange(4):
            for x in sm.xrange(2):
                assert np.allclose(heatmaps_drawn[y, x], v1)

        for y in sm.xrange(4):
            for x in sm.xrange(2, 4):
                assert np.allclose(heatmaps_drawn[y, x], v2)


# TODO test other cmaps
class TestHeatmapsOnImage_draw_on_image(unittest.TestCase):
    @property
    def heatmaps(self):
        heatmaps_arr = np.float32([
            [0.0, 1.0],
            [0.0, 1.0]
        ])
        return ia.HeatmapsOnImage(heatmaps_arr, shape=(2, 2, 3))

    def test_cmap_is_none(self):
        heatmaps = self.heatmaps

        image = np.uint8([
            [0, 0, 0, 255],
            [0, 0, 0, 255],
            [0, 0, 0, 255],
            [0, 0, 0, 255]
        ])
        image = np.tile(image[..., np.newaxis], (1, 1, 3))

        heatmaps_drawn = heatmaps.draw_on_image(image, alpha=0.5, cmap=None)[0]
        assert heatmaps_drawn.shape == (4, 4, 3)
        assert np.all(heatmaps_drawn[0:4, 0:2, :] == 0)
        assert (
            np.all(heatmaps_drawn[0:4, 2:3, :] == 128)
            or np.all(heatmaps_drawn[0:4, 2:3, :] == 127))
        assert (
            np.all(heatmaps_drawn[0:4, 3:4, :] == 255)
            or np.all(heatmaps_drawn[0:4, 3:4, :] == 254))

    def test_cmap_is_none_and_resize_is_image(self):
        heatmaps = self.heatmaps

        image = np.uint8([
            [0, 0, 0, 0],
            [0, 0, 0, 0],
            [0, 0, 0, 0],
            [0, 0, 0, 0]
        ])
        image = np.tile(image[..., np.newaxis], (1, 1, 3))

        heatmaps_drawn = heatmaps.draw_on_image(
            image, alpha=0.5, resize="image", cmap=None)[0]
        assert heatmaps_drawn.shape == (2, 2, 3)
        assert np.all(heatmaps_drawn[0:2, 0, :] == 0)
        assert (
            np.all(heatmaps_drawn[0:2, 1, :] == 128)
            or np.all(heatmaps_drawn[0:2, 1, :] == 127))


class TestHeatmapsOnImage_invert(unittest.TestCase):
    @property
    def heatmaps_arr(self):
        return np.float32([
            [0.0, 5.0, 10.0],
            [-1.0, -2.0, 7.5]
        ])

    @property
    def expected_arr(self):
        return np.float32([
            [8.0, 3.0, -2.0],
            [9.0, 10.0, 0.5]
        ])

    def test_with_2d_input_array(self):
        # (H, W)
        heatmaps_arr = self.heatmaps_arr
        expected = self.expected_arr
        heatmaps = ia.HeatmapsOnImage(heatmaps_arr,
                                      shape=(2, 3),
                                      min_value=-2.0,
                                      max_value=10.0)
        assert np.allclose(heatmaps.get_arr(), heatmaps_arr)
        assert np.allclose(heatmaps.invert().get_arr(), expected)

    def test_with_3d_input_array(self):
        # (H, W, 1)
        heatmaps_arr = self.heatmaps_arr
        expected = self.expected_arr
        heatmaps = ia.HeatmapsOnImage(heatmaps_arr[..., np.newaxis],
                                      shape=(2, 3),
                                      min_value=-2.0,
                                      max_value=10.0)
        assert np.allclose(heatmaps.get_arr(),
                           heatmaps_arr[..., np.newaxis])
        assert np.allclose(heatmaps.invert().get_arr(),
                           expected[..., np.newaxis])


class TestHeatmapsOnImage_pad(unittest.TestCase):
    @property
    def heatmaps(self):
        heatmaps_arr = np.float32([
            [0.0, 1.0],
            [0.0, 1.0]
        ])
        return ia.HeatmapsOnImage(heatmaps_arr, shape=(2, 2, 3))

    def test_defaults(self):
        heatmaps = self.heatmaps
        heatmaps_padded = heatmaps.pad(top=1, right=2, bottom=3, left=4)
        assert heatmaps_padded.arr_0to1.shape == (2+(1+3), 2+(4+2), 1)
        assert np.allclose(
            heatmaps_padded.arr_0to1[:, :, 0],
            np.float32([
                [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
                [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0],
                [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0],
                [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
                [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
                [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
            ])
        )

    def test_mode_constant_with_cval_050(self):
        heatmaps = self.heatmaps
        heatmaps_padded = heatmaps.pad(top=1, right=2, bottom=3, left=4,
                                       cval=0.5)
        assert heatmaps_padded.arr_0to1.shape == (2+(1+3), 2+(4+2), 1)
        assert np.allclose(
            heatmaps_padded.arr_0to1[:, :, 0],
            np.float32([
                [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5],
                [0.5, 0.5, 0.5, 0.5, 0.0, 1.0, 0.5, 0.5],
                [0.5, 0.5, 0.5, 0.5, 0.0, 1.0, 0.5, 0.5],
                [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5],
                [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5],
                [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]
            ])
        )

    def test_mode_edge(self):
        heatmaps = self.heatmaps
        heatmaps_padded = heatmaps.pad(top=1, right=2, bottom=3, left=4,
                                       mode="edge")
        assert heatmaps_padded.arr_0to1.shape == (2+(1+3), 2+(4+2), 1)
        assert np.allclose(
            heatmaps_padded.arr_0to1[:, :, 0],
            np.float32([
                [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
                [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
                [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
                [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
                [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
                [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0]
            ])
        )


class TestHeatmapsOnImage_pad_to_aspect_ratio(unittest.TestCase):
    @property
    def heatmaps(self):
        heatmaps_arr = np.float32([
            [0.0, 0.0, 1.0],
            [0.0, 0.0, 1.0]
        ])
        return ia.HeatmapsOnImage(heatmaps_arr, shape=(2, 2, 3))

    def test_square_ratio_with_default_mode_and_cval(self):
        heatmaps = self.heatmaps
        heatmaps_padded = heatmaps.pad_to_aspect_ratio(1.0)
        assert heatmaps_padded.arr_0to1.shape == (3, 3, 1)
        assert np.allclose(
            heatmaps_padded.arr_0to1[:, :, 0],
            np.float32([
                [0.0, 0.0, 1.0],
                [0.0, 0.0, 1.0],
                [0.0, 0.0, 0.0]
            ])
        )

    def test_square_ratio_with_cval_050(self):
        heatmaps = self.heatmaps
        heatmaps_padded = heatmaps.pad_to_aspect_ratio(1.0, cval=0.5)
        assert heatmaps_padded.arr_0to1.shape == (3, 3, 1)
        assert np.allclose(
            heatmaps_padded.arr_0to1[:, :, 0],
            np.float32([
                [0.0, 0.0, 1.0],
                [0.0, 0.0, 1.0],
                [0.5, 0.5, 0.5]
            ])
        )

    def test_square_ratio_with_edge_mode(self):
        heatmaps = self.heatmaps
        heatmaps_padded = heatmaps.pad_to_aspect_ratio(1.0, mode="edge")
        assert heatmaps_padded.arr_0to1.shape == (3, 3, 1)
        assert np.allclose(
            heatmaps_padded.arr_0to1[:, :, 0],
            np.float32([
                [0.0, 0.0, 1.0],
                [0.0, 0.0, 1.0],
                [0.0, 0.0, 1.0]
            ])
        )

    def test_wider_than_high_ratio_with_cval_010(self):
        heatmaps = self.heatmaps
        heatmaps_padded = heatmaps.pad_to_aspect_ratio(2.0, cval=0.1)
        assert heatmaps_padded.arr_0to1.shape == (2, 4, 1)
        assert np.allclose(
            heatmaps_padded.arr_0to1[:, :, 0],
            np.float32([
                [0.0, 0.0, 1.0, 0.1],
                [0.0, 0.0, 1.0, 0.1]
            ])
        )

    def test_higher_than_wide_ratio_with_cval_010(self):
        heatmaps = self.heatmaps
        heatmaps_padded = heatmaps.pad_to_aspect_ratio(0.25, cval=0.1)
        assert heatmaps_padded.arr_0to1.shape == (12, 3, 1)
        assert np.allclose(
            heatmaps_padded.arr_0to1[:, :, 0],
            np.float32([
                [0.1, 0.1, 0.1],
                [0.1, 0.1, 0.1],
                [0.1, 0.1, 0.1],
                [0.1, 0.1, 0.1],
                [0.1, 0.1, 0.1],
                [0.0, 0.0, 1.0],
                [0.0, 0.0, 1.0],
                [0.1, 0.1, 0.1],
                [0.1, 0.1, 0.1],
                [0.1, 0.1, 0.1],
                [0.1, 0.1, 0.1],
                [0.1, 0.1, 0.1]
            ])
        )


class TestHeatmapsOnImage_avg_pool(unittest.TestCase):
    def test_with_kernel_size_2(self):
        heatmaps_arr = np.float32([
            [0.0, 0.0, 0.5, 1.0],
            [0.0, 0.0, 0.5, 1.0],
            [0.0, 0.0, 0.5, 1.0],
            [0.0, 0.0, 0.5, 1.0]
        ])
        heatmaps = ia.HeatmapsOnImage(heatmaps_arr, shape=(4, 4, 3))

        heatmaps_pooled = heatmaps.avg_pool(2)
        assert heatmaps_pooled.arr_0to1.shape == (2, 2, 1)
        assert np.allclose(
            heatmaps_pooled.arr_0to1[:, :, 0],
            np.float32([[0.0, 0.75],
                        [0.0, 0.75]])
        )

class TestHeatmapsOnImage_max_pool(unittest.TestCase):
    def test_with_kernel_size_2(self):
        heatmaps_arr = np.float32([
            [0.0, 0.0, 0.5, 1.0],
            [0.0, 0.0, 0.5, 1.0],
            [0.0, 0.0, 0.5, 1.0],
            [0.0, 0.0, 0.5, 1.0]
        ])
        heatmaps = ia.HeatmapsOnImage(heatmaps_arr, shape=(4, 4, 3))

        heatmaps_pooled = heatmaps.max_pool(2)
        assert heatmaps_pooled.arr_0to1.shape == (2, 2, 1)
        assert np.allclose(
            heatmaps_pooled.arr_0to1[:, :, 0],
            np.float32([[0.0, 1.0],
                        [0.0, 1.0]])
        )


class TestHeatmapsOnImage_resize(unittest.TestCase):
    def test_resize_to_exact_shape(self):
        heatmaps_arr = np.float32([
            [0.0, 1.0]
        ])
        heatmaps = ia.HeatmapsOnImage(heatmaps_arr, shape=(4, 4, 3))

        heatmaps_scaled = heatmaps.resize((4, 4), interpolation="nearest")
        assert heatmaps_scaled.arr_0to1.shape == (4, 4, 1)
        assert heatmaps_scaled.arr_0to1.dtype.name == "float32"
        assert np.allclose(
            heatmaps_scaled.arr_0to1[:, :, 0],
            np.float32([
                [0.0, 0.0, 1.0, 1.0],
                [0.0, 0.0, 1.0, 1.0],
                [0.0, 0.0, 1.0, 1.0],
                [0.0, 0.0, 1.0, 1.0]
            ])
        )

    def test_resize_to_twice_the_size(self):
        heatmaps_arr = np.float32([
            [0.0, 1.0]
        ])
        heatmaps = ia.HeatmapsOnImage(heatmaps_arr, shape=(4, 4, 3))

        heatmaps_scaled = heatmaps.resize(2.0, interpolation="nearest")
        assert heatmaps_scaled.arr_0to1.shape == (2, 4, 1)
        assert heatmaps_scaled.arr_0to1.dtype.name == "float32"
        assert np.allclose(
            heatmaps_scaled.arr_0to1[:, :, 0],
            np.float32([
                [0.0, 0.0, 1.0, 1.0],
                [0.0, 0.0, 1.0, 1.0]
            ])
        )


class TestHeatmapsOnImage_from_uint8(unittest.TestCase):
    def test_3d_uint8_array(self):
        hm = ia.HeatmapsOnImage.from_uint8(
            np.uint8([
                [0, 128, 255],
                [255, 128, 0]
            ])[..., np.newaxis],
            (20, 30, 3)
        )
        assert hm.shape == (20, 30, 3)
        assert hm.arr_0to1.shape == (2, 3, 1)
        assert np.allclose(hm.arr_0to1[..., 0], np.float32([
            [0, 128/255, 1.0],
            [1.0, 128/255, 0]
        ]))

    def test_2d_uint8_array(self):
        hm = ia.HeatmapsOnImage.from_uint8(
            np.uint8([
                [0, 128, 255],
                [255, 128, 0]
            ]),
            (20, 30, 3)
        )
        assert hm.shape == (20, 30, 3)
        assert hm.arr_0to1.shape == (2, 3, 1)
        assert np.allclose(hm.arr_0to1[..., 0], np.float32([
            [0, 128/255, 1.0],
            [1.0, 128/255, 0]
        ]))

    def test_min_value_and_max_value(self):
        # min_value, max_value
        hm = ia.HeatmapsOnImage.from_uint8(
            np.uint8([
                [0, 128, 255],
                [255, 128, 0]
            ])[..., np.newaxis],
            (20, 30, 3),
            min_value=-1.0,
            max_value=2.0
        )
        assert hm.shape == (20, 30, 3)
        assert hm.arr_0to1.shape == (2, 3, 1)
        assert np.allclose(hm.arr_0to1[..., 0], np.float32([
            [0, 128/255, 1.0],
            [1.0, 128/255, 0]
        ]))
        assert np.allclose(hm.min_value, -1.0)
        assert np.allclose(hm.max_value, 2.0)


class TestHeatmapsOnImage_change_normalization(unittest.TestCase):
    def test_increase_max_value(self):
        # (0.0, 1.0) -> (0.0, 2.0)
        arr = np.float32([
            [0.0, 0.5, 1.0],
            [1.0, 0.5, 0.0]
        ])

        observed = ia.HeatmapsOnImage.change_normalization(
            arr, (0.0, 1.0), (0.0, 2.0))

        expected = np.float32([
            [0.0, 1.0, 2.0],
            [2.0, 1.0, 0.0]
        ])
        assert np.allclose(observed, expected)

    def test_decrease_min_and_max_value(self):
        # (0.0, 1.0) -> (-1.0, 0.0)
        arr = np.float32([
            [0.0, 0.5, 1.0],
            [1.0, 0.5, 0.0]
        ])

        observed = ia.HeatmapsOnImage.change_normalization(
            arr, (0.0, 1.0), (-1.0, 0.0))

        expected = np.float32([
            [-1.0, -0.5, 0.0],
            [0.0, -0.5, -1.0]
        ])
        assert np.allclose(observed, expected)

    def test_increase_min_and_max_value__non_standard_source(self):
        # (-1.0, 1.0) -> (1.0, 3.0)
        arr = np.float32([
            [-1.0, 0.0, 1.0],
            [1.0, 0.0, -1.0]
        ])

        observed = ia.HeatmapsOnImage.change_normalization(
            arr, (-1.0, 1.0), (1.0, 3.0))

        expected = np.float32([
            [1.0, 2.0, 3.0],
            [3.0, 2.0, 1.0]
        ])
        assert np.allclose(observed, expected)

    def test_value_ranges_given_as_heatmaps_on_image(self):
        # (-1.0, 1.0) -> (1.0, 3.0)
        # value ranges given as HeatmapsOnImage
        arr = np.float32([
            [-1.0, 0.0, 1.0],
            [1.0, 0.0, -1.0]
        ])
        source = ia.HeatmapsOnImage(
            np.float32([[0.0]]), min_value=-1.0, max_value=1.0, shape=(1, 1, 3))
        target = ia.HeatmapsOnImage(
            np.float32([[1.0]]), min_value=1.0, max_value=3.0, shape=(1, 1, 3))

        observed = ia.HeatmapsOnImage.change_normalization(arr, source, target)

        expected = np.float32([
            [1.0, 2.0, 3.0],
            [3.0, 2.0, 1.0]
        ])
        assert np.allclose(observed, expected)
