#                🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
#           This file was automatically generated from src/transformers/models/neomme/modular_neomme.py.
#               Do NOT edit this file manually as any edits will be overwritten by the generation of
#             the file from the modular. If any change should be done, please apply the change to the
#                          modular_neomme.py file directly. One of our CI enforces this.
#                🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
# Copyright 2026 H Company and the HuggingFace Inc. team. All rights reserved.
#
# 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 math
from typing import Annotated

import torch
from torchvision.transforms.v2 import functional as tvF

from ...image_processing_backends import TorchvisionBackend
from ...image_processing_utils import BatchFeature
from ...image_transforms import group_images_by_shape, reorder_images
from ...image_utils import ImageInput, PILImageResampling, SizeDict
from ...processing_utils import ImagesKwargs, Unpack
from ...utils import TensorType, auto_docstring
from ...utils.constants import IMAGENET_STANDARD_MEAN, IMAGENET_STANDARD_STD
from ...utils.type_validators import positive_int


class NeoMMEImageProcessorKwargs(ImagesKwargs, total=False):
    r"""
    patch_size (`int`, *optional*, defaults to `self.patch_size`):
        Side, in pixels, of one patch token. The image is padded to a whole multiple of it.
    max_side (`int`, *optional*):
        Longest-side cap in pixels. Unset means no longest-side resize.
    """

    patch_size: Annotated[int, positive_int()]
    max_side: Annotated[int | None, positive_int()]


def convert_image_to_patches(images: "torch.Tensor", patch_size: int) -> "torch.Tensor":
    """
    Convert 3D array image of shape (image_height, image_width, num_channels) into 2D array of patches of shape
    (num_patches_height * num_patches_width, patch_size * patch_size * num_channels).
    """
    batch_size, num_channels, image_height, image_width = images.shape
    num_patches_height = image_height // patch_size
    num_patches_width = image_width // patch_size
    patched_image = images.reshape(
        batch_size, num_channels, num_patches_height, patch_size, num_patches_width, patch_size
    )
    patched_image = patched_image.permute(0, 2, 4, 3, 5, 1)
    patched_image = patched_image.reshape(batch_size, num_patches_height * num_patches_width, -1)
    return patched_image


def get_resize_output_size(height: int, width: int, max_side: int | None, size: SizeDict | None) -> tuple[int, int]:
    """Compute integer height and width from the configured image size targets."""
    min_pixels = size.min_pixels if size is not None else None
    max_pixels = size.max_pixels if size is not None else None
    scale = (min_pixels / (height * width)) ** 0.5 if min_pixels is not None and height * width < min_pixels else 1.0
    if max_side is not None:
        scale = min(scale, max_side / max(height, width))
    if max_pixels is not None:
        scale = min(scale, (max_pixels / (height * width)) ** 0.5)
    if scale == 1.0:
        return height, width

    resized_height = max(1, round(height * scale))
    resized_width = max(1, round(width * scale))
    exceeds_cap = (max_side is not None and max(resized_height, resized_width) > max_side) or (
        max_pixels is not None and resized_height * resized_width > max_pixels
    )
    if exceeds_cap:
        resized_height = max(1, math.floor(height * scale))
        resized_width = max(1, math.floor(width * scale))
        if max_pixels is not None and resized_height * resized_width > max_pixels:
            if resized_height >= resized_width:
                resized_height = max(1, max_pixels // resized_width)
            else:
                resized_width = max(1, max_pixels // resized_height)
        return resized_height, resized_width

    if min_pixels is not None and height * width < min_pixels:
        if resized_height * resized_width >= min_pixels:
            return resized_height, resized_width
        ceiled_height = max(1, math.ceil(height * scale))
        ceiled_width = max(1, math.ceil(width * scale))
        exceeds_cap = (max_side is not None and max(ceiled_height, ceiled_width) > max_side) or (
            max_pixels is not None and ceiled_height * ceiled_width > max_pixels
        )
        if not exceeds_cap:
            return ceiled_height, ceiled_width

    return resized_height, resized_width


@auto_docstring
class NeoMMEImageProcessor(TorchvisionBackend):
    r"""
    Constructs an image processor for NeoMME.

    The processor converts each image to RGB and splits it into row-major patches. It preserves the original size by
    default, but `max_side` or a pixel-area `size` can resize images. It pads the bottom and right edges to complete
    the final patch. A batch returns one concatenated patch tensor and one grid height and width pair per image.
    """

    valid_kwargs = NeoMMEImageProcessorKwargs

    resample = PILImageResampling.BILINEAR
    image_mean = IMAGENET_STANDARD_MEAN
    image_std = IMAGENET_STANDARD_STD
    do_convert_rgb = True
    do_resize = True
    do_rescale = True
    rescale_factor = 1 / 255
    do_normalize = True
    patch_size = 32
    # Checkpoints may set resize limits; unset limits preserve native resolution.
    max_side = None
    size = None

    model_input_names = ["pixel_values", "image_grid_hw"]

    def __init__(self, **kwargs: Unpack[NeoMMEImageProcessorKwargs]):
        super().__init__(**kwargs)

    @auto_docstring
    def preprocess(self, images: ImageInput, **kwargs: Unpack[NeoMMEImageProcessorKwargs]) -> BatchFeature:
        r"""
        Returns:
            [`BatchFeature`] with `pixel_values` of shape `(total_patches, 3 * patch_size ** 2)` and
            `image_grid_hw` of shape `(batch_size, 2)`. `pixel_values` concatenates patches from every image in the
            batch.
        """
        return super().preprocess(images, **kwargs)

    def _validate_preprocess_kwargs(self, **kwargs) -> tuple:
        size = kwargs["size"]
        if size is not None and set(dict(size)) != {"min_pixels", "max_pixels"}:
            raise ValueError("size must contain exactly min_pixels and max_pixels.")

        if kwargs["size"] is None:
            # Generic resize validation requires `size`; NeoMME can resize from `max_side` alone.
            kwargs.pop("do_resize", None)
        return super()._validate_preprocess_kwargs(**kwargs)

    def _resize_to_budget(
        self,
        image: "torch.Tensor",
        max_side: int | None,
        size: SizeDict | None,
        resample: "PILImageResampling | tvF.InterpolationMode | int | None",
    ) -> "torch.Tensor":
        height, width = image.shape[-2], image.shape[-1]
        resized_height, resized_width = get_resize_output_size(height, width, max_side, size)
        if (resized_height, resized_width) == (height, width):
            return image
        size = SizeDict(height=resized_height, width=resized_width)
        return self.resize(image=image, size=size, resample=resample, antialias=True)

    def _pad_to_patch_grid(self, image: "torch.Tensor", patch_size: int) -> tuple["torch.Tensor", int, int]:
        height, width = image.shape[-2], image.shape[-1]
        grid_height, grid_width = -(-height // patch_size), -(-width // patch_size)
        pad_height = grid_height * patch_size - height
        pad_width = grid_width * patch_size - width
        if pad_height or pad_width:
            image = tvF.pad(image, [0, 0, pad_width, pad_height], fill=0)
        # image: (num_channels, grid_height * patch_size, grid_width * patch_size)
        return image, grid_height, grid_width

    def _preprocess(
        self,
        images: list["torch.Tensor"],
        do_resize: bool,
        patch_size: int,
        max_side: int | None,
        size: SizeDict | None,
        resample: "PILImageResampling | tvF.InterpolationMode | int | None",
        do_rescale: bool,
        rescale_factor: float,
        do_normalize: bool,
        image_mean: float | list[float] | None,
        image_std: float | list[float] | None,
        disable_grouping: bool | None,
        return_tensors: str | TensorType | None,
        **kwargs,
    ) -> BatchFeature:
        grouped_images, grouped_images_index = group_images_by_shape(images, disable_grouping=disable_grouping)
        resized_images_grouped = {}
        for shape, stacked_images in grouped_images.items():
            if do_resize:
                stacked_images = self._resize_to_budget(stacked_images, max_side, size, resample)
            resized_images_grouped[shape] = stacked_images
        resized_images = reorder_images(resized_images_grouped, grouped_images_index)

        grouped_images, grouped_images_index = group_images_by_shape(resized_images, disable_grouping=disable_grouping)
        processed_images_grouped = {}
        image_grids_grouped = {}
        for shape, stacked_images in grouped_images.items():
            # Pad to a whole patch grid before rescaling, so padded pixels become -1 exactly like the
            # black canvas the reference implementation pastes onto.
            stacked_images, grid_height, grid_width = self._pad_to_patch_grid(stacked_images, patch_size)
            stacked_images = self.rescale_and_normalize(
                stacked_images, do_rescale, rescale_factor, do_normalize, image_mean, image_std
            )
            processed_images_grouped[shape] = convert_image_to_patches(stacked_images, patch_size)
            image_grids_grouped[shape] = torch.tensor(
                [[grid_height, grid_width]] * len(stacked_images), dtype=torch.int64
            )

        pixel_values = reorder_images(processed_images_grouped, grouped_images_index)
        image_grid_hw = reorder_images(image_grids_grouped, grouped_images_index)
        return BatchFeature(
            data={
                "pixel_values": torch.cat(pixel_values, dim=0),  # (total_patches, patch_dim)
                "image_grid_hw": torch.stack(image_grid_hw),  # (images, 2)
            },
            tensor_type=return_tensors,
        )

    def get_number_of_image_patches(self, height: int, width: int, images_kwargs=None) -> int:
        """
        Return the number of image patches for one image, excluding row markers.

        The method applies the effective resize settings, then rounds each image dimension up to a whole patch.
        Values in `images_kwargs` override the processor settings.
        """
        images_kwargs = images_kwargs or {}
        patch_size = images_kwargs.get("patch_size", self.patch_size)
        max_side = images_kwargs.get("max_side", self.max_side)
        size = self._standardize_kwargs(size=images_kwargs.get("size", self.size))["size"]

        if images_kwargs.get("do_resize", self.do_resize):
            height, width = get_resize_output_size(height, width, max_side, size)

        return -(-height // patch_size) * (-(-width // patch_size))


__all__ = ["NeoMMEImageProcessor"]
