/*
 * 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 codegen_webgpu.cc
 */
#include "codegen_webgpu.h"

#include <tvm/arith/analyzer.h>
#include <tvm/ffi/cast.h>
#include <tvm/ffi/extra/json.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/support/io.h>
#include <tvm/tirx/builtin.h>
#include <tvm/tirx/transform.h>

#include <algorithm>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>

#include "../../../arith/pattern_match.h"
#include "../../../runtime/file_utils.h"
#include "../../../runtime/metadata.h"
#include "../../../runtime/thread_storage_scope.h"
#include "../../../support/bytes_io.h"
#include "../../../target/build_common.h"
#include "webgpu_fallback_module.h"

namespace tvm {
namespace codegen {

// WebGPU Info
struct WebGPUWorkGroupInfo {
  int workgroup_size[3] = {1, 1, 1};
  // whether we have ref to block index z is used.
  bool has_block_index_z{false};
  // set of handles that have write access
  std::unordered_set<Var> write_access_set;
};

class WebGPUWorkgroupInfoCollector : public StmtExprVisitor {
 public:
  static WebGPUWorkGroupInfo Collect(const Stmt& stmt) {
    WebGPUWorkgroupInfoCollector collector;
    collector(stmt);
    return collector.info_;
  }

 private:
  using StmtExprVisitor::VisitExpr_;

  void VisitExpr_(const VarNode* op) final {
    StmtExprVisitor::VisitExpr_(op);
    Var buffer_var = ffi::GetRef<Var>(op);
    if (buffer_var->ty.as<PointerTypeNode>()) {
      info_.write_access_set.insert(buffer_var);
    }
  }

  void VisitStmt_(const BufferStoreNode* op) final {
    StmtExprVisitor::VisitStmt_(op);
    info_.write_access_set.insert(op->buffer.var());
  }

  void VisitStmt_(const AttrStmtNode* op) final {
    // record workgroup size
    if (op->attr_key == tirx::attr::thread_extent) {
      IterVar iv = op->node.as_or_throw<IterVar>();
      if (iv->thread_tag.length() != 0) {
        runtime::ThreadScope ts = runtime::ThreadScope::Create(iv->thread_tag);
        if (ts.rank == 1) {
          TVM_FFI_ICHECK_GE(ts.dim_index, 0) << "vthread should have been optimized out by here";
          TVM_FFI_ICHECK_LT(ts.dim_index, 3);
          auto* sizeptr = op->value.as<tirx::IntImmNode>();
          TVM_FFI_ICHECK(sizeptr) << "CodeGenWebGPU: only allows constant thread group size "
                                  << " get " << op->value;
          info_.workgroup_size[ts.dim_index] = static_cast<uint32_t>(sizeptr->value);
        } else if (ts.rank == 0) {
          if (ts.dim_index == 2) {
            info_.has_block_index_z = true;
          }
        }
      }
    }
    // normal operation
    StmtExprVisitor::VisitStmt_(op);
  }
  WebGPUWorkGroupInfo info_;
};

std::string CodeGenWebGPU::Finish() {
  // Using f16 requires enable directive
  if (enable_fp16_) {
    header_stream << "enable f16;\n\n";
  }
  if (enable_subgroups_) {
    header_stream << "enable subgroups;\n\n";
  }
  return header_stream.str() + decl_stream.str() + this->fwd_decl_stream.str() + stream.str();
}

void CodeGenWebGPU::InitFuncState(const PrimFunc& f) {
  CodeGenC::InitFuncState(f);
  // analyze the data;
  for (Var arg : f->params) {
    if (arg->ty.as<PointerTypeNode>()) {
      alloc_storage_scope_[arg.get()] = "global";
    }
  }
}

CodeGenWebGPU::CodeGenWebGPU(Target target) : target_(target) {
  enable_subgroups_ = target_->GetAttr<bool>("supports_subgroups").value_or(false);
}

runtime::FunctionInfo CodeGenWebGPU::AddFunction(const PrimFunc& f, bool skip_readonly_decl) {
  // clear previous generated state.
  this->InitFuncState(f);
  // reserve keywords
  name_supply_->ReserveName("var");
  name_supply_->ReserveName("let");
  name_supply_->ReserveName("const");
  name_supply_->ReserveName("std");
  name_supply_->ReserveName("storage");
  name_supply_->ReserveName("uniform");
  name_supply_->ReserveName("workgroup");
  name_supply_->ReserveName("private");
  name_supply_->ReserveName("function");
  name_supply_->ReserveName("read");
  name_supply_->ReserveName("read_write");

  // skip the first underscore, so SSA variable starts from
  name_supply_->FreshName("v_");
  // Setup the thread group info.
  TVM_FFI_ICHECK_EQ(name_supply_->FreshName("threadIdx"), "threadIdx");
  TVM_FFI_ICHECK_EQ(name_supply_->FreshName("blockIdx"), "blockIdx");
  TVM_FFI_ICHECK_EQ(name_supply_->FreshName("gridDim"), "gridDim");

  // add to alloc buffer type.
  auto global_symbol = f->GetAttr<ffi::String>(tvm::attr::kGlobalSymbol);
  TVM_FFI_ICHECK(global_symbol.has_value())
      << "CodeGenWebGPU: Expect PrimFunc to have the global_symbol attribute";

  header_stream << "//----------------------------------------\n"
                << "// Function: " << global_symbol.value() << "\n"
                << "//----------------------------------------\n";
  ffi::String func_name = global_symbol.value();
  ffi::Array<DLDataType> runtime_arg_types;
  ffi::Array<ffi::String> func_launch_param_tags;

  // FunctionInfo stores runtime launch ABI dtypes rather than semantic IR Types.  WebGPU's
  // runtime classifies storage-buffer arguments by the serialized "handle" dtype.
  constexpr DLDataType kRuntimeOpaqueHandleType{kDLOpaqueHandle, 64, 1};

  WebGPUWorkGroupInfo info = WebGPUWorkgroupInfoCollector::Collect(f->body);

  std::vector<Var> pod_args;
  int num_buffer = 0;

  // add param_access modes info to launch params
  std::ostringstream os_param_access;
  os_param_access << "paramWriteAccess:[";
  // setup buffer argumemts
  for (Var arg : f->params) {
    if (auto* ptr = arg->ty.as<PointerTypeNode>()) {
      runtime_arg_types.push_back(kRuntimeOpaqueHandleType);
      auto* prim = ptr->element_type.as<PrimTypeNode>();
      TVM_FFI_ICHECK(prim)
          << "All handles passed to the CodeGenWebGPU must have a type_annotation as a "
             "PointerType, "
          << "and must point to a PrimType";
      PrimType value_storage_type = ffi::GetRef<PrimType>(prim);
      TVM_FFI_ICHECK(!value_storage_type.IsVoid())
          << "WebGPU storage buffers must have a concrete primitive element type";
      if (value_storage_type.MatchesCode(DLDataTypeCode::kDLBool)) {
        // We need a physically addressable buffer type to support boolean tensors.
        // The loaded byte is cast to bool inside the LoadNode visitor below.
        value_storage_type = boolean_storage_type_.WithLanes(value_storage_type.lanes());
      }
      std::string vid = AllocVarID(arg.get());
      std::string access_mode;
      if (num_buffer != 0) {
        os_param_access << ",";
      }
      if (skip_readonly_decl || info.write_access_set.count(arg)) {
        access_mode = "read_write";
        os_param_access << "1";
      } else {
        access_mode = "read";
        os_param_access << "0";
      }
      // add extra access mode info to launch params
      this->decl_stream << "@group(0) @binding(" << num_buffer++ << ") "
                        << "var<storage, " << access_mode << "> " << vid << " : array<";
      this->PrintType(value_storage_type, this->decl_stream);
      this->decl_stream << ">;\n";
    } else {
      PrimType pod_type = arg->ty.as_or_throw<PrimType>();
      TVM_FFI_ICHECK(!pod_type.IsVoid()) << "WebGPU POD arguments cannot have void type";
      runtime_arg_types.push_back(pod_type->dtype);
      pod_args.push_back(arg);
    }
  }

  // Store all pod arguments in a single buffer of int32
  // do bitcast to change to other data types
  // always pass gridDimX in to get around of the 65535 gridDim
  // restrictions in some platforms
  std::string type_pod_args = name_supply_->FreshName("PODArgs");
  std::string val_pod_args = name_supply_->FreshName("podArgs");
  std::string packGridDimX = name_supply_->FreshName("packGridDimX");

  this->decl_stream << "\nstruct " << type_pod_args << " {\n";

  for (size_t i = 0; i < pod_args.size(); ++i) {
    Var v = pod_args[i];
    PrimType value_type = v->ty.as_or_throw<PrimType>();
    TVM_FFI_ICHECK(!value_type.IsVoid());
    std::string vid = AllocVarID(v.get());

    if (value_type == PrimType::Int(32)) {
      this->decl_stream << "  " << vid << ": i32";
    } else if (value_type == PrimType::UInt(32)) {
      this->decl_stream << "  " << vid << ": u32";
    } else if (value_type == PrimType::Float(32)) {
      this->decl_stream << "  " << vid << ": f32";
    } else {
      TVM_FFI_THROW(InternalError) << "Do not support pod argument type " << value_type;
    }
    this->decl_stream << ",\n";
    // value ref
    std::ostringstream vref;
    vref << val_pod_args << "." << vid;
    var_idmap_[v.get()] = vref.str();
  }
  this->decl_stream << "  " << packGridDimX << ": u32\n}\n";

  this->decl_stream << "@group(0) @binding(" << num_buffer++ << ") "
                    << "var<uniform> " << val_pod_args << " : " << type_pod_args << ";\n\n";

  // setup thread tags and param access in launch param tags;
  if (auto opt = f->GetAttr<ffi::Array<ffi::String>>(tirx::attr::kKernelLaunchParams)) {
    for (const auto& thread_tag : opt.value()) {
      func_launch_param_tags.push_back(thread_tag);
    }
  }
  os_param_access << "]";
  func_launch_param_tags.push_back(os_param_access.str());

  TVM_FFI_ICHECK(!info.has_block_index_z)
      << "blockIdx.z is not supported in WebGPU to accomodate large blockIdx.x";
  // anotate workgroup
  this->stream << "@compute @workgroup_size(" << info.workgroup_size[0] << ", "
               << info.workgroup_size[1] << ", " << info.workgroup_size[2] << ")\n";

  // add to alloc buffer type.
  // Function header.
  this->stream << "fn " << func_name << "(\n"
               << "  @builtin(workgroup_id) blockIdx : vec3<u32>,\n"
               << "  @builtin(num_workgroups) gridDim : vec3<u32>,\n"
               << "  @builtin(local_invocation_id) threadIdx : vec3<u32>\n"
               << ") {\n";
  // skip out of bound grids
  this->stream << "  if (blockIdx.z * gridDim.x + blockIdx.x > "  // NOLINT(*)
               << val_pod_args << "." << packGridDimX << ") { return; }\n";
  // the function scope.
  int func_scope = this->BeginScope();
  this->PrintStmt(f->body);
  this->EndScope(func_scope);
  this->PrintIndent();
  this->stream << "}\n\n";
  return runtime::FunctionInfo(std::move(func_name), std::move(runtime_arg_types),
                               std::move(func_launch_param_tags), {});
}

void CodeGenWebGPU::BindThreadIndex(const IterVar& iv) {
  TVM_FFI_ICHECK(!var_idmap_.count(iv->var.get()));
  std::ostringstream os;
  PrintType(iv->var.ty(), os);
  if (iv->thread_tag == "blockIdx.x") {
    // WebGPU have restriction to limit the maximum size of blockId.x to be 65535
    // We allow runtime to spread the load out to blockIdx.z so it can be a large number.
    os << "(blockIdx.z * gridDim.x + blockIdx.x)";
    std::string tidx = os.str();
    std::string aggregated_bidx = SSAGetID(os.str(), iv->var.ty());
    var_idmap_[iv->var.get()] = aggregated_bidx;
  } else {
    os << "(" << iv->thread_tag << ")";
    std::string tidx = os.str();
    this->MarkConst(tidx);
    var_idmap_[iv->var.get()] = tidx;
  }
}

void CodeGenWebGPU::PrintType(const PrimType& t, std::ostream& os) {  // NOLINT(*)
  int lanes = t.lanes();
  if (t.IsVoid()) {
    os << "void";
    return;
  }
  if (t == PrimType::Bool()) {
    os << "bool";
    return;
  }

  if (lanes != 1) {
    TVM_FFI_ICHECK(lanes >= 2 && lanes <= 4)
        << "CodeGenWebGPU: only allows vector with lanes in {2, 3, 4}";
    // Currently WebGPU doesn't support `i8` and an `int8x4` is represented as a `u32`.
    if (t.MatchesCode(DLDataTypeCode::kDLInt) && t.bits() == 8 && lanes == 4) {
      os << "u32";
      return;
    }
    os << "vec" << lanes << "<";
  }

  if (t.code() == DLDataTypeCode::kDLFloat) {
    TVM_FFI_ICHECK(t.bits() == 16 || t.bits() == 32) << "CodeGenWebGPU: only support f16 or f32";
    if (t.bits() == 16) {
      // Using f16 requires enable directive
      enable_fp16_ = true;
    }
    os << "f" << t.bits();
  } else if (t.MatchesCode(DLDataTypeCode::kDLUInt)) {
    TVM_FFI_ICHECK(t.bits() != 64) << "CodeGenWebGPU: do not support u64";
    os << "u" << t.bits();
  } else if (t.MatchesCode(DLDataTypeCode::kDLInt)) {
    TVM_FFI_ICHECK(t.bits() != 64) << "CodeGenWebGPU: do not support i64";
    os << "i" << t.bits();
  } else {
    TVM_FFI_THROW(InternalError) << "CodeGenWebGPU: Cannot convert type "
                                 << ffi::DLDataTypeToString(t->dtype) << " to WebGPU type";
  }
  if (lanes != 1) {
    os << ">";
  }
}

void CodeGenWebGPU::PrintStorageSync(const CallNode* op) {
  const std::string& sync = op->args[0].as<StringImmNode>()->value;
  if (sync == "warp") {
    this->PrintIndent();
    this->stream << "workgroupBarrier();\n";
  } else if (sync == "shared") {
    this->PrintIndent();
    this->stream << "workgroupBarrier();\n";
  } else if (sync == "global") {
    TVM_FFI_THROW(InternalError) << "global barrier not supported";
  }
}

void CodeGenWebGPU::PrintSSAAssign(const std::string& target, const std::string& src,
                                   const Type& type) {
  auto prim_type = type.as<PrimType>();
  TVM_FFI_ICHECK(prim_type) << "WebGPU SSA values must have a primitive type, but got " << type;
  stream << "let " << target << " : ";
  PrintType(prim_type.value(), stream);
  stream << " = " << src << ";\n";
}

void CodeGenWebGPU::PrintVecElemLoad(const std::string& vec, const PrimType& t, int i,
                                     std::ostream& os) {  // NOLINT(*)
  os << vec << "[" << i << "]";
}

void CodeGenWebGPU::PrintVecElemStore(const std::string& vec, const PrimType& t, int i,
                                      const std::string& value) {
  this->PrintIndent();
  stream << vec << "[" << i << "] = " << value << ";\n";
}

void CodeGenWebGPU::VisitExpr_(const BroadcastNode* op, std::ostream& os) {  // NOLINT(*)
  std::string v = PrintExpr(op->value);
  int lanes = op->ty.as_or_throw<PrimType>().lanes();
  PrintType(op->ty.as_or_throw<PrimType>(), os);
  os << "(";
  for (int i = 0; i < lanes; ++i) {
    if (i != 0) os << ", ";
    os << v;
  }
  os << ')';
}

PrimExpr CodeGenWebGPU::EnforceU32(PrimExpr value) {
  return cast(PrimType::UInt(32, value.ty().lanes()), value);
}

void CodeGenWebGPU::VisitExpr_(const CallNode* op, std::ostream& os) {  // NOLINT(*)
  if (op->op.same_as(builtin::reinterpret())) {
    // generate bitcast<TYPE>(ARG)
    os << "bitcast<";
    this->PrintType(op->ty.as_or_throw<PrimType>(), os);
    os << ">(";
    this->PrintExpr(op->args[0], os);
    os << ")";
  } else if (op->op.same_as(builtin::shift_right())) {
    os << '(';
    this->PrintExpr(op->args[0], os);
    os << ">>";
    // WebGPU requires shift bits to be u32.
    this->PrintExpr(EnforceU32(op->args[1].as_or_throw<PrimExpr>()), os);
    os << ')';
  } else if (op->op.same_as(builtin::shift_left())) {
    os << '(';
    this->PrintExpr(op->args[0], os);
    os << "<<";
    // WebGPU requires shift bits to be u32.
    this->PrintExpr(EnforceU32(op->args[1].as_or_throw<PrimExpr>()), os);
    os << ')';
  } else if (op->op.same_as(builtin::if_then_else())) {
    // conditional that skips eval if cond evals to false
    std::string result = name_supply_->FreshName("condval");
    std::string cond = PrintExpr(op->args[0]);
    this->PrintIndent();
    this->stream << "var " << result << " : ";
    PrintType(op->ty.as_or_throw<PrimType>(), this->stream);
    this->stream << ";\n";
    this->PrintIndent();
    this->stream << "if (" << cond << ") {\n";
    {
      int then_scope = this->BeginScope();
      std::string true_val = PrintExpr(op->args[1]);
      this->PrintIndent();
      this->stream << result << " = " << true_val << ";\n} else {\n";
      this->EndScope(then_scope);
    }
    {
      int else_scope = this->BeginScope();
      std::string false_val = PrintExpr(op->args[2]);
      this->PrintIndent();
      this->stream << result << " = " << false_val << ";\n}\n";
      this->EndScope(else_scope);
    }
    os << result;
  } else if (op->op.same_as(builtin::dp4a())) {
    // generate `dot4I8Packed(vec1, vec2) + acc` for the builtin `dp4a`
    os << "dot4I8Packed(";
    this->PrintExpr(op->args[0], os);
    os << ", ";
    this->PrintExpr(op->args[1], os);
    os << ") + ";
    this->PrintExpr(op->args[2], os);
  } else {
    CodeGenC::VisitExpr_(op, os);
  }
}

void CodeGenWebGPU::VisitExpr_(const CastNode* op, std::ostream& os) {  // NOLINT(*)
  PrintType(op->ty.as_or_throw<PrimType>(), os);
  os << "(" << PrintExpr(op->value) << ")";
}

void CodeGenWebGPU::VisitExpr_(const SelectNode* op, std::ostream& os) {  // NOLINT(*)
  os << "select(" << PrintExpr(op->false_value) << ", " << PrintExpr(op->true_value) << ", "
     << PrintExpr(op->condition) << ")";
}

void CodeGenWebGPU::VisitExpr_(const LetNode* op, std::ostream& os) {  // NOLINT(*)
  // use ssa form.
  if (print_ssa_form_) {
    std::string value = PrintExpr(op->value);
    TVM_FFI_ICHECK(!var_idmap_.count(op->var.get()));
    var_idmap_[op->var.get()] = value;
  } else {
    PrintIndent();
    std::string value = PrintExpr(op->value);
    this->stream << "let " << AllocVarID(op->var.get()) << " : ";
    PrintType(op->var->ty, this->stream);
    this->stream << " = " << value << ";\n";
  }
  os << PrintExpr(op->body);
  // Pop the defined var from var_idmap when exiting its scope.
  // We do this because it is hard to completely avoid a same LetNode appearing
  // at different places.
  bool removed = var_idmap_.erase(op->var.get());
  TVM_FFI_ICHECK(removed);
}

void CodeGenWebGPU::VisitExpr_(const IntImmNode* op, std::ostream& os) {  // NOLINT(*)
  if (op->ty.as_or_throw<PrimType>().bits() == 32) {
    std::ostringstream temp;
    if (op->ty.as_or_throw<PrimType>().MatchesCode(DLDataTypeCode::kDLInt)) {
      temp << op->value << "i";
    } else {
      TVM_FFI_ICHECK(op->ty.as_or_throw<PrimType>().MatchesCode(DLDataTypeCode::kDLUInt));
      temp << op->value << "u";
    }
    this->MarkConst(temp.str());
    os << temp.str();
  } else {
    this->PrintType(op->ty.as_or_throw<PrimType>(), os);
    os << "(" << op->value << ")";
  }
}

void CodeGenWebGPU::VisitExpr_(const FloatImmNode* op, std::ostream& os) {  // NOLINT(*)
  std::ostringstream temp;
  temp << std::scientific << op->value;
  if (op->ty.as_or_throw<PrimType>().bits() == 32) {
    temp << 'f';
  } else if (op->ty.as_or_throw<PrimType>().bits() == 16) {
    // Using f16 requires enable directive
    enable_fp16_ = true;
    temp << 'h';
  } else {
    TVM_FFI_THROW(InternalError) << "Unsupported floating point bits "
                                 << op->ty.as_or_throw<PrimType>().bits();
  }
  MarkConst(temp.str());
  os << temp.str();
}

void CodeGenWebGPU::VisitExpr_(const BufferLoadNode* op, std::ostream& os) {  // NOLINT(*)
  // NOTE: direct impl of load/store for correctness
  // Each printing stmt must stand on their own after all preprocessing steps
  // to ensure correctness in the case of nested-expression
  // do not try to lift common printings from each case
  TVM_FFI_ICHECK_EQ(op->indices.size(), 1) << "Load from non-flat memory not supported.";
  TVM_FFI_ICHECK(!op->predicate.has_value()) << "Predicated buffer load is not supported.";

  PrimType value_ty = op->ty.as_or_throw<PrimType>();
  PrimExpr index = op->indices[0];
  Var buffer_var = op->buffer.var();
  const PrimType& element_ty = op->buffer->dtype;

  int lanes = value_ty.lanes();
  std::string buffer_vid = GetVarID(buffer_var.get());

  if (value_ty.lanes() == element_ty.lanes()) {
    // Direct buffer loading
    // Special handle bool loading
    if (value_ty == PrimType::Bool()) {
      this->PrintType(value_ty, os);
      os << "(";
    } else {
      TVM_FFI_ICHECK(value_ty == element_ty);
    }
    TVM_FFI_ICHECK_EQ(index.ty().lanes(), 1);
    os << buffer_vid << "[" << this->PrintExpr(index) << "]";
    // Special handle bool loading
    if (value_ty == PrimType::Bool()) {
      os << ")";
    }
  } else {
    // Vector load from scalar buffer
    TVM_FFI_ICHECK_EQ(element_ty.lanes(), 1) << "Can only vector load scalar array";
    TVM_FFI_ICHECK(value_ty.WithLanes(1) == element_ty)
        << "WebGPU vector loading requires base type to match";
    arith::PVar<PrimExpr> base;
    if (arith::ramp(base, 1, value_ty.lanes()).Match(index)) {
      // vec3<f32>(buf[base + 0], buf[base + 1], buf[base + 2]);
      std::string base_vid = SSAGetID(PrintExpr(base.Eval()), base.Eval().ty());
      PrintType(element_ty.WithLanes(value_ty.lanes()), os);
      os << "(";
      for (int i = 0; i < lanes; ++i) {
        if (i != 0) os << ", ";
        os << buffer_vid << "[" << base_vid << " + " << i << "]";
      }
      os << ")";
    } else {
      // vec3<f32>(buf[index[0]], buf[index[1]], buf[index[2]]);
      std::string index_vid = SSAGetID(PrintExpr(index), index.ty());
      PrintType(element_ty.WithLanes(value_ty.lanes()), os);
      os << "(";
      for (int i = 0; i < lanes; ++i) {
        if (i != 0) os << ", ";
        os << buffer_vid << "[" << index_vid << "[" << i << "]]";
      }
      os << ")";
    }
  }
}

void CodeGenWebGPU::VisitStmt_(const BindNode* op) {
  // use ssa form.
  if (print_ssa_form_) {
    std::string value = PrintExpr(op->value);
    TVM_FFI_ICHECK(!var_idmap_.count(op->var.get()));
    var_idmap_[op->var.get()] = value;
  } else {
    PrintIndent();
    std::string value = PrintExpr(op->value);
    this->stream << "let " << AllocVarID(op->var.get()) << " : ";
    PrintType(op->var->ty, this->stream);
    this->stream << " = " << value << ";\n";
  }
}

void CodeGenWebGPU::VisitStmt_(const BufferStoreNode* op) {
  TVM_FFI_ICHECK_EQ(op->indices.size(), 1) << "Store to non-flat memory not supported.";
  TVM_FFI_ICHECK(!op->predicate.has_value()) << "Predicated buffer store is not supported.";

  PrimType value_ty = op->value.ty();
  const PrimType& element_ty = op->buffer->dtype;
  PrimExpr index = op->indices[0];
  Var buffer_var = op->buffer.var();

  std::string buffer_vid = GetVarID(buffer_var.get());

  if (value_ty.lanes() == element_ty.lanes()) {
    // must execute print expr first
    // so we won't have recursive append to stream
    std::string index_vid = PrintExpr(index);
    std::string value_vid = PrintExpr(op->value);
    // now print the assignment line.
    this->PrintIndent();
    stream << buffer_vid << "[" << index_vid << "] = ";
    // special explicit conversion of bool
    if (value_ty == PrimType::Bool()) {
      PrintType(element_ty, stream);
      stream << "(";
    } else {
      TVM_FFI_ICHECK(value_ty == element_ty);
    }
    stream << value_vid;
    // Special handle bool store
    if (value_ty == PrimType::Bool()) {
      stream << ")";
    }
    stream << ";\n";
  } else {
    // Vector store into scalar buffer
    TVM_FFI_ICHECK_EQ(element_ty.lanes(), 1) << "Can only vector load scalar array";
    TVM_FFI_ICHECK(value_ty.WithLanes(1) == element_ty)
        << "WebGPU vector stire requires base type to match";
    std::string value_vid = PrintExpr(op->value);
    arith::PVar<PrimExpr> base;
    if (arith::ramp(base, 1, value_ty.lanes()).Match(index)) {
      // buf[base + 0] = value[0]
      // buf[base + 1] = value[1]
      std::string base_vid = SSAGetID(PrintExpr(base.Eval()), base.Eval().ty());
      for (int i = 0; i < value_ty.lanes(); ++i) {
        this->PrintIndent();
        stream << buffer_vid << "[" << base_vid << " + " << i << "] = " << value_vid << "[" << i
               << "];\n";
      }
    } else {
      // buf[index[0]] = value[0]
      // buf[index[1]] = value[1]
      std::string index_vid = SSAGetID(PrintExpr(index), index.ty());
      for (int i = 0; i < value_ty.lanes(); ++i) {
        this->PrintIndent();
        stream << buffer_vid << "[" << index_vid << "[" << i << "]] = " << value_vid << "[" << i
               << "];\n";
      }
    }
  }
}

void CodeGenWebGPU::VisitStmt_(const AllocBufferNode* op) {
  TVM_FFI_ICHECK(op->buffer.defined());
  std::string vid = AllocVarID(op->buffer.get());
  size_t constant_size = 1;
  for (const auto& dim : op->buffer->shape) {
    const IntImmNode* dim_imm = dim.as<IntImmNode>();
    TVM_FFI_ICHECK(dim_imm) << "Can only handle constant size stack allocation for now";
    constant_size *= dim_imm->value;
  }
  TVM_FFI_ICHECK_GT(constant_size, 0) << "Can only handle constant size stack allocation for now";
  auto storage_scope = runtime::StorageScope::Create(op->buffer.scope());

  if (storage_scope.rank == runtime::StorageRank::kShared) {
    this->decl_stream << "var<workgroup> " << vid << " : array<";
    PrintType(op->buffer->dtype, this->decl_stream);
    this->decl_stream << ", " << constant_size << ">;\n";
  } else if (storage_scope.rank == runtime::StorageRank::kLocal) {
    this->PrintIndent();
    this->stream << "var " << vid << " : array<";
    PrintType(op->buffer->dtype, this->stream);
    this->stream << ", " << constant_size << ">;\n";
  } else {
    TVM_FFI_THROW(InternalError) << "WebGPU: Do not support storage scope: "
                                 << storage_scope.to_string();
  }
}

void CodeGenWebGPU::VisitStmt_(const ForNode* op) {
  std::string begin_str = PrintExpr(op->min);
  PrimExpr end = is_zero(op->min) ? op->extent : arith::Analyzer()->Simplify(op->min + op->extent);
  std::string end_str = PrintExpr(end);
  std::string step_str = op->step.has_value() ? PrintExpr(*op->step) : "";
  std::string vid = AllocVarID(op->loop_var.get());
  PrintIndent();
  stream << "for (var " << vid << " : ";
  PrintType(op->loop_var.ty(), stream);
  stream << " = " << begin_str << "; " << vid << " < " << end_str << "; " << vid;
  if (step_str.empty()) {
    stream << "++";
  } else {
    stream << " += " << step_str;
  }
  stream << ") {\n";
  int for_scope = BeginScope();
  PrintStmt(op->body);
  this->EndScope(for_scope);
  PrintIndent();
  stream << "}\n";
}

void CodeGenWebGPU::VisitStmt_(const AssertStmtNode* op) {
  // skip assert — AssertStmt is a leaf, nothing to emit.
}

void CodeGenWebGPU::VisitStmt_(const WhileNode* op) {
  PrintIndent();
  stream << "while (true) {\n";
  int while_scope = BeginScope();
  std::string cond = PrintExpr(op->condition);
  PrintIndent();
  stream << "if (!(" << cond << ")) { break; }\n";
  PrintStmt(op->body);
  this->EndScope(while_scope);
  PrintIndent();
  stream << "}\n";
}

void CodeGenWebGPU::VisitStmt_(const BreakNode* op) {
  PrintIndent();
  stream << "break;\n";
}

void CodeGenWebGPU::VisitStmt_(const ContinueNode* op) {
  PrintIndent();
  stream << "continue;\n";
}

//-------------------------------------------------
// Build logic.
//-------------------------------------------------
//
// The "C++ side" canonical WebGPU module is `WebGPUFallbackModuleNode` in
// src/backend/webgpu/codegen/webgpu_fallback_module.{h,cc} — there is no native
// WebGPU runtime in the C++ tree (the real receiver is the wasm runtime
// in web/emcc/webgpu_runtime.cc).
ffi::Module BuildWebGPU(IRModule mod, Target target) {
  mod = tirx::transform::PointerValueTypeRewrite()(std::move(mod));
  bool output_ssa = false;
  bool skip_readonly_decl = false;
  ffi::Map<ffi::String, ffi::Bytes> smap;
  ffi::Map<ffi::String, runtime::FunctionInfo> fmap;
  std::ostringstream source_maker;

  // narrow all i64 to i32
  mod = tirx::transform::ForceNarrowIndexToInt32()(std::move(mod));

  for (auto kv : mod->functions) {
    CodeGenWebGPU cg(target);
    TVM_FFI_ICHECK(kv.second->IsInstance<PrimFuncNode>())
        << "CodeGenWebGPU: Can only take PrimFunc";
    auto f = kv.second.as_or_throw<PrimFunc>();
    auto calling_conv = f->GetAttr<CallingConv>(tvm::attr::kCallingConv);
    TVM_FFI_ICHECK(calling_conv.has_value())
        << "CodeGenWebGPU: expected kCallingConv attribute to be set.";
    TVM_FFI_ICHECK(calling_conv.value() == CallingConv::kDeviceKernelLaunch)
        << "CodeGenWebGPU: expect calling_conv equals CallingConv::kDeviceKernelLaunch, but got "
        << static_cast<int>(calling_conv.value());
    auto global_symbol = f->GetAttr<ffi::String>(tvm::attr::kGlobalSymbol);
    TVM_FFI_ICHECK(global_symbol.has_value())
        << "CodeGenWebGPU: Expect PrimFunc to have the global_symbol attribute";
    std::string f_name = global_symbol.value();
    cg.Init(output_ssa);
    fmap.Set(f_name, cg.AddFunction(f, skip_readonly_decl));
    std::string code = cg.Finish();
    source_maker << "// Function: " << f_name << "\n" << code << "\n";
    smap.Set(f_name, ffi::Bytes(std::move(code)));
  }

  // The aggregated WGSL source dump is preserved in the in-memory source
  // map keyed by "wgsl" — only used by InspectSource and never serialized.
  ffi::Map<ffi::String, ffi::String> source;
  source.Set("wgsl", source_maker.str());
  return target::WebGPUModuleCreateWithFallback(std::move(smap), ffi::String("wgsl"),
                                                std::move(fmap), std::move(source));
}

void RegisterWebGPUCodegen() {
  static bool registered = false;
  if (registered) return;
  registered = true;

  namespace refl = tvm::ffi::reflection;
  refl::GlobalDef().def("target.build.webgpu",
                        [](IRModule mod, Target target) { return BuildWebGPU(mod, target); });
}

}  // namespace codegen
}  // namespace tvm
