! Copyright © 2019-2023 Dynare Team
!
! This file is part of Dynare.
!
! Dynare is free software: you can redistribute it and/or modify
! it under the terms of the GNU General Public License as published by
! the Free Software Foundation, either version 3 of the License, or
! (at your option) any later version.
!
! Dynare is distributed in the hope that it will be useful,
! but WITHOUT ANY WARRANTY; without even the implied warranty of
! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
! GNU General Public License for more details.
!
! You should have received a copy of the GNU General Public License
! along with Dynare.  If not, see <https://www.gnu.org/licenses/>.

module blas
  use iso_fortran_env
  implicit none (type, external)

#if defined(MATLAB_MEX_FILE) && __SIZEOF_POINTER__ == 8
  integer, parameter :: blint = int64
  integer, parameter :: bllog = 8 ! Logical kind, gfortran-specific
#else
  integer, parameter :: blint = int32
  integer, parameter :: bllog = 4 ! Logical kind, gfortran-specific
#endif

  interface
     subroutine dgemm(transa, transb, m, n, k, alpha, a, lda, b, ldb, beta, c, ldc)
       import :: blint, real64
       implicit none
       character, intent(in) :: transa, transb
       integer(blint), intent(in) :: m, n, k, lda, ldb, ldc
       real(real64), dimension(*), intent(in) :: a, b
       real(real64), intent(in) :: alpha, beta
       real(real64), dimension(*), intent(inout) :: c
     end subroutine dgemm

     subroutine dgemv(trans, m, n, alpha, a, lda, x, incx, beta, y, incy)
       import :: blint, real64
       implicit none
       character, intent(in) :: trans
       integer(blint), intent(in) :: m, n, lda, incx, incy
       real(real64), dimension(*), intent(in) :: a, x
       real(real64), intent(in) :: alpha, beta
       real(real64), dimension(*), intent(inout) :: y
     end subroutine dgemv
  end interface

contains

  ! Updating a matrix using blas DGEMM
  ! C <- alpha*op(A)*op(B) + beta*C
  subroutine matmul_add(opA, opB, alpha, A, B, beta, C)
     character, intent(in) :: opA, opB
     real(real64), dimension(:,:), intent(in) :: A, B      
     real(real64), dimension(:,:), intent(inout) :: C 
     real(real64), intent(in) :: alpha, beta
     integer(blint) :: m, n, k, lda, ldb
     if (opA == "N") then
        m = int(size(A,1), blint)
        k = int(size(A,2), blint)
        lda = m
     else
        m = int(size(A,2), blint)
        k = int(size(A,1), blint)
        lda = k
     end if 
     if (opB == "N") then
        n = int(size(B,2), blint)        
        ldb = k
     else
        n = int(size(B,1), blint)
        ldb = n
     end if
#ifdef DEBUG
     if ( (opA /= "N") .and. (opA /= "T") .and. (opA /= "C") ) then
        print *, "opA must be either N, T or C"
     end if 
     if ( (opB /= "N") .and. (opB /= "T") .and. (opB /= "C") ) then
        print *, "opB must be either N, T or C"
     end if 
     if (((opA == "N") .and. (opB == "N") .and. (size(A,2) /= size(B,1))) .or.&
        &((opA == "N") .and. (opB /= "N") .and. (size(A,2) /= size(B,2))) .or.&
        &((opA /= "N") .and. (opB == "N") .and. (size(A,1) /= size(B,1))) .or.&
        &((opA /= "N") .and. (opB /= "N") .and. (size(A,1) /= size(B,2))))    &
        then
        print *, "Inconsistent number of columns of op(A) and number of rows &
                 &of op(B)"
     end if
     if (m /= size(C,1)) then
        print *, "Inconsistent number of rows of op(A) and number of rows &
                 &of C"
     end if
#endif
     call dgemm(opA, opB, m, n, k, alpha, A, lda, B, &
               ldb, beta, C, m)
  end subroutine

end module blas

module lapack
  use blas
  implicit none (type, external)

  interface
     subroutine dgelsd(m, n, nrhs, a, lda, b, ldb, s, rcond, rank, work, lwork, iwork, info)
       import :: blint, real64
       implicit none
       integer(blint), intent(in) :: m, n, nrhs, lda, ldb, lwork
       real(real64), dimension(*), intent(inout) :: a, b
       real(real64), dimension(*), intent(out) :: s, work
       real(real64), intent(in) :: rcond
       integer(blint), dimension(*), intent(out) :: iwork
       integer(blint), intent(out) :: rank, info
     end subroutine dgelsd

     subroutine dgesv(n, nrhs, a, lda, ipiv, b, ldb, info)
       import :: blint, real64
       implicit none
       integer(blint), intent(in) :: n, nrhs, lda, ldb
       real(real64), dimension(*), intent(inout) :: a, b
       integer(blint), dimension(*), intent(out) :: ipiv
       integer(blint), intent(out) :: info
     end subroutine dgesv

     subroutine dgges(jobvsl, jobvsr, sort, selctg, n, a, lda, b, ldb, sdim, &
          alphar, alphai, beta, vsl, ldvsl, vsr, ldvsr, work, lwork, bwork, &
          info)
       import :: blint, bllog, real64
       implicit none
       character, intent(in) :: jobvsl, jobvsr, sort
       interface
          logical(bllog) function selctg(alphar, alphai, beta)
            import :: bllog, real64
            real(real64), intent(in) :: alphar, alphai, beta
          end function selctg
       end interface
       integer(blint), intent(in) :: n, lda, ldb, ldvsl, ldvsr, lwork
       real(real64), dimension(*), intent(inout) :: a, b
       real(real64), dimension(*), intent(out) :: alphar, alphai, beta, vsl, vsr, work
       logical(bllog), dimension(*), intent(out) :: bwork
       integer(blint), intent(out) :: sdim, info
     end subroutine dgges

     subroutine dgecon(norm, n, a, lda, anorm, rcond, work, iwork, info)
       import :: blint, real64
       implicit none
       character, intent(in) :: norm
       integer(blint), intent(in) :: n, lda
       real(real64), dimension(*), intent(in) :: a
       real(real64), intent(in) :: anorm
       real(real64), intent(out) :: rcond
       real(real64), dimension(*), intent(out) :: work
       integer(blint), dimension(*), intent(out) :: iwork
       integer(blint), intent(out) :: info
     end subroutine dgecon

     subroutine dgetrf(m, n, a, lda, ipiv, info)
       import :: blint, real64
       implicit none
       integer(blint), intent(in) :: m, n, lda
       real(real64), dimension(*), intent(inout) :: a
       integer(blint), dimension(*), intent(out) :: ipiv
       integer(blint), intent(out) :: info
     end subroutine dgetrf

     subroutine dgetri(n, a, lda, ipiv, work, lwork, info)
       import :: blint, real64
       implicit none
       integer(blint), intent(in) :: n, lda, lwork
       real(real64), dimension(*), intent(inout) :: a
       integer(blint), dimension(*), intent(in):: ipiv
       real(real64), dimension(*), intent(out) :: work
       integer(blint), intent(out) :: info
     end subroutine dgetri

     subroutine dpotrf(uplo, n, a, lda, info)
       import :: blint, real64
       implicit none
       character, intent(in) :: uplo
       integer(blint), intent(in) :: n, lda
       real(real64), dimension(*), intent(inout) :: a
       integer(blint), intent(out) :: info
     end subroutine dpotrf

     function dlange(norm, m, n, a, lda, work)
       import :: blint, real64
       implicit none
       character, intent(in) :: norm
       integer(blint), intent(in) :: m, n, lda
       real(real64), dimension(*), intent(in) :: a
       real(real64), dimension(*), intent(out) :: work
       real(real64) :: dlange
     end function dlange

     function ilaenv(ispec, name, opts, n1, n2, n3, n4)
       import :: blint  
       implicit none
       integer(blint) :: ispec, n1, n2, n3, n4
       character(len=*), intent(in) :: name, opts
       integer(blint) :: ilaenv
     end function ilaenv
  end interface

contains

  ! Computing inv(A)*B using lapack DGESV
  ! B <- inv(A)*B
  subroutine left_divide(A, B, ipiv, info)
     real(real64), dimension(:,:), intent(inout) :: A
     real(real64), dimension(:,:), intent(inout) :: B
     integer(blint), dimension(:), intent(inout) :: ipiv
     integer(blint), intent(inout) :: info
     integer(blint) :: m, n
#ifdef DEBUG
     if (size(ipiv) /= size(A,1)) then 
        print *, "Inconsistent number of rows of A and size of ipiv"
     end if
     if (size(A,2) /= size(B,1)) then
        print *, "Inconsistent number of columns of A and number of rows of B"
     end if
#endif
     m = int(size(A,1), blint)
     n = int(size(B,2), blint)
     call dgesv(m, n, A, m, ipiv, B, m, info)
  end subroutine left_divide

  ! Matlab-like norm function calling lapack DLANGE routine
  real(real64) function norm(A, c)
     real(real64), dimension(:,:), intent(in) :: A
     character, intent(in) :: c
     real(real64), dimension(:), allocatable :: work
     integer(blint) :: nrow
     nrow = int(size(A,1),blint)
     norm = dlange(c, nrow, int(size(A,2), blint), &
                  &A, nrow, work)
  end function norm

end module lapack
