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

#include <tvm/arith/iter_affine_map.h>
#include <tvm/ffi/cast.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/ir/type.h>
#include <tvm/tirx/analysis.h>
#include <tvm/tirx/layout.h>
#include <tvm/tirx/stmt_functor.h>
#include <tvm/tirx/transform.h>

#include <unordered_set>

#include "../../arith/ir_mutator_with_analyzer.h"
#include "ir_utils.h"

namespace tvm {
namespace tirx {

/*!
 * \brief Flatten each n-d buffer ``buf`` into a 1-d storage view ``buf'``,
 *        rewriting every access ``buf[x]`` into ``buf'[f(x)]``.
 *
 *  The invariant: ``f(x) = layout.apply(x, shape) + elem_offset`` is fully
 *  determined by ``buf``'s geometry, and ``buf'`` is only a storage husk —
 *  same data origin, dtype, alignment and scope; no layout, no elem_offset.
 *
 *  The pass walks the AST top-down. At each buffer definition point
 *  (AllocBuffer/DeclBuffer; PrimFunc params are seeded up front) it derives,
 *  exactly once:
 *    - the fold view: the original geometry with its expression fields
 *      (runtime elem_offset, symbolic shapes/strides, layout iters) rewritten
 *      by the pass — the folded indices live in the rewritten program, so
 *      ``f``'s coefficients must reference rebuilt buffers; and
 *    - ``buf'``, the flattened storage husk.
 *  Every use site then only looks the pair up; a use before its definition is
 *  a hard error instead of a silently stale reference.
 */
class BufferFlattener : public arith::IRMutatorWithAnalyzer {
 public:
  static PrimFunc Flatten(PrimFunc func) {
    arith::Analyzer ana;
    auto pass = BufferFlattener(ana);
    pass.MarkBufferParamShapes(func);
    for (const Var& param : func->params) {
      if (auto buffer = param.as<BufferVar>()) {
        pass.extern_buffers_.insert(buffer.value());
        pass.Define(buffer.value());
      }
    }
    auto body = pass.VisitStmt(func->body);

    // Buffer parameters are deliberately left unflattened, as they are used
    // for validation of user-provided arguments.  The flattened buffers used
    // in the updated function body alias the argument buffers.
    for (size_t i = func->params.size(); i > 0; i--) {
      if (auto old_buf = func->params[i - 1].as<BufferVar>()) {
        if (pass.buffers_used_.count(old_buf.value())) {
          auto new_buf = pass.Lookup(old_buf.value()).flattened;
          if (!old_buf.value().same_as(new_buf)) {
            body = SeqStmt::Flatten(DeclBuffer(new_buf, old_buf.value().data()), std::move(body));
          }
        }
      }
    }

    if (!body.same_as(func->body)) {
      func.CopyOnWrite()->body = std::move(body);
    }
    return func;
  }

 private:
  using IRMutatorWithAnalyzer::VisitExpr;
  using IRMutatorWithAnalyzer::VisitExpr_;
  using IRMutatorWithAnalyzer::VisitStmt;
  using IRMutatorWithAnalyzer::VisitStmt_;

  explicit BufferFlattener(const arith::Analyzer& ana) : IRMutatorWithAnalyzer(ana) {}

  struct FlatInfo {
    /*! \brief Original geometry with rewritten expression fields; the source
     *   of ``f``. Only used to fold indices, never emitted into the IR. */
    BufferVar fold_view;
    /*! \brief The 1-d storage husk ``buf'``. */
    BufferVar flattened;
  };

  /*! \brief Derive {fold view, flattened husk} for ``buf`` at its definition
   *   point. Idempotent so params can be seeded up front. */
  const FlatInfo& Define(const BufferVar& buf) {
    if (auto it = flat_map_.find(buf.var()); it != flat_map_.end()) {
      return it->second;
    }

    // Fold view: rewrite the geometry's expression leaves.
    auto view_type = CopyBufferType(buf);
    for (size_t i = 0; i < view_type->shape.size(); ++i) {
      view_type->shape.Set(i, this->VisitPrimExpr(view_type->shape[i]));
    }
    for (size_t i = 0; i < view_type->strides.size(); ++i) {
      view_type->strides.Set(i, this->VisitPrimExpr(view_type->strides[i]));
    }
    if (view_type->elem_offset.defined()) {
      view_type->elem_offset = this->VisitPrimExpr(view_type->elem_offset);
    }
    if (auto tile = view_type->layout.as<TileLayoutNode>()) {
      auto remap_iter = [this](const Iter& iter) {
        PrimExpr extent = this->VisitPrimExpr(iter->extent);
        PrimExpr stride = this->VisitPrimExpr(iter->stride);
        if (extent.same_as(iter->extent) && stride.same_as(iter->stride)) {
          return iter;
        }
        return Iter(extent, stride, iter->axis);
      };
      auto shard = tile->shard.Map(remap_iter);
      auto replica = tile->replica.Map(remap_iter);
      if (!shard.same_as(tile->shard) || !replica.same_as(tile->replica)) {
        view_type->layout = TileLayout(shard, replica, tile->offset);
      }
    }
    BufferVar fold_view = RebuildBufferVar(buf, std::move(view_type));

    // buf': the storage husk. The linearized indices carry layout and
    // elem_offset, so the husk keeps neither.
    auto flat = fold_view.GetFlattenedBuffer();
    auto type = CopyBufferType(flat);
    for (size_t i = 0; i < type->shape.size(); ++i) {
      type->shape.Set(i, analyzer_->canonical_simplify(type->shape[i]));
    }
    type->layout = std::nullopt;
    if (type->elem_offset.defined() && !is_zero(type->elem_offset)) {
      type->elem_offset = IntImm(type->elem_offset.ty().as_or_throw<PrimType>(), 0);
    }
    // Body-local buffers keep their identity when flattening changes nothing.
    // PrimFunc-parameter buffers always rebuild: the epilogue aliases the
    // rebuilt view onto the argument buffer with an explicit DeclBuffer, and
    // downstream s_tir passes pin that shape.
    BufferVar flattened =
        (!extern_buffers_.count(buf) && ffi::StructuralEqual()(BufferType(type), buf.type()))
            ? buf
            : RebuildBufferVar(buf, std::move(type));

    // Feed the base mutator's remap so stray buffer-var expressions follow.
    buffer_remap_.Set(buf, flattened);
    auto [it, inserted] = flat_map_.emplace(buf.var(), FlatInfo{fold_view, flattened});
    return it->second;
  }

  const FlatInfo& Lookup(const BufferVar& buf) {
    auto it = flat_map_.find(buf.var());
    TVM_FFI_ICHECK(it != flat_map_.end())
        << "Buffer " << buf.name()
        << " is used before its definition (AllocBuffer/DeclBuffer/PrimFunc param)";
    return it->second;
  }

  Stmt VisitStmt_(const SBlockNode* op) final {
    TVM_FFI_ICHECK_EQ(op->match_buffers.size(), 0)
        << "Unexpected MatchBufferRegion found during tirx.transform.FlattenBuffer.  "
        << "All MatchBufferRegion should be removed in tirx.transform.LowerMatchBuffer.";

    SBlock block = ffi::GetRef<SBlock>(op);

    ffi::Array<BufferVar> alloc_buffers = op->alloc_buffers;
    alloc_buffers.MutateByApply([this](BufferVar buf) { return Define(buf).flattened; });
    if (!alloc_buffers.same_as(op->alloc_buffers)) {
      block.CopyOnWrite()->alloc_buffers = alloc_buffers;
    }

    ffi::Array<BufferRegion> reads = op->reads;
    reads.MutateByApply([this](BufferRegion region) { return MutateBufferRegion(region); });
    if (!reads.same_as(op->reads)) {
      block.CopyOnWrite()->reads = reads;
    }

    ffi::Array<BufferRegion> writes = op->writes;
    writes.MutateByApply([this](BufferRegion region) { return MutateBufferRegion(region); });
    if (!writes.same_as(op->writes)) {
      block.CopyOnWrite()->writes = writes;
    }

    return StmtExprMutator::VisitStmt_(block.get());
  }

  Stmt VisitStmt_(const AllocBufferNode* op) final {
    const FlatInfo& info = Define(op->buffer);
    if (info.flattened.same_as(op->buffer)) {
      return ffi::GetRef<Stmt>(op);
    }
    auto n = CopyOnWrite(op);
    n->buffer = info.flattened;
    return Stmt(n);
  }

  Stmt VisitStmt_(const DeclBufferNode* op) final {
    Expr data = op->data;
    bool is_extern_buffer_source = false;
    if (const auto* call = op->data.as<CallNode>();
        call && call->op.same_as(builtin::buffer_data()) && call->args.size() == 1) {
      if (const auto* var = call->args[0].as<VarNode>(); var && var->ty.as<BufferTypeNode>()) {
        is_extern_buffer_source = extern_buffers_.count(BufferVar(ffi::GetRef<Var>(var)));
      }
    }
    if (!is_extern_buffer_source) {
      data = VisitExpr(op->data);
    }
    const FlatInfo& info = Define(op->buffer);
    if (info.flattened.same_as(op->buffer) && data.same_as(op->data)) {
      return ffi::GetRef<Stmt>(op);
    }
    return DeclBuffer(info.flattened, std::move(data), op->span);
  }

  Stmt VisitStmt_(const BufferStoreNode* op) final {
    BufferVar original_buffer = op->buffer;
    BufferStore store = StmtExprMutator::VisitStmt_(op).as_or_throw<BufferStore>();
    store = VisitBufferAccess(store, original_buffer);
    return store;
  }

  Expr VisitExpr_(const BufferLoadNode* op) final {
    BufferVar original_buffer = op->buffer;
    BufferLoad load = StmtExprMutator::VisitExpr_(op).as_or_throw<BufferLoad>();
    load = VisitBufferAccess(load, original_buffer);
    return load;
  }

  Expr VisitExpr_(const CallNode* op) final {
    if (op->op.same_as(builtin::buffer_data()) && op->args.size() == 1) {
      if (auto var = op->args[0].as<Var>()) {
        if (var.value()->ty.as<BufferTypeNode>()) {
          BufferVar original(var.value());
          buffers_used_.insert(original);
          return Lookup(original).flattened.data();
        }
      }
    }
    return IRMutatorWithAnalyzer::VisitExpr_(op);
  }

  ffi::Array<PrimExpr> FoldIndices(const FlatInfo& info, const ffi::Array<PrimExpr>& indices) {
    auto flattened_indices = info.fold_view->ElemOffset(indices);
    return this->IterMapSimplifyWithContext(flattened_indices, false);
  }

  template <typename Node>
  Node VisitBufferAccess(Node node, const BufferVar& original_buffer) {
    TVM_FFI_ICHECK(node->buffer.defined());
    buffers_used_.insert(original_buffer);
    const FlatInfo& info = Lookup(original_buffer);
    auto flattened_indices = FoldIndices(info, node->indices);

    auto writer = node.CopyOnWrite();
    writer->buffer = info.flattened;
    writer->indices = flattened_indices;
    return node;
  }

  BufferRegion MutateBufferRegion(BufferRegion region) {
    const FlatInfo& info = Lookup(region->buffer);
    if (info.flattened.same_as(region->buffer)) {
      return region;
    }

    ffi::Array<PrimExpr> min_values;
    ffi::Array<PrimExpr> max_values;
    for (const auto& range : region->region) {
      min_values.push_back(range->min);
      max_values.push_back(range->min + range->extent - 1);
    }

    ffi::Array<PrimExpr> flattened_min = FoldIndices(info, min_values);
    ffi::Array<PrimExpr> flattened_max = FoldIndices(info, max_values);

    ffi::Array<Range> flattened_ranges;
    TVM_FFI_ICHECK_EQ(flattened_min.size(), flattened_max.size());
    for (size_t i = 0; i < flattened_min.size(); i++) {
      flattened_ranges.push_back(Range(flattened_min[i], flattened_max[i] + 1));
    }

    return BufferRegion(info.flattened, flattened_ranges);
  }

  /*! \brief Set of buffers accessed during visitation (used to emit DeclBuffer for param buffers).
   */
  std::unordered_set<BufferVar, ffi::ObjectPtrHash, ffi::ObjectPtrEqual> buffers_used_;

  /*! \brief Buffers whose storage is supplied by a PrimFunc parameter. */
  std::unordered_set<BufferVar, ffi::ObjectPtrHash, ffi::ObjectPtrEqual> extern_buffers_;

  /*! \brief Per-buffer {fold view, flattened husk}, derived at definition points. */
  std::unordered_map<Var, FlatInfo, ffi::ObjectPtrHash, ffi::ObjectPtrEqual> flat_map_;
};

PrimFunc FlattenBuffer(PrimFunc f) { return BufferFlattener::Flatten(f); }

namespace transform {

Pass FlattenBuffer() {
  auto pass_func = [=](PrimFunc f, IRModule m, PassContext ctx) {
    return FlattenBuffer(std::move(f));
  };
  return CreatePrimFuncPass(pass_func, 0, "tirx.FlattenBuffer", {});
}

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

}  // namespace tirx
}  // namespace tvm
