# Copyright 2019 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 sys
import numpy as np


def random_gaussian(size, miu=3, sigma=1):
    """ Generate random array with absolution value obeys gaussian distribution """
    if sigma <= 0:
        sys.stderr.write("Error: Expect positive sigmal for gaussian distribution. but get %f\n" % sigma)
        sys.exit(1)

    rgn = np.random.RandomState(2019)
    ret = rgn.normal(miu, sigma, size)
    for x in np.nditer(ret, op_flags=['readwrite']):
        if np.random.randint(0, 2):
            continue
        x[...] = x * -1
    return ret

def conv_param_prepare(conv_param):

    stride = 1
    pad = 0
    dilation = 1
    if 'stride' in conv_param:
        stride = conv_param['stride']
    if 'pad' in conv_param:
        pad = conv_param['pad']
    if 'dilation' in conv_param:
        dilation = conv_param['dilation']

    if isinstance(stride, int):
        stride = [stride] * 2
    elif isinstance(stride, (list, tuple)) and 1 == len(stride):
        stride = list(stride) * 2
    elif isinstance(stride, (list, tuple)) and 2 == len(stride):
        pass
    else:
        raise RuntimeError('stride para illegal !!!')

    if isinstance(pad, int):
        pad = [pad] * 4
    elif isinstance(pad, (list, tuple)) and 1 == len(pad):
        pad = list(pad) * 4
    elif isinstance(pad, (list, tuple)) and 4 == len(pad):
        pass
    else:
        raise RuntimeError('pad para illegal !!!')

    if isinstance(dilation, int):
        dilation = [dilation] * 2
    elif isinstance(dilation, (list, tuple)) and 1 == len(dilation):
        dilation = list(dilation) * 2
    elif isinstance(dilation, (list, tuple)) and 2 == len(dilation):
        pass
    else:
        raise RuntimeError('dilation para illegal !!!')
    return stride, pad, dilation

def conv_shape_4d(fm_shape, w_shape, pad, stride, dilation):

    S_h, S_w = stride
    P_top, P_bottom, P_left, P_right = pad
    D_h, D_w = dilation

    IN, IC, IH, IW = fm_shape
    C0 = 16
    IC = ((IC + C0 - 1) // C0) * C0

    WN, WC, WH, WW = w_shape
    WN = ((WN + C0 - 1) // C0) * C0
    WC = ((WC + C0 - 1) // C0) * C0

    ON = IN
    OC = WN
    WHD = (WH - 1) * D_h + 1
    WWD = (WW - 1) * D_w + 1
    OH = (IH + P_top + P_bottom - WHD) // S_h + 1
    OW = (IW + P_left + P_right - WWD) // S_w + 1

    fm_shape = [IN, IC, IH, IW]
    w_shape = [WN, WC, WH, WW]
    out_shape = [ON, OC, OH, OW]

    return fm_shape, w_shape, out_shape

def conv_tensor_4d_to_5d(x, w, b, out):

    IN, IC, IH, IW = x.shape
    WN, WC, WH, WW = w.shape
    ON, OC, OH, OW = out.shape
    C0 =16

    ''' transpose to 5D - NC1HWC0 '''
    feature = x.reshape(IN, IC // C0, C0, IH, IW).transpose(0, 1, 3, 4, 2).copy()
    ''' transpose to 5D - C1HWNC0 '''
    filter = w.reshape(WN, WC // C0, C0, WH, WW).transpose(1, 3, 4, 0, 2).copy()
    filter = filter.reshape(WC // C0 * WH * WW, WN // 16, 16, C0)

    bb = b.reshape(1, WN // 16, 1, 1, 16)
    ''' transpose to 5D - NC1HWC0 '''
    output = out.reshape(ON, OC // C0, C0, OH, OW).transpose(0, 1, 3, 4, 2).copy()

    return feature, filter, bb, output

def conv_forward_naive(x, w, b, conv_param):

    stride, pad, dilation = conv_param_prepare(conv_param)

    P_top, P_bottom, P_left, P_right = pad[0:4]
    S_h, S_w = stride[0:2]
    D_h, D_w = dilation[0:2]

    fm_shape, w_shape, out_shape = conv_shape_4d(x.shape, w.shape, pad, stride, dilation)

    N, C, H, W = fm_shape
    WN, WC, WH, WW = w_shape
    _, F, Ho, Wo = out_shape
    WHD = (WH - 1) * D_h + 1
    WWD = (WW - 1) * D_w + 1

    x_pad = np.zeros((N, C, H + P_top + P_bottom, W + P_left + P_right))
    x_pad[:, :, P_top:P_top + H, P_left:P_left + W] = x
    out = np.zeros((N, F, Ho, Wo))

    for f in range(F):
        for i in range(Ho):
            for j in range(Wo):
                # N*C*HH*WW, C*HH*WW = N*C*HH*WW, sum -> N*1
                out[:, f, i, j] = np.sum(x_pad[:, :, i * S_h: i * S_h + WHD: D_h, j * S_w: j * S_w + WWD: D_w] * w[f, :, :, :], axis=(1, 2, 3))

        if b is not None:
            out[:, f, :, :] += b[f]

    return out
