/*
 * 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.
 */
#ifndef TVM_SCRIPT_PRINTER_IR_DOCSIFIER_FUNCTOR_H_
#define TVM_SCRIPT_PRINTER_IR_DOCSIFIER_FUNCTOR_H_

#include <tvm/ffi/function.h>
#include <tvm/runtime/logging.h>

#include <optional>
#include <string>
#include <type_traits>
#include <unordered_map>
#include <utility>
#include <vector>

namespace tvm {
namespace script {
namespace printer {

/*!
 * \brief Dynamic dispatch functor based on AccessPath.
 *
 * This functor dispatches based on the type of object and the input dispatch token.
 */
template <typename R, typename... Args>
class IRDocsifierFunctor {
 private:
  using TSelf = IRDocsifierFunctor<R, Args...>;

  template <class TObjectRef, class TCallable>
  using IsDispatchFunction =
      typename std::is_convertible<TCallable, std::function<R(TObjectRef, Args...)>>;

 public:
  /*!
   * \brief Call the dispatch function.
   * \param token The dispatch token.
   * \param obj The object.
   * \param args Other args.
   *
   * \return The return value of the dispatch function
   *
   * If the TObjectRef isn't registered with the token, it will try to find
   * dispatch function for TObjectRef with the default dispatch token (empty string).
   */
  template <class TObjectRef>
  R operator()(const ffi::String& token, TObjectRef obj, Args... args) const {
    uint32_t type_index = obj.defined() ? obj->type_index() : 0;
    const ffi::Function* pf = nullptr;
    if ((pf = LookupDispatchTable(token, type_index)) != nullptr) {
      return (*pf)(obj, args...).template cast<R>();
    }
    if ((pf = LookupDispatchTable("", type_index)) != nullptr) {
      return (*pf)(obj, args...).template cast<R>();
    }
    if ((pf = LookupFallback()) != nullptr) {
      return (*pf)(obj, args...).template cast<R>();
    }

    LOG(WARNING) << "ObjectFunctor calls un-registered function on type: "
                 << ffi::Object::TypeIndex2Key(type_index) << " (token: " << token << ")"
                 << ". ObjectType: " << obj->GetTypeKey() << ". Object: " << obj;
    TVM_FFI_ICHECK(false) << "ObjectFunctor calls un-registered function on type: "
                          << ffi::Object::TypeIndex2Key(type_index) << " (token: " << token << ")"
                          << ". ObjectType: " << obj->GetTypeKey() << ". Object: " << obj;
    TVM_FFI_UNREACHABLE();
  }

  /*!
   * \brief Set the dispatch function
   * \param token The dispatch token.
   * \param type_index The TVM object type index for this dispatch function.
   * \param f The dispatch function.
   *
   * This takes a type-erased packed function as input. It should be used
   * through FFI boundary, for example, registering dispatch function from Python.
   */
  TSelf& set_dispatch(ffi::String token, uint32_t type_index, ffi::Function f) {
    std::vector<ffi::Function>* table = &dispatch_table_[token];
    if (table->size() <= type_index) {
      table->resize(type_index + 1, nullptr);
    }
    ffi::Function& slot = (*table)[type_index];
    if (slot != nullptr) {
      TVM_FFI_ICHECK(false) << "Dispatch for type is already registered: "
                            << ffi::Object::TypeIndex2Key(type_index);
    }
    slot = f;
    return *this;
  }

  TSelf& set_fallback(ffi::Function f) {
    TVM_FFI_ICHECK(!dispatch_fallback_.has_value()) << "Fallback is already defined";
    dispatch_fallback_ = f;
    return *this;
  }

  void remove_fallback() { dispatch_fallback_ = std::nullopt; }

  /*!
   * \brief Set the dispatch function
   * \param token The dispatch token.
   * \param f The dispatch function.
   */
  template <typename TObjectRef, typename TCallable,
            typename = std::enable_if_t<IsDispatchFunction<TObjectRef, TCallable>::value>>
  TSelf& set_dispatch(ffi::String token, TCallable f) {
    return set_dispatch(token, TObjectRef::ContainerType::RuntimeTypeIndex(),
                        ffi::TypedFunction<R(TObjectRef, Args...)>(f));
  }

  template <typename TCallable,
            typename = std::enable_if_t<IsDispatchFunction<ffi::ObjectRef, TCallable>::value>>
  TSelf& set_fallback(TCallable f) {
    ffi::Function func = ffi::TypedFunction<R(ffi::ObjectRef, Args...)>(f);
    return set_fallback(func);
  }

  /*!
   * \brief Remove dispatch function
   * \param token The dispatch token.
   * \param type_index The TVM object type index for the dispatch function to be removed.
   *
   * This is useful when dispatch function comes from other language's runtime, and
   * those function should be removed before that language runtime shuts down.
   */
  void remove_dispatch(ffi::String token, uint32_t type_index) {
    std::vector<ffi::Function>* table = &dispatch_table_[token];
    if (table->size() <= type_index) {
      return;
    }
    (*table)[type_index] = nullptr;
  }

 private:
  /*!
   * \brief Look up the dispatch table for the given token and type_index.
   * \param token The dispatch token.
   * \param type_index The TVM object type index.
   * \return Returns the functor if the lookup succeeds, nullptr otherwise.
   */
  const ffi::Function* LookupDispatchTable(const ffi::String& token, uint32_t type_index) const {
    auto it = dispatch_table_.find(token);
    if (it == dispatch_table_.end()) {
      return nullptr;
    }
    const std::vector<ffi::Function>& tab = it->second;
    if (type_index >= tab.size()) {
      return nullptr;
    }
    const ffi::Function* f = &tab[type_index];
    if (f->defined()) {
      return f;
    } else {
      return nullptr;
    }
  }

  /*!
   * \brief Look up the fallback to be used if no handler is registered
   */
  const ffi::Function* LookupFallback() const {
    if (dispatch_fallback_.has_value()) {
      return &*dispatch_fallback_;
    } else {
      return nullptr;
    }
  }

  /*
   * This type alias and the following free functions are created to reduce the binary bloat
   * from template and also hide implementation details from this header
   */
  using DispatchTable = std::unordered_map<std::string, std::vector<ffi::Function>>;
  /*! \brief The dispatch table. */
  DispatchTable dispatch_table_;
  std::optional<ffi::Function> dispatch_fallback_;
};

}  // namespace printer
}  // namespace script
}  // namespace tvm
#endif  // TVM_SCRIPT_PRINTER_IR_DOCSIFIER_FUNCTOR_H_
