! -*- mode: f90; coding: utf-8 -*-
! ----------------------------------------------------------------------------
! 定数・関数ユーティリティ/Constant and Function Utilities
! ----------------------------------------------------------------------------
!> @author Shin-ichi Takehiro, Youhei SASAKI
!> @copyright Copyright (C) SPMODEL Development Group, 2026
!>            License is MIT/X11. see [COPYRIGHT](@ref COPYRIGHT) in detail
!> @ja
!> @brief 定数・関数ユーティリティ
!>
!> @details
!> 複数のモジュールで個別に設定していた定数, 関数をまとめた.
!> また, 単精度/倍精度両対応にした固有値計算と並び替え関数も提供している.
!> @endja
!> @en
!> @brief Constant and Function Utilities
!>
!> @details
!> Constants and functions previously configured individually across
!> multiple modules have been consolidated. Furthermore, eigenvalue
!> calculations and sorting functions supporting both single-precision
!> and double-precision are now provided.
!> @enden
module spml_utils
  use dc_message, only : MessageNotify
  use dc_types, only: DP, DP_EPS, SP, SP_EPS
  implicit none
  private
  public pi
  public gammaln
  public factrl

#ifdef EIG_LAPACK
  public eigen
  public ceigen

  ! --------------------------------------------------------------------------
  !> LAPACK interfaces
  !> (explicit interface for Fortran 2003 conformance)
  interface
    subroutine DGEEV(jobvl, jobvr, n, a, lda, wr, wi, &
      & vl, ldvl, vr, ldvr, work, lwork, info)
      use dc_types, only: DP
      implicit none
      character(len=1), intent(in) :: jobvl, jobvr
      integer, intent(in) :: n, lda, ldvl, ldvr, lwork
      real(DP), intent(inout) :: a(lda,*)
      real(DP), intent(out)   :: wr(*), wi(*)
      real(DP), intent(out)   :: vl(ldvl,*), vr(ldvr,*)
      real(DP), intent(inout) :: work(*)
      integer, intent(out) :: info
    end subroutine DGEEV

    subroutine SGEEV(jobvl, jobvr, n, a, lda, wr, wi, &
      vl, ldvl, vr, ldvr, work, lwork, info)
      use dc_types, only: SP
      implicit none
      character(len=1), intent(in) :: jobvl, jobvr
      integer, intent(in) :: n, lda, ldvl, ldvr, lwork
      real(SP), intent(inout) :: a(lda,*)
      real(SP), intent(out)   :: wr(*), wi(*)
      real(SP), intent(out)   :: vl(ldvl,*), vr(ldvr,*)
      real(SP), intent(inout) :: work(*)
      integer, intent(out) :: info
    end subroutine SGEEV

    subroutine ZGEEV(jobvl, jobvr, n, a, lda, w, &
      & vl, ldvl, vr, ldvr, work, lwork, rwork, info)
      use dc_types, only: DP
      implicit none
      character(len=1), intent(in) :: jobvl, jobvr
      integer, intent(in) :: n, lda, ldvl, ldvr, lwork
      complex(DP), intent(inout) :: a(lda,*)
      complex(DP), intent(out)   :: w(*)
      complex(DP), intent(out)   :: vl(ldvl,*), vr(ldvr,*)
      complex(DP), intent(inout) :: work(*)
      real(DP), intent(inout)    :: rwork(*)
      integer, intent(out) :: info
    end subroutine ZGEEV
  end interface

  interface eigen
    module procedure deigen
    module procedure seigen
  end interface eigen

  interface indexx
    module procedure indexx_d
    module procedure indexx_s
  end interface indexx

  interface merge_sort
    module procedure merge_sort_d
    module procedure merge_sort_s
  end interface merge_sort
#endif
  ! --------------------------------------------------------------------------
  !> PI: 3.14159265358979323846264338327950288
  real(DP), parameter :: pi = 3.141592653589793238d0

  !> -------------------------------------------------------------------------
  !> external file: dlgamma.f90
  interface
    real(DP) function dlgamma(x)
      use dc_types, only: DP
      real(DP), intent(in) :: x
    end function dlgamma
  end interface

contains
  ! --------------------------------------------------------------------------
  !> Returns the vaule \f$ ln[Γ(x)] for x >= 0 \f$
  function gammaln(xx)
    real(DP), intent(in) :: xx
    real(DP)             :: gammaln

    gammaln = dlgamma(xx)

  end function gammaln

  ! --------------------------------------------------------------------------
  !> Returns the value \f$n!\f$ as a floating point number
  function factrl(n)
    integer, intent(in)  :: n
    real(DP)             :: factrl

    factrl = exp(gammaln(n+1.0d0))

  end function factrl

#ifdef EIG_LAPACK
  !---------------------------------------------------------------------
  !> @ja
  !> 実行列の固有値計算: DGEEV/LAPACK の wrapper
  !> * 行列 AMAT の i 番目固有値を eigen_r(i), eigen_i(i) に格納
  !> * 対応する固有ベクトルを eigvec_r(:,i), eigvec_i(:,i) に格納
  !> * 格納する固有値の数は引数 eigen_r の大きさで決まる
  !> * 固有値の順番は sort と order で定められる.
  !> * sort によって順番を定めるために用いる量を指定する.
  !> 実部(R), 実部の絶対値(RA), 虚部(I), 虚部の絶対値(IA)
  !> * reverse によって小さい順(.false.), 大きい順(.true.)を指定できる.
  !> * デフォルトは sort='R', reverse=.false.
  !> @endja
  !> @en
  !> Eigenvalue calculation for matrix AMAT: DGEEV/LAPACK wrapper @enden
  !> * Stores the i-th eigenvalue of matrix AMAT in eigen_r(i) and eigen_i(i)
  !> * Stores the corresponding eigenvector in eigvec_r(:,i) and eigvec_i(:,i)
  !> * The number of eigenvalues stored is determined by the size of the
  !>   eigen_r argument
  !> * The order of eigenvalues is determined by sort and order.
  !> * sort specifies the quantity used to determine the order.
  !>   Real part (R), absolute real part (RA), imaginary part (I), absolute
  !>   imaginary part (IA)
  !> * reverse specifies ascending (.false.) or descending (.true.) order.
  !> * Default is sort=‘R’, reverse=.false.
  !> @enden
  subroutine deigen(                            &
    & amat,eigen_r, eigen_i, eigvec_r,eigvec_i, &
    & info, sort, reverse )

    ! 入力正方行列
    real(DP), dimension(:,:)                  :: amat
    ! 固有値実数部
    real(DP), intent(out), dimension(:)       :: eigen_r
    ! 固有値虚数部
    real(DP), intent(out), dimension(:)       :: eigen_i
    ! 固有ベクトル実部
    real(DP), intent(out), &
      & dimension(size(amat,1),size(eigen_r)) :: eigvec_r
    ! 固有ベクトル虚部
    real(DP), intent(out), &
      & dimension(size(amat,1),size(eigen_i)) :: eigvec_i
    ! ステータス
    integer, intent(out)                      :: info
    ! 並び変えの量
    character(len=2), intent(in), optional    :: sort
    ! 並び変えスイッチ
    logical, intent(in), optional             :: reverse

    integer :: nm, i, j
    integer :: lwork, nout
    integer :: lo, hi, tmp
    real(DP), allocatable, dimension(:)       :: wr
    real(DP), allocatable, dimension(:)       :: wi
    real(DP), allocatable, dimension(:)       :: work
    real(DP), allocatable, dimension(:,:)     :: vr
    integer,  allocatable, dimension(:)       :: index
    real(DP), dimension(1,1)                  :: vl
    character(len=1), parameter               :: jobvl='N'
    character(len=1), parameter               :: jobvr='V'

    !------- 形状チェック ------
    nm = size(amat,1)
    if ( nm /= size(amat,2))then
      call MessageNotify('E','DEIGEN_LAPACK','Input matrix not square')
      return
    end if

    allocate(wr(nm), wi(nm), vr(nm,nm), index(nm))

    !------- DGEEV/LAPACK による計算 ----------------------------------------
    ! workspace size query
    allocate(work(1))
    lwork = -1
    call DGEEV( jobvl, jobvr, nm, amat, nm, wr, wi, &
      & vl, nm, vr, nm, work, lwork, info )
    lwork = max(1, int(work(1)))
    deallocate(work)
    ! main
    allocate(work(lwork))
    call DGEEV( jobvl, jobvr, nm, amat, nm, wr, wi, &
      & vl, nm, vr, nm, work, lwork, info )

    !------- サブルーチンエラー処理 -------
    if ( info /= 0 ) then
      call MessageNotify('E','DEIGEN_LAPACK',&
        'Error in calculating eigenvalues/vectors...',i=(/info/) )
      deallocate(wr, wi, vr, work, index)
      return
    end if

    !------- 固有ベクトル入れ換え -------
    if ( present(sort) ) then
      select case (trim(sort))
      case('RA')
        index = indexx(abs(wr))
      case('I ')
        index = indexx(wi)
      case('IA')
        index = indexx(abs(wi))
      case default
        index = indexx(wr)
      end select
    else
      index = indexx(wr)
    end if

    if ( present(reverse) )then
      if ( reverse ) then               ! 大きい順詰め変え
        ! index = index(size(index):1:-1)
        ! 陽にループ展開
        lo = 1; hi = nm
        do while (lo < hi)
          tmp = index(lo)
          index(lo) = index(hi)
          index(hi) = tmp
          lo = lo + 1
          hi = hi - 1
        end do
      end if
    end if

    i = 1
    nout = min(size(eigen_r), size(eigen_i), nm)
    do while (i <= nout)
      j = index(i)
      eigen_r(i) = wr(j)
      eigen_i(i) = wi(j)

      if ( abs(wi(j)) <= DP_EPS ) then
        eigvec_r(:,i) = vr(:,j)
        eigvec_i(:,i) = 0.0_DP
        i = i +1
      elseif ( wi(j) > 0.0_DP) then
        if (i + 1 > nout) exit
        if (j >= nm) exit
        eigvec_r(:,i)   = vr(:,j)
        eigvec_i(:,i)   = vr(:,j+1)
        eigvec_r(:,i+1) = vr(:,j)
        eigvec_i(:,i+1) = -vr(:,j+1)
        eigen_r(i+1)    = wr(j+1)
        eigen_i(i+1)    = wi(j+1)
        i = i + 2
      else
        i = i + 1
      end if
    end do

  end subroutine deigen

  !---------------------------------------------------------------------
  !> @ja
  !> 実行列の固有値計算: DGEEV/LAPACK の wrapper (eigen の単精度版)
  !> * 行列 AMAT の i 番目固有値を eigen_r(i), eigen_i(i) に格納
  !> * 対応する固有ベクトルを eigvec_r(:,i), eigvec_i(:,i) に格納
  !> * 格納する固有値の数は引数 eigen_r の大きさで決まる
  !> * 固有値の順番は sort と order で定められる.
  !> * sort によって順番を定めるために用いる量を指定する.
  !>   実部(R), 実部の絶対値(RA), 虚部(I), 虚部の絶対値(IA)
  !> * reverse によって小さい順(.false.), 大きい順(.true.)を指定できる.
  !> * デフォルトは sort='R', reverse=.false.
  !> @endja
  !> @en
  !> Eigenvalue calculation for matrix AMAT: DGEEV/LAPACK wrapper (single-precision)
  !> * Stores the i-th eigenvalue of matrix AMAT in eigen_r(i) and eigen_i(i)
  !> * Stores the corresponding eigenvector in eigvec_r(:,i) and eigvec_i(:,i)
  !> * The number of eigenvalues stored is determined by the size of the
  !>   eigen_r argument
  !> * The order of eigenvalues is determined by sort and order.
  !> * sort specifies the quantity used to determine the order.
  !>   Real part (R), absolute real part (RA), imaginary part (I), absolute
  !>   imaginary part (IA)
  !> * reverse specifies ascending (.false.) or descending (.true.) order.
  !> * Default is sort=‘R’, reverse=.false.
  !> @enden
  !>
  subroutine seigen(                            &
    & amat,eigen_r, eigen_i, eigvec_r,eigvec_i, &
    & info, sort, reverse )

    !> 入力正方行列
    real(SP), dimension(:,:)                  :: amat
    !> 固有値実数部
    real(SP), intent(out), dimension(:)       :: eigen_r
    !> 固有値虚数部
    real(SP), intent(out), dimension(:)       :: eigen_i
    !> 固有ベクトル実部
    real(SP), intent(out), &
      & dimension(size(amat,1),size(eigen_r)) :: eigvec_r
    !> 固有ベクトル虚部
    real(SP), intent(out), &
      & dimension(size(amat,1),size(eigen_i)) :: eigvec_i
    !> ステータス
    integer, intent(out)                      :: info
    !> 並び変えの量
    character(len=2), intent(in), optional    :: sort
    !> 並び変えスイッチ
    logical, intent(in), optional             :: reverse

    integer :: nm, i, j
    integer :: lwork, nout
    integer :: lo, hi, tmp
    real(SP), allocatable, dimension(:)       :: wr
    real(SP), allocatable, dimension(:)       :: wi
    real(SP), allocatable, dimension(:)       :: work
    real(SP), allocatable, dimension(:,:)     :: vr
    integer,  allocatable, dimension(:)       :: index
    real(SP), dimension(1,1)                  :: vl
    character(len=1), parameter               :: jobvl='N'
    character(len=1), parameter               :: jobvr='V'

    !------- 形状チェック ------
    nm = size(amat,1)
    if ( nm /= size(amat,2))then
      call MessageNotify('E','DEIGEN_LAPACK','Input matrix not square')
      return
    end if

    allocate(wr(nm), wi(nm), vr(nm,nm), index(nm))

    !------- SGEEV/LAPACK による計算 ------
    ! workspace quer
    allocate(work(1))
    lwork = -1
    call SGEEV( jobvl, jobvr, nm, amat, nm, wr, wi, &
      & vl, nm, vr, nm, work, lwork, info )
    lwork = max(1, int(work(1)))
    deallocate(work)
    ! 本計算
    allocate(work(lwork))
    call SGEEV( jobvl, jobvr, nm, amat, nm, wr, wi, &
      & vl, nm, vr, nm, work, lwork, info )

    !------- サブルーチンエラー処理 -------
    if ( info /= 0 ) then
      call MessageNotify('E','DEIGEN_LAPACK',&
        'Error in calculating eigenvalues/vectors...',i=(/info/) )
      deallocate(wr, wi, vr, work, index)
      return
    end if

    !------- 固有ベクトル入れ換え -------
    if ( present(sort) ) then
      select case (trim(sort))
      case('RA')
        index = indexx(abs(wr))
      case('I ')
        index = indexx(wi)
      case('IA')
        index = indexx(abs(wi))
      case default
        index = indexx(wr)
      end select
    else
      index = indexx(wr)
    end if

    if ( present(reverse) )then
      if ( reverse ) then               ! 大きい順
        ! index = index(size(index):1:-1)
        ! 陽にループ展開
        lo = 1; hi = nm
        do while (lo < hi)
          tmp = index(lo)
          index(lo) = index(hi)
          index(hi) = tmp
          lo = lo + 1
          hi = hi - 1
        end do
      end if
    end if

    i = 1
    nout = min(size(eigen_r), size(eigen_i), nm)
    do while (i <= nout)
      j = index(i)
      eigen_r(i) = wr(j)
      eigen_i(i) = wi(j)

      if ( abs(wi(j)) <= SP_EPS ) then
        eigvec_r(:,i) = vr(:,j)
        eigvec_i(:,i) = 0.0_SP
        i = i +1
      elseif ( wi(j) > 0.0_SP ) then
        if (i + 1 > nout) exit
        if (j >= nm) exit
        eigvec_r(:,i)   = vr(:,j)
        eigvec_i(:,i)   = vr(:,j+1)
        eigvec_r(:,i+1) = vr(:,j)
        eigvec_i(:,i+1) = -vr(:,j+1)
        eigen_r(i+1)    = wr(j+1)
        eigen_i(i+1)    = wi(j+1)
        i = i + 2
      else
        i = i + 1
      end if
    end do

  end subroutine seigen

  !---------------------------------------------------------------------
  !> @ja
  !> 複素行列の固有値計算: ZGEEV/LAPACK の wrapper
  !> * 行列 CMAT の i 番目固有値を eigen(i) に格納
  !> * 対応する固有ベクトルを eigvec(:,i) に格納
  !> * 格納する固有値の数は引数 eigen の大きさで決まる
  !> * 固有値の順番は sort と order で定められる.
  !> * sort によって順番を定めるために用いる量を指定する.
  !> 実部(R), 実部の絶対値(RA), 虚部(I), 虚部の絶対値(IA)
  !> * reverse によって小さい順(.false.), 大きい順(.true.)を指定できる.
  !> * デフォルトは sort='R', reverse=.false.
  !> * 行列 CMAT は保存されない.
  !> @endja
  !> @en
  !> Eigenvalue Calculation for Complex Matrices: ZGEEV/LAPACK Wrapper
  !>
  !> * The i-th eigenvalue of matrix CMAT must be stored in eigen(i).
  !> * Similarly, the corresponding eigenvector must be stored in eigvec(:,i).
  !> * The number of eigenvalues that are stored is determined by the size of
  !> the eigen argument.
  !> * Finally, the order of eigenvalues is determined by sort and order.
  !> * Please specify the quantity utilized to ascertain the order via sort.
  !>   The real part (R), absolute real part (RA), imaginary part (I), and
  !>   absolute imaginary part (IA) must be designated.
  !> * Employ the reverse function to stipulate ascending order (.false.) or
  !>   descending order (.true.).
  !> * The default setting is sort=‘R’, reverse=.false.
  !> * It is imperative to note that the matrix CMAT is not saved.
  !> @enden
  subroutine ceigen(cmat,eigen,eigvec,info,sort,reverse)
    !> 入力正方行列
    complex(DP), dimension(:,:)               :: cmat
    !> 固有値実数部
    complex(DP), intent(out), dimension(:)    :: eigen
    !> 固有ベクトル実部
    complex(DP), intent(out), &
      & dimension(size(cmat,1),size(eigen))   :: eigvec
    !> ステータス
    integer, intent(out)                      :: info
    !> 並び変えの量
    character(len=2), intent(in), optional    :: sort
    !> 並び変えスイッチ
    logical, intent(in), optional             :: reverse

    !------------ 作業変数 ------------
    integer :: nm, i, j
    integer :: lwork, nout
    integer :: lo, hi, tmp
    complex(DP), allocatable, dimension(:)    :: ceig
    complex(DP), allocatable, dimension(:,:)  :: vl
    complex(DP), allocatable, dimension(:,:)  :: vr
    complex(DP), allocatable, dimension(:)    :: work
    real(DP),  allocatable, dimension(:)      :: rwork
    integer,  allocatable, dimension(:)       :: index
    character(len=1), parameter               :: jobvl='N'
    character(len=1), parameter               :: jobvr='V'

    !------- 形状チェック ------
    nm = size(cmat,1)
    if (nm  /= size(cmat,2))then
      call MessageNotify('E','DCEIGEN_LAPACK','Input matrix not square')
      return
    end if

    allocate(vl(nm, nm), vr(nm, nm))
    allocate(ceig(nm), index(nm))
    allocate(rwork(2*nm))

    !------- ZGEEV/LAPACK による計算 -----------------------------------------
    ! workspace query
    allocate(work(1))
    lwork = -1
    call ZGEEV( jobvl, jobvr, nm, cmat, nm, ceig,   &
      & vl, nm, vr, nm, work, lwork, rwork, info )
    lwork = max(1, int(real(work(1))))
    deallocate(work)
    ! 本計算    --------------------------------------------------------------
    allocate(work(lwork))
    call ZGEEV( jobvl, jobvr, nm, cmat, nm, ceig,   &
      & vl, nm, vr, nm, work, lwork, rwork, info )

    !------- サブルーチンエラー処理 -------
    if ( info /= 0 ) then
      call MessageNotify('E','DCEIGEN_LAPACK',&
        'Error in calculating eigenvalues/vectors...',i=(/info/) )
      deallocate(ceig, vl, vr, work, rwork, index)
      return
    end if

    !------- 固有ベクトル入れ換え -------
    if ( present(sort) ) then
      select case (trim(sort))
      case('RA')
        index = indexx(abs(dble(ceig)))
      case('I ')
        index = indexx(aimag(ceig))
      case('IA')
        index = indexx(abs(aimag(ceig)))
      case default
        index = indexx(dble(ceig))
      end select
    else
      index = indexx(dble(ceig))
    end if

    if ( present(reverse) )then
      if ( reverse ) then               ! 大きい順
        ! index = index(size(index):1:-1)
        ! 陽にループ展開
        lo = 1; hi = nm
        do while (lo < hi)
          tmp = index(lo)
          index(lo) = index(hi)
          index(hi) = tmp
          lo = lo + 1
          hi = hi - 1
        end do
      end if
    end if

    nout = min(size(eigen), nm)
    do i=1,nout
      j = index(i)
      eigen(i)    = ceig(j)
      eigvec(:,i) = vr(:,j)
    end do

  end subroutine ceigen

  !---------------------------------------------------------------------
  !> @ja
  !> 与えられた配列を値の小さい順に sort する (倍精度)
  !>
  !> @endja
  !> @en
  !> The given array must be sorted in ascending order by value (double precision)
  !> @enden
  function indexx_d(arrin) result(indexx)
    implicit none
    real(DP), dimension(:), intent(in)  :: arrin
    integer, allocatable, dimension(:)   :: indexx

    integer :: n, i
    integer, allocatable :: temp(:)

    n = size(arrin)
    allocate(indexx(n), temp(n))

    ! 初期インデックス
    indexx = (/ (i, i=1,n) /)

    call merge_sort(arrin, indexx, temp, 1, n)

  end function indexx_d
  !---------------------------------------------------------------------
  !> @ja
  !> 与えられた配列を値の小さい順に sort する (単精度)
  !> @endja
  !> @en
  !> The given array must be sorted in ascending order by value (single precision)
  !> @enden
  function indexx_s(arrin) result(indexx)
    implicit none
    real(SP), dimension(:), intent(in)  :: arrin
    integer, allocatable, dimension(:)   :: indexx

    integer :: n, i
    integer, allocatable :: temp(:)

    n = size(arrin)
    allocate(indexx(n), temp(n))

    ! 初期インデックス
    indexx = (/ (i, i=1,n) /)

    call merge_sort(arrin, indexx, temp, 1, n)

  end function indexx_s

  !---------------------------------------------------------------------
  !> merge sort (double precision)
  recursive subroutine merge_sort_d(a, idx, temp, left, right)
    real(DP), dimension(:), intent(in)  :: a
    integer, dimension(:), intent(inout) :: idx
    integer, dimension(:), intent(inout) :: temp
    integer, intent(in) :: left, right

    integer :: mid, i, j, k

    if (left >= right) return

    mid = (left + right) / 2
    call merge_sort_d(a, idx, temp, left, mid)
    call merge_sort_d(a, idx, temp, mid+1, right)

    ! マージ処理
    i = left
    j = mid + 1
    k = left

    ! メイン（両方残っている間だけ比較）
    do while (i <= mid .and. j <= right)
      if (a(idx(i)) <= a(idx(j))) then
        temp(k) = idx(i); i = i + 1
      else
        temp(k) = idx(j); j = j + 1
      end if
      k = k + 1
    end do

    ! tail copy（片側は丸ごと）
    do while (i <= mid)
      temp(k) = idx(i); i = i + 1; k = k + 1
    end do
    do while (j <= right)
      temp(k) = idx(j); j = j + 1; k = k + 1
    end do

    !$omp simd
    do i = left, right
      idx(i) = temp(i)
    end do
  end subroutine merge_sort_d

  !---------------------------------------------------------------------
  !> merge sort (single precision)
  recursive subroutine merge_sort_s(a, idx, temp, left, right)
    real(SP), dimension(:), intent(in)  :: a
    integer, dimension(:), intent(inout) :: idx
    integer, dimension(:), intent(inout) :: temp
    integer, intent(in) :: left, right

    integer :: mid, i, j, k

    if (left >= right) return

    mid = (left + right) / 2
    call merge_sort_s(a, idx, temp, left, mid)
    call merge_sort_s(a, idx, temp, mid+1, right)

    ! マージ処理
    i = left
    j = mid + 1
    k = left

    ! メイン（両方残っている間だけ比較）
    do while (i <= mid .and. j <= right)
      if (a(idx(i)) <= a(idx(j))) then
        temp(k) = idx(i); i = i + 1
      else
        temp(k) = idx(j); j = j + 1
      end if
      k = k + 1
    end do

    ! tail copy（片側は丸ごと）
    do while (i <= mid)
      temp(k) = idx(i); i = i + 1; k = k + 1
    end do
    do while (j <= right)
      temp(k) = idx(j); j = j + 1; k = k + 1
    end do

    !$omp simd
    do i = left, right
      idx(i) = temp(i)
    end do
  end subroutine merge_sort_s
#endif

end module spml_utils
