/*
 * 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 tvm/relax/transform/lambda_lift.cc
 * \brief Lift local functions into global functions.
 */

#include <tvm/ffi/cast.h>
#include <tvm/ffi/error.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/relax/analysis.h>
#include <tvm/relax/expr.h>
#include <tvm/relax/expr_functor.h>
#include <tvm/relax/transform.h>

#include <iostream>
#include <vector>

namespace tvm {
namespace relax {

namespace {

/* \brief Collect names of functions to be lifted out */
class LambdaNameCollector : ExprVisitor {
 public:
  static std::unordered_map<const FunctionNode*, ffi::String> Collect(const IRModule& mod) {
    LambdaNameCollector visitor;

    for (const auto& [gvar, base_func] : mod->functions) {
      visitor.previous_global_vars_.insert(gvar->name_hint);
    }

    for (const auto& [gvar, base_func] : mod->functions) {
      if (auto func = base_func.as<Function>()) {
        visitor.name_stack_.push_back(gvar->name_hint);
        visitor(func.value());
        visitor.name_stack_.pop_back();
      }
    }

    return visitor.Finalize();
  }

 private:
  void VisitBinding_(const VarBindingNode* binding, const FunctionNode* func) override {
    if (auto opt = func->GetAttr<ffi::String>(tvm::attr::kGlobalSymbol)) {
      ffi::String public_name = opt.value();

      // If a kGlobalSymbol exists, we must use the name exactly as it
      // appears, with no modifications.  Because these errors would
      // be raised from deep within an optimization pipeline, but
      // depends on small annotation changes from a user's initial
      // model definition, they are intentionally verbose to
      // (hopefully) provide sufficient context to a user encountering
      // the error.
      TVM_FFI_ICHECK(!previous_global_vars_.count(public_name))
          << "Function " << name_stack_.front() << " contains a lambda with kGlobalSymbol (\""
          << tvm::attr::kGlobalSymbol << "\" attribute of \"" << public_name << "\".  "
          << "However, the module already contains a GlobalVar with this name.  "
          << "If present, the kGlobalSymbol attribute must match the name of the GlobalVar, "
          << "and GlobalVar names must be unique across an IRModule.  "
          << "Lifting the " << public_name << " function out of " << name_stack_.front()
          << " would require violating one of these two conditions.";

      auto it = new_public_names_.find(public_name);
      TVM_FFI_ICHECK(it == new_public_names_.end())
          << "Function " << name_stack_.front() << " contains a lambda with kGlobalSymbol (\""
          << tvm::attr::kGlobalSymbol << "\" attribute of \"" << public_name << "\".  "
          << "However, the function " << it->second.front()
          << " also contains a lambda with the same value for kGlobalSymbol.  "
          << "If present, the kGlobalSymbol attribute must match the name of the GlobalVar, "
          << "and GlobalVar names must be unique across an IRModule.  "
          << "Lifting the " << public_name << " function out of both " << name_stack_.front()
          << " and " << it->second.front()
          << " would require violating one of these two conditions.";

      new_public_names_.insert({public_name, name_stack_});
      lifted_with_global_symbol_.insert({func, public_name});
    }

    name_stack_.push_back(binding->var->name_hint);
    lambda_location_.insert({func, name_stack_});
    ExprVisitor::VisitBinding_(binding, func);
    name_stack_.pop_back();
  }

  // De-duplication of collected names
  std::unordered_map<const FunctionNode*, ffi::String> Finalize() const {
    // The functions which still must be assigned a name
    std::unordered_map<const FunctionNode*, ffi::Array<ffi::String>> remaining_to_name =
        lambda_location_;

    // Collecting the functions that now have a name.
    std::unordered_map<const FunctionNode*, ffi::String> lifted_names;

    // A lookup for names that are unavailable for use.
    std::unordered_set<ffi::String> unavailable_names = previous_global_vars_;

    // A helper function to generate de-duplicated names.  The
    // `proposed_name_generation_func` should be a function with
    // signature:
    //
    //     ffi::Optional<ffi::String> func(const FunctionNode*, const ffi::Array<ffi::String>&)
    //
    // The first argument will be the lambda function being lifted.
    // The second argument will be the nested location where that
    // lambda function was found.  The function should return the
    // proposed name for the lifted lambda function.  The proposed
    // name will be accepted if it does not conflict with any previous
    // names, and is unique for all lambda functions being lifted.
    //
    // This helper function is used to apply several different schemes
    // to generate the name of the lifted lambda function.  The
    // overall goal is to provide names that are unique (required by
    // IRModule), deterministic (required for unit testing), and
    // human-readable.
    auto attempt_name_generation = [&](const auto& proposed_name_generation_func) {
      if (remaining_to_name.empty()) {
        return;
      }

      std::unordered_map<ffi::String, const FunctionNode*> new_names;
      for (const auto& [func, location] : remaining_to_name) {
        if (ffi::Optional<ffi::String> opt_proposed_name =
                proposed_name_generation_func(func, location)) {
          auto proposed_name = opt_proposed_name.value();

          if (unavailable_names.count(proposed_name)) {
            // The name is already used, either from a GlobalVar, or
            // from a previous round of attempted names.
          } else if (auto it = new_names.find(proposed_name); it != new_names.end()) {
            // The name is not unique within the current attempt.  Mark
            // the function as nullptr to previous any use of this name
            it->second = nullptr;
          } else {
            // The name is unique so far.  Track it for use.
            new_names.insert({proposed_name, func});
          }
        }
      }

      for (const auto& [name, func] : new_names) {
        if (func) {
          lifted_names.insert({func, name});
          remaining_to_name.erase(func);
        }
      }
    };

    // 1. Start with any publicly explosed names from kGlobalSymbol
    attempt_name_generation([&](const FunctionNode* func,
                                const auto&) -> ffi::Optional<ffi::String> {
      if (auto it = lifted_with_global_symbol_.find(func); it != lifted_with_global_symbol_.end()) {
        return it->second;
      } else {
        return std::nullopt;
      }
    });

    // 2. Try concatenating the name of the relax variable with the
    // name of the function that contains it.
    attempt_name_generation([&](const FunctionNode*, const auto& location) -> ffi::String {
      std::stringstream stream;
      stream << location.front() << "_" << location.back();
      return stream.str();
    });

    // 3. Try concatenating the entire path together.  Don't include
    // paths of length 2, as they would already be attempted earlier.
    attempt_name_generation(
        [&](const FunctionNode*, const auto& location) -> ffi::Optional<ffi::String> {
          if (location.size() == 2) return std::nullopt;

          std::stringstream stream;
          bool is_first = true;
          for (const auto& loc : location) {
            if (is_first) {
              is_first = false;
            } else {
              stream << "_";
            }
            stream << loc;
          }
          return ffi::String(stream.str());
        });

    // 4. Fallback.  Count the number of times a relax variable with
    // that name was used.
    std::unordered_map<ffi::String, int> usage_count;
    attempt_name_generation([&](const FunctionNode*, const auto& location) -> ffi::String {
      std::stringstream stream;
      stream << location.front() << "_" << location.back();
      int usage = usage_count[stream.str()]++;
      stream << "_" << usage;

      return stream.str();
    });

    TVM_FFI_ICHECK(remaining_to_name.empty())
        << "Fallback failed to make unique names for all lifted lambda functions";

    return lifted_names;
  }

  ffi::Array<ffi::String> name_stack_;
  std::unordered_set<ffi::String> previous_global_vars_;
  std::unordered_map<ffi::String, ffi::Array<ffi::String>> new_public_names_;
  std::unordered_map<const FunctionNode*, ffi::String> lifted_with_global_symbol_;
  std::unordered_map<const FunctionNode*, ffi::Array<ffi::String>> lambda_location_;
};

}  // namespace

/* The goal of this class is to lift out any nested functions into top-level
 * functions.
 *
 * We will lift a function out into a global which takes the set of the free
 * vars and then return the new created function.
 */
class LambdaLifter : public ExprMutator {
 public:
  explicit LambdaLifter(const IRModule& module)
      : ExprMutator(module), mod_(module), lifted_names_(LambdaNameCollector::Collect(module)) {}

  using ExprMutator::VisitExpr_;

  void VisitBinding_(const VarBindingNode* binding, const FunctionNode* func_node) final {
    auto cache = current_lambda_var_;
    current_lambda_var_ = binding->var;

    auto new_value = VisitExpr(binding->value);
    if (!rebind_map_.count(binding->var)) {
      ReEmitBinding(binding, new_value);
    }

    current_lambda_var_ = cache;
  }

  Expr VisitExpr_(const FunctionNode* func_node) final {
    if (!current_lambda_var_) {
      // Early bail-out for top-level functions
      return ExprMutator::VisitExpr_(func_node);
    }

    auto func = ffi::GetRef<Function>(func_node);

    ffi::String lift_func_name = [&]() {
      auto it = lifted_names_.find(func_node);
      TVM_FFI_CHECK(it != lifted_names_.end(), InternalError)
          << "Found lambda function during mutation step, "
          << "but it wasn't found during the earlier name-generation step.";
      return it->second;
    }();

    ffi::Array<Var> captured_vars;
    bool is_recursive = false;
    bool is_closure = false;
    for (const auto& var : FreeVars(func)) {
      if (current_lambda_var_.same_as(var)) {
        is_recursive = true;
      } else {
        is_closure = true;
        captured_vars.push_back(var);
      }
    }

    ffi::Array<Var> typed_captured_vars;
    ffi::Map<Var, Expr> rebinding_map;
    for (auto free_var : captured_vars) {
      Var var = Var(free_var->name_hint, GetType(free_var), free_var->span);
      typed_captured_vars.push_back(var);
      rebinding_map.Set(free_var, var);
    }

    tvm::ffi::Array<Var> lifted_func_params =
        func_node->params.Map([this](Var var) { return VisitVarDef(var); });
    for (const auto& var : typed_captured_vars) {
      lifted_func_params.push_back(var);
    }

    auto gvar_lifted_func = GlobalVar(lift_func_name);
    {
      auto func_ty = func_node->ty.as_or_throw<FuncType>();
      if (is_closure) {
        func_ty = FuncType(lifted_func_params.Map(GetType), func_ty->ret, func_ty->purity);
      }
      UpdateType(gvar_lifted_func, func_ty);
    }

    Expr body = func_node->body;

    // Defining the rewrite rule prior to visiting the body, so that
    // recursive closures can be updated.
    if (is_recursive && is_closure) {
      nested_closure_map_.emplace(current_lambda_var_.value(),
                                  Call(Type::Missing(), gvar_lifted_func,
                                       captured_vars.Map([](Var var) -> Expr { return var; })));
    }

    if (!is_closure) {
      rebind_map_.emplace(current_lambda_var_.value(), gvar_lifted_func);
    }

    body = this->VisitWithNewScope(body, lifted_func_params);
    Type ret_ty = GetType(body);
    body = Bind(body, rebinding_map);

    Function lifted_func;
    if (lifted_func_params.same_as(func_node->params) && body.same_as(func_node->body) &&
        ret_ty.same_as(func_node->ret_ty)) {
      lifted_func = ffi::GetRef<Function>(func_node);
    } else {
      lifted_func =
          Function(lifted_func_params, body, ret_ty, func_node->is_pure, func_node->attrs);
    }

    TVM_FFI_ICHECK(lifted_func.defined());

    if (is_closure || IsClosure(lifted_func)) {
      closures_.insert(gvar_lifted_func);
    }

    // Add the lifted function to the module.
    lifted_func = CopyWithNewVars(lifted_func);
    gvar_lifted_func->ty = GetType(lifted_func);

    builder_->UpdateFunction(gvar_lifted_func, lifted_func);

    Expr callable_value = gvar_lifted_func;
    if (is_closure) {
      // If we need to allocate a closure,
      // we pass the variables in its environment here.
      Tuple arg_tuple(captured_vars.Map([](Var var) -> Expr { return var; }));
      // Call make_closure intrinsic
      callable_value =
          Call(Type::Missing(), make_closure_op_, {gvar_lifted_func, arg_tuple}, {}, {});
    }

    return callable_value;
  }

  Expr VisitExpr_(const CallNode* call_node) final {
    auto call = ffi::GetRef<Call>(call_node);

    auto orig_ty = call->ty.as_or_throw<Type>();

    if (auto opt_var = call->op.as<Var>()) {
      auto var = opt_var.value();

      // Call "relax.invoke_closure" to invoke closure

      auto bound_value = LookupBinding(var);
      if (IsClosure(var) && bound_value.as<CallNode>()) {
        // if the original op was pure, we should use invoke_pure_closure
        Call orig_call = bound_value.value().as_or_throw<Call>();
        bool is_pure = [&]() -> bool {
          if (auto op = orig_call->op.as<Op>()) {
            static const auto& purity_map = Op::GetAttrMap<bool>("FPurity");
            return purity_map.get(op.value(), false);
          } else if (const auto* func_ty = orig_call->op->ty.as<FuncTypeNode>()) {
            return func_ty->purity;
          } else {
            TVM_FFI_THROW(InternalError)
                << "Could not determine purity of call to " << orig_call->op
                << ", as it is neither a tvm::Op (type = \"" << orig_call->op->GetTypeKey()
                << "\"), "
                << "nor is is annotated with FuncType (ty = " << orig_call->op->ty << ")";
          }
        }();

        auto prev = call;
        call = Call(Type::Missing(), is_pure ? invoke_pure_closure_op_ : invoke_closure_op_,
                    {var, Tuple(call->args)}, {}, {orig_ty});
      }
    }

    if (auto opt_var = call->op.as<Var>()) {
      auto var = opt_var.value();
      if (auto it = nested_closure_map_.find(var); it != nested_closure_map_.end()) {
        Call nested_call = it->second;

        ffi::Array<Expr> new_args = call->args;
        for (const auto arg : nested_call->args) {
          new_args.push_back(arg);
        }

        auto prev = call;
        call = Call(Type::Missing(), nested_call->op, new_args, call->attrs, call->ty_args);
      }
    }

    return ExprMutator::VisitExpr_(call.get());
  }

  Expr VisitExpr_(const VarNode* op) override {
    auto var = ffi::GetRef<Var>(op);
    if (auto it = rebind_map_.find(var); it != rebind_map_.end()) {
      return it->second;
    }
    return ExprMutator::VisitExpr_(op);
  }

  bool IsClosure(Expr val) {
    if (auto opt_var = val.as<Var>()) {
      if (closures_.count(opt_var.value())) {
        return true;
      }
      if (auto bound_value = LookupBinding(opt_var.value())) {
        val = bound_value.value();
      }
    }

    if (const auto* call_node = val.as<tvm::CallNode>()) {
      // recursive call
      auto op = call_node->op;
      if (auto local_var = op.as<Var>()) {
        return IsClosure(local_var.value());
      } else if (auto global_var = op.as<GlobalVar>()) {
        return IsClosure(global_var.value());
      } else {
        return make_closure_op_.same_as(op);
      }

    } else if (const auto* global_var = val.as<GlobalVarNode>()) {
      if (closures_.count(ffi::GetRef<GlobalVar>(global_var))) {
        return true;
      }
      IRModule ctx_mod = builder_->GetContextIRModule();
      TVM_FFI_ICHECK(ctx_mod->functions.size() > 0);
      BaseFunc func = ctx_mod->Lookup(ffi::GetRef<GlobalVar>(global_var));
      const auto* func_node = func.as<FunctionNode>();
      if (func_node) {
        return IsClosure(func_node->body);
      } else {
        return false;
      }

    } else if (const auto* func_node = val.as<FunctionNode>()) {
      return IsClosure(func_node->body);

    } else if (const auto* seq_node = val.as<SeqExprNode>()) {
      return IsClosure(seq_node->body);

    } else {
      return false;
    }
  }

  IRModule Lift() {
    auto glob_funcs = mod_->functions;
    for (auto [gvar, base_func] : glob_funcs) {
      if (auto opt = base_func.as<Function>()) {
        // Must visit the function itself, and not just the function
        // body, to ensure that EraseToWellDefined recognized symbolic
        // variables that are exposed by the function signature.
        auto func = VisitExpr(opt.value()).as_or_throw<Function>();
        builder_->UpdateFunction(gvar, func);
      }
    }
    return builder_->GetContextIRModule();
  }

 private:
  std::unordered_map<Var, Call> nested_closure_map_;
  std::unordered_map<Var, Expr> rebind_map_;
  std::unordered_set<ffi::Variant<GlobalVar, Var>, ffi::ObjectPtrHash, ffi::ObjectPtrEqual>
      closures_;
  ffi::Optional<Var> current_lambda_var_ = std::nullopt;
  IRModule mod_;

  std::unordered_map<const FunctionNode*, ffi::String> lifted_names_;

  /*! \brief Cache ops that would be used later to reduce lookup overhead. */
  const Op& make_closure_op_ = Op::Get("relax.make_closure");
  const Op& invoke_closure_op_ = Op::Get("relax.invoke_closure");
  const Op& invoke_pure_closure_op_ = Op::Get("relax.invoke_pure_closure");
};

namespace transform {

Pass LambdaLift() {
  auto pass_func = [=](IRModule mod, PassContext pc) { return relax::LambdaLifter(mod).Lift(); };
  return tvm::transform::CreateModulePass(pass_func, 1, "LambdaLift", {});
}

TVM_FFI_STATIC_INIT_BLOCK() {
  namespace refl = tvm::ffi::reflection;
  refl::GlobalDef().def("relax.transform.LambdaLift", LambdaLift);
}

}  // namespace transform
}  // namespace relax
}  // namespace tvm
