#    _____           ______  _____
#  / ____/    /\    |  ____ |  __ \
# | |        /  \   | |__   | |__) | Caer - Modern Computer Vision
# | |       / /\ \  |  __|  |  _  /  Languages: Python, C, C++, Cuda
# | |___   / ____ \ | |____ | | \ \  http://github.com/jasmcaus/caer
#  \_____\/_/    \_ \______ |_|  \_\

# Licensed under the MIT License <http://opensource.org/licenses/MIT>
# SPDX-License-Identifier: MIT
# Copyright (c) 2020-2021 The Caer Authors <http://github.com/jasmcaus>


import math
import cv2 as cv

from ..coreten import Tensor, to_tensor
from .._internal import _check_target_size
from ..globals import (
    INTER_AREA, INTER_CUBIC, INTER_NEAREST, INTER_LINEAR
)
from typing import Tuple, Optional, Union

__all__ = [
    "resize"
]


def resize(
    tens: Tensor,
    target_size: Optional[Tuple[int, int]] = None,
    resize_factor: Optional[Union[float, Tuple]] = None,
    preserve_aspect_ratio: bool = False,
    interpolation: str = "bilinear",
) -> Tensor:
    r"""
        Resizes an image to a target_size without aspect ratio distortion.

        Your output images will be of size ``target_size``, and will not be distorted. Instead, the parts of the image that do not fit within the target size get cropped out.

        The resizing process is:
        1. Resize the image as minimally as possible.
        2. Take the largest centered crop of the image with dimensions = ``target_size``.

        Alternatively, you may use:
        ```python
        size = (200,200)
        tens = caer.resize(tens, target_size=size, preserve_aspect_ratio=True)
        ```

        Note:
            ``caer.imread()`` comes with an in-built functionality to resize your images, eliminating the need for you to call ``caer.resize()``. This is purely optional and may appeal to certain users.

            You may also use ``caer.smart_resize()`` for on-the-fly image resizing that `preserves the aspect ratio`.


        Args:
            tens (Tensor): Input Image. Must be in the format ``(height, width, channels)``.
            target_size (tuple): Target size. Must be a tuple of ``(width, height)`` integer.
            resize_factor (float, tuple): Resizing Factor to employ.
                Shrinks the image if ``resize_factor < 1``
                Enlarges the image if ``resize_factor > 1``
            preserve_aspect_ratio (bool): Prevent aspect ratio distortion (employs center crop).
            interpolation (str): Interpolation to use for resizing. Defaults to `'bilinear'`.
                Supports `'bilinear'`, `'bicubic'`, `'area'`, `'nearest'`.


        Returns:
        Tensor of shape ``(height, width, channels)``.


        Examples::

            >> tens = caer.data.sunrise()
            >> tens.shape
            (427, 640, 3)

            >> resized = caer.resize(tens, target_size=(200,200)) # Hard-resize. May distort aspect ratio
            >> resized.shape
            (200, 200, 3)

            >> resized_wf = caer.resize(tens, resize_factor=.5) # Resizes the image to half its dimensions
            >> resized_wf.shape
            (213, 320, 3)

            >> resized = caer.resize(tens, target_size=(200,200), preserve_aspect_ratio=True) # Preserves aspect ratio
            >> resized.shape
            (200, 200, 3)

    """
    # Opencv uses the (h,w) format
    height, width = tens.shape[:2]
    interpolation = str(interpolation)
    cspace = None

    if isinstance(tens, Tensor):
        # We'll need to preserve this before returning
        cspace = tens.cspace

    if resize_factor is None:
        if target_size is None:
            if preserve_aspect_ratio:
                raise ValueError("Specify a target size")
            else:
                raise ValueError("Specify either a resize factor or target dimensions")
        if target_size is not None:
            if len(target_size) == 2:
                new_shape = target_size
            else:
                raise ValueError("Tuple shape must be = 2 (width, height)")

    if resize_factor is not None:
        target_size = None
        preserve_aspect_ratio = False

        if not isinstance(resize_factor, (int, float)):
            raise ValueError("resize_factor must be an integer or float")

        if resize_factor > 1:
            interpolation = 'bicubic'

        new_shape = (int(resize_factor * width), int(resize_factor * height))

    interpolation_methods = {
        "nearest": INTER_NEAREST,  "0": INTER_NEAREST,
        "bilinear": INTER_LINEAR,  "1": INTER_LINEAR,
        "bicubic": INTER_CUBIC,    "2": INTER_CUBIC,
        "area": INTER_AREA,        "3": INTER_AREA,
    }

    if interpolation not in interpolation_methods:
        raise ValueError("Specify a valid interpolation type - area/nearest/bicubic/bilinear")

    if preserve_aspect_ratio:
        img = _resize_with_ratio(
                tens,
                target_size=target_size, # type: ignore[arg-type]
                preserve_aspect_ratio=preserve_aspect_ratio,
                interpolation=interpolation_methods[interpolation]
            )
    else:
        width, height = new_shape[:2]
        img = _cv2_resize(tens, (width, height), interpolation=interpolation_methods[interpolation])

    # For this function, the <cspace> attribute is not required.
    # So, we disable the mandatory check that the <cspace> attribute needs to be passed for
    # foreign Tensors/ndarrays
    return to_tensor(img, cspace=cspace, override_checks=True)


def smart_resize(tens: Tensor, target_size: Tuple[int, int], interpolation="bilinear") -> Tensor:
    r"""
        Resizes an image to a target_size without aspect ratio distortion.

        Your output images will be of size `target_size`, and will not be distorted. Instead, the parts of the image that do not fit within the target size get cropped out.

        The resizing process is:
        1. Resize the image as minimally as possible.
        2. Take the largest centered crop of the image with dimensions = `target_size`.

        Alternatively, you may use:
        ```python
        size = (200,200)
        tens = caer.resize(tens, target_size=size, preserve_aspect_ratio=True)
        ```

        Args:
            tens (Tensor): Input Image. Must be in the format `(height, width, channels)`.
            target_size (tuple): Target size. Must be a tuple of `(width, height)` integer.
            interpolation (str): Interpolation to use for resizing. Defaults to `'bilinear'`.
                Supports `'bilinear'`, `'bicubic'`, `'area'`, `'nearest'`.

        Returns:
            Tensor of shape `(height, width, channels)`

        Examples::

            >> tens = caer.data.sunrise()
            >> tens.shape
            (427, 640, 3)
            >> resized = caer.smart_resize(tens, target_size=(200,200))
            >> resized.shape
            (200, 200, 3)
    """
    # if not isinstance(tens, Tensor):
    #     raise ValueError("To use `caer.smart_resize()`, `tens` needs to be a caer.Tensor")

    im = _resize_with_ratio(tens, target_size=target_size, preserve_aspect_ratio=True, interpolation=interpolation)

    # For this function, the <cspace> attribute is not required.
    # So, we disable the mandatory check that the <cspace> attribute needs to be passed for
    # foreign Tensors/ndarrays
    return to_tensor(im, override_checks=True)


def _cv2_resize(image, target_size, interpolation=None):
    """
    ONLY TO BE USED INTERNALLY. NOT AVAILABLE FOR EXTERNAL USAGE.
    Resizes the image ignoring the aspect ratio of the original image
    """
    _check_target_size(target_size)

    width, height = target_size[:2]

    if interpolation is None:
        interpolation = INTER_AREA

    dimensions = (width, height)

    return cv.resize(image, dimensions, interpolation=interpolation)


def _resize_with_ratio(
    tens: Tensor, 
    target_size: Tuple[int, int], 
    preserve_aspect_ratio: bool = False, 
    interpolation: int = INTER_LINEAR
) -> Tensor:
    """
        Resizes an image using advanced algorithms
        :param target_size: Tuple of size 2 in the format (width,height)
        :param preserve_aspect_ratio: Boolean to keep/ignore aspect ratio when resizing
    """
    _check_target_size(target_size)

    if not isinstance(preserve_aspect_ratio, bool):
        raise ValueError("preserve_aspect_ratio must be a boolean")

    oh, ow = tens.shape[:2]
    target_w, target_h = target_size

    if target_h > oh or target_w > ow:
        raise ValueError(
            "To compute resizing keeping the aspect ratio, the target size dimensions "
            "must be <= actual image dimensions"
        )

    # Computing minimal resize
    # min_width, w_factor = _compute_minimal_resize(ow, target_w)
    # min_height, h_factor = _compute_minimal_resize(oh, target_h)
    minimal_resize_factor = _compute_minimal_resize((ow, oh), (target_w, target_h))

    # Resizing minimally
    tens = _cv2_resize(tens, (ow // minimal_resize_factor, oh // minimal_resize_factor))

    # Computing centre crop (to avoid extra crop, we resize minimally first)
    tens = _compute_centre_crop(tens, (target_w, target_h))

    if tens.shape[:2] != target_size[:2]:
        tens = _cv2_resize(tens, (target_w, target_h), interpolation=interpolation)

    return tens


def _compute_minimal_resize(org_size: Tuple[int, int], target_dim: Tuple[int, int]) -> int:
    # for i in range(10):
    #     i += 1
    #     d = dim*i
    #     if org_dim >= d and dim < dim*(i+1):
    #         if (org_dim - dim*(i+1)) > dim:
    #             continue
    #         else:
    #             return d, i
    # import math
    # mi = math.floor(org_dim/dim)
    # d = dim * mi
    # return d, mi

    if not isinstance(org_size, tuple) or not isinstance(target_dim, tuple):
        raise ValueError("org_size and target_dim must be a tuple")

    if len(org_size) != 2 or len(target_dim) != 2:
        raise ValueError("Size of tuple must be = 2")

    oh, ow = org_size[:2]
    targ_w, targ_h = target_dim[:2]

    h_factor = math.floor(oh / targ_h)
    w_factor = math.floor(ow / targ_w)

    if h_factor <= w_factor:
        return h_factor
    else:
        return w_factor


def _compute_centre_crop(tens: Tensor, target_size: Tuple[int, int]) -> Tensor:
    _check_target_size(target_size)

    # Getting org height and target
    oh, ow = tens.shape[:2]
    target_w, target_h = target_size

    # The following line is actually the right way of accessing height and width of an opencv-specific image (height, width). However for some reason, while the code runs, this is flipped (it now becomes (width,height)). Testing needs to be done to catch this little bug
    # oh, ow = tens.shape[:2]

    if target_h > oh or target_w > ow:
        raise ValueError("To compute centre crop, target size dimensions must be <= tens dimensions")

    diff_h = (oh - target_h) // 2
    diff_w = (ow - target_w) // 2

    # tens[y:y+h, x:x+h]
    return tens[diff_h : diff_h + target_h, diff_w : diff_w + target_w]
