// This should work on LLVM 7, 8, 9, however in CI the version of clang
// installed on Ubuntu 18.04 cannot load a clang plugin properly without
// segfaulting on exit. This is fine on Ubuntu 20.04 or later LLVM versions...
// RUN: %clang++ -fno-exceptions -std=c++11 -O1 %s -S -emit-llvm -o - %loadClangEnzyme -mllvm -enzyme-lapack-copy=1 | %lli -
// RUN: %clang++ -fno-exceptions -std=c++11 -O2 %s -S -emit-llvm -o - %loadClangEnzyme -mllvm -enzyme-lapack-copy=1  | %lli -
// RUN: %clang++ -fno-exceptions -std=c++11 -O3 %s -S -emit-llvm -o - %loadClangEnzyme  -mllvm -enzyme-lapack-copy=1 | %lli -
// RUN: %clang++ -fno-exceptions -std=c++11 -O1 %s -S -emit-llvm -o - %loadClangEnzyme -mllvm -enzyme-inline=1 -mllvm -enzyme-lapack-copy=1 -S | %lli -
// RUN: %clang++ -fno-exceptions -std=c++11 -O2 %s -S -emit-llvm -o - %loadClangEnzyme -mllvm -enzyme-inline=1 -mllvm -enzyme-lapack-copy=1 -S | %lli -
// RUN: %clang++ -fno-exceptions -std=c++11 -O3 %s -S -emit-llvm -o - %loadClangEnzyme -mllvm -enzyme-inline=1 -mllvm -enzyme-lapack-copy=1  -S | %lli -
// TODO: if [ %llvmver -ge 12 ]; then %clang++ -fno-exceptions -std=c++11 -O1 %s -S -emit-llvm -o - %newLoadClangEnzyme | %lli - ; fi
// TODO: if [ %llvmver -ge 12 ]; then %clang++ -fno-exceptions -std=c++11 -O2 %s -S -emit-llvm -o - %newLoadClangEnzyme | %lli - ; fi
// TODO: if [ %llvmver -ge 12 ]; then %clang++ -fno-exceptions -std=c++11 -O3 %s -S -emit-llvm -o - %newLoadClangEnzyme | %lli - ; fi
// TODO: if [ %llvmver -ge 12 ]; then %clang++ -fno-exceptions -std=c++11 -O1 %s -S -emit-llvm -o - %newLoadClangEnzyme -mllvm -enzyme-inline=1 -S | %lli - ; fi
// TODO: if [ %llvmver -ge 12 ]; then %clang++ -fno-exceptions -std=c++11 -O2 %s -S -emit-llvm -o - %newLoadClangEnzyme -mllvm -enzyme-inline=1 -S | %lli - ; fi
// TODO: if [ %llvmver -ge 12 ]; then %clang++ -fno-exceptions -std=c++11 -O3 %s -S -emit-llvm -o - %newLoadClangEnzyme -mllvm -enzyme-inline=1 -S | %lli - ; fi

#include "../blasinfra.h"

int enzyme_dup;
int enzyme_out;
int enzyme_const;
template <typename... T> void __enzyme_autodiff(void *, T...);

void my_dscal_v2(cublasHandle_t *handle, int N, double alpha,
                 double *__restrict__ X, int incx) {
  cublasDscal_v2(handle, N, &alpha, X, incx);
  inDerivative = true;
}

void my_dgemv(cublasHandle_t *handle, cublasOperation_t trans, int M, int N,
              double alpha, double *__restrict__ A, int lda,
              double *__restrict__ X, int incx, double beta,
              double *__restrict__ Y, int incy) {
  cublasDgemv(handle, trans, M, N, &alpha, A, lda, X, incx, &beta, Y, incy);
  inDerivative = true;
}

void ow_dgemv(cublasHandle_t *handle, cublasOperation_t trans, int M, int N,
              double alpha, double *A, int lda, double *X, int incx,
              double beta, double *Y, int incy) {
  cublasDgemv(handle, trans, M, N, &alpha, A, lda, X, incx, &beta, Y, incy);
  inDerivative = true;
}

double my_ddot(cublasHandle_t *handle, int N, double *__restrict__ X, int incx,
               double *__restrict__ Y, int incy) {
  double res = cublasDdot(handle, N, X, incx, Y, incy);
  inDerivative = true;
  return res;
}

double my_ddot2(cublasHandle_t *handle, int N, double *__restrict__ X, int incx,
                double *__restrict__ Y, int incy) {
  double res = 0.0;
  cublasDdot_v2(handle, N, X, incx, Y, incy, &res);
  inDerivative = true;
  return res;
}

void my_dgemm(cublasHandle_t *handle, cublasOperation_t transA,
              cublasOperation_t transB, int M, int N, int K, double alpha,
              double *__restrict__ A, int lda, double *__restrict__ B, int ldb,
              double beta, double *__restrict__ C, int ldc) {
  cublasDgemm(handle, transA, transB, M, N, K, &alpha, A, lda, B, ldb, &beta, C,
              ldc);
  inDerivative = true;
}

static void scal2Tests() {

  std::string Test = "SCAL2 active both ";
  cublasHandle_t *handle = DEFAULT_CUBLAS_HANDLE;
  BlasInfo inputs[6] = {
      /*A*/ BlasInfo(A, N, incA),
      BlasInfo(),
      BlasInfo(),
      BlasInfo(),
      BlasInfo(),
      BlasInfo(),
  };
  init();

  double alpha = 3.14;
  // cublasHandle_t handle;
  my_dscal_v2(handle, N, alpha, A, incA);

  // Check memory of primal on own.
  checkMemoryTrace(inputs, "Primal " + Test, calls);

  init();
  __enzyme_autodiff((void *)my_dscal_v2, enzyme_const, handle, enzyme_const, N,
                    enzyme_out, alpha, enzyme_dup, A, dA, enzyme_const, incA);
  foundCalls = calls;

  init();

  my_dscal_v2(handle, N, alpha, A, incA);

  inDerivative = true;

  double *dalpha = (double *)foundCalls[1].pout_arg1;
  inputs[3] = BlasInfo(dalpha, 1, 1);

  cublasDdot_v2(handle, N, A, incA, dA, incA, dalpha);
  cublasDscal_v2(handle, N, &alpha, dA, incA);

  checkTest(Test);

  // Check memory of primal of expected derivative
  checkMemoryTrace(inputs, "Expected " + Test, calls);

  // Check memory of primal of our derivative (if equal above, it
  // should be the same).
  checkMemoryTrace(inputs, "Found " + Test, foundCalls);
}

static void dotTests() {

  std::string Test = "DOT active both ";
  cublasHandle_t *handle = DEFAULT_CUBLAS_HANDLE;
  BlasInfo inputs[6] = {
      /*A*/ BlasInfo(A, N, incA),
      /*B*/ BlasInfo(B, N, incB),
      /*C*/ BlasInfo(C, M, incC),
      BlasInfo(),
      BlasInfo(),
      BlasInfo(),
  };
  init();
  // cublasHandle_t handle;
  my_ddot(handle, N, A, incA, B, incB);

  // Check memory of primal on own.
  checkMemoryTrace(inputs, "Primal " + Test, calls);

  init();
  __enzyme_autodiff((void *)my_ddot, enzyme_const, handle, enzyme_const, N,
                    enzyme_dup, A, dA, enzyme_const, incA, enzyme_dup, B, dB,
                    enzyme_const, incB);
  foundCalls = calls;

  init();

  my_ddot(handle, N, A, incA, B, incB);

  inDerivative = true;

  cublasDaxpy(handle, N, 1.0, B, incB, dA, incA);
  cublasDaxpy(handle, N, 1.0, A, incA, dB, incB);

  checkTest(Test);

  // Check memory of primal of expected derivative
  checkMemoryTrace(inputs, "Expected " + Test, calls);

  // Check memory of primal of our derivative (if equal above, it
  // should be the same).
  checkMemoryTrace(inputs, "Found " + Test, foundCalls);
}

static void dot2Tests() {

  std::string Test = "DOTv2 active both ";
  cublasHandle_t *handle = DEFAULT_CUBLAS_HANDLE;
  BlasInfo inputs[6] = {
      /*A*/ BlasInfo(A, N, incA),
      /*B*/ BlasInfo(B, N, incB),
      /*C*/ BlasInfo(C, M, incC), BlasInfo(), BlasInfo(), BlasInfo(),
  };
  init();
  // cublasHandle_t handle;
  my_ddot2(handle, N, A, incA, B, incB);
  {
    auto primal_stack_ret = (double *)calls[0].pout_arg1;
    inputs[3] = BlasInfo(primal_stack_ret, 1, 1);
  }

  // Check memory of primal on own.
  checkMemoryTrace(inputs, "Primal " + Test, calls);

  init();
  __enzyme_autodiff((void *)my_ddot2, enzyme_const, handle, enzyme_const, N,
                    enzyme_dup, A, dA, enzyme_const, incA, enzyme_dup, B, dB,
                    enzyme_const, incB);
  {
    auto primal_stack_ret = (double *)calls[0].pout_arg1;
    inputs[3] = BlasInfo(primal_stack_ret, 1, 1);
  }
  foundCalls = calls;

  auto stack_ret = (double*)foundCalls[1].pin_arg2;
  inputs[4] = BlasInfo(stack_ret, 1, 1);

  init();

  my_ddot2(handle, N, A, incA, B, incB);

  calls[0].pout_arg1 = (double*)foundCalls[0].pout_arg1;

  inDerivative = true;

  cublasDaxpy_v2(handle, N, stack_ret, B, incB, dA, incA);
  cublasDaxpy_v2(handle, N, stack_ret, A, incA, dB, incB);
  cudaMemset(stack_ret, 0, sizeof(double));

  checkTest(Test);

  // Check memory of primal of expected derivative
  checkMemoryTrace(inputs, "Expected " + Test, calls);

  // Check memory of primal of our derivative (if equal above, it
  // should be the same).
  checkMemoryTrace(inputs, "Found " + Test, foundCalls);
}

static void gemvTests() {
  // N means normal matrix, T means transposed
  for (cublasOperation_t transA :
       {cublasOperation_t::CUBLAS_OP_N, cublasOperation_t::CUBLAS_OP_T}) {

    {

      bool trans = !is_normal(transA);
      auto handle = DEFAULT_CUBLAS_HANDLE;
      std::string Test = "GEMV active A, C ";
      BlasInfo inputs[6] = {/*A*/ BlasInfo(A, CUBLAS_LAYOUT, M, N, lda),
                            /*B*/ BlasInfo(B, trans ? M : N, incB),
                            /*C*/ BlasInfo(C, trans ? N : M, incC),
                            BlasInfo(),
                            BlasInfo(),
                            BlasInfo()};
      init();
      my_dgemv(handle, transA, M, N, alpha, A, lda, B, incB, beta, C, incC);

      assert(calls.size() == 1);
      assert(calls[0].inDerivative == false);
      assert(calls[0].type == CallType::GEMV);
      assert(calls[0].pout_arg1 == C);
      assert(calls[0].pin_arg1 == A);
      assert(calls[0].pin_arg2 == B);
      assert(calls[0].farg1 == alpha);
      assert(calls[0].farg2 == beta);
      assert(calls[0].handle == DEFAULT_CUBLAS_HANDLE);
      assert(calls[0].targ1 == (char)transA);
      assert(calls[0].targ2 == (char)UNUSED_TRANS);
      assert(calls[0].iarg1 == M);
      assert(calls[0].iarg2 == N);
      assert(calls[0].iarg3 == UNUSED_INT);
      assert(calls[0].iarg4 == lda);
      assert(calls[0].iarg5 == incB);
      assert(calls[0].iarg6 == incC);

      // Check memory of primal on own.
      checkMemoryTrace(inputs, "Primal " + Test, calls);

      init();
      __enzyme_autodiff((void *)my_dgemv, enzyme_const, handle, enzyme_const,
                        transA, enzyme_const, M, enzyme_const, N, enzyme_const,
                        alpha, enzyme_dup, A, dA, enzyme_const, lda,
                        enzyme_const, B, enzyme_const, incB, enzyme_const, beta,
                        enzyme_dup, C, dC, enzyme_const, incC);
      foundCalls = calls;
      init();

      my_dgemv(handle, transA, M, N, alpha, A, lda, B, incB, beta, C, incC);

      inDerivative = true;
      // dC = alpha * X * transpose(Y) + A
      cublasDger(handle, M, N, &alpha, trans ? B : dC, trans ? incB : incC,
                 trans ? dC : B, trans ? incC : incB, dA, lda);
      // dY = beta * dY
      cublasDscal(handle, trans ? N : M, &beta, dC, incC);

      checkTest(Test);

      // Check memory of primal of expected derivative
      checkMemoryTrace(inputs, "Expected " + Test, calls);

      // Check memory of primal of our derivative (if equal above, it
      // should be the same).
      checkMemoryTrace(inputs, "Found " + Test, foundCalls);

      Test = "GEMV active A, B, C ";

      init();
      __enzyme_autodiff((void *)my_dgemv, enzyme_const, handle, enzyme_const,
                        transA, enzyme_const, M, enzyme_const, N, enzyme_const,
                        alpha, enzyme_dup, A, dA, enzyme_const, lda, enzyme_dup,
                        B, dB, enzyme_const, incB, enzyme_const, beta,
                        enzyme_dup, C, dC, enzyme_const, incC);
      foundCalls = calls;
      init();

      my_dgemv(handle, transA, M, N, alpha, A, lda, B, incB, beta, C, incC);

      inDerivative = true;
      // dC = alpha * X * transpose(Y) + A
      cublasDger(handle, M, N, &alpha, trans ? B : dC, trans ? incB : incC,
                 trans ? dC : B, trans ? incC : incB, dA, lda);

      // dB = alpha * trans(A) * dC + dB
      double c1 = 1.0;
      cublasDgemv(handle, transpose(transA), M, N, &alpha, A, lda, dC, incC,
                  &c1, dB, incB);

      // dY = beta * dY
      cublasDscal(handle, trans ? N : M, &beta, dC, incC);

      checkTest(Test);

      // Check memory of primal of expected derivative
      checkMemoryTrace(inputs, "Expected " + Test, calls);

      // Check memory of primal of our derivative (if equal above, it
      // should be the same).
      checkMemoryTrace(inputs, "Found " + Test, foundCalls);

      // Next test fails to compile because the matrixcopy will use lacpy
      // which isn't supported by cublas
      //Test = "GEMV A,B,C active/overwrite";

      //init();
      //__enzyme_autodiff((void *)ow_dgemv, enzyme_const, handle, enzyme_const,
      //                  transA, enzyme_const, M, enzyme_const, N, enzyme_const,
      //                  alpha, enzyme_dup, A, dA, enzyme_const, lda, enzyme_dup,
      //                  B, dB, enzyme_const, incB, enzyme_const, beta,
      //                  enzyme_dup, C, dC, enzyme_const, incC);
      //foundCalls = calls;
      //init();

      //assert(foundCalls.size() > 2);
      //auto A_cache = (double *)foundCalls[0].pout_arg1;
      //// dlacpy is not supported for cublas @wsmoses
      //cublas_dlacpy(handle, '\0', M, N, A, lda, A_cache, M);
      //inputs[4] = BlasInfo(A_cache, handle, M, N, M);
      //auto B_cache = (double *)foundCalls[1].pout_arg1;
      //cublas_dcopy(handle, trans ? M : N, B, incB, B_cache, 1);
      //inputs[5] = BlasInfo(B_cache, handle, trans ? M : N, 1);

      //ow_dgemv(handle, transA, M, N, alpha, A, lda, B, incB, beta, C, incC);

      //inDerivative = true;
      //// dC = alpha * X * transpose(Y) + A
      //cublas_dger(handle, M, N, alpha, trans ? B_cache : dC, trans ? 1 : incC,
      //            trans ? dC : B_cache, trans ? incC : 1, dA, lda);

      //// dB = alpha * trans(A) * dC + dB
      //cublas_dgemv(handle, transpose(transA), M, N, alpha, A_cache, M, dC, incC,
      //             1.0, dB, incB);

      //// dY = beta * dY
      //cublas_dscal(handle, trans ? N : M, beta, dC, incC);

      //checkTest(Test);

      //// Check memory of primal of expected derivative
      //checkMemoryTrace(inputs, "Expected " + Test, calls);

      //// Check memory of primal of our derivative (if equal above, it
      //// should be the same).
      //checkMemoryTrace(inputs, "Found " + Test, foundCalls);

      //inputs[4] = BlasInfo();
      //inputs[5] = BlasInfo();
    }
  }
}

static void gemmTests() {
  // N means normal matrix, T means transposed
  auto handle = DEFAULT_CUBLAS_HANDLE;
  for (auto transA :
       {cublasOperation_t::CUBLAS_OP_N, cublasOperation_t::CUBLAS_OP_T}) {
    for (auto transB :
         {cublasOperation_t::CUBLAS_OP_N, cublasOperation_t::CUBLAS_OP_T}) {

      {

        bool transA_bool = !is_normal(transA);
        bool transB_bool = !is_normal(transB);
        std::string Test = "GEMM";
        BlasInfo inputs[6] = {
            /*A*/ BlasInfo(A, CUBLAS_LAYOUT, transA_bool ? K : M, transA_bool ? M : K,
                           lda),
            /*B*/
            BlasInfo(B, CUBLAS_LAYOUT, transB_bool ? N : K, transB_bool ? K : N, incB),
            /*C*/ BlasInfo(C, CUBLAS_LAYOUT, M, N, incC),
            BlasInfo(),
            BlasInfo(),
            BlasInfo()};
        init();
        my_dgemm(handle, transA, transB, M, N, K, alpha, A, lda, B, incB, beta,
                 C, incC);

        assert(calls.size() == 1);
        assert(calls[0].inDerivative == false);
        assert(calls[0].type == CallType::GEMM);
        assert(calls[0].pout_arg1 == C);
        assert(calls[0].pin_arg1 == A);
        assert(calls[0].pin_arg2 == B);
        assert(calls[0].farg1 == alpha);
        assert(calls[0].farg2 == beta);
        assert(calls[0].handle == DEFAULT_CUBLAS_HANDLE);
        assert(calls[0].targ1 == (char)transA);
        assert(calls[0].targ2 == (char)transB);
        assert(calls[0].iarg1 == M);
        assert(calls[0].iarg2 == N);
        assert(calls[0].iarg3 == K);
        assert(calls[0].iarg4 == lda);
        assert(calls[0].iarg5 == incB);
        assert(calls[0].iarg6 == incC);

        // Check memory of primal on own.
        checkMemoryTrace(inputs, "Primal " + Test, calls);

        init();
        __enzyme_autodiff((void *)my_dgemm, enzyme_const, handle, enzyme_const,
                          transA, enzyme_const, transB, enzyme_const, M,
                          enzyme_const, N, enzyme_const, K, enzyme_const, alpha,
                          enzyme_dup, A, dA, enzyme_const, lda, enzyme_dup, B,
                          dB, enzyme_const, incB, enzyme_const, beta,
                          enzyme_dup, C, dC, enzyme_const, incC);
        foundCalls = calls;
        init();

        my_dgemm(handle, transA, transB, M, N, K, alpha, A, lda, B, incB, beta,
                 C, incC);

        inDerivative = true;

        // dA =
        my_dgemm(handle, transA_bool ? transB : cublasOperation_t::CUBLAS_OP_N,
                 transA_bool ? cublasOperation_t::CUBLAS_OP_T
                             : transpose(transB),
                 transA_bool ? K : M, transA_bool ? M : K, N, alpha,
                 transA_bool ? B : dC, transA_bool ? incB : incC,
                 transA_bool ? dC : B, transA_bool ? incC : incB, 1.0, dA, lda);

        // dB =
        my_dgemm(
            handle,
            transB_bool ? cublasOperation_t::CUBLAS_OP_T : transpose(transA),
            transB_bool ? transA : cublasOperation_t::CUBLAS_OP_N, // transB,
            transB_bool ? N : K, transB_bool ? K : N, M, alpha,
            transB_bool ? dC : A, transB_bool ? incC : lda,
            transB_bool ? A : dC, transB_bool ? lda : incC, 1.0, dB, incB);

        // TODO we are currently faking support here, this needs to be actually implemented
        double c10 = 1.0;
        cublasDlascl(handle, (cublasOperation_t)2, 0, 0, &c10, &beta, M, N,
                     dC, incC, 0);

        checkTest(Test);

        // Check memory of primal of expected derivative
        checkMemoryTrace(inputs, "Expected " + Test, calls);

        // Check memory of primal of our derivative (if equal above, it
        // should be the same).
        checkMemoryTrace(inputs, "Found " + Test, foundCalls);
      }
    }
  }
}

int main() {
  gemmTests();

  gemvTests();

  dotTests();

  dot2Tests();

  scal2Tests();
}
