/*
 * 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/ir/module.h>
#include <tvm/ir/node_functor.h>
#include <tvm/runtime/logging.h>
#include <tvm/tirx/analysis.h>
#include <tvm/tirx/builtin.h>
#include <tvm/tirx/expr.h>
#include <tvm/tirx/expr_functor.h>
#include <tvm/tirx/function.h>
#include <tvm/tirx/op.h>
#include <tvm/tirx/stmt_functor.h>

TEST(IRF, Basic) {
  using namespace tvm;
  using namespace tvm::tirx;
  PrimVar x("x");
  auto z = x + 1;

  NodeFunctor<int(const ffi::ObjectRef& n, int b)> f;
  f.set_dispatch<VarNode>([](const ffi::ObjectRef& n, int b) { return b; });
  f.set_dispatch<AddNode>([](const ffi::ObjectRef& n, int b) { return b + 2; });
  TVM_FFI_ICHECK_EQ(f(x, 2), 2);
  TVM_FFI_ICHECK_EQ(f(z, 2), 4);
}

TEST(IRF, CountVar) {
  using namespace tvm;
  using namespace tvm::tirx;
  int n_var = 0;
  PrimVar x("x"), y("y");

  auto z = x + 1 + y + y;
  tirx::PostOrderVisit(z, [&n_var](const ffi::ObjectRef& n) {
    if (n.as<VarNode>()) ++n_var;
  });
  TVM_FFI_ICHECK_EQ(n_var, 2);
}

TEST(IRF, PreOrderVisit) {
  using namespace tvm;
  using namespace tvm::tirx;
  Stmt init =
      IfThenElse(IntImm::Bool(true), Evaluate(IntImm::Int32(0)), Evaluate(IntImm::Int32(0)));
  Stmt body = Evaluate(IntImm::Int32(1));
  SBlock block(/*iter_vars=*/{}, /*reads=*/{},
               /*writes=*/{}, /*name_hint=*/"block", /*body=*/body,
               /*init=*/init);
  bool init_visited = false;
  bool stopped_at_if = true;
  bool body_visited = false;
  PreOrderVisit(block, [&](const ffi::ObjectRef& n) -> bool {
    if (n->IsInstance<IfThenElseNode>()) {
      init_visited = true;
      return false;
    }
    if (const auto* eval = n.as<EvaluateNode>()) {
      if (const auto* int_imm = eval->value.as<IntImmNode>()) {
        if (int_imm->value == 0) {
          stopped_at_if = false;
        } else if (int_imm->value == 1) {
          body_visited = true;
        } else {
          TVM_FFI_THROW(InternalError) << "Unreachable";
        }
      }
    }
    return true;
  });
  ASSERT_EQ(init_visited, true);
  ASSERT_EQ(stopped_at_if, true);
  ASSERT_EQ(body_visited, true);
}

TEST(IRF, ExprTransform) {
  using namespace tvm;
  using namespace tvm::tirx;
  PrimVar x("x");
  auto z = x + 1;

  class MyExprFunctor : public tirx::ExprFunctor<int(const Expr&, int)> {
   public:
    int VisitExpr_(const VarNode* op, int b) final { return b; }
    int VisitExpr_(const IntImmNode* op, int b) final { return op->value; }
    int VisitExpr_(const AddNode* op, int b) final {
      return VisitExpr(op->a, b) + VisitExpr(op->b, b);
    }
  };
  MyExprFunctor f;
  TVM_FFI_ICHECK_EQ(f(x, 2), 2);
  TVM_FFI_ICHECK_EQ(f(z, 2), 3);
  try {
    f(z - 1, 2);
    TVM_FFI_THROW(InternalError) << "should fail";
  } catch (tvm::ffi::Error&) {
  }
}

TEST(IRF, ExprVisit) {
  using namespace tvm;
  using namespace tvm::tirx;
  PrimVar x("x");
  auto z = x + 1;

  class MyVisitor : public tirx::ExprFunctor<void(const Expr&)>,
                    public tirx::StmtFunctor<void(const Stmt&)> {
   public:
    int count = 0;
    // implementation
    void VisitExpr_(const VarNode* op) final { ++count; }
    void VisitExpr_(const IntImmNode* op) final {}
    void VisitExpr_(const AddNode* op) final {
      VisitExpr(op->a);
      VisitExpr(op->b);
    }
    void VisitStmt_(const EvaluateNode* op) final { VisitExpr(op->value); }
  };
  MyVisitor v;
  v.VisitStmt(Evaluate(z));
  TVM_FFI_ICHECK_EQ(v.count, 1);
}

TEST(IRF, StmtVisitor) {
  using namespace tvm;
  using namespace tvm::tirx;
  PrimVar x("x");
  class MyVisitor : public StmtExprVisitor {
   public:
    int count = 0;
    // implementation
    void VisitExpr_(const VarNode* op) final { ++count; }
  };
  MyVisitor v;
  auto fmaketest = [&]() {
    auto z = x + 1;
    Stmt eval_body = Evaluate(z);
    PrimType dtype = PrimType::Float(32);
    BufferVar buf("b", BufferType("global", dtype, {z, z}, {}, PrimExpr(), 0, 0));
    // AllocBuffer is flat (no body). Return as SeqStmt with eval.
    return SeqStmt({AllocBuffer(buf), eval_body});
  };
  v(fmaketest());
  // AllocBuffer visits buffer shape via VisitBufferDef.
  // shape = {z, z} where z = x + 1, so x is visited twice from shape + once from eval = 3
  TVM_FFI_ICHECK_EQ(v.count, 3);

  {
    // tests for block and block_realize
    Stmt body = fmaketest();
    PrimType dtype = PrimType::Float(32);
    tirx::Var buf_var("b", PointerType(dtype));
    BufferVar buffer = decl_buffer({16});
    body = SeqStmt({DeclBuffer(buffer, buf_var), std::move(body)});
    BufferRegion buffer_region(buffer, {Range::FromMinExtent(x + 1, 1)});
    MatchBufferRegion match_buffer_region(decl_buffer({1}), buffer_region);

    // construct block and block_realize
    SBlock block = SBlock({}, {buffer_region}, {buffer_region}, "block", body, body, {},
                          {match_buffer_region});
    Stmt block_realize = SBlockRealize({}, IntImm::Bool(true), block);

    v.count = 0;
    v(block_realize);
    // x visited in: reads range (1), writes range (1), match_buffers range (1),
    // init DeclBuffer(0) + AllocBuffer shape(2) + Evaluate(1) = 3,
    // body DeclBuffer(0) + AllocBuffer shape(2) + Evaluate(1) = 3.
    // The block's read/write BufferTypes each visit their dependent shape once,
    // in addition to the ranges, match buffer, init, and body.
    // Total: 2 + 2 + 1 + 3 + 3 = 11.
    TVM_FFI_ICHECK_EQ(v.count, 11);
  }
}

TEST(IRF, StmtMutator) {
  using namespace tvm;
  using namespace tvm::tirx;
  PrimVar x("x");

  class MyVisitor : public tirx::StmtMutator, public tirx::ExprMutator {
   public:
    using StmtMutator::operator();
    using ExprMutator::operator();

   protected:
    // implementation
    Expr VisitExpr_(const AddNode* op) final { return op->a; }
    Stmt VisitStmt_(const SeqStmtNode* op) final { return StmtMutator::VisitSeqStmt_(op, true); }
    Expr VisitExpr(const Expr& expr) final { return ExprMutator::VisitExpr(expr); }
  };
  auto fmakealloc = [&]() {
    auto z = x + 1;
    PrimType dtype = PrimType::Float(32);
    BufferVar buf("b", BufferType("global", dtype, {1, z}, {}, PrimExpr(), 0, 0));
    return AllocBuffer(buf);
  };

  auto fmakeif = [&]() {
    auto z = x + 1;
    Stmt body = Evaluate(z);
    return IfThenElse(x, Evaluate(0), body);
  };

  MyVisitor v;
  {
    auto alloc = fmakealloc();
    Stmt body2 = Evaluate(1);
    auto* bufptr = alloc.as<AllocBufferNode>()->buffer.get();
    ffi::Array<Stmt> arr{std::move(alloc), body2, body2};
    auto* arrptr = arr.get();
    arr.MutateByApply([&](Stmt s) { return v(std::move(s)); });
    TVM_FFI_ICHECK(arr.get() == arrptr);
    // buffer IS mutated now (AllocBuffer mutator visits buffer shape via VisitBufferDef)
    // shape was {1, x+1}, mutator transforms x+1 -> x, so buffer changes
    TVM_FFI_ICHECK(arr[0].as<AllocBufferNode>()->buffer.get() != bufptr);
  }
  {
    ffi::Array<Stmt> arr{fmakealloc()};
    // mutate array get reference by another one, trigger copy.
    ffi::Array<Stmt> arr2 = arr;
    auto* arrptr = arr.get();
    arr.MutateByApply([&](Stmt s) { return v(std::move(s)); });
    TVM_FFI_ICHECK(arr.get() != arrptr);
    // buffer is mutated in arr but not in arr2
    TVM_FFI_ICHECK(arr[0].as<AllocBufferNode>()->buffer.get() !=
                   arr2[0].as<AllocBufferNode>()->buffer.get());
    // mutate but no content change.
    arr2 = arr;
    arr.MutateByApply([&](Stmt s) { return v(std::move(s)); });
    TVM_FFI_ICHECK(arr2.get() == arr.get());
  }
  {
    ffi::Array<Stmt> arr{fmakeif()};
    arr.MutateByApply([&](Stmt s) { return v(std::move(s)); });
    TVM_FFI_ICHECK(arr[0].as<IfThenElseNode>()->else_case.as<EvaluateNode>()->value.same_as(x));
    // mutate but no content change.
    auto arr2 = arr;
    arr.MutateByApply([&](Stmt s) { return v(std::move(s)); });
    TVM_FFI_ICHECK(arr2.get() == arr.get());
  }

  {
    auto body =
        Evaluate(Call(PrimType::Int(32), builtin::call_extern(), {StringImm("xyz"), x + 1}));
    auto res = v(std::move(body));
    TVM_FFI_ICHECK(res.as<EvaluateNode>()->value.as<CallNode>()->args[1].same_as(x));
  }
  {
    Stmt body = fmakealloc();
    Stmt body2 = Evaluate(1);
    auto* ref2 = body2.get();
    auto* bufptr = body.as<AllocBufferNode>()->buffer.get();
    // construct a recursive SeqStmt.
    body = SeqStmt({body, body2});
    body = SeqStmt({body, body2});
    body = v(std::move(body));
    // the seq get flattened
    TVM_FFI_ICHECK(body.as<SeqStmtNode>()->size() == 3);
    // buffer is now mutated (shape x+1 -> x via VisitBufferDef)
    TVM_FFI_ICHECK(body.as<SeqStmtNode>()->seq[0].as<AllocBufferNode>()->buffer.get() != bufptr);
    TVM_FFI_ICHECK(body.as<SeqStmtNode>()->seq[1].get() == ref2);
  }

  {
    // Cannot cow because of bref
    Stmt body = fmakealloc();
    Stmt body2 = Evaluate(1);
    // construct a recursive SeqStmt.
    body = SeqStmt({body, body2});
    auto bref = body;
    body = SeqStmt({body, body2});
    body = v(std::move(body));
    // the seq get flattened
    TVM_FFI_ICHECK(body.as<SeqStmtNode>()->size() == 3);
    // buffer is mutated (shape x+1 -> x via VisitBufferDef)
    TVM_FFI_ICHECK(body.as<SeqStmtNode>()->seq[0].as<AllocBufferNode>() != nullptr);
    // bref still holds the old SeqStmt (not shared with new one due to copy)
    TVM_FFI_ICHECK(!bref.same_as(body));
  }

  {
    // tests for block and block_realize
    // AllocBuffer and DeclBuffer are flat (no body), placed as siblings in SeqStmt
    Stmt eval_body = Evaluate(x + 1);
    BufferVar buffer = decl_buffer({16});
    tirx::Var buffer_data("buffer_data", buffer.DataPointerType());
    Stmt decl = DeclBuffer(buffer, buffer_data);
    Stmt alloc = fmakealloc();
    // body is: DeclBuffer, AllocBuffer, Evaluate
    Stmt body = SeqStmt({decl, alloc, eval_body});
    BufferRegion buffer_region(buffer, {Range::FromMinExtent(x + 1, 1)});
    MatchBufferRegion match_buffer_region(decl_buffer({1}), buffer_region);
    // construct block and block_realize
    SBlock block = SBlock({}, {buffer_region}, {buffer_region}, "block", body, body, {},
                          {match_buffer_region});
    Stmt block_realize = SBlockRealize({}, IntImm::Bool(true), block);
    body = v(std::move(block_realize));
    // the body should be changed
    SBlock new_block = body.as<SBlockRealizeNode>()->block;
    // body is a SeqStmt; the Evaluate(x+1) -> Evaluate(x)
    auto* seq = new_block->body.as<SeqStmtNode>();
    TVM_FFI_ICHECK(seq != nullptr);
    TVM_FFI_ICHECK(seq->seq[2].as<EvaluateNode>()->value.same_as(x));
    auto* init_seq = new_block->init.value().as<SeqStmtNode>();
    TVM_FFI_ICHECK(init_seq != nullptr);
    TVM_FFI_ICHECK(init_seq->seq[2].as<EvaluateNode>()->value.same_as(x));
    // buffer region min is mutated: x+1 -> x
    TVM_FFI_ICHECK(new_block->reads[0]->region[0]->min.same_as(x));
    TVM_FFI_ICHECK(new_block->writes[0]->region[0]->min.same_as(x));
    TVM_FFI_ICHECK(new_block->match_buffers[0]->source->region[0]->min.same_as(x));
  }
}

TEST(IRF, Substitute) {
  using namespace tvm;
  using namespace tvm::tirx;
  PrimType dtype = PrimType::Float(32);
  tirx::Var x("x", PointerType(dtype, ""));
  PrimVar n("n", PrimType::Int(32));

  auto fmakebuffer = [&]() {
    return BufferVar("buf", BufferType(/*storage_scope=*/"global",
                                       /*dtype=*/PrimType::Float(32),
                                       /*shape=*/{n},
                                       /*strides=*/{},
                                       /*elem_offset=*/PrimExpr(),
                                       /*data_alignment=*/1,
                                       /*offset_factor=*/1));
  };

  {
    // Test substitution of an explicit DeclBuffer source and a dependent
    // BufferType shape.  Changing the type creates one fresh Var identity
    // that is shared by the declaration and every use.
    tirx::Var y = x.CopyWithSuffix("subst");
    PrimVar m("m", PrimType::Int(32));
    BufferVar buffer = fmakebuffer();
    Stmt store = BufferStore(buffer, FloatImm(dtype, 0), {IntImm::Int32(0)});
    Stmt decl = SeqStmt({DeclBuffer(buffer, x), store});
    auto f_subst = [&](const tirx::Var& var) -> ffi::Optional<Expr> {
      if (var.same_as(x)) return Expr(y);
      if (var.same_as(n)) return Expr(m);
      return std::nullopt;
    };
    Stmt new_decl = Substitute(decl, f_subst);
    auto* seq_node = new_decl.as<SeqStmtNode>();
    TVM_FFI_ICHECK(seq_node != nullptr);
    auto* decl_node = seq_node->seq[0].as<DeclBufferNode>();
    TVM_FFI_ICHECK(decl_node != nullptr);
    TVM_FFI_ICHECK(decl_node->data.same_as(y));
    TVM_FFI_ICHECK(decl_node->buffer->shape[0].same_as(m));
    TVM_FFI_ICHECK(!decl_node->buffer.same_as(buffer));
    auto* store_node = seq_node->seq[1].as<BufferStoreNode>();
    TVM_FFI_ICHECK(store_node != nullptr);
    TVM_FFI_ICHECK(store_node->buffer.same_as(decl_node->buffer));
  }

  {
    // test identity substitution on expression
    BufferVar buffer = fmakebuffer();
    PrimExpr expr = BufferLoad(buffer, {IntImm::Int32(0)});
    auto f_subst = [&](const tirx::Var& var) -> ffi::Optional<Expr> { return Expr(var); };
    PrimExpr new_expr = Substitute(expr, f_subst);
    // the expression is not changed
    TVM_FFI_ICHECK(new_expr.same_as(expr));
  }
}
