/*
 * 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 <tvm/ir/op.h>
#include <tvm/runtime/logging.h>
#include <tvm/script/ir_builder/ir/ir.h>
#include <tvm/tirx/builtin.h>
#include <tvm/tirx/exec_scope.h>
#include <tvm/tirx/function.h>
#include <tvm/tirx/op.h>
#include <tvm/tirx/script/builder/frame.h>
#include <tvm/tirx/stmt_functor.h>

#include "../../../tirx/ir/script/script_complete.h"
#include "./utils.h"

namespace tvm {
namespace script {
namespace ir_builder {
namespace tirx {

namespace {

// In s_tir functions, buffer-typed parameters must not carry a layout (the
// s_tir IR doesn't track per-buffer layouts on params). When `T.Buffer(...)` is
// used as a parameter annotation, the parser evaluates the annotation outside
// the PrimFunc frame; if the annotation captures an outer-scope variable (e.g.
// `dtype` in a closure-based generator), the evaluation happens *before*
// `_current_s_tir()` becomes true, so the resulting BufferVar is built with the
// default tile layout instead of None. Direct annotations using only literals
// are re-evaluated inside the frame and correctly get layout=None.
//
// This normalizer runs at PrimFunc construction time: it strips any defined
// layout from buffers in `buffer_map` / `root_alloc_buffers` and rewrites
// matching body references through the StmtExprMutator's built-in
// `buffer_remap_` machinery, so the body remains well-formed.
class STirBufferLayoutNormalizer : public tvm::tirx::StmtExprMutator {
 public:
  void Register(const tvm::tirx::BufferVar& old_buf, const tvm::tirx::BufferVar& new_buf) {
    this->buffer_remap_.Set(old_buf, new_buf);
  }
  bool Empty() const { return this->buffer_remap_.empty(); }
  tvm::tirx::BufferVar Lookup(const tvm::tirx::BufferVar& buf) const {
    auto it = this->buffer_remap_.find(buf);
    if (it != this->buffer_remap_.end()) {
      return (*it).second;
    }
    return buf;
  }
};

}  // namespace

TVM_FFI_STATIC_INIT_BLOCK() {
  TIRFrameNode::RegisterReflection();
  PrimFuncFrameNode::RegisterReflection();
  SBlockFrameNode::RegisterReflection();
  BlockInitFrameNode::RegisterReflection();
  ForFrameNode::RegisterReflection();
  AssertFrameNode::RegisterReflection();
  LaunchThreadFrameNode::RegisterReflection();
  AttrFrameNode::RegisterReflection();
  WhileFrameNode::RegisterReflection();
  IfFrameNode::RegisterReflection();
  ThenFrameNode::RegisterReflection();
  ElseFrameNode::RegisterReflection();
  ComposeOpFrameNode::RegisterReflection();
  DeclBufferFrameNode::RegisterReflection();
  AllocBufferFrameNode::RegisterReflection();
  HintFrameNode::RegisterReflection();
}

void PrimFuncFrameNode::ExitWithScope() {
  TIRFrameNode::ExitWithScope();
  // if the prim func is not private and there isn't already a global symbol,
  // add a global symbol
  auto insert_attr = [&](ffi::String key, ffi::Any value) {
    if (!attrs.defined()) {
      attrs = {{key, value}};
    } else if (!attrs.count(key)) {
      // copy over attributes (can't mutate the dict inside the optional in-place)
      ffi::Map<ffi::String, ffi::Any> new_attrs;
      for (auto kv : attrs) {
        new_attrs.Set(kv.first, kv.second);
      }
      new_attrs.Set(key, value);
      attrs = std::move(new_attrs);
    }
  };
  if (!is_private && name.has_value() && !attrs.count(tvm::attr::kGlobalSymbol)) {
    insert_attr(tvm::attr::kGlobalSymbol, name.value());
  }
  if (s_tir) {
    insert_attr(tvm::attr::kSTir, true);
  }
  if (persistent) {
    insert_attr(tvm::tirx::attr::kPersistentKernel, true);
  }
  // s_tir-mode normalization: drop stale default layouts (see comment on
  // STirBufferLayoutNormalizer above) and rewrite body references coherently.
  ffi::Array<tvm::tirx::BufferVar> effective_root_alloc_buffers = root_alloc_buffers;
  tvm::tirx::Stmt body = AsStmt(stmts);
  STirBufferLayoutNormalizer normalizer;
  ffi::Array<tvm::tirx::Var> effective_args;
  ffi::Map<tvm::tirx::Var, tvm::Expr> param_replacements;
  for (const tvm::tirx::Var& arg : args) {
    ffi::Optional<tvm::tirx::BufferVar> opt_buffer = buffer_map.Get(arg);
    bool replaces_legacy_param = opt_buffer.has_value();
    if (!opt_buffer.has_value() && arg->ty.as<tvm::tirx::BufferTypeNode>()) {
      opt_buffer = tvm::tirx::BufferVar(arg);
    }
    if (!opt_buffer.has_value()) {
      effective_args.push_back(arg);
      continue;
    }
    tvm::tirx::BufferVar buffer = opt_buffer.value();
    if (s_tir && buffer->layout.has_value()) {
      ffi::ObjectPtr<tvm::tirx::BufferTypeNode> type = tvm::tirx::CopyBufferType(buffer);
      type->layout = std::nullopt;
      tvm::tirx::BufferVar new_buffer = tvm::tirx::RebuildBufferVar(buffer, std::move(type));
      normalizer.Register(buffer, new_buffer);
      buffer = new_buffer;
    }
    effective_args.push_back(buffer.var());
    if (replaces_legacy_param && !arg.same_as(buffer.var()) &&
        !arg->ty.as<tvm::tirx::BufferTypeNode>()) {
      tvm::Expr data = buffer.data();
      param_replacements.Set(arg, ffi::StructuralEqual()(arg->ty, data->ty)
                                      ? data
                                      : tvm::reinterpret(arg->ty, std::move(data)));
    }
  }
  if (!normalizer.Empty()) {
    body = normalizer(std::move(body));
    ffi::Array<tvm::tirx::BufferVar> new_root_alloc_buffers;
    for (const tvm::tirx::BufferVar& buffer : root_alloc_buffers) {
      new_root_alloc_buffers.push_back(normalizer.Lookup(buffer));
    }
    effective_root_alloc_buffers = std::move(new_root_alloc_buffers);
  }
  if (!param_replacements.empty()) {
    body = tvm::tirx::Substitute(std::move(body), param_replacements);
  }
  tvm::tirx::PrimFunc func(
      /*params=*/effective_args,
      /*body=*/body,
      /*ret_type=*/ret_type.value_or(TupleType::Empty()),
      /*attrs=*/attrs.defined() ? DictAttrs(attrs) : DictAttrs(),
      /*span=*/tvm::Span());
  func = tvm::tirx::ScriptComplete(func, effective_root_alloc_buffers, s_tir);
  IRBuilder builder = IRBuilder::Current();
  if (builder->frames.empty()) {
    TVM_FFI_CHECK(!builder->result.has_value(), ValueError)
        << "Builder.result has already been set";
    builder->result = func;
  } else if (ffi::Optional<ir::IRModuleFrame> opt_frame = builder->FindFrame<ir::IRModuleFrame>()) {
    TVM_FFI_CHECK(name.has_value(), ValueError)
        << "The function name must be defined before exiting the "
           "function scope, if it's defined in a Module";
    const ir::IRModuleFrame& frame = opt_frame.value();
    const ffi::String& func_name = name.value_or("");
    if (!frame->global_var_map.count(func_name)) {
      // Case. First time visiting the function.
      ir::DeclFunction(func_name, func);
    }
    // Define the function.
    // Note we do checks to disallow redefinition of functions inside the `DefFunction`.
    ir::DefFunction(func_name, func);
  } else {
    TVM_FFI_THROW(ValueError) << "Cannot find where to insert PrimFunc";
  }
}

void SBlockFrameNode::ExitWithScope() {
  TIRFrameNode::ExitWithScope();

  // Allow SBlock construction in raw IRBuilder context (no enclosing PrimFuncFrame)
  // so test fixtures can construct blocks/block-realizes directly.

  ffi::Array<tvm::tirx::BufferVar> tir_alloc_buffers;
  for (const tvm::tirx::BufferVar& buffer : alloc_buffers) {
    tir_alloc_buffers.push_back(buffer);
  }
  ffi::Map<ffi::String, Any> attrs = annotations.value_or({});
  if (int detect_access = (!reads.has_value()) | (!writes.has_value() << 1)) {
    attrs.Set("tirx.script_parsing_detect_access", tvm::IntImm::Int64(detect_access));
  }
  tvm::tirx::SBlock block(iter_vars, reads.value_or(ffi::Array<tvm::tirx::BufferRegion>()),
                          writes.value_or(ffi::Array<tvm::tirx::BufferRegion>()), name,
                          AsStmt(stmts), init, tir_alloc_buffers, match_buffers, attrs,
                          tvm::Span());
  if (no_realize) {
    TVM_FFI_CHECK(iter_values.empty(), ValueError)
        << "Block bindings are not allowed when `no_realize=True`";
    TVM_FFI_CHECK(!predicate.has_value(), ValueError)
        << "`T.where` is not allowed when `no_realize=True`";
    AddToParent(block);
  } else {
    AddToParent(
        tvm::tirx::SBlockRealize(iter_values, predicate.value_or(IntImm::Bool(true)), block));
  }
}

void BlockInitFrameNode::EnterWithScope() {
  SBlockFrame frame = FindSBlockFrame("T.init");
  if (frame->init.has_value()) {
    TVM_FFI_THROW(ValueError) << "Duplicate block init declaration";
  }
  TIRFrameNode::EnterWithScope();
}

void BlockInitFrameNode::ExitWithScope() {
  TIRFrameNode::ExitWithScope();
  SBlockFrame frame = FindSBlockFrame("T.init");
  frame->init = AsStmt(stmts);
}

void ForFrameNode::ExitWithScope() {
  TIRFrameNode::ExitWithScope();
  AddToParent(this->f_make_for_loop(vars, doms, steps, AsStmt(stmts)));
}

void AssertFrameNode::ExitWithScope() {
  TIRFrameNode::ExitWithScope();
  if (stmts.empty()) {
    AddToParent(tvm::tirx::AssertStmt(condition, error_kind, message_parts));
  } else {
    ffi::Array<tvm::tirx::Stmt> seq;
    seq.push_back(tvm::tirx::AssertStmt(condition, error_kind, message_parts));
    for (const auto& stmt : stmts) {
      seq.push_back(stmt);
    }
    AddToParent(tvm::tirx::SeqStmt(seq));
  }
}

void LaunchThreadFrameNode::ExitWithScope() {
  TIRFrameNode::ExitWithScope();
  AddToParent(tvm::tirx::AttrStmt(iter_var, attr_key, extent, AsStmt(stmts)));
}

void AttrFrameNode::ExitWithScope() {
  TIRFrameNode::ExitWithScope();
  AddToParent(tvm::tirx::AttrStmt(node, attr_key, value, AsStmt(stmts)));
}

void WhileFrameNode::ExitWithScope() {
  TIRFrameNode::ExitWithScope();
  AddToParent(tvm::tirx::While(condition, AsStmt(stmts)));
}

void IfFrameNode::ExitWithScope() {
  TIRFrameNode::ExitWithScope();
  if (!stmts.empty()) {
    TVM_FFI_THROW(InternalError)
        << "stmt within IfThenElse frame should be either in ThenFrame or ElseFrame";
  }
  if (!then_stmts.has_value()) {
    TVM_FFI_THROW(InternalError) << "IfThenElse frame should have at least one then branch";
  }
  AddToParent(tvm::tirx::IfThenElse(
      condition, AsStmt(then_stmts.value()),
      else_stmts.has_value() ? AsStmt(else_stmts.value()) : tvm::tirx::Stmt(nullptr)));
}

void ThenFrameNode::EnterWithScope() {
  IfFrame frame = FindIfFrame("T.then_");
  if (frame->then_stmts.has_value()) {
    TVM_FFI_THROW(ValueError) << "Duplicate then branch declaration, previous one is "
                              << frame->then_stmts.value();
  }
  TIRFrameNode::EnterWithScope();
}

void ThenFrameNode::ExitWithScope() {
  TIRFrameNode::ExitWithScope();
  FindIfFrame("T.then_")->then_stmts = stmts;
}

void ElseFrameNode::EnterWithScope() {
  IfFrame frame = FindIfFrame("T.else_");
  if (!frame->then_stmts.has_value()) {
    TVM_FFI_THROW(InternalError) << "The else branch should follow then branch";
  }
  if (frame->else_stmts.has_value()) {
    TVM_FFI_THROW(ValueError) << "Duplicate else branch declaration, previous one is "
                              << frame->else_stmts.value();
  }
  TIRFrameNode::EnterWithScope();
}

void ElseFrameNode::ExitWithScope() {
  TIRFrameNode::ExitWithScope();
  FindIfFrame("T.else_")->else_stmts = stmts;
}

void DeclBufferFrameNode::ExitWithScope() {
  TIRFrameNode::ExitWithScope();
  if (allocated) {
    AddToParent(tvm::tirx::SeqStmt::Flatten(tvm::tirx::DeclBuffer(buffer, data), AsStmt(stmts)));
  } else {
    // data is undefined in `decl_buffer(...)`, lower to `alloc_buffer(...)`.
    AddToParent(tvm::tirx::SeqStmt::Flatten(tvm::tirx::AllocBuffer(buffer), AsStmt(stmts)));
  }
}

void ComposeOpFrameNode::ExitWithScope() {
  TIRFrameNode::ExitWithScope();
  ffi::Array<ffi::ObjectRef> ops;
  for (const auto& stmt : stmts) {
    auto op_call = stmt.as<tvm::tirx::TilePrimitiveCallNode>();
    TVM_FFI_ICHECK(op_call) << "ValueError: Only TIRx op calls allowed in ComposeOp. Violated by "
                            << stmt;
    ops.push_back(ffi::GetRef<tvm::tirx::TilePrimitiveCall>(op_call));
  }
  static const Op& compose_op_op = Op::Get("tirx.tile.compose_op");
  AddToParent(tvm::tirx::TilePrimitiveCall(compose_op_op, ops, workspace, config, dispatch));
}

void AllocBufferFrameNode::ExitWithScope() {
  TIRFrameNode::ExitWithScope();
  AddToParent(tvm::tirx::SeqStmt::Flatten(tvm::tirx::AllocBuffer(buffer), AsStmt(stmts)));
}

void HintFrameNode::ExitWithScope() {
  TIRFrameNode::ExitWithScope();
  // Always store attrs as a structured Map in the node field
  ffi::Map<ffi::String, Any> full_attrs;
  if (!message.empty()) {
    full_attrs.Set("message", ffi::String(message));
  }
  for (const auto& [k, v] : attrs) {
    full_attrs.Set(k, v);
  }
  AddToParent(tvm::tirx::AttrStmt(full_attrs, "tirx_hint", IntImm::Int32(1), AsStmt(stmts)));
}

}  // namespace tirx
}  // namespace ir_builder
}  // namespace script
}  // namespace tvm
