// 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.

#include "gandiva/engine.h"

#include <gtest/gtest.h>
#include <functional>
#include "gandiva/llvm_types.h"
#include "gandiva/tests/test_util.h"

namespace gandiva {

typedef int64_t (*add_vector_func_t)(int64_t* data, int n);

class TestEngine : public ::testing::Test {
 protected:
  std::string BuildVecAdd(Engine* engine) {
    auto types = engine->types();
    llvm::IRBuilder<>* builder = engine->ir_builder();
    llvm::LLVMContext* context = engine->context();

    // Create fn prototype :
    //   int64_t add_longs(int64_t *elements, int32_t nelements)
    std::vector<llvm::Type*> arguments;
    arguments.push_back(types->i64_ptr_type());
    arguments.push_back(types->i32_type());
    llvm::FunctionType* prototype =
        llvm::FunctionType::get(types->i64_type(), arguments, false /*isVarArg*/);

    // Create fn
    std::string func_name = "add_longs";
    engine->AddFunctionToCompile(func_name);
    llvm::Function* fn = llvm::Function::Create(
        prototype, llvm::GlobalValue::ExternalLinkage, func_name, engine->module());
    assert(fn != nullptr);

    // Name the arguments
    llvm::Function::arg_iterator args = fn->arg_begin();
    llvm::Value* arg_elements = &*args;
    arg_elements->setName("elements");
    ++args;
    llvm::Value* arg_nelements = &*args;
    arg_nelements->setName("nelements");
    ++args;

    llvm::BasicBlock* loop_entry = llvm::BasicBlock::Create(*context, "entry", fn);
    llvm::BasicBlock* loop_body = llvm::BasicBlock::Create(*context, "loop", fn);
    llvm::BasicBlock* loop_exit = llvm::BasicBlock::Create(*context, "exit", fn);

    // Loop entry
    builder->SetInsertPoint(loop_entry);
    builder->CreateBr(loop_body);

    // Loop body
    builder->SetInsertPoint(loop_body);

    llvm::PHINode* loop_var = builder->CreatePHI(types->i32_type(), 2, "loop_var");
    llvm::PHINode* sum = builder->CreatePHI(types->i64_type(), 2, "sum");

    loop_var->addIncoming(types->i32_constant(0), loop_entry);
    sum->addIncoming(types->i64_constant(0), loop_entry);

    // setup loop PHI
    llvm::Value* loop_update =
        builder->CreateAdd(loop_var, types->i32_constant(1), "loop_var+1");
    loop_var->addIncoming(loop_update, loop_body);

    // get the current value
    llvm::Value* offset =
        builder->CreateGEP(types->i64_type(), arg_elements, loop_var, "offset");
    llvm::Value* current_value = builder->CreateLoad(types->i64_type(), offset, "value");

    // setup sum PHI
    llvm::Value* sum_update = builder->CreateAdd(sum, current_value, "sum+ith");
    sum->addIncoming(sum_update, loop_body);

    // check loop_var
    llvm::Value* loop_var_check =
        builder->CreateICmpSLT(loop_update, arg_nelements, "loop_var < nrec");
    builder->CreateCondBr(loop_var_check, loop_body, loop_exit);

    // Loop exit
    builder->SetInsertPoint(loop_exit);
    builder->CreateRet(sum_update);
    return func_name;
  }

  void BuildEngine() { ASSERT_OK(Engine::Make(TestConfiguration(), false, &engine)); }

  std::unique_ptr<Engine> engine;
  std::shared_ptr<Configuration> configuration = TestConfiguration();
};

TEST_F(TestEngine, TestAddUnoptimised) {
  configuration->set_optimize(false);
  BuildEngine();

  std::string fn_name = BuildVecAdd(engine.get());
  ASSERT_OK(engine->FinalizeModule());
  auto add_func = reinterpret_cast<add_vector_func_t>(engine->CompiledFunction(fn_name));

  int64_t my_array[] = {1, 3, -5, 8, 10};
  EXPECT_EQ(add_func(my_array, 5), 17);
}

TEST_F(TestEngine, TestAddOptimised) {
  configuration->set_optimize(true);
  BuildEngine();

  std::string fn_name = BuildVecAdd(engine.get());
  ASSERT_OK(engine->FinalizeModule());
  auto add_func = reinterpret_cast<add_vector_func_t>(engine->CompiledFunction(fn_name));

  int64_t my_array[] = {1, 3, -5, 8, 10};
  EXPECT_EQ(add_func(my_array, 5), 17);
}

}  // namespace gandiva
