/*
 * 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 ir_builder.h
 * \brief Utility for building SPIRV code block
 */
#ifndef TVM_TARGET_VULKAN_IR_BUILDER_H_
#define TVM_TARGET_VULKAN_IR_BUILDER_H_

#include <tvm/ffi/function.h>
#include <tvm/tirx/expr.h>

// clang-format off
#include <algorithm>
#include <map>
#include <set>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include <tuple>
#include <spirv.hpp>
// clang-format on

#include "spirv_support.h"

namespace tvm {
namespace codegen {
namespace spirv {

/*! \brief Represent the SPIRV Type */
struct SType {
  /*! \brief The Id to represent type */
  uint32_t id{0};
  /*! \brief corresponding TVM type */
  tvm::Type type{tvm::PrimType::Void()};
  /*! \brief content type id if it is a pointer/struct-array class */
  uint32_t element_type_id{0};
  /*! \brief The storage class, if it is a pointer */
  spv::StorageClass storage_class{spv::StorageClassMax};
};

enum ValueKind {
  kNormal,
  kConstant,
  kVectorPtr,
  kStructArrayPtr,
  kPushConstantPtr,
  kFunction,
  kExtInst,
  kUniformPtr,
  kSpecConst,
};

/*! \brief Represent the SPIRV Value */
struct Value {
  /*! \brief The Id to represent value */
  uint32_t id{0};
  /*! \brief The data type */
  SType stype;
  /*! \brief additional flags about the value */
  ValueKind flag{kNormal};
};

/*! \brief Represent the SPIRV Label */
struct Label {
  /*! \brief The Id to represent label */
  uint32_t id{0};
};

/*!
 * \brief A SPIRV instruction,
 *  can be used as handle to modify its content later
 */
class Instr {
 public:
  /*! \return the word count */
  uint32_t WordCount() const { return word_count_; }
  /*!
   * \brief Access idx-th word of instruction
   * \param idx The index
   * \return reference to idx-th word.
   */
  uint32_t& operator[](uint32_t idx) {
    TVM_FFI_ICHECK_LT(idx, word_count_);
    return (*data_)[begin_ + idx];
  }

 private:
  friend class InstrBuilder;
  /*!
   * \brief the data that backs this instruction
   *  Have to use vector reference because
   *  vector can change.
   */
  std::vector<uint32_t>* data_{nullptr};
  /*! \brief begin location of instruction */
  uint32_t begin_{0};
  /*! \brief work count */
  uint32_t word_count_{0};
};

/*! \brief Representation of phi value */
struct PhiValue : public Value {
  /*! \brief The corresponding instr */
  Instr instr;
  /*!
   * \brief Add incoming information of a PhiValue
   * \param index The location of Phi
   * \param value The value to come
   * \param parent The parent label.
   */
  void SetIncoming(uint32_t index, const Value& value, const Label& parent) {
    TVM_FFI_ICHECK_EQ(this->stype.id, value.stype.id);
    instr[3 + index * 2] = value.id;
    instr[3 + index * 2 + 1] = parent.id;
  }
};

/*!
 * \brief Helper class to build SPIRV instruction.
 *
 * \code
 *
 *  std::vector<uint32_t> func_seg_vec_;
 *  InstrBuilder ib;
 *
 *  // construct and append to the end of func_seg_vec_;
 *  ib.Begin(spv::OpIAdd)
 *    .Add(result).Add(v1).Add(v2)
 *    .Commit(&func_seg_vec_);
 *
 * \endcode
 */
class InstrBuilder {
 public:
  /*!
   * \brief Begin construction of instruction.
   * \param op The op code
   * \return reference to self.
   */
  InstrBuilder& Begin(spv::Op op) {  // NOLINT(*);
    // finish previous build
    TVM_FFI_ICHECK_EQ(data_.size(), 0U);
    op_ = op;
    data_.push_back(0);
    return *this;
  }
  /*!
   * \brief Add v to end of instruction.
   * \param v The value to be appended to the instruction.
   * \return reference to self.
   */
  InstrBuilder& Add(const Value& v) {
    data_.push_back(v.id);
    return *this;
  }
  /*!
   * \brief Add v to end of instruction.
   * \param v The type to be appended to the instruction.
   * \return reference to self.
   */
  InstrBuilder& Add(const SType& v) {
    data_.push_back(v.id);
    return *this;
  }
  /*!
   * \brief Add v to end of instruction.
   * \param v The label to be appended to the instruction.
   * \return reference to self.
   */
  InstrBuilder& Add(const Label& v) {
    data_.push_back(v.id);
    return *this;
  }
  /*!
   * \brief Add a word to end of instruction.
   * \param v The value to be added.
   * \return reference to self.
   */
  InstrBuilder& Add(const uint32_t& v) {
    data_.push_back(v);
    return *this;
  }
  /*!
   * \brief Add string literal of end of instruction.
   * \param v The string literal to be appended.
   * \return reference to self.
   */
  InstrBuilder& Add(const std::string& v) {
    const uint32_t kWordSize = sizeof(uint32_t);
    uint32_t nwords = (static_cast<uint32_t>(v.length()) + kWordSize) / kWordSize;
    size_t begin = data_.size();
    data_.resize(begin + nwords, 0U);
    std::copy(v.begin(), v.end(), reinterpret_cast<char*>(&data_[begin]));
    return *this;
  }
  /*!
   * \brief add sequence of values to instruction
   * \param args The instruction sequence
   * \return reference to self.
   * \tparam Args The positional arguments
   */
  template <typename... Args>
  InstrBuilder& AddSeq(Args&&... args) {
    AddSeqHelper helper;
    helper.builder = this;
    ffi::details::for_each(helper, std::forward<Args>(args)...);
    return *this;
  }
  /*!
   * \brief Finish build, commit the current
   *   instruction to the end of seg.
   *
   * \param seg The code segment to commit to
   * \return The result instruction.
   */
  Instr Commit(std::vector<uint32_t>* seg) {
    Instr ret;
    ret.data_ = seg;
    ret.begin_ = seg->size();
    ret.word_count_ = static_cast<uint32_t>(data_.size());
    data_[0] = op_ | (ret.word_count_ << spv::WordCountShift);
    seg->insert(seg->end(), data_.begin(), data_.end());
    data_.clear();
    return ret;
  }

 private:
  // current op code.
  spv::Op op_;
  // The internal data to store code
  std::vector<uint32_t> data_;
  // helper class to support variadic arguments
  struct AddSeqHelper {
    // The reference to builder
    InstrBuilder* builder;
    // invoke function
    template <typename T>
    void operator()(size_t, const T& v) const {
      builder->Add(v);
    }
  };
};

/*!
 * \brief Builder to build up a single SPIR-V module
 *
 *  This is a thin wrapper to build SPIRV binary.
 *  SPIRV adopts structure control-flow.
 *  We can build the code by always appending to the end of the
 *  binary code block and revisit some
 *
 *  This IRBuilder did not introduce concept of BasicBlock.
 *  instead instructions are append to end of each segment.
 */
class IRBuilder {
 public:
  /*!
   * \brief Initialize the codegen based on a specific feature set.
   *
   * \param support The features in SPIRV that are supported by the
   * target device.
   */
  explicit IRBuilder(const SPIRVSupport& support);

  /*! \brief Initialize header */
  void InitHeader();
  /*! \brief Initialize the predefined contents */
  void InitPreDefs();
  /*!
   * \brief Import additional extension libraries.
   * \param name The name of the library.
   * \return The finalized binary instruction.
   */
  Value ExtInstImport(const std::string& name) {
    auto it = ext_inst_tbl_.find(name);
    if (it != ext_inst_tbl_.end()) {
      return it->second;
    }
    Value val = NewValue(SType(), kExtInst);
    ib_.Begin(spv::OpExtInstImport).AddSeq(val, name).Commit(&extended_instruction_section_);
    ext_inst_tbl_[name] = val;
    return val;
  }
  /*!
   * \brief Get the final binary built from the builder
   * \return The finalized binary instruction.
   */
  std::vector<uint32_t> Finalize();

  /*!
   * \brief Create new label
   * \return The created new label
   */
  Label NewLabel() {
    Label label;
    label.id = id_counter_++;
    return label;
  }
  /*!
   * \brief Start a new block with given label
   * \param label The label we use.
   */
  void StartLabel(Label label) {
    MakeInst(spv::OpLabel, label);
    curr_label_ = label;
  }
  /*! \return The current label */
  Label CurrentLabel() const { return curr_label_; }
  /*!
   * \brief Add code to debug segment.
   * \param op The operator
   * \param args The instruction sequence
   * \tparam Args The positional arguments
   */
  template <typename... Args>
  void Debug(spv::Op op, Args&&... args) {
    ib_.Begin(op).AddSeq(std::forward<Args>(args)...).Commit(&debug_);
  }

  /*!
   * \brief Set the name of a value or label
   * \param obj The object to be named
   * \param name The name of the object
   * \tparam Obj The type of the object being named.  Typically a Label or Value.
   */
  template <typename Obj>
  void SetName(Obj&& obj, const std::string& name) {
    Debug(spv::OpName, std::forward<Obj>(obj), name);
  }

  /*!
   * \brief Add Execution mode to a function.
   * \param func The function value
   * \param args The instruction sequence
   * \tparam Args The positional arguments
   */
  template <typename... Args>
  void ExecutionMode(Value func, Args&&... args) {
    ib_.Begin(spv::OpExecutionMode).AddSeq(func, std::forward<Args>(args)...).Commit(&exec_mode_);
  }
  /*!
   * \brief Add code to decorate segment.
   * \param op The operator
   * \param args The instruction sequence
   * \tparam Args The positional arguments
   */
  template <typename... Args>
  void Decorate(spv::Op op, Args&&... args) {
    ib_.Begin(op).AddSeq(std::forward<Args>(args)...).Commit(&decorate_);
  }
  /*!
   * \brief Add code to global segment.
   * \param op The operator
   * \param args The instruction sequence
   * \tparam Args The positional arguments
   */
  template <typename... Args>
  void DeclareGlobal(spv::Op op, Args&&... args) {
    ib_.Begin(op).AddSeq(std::forward<Args>(args)...).Commit(&global_);
  }
  /*!
   * \brief Make a new instruction and append it to end of function segment.
   *
   * \param op The operator
   * \param args The instruction sequence
   * \return The result SSA value.
   * \tparam Args The positional arguments
   */
  template <typename... Args>
  Instr MakeInst(spv::Op op, Args&&... args) {
    return ib_.Begin(op).AddSeq(std::forward<Args>(args)...).Commit(&function_);
  }
  /*!
   * \brief Make a new SSA value,
   *
   * \param op The operator.
   * \param out_type The result type.
   * \param args The instruction sequence
   * \return The result SSA value.
   * \tparam Args The positional arguments
   */
  template <typename... Args>
  Value MakeValue(spv::Op op, const SType& out_type, Args&&... args) {
    Value val = NewValue(out_type, kNormal);
    MakeInst(op, out_type, val, std::forward<Args>(args)...);
    return val;
  }
  /*!
   * \brief Make a phi value.
   *
   * \param out_type The output data type.
   * \param num_incoming number of incoming blocks.
   * \return The result Phi value.
   */
  PhiValue MakePhi(const SType& out_type, uint32_t num_incoming);
  /*!
   * \brief Create a GLSL450 call
   *
   * \param ret_type The result type.
   * \param inst_id The instance id of the function.
   * \param args The arguments
   * \return The result value.
   */
  Value CallGLSL450(const SType& ret_type, uint32_t inst_id, const std::vector<Value>& args);

  /*!
   * \brief Create a SPIRV_KHR_integer_dot_product call
   *
   * \param ret_type The result type.
   * \param args The arguments
   * \return The result value.
   */
  Value CallKHRIntegerDotProduct(const SType& ret_type, const std::vector<Value>& args,
                                 const PrimType& dtype);

  /*!
   * \brief Build vector by concatenating components
   *
   * \param vec The vector component
   * \tparam Args The positional arguments
   */
  Value Concat(const std::vector<Value>& vec);
  /*!
   * \brief Get the spirv type for a given tvm data type.
   * \param dtype The data type.
   * \return The corresponding spirv type.
   */
  SType GetSType(const tvm::PrimType& dtype, uint32_t row = 0, uint32_t col = 0);
  /*!
   * \brief Get the pointer type that points to value_type
   * \param value_type.
   * \param storage_class The storage class
   * \return The corresponding spirv type.
   */
  SType GetPointerType(const SType& value_type, spv::StorageClass storage_class);
  /*!
   * \brief Get a struct{ value_type[num_elems] } type.
   * \param value_type the content value type.
   * \param num_elems number of elements in array
   *   num_elems = 0 means runtime array with BufferBlock Decoration
   * \param interface_block if this array type for interface blocks(input, output, uniform,
   *   storage buffer).
   *
   * \return The corresponding spirv type.
   */
  SType GetStructArrayType(const SType& value_type, uint32_t num_elems, bool interface_block);
  /*!
   * \brief Get a struct array access with a given index.
   * \param ptr_type The pointer type.
   * \param buffer The buffer ptr to struct array
   * \param index The array index.
   */
  Value StructArrayAccess(const SType& ptr_type, Value buffer, Value index);
  /*!
   * \brief Create a cast that cast value to dst_type
   * \param dst_type The target type.
   * \param value the source value.
   * \return The result value
   */
  Value Cast(const SType& dst_type, Value value);
  /*
   * \brief Create a const integer.
   * \param dtype The content data type.
   * \param value The data value.
   */
  Value IntImm(const SType& dtype, int64_t value);
  /*
   * \brief Create a const unsigned integer.
   * \param dtype The content data type.
   * \param value The data value.
   */
  Value UIntImm(const SType& dtype, uint64_t value);
  /*
   * \brief Create a const float.
   * \param dtype The content data type.
   * \param value The data value.
   */
  Value FloatImm(const SType& dtype, double value);
  /*
   * \brief Declare buffer argument of function
   *
   * \param arg_type The type of argument.
   * \param descriptor_set The descriptor set we want to use.
   * \param binding The binding location in descriptor set.
   * \param The argument type.
   */
  Value BufferArgument(const SType& value_type, uint32_t descriptor_set, uint32_t binding);

  /*!
   * \brief Declare POD arguments through push constants.
   *
   * \note Only call this function once!
   * \param value_types The values in the push constant
   * \return reference to self.
   */
  Value DeclarePushConstant(const std::vector<SType>& value_types);
  /*!
   * \brief Get i-th push constant
   * \param v_type The value type
   * \param index The push constant index
   * \return the value of push constant
   */
  Value GetPushConstant(Value ptr_push_const, const SType& v_type, uint32_t index);

  /*!
   * \brief Declare POD arguments through uniform buffer.
   *
   * \note Only call this function once!
   * \param value_types The values in the uniform buffer
   * \param descriptor_set The descriptor set we want to use
   * \param binding The binding location in descriptor set
   * \return reference to self.
   */
  Value DeclareUniformBuffer(const std::vector<SType>& value_types, uint32_t descriptor_set,
                             uint32_t binding);
  /*!
   * \brief Get i-th uniform constant
   * \param v_type The value type
   * \param index The uniform index
   * \return the value of uniform constant
   */
  Value GetUniform(Value ptr_ubo, const SType& v_type, uint32_t index);
  /*!
   * \brief Declare a new function
   * \return The created function ID.
   */
  Value NewFunction();
  /*!
   * \brief Declare the entry point for a kernel function. This should be
   * invoked after building the function so the builder is aware of which
   * variables to declare as part of the function's interface.
   * \param func The previously declared function.
   * \param name Name of the entry point.
   */
  void CommitKernelFunction(const Value& func, const std::string& name);
  /*!
   * \brief Start function scope.
   * \param func function to be started.
   */
  void StartFunction(const Value& func);
  /*!
   * \brief Set the local size of the function
   * \param func function of interest
   * \param local_size The local workgroup_size
   */
  void SetLocalSize(const Value& func, uint32_t local_size[3]);
  /*
   * \brief Allocate space
   * \param value_type The content value type
   * \param num_elems Number of elements to allocate.
   * \param storage_class The storage class we want to store to.
   */
  Value Allocate(const SType& value_type, uint32_t num_elems, spv::StorageClass storage_class);
  /*
   * \brief Get the i-th workgroup id.
   * \return The value representing the workgroup id.
   */
  Value GetWorkgroupID(uint32_t dim_index);
  /*
   * \brief Get the i-th local id.
   * \return The value representing the local id.
   */
  Value GetLocalID(uint32_t dim_index);
  // Expressions
  Value Add(Value a, Value b);
  Value Sub(Value a, Value b);
  Value Mul(Value a, Value b);
  Value Div(Value a, Value b);
  Value Mod(Value a, Value b);
  Value EQ(Value a, Value b);
  Value NE(Value a, Value b);
  Value LT(Value a, Value b);
  Value LE(Value a, Value b);
  Value GT(Value a, Value b);
  Value GE(Value a, Value b);
  Value Select(Value cond, Value a, Value b);
  /*
   * \brief Get composite constant
   * \param ele_stype The value type of elements in the composite.
   * \param composite_type The value type of the composite.
   * \param dval The initial value for all elements in the composite.
   */
  Value GetCompositeConst(const SType& ele_stype, const SType& composite_stype, double dval);
  /*
   * Get specialization constant
   * \param dtype The content value type
   * \param value The default value
   */
  Value GetSpecConst(const SType& dtype, uint64_t value);

 private:
  /*!
   * \brief Create new value
   * \return The created new label
   */
  Value NewValue(const SType& stype, ValueKind flag) {
    Value val;
    val.id = id_counter_++;
    val.stype = stype;
    val.flag = flag;
    return val;
  }

  /*! \brief Get a built-in value provided by SPIR-V
   *
   *  \param built_in The SPIR-V built-in array to access.  For
   *  example, spv::BuiltInLocalInvocationId to access the thread
   *  id.
   *
   *  \param index The index of the built-in array to access.
   *
   *  \param name The name of the value being accessed.  For
   *  example, "threadIdx.x".  This is for debug purposes, and is
   *  used to tag the variable with OpName.
   */
  Value GetBuiltInValue(spv::BuiltIn built_in, uint32_t index, const std::string& name = "");

  /*!
   * \brief The common function to declare push constants or uniform buffer
   * \param value_types The values in the push constants or uniform buffer
   * \param storage_class An enum defined by SPIR-V indicating push constant or uniform
   * \param kind An enum indicating push constant or uniform
   * \return The created new label
   */
  Value DeclareStorageVariable(const std::vector<SType>& value_types,
                               spv::StorageClass storage_class, ValueKind kind);

  /*!
   * \brief The common function to decorate storage buffer or uniform buffer arguments.
   * \param val The Value to be decorated.
   * \param descriptor_set The index of the descriptor set containing the buffer's descriptor
   * \param binding The index of the buffer's descriptor within the descriptor set
   */
  void DecorateBufferArgument(Value val, uint32_t descriptor_set, uint32_t binding);

  // get constant given value encoded in uint64_t
  Value GetConst_(const SType& dtype, const uint64_t* pvalue);

  // declare type
  SType DeclareType(const PrimType& dtype, uint32_t row = 0, uint32_t col = 0);

  // Declare the appropriate SPIR-V capabilities and extensions to use
  // this data type.
  void AddCapabilityFor(const PrimType& dtype);

  /*! \brief SPIRV-related capabilities of the target
   *
   * This SPIRVSupport object is owned by the same CodeGenSPIRV
   * object that owns the IRBuilder.  Therefore, safe to use a
   * reference as the CodeGenSPIRV will live longer.
   */
  const SPIRVSupport& spirv_support_;

  /*! \brief internal instruction builder  */
  InstrBuilder ib_;
  /*! \brief Current label */
  Label curr_label_;
  /*! \brief The current maximum id */
  uint32_t id_counter_{1};
  /*! \brief glsl 450 extension */
  Value ext_glsl450_;
  /*! \brief Special cache int32, fp32, void*/
  SType t_bool_, t_int32_, t_uint32_, t_fp32_, t_void_, t_void_func_;
  /*! \brief quick cache for const one i32 */
  Value const_i32_zero_;

  /*! \brief The cached values for built-in arrays
   *
   *  Maps from a tuple of spv::BuiltIn enum to the Value containing
   *  that built-in array.  For example,
   *  ``built_in_tbl_[spv::BuiltInLocalInvocationId]`` is the array
   *  of invocation ids, equivalent to an array of ``threadIdx.x``,
   *  ``threadIdx.y``, and ``threadIdx.z`` in CUDA.
   *
   *  These are declared in the global section of the shader.
   */
  std::unordered_map<spv::BuiltIn, Value> built_in_tbl_;

  /*! \brief Module-scope variables used by the entry point.
   *
   * SPIR-V 1.4 and later require all used global variables in OpEntryPoint.
   */
  std::vector<Value> entry_point_interface_;

  /*! \brief The cached values for built-in values
   *
   *  Maps from a tuple of (spv::BuiltIn enum, index) to the value
   *  stored at that index of the built-in array.  For example,
   *  ``built_in_tbl_[{spv::BuiltInLocalInvocationId, 0}]`` is the
   *  first index of the invocation id, equivalent to
   *  ``threadIdx.x`` in CUDA.
   *
   *  These are declared in the first block of the function, in the
   *  ``function_scope_vars_`` section.
   */
  std::map<std::tuple<spv::BuiltIn, uint32_t>, Value> built_in_values_tbl_;

  /*! \brief whether push constant is defined */
  Value push_const_;
  /*! \brief map from type code to the type */
  std::unordered_map<uint64_t, SType> pod_type_tbl_;
  /*! \brief map from value to array type */
  std::map<std::tuple<uint32_t, uint32_t, bool>, SType> struct_array_type_tbl_;
  /*! \brief map from value to its pointer type */
  std::map<std::pair<uint32_t, spv::StorageClass>, SType> pointer_type_tbl_;
  /*! \brief map from constant int to its value */
  std::map<std::pair<uint32_t, uint64_t>, Value> const_tbl_;
  /*! \brief map from floating point composite constant to its value */
  std::map<std::pair<uint32_t, double>, Value> composite_const_tbl_;
  /*! \brief map from name of a ExtInstImport to its value */
  std::map<std::string, Value> ext_inst_tbl_;

  /*! \brief Header segment
   *
   * 5 words long, described in "First Words of Physical Layout"
   * section of SPIR-V documentation.
   */
  std::vector<uint32_t> header_;
  /*! \brief SPIR-V capabilities used by this module. */
  std::set<spv::Capability> capabilities_used_;
  /*! \brief SPIR-V extensions used by this module. */
  std::set<std::string> extensions_used_;
  /*! \brief entry point segment */
  std::vector<uint32_t> extended_instruction_section_;
  /*! \brief entry point segment */
  std::vector<uint32_t> entry_;
  /*! \brief Header segment */
  std::vector<uint32_t> exec_mode_;
  /*! \brief Debug segment */
  std::vector<uint32_t> debug_;
  /*! \brief Annotation segment */
  std::vector<uint32_t> decorate_;
  /*! \brief Global segment: types, variables, types */
  std::vector<uint32_t> global_;
  /*! \brief Function header segment
   *
   * Contains the start of function (spv::OpFunction), first label
   * (spv::OpLabel), and all array allocations (spv::OpVariable).
   */
  std::vector<uint32_t> func_header_;
  /*! \brief Function-scope variable declarations
   *
   * Contains variable declarations that should be accessible
   * throughout the entire kernel (e.g. threadIdx.x).  This must be
   * separate from func_header_, because the function-level
   * spv::OpVariable declarations must come first in the first block
   * of a function.
   */
  std::vector<uint32_t> function_scope_vars_;
  /*! \brief Function segment */
  std::vector<uint32_t> function_;
};

}  // namespace spirv
}  // namespace codegen
}  // namespace tvm

#endif  // TVM_TARGET_VULKAN_IR_BUILDER_H_
