MODULE PROFILER
  USE ISO_C_BINDING, ONLY: C_CHAR
  USE ISO_FORTRAN_ENV, ONLY: INT64, REAL64
  USE PCG32_MODULE, ONLY: PCG32_HASH_STRING
  IMPLICIT NONE

  PUBLIC :: START_PROFILING, STOP_PROFILING, GET_PROFILE, PROFILE_ENTRY
  PRIVATE

  TYPE, BIND(C) :: PROFILE_ENTRY
     REAL(KIND=REAL64) :: WALL_TIME = 0.0
     REAL(KIND=REAL64) :: CPU_TIME = 0.0
     INTEGER(KIND=INT64) :: CALL_COUNT = 0
     REAL(KIND=REAL64) :: START_WALL_TIME = 0.0
     REAL(KIND=REAL64) :: START_CPU_TIME = 0.0
  END TYPE PROFILE_ENTRY

  INTEGER(KIND=INT64) :: CLOCK_RATE, CLOCK_MAX
  INTEGER, ALLOCATABLE, DIMENSION(:) :: SUBROUTINE_NUMBERS
  TYPE(PROFILE_ENTRY), ALLOCATABLE, DIMENSION(:) :: PROFILES

CONTAINS

  ! -------------------------------------------------------------------

  SUBROUTINE START_PROFILING(SUBROUTINE_NAME)
    CHARACTER(LEN=*), INTENT(IN) :: SUBROUTINE_NAME
    INTEGER :: SUBROUTINE_NUMBER
    INTEGER :: IDX
    SUBROUTINE_NUMBER = PCG32_HASH_STRING(SUBROUTINE_NAME)
    IDX = FIND_PROFILE_INDEX(SUBROUTINE_NUMBER)
    IF (IDX == 0) THEN
       CALL ADD_PROFILE(SUBROUTINE_NUMBER)
       IDX = SIZE(PROFILES)
    ENDIF
    PROFILES(IDX)%START_WALL_TIME = GET_TIME()
    PROFILES(IDX)%START_CPU_TIME = GET_CPU_TIME()
  END SUBROUTINE START_PROFILING

  SUBROUTINE STOP_PROFILING(SUBROUTINE_NAME)
    CHARACTER(LEN=*), INTENT(IN) :: SUBROUTINE_NAME
    INTEGER :: SUBROUTINE_NUMBER
    INTEGER :: IDX
    REAL(KIND=REAL64) :: WALL_TIME_ELAPSED, CPU_TIME_ELAPSED
    SUBROUTINE_NUMBER = PCG32_HASH_STRING(SUBROUTINE_NAME)
    IDX = FIND_PROFILE_INDEX(SUBROUTINE_NUMBER)
    IF (IDX == 0) RETURN
    WALL_TIME_ELAPSED = GET_TIME() - PROFILES(IDX)%START_WALL_TIME
    CPU_TIME_ELAPSED = GET_CPU_TIME() - PROFILES(IDX)%START_CPU_TIME
    PROFILES(IDX)%WALL_TIME = PROFILES(IDX)%WALL_TIME + WALL_TIME_ELAPSED
    PROFILES(IDX)%CPU_TIME = PROFILES(IDX)%CPU_TIME + CPU_TIME_ELAPSED
    PROFILES(IDX)%CALL_COUNT = PROFILES(IDX)%CALL_COUNT + 1
  END SUBROUTINE STOP_PROFILING

  FUNCTION GET_PROFILE(SUBROUTINE_NAME) RESULT(PROFILE)
    CHARACTER(LEN=*), INTENT(IN) :: SUBROUTINE_NAME
    INTEGER :: SUBROUTINE_NUMBER
    TYPE(PROFILE_ENTRY) :: PROFILE
    INTEGER :: IDX
    SUBROUTINE_NUMBER = PCG32_HASH_STRING(SUBROUTINE_NAME)
    IDX = FIND_PROFILE_INDEX(SUBROUTINE_NUMBER)
    IF (IDX == 0) RETURN
    PROFILE = PROFILES(IDX)
  END FUNCTION GET_PROFILE

  ! -------------------------------------------------------------------

  SUBROUTINE ADD_PROFILE(SUBROUTINE_NUMBER)
    INTEGER, INTENT(IN) :: SUBROUTINE_NUMBER
    TYPE(PROFILE_ENTRY) :: NEW_PROFILE
    IF (ALLOCATED(PROFILES)) THEN
       SUBROUTINE_NUMBERS = [SUBROUTINE_NUMBERS, SUBROUTINE_NUMBER]
       PROFILES = [PROFILES, NEW_PROFILE]
    ELSE
       SUBROUTINE_NUMBERS = [SUBROUTINE_NUMBER]
       PROFILES = [NEW_PROFILE]
    ENDIF
  END SUBROUTINE ADD_PROFILE

  FUNCTION GET_TIME() RESULT(TIME)
    INTEGER(KIND=INT64) :: WALL_TIME
    REAL(KIND=REAL64) :: TIME
    CALL SYSTEM_CLOCK(WALL_TIME, CLOCK_RATE, CLOCK_MAX)
    TIME = REAL(WALL_TIME,KIND=REAL64) / REAL(CLOCK_RATE,KIND=REAL64)
  END FUNCTION GET_TIME

  FUNCTION GET_CPU_TIME() RESULT(TIME)
    REAL(KIND=REAL64) :: TIME
    CALL CPU_TIME(TIME)
  END FUNCTION GET_CPU_TIME

  FUNCTION FIND_PROFILE_INDEX(SUBROUTINE_NUMBER) RESULT(IDX)
    INTEGER, INTENT(IN) :: SUBROUTINE_NUMBER
    INTEGER :: IDX, I
    IDX = 0
    IF (.NOT. ALLOCATED(PROFILES)) RETURN
    DO I = 1, SIZE(PROFILES)
       IF (SUBROUTINE_NUMBERS(I) == SUBROUTINE_NUMBER) THEN
          IDX = I
          EXIT
       ENDIF
    END DO
  END FUNCTION FIND_PROFILE_INDEX

END MODULE PROFILER
