/*
 * 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.
 */

#include <gtest/gtest.h>
#include <tvm/ffi/dtype.h>
#include <tvm/ffi/extra/structural_equal.h>
#include <tvm/relax/block_builder.h>
#include <tvm/relax/nested_msg.h>
#include <tvm/relax/type.h>
#include <tvm/runtime/logging.h>
#include <tvm/tirx/expr.h>

#include <algorithm>
#include <array>
#include <cstring>
#include <functional>
#include <iterator>
#include <new>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

using namespace tvm;
using namespace tvm::relax;

namespace {

TensorType ScalarTensorType(PrimType dtype) {
  auto n = tvm::ffi::make_object<TensorTypeNode>();
  n->dtype = std::move(dtype);
  n->ndim = 0;
  return TensorType(n);
}

}  // namespace

TEST(NestedMsg, Basic) {
  // start with no annotation
  tvm::Var x("x", std::nullopt), y("y", std::nullopt);

  // constructor from array, T and nullopt.
  NestedMsg<relax::Expr> msg({x, std::nullopt, x});

  EXPECT_TRUE(msg.IsNested());
  EXPECT_FALSE(msg.IsLeaf());
  EXPECT_TRUE(msg != nullptr);

  EXPECT_ANY_THROW(msg.LeafValue());

  auto arr = msg.NestedArray();
  EXPECT_TRUE(arr[0].LeafValue().same_as(x));
  EXPECT_TRUE(arr[1] == nullptr);
  EXPECT_TRUE(arr[1].IsNull());

  EXPECT_TRUE(arr[2].LeafValue().same_as(x));

  auto a0 = arr[0];
  EXPECT_TRUE(a0.IsLeaf());

  // assignment
  // assign null
  a0 = std::nullopt;
  EXPECT_TRUE(a0 == nullptr);

  // assign array
  a0 = {x, {x, std::nullopt, y}};
  EXPECT_TRUE(a0.IsNested());
  auto t0 = a0.NestedArray()[1];
  EXPECT_TRUE(t0.IsNested());
  EXPECT_TRUE(t0.NestedArray()[2].LeafValue().same_as(y));

  // assign leaf
  a0 = x;

  EXPECT_TRUE(a0.IsLeaf());
  EXPECT_TRUE(a0.LeafValue().same_as(x));
}

TEST(NestedMsg, IntAndAny) {
  NestedMsg<int64_t> msg({1, std::nullopt, 2});
  Any any_msg = msg;
  NestedMsg<int64_t> msg2 = any_msg.cast<NestedMsg<int64_t>>();

  EXPECT_TRUE(msg2.IsNested());
  EXPECT_EQ(msg2.NestedArray()[0].LeafValue(), 1);
  EXPECT_TRUE(msg2.NestedArray()[1].IsNull());
  EXPECT_EQ(msg2.NestedArray()[2].LeafValue(), 2);
}

TEST(NestedMsg, ForEachLeaf) {
  tvm::Var x("x", std::nullopt), y("y", std::nullopt);
  NestedMsg<Expr> msg = {x, {x, y}, std::nullopt, {x, {x, y}}};

  int x_count = 0, y_count = 0;

  ForEachLeaf(msg, [&](const Expr& v) {
    if (v.same_as(x)) ++x_count;
    if (v.same_as(y)) ++y_count;
  });
  EXPECT_EQ(x_count, 4);
  EXPECT_EQ(y_count, 2);
}

TEST(NestedMsg, Equal) {
  tvm::Var x("x", std::nullopt), y("y", std::nullopt);
  tvm::Var z("z", std::nullopt);

  auto fequal = [](Expr lhs, Expr rhs) { return lhs.same_as(rhs); };

  using M = NestedMsg<relax::Expr>;

  EXPECT_TRUE(Equal(M(std::nullopt), M(std::nullopt), fequal));

  EXPECT_TRUE(Equal(M(x), M(x), fequal));

  EXPECT_TRUE(Equal(M({x, y}), M({x, y}), fequal));

  EXPECT_TRUE(Equal(M({x, std::nullopt}), M({x, std::nullopt}), fequal));

  EXPECT_TRUE(Equal(M({x, {std::nullopt, y}}), M({x, {std::nullopt, y}}), fequal));

  EXPECT_TRUE(Equal(M({x, {std::nullopt, y}, {x, z}}), M({x, {std::nullopt, y}, {x, z}}), fequal));

  // type mismatch
  EXPECT_FALSE(Equal(M({x, {std::nullopt, y}, x}), M({x, {std::nullopt, y}, {x, z}}), fequal));

  EXPECT_FALSE(Equal(M({x, {std::nullopt, y}, {x, std::nullopt}}),
                     M({x, {std::nullopt, y}, {x, z}}), fequal));

  EXPECT_FALSE(Equal(M({x, {std::nullopt, y}}), M({x, {std::nullopt, y}, {x, z}}), fequal));

  EXPECT_FALSE(Equal(M(x), M(std::nullopt), fequal));

  EXPECT_FALSE(Equal(M(std::nullopt), M(x), fequal));

  EXPECT_FALSE(Equal(M(x), M(ffi::Array<M>({x})), fequal));

  EXPECT_FALSE(Equal(M(ffi::Array<M>({x})), M(x), fequal));
}

TEST(NestedMsg, MapAndDecompose) {
  tvm::Var x("x", PrimType::Int(16));
  tvm::Var y("y", PrimType::Int(32));
  tvm::Var z("z", PrimType::Int(64));

  BlockBuilder bb = BlockBuilder::Create(std::nullopt);
  relax::Expr t0 = bb->Normalize(Tuple({x, y}));
  relax::Expr t1 = bb->Normalize(Tuple({t0, x, z, t0}));

  auto c0 = IntImm::Int32(0);
  auto c1 = IntImm::Int32(1);
  auto c2 = IntImm::Int32(2);

  auto output = MapToNestedMsg<IntImm>(t1, [&](Expr value) {
    if (value.same_as(x)) return c0;
    if (value.same_as(y)) return c1;
    return c2;
  });

  NestedMsg<IntImm> expected = {{c0, c1}, c0, c2, {c0, c1}};

  EXPECT_TRUE(Equal(output, expected,
                    [](IntImm lhs, IntImm rhs) -> bool { return lhs->value == rhs->value; }));

  auto output2 = MapToNestedMsg<IntImm>(GetType(t1), [&](Type ty) -> NestedMsg<IntImm> {
    const auto* prim_ty = ty.as<PrimTypeNode>();
    if (prim_ty == nullptr) return std::nullopt;
    int bits = prim_ty->dtype.bits;
    if (bits == 16) return c0;
    if (bits == 32) return c1;
    if (bits == 64) return c2;
    return std::nullopt;
  });

  EXPECT_TRUE(Equal(output2, expected,
                    [](IntImm lhs, IntImm rhs) -> bool { return lhs->value == rhs->value; }));

  int x_count = 0, y_count = 0, z_count = 0;

  DecomposeNestedMsg(t1, expected, [&](Expr value, NestedMsg<IntImm> msg) {
    if (value.same_as(x)) {
      EXPECT_TRUE(msg.LeafValue().same_as(c0));
      ++x_count;
    } else if (value.same_as(y)) {
      EXPECT_TRUE(msg.LeafValue().same_as(c1));
      ++y_count;
    } else {
      EXPECT_TRUE(msg.LeafValue().same_as(c2));
      ++z_count;
    }
  });
  EXPECT_EQ(x_count, 3);
  EXPECT_EQ(y_count, 2);
  EXPECT_EQ(z_count, 1);
}

TEST(NestedMsg, MapToNestedMsgByType) {
  auto sf0 = ScalarTensorType(PrimType::Float(32));
  auto sf1 = TupleType({sf0, sf0});
  auto sf2 = TupleType({sf0, sf0});
  auto x = tvm::Var("x", TupleType({sf1, sf2, sf0}));

  auto msg = MapToNestedMsgByType<Expr>(x, [](Expr value) { return value; });

  EXPECT_TRUE(msg.IsNested());
  auto arr = msg.NestedArray();

  EXPECT_TRUE(arr[1].IsNested());
  auto arr1 = arr[1].NestedArray();

  EXPECT_TRUE(arr1[0].IsLeaf());
  EXPECT_TRUE(
      tvm::ffi::StructuralEqual()(arr1[0].LeafValue(), TupleGetItem(TupleGetItem(x, 1), 0)));

  EXPECT_TRUE(arr[2].IsLeaf());
  EXPECT_TRUE(tvm::ffi::StructuralEqual()(arr[2].LeafValue(), TupleGetItem(x, 2)));
}

TEST(NestedMsg, NestedMsgToExpr) {
  auto sf0 = ScalarTensorType(PrimType::Float(32));
  auto sf1 = TupleType({sf0, sf0});

  auto c0 = IntImm::Int32(0);
  auto c1 = IntImm::Int32(1);
  auto c2 = IntImm::Int32(2);

  tvm::Var x("x", sf0), y("y", sf0), z("z", sf0);

  NestedMsg<IntImm> msg = {c0, {c0, c1}, {c0, {c1, c2}}};
  auto expr = NestedMsgToExpr<IntImm>(msg, [&](ffi::Optional<IntImm> leaf) {
    TVM_FFI_ICHECK(leaf.has_value());
    int value = leaf.value()->value;
    switch (value) {
      case 0:
        return x;
      case 1:
        return y;
      default:
        return z;
    }
  });

  Expr expected = Tuple({x, Tuple({x, y}), Tuple({x, Tuple({y, z})})});
  EXPECT_TRUE(tvm::ffi::StructuralEqual()(expr, expected));

  // test simplified
  tvm::Var t("t", sf1);
  NestedMsg<Expr> msg1 = {TupleGetItem(t, 0), TupleGetItem(t, 1)};
  auto expr1 = NestedMsgToExpr<Expr>(msg1, [](ffi::Optional<Expr> leaf) { return leaf.value(); });
  EXPECT_TRUE(tvm::ffi::StructuralEqual()(expr1, t));
}

TEST(NestedMsg, CombineNestedMsg) {
  auto c0 = IntImm::Int32(0);
  auto c1 = IntImm::Int32(1);
  auto c2 = IntImm::Int32(2);

  NestedMsg<IntImm> lhs = {c0, {c0, c1}, std::nullopt, {c0, {c1, c2}}};
  NestedMsg<IntImm> rhs = {c1, {c2, std::nullopt}, std::nullopt, {c1, {c2, c2}}};
  NestedMsg<IntImm> expected = {c1, {c2, c1}, std::nullopt, {c1, {c2, c2}}};

  auto output = CombineNestedMsg(lhs, rhs, [](IntImm x, IntImm y) {
    if (x->value > y->value) return x;
    return y;
  });

  EXPECT_TRUE(Equal(output, expected,
                    [](IntImm lhs, IntImm rhs) -> bool { return lhs->value == rhs->value; }));
}

TEST(NestedMsg, MapNestedMsg) {
  auto c0 = IntImm::Int32(0);
  auto c1 = IntImm::Int32(1);
  auto c2 = IntImm::Int32(2);
  auto c3 = IntImm::Int32(3);

  NestedMsg<IntImm> msg = {c0, {c0, c1}, std::nullopt, {c0, {c2, c1}}};
  NestedMsg<IntImm> expected = {c3, {c3, std::nullopt}, std::nullopt, {c3, {c2, std::nullopt}}};

  auto output = MapNestedMsg(msg, [](IntImm x) {
    if (x->value == 0) {
      return NestedMsg<IntImm>(IntImm::Int32(3));
    } else if (x->value == 1) {
      return NestedMsg<IntImm>();
    } else {
      return NestedMsg<IntImm>(x);
    }
  });

  EXPECT_TRUE(Equal(output, expected,
                    [](IntImm lhs, IntImm rhs) -> bool { return lhs->value == rhs->value; }));
}

TEST(NestedMsg, TransformTupleLeaf) {
  auto c0 = IntImm::Int32(0);
  auto c1 = IntImm::Int32(1);
  auto c2 = IntImm::Int32(2);
  using NInt = NestedMsg<IntImm>;

  NInt msg1 = {c0, {c0, c1}, c2, {c0, {c1, c2}}};
  NInt msg2 = {c1, {c2, c0}, c2, {c1, {c2, c0}}};

  PrimType s = PrimType::Int(32);
  tvm::Var x("x", s), y("y", s), z("z", s);
  BlockBuilder bb = BlockBuilder::Create(std::nullopt);
  Expr expr = bb->Normalize(Tuple({x, Tuple({x, x}), x, Tuple({x, Tuple({x, x})})}));

  auto ftransleaf = [&](Expr value, std::array<NInt, 2> msgs) -> Expr {
    int lhs = msgs[0].LeafValue().as_or_throw<IntImm>()->value;
    int rhs = msgs[1].LeafValue().as_or_throw<IntImm>()->value;
    if (lhs > rhs)
      return z;
    else if (lhs == rhs)
      return value;
    else
      return y;
  };

  Expr expected = Tuple({y, Tuple({y, z}), x, Tuple({y, Tuple({y, z})})});

  EXPECT_TRUE(tvm::ffi::StructuralEqual()(
      TransformTupleLeaf(expr, std::array<NInt, 2>({msg1, msg2}), ftransleaf), expected));

  EXPECT_TRUE(
      expr.same_as(TransformTupleLeaf(expr, std::array<NInt, 2>({msg1, msg1}), ftransleaf)));
}
