#!/usr/bin/env python3
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved

from typing import List, Tuple, Dict

import detectron2.data.transforms.augmentation as aug
import numpy as np
from detectron2.config import CfgNode
from detectron2.data.transforms import NoOpTransform, Transform

from .build import TRANSFORM_OP_REGISTRY, _json_load


class LocalizedBoxMotionBlurTransform(Transform):
    """ Transform to blur provided bounding boxes from an image. """

    def __init__(
        self,
        bounding_boxes: List[List[int]],
        k: Tuple[float, float] = (7, 15),
        angle: Tuple[float, float] = (0, 360),
        direction: Tuple[float, float] = (-1.0, 1.0),
    ):
        import imgaug.augmenters as iaa

        super().__init__()
        self._set_attributes(locals())
        self.aug = iaa.MotionBlur(k, angle, direction, 1)

    def apply_image(self, img: np.ndarray) -> np.ndarray:
        bbox_regions = [img[y : y + h, x : x + w] for x, y, w, h in self.bounding_boxes]
        blurred_boxes = self.aug.augment_images(bbox_regions)
        new_img = np.array(img)
        for (x, y, w, h), blurred in zip(self.bounding_boxes, blurred_boxes):
            new_img[y : y + h, x : x + w] = blurred
        return new_img

    def apply_segmentation(self, segmentation: np.ndarray) -> np.ndarray:
        """ Apply no transform on the full-image segmentation. """
        return segmentation

    def apply_coords(self, coords: np.ndarray):
        """ Apply no transform on the coordinates. """
        return coords

    def inverse(self) -> Transform:
        """ The inverse is a No-op, only for geometric transforms. """
        return NoOpTransform()


class LocalizedBoxMotionBlur(aug.Augmentation):
    """
    Performs faked motion blur on bounding box annotations in an image.
    Randomly selects motion blur parameters from the ranges `k`, `angle`, `direction`.
    """

    def __init__(
        self,
        prob: float = 0.5,
        k: Tuple[float, float] = (7, 15),
        angle: Tuple[float, float] = (0, 360),
        direction: Tuple[float, float] = (-1.0, 1.0),
    ):
        super().__init__()
        self._init(locals())

    def _validate_bbox_xywh_within_bounds(
        self, bbox: List[int], img_h: int, img_w: int
    ):
        x, y, w, h = bbox
        assert x >= 0, f"Invalid x {x}"
        assert y >= 0, f"Invalid y {x}"
        assert y + h <= img_h, f"Invalid right {x+w} (img width {img_w})"
        assert y + h <= img_h, f"Invalid bottom {y+h} (img height {img_h})"

    def get_transform(self, image: np.ndarray, annotations: List[Dict]) -> Transform:
        do_tfm = self._rand_range() < self.prob
        if do_tfm:
            return self._get_blur_transform(image, annotations)
        else:
            return NoOpTransform()

    def _get_blur_transform(
        self, image: np.ndarray, annotations: List[Dict]
    ) -> Transform:
        """
        Return a `Transform` that simulates motion blur within the image's bounding box regions.
        """
        img_h, img_w = image.shape[:2]
        bboxes = [ann["bbox"] for ann in annotations]
        # Debug
        for bbox in bboxes:
            self._validate_bbox_xywh_within_bounds(bbox, img_h, img_w)

        return LocalizedBoxMotionBlurTransform(
            bboxes,
            k=self.k,
            angle=self.angle,
            direction=self.direction,
        )


# example repr: "LocalizedBoxMotionBlurOp::{'prob': 0.5, 'k': [3,7], 'angle': [0, 360]}"
@TRANSFORM_OP_REGISTRY.register()
def RandomLocalizedBoxMotionBlurOp(
    cfg: CfgNode, arg_str: str, is_train: bool
) -> List[Transform]:
    assert is_train
    kwargs = _json_load(arg_str) if arg_str is not None else {}
    assert isinstance(kwargs, dict)
    return [LocalizedBoxMotionBlur(**kwargs)]


class MotionBlurTransform(Transform):
    def __init__(
        self,
        k: Tuple[float, float] = (7, 15),
        angle: Tuple[float, float] = (0, 360),
        direction: Tuple[float, float] = (-1.0, 1.0),
    ):
        """
        Args:
           will apply the specified blur to the image
        """
        import imgaug.augmenters as iaa

        super().__init__()
        self._set_attributes(locals())
        self.aug = iaa.MotionBlur(k, angle, direction, 1)

    def apply_image(self, img: np.ndarray) -> np.ndarray:
        img = self.aug.augment_image(img)
        return img

    def apply_segmentation(self, segmentation: np.ndarray) -> np.ndarray:
        return segmentation

    def apply_coords(self, coords: np.ndarray) -> np.ndarray:
        return coords


class RandomMotionBlur(aug.Augmentation):
    """
    Apply random motion blur.
    """

    def __init__(
        self,
        prob: float = 0.5,
        k: Tuple[float, float] = (3, 7),
        angle: Tuple[float, float] = (0, 360),
        direction: Tuple[float, float] = (-1.0, 1.0),
    ):
        """
        Args:
            prob (float): probability of applying transform
            k (tuple): refer to `iaa.MotionBlur`
            angle (tuple): refer to `iaa.MotionBlur`
            direction (tuple): refer to `iaa.MotionBlur`
        """
        super().__init__()
        # Turn all locals into member variables.
        self._init(locals())

    def get_transform(self, img: np.ndarray) -> Transform:
        do = self._rand_range() < self.prob
        if do:
            return MotionBlurTransform(self.k, self.angle, self.direction)
        else:
            return NoOpTransform()


# example repr: "RandomMotionBlurOp::{'prob': 0.5, 'k': [3,7], 'angle': [0, 360]}"
@TRANSFORM_OP_REGISTRY.register()
def RandomMotionBlurOp(cfg: CfgNode, arg_str: str, is_train: bool) -> List[Transform]:
    assert is_train
    kwargs = _json_load(arg_str) if arg_str is not None else {}
    assert isinstance(kwargs, dict)
    return [RandomMotionBlur(**kwargs)]
