# Copyright 2024 HuggingFace Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import unittest

import numpy as np
import pytest

from transformers.testing_utils import require_torch, require_torch_accelerator, require_vision
from transformers.utils import is_torch_available, is_vision_available

from ...test_image_processing_common import (
    ImageProcessingTester,
    ImageProcessingTestMixin,
    load_coco_image,
)


if is_torch_available():
    import torch


if is_vision_available():
    from PIL import Image


class VitPoseImageProcessingTester(ImageProcessingTester):
    def __init__(self, **kwargs):
        # Image processor init kwargs
        kwargs.setdefault("size", {"height": 20, "width": 20})

        super().__init__(**kwargs)


@require_torch
@require_vision
class VitPoseImageProcessingTest(ImageProcessingTestMixin, unittest.TestCase):
    image_processor_tester_class = VitPoseImageProcessingTester

    def test_call_pil(self):
        for image_processing_class in self.image_processing_classes.values():
            image_processing = image_processing_class(**self.image_processor_dict)
            # create random PIL images
            image_inputs = self.image_processor_tester.prepare_image_inputs(equal_resolution=False)
            for image in image_inputs:
                self.assertIsInstance(image, Image.Image)

            # Test not batched input
            boxes = [[[0, 0, 1, 1], [0.5, 0.5, 0.5, 0.5]]]
            encoded_images = image_processing(image_inputs[0], boxes=boxes, return_tensors="pt").pixel_values
            expected_output_image_shape = self.image_processor_tester.expected_output_image_shape([image_inputs[0]])
            self.assertEqual(tuple(encoded_images.shape), (2, *expected_output_image_shape))

            # Test batched
            boxes = [[[0, 0, 1, 1], [0.5, 0.5, 0.5, 0.5]]] * self.image_processor_tester.batch_size
            encoded_images = image_processing(image_inputs, boxes=boxes, return_tensors="pt").pixel_values
            expected_output_image_shape = self.image_processor_tester.expected_output_image_shape(image_inputs)
            self.assertEqual(
                tuple(encoded_images.shape), (self.image_processor_tester.batch_size * 2, *expected_output_image_shape)
            )

    def test_call_numpy(self):
        for image_processing_class in self.image_processing_classes.values():
            image_processing = image_processing_class(**self.image_processor_dict)
            # create random numpy tensors
            image_inputs = self.image_processor_tester.prepare_image_inputs(equal_resolution=False, numpify=True)
            for image in image_inputs:
                self.assertIsInstance(image, np.ndarray)

            # Test not batched input
            boxes = [[[0, 0, 1, 1], [0.5, 0.5, 0.5, 0.5]]]
            encoded_images = image_processing(image_inputs[0], boxes=boxes, return_tensors="pt").pixel_values
            expected_output_image_shape = self.image_processor_tester.expected_output_image_shape([image_inputs[0]])
            self.assertEqual(tuple(encoded_images.shape), (2, *expected_output_image_shape))

            # Test batched
            boxes = [[[0, 0, 1, 1], [0.5, 0.5, 0.5, 0.5]]] * self.image_processor_tester.batch_size
            encoded_images = image_processing(image_inputs, boxes=boxes, return_tensors="pt").pixel_values
            expected_output_image_shape = self.image_processor_tester.expected_output_image_shape(image_inputs)
            self.assertEqual(
                tuple(encoded_images.shape), (self.image_processor_tester.batch_size * 2, *expected_output_image_shape)
            )

    def test_call_pytorch(self):
        for image_processing_class in self.image_processing_classes.values():
            image_processing = image_processing_class(**self.image_processor_dict)
            # create random PyTorch tensors
            image_inputs = self.image_processor_tester.prepare_image_inputs(equal_resolution=False, torchify=True)

            for image in image_inputs:
                self.assertIsInstance(image, torch.Tensor)

            # Test not batched input
            boxes = [[[0, 0, 1, 1], [0.5, 0.5, 0.5, 0.5]]]
            encoded_images = image_processing(image_inputs[0], boxes=boxes, return_tensors="pt").pixel_values
            expected_output_image_shape = self.image_processor_tester.expected_output_image_shape([image_inputs[0]])
            self.assertEqual(tuple(encoded_images.shape), (2, *expected_output_image_shape))

            # Test batched
            boxes = [[[0, 0, 1, 1], [0.5, 0.5, 0.5, 0.5]]] * self.image_processor_tester.batch_size
            encoded_images = image_processing(image_inputs, boxes=boxes, return_tensors="pt").pixel_values
            expected_output_image_shape = self.image_processor_tester.expected_output_image_shape(image_inputs)
            self.assertEqual(
                tuple(encoded_images.shape), (self.image_processor_tester.batch_size * 2, *expected_output_image_shape)
            )

    def test_call_numpy_4_channels(self):
        for image_processing_class in self.image_processing_classes.values():
            image_processor = image_processing_class(**self.image_processor_dict)

            # create random numpy tensors
            self.image_processor_tester.num_channels = 4
            image_inputs = self.image_processor_tester.prepare_image_inputs(equal_resolution=False, numpify=True)
            # Test not batched input
            boxes = [[[0, 0, 1, 1], [0.5, 0.5, 0.5, 0.5]]]
            encoded_images = image_processor(
                image_inputs[0],
                boxes=boxes,
                return_tensors="pt",
                input_data_format="channels_last",
                image_mean=(0.0, 0.0, 0.0, 0.0),
                image_std=(1.0, 1.0, 1.0, 1.0),
            ).pixel_values
            expected_output_image_shape = self.image_processor_tester.expected_output_image_shape([image_inputs[0]])
            self.assertEqual(tuple(encoded_images.shape), (len(boxes[0]), *expected_output_image_shape))

            # Test batched
            boxes = [[[0, 0, 1, 1], [0.5, 0.5, 0.5, 0.5]]] * self.image_processor_tester.batch_size
            encoded_images = image_processor(
                image_inputs,
                boxes=boxes,
                return_tensors="pt",
                input_data_format="channels_last",
                image_mean=(0.0, 0.0, 0.0, 0.0),
                image_std=(1.0, 1.0, 1.0, 1.0),
            ).pixel_values
            expected_output_image_shape = self.image_processor_tester.expected_output_image_shape(image_inputs)
            self.assertEqual(
                tuple(encoded_images.shape),
                (self.image_processor_tester.batch_size * len(boxes[0]), *expected_output_image_shape),
            )
            self.image_processor_tester.num_channels = 3

    @require_vision
    @require_torch
    def test_backends_equivalence(self):
        """VitPose requires boxes parameter for preprocessing."""
        if len(self.image_processing_classes) < 2:
            self.skipTest(reason="Skipping backends equivalence test as there are less than 2 backends")

        dummy_image = load_coco_image("000000039769.jpg")
        boxes = [[[0, 0, 1, 1]]]

        encodings = {}
        for backend_name, image_processing_class in self.image_processing_classes.items():
            image_processor = image_processing_class(**self.image_processor_dict)
            encodings[backend_name] = image_processor(dummy_image, boxes=boxes, return_tensors="pt")

        backend_names = list(encodings.keys())
        reference_encoding = encodings[backend_names[0]].pixel_values
        for backend_name in backend_names[1:]:
            self._assert_tensors_equivalence(reference_encoding, encodings[backend_name].pixel_values)

    @require_vision
    @require_torch
    def test_backends_equivalence_batched(self):
        """VitPose requires boxes parameter for batched preprocessing."""
        if len(self.image_processing_classes) < 2:
            self.skipTest(reason="Skipping backends equivalence test as there are less than 2 backends")

        dummy_images = self.image_processor_tester.prepare_image_inputs(equal_resolution=False, torchify=True)
        boxes = [[[0, 0, 1, 1]]] * len(dummy_images)

        encodings = {}
        for backend_name, image_processing_class in self.image_processing_classes.items():
            image_processor = image_processing_class(**self.image_processor_dict)
            encodings[backend_name] = image_processor(dummy_images, boxes=boxes, return_tensors="pt")

        backend_names = list(encodings.keys())
        reference_encoding = encodings[backend_names[0]].pixel_values
        for backend_name in backend_names[1:]:
            self._assert_tensors_equivalence(reference_encoding, encodings[backend_name].pixel_values)

    @require_torch_accelerator
    @require_vision
    @pytest.mark.torch_compile_test
    def test_can_compile_torchvision_backend(self):
        """VitPose requires boxes parameter for preprocessing."""
        from transformers.testing_utils import torch_device

        if "torchvision" not in self.image_processing_classes:
            self.skipTest("Skipping compilation test as torchvision backend is not available")

        torch.compiler.reset()
        input_image = torch.randint(0, 255, (3, 224, 224), dtype=torch.uint8)
        image_processor = self.image_processing_classes["torchvision"](**self.image_processor_dict)
        boxes = [[[0, 0, 1, 1]]]
        output_eager = image_processor(input_image, boxes=boxes, device=torch_device, return_tensors="pt")

        image_processor = torch.compile(image_processor, mode="reduce-overhead")
        output_compiled = image_processor(input_image, boxes=boxes, device=torch_device, return_tensors="pt")
        self._assert_tensors_equivalence(
            output_eager.pixel_values, output_compiled.pixel_values, atol=1e-4, rtol=1e-4, mean_atol=1e-5
        )
