# 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.
import tvm.testing
from tvm import relax
from tvm.ir.base import assert_structural_equal
from tvm.script import ir as I
from tvm.script import relax as R


@I.ir_module
class Module:
    @R.function
    def forward(
        x: R.Tensor((2, 4), "float32"),
        w: R.Tensor((4, 4), "float32"),
        b: R.Tensor((2, 4), "float32"),
    ) -> R.Tensor((2, 4), "float32"):
        with R.dataflow():
            lv: R.Tensor((2, 4), "float32") = R.matmul(x, w)
            out: R.Tensor((2, 4), "float32") = R.add(lv, b)
            R.output(out)
        return out


def test_l1_loss():
    N = 3
    C = 5
    predictions = relax.TensorType((N, C), "float32")
    targets = relax.TensorType((N, C), "float32")
    l1_loss = relax.training.loss.L1Loss()

    @R.function
    def expected(
        predictions: R.Tensor((3, 5), "float32"), targets: R.Tensor((3, 5), "float32")
    ) -> R.Tensor((), "float32"):
        R.func_attr({"global_symbol": "l1_loss"})
        with R.dataflow():
            lv: R.Tensor((3, 5), "float32") = R.subtract(predictions, targets)
            lv1: R.Tensor((3, 5), "float32") = R.abs(lv)
            gv: R.Tensor((), "float32") = R.mean(lv1, axis=None, keepdims=False)
            R.output(gv)
        return gv

    assert_structural_equal(l1_loss(predictions, targets), expected)


def test_l1_loss_append():
    s = Module["forward"].ret_ty
    l1_loss = relax.training.loss.L1Loss(reduction="sum")
    After = relax.training.AppendLoss("forward", l1_loss(s, s), l1_loss.num_backbone_outputs)(
        Module
    )

    @R.function
    def expected(
        x: R.Tensor((2, 4), "float32"),
        w: R.Tensor((4, 4), "float32"),
        b: R.Tensor((2, 4), "float32"),
        targets: R.Tensor((2, 4), "float32"),
    ) -> R.Tensor((), "float32"):
        R.func_attr({"global_symbol": "forward_loss"})
        with R.dataflow():
            lv: R.Tensor((2, 4), "float32") = R.matmul(x, w)
            out: R.Tensor((2, 4), "float32") = R.add(lv, b)
            lv1: R.Tensor((2, 4), "float32") = R.subtract(out, targets)
            lv11: R.Tensor((2, 4), "float32") = R.abs(lv1)
            gv: R.Tensor((), "float32") = R.sum(lv11, axis=None, keepdims=False)
            R.output(gv)
        return gv

    assert_structural_equal(After["forward_loss"], expected)


def test_mse_loss():
    N = 3
    C = 5
    predictions = relax.TensorType((N, C), "float32")
    targets = relax.TensorType((N, C), "float32")
    mse_loss = relax.training.loss.MSELoss()

    @R.function
    def expected(
        predictions: R.Tensor((3, 5), "float32"), targets: R.Tensor((3, 5), "float32")
    ) -> R.Tensor((), "float32"):
        R.func_attr({"global_symbol": "mse_loss"})
        with R.dataflow():
            lv: R.Tensor((3, 5), "float32") = R.subtract(predictions, targets)
            lv1: R.Tensor((3, 5), "float32") = R.multiply(lv, lv)
            gv: R.Tensor((), "float32") = R.mean(lv1, axis=None, keepdims=False)
            R.output(gv)
        return gv

    assert_structural_equal(mse_loss(predictions, targets), expected)


def test_mse_loss_append():
    s = Module["forward"].ret_ty
    mse_loss = relax.training.loss.MSELoss(reduction="sum")
    After = relax.training.AppendLoss("forward", mse_loss(s, s), mse_loss.num_backbone_outputs)(
        Module
    )

    @R.function
    def expected(
        x: R.Tensor((2, 4), "float32"),
        w: R.Tensor((4, 4), "float32"),
        b: R.Tensor((2, 4), "float32"),
        targets: R.Tensor((2, 4), "float32"),
    ) -> R.Tensor((), "float32"):
        R.func_attr({"global_symbol": "forward_loss"})
        with R.dataflow():
            lv: R.Tensor((2, 4), "float32") = R.matmul(x, w)
            out: R.Tensor((2, 4), "float32") = R.add(lv, b)
            lv1: R.Tensor((2, 4), "float32") = R.subtract(out, targets)
            lv11: R.Tensor((2, 4), "float32") = R.multiply(lv1, lv1)
            gv: R.Tensor((), "float32") = R.sum(lv11, axis=None, keepdims=False)
            R.output(gv)
        return gv

    assert_structural_equal(After["forward_loss"], expected)


def test_cross_entropy_loss():
    N = 3
    C = 5
    predictions = relax.TensorType((N, C), "float32")
    targets = relax.TensorType((N,), "int64")
    weights = relax.TensorType((C,), "float32")
    cross_entropy_loss = relax.training.loss.CrossEntropyLoss(reduction="sum", ignore_index=1)

    @R.function
    def expected(
        predictions: R.Tensor((3, 5), "float32"),
        targets: R.Tensor((3,), "int64"),
        weights: R.Tensor((5,), "float32"),
    ) -> R.Tensor((), "float32"):
        R.func_attr({"global_symbol": "cross_entropy_loss"})
        with R.dataflow():
            lv: R.Tensor((3, 5), "float32") = R.nn.log_softmax(predictions, axis=-1)
            gv: R.Tensor((), "float32") = R.nn.nll_loss(
                lv, targets, weights, reduction="sum", ignore_index=1
            )
            R.output(gv)
        return gv

    assert_structural_equal(cross_entropy_loss(predictions, targets, weights), expected)


def test_cross_entropy_loss_without_weights():
    N = 3
    C = 5
    predictions = relax.TensorType((N, C), "float32")
    targets = relax.TensorType((N,), "int64")
    cross_entropy_loss = relax.training.loss.CrossEntropyLoss()

    @R.function
    def expected(
        predictions: R.Tensor((3, 5), "float32"), targets: R.Tensor((3,), "int64")
    ) -> R.Tensor((), "float32"):
        R.func_attr({"global_symbol": "cross_entropy_loss"})
        with R.dataflow():
            lv: R.Tensor((3, 5), "float32") = R.nn.log_softmax(predictions, axis=-1)
            gv: R.Tensor((), "float32") = R.nn.nll_loss(
                lv, targets, reduction="mean", ignore_index=-100
            )
            R.output(gv)
        return gv

    assert_structural_equal(cross_entropy_loss(predictions, targets), expected)


def test_cross_entropy_loss_append():
    s = Module["forward"].ret_ty
    N = s.shape[0]
    C = s.shape[1]
    targets = relax.TensorType((N,), "int64")
    weights = relax.TensorType((C,), "float32")
    cross_entropy_loss = relax.training.loss.CrossEntropyLoss(reduction="sum", ignore_index=1)
    After = relax.training.AppendLoss(
        "forward", cross_entropy_loss(s, targets, weights), cross_entropy_loss.num_backbone_outputs
    )(Module)

    @R.function
    def expected(
        x: R.Tensor((2, 4), "float32"),
        w: R.Tensor((4, 4), "float32"),
        b: R.Tensor((2, 4), "float32"),
        targets: R.Tensor((2,), "int64"),
        weights: R.Tensor((4,), "float32"),
    ) -> R.Tensor((), "float32"):
        R.func_attr({"global_symbol": "forward_loss"})
        with R.dataflow():
            lv: R.Tensor((2, 4), "float32") = R.matmul(x, w)
            out: R.Tensor((2, 4), "float32") = R.add(lv, b)
            lv1: R.Tensor((2, 4), "float32") = R.nn.log_softmax(out, axis=-1)
            gv: R.Tensor((), "float32") = R.nn.nll_loss(
                lv1, targets, weights, reduction="sum", ignore_index=1
            )
            R.output(gv)
        return gv

    assert_structural_equal(After["forward_loss"], expected)


def test_categorical_cross_entropy_loss():
    N = 3
    C = 5
    predictions = relax.TensorType((N, C), "float32")
    targets = relax.TensorType((N, C), "int64")
    weights = relax.TensorType((C,), "float32")
    categorical_cross_entropy_loss = relax.training.loss.CategoricalCrossEntropyLoss(
        reduction="sum"
    )

    @R.function
    def expected(
        predictions: R.Tensor((3, 5), "float32"),
        targets: R.Tensor((3, 5), "int64"),
        weights: R.Tensor((5,), "float32"),
    ) -> R.Tensor((), "float32"):
        R.func_attr({"global_symbol": "categorical_cross_entropy_loss"})
        with R.dataflow():
            lv: R.Tensor((3, 5), "float32") = R.nn.log_softmax(predictions, axis=-1)
            lv: R.Tensor((), "float32") = -lv * targets.astype("float32")
            gv: R.Tensor((), "float32") = R.sum(lv * weights)
            R.output(gv)
        return gv

    assert_structural_equal(categorical_cross_entropy_loss(predictions, targets, weights), expected)


def test_categorical_cross_entropy_loss_without_weights():
    N = 3
    C = 5
    predictions = relax.TensorType((N, C), "float32")
    targets = relax.TensorType((N, C), "int64")
    categorical_cross_entropy_loss = relax.training.loss.CategoricalCrossEntropyLoss()

    @R.function
    def expected(
        predictions: R.Tensor((3, 5), "float32"), targets: R.Tensor((3, 5), "int64")
    ) -> R.Tensor((), "float32"):
        R.func_attr({"global_symbol": "categorical_cross_entropy_loss"})
        with R.dataflow():
            lv: R.Tensor((3, 5), "float32") = R.nn.log_softmax(predictions, axis=-1)
            gv: R.Tensor((), "float32") = R.mean(-lv * targets.astype("float32"))
            R.output(gv)
        return gv

    assert_structural_equal(categorical_cross_entropy_loss(predictions, targets), expected)


def test_categorical_cross_entropy_loss_with_ignore_index():
    N = 3
    C = 5
    predictions = relax.TensorType((N, C), "float32")
    targets = relax.TensorType((N, C), "int64")
    weights = relax.TensorType((C,), "float32")
    categorical_cross_entropy_loss = relax.training.loss.CategoricalCrossEntropyLoss(
        reduction="sum", ignore_index=1
    )

    @R.function
    def expected(
        predictions: R.Tensor((3, 5), "float32"),
        targets: R.Tensor((3, 5), "int64"),
        weights: R.Tensor((5,), "float32"),
    ) -> R.Tensor((), "float32"):
        R.func_attr({"global_symbol": "categorical_cross_entropy_loss"})
        with R.dataflow():
            lv: R.Tensor((3, 5), "float32") = R.nn.log_softmax(predictions, axis=-1)
            targets = relax.op.reshape(
                relax.op.argmax(targets, axis=1), shape=(targets.ty.shape[0],)
            )
            gv: R.Tensor((), "float32") = R.nn.nll_loss(
                lv, targets, weights, reduction="sum", ignore_index=1
            )
            R.output(gv)
        return gv

    assert_structural_equal(categorical_cross_entropy_loss(predictions, targets, weights), expected)


if __name__ == "__main__":
    tvm.testing.main()
