"""_misc"""
import keras
from .. import backend
from ..utils import anchors as utils_anchors

import numpy as np


class Anchors(keras.layers.Layer):
    """ Keras layer for generating achors for a given shape.
    """

    def __init__(self, size, stride, ratios=None,
                 scales=None, *args, **kwargs):
        """ Initializer for an Anchors layer.

        Parameters
        ----------
        size : int
            The base size of the anchors to generate.
        stride : int
            The stride of the anchors to generate.
        ratios : float
            The ratios of the anchors to generate (defaults to 
            AnchorParameters.default.ratios).
        scales: float
            The scales of the anchors to generate (defaults to 
            AnchorParameters.default.scales).
        """
        self.size = size
        self.stride = stride
        self.ratios = ratios
        self.scales = scales

        if ratios is None:
            self.ratios = utils_anchors.AnchorParameters.default.ratios
        elif isinstance(ratios, list):
            self.ratios = np.array(ratios)
        if scales is None:
            self.scales = utils_anchors.AnchorParameters.default.scales
        elif isinstance(scales, list):
            self.scales = np.array(scales)

        self.num_anchors = len(ratios) * len(scales)
        self.anchors = keras.backend.variable(utils_anchors.generate_anchors(
            base_size=size,
            ratios=ratios,
            scales=scales,
        ))

        super(Anchors, self).__init__(*args, **kwargs)

    def call(self, inputs, **kwargs):
        """call
        """
        features = inputs
        features_shape = keras.backend.shape(features)

        # generate proposals from bbox deltas and shifted anchors
        if keras.backend.image_data_format() == 'channels_first':
            anchors = backend.shift(
                features_shape[2:4], self.stride, self.anchors)
        else:
            anchors = backend.shift(
                features_shape[1:3], self.stride, self.anchors)
        anchors = keras.backend.tile(
            keras.backend.expand_dims(
                anchors, axis=0), (features_shape[0], 1, 1))

        return anchors

    def compute_output_shape(self, input_shape):
        """
        compute output shape
        """
        if None not in input_shape[1:]:
            if keras.backend.image_data_format() == 'channels_first':
                total = np.prod(input_shape[2:4]) * self.num_anchors
            else:
                total = np.prod(input_shape[1:3]) * self.num_anchors

            return (input_shape[0], total, 4)
        else:
            return (input_shape[0], None, 4)

    def get_config(self):
        """get config
        """
        config = super(Anchors, self).get_config()
        config.update({
            'size': self.size,
            'stride': self.stride,
            'ratios': self.ratios.tolist(),
            'scales': self.scales.tolist(),
        })

        return config


class UpsampleLike(keras.layers.Layer):
    """ Keras layer for upsampling a Tensor to be the same shape as 
    another Tensor.
    """

    def call(self, inputs, **kwargs):
        """call
        """
        source, target = inputs
        target_shape = keras.backend.shape(target)
        if keras.backend.image_data_format() == 'channels_first':
            source = backend.transpose(source, (0, 2, 3, 1))
            output = backend.resize_images(
                source, (target_shape[2], target_shape[3]), method='nearest')
            output = backend.transpose(output, (0, 3, 1, 2))
            return output
        else:
            return backend.resize_images(
                source, (target_shape[1], target_shape[2]), method='nearest')

    def compute_output_shape(self, input_shape):
        """ compute output shape
        """
        if keras.backend.image_data_format() == 'channels_first':
            return (input_shape[0][0], 
                    input_shape[0][1]) + input_shape[1][2:4]
        else:
            return (input_shape[0][0],) + \
                input_shape[1][1:3] + (input_shape[0][-1],)


class RegressBoxes(keras.layers.Layer):
    """ Keras layer for applying regression values to boxes.
    """

    def __init__(self, mean=None, std=None, *args, **kwargs):
        """ Initializer for the RegressBoxes layer.

        Parameters
        ----------
        mean: float
            The mean value of the regression values which was used for 
            normalization.
        std: float
            The standard value of the regression values which was used 
            for normalization.
        """
        if mean is None:
            mean = np.array([0, 0, 0, 0])
        if std is None:
            std = np.array([0.2, 0.2, 0.2, 0.2])

        if isinstance(mean, (list, tuple)):
            mean = np.array(mean)
        elif not isinstance(mean, np.ndarray):
            raise ValueError(
                'Expected mean to be a np.ndarray, list or tuple. \
                    Received: {}'.format(
                    type(mean)))

        if isinstance(std, (list, tuple)):
            std = np.array(std)
        elif not isinstance(std, np.ndarray):
            raise ValueError(
                'Expected std to be a np.ndarray, list or tuple. \
                    Received: {}'.format(
                    type(std)))

        self.mean = mean
        self.std = std
        super(RegressBoxes, self).__init__(*args, **kwargs)

    def call(self, inputs, **kwargs):
        """call
        """
        anchors, regression = inputs
        return backend.bbox_transform_inv(
            anchors, regression, mean=self.mean, std=self.std)

    def compute_output_shape(self, input_shape):
        """compute output shape
        """
        return input_shape[0]

    def get_config(self):
        """
        get config
        """
        config = super(RegressBoxes, self).get_config()
        config.update({
            'mean': self.mean.tolist(),
            'std': self.std.tolist(),
        })

        return config


class ClipBoxes(keras.layers.Layer):
    """ Keras layer to clip box values to lie inside a given shape.
    """

    def call(self, inputs, **kwargs):
        """call
        """
        image, boxes = inputs
        shape = keras.backend.cast(
            keras.backend.shape(image),
            keras.backend.floatx())
        if keras.backend.image_data_format() == 'channels_first':
            height = shape[2]
            width = shape[3]
        else:
            height = shape[1]
            width = shape[2]
        x1 = backend.clip_by_value(boxes[:, :, 0], 0, width)
        y1 = backend.clip_by_value(boxes[:, :, 1], 0, height)
        x2 = backend.clip_by_value(boxes[:, :, 2], 0, width)
        y2 = backend.clip_by_value(boxes[:, :, 3], 0, height)

        return keras.backend.stack([x1, y1, x2, y2], axis=2)

    def compute_output_shape(self, input_shape):
        """compute output shape"""
        return input_shape[1]
