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

/*!
 * \file ir_utils.cc
 * \brief Helper functions to construct and compose IR nodes.
 */
#include "ir_utils.h"

#include <tvm/arith/analyzer.h>
#include <tvm/arith/int_solver.h>
#include <tvm/ffi/cast.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/ir/scope_stack.h>
#include <tvm/s_tir/stmt.h>
#include <tvm/tirx/layout.h>
#include <tvm/tirx/stmt_functor.h>
#include <tvm/tirx/transform.h>

#include <unordered_map>
#include <unordered_set>
#include <utility>

namespace tvm {
namespace tirx {

Stmt MergeNest(const std::vector<Stmt>& nest, Stmt body) {
  // use reverse iteration
  for (auto ri = nest.rbegin(); ri != nest.rend(); ++ri) {
    Stmt s = *ri;
    if (const auto* for_ = s.as<ForNode>()) {
      auto n = ffi::make_object<ForNode>(*for_);
      TVM_FFI_ICHECK(is_no_op(n->body));
      n->body = body;
      body = Stmt(n);
    } else if (const auto* bind = s.as<BindNode>()) {
      // Bind has no body -- prepend it before the accumulated body in a SeqStmt.
      body = SeqStmt::Flatten(ffi::GetRef<Stmt>(bind), body);
    } else if (const auto* attr = s.as<AttrStmtNode>()) {
      auto n = ffi::make_object<AttrStmtNode>(*attr);
      TVM_FFI_ICHECK(is_no_op(n->body));
      n->body = body;
      body = Stmt(n);
    } else if (const auto* ite = s.as<IfThenElseNode>()) {
      auto n = ffi::make_object<IfThenElseNode>(*ite);
      TVM_FFI_ICHECK(is_no_op(n->then_case));
      TVM_FFI_ICHECK(!n->else_case);
      n->then_case = body;
      body = Stmt(n);
    } else if (const auto* seq = s.as<SeqStmtNode>()) {
      auto n = ffi::make_object<SeqStmtNode>(*seq);
      TVM_FFI_ICHECK(n->size() != 0 && is_no_op(n->seq[n->size() - 1]));
      n->seq.Set(n->size() - 1, body);
      body = Stmt(n);
    } else if (s.as<AssertStmtNode>()) {
      body = SeqStmt({s, body});
    } else if (s.as<AllocBufferNode>() || s.as<DeclBufferNode>()) {
      body = SeqStmt::Flatten(s, body);
    } else {
      TVM_FFI_THROW(InternalError) << "not supported nest type";
    }
  }
  return body;
}

Stmt MergeNest(const std::vector<std::vector<Stmt>>& nest, Stmt body) {
  for (auto ri = nest.rbegin(); ri != nest.rend(); ++ri) {
    body = MergeNest(*ri, body);
  }
  return body;
}

class IRConvertSSA final : public StmtExprMutator {
 public:
  PrimFunc VisitPrimFunc(PrimFunc func) {
    // Remap parameters, if they were used in another function.
    // Function-scope remaps use function_scope_var_remap_ (not the scope stack),
    // because they persist across the entire function body.
    auto params = func->params.Map([&](const tirx::Var& var) -> tirx::Var {
      if (defined_.count(var.get())) {
        Var new_var = MakeNewVar(var);
        PushVarRemap(var, new_var);
        return new_var;
      } else {
        defined_.insert(var.get());
        return var;
      }
    });

    // Remap implicitly defined buffer parameters
    {
      std::unordered_set<const VarNode*> defined_params;
      for (const auto& var : func->params) {
        defined_params.insert(var.get());
      }
      for (const auto& [var, buffer] : func->buffer_map) {
        static_cast<void>(var);  // gcc 7.x bug, https://gcc.gnu.org/bugzilla/show_bug.cgi?id=81767
        auto check_expr = [&](const PrimExpr& expr) {
          auto* var_ptr = expr.as<VarNode>();
          if (!var_ptr) return;
          if (defined_params.count(var_ptr)) return;

          // Buffer_map shape vars use "match" semantics: first occurrence
          // defines the var, subsequent occurrences (in other buffers) are
          // just consistent uses of the same var -- not redefinitions.
          if (!defined_.count(var_ptr)) {
            defined_.insert(var_ptr);
          }
        };
        for (const auto& dim : buffer->shape) {
          check_expr(dim);
        }
        for (const auto& stride : buffer->strides) {
          check_expr(stride);
        }
        check_expr(buffer->elem_offset);
      }
    }

    // Update the buffer map, based on the redefined parameters
    auto buffer_map = [&]() {
      ffi::Map<Var, Buffer> buffer_map;
      bool made_change = false;
      for (const auto& [var, buffer] : func->buffer_map) {
        auto new_var = GetRemappedVar(var);
        if (defined_.count(buffer->data.get())) {
          Var new_data = MakeNewVar(buffer->data);
          PushVarRemap(buffer->data, new_data);
        } else {
          defined_.insert(buffer->data.get());
        }
        auto new_buf = GetRemappedBuffer(buffer);

        made_change = made_change || !var.same_as(new_var) || !buffer.same_as(new_buf);
        buffer_map.Set(new_var, new_buf);
      }
      if (made_change) {
        return buffer_map;
      } else {
        return func->buffer_map;
      }
    }();

    auto attrs = [&]() -> DictAttrs {
      ffi::Map<ffi::String, ffi::Any> dict;
      bool made_change = false;

      for (const auto& [key, old_value] : func->attrs->dict) {
        auto value = old_value;
        if (auto expr = value.as<PrimExpr>()) {
          value = VisitPrimExpr(expr.value());
        } else if (auto* stmt = value.as<StmtNode>()) {
          value = VisitStmt(ffi::GetRef<Stmt>(stmt));
        }

        made_change = made_change || !value.same_as(old_value);
        dict.Set(key, value);
      }

      if (made_change) {
        return DictAttrs(dict);
      } else {
        return func->attrs;
      }
    }();

    auto body = VisitStmt(func->body);

    // If anything changed, update the returned function
    if (!params.same_as(func->params) || !buffer_map.same_as(func->buffer_map) ||
        !attrs.same_as(func->attrs) || !body.same_as(func->body)) {
      func = PrimFunc(params, body, func->ret_type, buffer_map, attrs);
    }

    // Pop function-scope remaps in reverse order
    PopAllRemapsInCurrentScope();
    function_scope_var_remap_.clear();
    return func;
  }

  // Do not use the base VisitBufferDef for buffer remapping.
  //
  // IRConvertSSA has its own scoped buffer remapping via GetRemappedBuffer and
  // buf_remap_, which handles SSA conversion of buffer data vars, shape, strides,
  // and elem_offset with proper scope tracking. The base StmtMutator::VisitBufferDef
  // would create a conflicting second remap (into base buffer_remap_) when called
  // from the default DeclBuffer/AllocBuffer handlers, producing buffers with
  // undefined SSA-renamed variables.
  Buffer VisitBufferDef(const Buffer& buffer, bool alloc_data) override { return buffer; }

  Expr VisitExpr_(const VarNode* op) final { return GetRemappedVar(ffi::GetRef<Var>(op)); }
  Expr VisitExpr_(const LetNode* op) final {
    const Var& v = op->var;
    if (defined_.count(v.get())) {
      PrimExpr value = this->VisitPrimExpr(op->value);
      Var new_var = MakeNewVar(v);
      PushVarRemap(v, new_var);
      PrimExpr body = this->VisitPrimExpr(op->body);
      PopVarRemap(v, new_var);
      return Let(new_var, value, body);
    } else {
      defined_.insert(v.get());
      return StmtExprMutator::VisitExpr_(op);
    }
  }

  Expr VisitExpr_(const BufferLoadNode* op) final {
    auto node = StmtExprMutator::VisitExpr_(op).as_or_throw<BufferLoad>();
    auto output = VisitBufferAccess(std::move(node));
    return output;
  }

  Stmt VisitStmt_(const BufferStoreNode* op) final {
    auto node = StmtExprMutator::VisitStmt_(op).as_or_throw<BufferStore>();
    auto output = VisitBufferAccess(std::move(node));
    return output;
  }

  Stmt VisitStmt_(const DeclBufferNode* op) final {
    DeclBuffer decl = StmtExprMutator::VisitStmt_(op).as_or_throw<DeclBuffer>();
    Buffer new_buffer = GetRemappedBuffer(decl->buffer);
    if (!new_buffer.same_as(decl->buffer)) {
      decl.CopyOnWrite()->buffer = std::move(new_buffer);
    }
    return decl;
  }

  Stmt VisitStmt_(const SBlockNode* op) final {
    SBlock block = ffi::GetRef<SBlock>(op);

    // The SBlockNode is the point of definition for the IterVar
    // instances.  These re-defines must be present before visiting
    // the body of the SBlockNode.
    return scope_.WithNewScope([&]() -> Stmt {
      ffi::Array<IterVar> iter_vars = op->iter_vars.Map([&](IterVar iter_var) {
        if (defined_.count(iter_var->var.get())) {
          Var new_var = MakeNewVar(iter_var->var);
          PushVarRemap(iter_var->var, new_var);
          iter_var.CopyOnWrite()->var = new_var.as_or_throw<PrimVar>();
        } else {
          defined_.insert(iter_var->var.get());
        }
        return iter_var;
      });
      ffi::Array<BufferRegion> reads =
          block->reads.Map([&](const auto& region) { return VisitBufferAccess(region); });
      ffi::Array<BufferRegion> writes =
          block->writes.Map([&](const auto& region) { return VisitBufferAccess(region); });

      if (!reads.same_as(block->reads) || !writes.same_as(block->writes) ||
          !iter_vars.same_as(op->iter_vars)) {
        auto write_ptr = block.CopyOnWrite();
        write_ptr->reads = reads;
        write_ptr->writes = writes;
        write_ptr->iter_vars = iter_vars;
      }

      return StmtExprMutator::VisitStmt_(block.get()).as_or_throw<SBlock>();
    });
  }

  template <typename Node>
  Node VisitBufferAccess(Node node) {
    Buffer new_buf = GetRemappedBuffer(node->buffer);
    if (!new_buf.same_as(node->buffer)) {
      auto writer = node.CopyOnWrite();
      writer->buffer = new_buf;
    }

    return node;
  }

  Var GetRemappedVar(Var var) {
    if (auto it = var_remap_.find(var.get()); it != var_remap_.end() && it->second.size()) {
      return it->second.back();
    } else if (auto it = function_scope_var_remap_.find(var.get());
               it != function_scope_var_remap_.end()) {
      return it->second;
    } else {
      return var;
    }
  }

  Buffer GetRemappedBuffer(Buffer buf) {
    // Determine the buffer var that should be in the updated buffer,
    // given the current scope.  If no redefines are present, then the
    // buffer var is unchanged.
    Var new_buffer_var = GetRemappedVar(buf->data);
    PrimExpr elem_offset = VisitPrimExpr(buf->elem_offset);
    auto visit_expr = [this](const PrimExpr& expr) { return VisitPrimExpr(expr); };
    ffi::Array<PrimExpr> shape = buf->shape.Map(visit_expr);
    ffi::Array<PrimExpr> strides = buf->strides.Map(visit_expr);

    // Rewrite the layout's per-iter extent/stride expressions in lockstep
    // with the shape. If we don't, SSA-renamed shape vars end up as fresh
    // Vars while the layout still references the original, producing
    // structurally-unequal buffers whose shape and layout disagree (e.g.,
    // test_dynamic_launch_thread).
    ffi::Optional<Layout> new_layout = buf->layout;
    bool layout_changed = false;
    if (buf->layout.has_value()) {
      if (auto opt_tile = buf->layout.value().as<TileLayoutNode>()) {
        auto remap_iter = [&](const Iter& it) -> Iter {
          PrimExpr new_extent = VisitPrimExpr(it->extent);
          PrimExpr new_stride = VisitPrimExpr(it->stride);
          if (new_extent.same_as(it->extent) && new_stride.same_as(it->stride)) {
            return it;
          }
          return Iter(new_extent, new_stride, it->axis);
        };
        auto new_shard = opt_tile->shard.Map(remap_iter);
        auto new_replica = opt_tile->replica.Map(remap_iter);
        if (!new_shard.same_as(opt_tile->shard) || !new_replica.same_as(opt_tile->replica)) {
          new_layout = TileLayout(new_shard, new_replica, opt_tile->offset);
          layout_changed = true;
        }
      }
    }

    // If no mapping is required, return the original buffer.
    if (new_buffer_var.same_as(buf->data) && elem_offset.same_as(buf->elem_offset) &&
        shape.same_as(buf->shape) && strides.same_as(buf->strides) && !layout_changed) {
      return buf;
    }

    // If the current scope already has a mapping of this buffer, use
    // the mapped buffer.
    auto key = buf.get();
    std::vector<Buffer>& buffers = buf_remap_[key];
    if (buffers.size() && buffers.back()->data.same_as(new_buffer_var)) {
      return buffers.back();
    }

    // Otherwise, make and return a new buffer object that uses the
    // new buffer, pushing it onto the scoped stack of existing
    // buffers.  This will be popped when the new_buffer_var
    // redefinition is popped.
    Buffer new_buf = buf;
    {
      auto write_ptr = new_buf.CopyOnWrite();
      write_ptr->data = new_buffer_var;
      write_ptr->shape = shape;
      write_ptr->strides = strides;
      write_ptr->elem_offset = elem_offset;
      if (layout_changed) {
        write_ptr->layout = std::move(new_layout);
      }
    }
    buffers.push_back(new_buf);
    return new_buf;
  }

  Stmt VisitStmt_(const BindNode* op) final {
    // Bind var remaps are tracked in the current scope so they persist
    // across SeqStmt siblings and are cleaned up when the enclosing
    // body-carrying statement's scope exits.
    const Var& v = op->var;
    if (defined_.count(v.get())) {
      Expr value = this->VisitExpr(op->value);
      Var new_var = MakeNewVar(v);
      PushVarRemap(v, new_var);
      return Bind(new_var, value);
    } else {
      defined_.insert(v.get());
      return StmtExprMutator::VisitStmt_(op);
    }
  }

  Stmt VisitStmt_(const IfThenElseNode* op) final {
    // Each branch gets its own scope so Bind remaps in one branch
    // do not leak into the other.
    PrimExpr condition = VisitPrimExpr(op->condition);
    Stmt then_case = scope_.WithNewScope([&]() -> Stmt { return VisitStmt(op->then_case); });
    ffi::Optional<Stmt> else_case;
    if (op->else_case) {
      else_case = scope_.WithNewScope([&]() -> Stmt { return VisitStmt(op->else_case.value()); });
    }
    if (condition.same_as(op->condition) && then_case.same_as(op->then_case) &&
        else_case.same_as(op->else_case)) {
      return ffi::GetRef<Stmt>(op);
    }
    return IfThenElse(condition, then_case, else_case);
  }

  Stmt VisitStmt_(const ForNode* op) final {
    const Var& v = op->loop_var;
    if (defined_.count(v.get())) {
      return scope_.WithNewScope([&]() -> Stmt {
        Var new_var = MakeNewVar(v);
        PushVarRemap(v, new_var);
        Stmt stmt = StmtExprMutator::VisitStmt_(op);
        auto n = ffi::make_object<ForNode>(*stmt.as<ForNode>());
        n->loop_var = new_var.as_or_throw<PrimVar>();
        return For(n);
      });
    } else {
      defined_.insert(v.get());
      return scope_.WithNewScope([&]() -> Stmt { return StmtExprMutator::VisitStmt_(op); });
    }
  }
  Stmt VisitStmt_(const WhileNode* op) final {
    return scope_.WithNewScope([&]() -> Stmt { return StmtExprMutator::VisitStmt_(op); });
  }
  Stmt VisitStmt_(const AllocBufferNode* op) final {
    const Var& v = op->buffer->data;
    if (defined_.count(v.get())) {
      Var new_var = MakeNewVar(v);
      PushVarRemap(v, new_var);
      Stmt stmt = StmtExprMutator::VisitStmt_(op);
      op = stmt.as<AllocBufferNode>();
      // Use GetRemappedBuffer so that the AllocBuffer's buffer is the same
      // object as the one used by BufferStore/BufferLoad in subsequent siblings.
      Buffer new_buf = GetRemappedBuffer(op->buffer);
      if (!new_buf.same_as(op->buffer)) {
        auto node = stmt.as_or_throw<AllocBuffer>();
        node.CopyOnWrite()->buffer = std::move(new_buf);
        return node;
      }
      return stmt;
    } else {
      defined_.insert(v.get());
      return StmtExprMutator::VisitStmt_(op);
    }
  }
  Stmt VisitStmt_(const AttrStmtNode* op) final {
    if (const IterVarNode* iter_var = op->node.as<IterVarNode>()) {
      Range dom = iter_var->dom;
      if (dom.defined()) {
        auto min = VisitPrimExpr(dom->min);
        auto extent = VisitPrimExpr(dom->extent);
        if (!min.same_as(iter_var->dom->min) || !extent.same_as(iter_var->dom->extent)) {
          dom = Range::FromMinExtent(min, extent);
        }
      }

      Var var = iter_var->var;
      bool delayed_define = false;
      if (auto it = function_scope_var_remap_.find(var.get());
          it != function_scope_var_remap_.end()) {
        var = it->second;
      } else if (defined_.count(var.get())) {
        Var new_var(var->name_hint, var->ty);

        function_scope_var_remap_.insert({var.get(), new_var});
        var = new_var;
      } else {
        // The AttrStmt refers to an undefined variable.  This is
        // allowed for some attributes, such as
        // "pragma_parallel_launch_point", which annotates a variable
        // that is about to occur in a ForNode.  In these cases, the
        // ForNode and the AttrStmt must continue using the same
        // variable defintion.
        //
        // However, other AttrStmt, such as "thread_extent", act as
        // points of definition for the variable they annotate.  If
        // the variable has not been defined after visiting the body,
        // we should mark it as defined before exiting.  This ensures
        // correct de-duplication between multiple functions.
        //
        // This implementation may be simplified in the future by
        // moving "pragma_parallel_launch_point" to be an annotation
        // on the `ForNode`, rather than an `AttrStmt`.
        delayed_define = true;
      }

      IterVar new_iter_var;
      if (dom.same_as(iter_var->dom) && var.same_as(iter_var->var)) {
        new_iter_var = ffi::GetRef<IterVar>(iter_var);
      } else {
        new_iter_var = IterVar(dom, var.as_or_throw<PrimVar>(), iter_var->iter_type,
                               iter_var->thread_tag, iter_var->span);
      }

      auto value = VisitPrimExpr(op->value);
      auto body = scope_.WithNewScope([&]() -> Stmt { return VisitStmt(op->body); });

      Stmt output;
      if (new_iter_var.get() == iter_var && body.same_as(op->body) && value.same_as(op->value)) {
        output = ffi::GetRef<Stmt>(op);
      } else {
        output = AttrStmt(new_iter_var, op->attr_key, value, body, iter_var->span);
      }

      if (delayed_define) {
        if (!defined_.count(var.get())) {
          function_scope_var_remap_.insert({var.get(), var});
          defined_.insert(var.get());
        }
      }

      return output;

    } else if (const VarNode* v = op->node.as<VarNode>()) {
      Stmt stmt = scope_.WithNewScope([&]() -> Stmt { return StmtExprMutator::VisitStmt_(op); });
      op = stmt.as<AttrStmtNode>();
      if (var_remap_.count(v) && var_remap_[v].size() != 0) {
        return AttrStmt(var_remap_[v].back(), op->attr_key, op->value, op->body);
      } else {
        return stmt;
      }
    } else {
      return scope_.WithNewScope([&]() -> Stmt { return StmtExprMutator::VisitStmt_(op); });
    }
  }

 private:
  /*! \brief Record of a variable remap pushed to the current scope. */
  struct VarRemap {
    Var old_var;
    Var new_var;
  };

  /*! \brief Create a new variable with the same name and type as the original. */
  static Var MakeNewVar(const Var& old_var) { return Var(old_var->name_hint, old_var->ty); }

  /*! \brief Push a variable remap to the current scope and the var_remap_ stack. */
  void PushVarRemap(const Var& old_var, const Var& new_var) {
    var_remap_[old_var.get()].push_back(new_var);
    auto& level = scope_.Current();
    level.parent = this;
    level.push_back({old_var, new_var});
  }

  /*! \brief Pop a single variable remap (used for expression-level Let scoping). */
  void PopVarRemap(const Var& old_var, const Var& new_var) {
    var_remap_[old_var.get()].pop_back();
    for (auto& kv : buf_remap_) {
      std::vector<Buffer>& buffers = kv.second;
      if (buffers.size() && (buffers.back()->data.get() == new_var.get())) {
        buffers.pop_back();
      }
    }
    // Also remove from the current scope's tracking vector
    auto& current = scope_.Current();
    if (current.size() && current.back().new_var.same_as(new_var)) {
      current.pop_back();
    }
  }

  /*! \brief Pop all remaps in the current scope level (used for function-scope cleanup). */
  void PopAllRemapsInCurrentScope() {
    auto& current = scope_.Current();
    while (current.size()) {
      auto& remap = current.back();
      var_remap_[remap.old_var.get()].pop_back();
      for (auto& kv : buf_remap_) {
        std::vector<Buffer>& buffers = kv.second;
        if (buffers.size() && (buffers.back()->data.get() == remap.new_var.get())) {
          buffers.pop_back();
        }
      }
      current.pop_back();
    }
  }

  /*! \brief Scope stack: each scope level holds the remaps introduced in that scope.
   *
   * When a body-carrying statement (For, SBlock, Allocate) calls
   * scope_.WithNewScope([&]{...}), a new scope level is pushed.
   * Bind statements push their remaps to the current scope.
   * On scope exit, the destructor of std::vector<VarRemap> triggers,
   * and we undo all remaps in that level.
   *
   * Note: ScopeStack<T>::WithNewScope calls T's destructor on exit.
   * std::vector's destructor destroys elements but does NOT call custom
   * cleanup.  So we wrap the vector in ScopeLevel which handles cleanup.
   */
  struct ScopeLevel {
    std::vector<VarRemap> remaps;
    IRConvertSSA* parent{nullptr};

    void push_back(VarRemap remap) { remaps.push_back(std::move(remap)); }
    size_t size() const { return remaps.size(); }
    VarRemap& back() { return remaps.back(); }
    void pop_back() { remaps.pop_back(); }

    ~ScopeLevel() {
      if (!parent) return;
      // Pop remaps in reverse order
      while (remaps.size()) {
        auto& remap = remaps.back();
        parent->var_remap_[remap.old_var.get()].pop_back();
        for (auto& kv : parent->buf_remap_) {
          std::vector<Buffer>& buffers = kv.second;
          if (buffers.size() && (buffers.back()->data.get() == remap.new_var.get())) {
            buffers.pop_back();
          }
        }
        remaps.pop_back();
      }
    }

    ScopeLevel() = default;
    ScopeLevel(const ScopeLevel&) = delete;
    ScopeLevel& operator=(const ScopeLevel&) = delete;
    ScopeLevel(ScopeLevel&& other) noexcept
        : remaps(std::move(other.remaps)), parent(other.parent) {
      other.parent = nullptr;  // prevent other's destructor from popping
    }
    ScopeLevel& operator=(ScopeLevel&& other) noexcept {
      if (this != &other) {
        // Run our destructor logic first
        if (parent) {
          while (remaps.size()) {
            auto& remap = remaps.back();
            parent->var_remap_[remap.old_var.get()].pop_back();
            for (auto& kv : parent->buf_remap_) {
              std::vector<Buffer>& buffers = kv.second;
              if (buffers.size() && (buffers.back()->data.get() == remap.new_var.get())) {
                buffers.pop_back();
              }
            }
            remaps.pop_back();
          }
        }
        remaps = std::move(other.remaps);
        parent = other.parent;
        other.parent = nullptr;
      }
      return *this;
    }
  };

  std::unordered_map<const VarNode*, std::vector<Var>> var_remap_;
  std::unordered_set<const VarNode*> defined_;
  std::unordered_map<const BufferNode*, std::vector<Buffer>> buf_remap_;
  std::unordered_map<const VarNode*, Var> function_scope_var_remap_;
  ScopeStack<ScopeLevel> scope_;
};

Stmt ConvertSSA(Stmt stmt) { return IRConvertSSA()(std::move(stmt)); }

ffi::String GetPtrStorageScope(Var buffer_var) {
  const auto* ptr_type = buffer_var->ty.as<PointerTypeNode>();
  TVM_FFI_ICHECK(ptr_type) << "The provided variable is not of pointer type";
  return ptr_type->storage_scope;
}

ffi::Array<PrimExpr> GetBufferAllocationShape(const Buffer& buffer) {
  ffi::Array<PrimExpr> alloc_shape = buffer->shape;
  if (buffer->strides.size()) {
    TVM_FFI_ICHECK_EQ(buffer->shape.size(), buffer->strides.size());
    for (size_t i = buffer->strides.size() - 1; i > 0; --i) {
      TVM_FFI_ICHECK(arith::Analyzer()->CanProveEqual(
          floormod(buffer->strides[i - 1], buffer->strides[i]), 0));
      alloc_shape.Set(i, buffer->strides[i - 1] / buffer->strides[i]);
    }
  }
  return alloc_shape;
}

ffi::Array<PrimExpr> ConvertIndices(const MatchBufferRegion& match_buffer,
                                    const ffi::Array<PrimExpr>& indices) {
  const Buffer& target = match_buffer->buffer;
  const BufferRegion& source = match_buffer->source;
  TVM_FFI_ICHECK_EQ(indices.size(), target->shape.size());

  arith::Analyzer analyzer;
  ffi::Array<PrimExpr> result;
  result.reserve(source->region.size());
  size_t offset = source->region.size() - indices.size();
  for (size_t i = 0; i < offset; ++i) {
    const Range& range = source->region[i];
    TVM_FFI_ICHECK(analyzer->CanProve(range->extent == 1));
    result.push_back(range->min);
  }
  for (size_t i = 0; i < indices.size(); ++i) {
    const Range& range = source->region[i + offset];
    const PrimExpr& index = indices[i];
    result.push_back(range->min + index);
  }
  return result;
}

Region ConvertRegion(const MatchBufferRegion& match_buffer, const Region& region) {
  const Buffer& target = match_buffer->buffer;
  const BufferRegion& source = match_buffer->source;
  TVM_FFI_ICHECK_EQ(region.size(), target->shape.size());

  arith::Analyzer analyzer;
  Region result;
  result.reserve(source->region.size());
  size_t offset = source->region.size() - region.size();
  for (size_t i = 0; i < offset; ++i) {
    const Range& source_range = source->region[i];
    TVM_FFI_ICHECK(analyzer->CanProve(source_range->extent == 1));
    result.push_back(Range::FromMinExtent(source_range->min, 1));
  }
  for (size_t i = 0; i < region.size(); ++i) {
    const Range& source_range = source->region[i + offset];
    const Range& target_range = region[i];
    result.push_back(
        Range::FromMinExtent(source_range->min + target_range->min, target_range->extent));
  }
  return result;
}

ffi::Optional<arith::IntConstraints> ConditionalBoundsContext::TrySolveCondition() {
  // extract equations and related vars from condition expression.
  // currently only extract simple integral equations which could be solvable.
  arith::Analyzer analyzer;
  PrimExpr condition = analyzer->Simplify(condition_);
  if (is_const_int(condition)) {
    return std::nullopt;
  }
  ffi::Array<PrimExpr> equations;
  ffi::Array<Var> vars;
  std::function<void(const PrimExpr&)> fvisit = [&equations, &vars, &fvisit](const PrimExpr& e) {
    if (e->IsInstance<GENode>() || e->IsInstance<GTNode>() || e->IsInstance<LENode>() ||
        e->IsInstance<LTNode>() || e->IsInstance<EQNode>() || e->IsInstance<NENode>()) {
      bool is_simple = true;
      std::vector<Var> cand_vars;
      PostOrderVisit(e, [&cand_vars, &is_simple, &e](const ffi::ObjectRef& obj) {
        if (obj.same_as(e)) {
          return;
        } else if (const VarNode* var = obj.as<VarNode>()) {
          PrimType var_ty = var->ty.as_or_throw<PrimType>();
          if (var_ty.MatchesCode(DLDataTypeCode::kDLInt, DLDataTypeCode::kDLUInt)) {
            cand_vars.push_back(ffi::GetRef<Var>(var));
          }
        } else {
          is_simple &= obj->IsInstance<AddNode>() || obj->IsInstance<SubNode>() ||
                       obj->IsInstance<MulNode>() || obj->IsInstance<FloorDivNode>() ||
                       obj->IsInstance<FloorModNode>() || obj->IsInstance<IntImmNode>();
        }
      });
      if (is_simple && !cand_vars.empty()) {
        for (const Var& new_var : cand_vars) {
          if (!std::any_of(vars.begin(), vars.end(),
                           [&new_var](const Var& v) { return v.same_as(new_var); })) {
            vars.push_back(new_var);
          }
        }
        equations.push_back(e.as_or_throw<PrimExpr>());
      }
    } else if (e->IsInstance<AndNode>()) {
      And op = e.as_or_throw<And>();
      fvisit(op->a);
      fvisit(op->b);
    } else if (e->IsInstance<CallNode>()) {
      Call op = e.as_or_throw<Call>();
      if (op->op.same_as(builtin::likely())) {
        fvisit(op->args[0].as_or_throw<PrimExpr>());
      }
    }
  };
  fvisit(condition);
  if (equations.empty() || vars.empty()) {
    return std::nullopt;
  }
  // build dom ranges for related vars
  ffi::Map<Var, Range> ranges;
  for (const Var& v : vars) {
    arith::IntSet dom;
    auto relax_it = relax_map_->find(v.get());
    if (relax_it != relax_map_->end()) {
      dom = relax_it->second;
    } else {
      auto hint_it = hint_map_->find(v.get());
      if (hint_it != hint_map_->end()) {
        dom = hint_it->second;
      }
    }
    if (dom.defined()) {
      ranges.Set(v, Range::FromMinExtent(dom.min(), analyzer->Simplify(dom.max() - dom.min() + 1)));
    }
  }
  // solve constraints
  arith::IntConstraints constraint(vars, ranges, equations);
  arith::IntConstraints result = arith::SolveInequalitiesToRange(constraint);
  if (!result->relations.empty()) {
    return std::nullopt;
  }
  return result;
}

ConditionalBoundsContext::ConditionalBoundsContext(
    const PrimExpr& condition, std::unordered_map<const VarNode*, arith::IntSet>* relax_map,
    std::unordered_map<const VarNode*, arith::IntSet>* hint_map,
    std::vector<PrimExpr>* pending_conditions)
    : condition_(condition),
      relax_map_(relax_map),
      hint_map_(hint_map),
      pending_conditions_(pending_conditions),
      origin_pending_conditions_num_(pending_conditions->size()) {}

void ConditionalBoundsContext::EnterWithScope() {
  ffi::Optional<arith::IntConstraints> constraints = TrySolveCondition();
  if (!constraints.has_value()) {
    // fail to process the condition, add to unresolved
    pending_conditions_->push_back(condition_);
    return;
  }
  // update solved var ranges
  for (const auto& kv : constraints.value()->ranges) {
    const VarNode* var = kv.first.get();
    arith::IntSet new_dom = arith::IntSet::FromRange(kv.second);
    auto relax_it = relax_map_->find(var);
    if (relax_it != relax_map_->end()) {
      // this is a bound for relaxed var
      origin_map_.emplace(var, relax_it->second);
      relax_it->second = arith::Intersect({relax_it->second, new_dom});
    } else {
      // this is a bound for free var
      auto hint_it = hint_map_->find(var);
      if (hint_it != hint_map_->end()) {
        origin_map_.emplace(var, hint_it->second);
        hint_it->second = arith::Intersect({hint_it->second, new_dom});
      } else {
        origin_map_.emplace(var, arith::IntSet::Nothing());
        hint_map_->insert(hint_it, {var, new_dom});
      }
    }
  }
}

void ConditionalBoundsContext::ExitWithScope() {
  pending_conditions_->resize(origin_pending_conditions_num_);
  for (const auto& p : origin_map_) {
    const auto* var = p.first;
    auto relax_it = relax_map_->find(var);
    if (relax_it != relax_map_->end()) {
      // recover bound for relaxed var
      relax_it->second = p.second;
    } else {
      // recover bound for free var
      auto hint_it = hint_map_->find(var);
      TVM_FFI_ICHECK(hint_it != hint_map_->end());
      if (p.second.IsNothing()) {
        hint_map_->erase(hint_it);
      } else {
        hint_it->second = p.second;
      }
    }
  }
}

std::pair<PrimExpr, PrimExpr> GetAsyncWaitAttributes(const AttrStmtNode* op) {
  TVM_FFI_ICHECK(op && op->attr_key == s_tir::attr::async_wait_queue_scope);
  auto inner = op->body.as<AttrStmtNode>();
  TVM_FFI_ICHECK(inner && inner->attr_key == s_tir::attr::async_wait_inflight_count);
  return std::make_pair(op->value, inner->value);
}

/*! \brief Collect storage alignment information from annotations. */
class StorageAlignCollector : public StmtVisitor {
 private:
  friend std::unordered_map<Var, StorageAlignAnnotation> CollectStorageAlignAnnotation(
      const Stmt& body);

  /*! \brief For s-stir, the alignment annotations reside in block annotations. */
  void VisitStmt_(const SBlockNode* op) final {
    auto it = op->annotations.find(s_tir::attr::buffer_dim_align);
    if (it != op->annotations.end()) {
      auto storage_align_annotation = (*it).second.as_or_throw<StorageAlignAnnotation>();
      for (const auto& storage_align_tuple : storage_align_annotation) {
        int buffer_index = storage_align_tuple.get<0>();
        const Buffer& buffer = op->writes[buffer_index]->buffer;
        storage_align_[buffer->data].push_back(storage_align_tuple);
      }
    }
    StmtVisitor::VisitStmt_(op);
  }

  /*! \brief AllocBuffer: check for buffer_dim_align annotations. */
  void VisitStmt_(const AllocBufferNode* op) final {
    auto it = op->annotations.find(s_tir::attr::buffer_dim_align);
    if (it != op->annotations.end()) {
      auto storage_align_annotation = (*it).second.as_or_throw<StorageAlignAnnotation>();
      for (const auto& storage_align_tuple : storage_align_annotation) {
        int buffer_index = storage_align_tuple.get<0>();
        // the first buffer idx info is meaningless for alloc
        // stmt and should set as negative intentionally.
        TVM_FFI_ICHECK_EQ(buffer_index, -1);
        storage_align_[op->buffer->data].push_back(storage_align_tuple);
      }
    }
    StmtVisitor::VisitStmt_(op);
  }

  /*! \brief The map from buffer var to its storage alignment information. */
  std::unordered_map<Var, StorageAlignAnnotation> storage_align_;
};

std::unordered_map<Var, StorageAlignAnnotation> CollectStorageAlignAnnotation(const Stmt& body) {
  StorageAlignCollector collector;
  collector(body);
  return std::move(collector.storage_align_);
}

int Stoi(const std::string& str) {
  try {
    return std::stoi(str);
  } catch (std::invalid_argument& e) {
    TVM_FFI_THROW(InternalError) << "Cannot convert \"" << str << "\" to int";
    throw;
  }
}

std::pair<int32_t, int32_t> GetWmmaFragmentDimSize(const std::string& shape_str,
                                                   const std::string& scope) {
  size_t m, n, k;
  size_t last_pos = 0, pos = 0;
  pos = shape_str.find(", ", last_pos);
  m = Stoi(shape_str.substr(last_pos, pos - last_pos));
  last_pos = pos + 2;
  pos = shape_str.find(", ", last_pos);
  n = Stoi(shape_str.substr(last_pos, pos - last_pos));
  last_pos = pos + 2;
  k = Stoi(shape_str.substr(last_pos, shape_str.length() - last_pos));
  if (scope == "wmma.matrix_a") {
    return std::pair<int32_t, int32_t>(m, k);
  } else if (scope == "wmma.matrix_b") {
    return std::pair<int32_t, int32_t>(k, n);
  } else if (scope == "wmma.accumulator") {
    return std::pair<int32_t, int32_t>(m, n);
  }
  return std::pair<int32_t, int32_t>(0, 0);
}

std::optional<bool> IsHostFunc(const PrimFunc& func) {
  if (func->HasNonzeroAttr(tvm::tirx::attr::kIsHostFunc)) {
    return true;
  } else if (auto target = func->GetAttr<Target>(tvm::attr::kTarget)) {
    return target.value()->HasKey("cpu");
  } else {
    return std::nullopt;
  }
}

namespace transform {
Pass ConvertSSA() {
  auto pass_func = [](IRModule mod, PassContext ctx) {
    tirx::IRConvertSSA converter;
    ffi::Map<GlobalVar, BaseFunc> functions;
    bool made_change = false;
    for (auto [gvar, base_func] : mod->functions) {
      if (auto* ptr = base_func.as<tirx::PrimFuncNode>()) {
        auto updated = converter.VisitPrimFunc(ffi::GetRef<tirx::PrimFunc>(ptr));
        if (!updated.same_as(base_func)) {
          made_change = true;
          base_func = updated;
        }
      }
      functions.Set(gvar, base_func);
    }
    if (made_change) {
      mod.CopyOnWrite()->functions = std::move(functions);
    }
    return mod;
  };
  return tvm::transform::CreateModulePass(pass_func, 0, "tirx.ConvertSSA", {});
}

TVM_FFI_STATIC_INIT_BLOCK() {
  namespace refl = tvm::ffi::reflection;
  refl::GlobalDef().def("tirx.transform.ConvertSSA", ConvertSSA);
}

}  // namespace transform
}  // namespace tirx
}  // namespace tvm
