/*
 * 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 src/lang/data_layout.cc
 * \brief Data SLayout expression.
 */
#include <tvm/arith/analyzer.h>
#include <tvm/ffi/dtype.h>
#include <tvm/ffi/function.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/ir/expr.h>
#include <tvm/runtime/logging.h>
#include <tvm/s_tir/data_layout.h>
#include <tvm/tirx/analysis.h>
#include <tvm/tirx/expr.h>
#include <tvm/tirx/stmt_functor.h>
#include <tvm/tirx/var.h>

#include <algorithm>
#include <cctype>

namespace tvm {
namespace tirx {
using tirx::IterVar;
using tirx::IterVarNode;
using tirx::Var;

TVM_FFI_STATIC_INIT_BLOCK() {
  SLayoutNode::RegisterReflection();
  SBijectiveLayoutNode::RegisterReflection();
}

const SLayoutAxis SLayoutAxis::UPPER_CASE[] = {
    SLayoutAxis('A'), SLayoutAxis('B'), SLayoutAxis('C'), SLayoutAxis('D'), SLayoutAxis('E'),
    SLayoutAxis('F'), SLayoutAxis('G'), SLayoutAxis('H'), SLayoutAxis('I'), SLayoutAxis('J'),
    SLayoutAxis('K'), SLayoutAxis('L'), SLayoutAxis('M'), SLayoutAxis('N'), SLayoutAxis('O'),
    SLayoutAxis('P'), SLayoutAxis('Q'), SLayoutAxis('R'), SLayoutAxis('S'), SLayoutAxis('T'),
    SLayoutAxis('U'), SLayoutAxis('V'), SLayoutAxis('W'), SLayoutAxis('X'), SLayoutAxis('Y'),
    SLayoutAxis('Z')};

const SLayoutAxis SLayoutAxis::LOWER_CASE[] = {
    SLayoutAxis('a'), SLayoutAxis('b'), SLayoutAxis('c'), SLayoutAxis('d'), SLayoutAxis('e'),
    SLayoutAxis('f'), SLayoutAxis('g'), SLayoutAxis('h'), SLayoutAxis('i'), SLayoutAxis('j'),
    SLayoutAxis('k'), SLayoutAxis('l'), SLayoutAxis('m'), SLayoutAxis('n'), SLayoutAxis('o'),
    SLayoutAxis('p'), SLayoutAxis('q'), SLayoutAxis('r'), SLayoutAxis('s'), SLayoutAxis('t'),
    SLayoutAxis('u'), SLayoutAxis('v'), SLayoutAxis('w'), SLayoutAxis('x'), SLayoutAxis('y'),
    SLayoutAxis('z')};

const SLayoutAxis& SLayoutAxis::Get(const char name) {
  TVM_FFI_ICHECK((name >= 'A' && name <= 'Z') || (name >= 'a' && name <= 'z'))
      << "Invalid layout axis name: " << name << ". Has to be A-Z or a-z.";
  return (name >= 'A' && name <= 'Z') ? SLayoutAxis::UPPER_CASE[name - 'A']
                                      : SLayoutAxis::LOWER_CASE[name - 'a'];
}

const SLayoutAxis& SLayoutAxis::Get(const IterVar& itvar) {
  const std::string axis = itvar->var.get()->name;
  TVM_FFI_ICHECK_EQ(axis.size(), 1) << "Invalid layout axis " << axis;
  return SLayoutAxis::Get(axis[0]);
}

const SLayoutAxis& SLayoutAxis::Get(const std::string& name) {
  TVM_FFI_ICHECK_EQ(name.length(), 1) << "Invalid axis " << name;
  return SLayoutAxis::Get(name[0]);
}

SLayout::SLayout(const ffi::Array<IterVar>& axes) {
  auto node = ffi::make_object<SLayoutNode>();
  node->axes = axes;
  std::ostringstream repr;

  for (const IterVar& packed_axis : axes) {
    auto unpacked_axes = UnpackIterVar(packed_axis);
    bool is_grouped = unpacked_axes.size() > 1;

    if (is_grouped) repr << "[";
    for (const IterVar& axis : unpacked_axes) {
      if (const auto* factor = axis->dom->extent.as<IntImmNode>()) {
        TVM_FFI_ICHECK_GT(factor->value, 0);
        repr << factor->value;
      } else {
        TVM_FFI_ICHECK(!is_grouped)
            << "Only Subordinate Axes with extent is allowed within a packed dim";
      }
      TVM_FFI_ICHECK_EQ(axis->var.get()->name.size(), 1)
          << "Invalid layout axis " << axis->var.get()->name;
      char c = axis->var.get()->name.operator std::string()[0];
      TVM_FFI_ICHECK((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
          << "Invalid layout axis " << c;
      repr << axis->var.get()->name;
    }
    if (is_grouped) repr << "]";
  }

  node->name = repr.str();
  data_ = std::move(node);
}

SLayout::SLayout(const std::string& name, PrimType index_ty) {  // NOLINT(*)
  TVM_FFI_CHECK(index_ty.code() == DLDataTypeCode::kDLInt, TypeError)
      << "The input dtype should be integer type";
  if (name == "__undef__") return;

  auto node = ffi::make_object<SLayoutNode>();
  node->name = name;

  if (name.empty()) return;  // scalar

  // parse layout string
  int32_t factor = 0;
  bool in_packing = false;
  std::vector<IterVar> unpacked_axes;

  for (char c : name) {
    if (c >= 'A' && c <= 'Z') {
      TVM_FFI_ICHECK_EQ(factor, 0) << "Invalid layout " << name << ": invalid factor size "
                                   << factor << " before dimension " << c;
      PrimVar axis_var(std::string(1, c), index_ty);
      IterVar axis(Range(IntImm(index_ty, 0), axis_var), axis_var, tirx::kDataPar);
      if (!in_packing) {
        node->axes.push_back(axis);
      } else {
        unpacked_axes.push_back(axis);
      }
    } else if (c >= 'a' && c <= 'z') {
      TVM_FFI_ICHECK_GT(factor, 0) << "Invalid layout " << name << ": invalid factor size "
                                   << factor << " for dimension " << c;
      std::stringstream name;
      name << factor << c;
      IterVar axis(Range(IntImm(index_ty, 0), IntImm(index_ty, factor)),
                   PrimVar(name.str(), index_ty), tirx::kDataPar);
      if (!in_packing) {
        node->axes.push_back(axis);
      } else {
        unpacked_axes.push_back(axis);
      }
      factor = 0;
    } else if (c >= '0' && c <= '9') {
      TVM_FFI_ICHECK(factor >= 0) << "Invalid layout " << name << ": _ is adjacent to a number.";
      factor = factor * 10 + c - '0';
    } else if (c == '[') {
      TVM_FFI_ICHECK(!in_packing) << "Invalid layout " << name << ": can't do nested packing";
      in_packing = true;
    } else if (c == ']') {
      TVM_FFI_ICHECK(in_packing) << "Invalid layout " << name
                                 << ": encountered ] without matching bracket";
      TVM_FFI_ICHECK(unpacked_axes.size() > 1)
          << "Invalid layout " << name << ": found empty/single packed axis";
      std::stringstream ss;
      int64_t extent = 1;
      for (auto& axis : unpacked_axes) {
        TVM_FFI_ICHECK(axis->dom->extent.as<IntImmNode>())
            << "Invalid SLayout " << name << ": can't have variable sized node(" << axis->var->name
            << ") within a packed axis";
        auto axis_name = axis->var->name.operator std::string();
        auto factor = axis->dom->extent.as<IntImm>().value();
        ss << axis_name;
        extent = extent * factor->value;
      }
      std::string grouped_name = ss.str();
      IterVar grouped_axis(Range(IntImm(index_ty, 0), IntImm(index_ty, extent)),
                           PrimVar(grouped_name, index_ty), tirx::kDataPar);
      node->axes.push_back(grouped_axis);

      in_packing = false;
      unpacked_axes.clear();
    } else {
      TVM_FFI_THROW(InternalError) << "Invalid layout " << name;
    }
  }
  TVM_FFI_ICHECK(in_packing == false)
      << "Invalid SLayout " << name << ": haven't terminated the packing sequence";

  // validate layout
  std::vector<int> axis_cnt(256, 0);
  for (const IterVar& pv : node->axes) {
    for (const IterVar& v : UnpackIterVar(pv)) {
      auto axis_str = v->var.get()->name.operator std::string();
      TVM_FFI_ICHECK_EQ(axis_str.size(), 1);
      char axis = axis_str[0];
      TVM_FFI_ICHECK((axis >= 'a' && axis <= 'z') || (axis >= 'A' && axis <= 'Z'));
      axis_cnt[axis] += 1;
    }
  }
  for (const IterVar& pv : node->axes) {
    for (const IterVar& v : UnpackIterVar(pv)) {
      char axis = v->var.get()->name.operator std::string()[0];
      if (axis >= 'a' && axis <= 'z') {
        TVM_FFI_ICHECK(axis_cnt[axis - 'a' + 'A'])
            << "Invalid layout " << name << ": missing axis " << std::toupper(axis);
        TVM_FFI_ICHECK(axis_cnt[axis] == 1)
            << "Invalid layout " << name << ": found more than one subordinate "
            << std::toupper(axis);
      }
    }
  }

  data_ = std::move(node);
}

SLayout SLayout::SubLayout(size_t pos, size_t len) const {
  if (!defined() || pos > ndim()) return SLayout::Undef();
  if (len == 0) return SLayout(ffi::Array<IterVar>());
  if (pos + len > ndim()) len = ndim() - pos;
  ffi::Array<IterVar> new_layout;
  const auto axes = operator->()->axes;
  for (size_t i = pos; i < pos + len; ++i) {
    new_layout.push_back(axes[i]);
  }
  return SLayout(new_layout);
}

ffi::Array<IterVar> SLayout::UnpackIterVar(IterVar packed_iter) {
  ffi::Array<IterVar> result;
  int64_t factor = 0, final_factor = 1;

  std::string name(packed_iter->var->name.c_str());
  PrimType index_ty = packed_iter->var.ty();

  for (auto ch : name) {
    if (ch >= '0' && ch <= '9') {
      factor = factor * 10 + (ch - '0');
    } else if (ch >= 'a' && ch <= 'z') {
      TVM_FFI_ICHECK(factor != 0) << "Invalid Factor Size";
      result.push_back(IterVar(Range(IntImm(index_ty, 0), IntImm(index_ty, factor)),
                               PrimVar(std::string(1, ch), index_ty), tirx::kDataPar));
      final_factor *= factor;
      factor = 0;
    } else if (ch >= 'A' && ch <= 'Z') {
      TVM_FFI_ICHECK(factor == 0) << "Can't have non-zero factors for primal axis";
      PrimVar axis_var(std::string(1, ch), index_ty);
      result.push_back(IterVar(Range(IntImm(index_ty, 0), axis_var), axis_var, tirx::kDataPar));
    }
  }

  return result;
}

IterVar SLayout::PackIterVar(ffi::Array<IterVar> iter_vars) {
  std::stringstream name;
  size_t extent = 1;

  PrimType index_ty = iter_vars[0]->dom->extent.as<PrimExpr>().value().ty();
  for (auto itvar : iter_vars) {
    TVM_FFI_ICHECK(itvar->dom->extent.as<IntImm>())
        << "Packed Axis can contain only Subordinate Axes";
    name << itvar->dom->extent.as<IntImm>().value() << itvar->var->name;
    extent = extent * itvar->dom->extent.as<IntImm>().value()->value;
  }

  return IterVar(Range(IntImm(index_ty, 0), IntImm(index_ty, extent)),
                 PrimVar(name.str(), index_ty), tirx::kDataPar);
}

int32_t SLayout::FactorOf(const SLayoutAxis& axis) const {
  if (!defined()) return -1;
  const SLayoutAxis& sub = axis.ToSubordinate();

  int32_t factor = 1;
  bool has_sub = false;
  for (const IterVar& packed_itvar : operator->()->axes) {
    for (auto itvar : UnpackIterVar(packed_itvar)) {
      if (sub == SLayoutAxis::Get(itvar)) {
        has_sub = true;
        int32_t val = itvar->dom->extent.as<IntImmNode>()->value;
        factor *= val;
      }
    }
  }
  factor = has_sub ? factor : -1;

  return factor;
}

TVM_FFI_STATIC_INIT_BLOCK() {
  namespace refl = tvm::ffi::reflection;
  refl::TypeAttrDef<SLayoutNode>().def(refl::type_attr::kRepr,
                                       [](SLayout l, ffi::Function) -> ffi::String {
                                         return "SLayout(" + std::string(l->name) + ")";
                                       });
}

inline bool GetStoreRule(ffi::Array<PrimExpr>* index_rule, ffi::Array<PrimExpr>* shape_rule,
                         const SLayout& src_layout, const SLayout& dst_layout) {
  if (!src_layout.defined() || src_layout.name().empty()) {
    LOG(WARNING) << "src layout '" << src_layout.name() << "' is invalid.";
    return false;
  }
  if (!dst_layout.defined() || dst_layout.name().empty()) {
    LOG(WARNING) << "dst layout '" << dst_layout.name() << "' is invalid.";
    return false;
  }

  std::vector<bool> exists(128, false);
  PrimExpr norm_indexes[128];
  for (auto& it : norm_indexes) it = PrimExpr(0);

  for (size_t i = 0; i < src_layout.ndim(); i++) {
    auto factor = src_layout.PackedAxisAt(i)->dom->extent;
    auto src_unpacked_axes = SLayout::UnpackIterVar(src_layout.PackedAxisAt(i));

    if (src_unpacked_axes.size() == 1 && SLayoutAxis::Get(src_unpacked_axes[0]).IsPrimal()) {
      const auto& prim_axis = SLayoutAxis::Get(src_unpacked_axes[0]);
      int64_t offset = src_layout.FactorOf(prim_axis);
      if (offset == -1)
        norm_indexes[prim_axis.name()[0] - 'A'] =
            norm_indexes[prim_axis.name()[0] - 'A'] + src_layout.PackedAxisAt(i);
      else
        norm_indexes[prim_axis.name()[0] - 'A'] =
            norm_indexes[prim_axis.name()[0] - 'A'] +
            src_layout.PackedAxisAt(i) * src_layout.FactorOf(prim_axis);
      exists[prim_axis.name()[0]] = true;
    } else {
      int64_t value = 1;
      std::vector<int> index_divs(src_unpacked_axes.size());
      for (size_t j = 0; j < src_unpacked_axes.size(); j++) {
        index_divs[j] = value;
        const auto* extent = src_unpacked_axes[j]->dom->extent.as<IntImmNode>();
        TVM_FFI_ICHECK(extent) << "Expected Integer Extents for Offset Calculation";
        index_divs.push_back(value);
        value = value * extent->value;
      }
      std::reverse(index_divs.begin(), index_divs.end());

      for (size_t j = 0; j < src_unpacked_axes.size(); j++) {
        const int extent = src_unpacked_axes[j]->dom->extent.as<IntImmNode>()->value;
        const SLayoutAxis& store_axis_impl = SLayoutAxis::Get(src_unpacked_axes[j]);
        const SLayoutAxis& sub_axis = store_axis_impl.ToSubordinate(); /* Not Needed */
        const SLayoutAxis& prim_axis = store_axis_impl.ToPrimal();

        PrimExpr factor_ij = indexdiv(src_layout.PackedAxisAt(i), index_divs[j]);
        if (j != 0) factor_ij = indexmod(factor_ij, extent);

        for (size_t k = i; k < src_layout.ndim(); k++) {
          size_t l = 0;
          if (k == i) l = j + 1;

          auto inter_unpacked_axes = SLayout::UnpackIterVar(src_layout.PackedAxisAt(k));
          for (; l < inter_unpacked_axes.size(); l++) {
            const SLayoutAxis& axis = SLayoutAxis::Get(inter_unpacked_axes[l]);
            if (axis == sub_axis) {
              IntImm sub_extent = inter_unpacked_axes[l]->dom->extent.as_or_throw<IntImm>();
              factor_ij = factor_ij * IntImm(sub_extent.ty(), sub_extent->value);
            }
          }
        }

        norm_indexes[prim_axis.name()[0] - 'A'] =
            norm_indexes[prim_axis.name()[0] - 'A'] + factor_ij;
      }
    }
  }

  arith::Analyzer ana;

  for (size_t i = 0; i < dst_layout.ndim(); i++) {
    const auto dst_unpacked_axes = SLayout::UnpackIterVar(dst_layout.PackedAxisAt(i));

    if (dst_unpacked_axes.size() == 1 && SLayoutAxis::Get(dst_unpacked_axes[0]).IsPrimal()) {
      const auto& prim_axis = SLayoutAxis::Get(dst_unpacked_axes[0]);
      if (!exists[prim_axis.name()[0]]) return false;
      int64_t offset = dst_layout.FactorOf(prim_axis);
      if (offset != -1) {
        index_rule->push_back(
            indexdiv(norm_indexes[prim_axis.name()[0] - 'A'], dst_layout.FactorOf(prim_axis)));
        shape_rule->push_back(
            indexdiv(norm_indexes[prim_axis.name()[0] - 'A'] + (dst_layout.FactorOf(prim_axis) - 1),
                     dst_layout.FactorOf(prim_axis)));
      } else {
        index_rule->push_back(norm_indexes[prim_axis.name()[0] - 'A']);
        shape_rule->push_back(norm_indexes[prim_axis.name()[0] - 'A']);
      }
    } else {
      PrimExpr factor(0);
      for (size_t j = 0; j < dst_unpacked_axes.size(); j++) {
        const auto& prim_axis = SLayoutAxis::Get(dst_unpacked_axes[j]).ToPrimal();
        const auto& sub_axis = SLayoutAxis::Get(dst_unpacked_axes[j]).ToSubordinate();
        const auto* extent = dst_unpacked_axes[j]->dom->extent.as<IntImmNode>();
        TVM_FFI_ICHECK(extent) << "Expected extent to be IntImmNode";

        size_t divfactor = 1;
        for (size_t k = i; k < dst_layout.ndim(); k++) {
          size_t l = 0;
          if (k == i) l = j + 1;

          const auto inter_unpacked_axes = SLayout::UnpackIterVar(dst_layout.PackedAxisAt(k));
          for (; l < inter_unpacked_axes.size(); l++) {
            const auto& axis = SLayoutAxis::Get(inter_unpacked_axes[l]);
            if (sub_axis == axis) {
              const auto* sub_extent = inter_unpacked_axes[l]->dom->extent.as<IntImmNode>();
              TVM_FFI_ICHECK(sub_extent) << "Expected Integer Extents for Offset Calculation";
              divfactor = divfactor * sub_extent->value;
            }
          }
        }

        factor = factor + indexmod(indexdiv(norm_indexes[prim_axis.name()[0] - 'A'], divfactor),
                                   extent->value);
        for (size_t k = j + 1; k < dst_unpacked_axes.size(); k++) {
          factor = factor * dst_unpacked_axes[k]->dom->extent.as<IntImm>().value();
        }
      }
      ana->Simplify(factor);
      index_rule->push_back(factor);
      shape_rule->push_back(factor);
    }
  }

  std::stringstream ss;
  ss << "index rule for " << src_layout.name() << "-->" << dst_layout.name() << ": [ ";
  for (const auto& r : *index_rule) {
    ss << r << ", ";
  }
  ss << "]" << std::endl;

  ss << "shape rule for " << src_layout.name() << "-->" << dst_layout.name() << ": [ ";
  for (const auto& r : *shape_rule) {
    ss << r << ", ";
  }
  ss << "]" << std::endl;
  VLOG(1) << ss.str() << std::endl;

  return true;
}

inline ffi::Array<PrimExpr> TransformIndex(const ffi::Array<PrimExpr>& src_index,
                                           const ffi::Array<IterVar>& src_axis,
                                           const ffi::Array<PrimExpr>& transform_rule) {
  arith::Analyzer ana;
  ffi::Array<PrimExpr> result;
  std::unordered_map<const tirx::VarNode*, PrimExpr> bind_map;
  for (size_t i = 0; i < src_index.size(); ++i) {
    bind_map[src_axis[i]->var.get()] = src_index[i];
  }
  for (PrimExpr rule : transform_rule) {
    result.push_back(ana->Simplify(tirx::Substitute(rule, bind_map)));
  }
  return result;
}

ffi::Array<PrimExpr> SBijectiveLayout::ForwardIndex(const ffi::Array<PrimExpr>& src_index) const {
  TVM_FFI_ICHECK(defined()) << "Cannot operate on an undefined bijective layout.";
  const SBijectiveLayoutNode* self = operator->();
  TVM_FFI_ICHECK_EQ(src_index.size(), self->src_layout->axes.size())
      << "Input mismatch with layout " << self->src_layout;
  return TransformIndex(src_index, self->src_layout->axes, self->index_forward_rule);
}

ffi::Array<PrimExpr> SBijectiveLayout::BackwardIndex(const ffi::Array<PrimExpr>& dst_index) const {
  TVM_FFI_ICHECK(defined()) << "Cannot operate on an undefined bijective layout.";
  const SBijectiveLayoutNode* self = operator->();
  TVM_FFI_ICHECK_EQ(dst_index.size(), self->dst_layout->axes.size())
      << "Output mismatch with layout " << self->dst_layout;
  return TransformIndex(dst_index, self->dst_layout->axes, self->index_backward_rule);
}

inline ffi::Array<PrimExpr> TransformShape(const ffi::Array<PrimExpr>& src_shape,
                                           const ffi::Array<IterVar>& src_axis,
                                           const ffi::Array<IterVar>& target_axis,
                                           const ffi::Array<PrimExpr>& transform_rule) {
  arith::Analyzer ana;
  TVM_FFI_ICHECK_EQ(src_shape.size(), src_axis.size())
      << "Input shape size " << src_shape.size() << " mismatch with the expected shape size "
      << src_axis.size();
  // bind variables for original axes
  // for major-axis, bind the corresponding size
  // for minor-axis, simply bind it as 0, so that we can reuse forward/backward_rule,
  // e.g., (C * 16 + c) / 32
  std::unordered_map<const tirx::VarNode*, PrimExpr> bind_map;
  for (size_t i = 0; i < src_shape.size(); ++i) {
    PrimExpr orig_shape = src_shape[i];
    IterVar orig_axis = src_axis[i];
    auto layout = SLayout::UnpackIterVar(orig_axis);
    if (layout.size() != 1 || !SLayoutAxis::Get(layout[0]).IsPrimal()) {
      if (orig_shape.defined()) {
        const auto* orig_shape_const = orig_shape.as<IntImmNode>();
        const auto* orig_axis_extent = orig_axis->dom->extent.as<IntImmNode>();
        if (orig_shape_const) {
          TVM_FFI_ICHECK_EQ(orig_shape_const->value, orig_axis_extent->value)
              << "Input shape mismatch at index " << i << ". Expected " << orig_axis->dom->extent
              << ", get " << orig_shape;
        }
      }
      bind_map[orig_axis->var.get()] = IntImm(orig_axis->var.ty(), 0);
    } else {
      bind_map[orig_axis->var.get()] = orig_axis->var.ty() == orig_shape.ty()
                                           ? orig_shape
                                           : cast(orig_axis->var.ty(), orig_shape);
    }
  }
  // infer the target shape,
  // for major-axis, use the forward/backward_rule directly,
  // for minor-axis, simply use the extent.
  ffi::Array<PrimExpr> result;
  TVM_FFI_ICHECK_EQ(transform_rule.size(), target_axis.size());
  for (size_t i = 0; i < transform_rule.size(); ++i) {
    PrimExpr rule = transform_rule[i];
    IterVar axis = target_axis[i];
    auto layout = SLayout::UnpackIterVar(axis);
    if (layout.size() != 1 || !SLayoutAxis::Get(layout[0]).IsPrimal()) {
      result.push_back(axis->dom->extent);
    } else {
      result.push_back(ana->Simplify(tirx::Substitute(rule, bind_map)));
    }
  }

  std::stringstream ss;
  ss << "shape rule for " << SLayout(src_axis).name() << "-->" << SLayout(target_axis).name()
     << ": [ ";
  for (const auto& r : transform_rule) {
    ss << r << ", ";
  }
  ss << "]" << std::endl;

  ss << "shape transform: [ ";
  for (const auto& s : src_shape) {
    ss << s << ", ";
  }
  ss << "] --> [ ";
  for (const auto& r : result) {
    ss << r << ", ";
  }
  ss << "]" << std::endl;
  VLOG(1) << std::endl << ss.str();

  return result;
}

ffi::Array<PrimExpr> SBijectiveLayout::ForwardShape(const ffi::Array<PrimExpr>& shape) const {
  TVM_FFI_ICHECK(defined()) << "Cannot operate on an undefined bijective layout.";
  const SBijectiveLayoutNode* self = operator->();
  return TransformShape(shape, self->src_layout->axes, self->dst_layout->axes,
                        self->shape_forward_rule);
}

ffi::Array<PrimExpr> SBijectiveLayout::BackwardShape(const ffi::Array<PrimExpr>& shape) const {
  TVM_FFI_ICHECK(defined()) << "Cannot operate on an undefined bijective layout.";
  const SBijectiveLayoutNode* self = operator->();
  return TransformShape(shape, self->dst_layout->axes, self->src_layout->axes,
                        self->shape_backward_rule);
}

SBijectiveLayout::SBijectiveLayout(SLayout src_layout, SLayout dst_layout) {
  auto n = ffi::make_object<SBijectiveLayoutNode>();

  n->src_layout = std::move(src_layout);
  n->dst_layout = std::move(dst_layout);
  // To be consistent with previous behavior, a nullptr layout is created
  // when argument is invalid.
  if (GetStoreRule(&n->index_forward_rule, &n->shape_forward_rule, n->src_layout, n->dst_layout)) {
    TVM_FFI_ICHECK(GetStoreRule(&n->index_backward_rule, &n->shape_backward_rule, n->dst_layout,
                                n->src_layout));
    data_ = std::move(n);
  }
}

TVM_FFI_STATIC_INIT_BLOCK() {
  namespace refl = tvm::ffi::reflection;
  refl::TypeAttrDef<SBijectiveLayoutNode>().def(
      refl::type_attr::kRepr, [](SBijectiveLayout bl, ffi::Function) -> ffi::String {
        return "SBijectiveLayout(" + std::string(bl->src_layout.name()) + "->" +
               std::string(bl->dst_layout.name()) + ")";
      });
}

TVM_FFI_STATIC_INIT_BLOCK() {
  namespace refl = tvm::ffi::reflection;
  refl::GlobalDef()
      .def("s_tir.SLayout", [](std::string name, PrimType dtype) { return SLayout(name, dtype); })
      .def("s_tir.SLayoutIndexOf",
           [](SLayout layout, std::string axis) -> int { return layout.IndexOf(axis); })
      .def("s_tir.SLayoutFactorOf",
           [](SLayout layout, std::string axis) -> int {
             return layout.FactorOf(SLayoutAxis::Get(axis));
           })
      .def("s_tir.SLayoutNdim", [](SLayout layout) -> int { return layout.ndim(); })
      .def("s_tir.SLayoutGetItem",
           [](SLayout layout, int idx) -> std::string {
             const auto& axis = layout.PackedAxisAt(idx);
             return axis->var->name;
           })
      .def("s_tir.SBijectiveLayout",
           [](SLayout src_layout, SLayout dst_layout) -> SBijectiveLayout {
             return SBijectiveLayout(src_layout, dst_layout);
           })
      .def_method("s_tir.SBijectiveLayoutForwardIndex", &SBijectiveLayout::ForwardIndex)
      .def_method("s_tir.SBijectiveLayoutBackwardIndex", &SBijectiveLayout::BackwardIndex)
      .def_method("s_tir.SBijectiveLayoutForwardShape", &SBijectiveLayout::ForwardShape)
      .def_method("s_tir.SBijectiveLayoutBackwardShape", &SBijectiveLayout::BackwardShape);
}
}  // namespace tirx
}  // namespace tvm
