# LICENSE HEADER MANAGED BY add-license-header
#
# Copyright 2018 Kornia Team
#
# 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.
#

from typing import Any, List, Optional, Tuple, Union

import torch


def _common_param_check(batch_size: int, same_on_batch: Optional[bool] = None) -> None:
    """Check valid batch_size and same_on_batch params."""
    if not (isinstance(batch_size, int) and batch_size >= 0):
        raise AssertionError(f"`batch_size` shall be a positive integer. Got {batch_size}.")
    if same_on_batch is not None and not isinstance(same_on_batch, bool):
        raise AssertionError(f"`same_on_batch` shall be boolean. Got {same_on_batch}.")


def _range_bound(
    factor: Union[torch.Tensor, float, Tuple[float, float], List[float]],
    name: str,
    center: Optional[float] = 0.0,
    bounds: Optional[Tuple[float, float]] = (0, float("inf")),
    check: Optional[str] = "joint",
    device: Optional[torch.device] = None,
    dtype: Optional[torch.dtype] = None,
) -> torch.Tensor:
    r"""Check inputs and compute the corresponding factor bounds."""
    if device is None:
        device = torch.device("cpu")
    if dtype is None:
        dtype = torch.get_default_dtype()
    if not isinstance(factor, (torch.Tensor)):
        factor = torch.tensor(factor, device=device, dtype=dtype)
    factor_bound: torch.Tensor

    if factor.dim() == 0:
        if factor < 0:
            raise ValueError(f"If {name} is a single number, it must be non negative. Got {factor}.")
        if center is None or bounds is None:
            raise ValueError(f"`center` and `bounds` cannot be None for single number. Got {center}, {bounds}.")
        # Should be something other than clamp
        # Currently, single value factor will not out of scope as long as the user provided it.
        # Note: I personally think throw an error will be better than a coarse clamp.
        factor_bound = factor.repeat(2) * torch.tensor([-1.0, 1.0], device=factor.device, dtype=factor.dtype) + center
        factor_bound = factor_bound.clamp(bounds[0], bounds[1]).to(device=device, dtype=dtype)
    else:
        factor_bound = torch.as_tensor(factor, device=device, dtype=dtype)

    if check is not None:
        if check == "joint":
            _joint_range_check(factor_bound, name, bounds)
        elif check == "singular":
            _singular_range_check(factor_bound, name, bounds)
        else:
            raise NotImplementedError(f"methods '{check}' not implemented.")

    return factor_bound


def _joint_range_check(ranged_factor: torch.Tensor, name: str, bounds: Optional[Tuple[float, float]] = None) -> None:
    """Check if bounds[0] <= ranged_factor[0] <= ranged_factor[1] <= bounds[1]."""
    if bounds is None:
        bounds = (float("-inf"), float("inf"))
    if ranged_factor.dim() == 1 and len(ranged_factor) == 2:
        if not bounds[0] <= ranged_factor[0] or not bounds[1] >= ranged_factor[1]:
            raise ValueError(f"{name} out of bounds. Expected inside {bounds}, got {ranged_factor}.")

        if not bounds[0] <= ranged_factor[0] <= ranged_factor[1] <= bounds[1]:
            raise ValueError(f"{name}[0] should be smaller than {name}[1] got {ranged_factor}")
    else:
        raise TypeError(
            f"{name} should be a torch.Tensor with length 2 whose values between {bounds}. Got {ranged_factor}."
        )


def _singular_range_check(
    ranged_factor: torch.Tensor,
    name: str,
    bounds: Optional[Tuple[float, float]] = None,
    skip_none: bool = False,
    mode: str = "2d",
) -> None:
    """Check if bounds[0] <= ranged_factor[0] <= bounds[1] and bounds[0] <= ranged_factor[1] <= bounds[1]."""
    if mode == "2d":
        dim_size = 2
    elif mode == "3d":
        dim_size = 3
    else:
        raise ValueError(f"'mode' shall be either 2d or 3d. Got {mode}")

    if skip_none and ranged_factor is None:
        return
    if bounds is None:
        bounds = (float("-inf"), float("inf"))
    if ranged_factor.dim() == 1 and len(ranged_factor) == dim_size:
        for f in ranged_factor:
            if not bounds[0] <= f <= bounds[1]:
                raise ValueError(f"{name} out of bounds. Expected inside {bounds}, got {ranged_factor}.")
    else:
        raise TypeError(
            f"{name} should be a float number or a tuple with length {dim_size} whose values between {bounds}."
            f"Got {ranged_factor}"
        )


def _tuple_range_reader(
    input_range: Union[torch.Tensor, float, Tuple[Any, ...]],
    target_size: int,
    device: Optional[torch.device] = None,
    dtype: Optional[torch.dtype] = None,
) -> torch.Tensor:
    """Given target_size, it will generate the corresponding (target_size, 2) range torch.Tensor.

    This is for element-wise params.

    Example:
    >>> degree = torch.tensor([0.2, 0.3])
    >>> _tuple_range_reader(degree, 3)  # read degree for yaw, pitch and roll.
    tensor([[0.2000, 0.3000],
            [0.2000, 0.3000],
            [0.2000, 0.3000]])

    """
    target_shape = torch.Size([target_size, 2])

    if isinstance(input_range, torch.Tensor):
        if (len(input_range.shape) == 0) or (len(input_range.shape) == 1 and len(input_range) == 1):
            if input_range < 0:
                raise ValueError(f"If input_range is only one number it must be a positive number. Got{input_range}")
            input_range_tmp = input_range.repeat(2).to(device=device, dtype=dtype) * torch.tensor(
                [-1, 1], device=device, dtype=dtype
            )
            input_range_tmp = input_range_tmp.repeat(target_shape[0], 1)

        elif len(input_range.shape) == 1 and len(input_range) == 2:
            input_range_tmp = input_range.repeat(target_shape[0], 1).to(device=device, dtype=dtype)

        elif len(input_range.shape) == 1 and len(input_range) == target_shape[0]:
            input_range_tmp = input_range.unsqueeze(1).repeat(1, 2).to(device=device, dtype=dtype) * torch.tensor(
                [-1, 1], device=device, dtype=dtype
            )

        elif input_range.shape == target_shape:
            input_range_tmp = input_range.to(device=device, dtype=dtype)

        else:
            raise ValueError(
                f"Degrees must be a {list(target_shape)} torch.tensor for the degree range for independent operation."
                f"Got {input_range}"
            )
    elif isinstance(input_range, (float, int)):
        if input_range < 0:
            raise ValueError(f"If input_range is only one number it must be a positive number. Got{input_range}")
        input_range_tmp = torch.tensor([-input_range, input_range], device=device, dtype=dtype).repeat(
            target_shape[0], 1
        )

    elif (
        isinstance(input_range, (tuple, list))
        and len(input_range) == 2
        and isinstance(input_range[0], (float, int))
        and isinstance(input_range[1], (float, int))
    ):
        input_range_tmp = torch.tensor(input_range, device=device, dtype=dtype).repeat(target_shape[0], 1)

    elif (
        isinstance(input_range, (tuple, list))
        and len(input_range) == target_shape[0]
        and all(isinstance(x, (float, int)) for x in input_range)
    ):
        input_range_tmp = torch.tensor([(-s, s) for s in input_range], device=device, dtype=dtype)

    elif (
        isinstance(input_range, (tuple, list))
        and len(input_range) == target_shape[0]
        and all(isinstance(x, (tuple, list)) for x in input_range)
    ):
        input_range_tmp = torch.tensor(input_range, device=device, dtype=dtype)

    else:
        raise TypeError(
            "If not pass a torch.tensor, it must be float, (float, float) for isotropic operation or a tuple of "
            f"{target_size} floats or {target_size} (float, float) for independent operation. Got {input_range}."
        )

    return input_range_tmp
