/*
 * 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 source_module.cc
 * \brief Source code module, only for viewing
 */

#include <tvm/ffi/cast.h>
#include <tvm/ffi/extra/module.h>
#include <tvm/ffi/function.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/runtime/tensor.h>
#include <tvm/support/io.h>

#include <algorithm>
#include <functional>
#include <string>
#include <unordered_set>
#include <vector>

#include "../../runtime/file_utils.h"
#include "../../support/bytes_io.h"
#include "codegen_source_base.h"

namespace tvm {
namespace codegen {

using ffi::Any;
using ffi::Function;
using ffi::PackedArgs;

using runtime::FunctionInfo;
using runtime::GetFileFormat;
using runtime::GetMetaFilePath;
using runtime::SaveBinaryToFile;

// Simulator function
class SourceModuleNode : public ffi::ModuleObj {
 public:
  SourceModuleNode(std::string code, std::string fmt) : code_(code), fmt_(fmt) {}
  const char* kind() const final { return "source"; }

  ffi::Optional<ffi::Function> GetFunction(const ffi::String& name) final {
    TVM_FFI_THROW(InternalError) << "Source module cannot execute, to get executable module"
                                 << " build TVM with \'" << fmt_ << "\' runtime support";
  }

  ffi::String InspectSource(const ffi::String& format) const final { return code_; }

  ffi::Array<ffi::String> GetWriteFormats() const override { return {fmt_}; }

 protected:
  std::string code_;
  std::string fmt_;
};

ffi::Module SourceModuleCreate(std::string code, std::string fmt) {
  auto n = ffi::make_object<SourceModuleNode>(code, fmt);
  return ffi::Module(n);
}

// Simulator function
class CSourceModuleNode : public ffi::ModuleObj {
 public:
  CSourceModuleNode(const std::string& code, const std::string& fmt,
                    const ffi::Array<ffi::String>& func_names,
                    const ffi::Array<ffi::String>& const_vars)
      : code_(code), fmt_(fmt), const_vars_(const_vars), func_names_(func_names) {
    if (fmt_.empty()) fmt_ = "c";
  }

  const char* kind() const final { return "c"; }

  ffi::Optional<ffi::Function> GetFunction(const ffi::String& name) final {
    ffi::ObjectPtr<ffi::Object> sptr_to_self = ffi::GetObjectPtr<ffi::Object>(this);
    // Currently c-source module is used as demonstration purposes with binary metadata module
    // that expects get_symbol interface. When c-source module is used as external module, it
    // will only contain one function. However, when its used as an internal module (e.g., target
    // "c") it can have many functions.
    if (name == "get_symbol") {
      return ffi::Function(
          [sptr_to_self, this](ffi::PackedArgs args, ffi::Any* rv) { *rv = this->func_names_[0]; });
    } else if (name == "get_const_vars") {
      return ffi::Function(
          [sptr_to_self, this](ffi::PackedArgs args, ffi::Any* rv) { *rv = this->const_vars_; });
    } else if (name == "get_func_names") {
      return ffi::Function(
          [sptr_to_self, this](ffi::PackedArgs args, ffi::Any* rv) { *rv = this->func_names_; });
    } else {
      return ffi::Function(nullptr);
    }
  }

  ffi::String InspectSource(const ffi::String& format) const final { return code_; }

  ffi::Array<ffi::String> GetWriteFormats() const override { return {fmt_}; }

  ffi::Bytes SaveToBytes() const final {
    std::string result;
    support::BytesOutStream stream(&result);
    stream.Write(code_);
    stream.Write(fmt_);

    std::vector<std::string> func_names;
    for (const auto func_name : func_names_) func_names.push_back(func_name);
    std::vector<std::string> const_vars;
    for (auto const_var : const_vars_) const_vars.push_back(const_var);
    stream.Write(func_names);
    stream.Write(const_vars);
    return ffi::Bytes(std::move(result));
  }

  static ffi::Module LoadFromBytes(const ffi::Bytes& bytes) {
    support::BytesInStream stream(bytes);

    std::string code, fmt;
    TVM_FFI_ICHECK(stream.Read(&code)) << "Loading code failed";
    TVM_FFI_ICHECK(stream.Read(&fmt)) << "Loading format failed";

    std::vector<std::string> tmp_func_names, tmp_const_vars;
    TVM_FFI_ICHECK(stream.Read(&tmp_func_names)) << "Loading func names failed";
    TVM_FFI_ICHECK(stream.Read(&tmp_const_vars)) << "Loading const vars failed";

    ffi::Array<ffi::String> func_names;
    for (auto func_name : tmp_func_names) func_names.push_back(ffi::String(func_name));

    ffi::Array<ffi::String> const_vars;
    for (auto const_var : tmp_const_vars) const_vars.push_back(ffi::String(const_var));

    auto n = ffi::make_object<CSourceModuleNode>(code, fmt, func_names, const_vars);
    return ffi::Module(n);
  }

  void WriteToFile(const ffi::String& file_name, const ffi::String& format) const final {
    std::string fmt = GetFileFormat(file_name, format);
    std::string meta_file = GetMetaFilePath(file_name);
    if (fmt == "c" || fmt == "cc" || fmt == "cpp" || fmt == "cu") {
      TVM_FFI_ICHECK_NE(code_.length(), 0);
      SaveBinaryToFile(file_name, code_);
    } else {
      TVM_FFI_ICHECK_EQ(fmt, fmt_) << "Can only save to format=" << fmt_;
    }
  }

  int GetPropertyMask() const override {
    return ffi::Module::kBinarySerializable | ffi::Module::kCompilationExportable;
  }

  bool ImplementsFunction(const ffi::String& name) final {
    return std::find(func_names_.begin(), func_names_.end(), name) != func_names_.end();
  }

 protected:
  std::string code_;
  std::string fmt_;
  ffi::Array<ffi::String> const_vars_;
  ffi::Array<ffi::String> func_names_;
};

ffi::Module CSourceModuleCreate(const ffi::String& code, const ffi::String& fmt,
                                const ffi::Array<ffi::String>& func_names,
                                const ffi::Array<ffi::String>& const_vars) {
  auto n = ffi::make_object<CSourceModuleNode>(code.operator std::string(),
                                               fmt.operator std::string(), func_names, const_vars);
  return ffi::Module(n);
}

TVM_FFI_STATIC_INIT_BLOCK() {
  namespace refl = tvm::ffi::reflection;
  refl::GlobalDef().def("ffi.Module.load_from_bytes.c", CSourceModuleNode::LoadFromBytes);
}

/*!
 * \brief A concrete class to get access to base methods of CodegenSourceBase.
 *
 * This class exist to get access to methods of CodegenSourceBase without duplicating
 * them. Therefore, keeping alignment with how codegen and source_module here generates
 * code.
 */
class ConcreteCodegenSourceBase : public CodeGenSourceBase {
  /*!
   * \brief Do nothing as this class exist to get access to methods of CodeGenSourceBase
   */
  void PrintSSAAssign(const std::string& target, const std::string& src, const Type& t) final {
    return;
  }
};

// supports limited save without cross compile
class DeviceSourceModuleNode final : public ffi::ModuleObj {
 public:
  DeviceSourceModuleNode(std::string data, std::string fmt,
                         ffi::Map<ffi::String, FunctionInfo> fmap, std::string type_key,
                         std::function<std::string(const std::string&)> fget_source)
      : data_(data), fmt_(fmt), fmap_(fmap), type_key_(type_key), fget_source_(fget_source) {}

  ffi::Optional<ffi::Function> GetFunction(const ffi::String& name) final {
    TVM_FFI_THROW(InternalError) << "Source module cannot execute, to get executable module"
                                 << " build TVM with \'" << fmt_ << "\' runtime support";
  }

  ffi::String InspectSource(const ffi::String& format) const final {
    if (fget_source_ != nullptr) {
      return fget_source_(format);
    } else {
      return data_;
    }
  }

  const char* kind() const final { return type_key_.c_str(); }
  /*! \brief Get the property of the runtime module .*/
  int GetPropertyMask() const final { return ffi::Module::kBinarySerializable; }

  void WriteToFile(const ffi::String& file_name, const ffi::String& format) const final {
    std::string fmt = GetFileFormat(file_name, format);
    TVM_FFI_ICHECK_EQ(fmt, fmt_) << "Can only save to format=" << fmt_;
    std::string meta_file = GetMetaFilePath(file_name);
    SaveMetaDataToFile(meta_file, fmap_);
    SaveBinaryToFile(file_name, data_);
  }

  ffi::Bytes SaveToBytes() const final {
    std::string result;
    support::BytesOutStream stream(&result);
    stream.Write(fmt_);
    stream.Write(fmap_);
    stream.Write(data_);
    return ffi::Bytes(std::move(result));
  }

 private:
  std::string data_;
  std::string fmt_;
  ffi::Map<ffi::String, FunctionInfo> fmap_;
  std::string type_key_;
  std::function<std::string(const std::string&)> fget_source_;
};

ffi::Module DeviceSourceModuleCreate(std::string data, std::string fmt,
                                     ffi::Map<ffi::String, FunctionInfo> fmap, std::string type_key,
                                     std::function<std::string(const std::string&)> fget_source) {
  auto n = ffi::make_object<DeviceSourceModuleNode>(data, fmt, fmap, type_key, fget_source);
  return ffi::Module(n);
}

TVM_FFI_STATIC_INIT_BLOCK() {
  namespace refl = tvm::ffi::reflection;
  refl::GlobalDef()
      .def("runtime.SourceModuleCreate", SourceModuleCreate)
      .def("runtime.CSourceModuleCreate", [](ffi::String code, ffi::String fmt,
                                             ffi::Optional<ffi::Array<ffi::String>> func_names,
                                             ffi::Optional<ffi::Array<ffi::String>> const_vars) {
        return CSourceModuleCreate(code, fmt, func_names.value_or({}), const_vars.value_or({}));
      });
}

}  // namespace codegen
}  // namespace tvm
