# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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.
# pylint: disable=invalid-name, too-many-nested-blocks
"""Gradient of conv2d with respect to weight in python"""

import numpy as np


# Reference: cutlass/tools/util/include/cutlass/util/reference/host/convolution.h
def conv2d_backward_weight_nchw_python(
    dy_np, x_np, kernel_size, stride, padding, groups=1, channels=None
):
    """Gradient of the conv2d op with respect to weight, in NCHW layout.

    Parameters
    ----------
    dy_np : numpy.ndarray
        4-D with shape [batch, in_channel, out_height, out_width]

    x_np : numpy.ndarray
        4-D with shape [batch, in_channel, in_height, in_width]

    kernel_size : tuple of two ints
        Height and width of the weight

    stride : tuple of two ints
        Stride size, or [stride_height, stride_width]

    padding : tuple of two ints
        Spatial padding, or [pad_h, pad_w]

    Returns
    -------
    dw_np : np.ndarray
        4-D with shape [num_filter, in_channel, filter_height, filter_width]

    """
    N, C, H, W = x_np.shape
    _, K, P, Q = dy_np.shape
    R, S = kernel_size
    pad_h, pad_w = padding
    stride_h, stride_w = stride
    is_depth_wise = C == K and C == groups

    if is_depth_wise:
        assert channels == groups, "Only channel_mult == 1 supported for now."
        dw = np.zeros((K, 1, R, S)).astype(dy_np.dtype)
    else:
        assert groups == 1, "General grouped conv2d not supported for now."
        dw = np.zeros((K, C, R, S)).astype(dy_np.dtype)

    for k in range(K):
        for r in range(R):
            for s in range(S):
                for c in range(dw.shape[1]):
                    acc = 0
                    for n in range(N):
                        for p in range(P):
                            for q in range(Q):
                                if not is_depth_wise:
                                    in_c = c
                                else:
                                    in_c = k

                                coord = (
                                    n,
                                    in_c,
                                    p * stride_h - pad_h + r,
                                    q * stride_w - pad_w + s,
                                )

                                if (
                                    coord[2] < H
                                    and coord[2] >= 0
                                    and coord[3] < W
                                    and coord[3] >= 0
                                ):
                                    acc += dy_np[n, k, p, q] * x_np[coord]

                    dw[k, c, r, s] = acc

    return dw


def conv2d_backward_weight_python(
    dy_np, x_np, kernel_size, stride, padding, layout="NCHW", groups=1, channels=None
):
    """Gradient of the conv2d op with respect to weight, in NCHW or NHWC layout.

    Parameters
    ----------
    dy_np : numpy.ndarray
        4-D with shape [batch, in_channel, out_height, out_width] for NCHW layout

    x_np : numpy.ndarray
        4-D with shape [batch, in_channel, in_height, in_width] for NCHW layout

    kernel_size : tuple of two ints
        Height and width of the weight

    stride : tuple of two ints
        Stride size, or [stride_height, stride_width]

    padding : tuple of two ints
        Spatial padding, or [pad_h, pad_w]

    layout: string
        Layout of dy_np and x_np

    groups: int
        Number of groups for grouped convolution.

    channels : int
        Number of output channels of this convolution.

    Returns
    -------
    dw_np : np.ndarray
        Tensor of shape [num_filter, in_channel, filter_height, filter_width] for NCHW layout,
        [num_filter, filter_height, filter_width, in_channel] for NHWC layout.
    """
    if layout == "NCHW":
        return conv2d_backward_weight_nchw_python(
            dy_np, x_np, kernel_size, stride, padding, groups, channels
        )

    dw_np_oihw = conv2d_backward_weight_nchw_python(
        np.transpose(dy_np, [0, 3, 1, 2]),
        np.transpose(x_np, [0, 3, 1, 2]),
        kernel_size,
        stride,
        padding,
        groups,
        channels,
    )
    return np.transpose(dw_np_oihw, [0, 2, 3, 1])
