/*
 * 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 codegen_arm.cc
 * \brief ARM specific code generator
 */
#ifdef TVM_LLVM_VERSION

#include <llvm/IR/Intrinsics.h>
#include <llvm/IR/IntrinsicsARM.h>
#include <llvm/Target/TargetMachine.h>
#include <tvm/ffi/function.h>
#include <tvm/ffi/reflection/registry.h>

#include "codegen_cpu.h"

namespace tvm {
namespace codegen {

// ARM specific code generator, this is used as an example on
// how to override behavior llvm code generator for specific target
class CodeGenARM final : public CodeGenCPU {
 public:
  CodeGenARM() = default;
  virtual ~CodeGenARM() = default;

  void InitTarget() final {
    // set native vector bits.
    native_vector_bits_ = 16 * 8;
    CodeGenCPU::InitTarget();
  }
  llvm::Value* CreateIntrinsic(const CallNode* op) override;

 private:
  PrimExpr ARMPopcount(const CallNode* op);
};

llvm::Value* CodeGenARM::CreateIntrinsic(const CallNode* op) {
  if (op->op.same_as(builtin_call_llvm_intrin_) || op->op.same_as(builtin_call_llvm_pure_intrin_)) {
    llvm::Intrinsic::ID id =
        static_cast<llvm::Intrinsic::ID>(op->args[0].as_or_throw<IntImm>()->value);
    if (id == llvm::Intrinsic::ctpop) {
      PrimExpr e = ARMPopcount(op);
      return CodeGenCPU::CreateIntrinsic(e.as<CallNode>());
    }
  }
  return CodeGenCPU::CreateIntrinsic(op);
}

PrimExpr CodeGenARM::ARMPopcount(const CallNode* call) {
  using namespace tirx;
  PrimExpr e = call->args[1].as_or_throw<PrimExpr>();
  PrimType call_ty = call->ty.as_or_throw<PrimType>();
  llvm::Intrinsic::ID ctpop_id = llvm::Intrinsic::ctpop;
  llvm::Intrinsic::ID vpaddlu_id = llvm::Intrinsic::arm_neon_vpaddlu;

  // Fallback to default llvm lowering rule if input type not a full vector or half vector length
  int total_size = call_ty.bits() * call_ty.lanes();
  if (!call_ty.IsFixedLengthVector() || call_ty.bits() == 8 ||
      (total_size != 128 && total_size != 64)) {
    ffi::Array<PrimExpr> vcnt_args;
    vcnt_args.push_back(IntImm(PrimType::UInt(32), ctpop_id));
    vcnt_args.push_back(e);
    return Call(call_ty, builtin_call_llvm_pure_intrin_, vcnt_args).as_or_throw<PrimExpr>();
  }

  // Popcount lowering rule:
  // Reinterpret input vector as a vector of 8bit values and preform popcount
  // Pairwise add between adjacent elements and double width with vpaddlu
  // to return back to original input type

  // Dvisions are always divisible (number of bits = 64 or 128)
  PrimType e_dtype = e.ty();
  PrimType uint8_type = PrimType(e_dtype.code(), 8, e_dtype.bits() * e_dtype.lanes() / 8);
  PrimType uint16_type =
      PrimType(uint8_type.code(), 16, uint8_type.bits() * uint8_type.lanes() / 16);
  PrimType uint32_type =
      PrimType(uint16_type.code(), 32, uint8_type.bits() * uint8_type.lanes() / 32);

  // Interpret input as vector of 8bit values
  PrimExpr input8 = reinterpret(uint8_type, e);
  // Popcount 8bit->8bit
  const CallNode* c0 = input8.as<CallNode>();
  TVM_FFI_ICHECK(c0 != nullptr);
  ffi::Array<PrimExpr> vcnt8_args;
  vcnt8_args.push_back(IntImm(PrimType::UInt(32), ctpop_id));
  vcnt8_args.push_back(input8);
  PrimExpr vcnt8 =
      Call(uint8_type, builtin_call_llvm_pure_intrin_, vcnt8_args).as_or_throw<PrimExpr>();

  // Accumulation 8->16bit
  ffi::Array<PrimExpr> vcnt16_args;
  vcnt16_args.push_back(IntImm(PrimType::UInt(32), vpaddlu_id));
  vcnt16_args.push_back(vcnt8);
  PrimExpr vcnt16 =
      Call(uint16_type, builtin_call_llvm_pure_intrin_, vcnt16_args).as_or_throw<PrimExpr>();
  if (call_ty.bits() == 16) {
    return vcnt16;
  }

  // Accumulation 16->32bit
  ffi::Array<PrimExpr> vcnt32_args;
  vcnt32_args.push_back(IntImm(PrimType::UInt(32), vpaddlu_id));
  vcnt32_args.push_back(vcnt16);
  PrimExpr vcnt32 =
      Call(uint32_type, builtin_call_llvm_pure_intrin_, vcnt32_args).as_or_throw<PrimExpr>();
  if (call_ty.bits() == 32) {
    return vcnt32;
  }

  // Accumulation 32->64bit
  ffi::Array<PrimExpr> vcnt64_args;
  vcnt64_args.push_back(IntImm(PrimType::UInt(32), vpaddlu_id));
  vcnt64_args.push_back(vcnt32);
  return Call(call_ty, builtin_call_llvm_pure_intrin_, vcnt64_args).as_or_throw<PrimExpr>();
}

TVM_FFI_STATIC_INIT_BLOCK() {
  namespace refl = tvm::ffi::reflection;
  refl::GlobalDef().def_packed("tvm.codegen.llvm.target_arm",
                               [](const ffi::PackedArgs& targs, ffi::Any* rv) {
                                 *rv = static_cast<void*>(new CodeGenARM());
                               });
}

}  // namespace codegen
}  // namespace tvm

#endif  // TVM_LLVM_VERSION
