#!/usr/bin/env python3

# Copyright (c) 2021 CINN Authors. All Rights Reserved.
#
# 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 cinn
import numpy as np
import paddle
import unittest

from cinn.frontend import *
from cinn.common import *
from cinn.runtime import *
from op_test import OpTest, OpTestTool
from op_test_helper import TestCaseHelper

set_cinn_cudnn_deterministic(True)
paddle.fluid.set_flags({'FLAGS_cudnn_deterministic': 1})


@OpTestTool.skip_if(not is_compiled_with_cuda(),
                    "x86 test will be skipped due to timeout.")
class TestConv2dOp(OpTest):
    def setUp(self):
        # print(f"\n{self.__class__.__name__}: {self.case}")
        self.prepare_inputs()

    def prepare_inputs(self):
        self.x_np = self.random(
            shape=self.case["x_shape"], dtype=self.case["dtype"])
        self.w_np = self.random(
            shape=self.case["w_shape"], dtype=self.case["dtype"])
        self.dy_np = self.random(
            shape=self.case["dy_shape"], dtype=self.case["dtype"])

    def build_paddle_program(self, target):
        x = paddle.to_tensor(self.x_np, stop_gradient=False)
        weight = paddle.to_tensor(self.w_np, stop_gradient=False)
        y = paddle.nn.functional.conv2d(
            x,
            weight,
            stride=self.case["stride"],
            padding=self.case["padding"],
            dilation=self.case["dilation"],
            groups=self.case["groups"],
            data_format=self.case["data_format"])
        self.paddle_outputs = [y]
        self.paddle_grads = self.get_paddle_grads([y], [x, weight],
                                                  [self.dy_np])

    def build_cinn_program(self, target):
        builder = NetBuilder("conv2d")
        x = builder.create_input(
            self.nptype2cinntype(self.case["dtype"]), self.case["x_shape"],
            "x")
        weight = builder.create_input(
            self.nptype2cinntype(self.case["dtype"]), self.case["w_shape"],
            "weight")
        dy = builder.create_input(
            self.nptype2cinntype(self.case["dtype"]), self.case["dy_shape"],
            "dy")

        if self.case["data_format"] == "NCHW":
            y = builder.conv2d(
                x,
                weight,
                strides=self.case["stride"],
                paddings=self.case["padding"],
                dilations=self.case["dilation"],
                groups=self.case["groups"],
                data_format=self.case["data_format"])
            x_grad = builder.conv(
                weight,
                dy,
                data_format=self.case["data_format"],
                conv_type="backward_data",
                output_shape=x.shape())
            weight_grad = builder.conv(
                x,
                dy,
                data_format=self.case["data_format"],
                conv_type="backward_filter",
                output_shape=weight.shape())
        elif self.case["data_format"] == "NHWC":
            weight_t = builder.transpose(weight, [0, 2, 3, 1])
            y = builder.conv2d(
                x,
                weight_t,
                strides=self.case["stride"],
                paddings=self.case["padding"],
                dilations=self.case["dilation"],
                groups=self.case["groups"],
                data_format=self.case["data_format"])
            x_grad = builder.conv(
                weight_t,
                dy,
                data_format=self.case["data_format"],
                conv_type="backward_data",
                output_shape=x.shape())
            w_grad = builder.conv(
                x,
                dy,
                data_format=self.case["data_format"],
                conv_type="backward_filter",
                output_shape=weight_t.shape())
            weight_grad = builder.transpose(w_grad, [0, 3, 1, 2])

        prog = builder.build()
        res = self.get_cinn_output(
            prog,
            target, [x, weight, dy], [self.x_np, self.w_np, self.dy_np],
            [y, x_grad, weight_grad],
            passes=[])

        self.cinn_outputs = [res[0]]
        self.cinn_grads = [res[1], res[2]]

    def test_check_results(self):
        max_relative_error = self.case[
            "max_relative_error"] if "max_relative_error" in self.case else 1e-5
        self.check_outputs_and_grads(max_relative_error=max_relative_error)


class TestConv2dOpAll(TestCaseHelper):
    def init_attrs(self):
        self.class_name = "TestConv2dCase"
        self.cls = TestConv2dOp
        self.inputs = [
            {
                "x_shape": [3, 16, 32, 32],
                "w_shape": [16, 16, 3, 3],
                "dy_shape": [3, 16, 30, 30],
                "data_format": "NCHW",
            },
            {
                "x_shape": [3, 16, 64, 64],
                "w_shape": [16, 16, 3, 3],
                "dy_shape": [3, 16, 62, 62],
                "data_format": "NCHW",
            },
            {
                "x_shape": [3, 32, 32, 16],
                "w_shape": [16, 16, 3, 3],
                "dy_shape": [3, 30, 30, 16],
                "data_format": "NHWC",
            },
            {
                "x_shape": [3, 64, 64, 16],
                "w_shape": [16, 16, 3, 3],
                "dy_shape": [3, 62, 62, 16],
                "data_format": "NHWC",
            },
        ]
        self.dtypes = [
            {
                "dtype": "float16",
                "max_relative_error": 1e-3,
            },
            {
                "dtype": "float32",
            },
        ]
        self.attrs = [
            {
                "stride": [1, 1],
                "padding": [0, 0],
                "dilation": [1, 1],
                "groups": 1,
            },
        ]


# Cause Conv2d backward_fliter mode do not support NHWC
class TestConv2dOpFP64(TestConv2dOpAll):
    def init_attrs(self):
        super().init_attrs()
        self.inputs = [
            {
                "x_shape": [3, 16, 32, 32],
                "w_shape": [16, 16, 3, 3],
                "dy_shape": [3, 16, 30, 30],
                "data_format": "NCHW",
            },
            {
                "x_shape": [3, 16, 64, 64],
                "w_shape": [16, 16, 3, 3],
                "dy_shape": [3, 16, 62, 62],
                "data_format": "NCHW",
            },
        ]
        self.dtypes = [
            {
                "dtype": "float64",
            },
        ]


if __name__ == "__main__":
    TestConv2dOpAll().run()
    TestConv2dOpFP64().run()
