# Copyright 2020 Huawei Technologies Co., Ltd
#
# 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 numpy as np
import math
from functools import reduce
from mindspore.ops import prim_attr_register, PrimitiveWithInfer
from mindspore import Tensor
from mindspore._checkparam import ParamValidator as validator
from mindspore._checkparam import Rel, check_bool, check_int_positive, twice
from mindspore.common import dtype as mstype
class Cus_Conv2D(PrimitiveWithInfer):
    r"""
    Applies 2D convolution for the input.

    Input is typically of shape :math:`(N, C, H, W)`, where :math:`N` is batch size and :math:`C` is channel number.
    For each batch of shape :math:`(C, H, W)` the formula (given mode 1) is defined as:

    .. math::
        out_j = \sum_{i=0}^{C_{in} - 1} ccor(W_{ij}, X_i) + b_j,

    where :math:`ccor` is cross correlation operator, :math:`C_{in}` is the input channel number, :math:`j` ranges
    from :math:`0` to :math:`C_{out} - 1`, :math:`W_{ij}` corresponds to i-th channel of the j-th filter and
    :math:`out_{j}` corresponds to the j-th channel of the output.

    The first introduction can be found in paper `Gradient Based Learning Applied to Document Recognition
    <http://vision.stanford.edu/cs598_spring07/papers/Lecun98.pdf>`_.

    More detailed introduction can be found here: http://cs231n.github.io/convolutional-networks/.

    Args:
        out_channel (int): The dimensionality of the output space.
        kernel_size (Union[int, tuple[int]]): The kernel size of the 2D convolution.
        mode (int): 0 Math convolutiuon, 1 cross-correlation convolution ,
                       2 deconvolution, 3 depthwise convolution. Default: 1.
        pad_mode (str): "valid", "same", "pad" the mode to fill padding. Default: "valid".
        pad (int): The pad value to fill. Default: 0.
        stride (int): The stride to apply conv filter. Default: 1.
        dilation (int): Specifying the dilation rate to use for dilated convolution. Default: 1.
        group (int): Split input into groups. Default: 1.

    Returns:
        Tensor, the value that applied 2D convolution.

    Inputs:
        - **input** (Tensor) - Tensor of shape :math:`(N, C_{in}, H_{in}, W_{in})`.

    Outputs:
        Tensor of shape :math:`(N, C_{out}, H_{out}, W_{out})`.
    """

    @prim_attr_register
    def __init__(self,
                 out_channel,
                 kernel_size,
                 mode=1,
                 pad_mode="valid",
                 pad=0,
                 stride=1,
                 dilation=1,
                 group=1):
        """init Conv2D"""
        self.init_prim_io_names(inputs=['x', 'w'], outputs=['output'])
        self.kernel_size = kernel_size
        self.kernel_size = validator.check_type('kernel_size', kernel_size, (int, tuple))
        if isinstance(self.kernel_size, int):
            self.kernel_size = (self.kernel_size, self.kernel_size)
        validator.check_integer('length of kernel_size', len(self.kernel_size), 2, Rel.GE)
        validator.equal('type of pad', type(pad), 'not bool', not isinstance(pad, bool))
        validator.equal('type of pad', type(pad), 'int', isinstance(pad, int))
        self.pad_mode = validator.check_string('pad_mode', pad_mode, ['valid', 'same', 'pad'])
        self.pad = validator.check_pad_value_by_mode(self.__class__.__name__, pad_mode, pad)
        if self.pad_mode == 'pad':
            validator.check_integer('pad', self.pad, 0, Rel.GE)

        self.mode = validator.check_integer('mode', mode, 1, Rel.EQ)
        self.add_prim_attr('data_format', "NCHW")
        self.out_channel = validator.check_integer('out_channel', out_channel, 0, Rel.GT)
        self.group = validator.check_integer('group', group, 0, Rel.GT)
        self.dilation = validator.check_integer('dilation', dilation, 1, Rel.GE)
        validator.check_type('kernel_size', kernel_size, [int, tuple])
        if isinstance(kernel_size, int) and kernel_size < 1:
            raise ValueError('Attr \'kernel_size\' of \'Conv2D\' Op passed '
                             + str(self.kernel_size)+', should be a int or tuple and equal to or greater than 1.')
        if isinstance(kernel_size, tuple) and (len(kernel_size) != 2 or
                                               (not isinstance(kernel_size[0], int)) or
                                               (not isinstance(kernel_size[1], int)) or
                                               kernel_size[0] < 1 or kernel_size[1] < 1):
            raise ValueError('Attr \'kernel_size\' of \'Conv2D\' Op passed '
                             + str(self.kernel_size)+', should be a int or tuple and equal to or greater than 1.')
        self.stride = validator.check_integer('stride', stride, 1, Rel.GE)
        from .cus_conv2d_impl import Cus_Conv2D

    def infer_shape(self, x_shape, w_shape):
        validator.check_integer("weight_shape", len(w_shape), 4, Rel.EQ)
        validator.check_integer("x_shape", len(x_shape), 4, Rel.EQ)
        validator.check_param_equal("x_shape[1]", x_shape[1] // self.group, "w_shape[1]", w_shape[1])
        validator.check_param_equal('out_channel', self.out_channel, 'w_shape[0]', w_shape[0])
        validator.check_param_equal('kernel_size', self.kernel_size, 'w_shape[2:4]', tuple(w_shape[2:4]))

        kernel_size_h = w_shape[2]
        kernel_size_w = w_shape[3]

        if self.pad_mode == "valid":
            h_out = math.ceil((x_shape[2] - kernel_size_h + 1) / self.stride)
            w_out = math.ceil((x_shape[3] - kernel_size_w + 1) / self.stride)
            pad_top, pad_bottom, pad_left, pad_right = 0, 0, 0, 0
        elif self.pad_mode == "same":
            h_out = math.ceil(x_shape[2] / self.stride)
            w_out = math.ceil(x_shape[3] / self.stride)

            pad_needed_h = max(0, (h_out - 1) * self.stride + kernel_size_h - x_shape[2])
            pad_top = math.floor(pad_needed_h / 2)
            pad_bottom = pad_needed_h - pad_top

            pad_needed_w = max(0, (w_out - 1) * self.stride + kernel_size_w - x_shape[3])
            pad_left = math.floor(pad_needed_w / 2)
            pad_right = pad_needed_w - pad_left
        elif self.pad_mode == 'pad':
            pad_top, pad_bottom, pad_left, pad_right = self.pad, self.pad, self.pad, self.pad

            h_out = 1 + (x_shape[2] + 2 * self.pad - kernel_size_h - (kernel_size_h - 1) * (self.dilation - 1)) \
                    / self.stride
            w_out = 1 + (x_shape[3] + 2 * self.pad - kernel_size_w - (kernel_size_w - 1) * (self.dilation - 1)) \
                    / self.stride
            h_out = math.floor(h_out)
            w_out = math.floor(w_out)

        self.pad_list = [pad_top, pad_bottom, pad_left, pad_right]
        self.add_prim_attr('pad_list', (pad_top, pad_bottom, pad_left, pad_right))

        out_channel = self.out_channel
        out_shape = [x_shape[0], out_channel, h_out, w_out]
        return out_shape

    def infer_dtype(self, x_dtype, w_dtype):
        args = {'x_dtype': x_dtype, 'w_dtype': w_dtype}
        validator.check_type_same(args, [mstype.int8, mstype.int32, mstype.float16, mstype.float32])
        return x_dtype