// Copyright (c) 2021 CINN Authors. All Rights Reserved.
//
// Licensed 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.

// Used in FactorizeReduction

#pragma once
#include "paddle/cinn/ir/ir.h"
#include "paddle/cinn/ir/ir_base.h"
#include "paddle/cinn/ir/ir_printer.h"
#include "paddle/cinn/ir/schedule/ir_schedule_util.h"
#include "paddle/cinn/ir/tensor.h"
#include "paddle/cinn/ir/utils/ir_copy.h"
#include "paddle/cinn/lang/compute.h"
#include "paddle/cinn/optim/replace_var_with_expr.h"
#include "paddle/cinn/utils/error.h"

namespace cinn {
namespace ir {

// Create the new Reduction-Factorized tensor,
// only used for FactorizeReduction schedule primitive.
Tensor CreateRFTensor(const Tensor& original_tensor,
                      const Expr& rf_loop,
                      int rf_axis) {
  std::string name = common::UniqName(original_tensor->name + "_rf");
  std::vector<Expr> new_shape = original_tensor->shape;
  new_shape.insert(new_shape.begin() + rf_axis, rf_loop.As<For>()->extent);
  Tensor rf_tensor = _Tensor_::Make(name,
                                    original_tensor->type(),
                                    new_shape,
                                    new_shape,
                                    original_tensor->operation,
                                    original_tensor->reduce_axis);
  rf_tensor->WithBuffer("global", name, original_tensor->type());
  return rf_tensor;
}

// Base class to create a new reduce block,
// only used for FactorizeReduction schedule primitive.
class ReduceBlockCreater {
 public:
  ReduceBlockCreater(const Expr& original_block,
                     const std::vector<Expr>& original_loops,
                     const Expr& rf_loop,
                     const Expr& original_update_stmt,
                     const ir::Tensor& rf_tensor,
                     bool is_rf_block)
      : original_block_(original_block),
        original_loops_(original_loops),
        rf_loop_(rf_loop),
        original_update_stmt_(original_update_stmt),
        rf_tensor_(rf_tensor),
        is_rf_block_(is_rf_block) {
    const ScheduleBlockRealize* block_real =
        original_block_.As<ir::ScheduleBlockRealize>();
    CHECK_NOTNULL(block_real);
    num_block_iters_ = block_real->iter_values.size();
  }

  void CreateBlock() {
    CreateRFIter();
    for (int i = 0; i < num_block_iters_; ++i) {
      CreateNormalIter(i);
    }
    CreateUpdateStmt();

    std::string new_update_block_name =
        original_block_.As<ir::ScheduleBlockRealize>()
            ->schedule_block.As<ir::ScheduleBlock>()
            ->name;
    if (is_rf_block_) {
      new_update_block_name = rf_tensor_->name;
    }
    std::string new_init_block_name =
        ir::GenReduceInitTensorNameOf(new_update_block_name);
    VLOG(5) << "new_init_block_name = " << new_init_block_name;

    const ir::Tensor& real_tensor =
        is_rf_block_
            ? rf_tensor_
            : original_update_stmt_.As<ir::Store>()->tensor.as_tensor_ref();
    Expr init_value = real_tensor->GetReduceInitVal();
    const std::vector<Expr>& domain = real_tensor->domain_without_reduce_axis();
    ir::Tensor init_tensor = lang::Compute(
        domain,
        [=](const std::vector<Expr>& axis) { return init_value; },
        new_init_block_name);
    init_tensor->Bind(real_tensor->buffer);
    Expr init_stmt = ir::Store::Make(
        init_tensor, init_value, new_update_stmt_.As<ir::Store>()->indices);
    new_init_sch_block_ = ScheduleBlock::Make(
        new_init_iter_vars_, {}, {}, new_init_block_name, init_stmt);
    new_init_block_realize_ =
        ScheduleBlockRealize::Make(new_init_iter_values_, new_init_sch_block_);

    new_update_sch_block_ = ScheduleBlock::Make(
        new_iter_vars_, {}, {}, new_update_block_name, new_update_stmt_);
    new_update_block_realize_ =
        ScheduleBlockRealize::Make(new_iter_values_, new_update_sch_block_);
    VLOG(4) << "new_update_block_realize:\n" << new_update_block_realize_;
  }

  Expr CreateLoops() {
    int num_loops = original_loops_.size();
    std::vector<Expr> new_loops(num_loops);
    Expr body = new_update_block_realize_;
    bool has_add_init_block = false;
    for (int i = num_loops - 1; i >= 0; --i) {
      bool is_spatial_loop =
          new_spatial_loop_var_names_.count(
              original_loops_[i].As<For>()->loop_var->name) > 0;
      bool is_rf_loop = rf_loop_.As<For>()->loop_var->name ==
                        original_loops_[i].As<For>()->loop_var->name;
      // Skip non rf reduction loops of write back block.
      if (!is_rf_block_ && !is_spatial_loop && !is_rf_loop) {
        continue;
      }
      // Add reduce init block.
      if (!has_add_init_block && is_spatial_loop) {
        body = Block::Make({new_init_block_realize_, body});
        has_add_init_block = true;
      }
      // Add loops
      Var loop_var = ir_utils::IRCopy(original_loops_[i].As<For>()->loop_var);
      Expr min = ir_utils::IRCopy(original_loops_[i].As<For>()->min);
      Expr extent = ir_utils::IRCopy(original_loops_[i].As<For>()->extent);
      body = For::Make(loop_var,
                       min,
                       extent,
                       original_loops_[i].As<For>()->for_type(),
                       original_loops_[i].As<For>()->device_api,
                       body,
                       original_loops_[i].As<For>()->vectorize_info(),
                       original_loops_[i].As<For>()->bind_info());
      VLOG(5) << "new body:\n" << body;
    }
    VLOG(4) << "new loop nest:\n" << body;
    return body;
  }

 private:
  virtual void CreateRFIter() = 0;
  virtual void CreateNormalIter(int idx) = 0;
  virtual void CreateUpdateStmt() = 0;

 public:
  Var rf_var_;
  std::vector<Expr> rf_tensor_access_indices_;

 protected:
  const Expr& original_block_;
  const std::vector<Expr>& original_loops_;
  const Expr& rf_loop_;
  const Expr& original_update_stmt_;
  const ir::Tensor& rf_tensor_;
  std::map<Var, Expr, CompVar> original_indice2new_expr_;
  int num_block_iters_;
  bool is_rf_block_;

  std::vector<Var> new_iter_vars_;
  std::vector<Expr> new_iter_values_;
  std::vector<Var> new_init_iter_vars_;
  std::vector<Expr> new_init_iter_values_;
  std::unordered_set<std::string> new_spatial_loop_var_names_;
  Expr new_update_stmt_;

  Expr new_update_sch_block_;
  Expr new_update_block_realize_;
  Expr new_init_sch_block_;
  Expr new_init_block_realize_;
};

// Implement class for building Reduction-Factorized block,
// only used for FactorizeReduction schedule primitive.
class RFBlockCreater : public ReduceBlockCreater {
 public:
  RFBlockCreater(const Expr& original_block,
                 const std::vector<Expr>& original_loops,
                 const Expr& rf_loop,
                 const Expr& original_update_stmt,
                 const ir::Tensor& rf_tensor,
                 const std::map<Var, Expr, CompVar>& var2loops,
                 int rf_axis)
      : ReduceBlockCreater(original_block,
                           original_loops,
                           rf_loop,
                           original_update_stmt,
                           rf_tensor,
                           true),
        var2loops_(var2loops),
        rf_axis_(rf_axis) {}

 private:
  void CreateRFIter() override {
    std::string loop_var_name = rf_loop_.As<ir::For>()->loop_var->name;
    std::string rf_var_name = "v" + loop_var_name;
    rf_var_ = Var(rf_loop_.As<ir::For>()->min,
                  rf_loop_.As<ir::For>()->extent,
                  rf_var_name,
                  /* is_reduce = */ false);
    loop_var2block_iters_[rf_loop_.As<ir::For>()->loop_var] = rf_var_;
    new_iter_vars_.push_back(rf_var_);
    new_iter_values_.push_back(rf_loop_.As<ir::For>()->loop_var);
    new_init_iter_vars_.push_back(rf_var_);
    new_init_iter_values_.push_back(rf_loop_.As<ir::For>()->loop_var);
    new_spatial_loop_var_names_.insert(rf_loop_.As<ir::For>()->loop_var->name);
    VLOG(4) << "create new_rf_var = " << rf_var_
            << ", with iter value = " << new_iter_values_.back();
  }

  void CreateNormalIter(int idx) override {
    Var original_iter_var = original_block_.As<ir::ScheduleBlockRealize>()
                                ->schedule_block.As<ir::ScheduleBlock>()
                                ->iter_vars[idx];
    Expr original_iter_value =
        original_block_.As<ir::ScheduleBlockRealize>()->iter_values[idx];
    // The original iter is either a spatial iter, or a reduction iter that
    // doesn't touch the rf loop. In this case reuse the old iter var and its
    // corresponding iter value.
    if (!original_iter_var->is_reduce_axis) {
      new_iter_vars_.push_back(original_iter_var);
      new_iter_values_.push_back(original_iter_value);
      new_init_iter_vars_.push_back(original_iter_var);
      new_init_iter_values_.push_back(original_iter_value);
      ir_utils::CollectIRNodesWithoutTensor(
          original_iter_value, [&](const Expr* x) {
            if (x->as_var()) {
              new_spatial_loop_var_names_.insert(x->as_var()->name);
            }
            return false;
          });
      return;
    } else if (!ContainVar({original_iter_value},
                           rf_loop_.As<ir::For>()->loop_var->name)) {
      new_iter_vars_.push_back(original_iter_var);
      new_iter_values_.push_back(original_iter_value);
      return;
    }
    CHECK(original_iter_var->is_reduce_axis);

    // This iter is a reduction iter and touches the rfactor loop. So we try to
    // create a new iter for each loop var that appear in the original iter
    // value.
    std::vector<Var> vars_in_original_iter_values;
    ir_utils::CollectIRNodesWithoutTensor(
        original_iter_value, [&](const Expr* x) {
          if (x->as_var()) {
            vars_in_original_iter_values.push_back(x->as_var_ref());
          }
          return false;
        });
    for (const Var& loop_var : vars_in_original_iter_values) {
      if (var2loops_.count(loop_var) == 0) {
        continue;
      }
      Expr loop = var2loops_.at(loop_var);
      if (loop_var2block_iters_.count(loop_var) == 0) {
        Var new_iter_var(loop.As<ir::For>()->min,
                         loop.As<ir::For>()->extent,
                         "v" + loop_var->name,
                         /* is_reduce = */ true);
        new_iter_vars_.push_back(new_iter_var);
        new_iter_values_.emplace_back(loop_var);
        loop_var2block_iters_[loop_var] = new_iter_var;
      }
    }
    // Substitute the original iter values with new iter vars,
    // and store the new iter values in original_indice2new_expr_,
    // it will be used in Load/Store indices.
    Expr new_iters = ir_utils::IRCopy(original_iter_value);
    ReplaceExpr(&new_iters, loop_var2block_iters_);
    original_indice2new_expr_[original_iter_var] = new_iters;
    VLOG(4) << "original_indice2new_expr_[" << original_iter_var
            << "] = " << new_iters;
  }

  void CreateUpdateStmt() override {
    rf_tensor_access_indices_ = original_update_stmt_.As<ir::Store>()->indices;
    rf_tensor_access_indices_.insert(
        rf_tensor_access_indices_.begin() + rf_axis_, rf_var_);
    Expr original_store_body = original_update_stmt_.As<ir::Store>()->value;
    Expr new_store_body = ir_utils::IRCopy(original_store_body);
#define REPLACE_RF_TENSOR(Op)                                    \
  if (new_store_body.As<Op>()) {                                 \
    auto* node = new_store_body.As<Op>();                        \
    CHECK(node);                                                 \
    auto& operand = node->a();                                   \
    operand = Load::Make(rf_tensor_, rf_tensor_access_indices_); \
  }

    REPLACE_RF_TENSOR(Add)
    REPLACE_RF_TENSOR(Mul)
    REPLACE_RF_TENSOR(Max)
    REPLACE_RF_TENSOR(Min)
    REPLACE_RF_TENSOR(And)
    REPLACE_RF_TENSOR(Or)
    REPLACE_RF_TENSOR(LT)
    REPLACE_RF_TENSOR(LE)
    REPLACE_RF_TENSOR(GT)
    REPLACE_RF_TENSOR(GE)
#undef REPLACE_RF_TENSOR

    new_update_stmt_ =
        ir::Store::Make(rf_tensor_, new_store_body, rf_tensor_access_indices_);
    ReplaceExpr(&new_update_stmt_, original_indice2new_expr_);
    VLOG(4) << "new_update_stmt of rf block: \n" << new_update_stmt_;
  }

 private:
  const std::map<Var, Expr, CompVar>& var2loops_;
  int rf_axis_;

  std::map<Var, Expr, CompVar> loop_var2block_iters_;
};

// Implement class for building Writing-Back block,
// only used for FactorizeReduction schedule primitive.
class RBBlockCreater : public ReduceBlockCreater {
 public:
  RBBlockCreater(const Expr& original_block,
                 const std::vector<Expr>& original_loops,
                 const Expr& rf_loop,
                 const Expr& original_update_stmt,
                 const ir::Tensor& rf_tensor,
                 const std::vector<Expr>& rf_tensor_access_indices,
                 const Var& rf_block_rf_iter_var)
      : ReduceBlockCreater(original_block,
                           original_loops,
                           rf_loop,
                           original_update_stmt,
                           rf_tensor,
                           false),
        rf_tensor_access_indices_(rf_tensor_access_indices),
        rf_block_rf_iter_var_(rf_block_rf_iter_var) {}

 private:
  void CreateRFIter() override {
    std::string loop_var_name = rf_loop_.As<ir::For>()->loop_var->name;
    std::string rf_var_name = "v" + loop_var_name;
    rf_var_ = Var(rf_loop_.As<ir::For>()->min,
                  rf_loop_.As<ir::For>()->extent,
                  rf_var_name,
                  /* is_reduce = */ true);
    new_iter_vars_.push_back(rf_var_);
    new_iter_values_.push_back(rf_loop_.As<ir::For>()->loop_var);
    original_indice2new_expr_[rf_block_rf_iter_var_] = Expr(rf_var_);
    VLOG(4) << "create new_rf_var = " << rf_var_
            << ", with iter value = " << new_iter_values_.back();
  }

  void CreateNormalIter(int idx) override {
    Var original_iter_var = original_block_.As<ir::ScheduleBlockRealize>()
                                ->schedule_block.As<ir::ScheduleBlock>()
                                ->iter_vars[idx];
    Expr original_iter_value =
        original_block_.As<ir::ScheduleBlockRealize>()->iter_values[idx];
    if (!original_iter_var->is_reduce_axis) {
      new_iter_vars_.push_back(original_iter_var);
      new_iter_values_.push_back(original_iter_value);
      new_init_iter_vars_.push_back(original_iter_var);
      new_init_iter_values_.push_back(original_iter_value);
      ir_utils::CollectIRNodesWithoutTensor(
          original_iter_value, [&](const Expr* x) {
            if (x->as_var()) {
              new_spatial_loop_var_names_.insert(x->as_var()->name);
            }
            return false;
          });
      // original_indice2new_expr_[original_iter_var] = new_iter_vars_.back();
      VLOG(4) << "create new iter var = " << new_iter_vars_.back()
              << ", with iter value = " << new_iter_values_.back();
    }
  }

  void CreateUpdateStmt() override {
    Expr original_store_body = original_update_stmt_.As<ir::Store>()->value;
    Expr new_store_body = ir_utils::IRCopy(original_store_body);
#define REPLACE_RF_TENSOR(Op)                                    \
  if (new_store_body.As<Op>()) {                                 \
    auto* node = new_store_body.As<Op>();                        \
    CHECK(node);                                                 \
    auto& operand = node->b();                                   \
    operand = Load::Make(rf_tensor_, rf_tensor_access_indices_); \
  }

    REPLACE_RF_TENSOR(Add)
    REPLACE_RF_TENSOR(Mul)
    REPLACE_RF_TENSOR(Max)
    REPLACE_RF_TENSOR(Min)
    REPLACE_RF_TENSOR(And)
    REPLACE_RF_TENSOR(Or)
    REPLACE_RF_TENSOR(LT)
    REPLACE_RF_TENSOR(LE)
    REPLACE_RF_TENSOR(GT)
    REPLACE_RF_TENSOR(GE)
#undef REPLACE_RF_TENSOR

    Expr original_store_tensor = original_update_stmt_.As<ir::Store>()->tensor;
    std::vector<Expr> original_store_indices =
        original_update_stmt_.As<ir::Store>()->indices;
    new_update_stmt_ = ir::Store::Make(
        original_store_tensor, new_store_body, original_store_indices);
    ReplaceExpr(&new_update_stmt_, original_indice2new_expr_);
    VLOG(4) << "new_update_stmt of write back block: \n" << new_update_stmt_;
  }

 private:
  const std::vector<Expr>& rf_tensor_access_indices_;
  const Var& rf_block_rf_iter_var_;
};

}  // namespace ir
}  // namespace cinn
