#!/usr/bin/env python3
# coding: utf-8
# 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.

"""operator dsl function: avgpool"""
import akg.tvm
from akg.tvm.hybrid import script
from akg import dim
from akg.dim import DIM
from akg.utils import custom_tiling as ct_util, validation_check as vc_util
from akg.utils.dsl_create import cal_pad_shapes_by_strategy, zero_const
from akg.utils.format_transform import get_shape
from akg.ops.nn.maxpool import img2col

avgpool_set_dim_map = {
    str(((1, 1, 16, 16, 16), (4, 4), (3, 3), 'VALID', 'float16')): ((16, 1), (20, 1), (5, 1)),
    str(((1, 1, 16, 16, 16), (4, 4), (3, 3), (0, 0, 0, 0), 'float16')): ((16, 1), (20, 1), (5, 1)),
    str(((10, 3, 16, 16, 16), (4, 4), (3, 3), (0, 0, 0, 0), 'float16')): ((2, 2), (3, 3), (16, 16), (5, 5), (5, 5)),
    str(((1, 2, 16, 16, 16), (4, 4), (3, 3), (1, 1, 1, 1), 'float16')): ((1, 1), (16, 16), (19, 19)),
}


def avgpool_set_dim_func(a_value, kernel, stride, pad):
    """set dim info to attr with avgpool_set_dim_map"""
    key = []
    key.append(tuple(get_shape(a_value)))
    key.append(kernel)
    key.append(stride)
    if isinstance(pad, list):
        pad = tuple(pad)
    key.append(pad)
    key.append(a_value.dtype)
    hash_key = str(tuple(key))

    if hash_key in avgpool_set_dim_map.keys():
        return ct_util.set_dims(avgpool_set_dim_map[hash_key]), hash_key
    return "", hash_key


def avg_pool_5d_hybrid(a_value, kernel, stride, strategy):
    """avgpool with 5d case via hybrid"""
    kernel_h, kernel_w = kernel
    stride_h, stride_w = stride
    shape = get_shape(a_value)
    batch_size, c1_, in_size_h, in_size_w, c0_ = shape
    dtype = a_value.dtype
    if len(shape) != 5:
        raise ValueError("Only support 5-dim pooling!")
    if len(kernel) != 2:
        raise ValueError("Only support 2-dim kernel!")

    [pad_height_head, _, pad_width_head, _], [out_size_h, out_size_w] = \
        cal_pad_shapes_by_strategy(shape, kernel, stride, strategy)

    avg_pre = akg.tvm.const(1.0000 / (kernel_w * kernel_h), dtype=dtype)
    zero = akg.tvm.const(0.0, dtype=dtype)

    @script(capture=locals())
    def avg_pool_hybrid(inputs, zero, avg_pre):
        output = output_tensor((batch_size, c1_, out_size_h, out_size_w, c0_), inputs.dtype)

        for n in range(batch_size):
            for c1 in range(c1_):
                # Head
                for ow in range(out_size_w):
                    for c0 in range(c0_):
                        output[n, c1, 0, ow, c0] = zero
                for ow in range(out_size_w):
                    for kh in range(kernel_h):
                        for kw in range(kernel_w):
                            for c0 in range(c0_):
                                if (kh >= pad_height_head) \
                                        and (ow * stride_w + kw - pad_width_head >= 0) \
                                        and (ow * stride_w + kw <= in_size_w + pad_width_head - 1):
                                    output[n, c1, 0, ow, c0] = output[n, c1, 0, ow, c0] +\
                                        inputs[n, c1, kh - pad_height_head,
                                               ow * stride_w + kw - pad_width_head, c0]
                                else:
                                    output[n, c1, 0, ow, c0] += zero
                for ow in range(out_size_w):
                    for c0 in range(c0_):
                        output[n, c1, 0, ow, c0] *= avg_pre
                # Tail
                for oh in range(out_size_h - 1):
                    for ow in range(out_size_w):
                        for c0 in range(c0_):
                            output[n, c1, oh + 1, ow, c0] = zero
                for oh in range(out_size_h - 1):
                    for ow in range(out_size_w):
                        for kh in range(kernel_h):
                            for kw in range(kernel_w):
                                for c0 in range(c0_):
                                    if ((oh + 1) * stride_h + kh <= in_size_h + pad_height_head - 1)\
                                            and (ow * stride_w + kw >= pad_width_head)\
                                            and (ow * stride_w + kw <= in_size_w + pad_width_head - 1):
                                        output[n, c1, oh + 1, ow, c0] = output[n, c1, oh + 1, ow, c0] +\
                                            inputs[n, c1, (oh + 1) * stride_h +
                                                   kh - pad_height_head, ow * stride_w +
                                                   kw - pad_width_head, c0]
                                    else:
                                        output[n, c1, oh + 1, ow, c0] += zero
                for oh in range(out_size_h - 1):
                    for ow in range(out_size_w):
                        for c0 in range(c0_):
                            output[n, c1, oh + 1, ow, c0] *= avg_pre
        return output

    res_value = avg_pool_hybrid(a_value, zero, avg_pre)

    # set dim
    info = dim.Dim()
    # first part
    info.setdim(index=0, axis=0, tilel1=out_size_w, tilel0=0)  # ow
    info.setdim(index=0, axis=1, tilel1=c0_, tilel0=0)  # c0
    info.setdim(index=0, axis=2, tilel1=kernel_h, tilel0=0)  # kh

    # second part
    info.setdim(index=1, axis=0, tilel1=out_size_h - 1, tilel0=0)  # oh-1
    info.setdim(index=1, axis=1, tilel1=out_size_w, tilel0=0)  # ow
    info.setdim(index=1, axis=2, tilel1=c0_, tilel0=0)  # c0
    info.setdim(index=1, axis=3, tilel1=kernel_h, tilel0=0)  # kh

    info = str(info)

    attrs = {DIM: info}
    return res_value, attrs


@vc_util.check_input_type(akg.tvm.tensor.Tensor, (list, tuple),
                          (list, tuple), (str, list, tuple))
def avgpool(data, kernel, stride, strategy):
    """
    Performs the average pooling on the input datas.

    Note:
        Only support 5D format(NC1HWC0), and pooling will work on H and W.
        Support **Strategies**:

        .. hlist::
          * VALID: will not pad, and drop tailed elements when pooling.
                   Output shape will be  `ceil((pool_shapes[i] - (kernel[i] - 1)) / stride[i])`
            > **example**:
            > params: inputs => 11, kernel width => 5, stride => 4
            > inputs: 1  2  3  4  5  6  7  8  9  10 11
            > 1st window contains: 1 2 3 4 5
            > 2nd window contains: 5 6 7 8 9
            > dropped: 10 11
          * SAME: will pad with zero evenly each side, but will add extra to tail
                  if the total padding amount is odd.
                  Output shape will be  `ceil(pool_shapes[i] / stride[i])`
            > **example**:
            > params: inputs => 10, kernel width => 5, stride => 4
            > inputs: 1  2  3  4  5  6  7  8  9  10
            > paded: 0(pad1) | 1  2  3  4  5  6  7  8  9  10 | 0(pad2) 0(pad3)
            > 1st window contains: 0(pad1) 1 2 3 4
            > 2nd window contains: 4 5 6 7 8
            > 3rd window contains: 8 9 10 0(pad2) 0(pad3)
            > dropped: None
          * CONSTANTS: will pad with zero according to given constants
                       (also dropped tailed elements when pooling).
            > **example**:
            > params: inputs => 10, kernel width => 5, stride => 4, pad => (2, 2)
            > inputs: 1  2  3  4  5  6  7  8  9  10
            > paded: 0(pad1) 0(pad2) | 1  2  3  4  5  6  7  8  9  10 | 0(pad2) 0(pad3)
            > 1st window contains: 0(pad1) 0(pad2) 1 2 3
            > 2nd window contains: 3 4 5 6 7
            > 3rd window contains: 7 8 9 10 0(pad3)
            > dropped: 0(pad4)

    Args:
        data (tvm.tensor.Tensor): Tensor of type float16, float32.
        kernel (Union[list, tuple]): List or tuple of two int numbers for pooling window's size.
        stride (Union[list, tuple]): List or tuple of two int numbers for window's stride.
        strategy (Union[str, list, tuple]): A string or list or tuple for padding strategy,
            should be 'VALID', 'SAME' or instance of list(including four int numbers,
            as 'CONSTANTS' strategy).

    Returns:
        Tensor as result for average pooling.
    """
    dim_info, _ = avgpool_set_dim_func(data, kernel, stride, strategy)
    attrs = {DIM: dim_info}
    attrs['disable_half_to_float_sum_opt'] = True

    shape = [x.value for x in data.shape]
    dtype = data.dtype
    vc_util.davinci_format_check(shape, "NC1HWC0", dim=5)
    vc_util.check_shape(kernel, 2, 'Kernel')
    vc_util.check_shape(stride, 2, 'Stride')

    if shape[2] > 60 and shape[3] > 60:
        return avg_pool_5d_hybrid(data, kernel, stride, strategy)

    kernel_h, kernel_w = kernel
    stride_h, stride_w = stride
    batch_size, c1, in_size_h, in_size_w, c0 = shape

    [pad_height_head, pad_height_tail, pad_width_head, pad_width_tail], [out_size_h, out_size_w] = \
        cal_pad_shapes_by_strategy(shape, kernel, stride, strategy)

    pad_shape = (batch_size,
                 c1,
                 in_size_h + pad_height_head + pad_height_tail,
                 in_size_w + pad_width_head + pad_width_tail,
                 c0)

    pad2d = akg.tvm.compute(pad_shape,
                            lambda n, c1, h, w, c0:
                            akg.tvm.if_then_else(
                                akg.tvm.any(
                                    h < pad_height_head,
                                    h > in_size_h + pad_height_head - 1,
                                    w < pad_width_head,
                                    w > in_size_w + pad_width_head - 1
                                ),
                                akg.tvm.const(0.0, dtype=dtype),
                                data[n, c1, h - pad_height_head, w - pad_width_head, c0],
                            ),
                            name="pad2d")

    axis_kernel_h = akg.tvm.reduce_axis((0, kernel_h), name="axis_kernel_h")
    axis_kernel_w = akg.tvm.reduce_axis((0, kernel_w), name="axis_kernel_w")

    out_shape = (batch_size, c1, out_size_h, out_size_w, c0)

    dividor = akg.tvm.const(kernel_h * kernel_w, dtype)
    res = akg.tvm.compute(out_shape,
                          lambda n, c1, h, w, c0:
                          akg.tvm.sum(
                              pad2d[n, c1, h * stride_h + axis_kernel_h, w * stride_w + axis_kernel_w, c0],
                              axis=[axis_kernel_h, axis_kernel_w]
                          ),
                          name="res")
    res_value = akg.tvm.compute(out_shape,
                                lambda n, c1, h, w, c0:
                                res[n, c1, h, w, c0] / dividor,
                                name="res_value")
    return res_value, attrs

def avgpool_with_img2col(data, kernel, stride, strategy):
    """
    Performs the avgpool with img2col.

    Note:
        Only support 5D format(NC1HWC0), and pooling will work on H and W.

    Args:
        data (tvm.tensor.Tensor): Tensor of type float16, float32.
        kernel (Union[list, tuple]): two int numbers for pooling window's size.
        stride (Union[list, tuple]): two int numbers for window's stride.
        strategy (Union[str, list, tuple]): padding, should be 'VALID','SAME' or
            instance of list(four int numbers, as 'CONSTANTS' strategy).
            Support **Strategies** is the same as avgpool.

    Returns:
        tvm.tensor.Tensor, result for gradient of avgpooling.
    """
    shape = get_shape(data)
    dtype = data.dtype

    vc_util.davinci_format_check(shape, "NC1HWC0", dim=5)
    vc_util.ops_dtype_check(dtype, vc_util.DtypeForDavinci.FLOAT16)
    vc_util.check_shape(kernel, 2, "Kernel")
    vc_util.check_shape(stride, 2, "Stride")

    kernel_h, kernel_w = kernel
    in_n, in_c1, _, _, in_c0 = shape

    [ph_h, ph_t, pw_h, pw_t], [out_h, out_w] = \
        cal_pad_shapes_by_strategy(shape, kernel, stride, strategy)

    pad = [ph_h, ph_t, pw_h, pw_t]
    pad_value = zero_const(dtype)

    # fmap img2col l1 -> ub in zZ format by fractal
    fmap_img2col_shp_ub = (in_n, in_c1, kernel_h, kernel_w, out_h, out_w, in_c0)
    fmap_img2col_ub = img2col(data, fmap_img2col_shp_ub, kernel_h, kernel_w,
                              pad, stride, pad_value, tag="")

    out_shape = (in_n, in_c1, out_h, out_w, in_c0)
    reduce_axis_h = akg.tvm.reduce_axis((0, kernel_h), name="reduce_h")
    reduce_axis_w = akg.tvm.reduce_axis((0, kernel_w), name="reduce_w")
    res_sum = akg.tvm.compute(out_shape,
                              lambda n, c1, oh, ow, c0:
                              akg.tvm.sum(
                                  fmap_img2col_ub[n, c1, reduce_axis_h,
                                                  reduce_axis_w, oh, ow, c0],
                                  axis=[reduce_axis_h, reduce_axis_w]),
                              name="pooling_avg")

    dividor = akg.tvm.const(kernel_h * kernel_w, dtype)
    output = akg.tvm.compute(out_shape, lambda *i: res_sum(*i) / dividor,
                             name="res_value")
    return output
