# 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.
# ============================================================================
""" test nn ops """
import numpy as np
from numpy.random import normal
import pytest

import mindspore.nn as nn
import mindspore.context as context
from mindspore.ops.composite import core
from mindspore.common.api import ms_function

from mindspore import Tensor
from mindspore.ops import functional as F
from mindspore.ops import prim_attr_register, PrimitiveWithInfer

context.set_context(mode=context.GRAPH_MODE, save_graphs=True)


class FakeOp(PrimitiveWithInfer):
    @prim_attr_register
    def __init__(self):
        """"""

    def infer_shape(self, x, y):
        self.second_shape = y
        self.add_prim_attr("second_shape", y)
        return x

    def infer_dtype(self, x, y):
        return x


# test the normal case that should generate independent primitive because of different
# generated attributes after inference
def test_conv2d_same_primitive():
    class Conv2DSameNet(nn.Cell):
        def __init__(self):
            super(Conv2DSameNet, self).__init__()
            self.conv1 = nn.Conv2d(16, 64, (1, 41), (1, 4), "same", 0, 1, has_bias=True)
            self.conv2 = nn.Conv2d(16, 64, (1, 41), (1, 4), "same", 0, 1, has_bias=True)

        def construct(self, x, y):
            r1 = self.conv1(x)
            r2 = self.conv2(y)
            return (r1, r2)

    t1 = Tensor(np.ones([1, 16, 1, 1918]).astype(np.float32))
    t2 = Tensor(np.ones([1, 16, 1, 3840]).astype(np.float32))
    net = Conv2DSameNet()
    net(t1, t2)


# test free variable function list as parameter
def test_remove_and_fv_2():
    @core(loop_can_uroll=True)
    def inner_loop(x, input_data, fv_func_list):
        ret = ()
        for fv_fn in fv_func_list:
            ele = fv_fn(input_data)
            ret += (ele,)
        return ret

    @ms_function
    def out_loop(input1, input_data):
        ret = ()

        def fv_func1(y):
            return input1 * y
        def fv_func2(y):
            return input1 - y
        fv_func_list = [fv_func1, fv_func2]
        ele0 = inner_loop(input1, input_data[0], fv_func_list)
        ele1 = inner_loop(input1, input_data[1], fv_func_list)
        ret = (ele0, ele1)
        return ret

    input_data = (Tensor(normal(0, 0.1, (3, 3))), Tensor(normal(0, 0.1, (3, 1))))
    input1 = Tensor(normal(0, 0.1, (3, 3)))
    out_loop(input1, input_data)


# test cell as high order argument
# The graph with free variables used as argument is not supported yet
# because of the limit of inference specialize system
def test_conv2d_op_with_argi_1():
    class Conv2dNet(nn.Cell):
        def __init__(self):
            super(Conv2dNet, self).__init__()

        def construct(self, op, x):
            return op(x)

    class OpsNet(nn.Cell):
        def __init__(self, net):
            super(OpsNet, self).__init__()
            self.opnet = net
            self.conv2 = nn.Conv2d(16, 64, (1, 41), (1, 4), "same", 0, 1, has_bias=True)

        def construct(self, x, y):
            conv_op = self.conv2
            a = self.opnet(conv_op, x)
            b = self.opnet(conv_op, y)
            return (a, b)

    t1 = Tensor(np.ones([1, 16, 1, 1918]).astype(np.float32))
    t2 = Tensor(np.ones([1, 16, 1, 3840]).astype(np.float32))
    net = OpsNet(Conv2dNet())
    net(t1, t2)


def test_conv2d_op_with_arg():
    class FackOpNet(nn.Cell):
        def __init__(self):
            super(FackOpNet, self).__init__()
            self.op = FakeOp()

        def construct(self, x, y):
            return self.op(x, y)

    class OpNet(nn.Cell):
        def __init__(self):
            super(OpNet, self).__init__()

        def construct(self, op, x, y):
            return op(x, y)

    class OpsNet(nn.Cell):
        def __init__(self, net):
            super(OpsNet, self).__init__()
            self.opnet = net
            self.op = FackOpNet()

        def construct(self, x, y):
            op = self.op
            a = self.opnet(op, x, y)
            b = self.opnet(op, y, x)
            return (a, b)

    t1 = Tensor(np.ones([1, 16, 1, 1918]).astype(np.float32))
    t2 = Tensor(np.ones([1, 16, 1, 3840]).astype(np.float32))
    net = OpsNet(OpNet())
    net(t1, t2)


def test_conv2d_op_with_arg_same_input():
    class FackOpNet(nn.Cell):
        def __init__(self):
            super(FackOpNet, self).__init__()
            self.op = FakeOp()

        def construct(self, x, y):
            return self.op(x, y)

    class OpNet(nn.Cell):
        def __init__(self):
            super(OpNet, self).__init__()

        def construct(self, op, x, y):
            return op(x, y)

    class OpsNet(nn.Cell):
        def __init__(self, net):
            super(OpsNet, self).__init__()
            self.opnet = net
            self.op = FackOpNet()

        def construct(self, x, y):
            op = self.op
            a = self.opnet(op, x, x)
            b = self.opnet(op, y, x)
            return (a, b)

    t1 = Tensor(np.ones([1, 16, 1, 1918]).astype(np.float32))
    t2 = Tensor(np.ones([1, 16, 1, 3840]).astype(np.float32))
    net = OpsNet(OpNet())
    net(t1, t2)


# test op with partial
def test_op_as_partial():
    class OpAsPartial(nn.Cell):
        def __init__(self):
            super(OpAsPartial, self).__init__()
            self.op = FakeOp()

        def construct(self, x, y, z):
            partial_op = F.partial(self.op, x)
            a = partial_op(y)
            b = partial_op(z)
            return a, b

    t1 = Tensor(np.ones([1, 16, 1, 1918]).astype(np.float32))
    t2 = Tensor(np.ones([1, 16, 1, 3840]).astype(np.float32))
    t3 = Tensor(np.ones([1, 16, 1, 1234]).astype(np.float32))
    net = OpAsPartial()
    net(t1, t2, t3)


# test op with partial
def test_op_as_partial_inside():
    class OpAsPartial(nn.Cell):
        def __init__(self):
            super(OpAsPartial, self).__init__()
            self.op = FakeOp()

        def construct(self, x, y, z):
            partial_op = F.partial(self.op, x)
            a = partial_op(y)
            b = partial_op(z)
            return a, b

    class OuterNet(nn.Cell):
        def __init__(self):
            super(OuterNet, self).__init__()
            self.net = OpAsPartial()

        def construct(self, x, y, z):
            a, b = self.net(x, y, z)
            return a, b

    t1 = Tensor(np.ones([1, 16, 1, 1918]).astype(np.float32))
    t2 = Tensor(np.ones([1, 16, 1, 3840]).astype(np.float32))
    t3 = Tensor(np.ones([1, 16, 1, 1234]).astype(np.float32))
    net = OuterNet()
    net(t1, t2, t3)


# test op with partial case 2
def test_op_as_partial_independent():
    class OpAsPartial(nn.Cell):
        def __init__(self):
            super(OpAsPartial, self).__init__()
            self.op = FakeOp()

        def construct(self, x, y, z):
            partial_op1 = F.partial(self.op, x)
            a = partial_op1(y)
            partial_op2 = F.partial(self.op, x)
            b = partial_op2(z)
            return a, b

    t1 = Tensor(np.ones([1, 16, 1, 1918]).astype(np.float32))
    t2 = Tensor(np.ones([1, 16, 1, 3840]).astype(np.float32))
    t3 = Tensor(np.ones([1, 16, 1, 1234]).astype(np.float32))
    net = OpAsPartial()
    net(t1, t2, t3)


def test_nest_partial():
    class NestPartial(nn.Cell):
        def __init__(self):
            super(NestPartial, self).__init__()
            self.op = FakeOp()

        def construct(self, x, y, z):
            partial_op1 = F.partial(self.op)
            partial_op2 = F.partial(partial_op1, x)
            a = partial_op2(y)
            partial_op3 = F.partial(self.op)
            partial_op4 = F.partial(partial_op3, x)
            b = partial_op4(z)
            return a, b

    t1 = Tensor(np.ones([1, 16, 1, 1918]).astype(np.float32))
    t2 = Tensor(np.ones([1, 16, 1, 3840]).astype(np.float32))
    t3 = Tensor(np.ones([1, 16, 1, 1234]).astype(np.float32))
    net = NestPartial()
    net(t1, t2, t3)


# high order argument
# op and op args as network arguments
def test_op_with_arg_as_input():
    class WithOpArgNet(nn.Cell):
        def __init__(self):
            super(WithOpArgNet, self).__init__()

        def construct(self, op, x, y):
            return op(x, y)

    class OpsNet(nn.Cell):
        def __init__(self, net):
            super(OpsNet, self).__init__()
            self.opnet = net
            self.op = FakeOp()

        def construct(self, x, y, z):
            op = self.op
            a = self.opnet(op, x, z)
            b = self.opnet(op, x, y)
            return (a, b)

    t1 = Tensor(np.ones([1, 16, 1, 1918]).astype(np.float32))
    t2 = Tensor(np.ones([1, 16, 1, 3840]).astype(np.float32))
    t3 = Tensor(np.ones([1, 16, 1, 1234]).astype(np.float32))
    net = OpsNet(WithOpArgNet())
    net(t1, t2, t3)


# The partial application used as argument is not supported yet
# because of the limit of inference specialize system
@pytest.mark.skip("poly in infer")
def test_partial_as_arg():
    class PartialArgNet(nn.Cell):
        def __init__(self):
            super(PartialArgNet, self).__init__()

        def construct(self, partial_op, y):
            return partial_op(y)

    class OpsNet(nn.Cell):
        def __init__(self, net):
            super(OpsNet, self).__init__()
            self.partial_net = net
            self.op = FakeOp()

        def construct(self, x, y, z):
            partial_op = F.partial(self.op, x)
            a = self.partial_net(partial_op, z)
            b = self.partial_net(partial_op, y)
            return (a, b)

    t1 = Tensor(np.ones([1, 16, 1, 1918]).astype(np.float32))
    t2 = Tensor(np.ones([1, 16, 1, 3840]).astype(np.float32))
    t3 = Tensor(np.ones([1, 16, 1, 1234]).astype(np.float32))
    net = OpsNet(PartialArgNet())
    net(t1, t2, t3)
