module timeutils
use phys_consts, only: wp
use, intrinsic :: iso_fortran_env, only: sp=>real32, dp=>real64, int32, int64

implicit none (type, external)
private
public :: ymd2doy, sza, dateinc, utsec2filestem, date_filename, day_wrap, find_lastdate, find_time_elapsed

real(wp), parameter :: pi = 4._wp*atan(1._wp)

contains

elemental integer function daysmonth(year, month) result(days)

integer, intent(in) :: year, month
integer :: monthdays(12)

if ((year < 1600) .or. (year > 2500)) error stop 'is year specified correctly?'
if ((month < 1) .or. (month > 12)) error stop 'impossible month'

monthdays = [31,28,31,30,31,30,31,31,30,31,30,31]

if (mod(year, 4)==0 .and. mod(year, 100)/=0 .or. mod(year, 400) == 0) monthdays(2)=29

days = monthdays(month)

end function daysmonth


elemental integer function ymd2doy(year, month, day) result(doy)

integer, intent(in) :: year, month, day
integer :: i

if ((day < 1) .or. (day > daysmonth(year, month))) error stop 'impossible day'

doy = 0
do i = 1, month-1
  doy = doy + daysmonth(year, i)
enddo

doy = doy + day

end function ymd2doy


elemental function sza(year, month, day, UTsec,glat,glon)
!! computes sza in radians
!! CALCULATE SOLAR ZENITH ANGLE OVER A GIVEN GET OF LAT/LON

integer, intent(in) :: year, month, day
real(wp), intent(in) :: UTsec
real(wp), intent(in) :: glat,glon
real(wp) :: sza

real(wp), parameter :: tau = 2._wp*pi

real(wp) :: doy,soldecrad
real(wp) :: lonrad,LThrs,latrad,hrang

!> SOLAR DECLINATION ANGLE
doy = ymd2doy(year, month, day)
soldecrad = -23.44_wp*cos(tau/365._wp*(doy+10)) * pi/180

!> HOUR ANGLE
lonrad=glon*pi/180
lonrad = modulo(lonrad, 2*pi)

LThrs=UTsec/3600._wp+lonrad/(pi/12._wp)
hrang=(12-LThrs)*(pi/12._wp)

!> SOLAR ZENITH ANGLE
latrad=glat*pi/180._wp
sza=acos(sin(soldecrad)*sin(latrad)+cos(soldecrad)*cos(latrad)*cos(hrang))

end function sza


pure subroutine dateinc(dtsec, ymd, UTsec)
!! increment datetime by dtsec

real(wp), intent(in) :: dtsec
!! seconds to increment
integer, intent(inout) :: ymd(3)
!! year, month, day of month
real(wp), intent(inout) :: UTsec
!! seconds since midnight UTC

integer :: year,month,day
integer :: monthinc          !< we incremented the month

year=ymd(1); month=ymd(2); day=ymd(3);

if (day < 1) error stop 'temporal:timeutils:dateinc(): days are positive integers'
if (utsec < 0) error stop 'negative UTsec, simulation should go forward in time only!'
if (dtsec < 0) error stop 'negative dtsec, simulation should go forward in time only!'
if (dtsec > 86400) error stop 'excessively large dtsec > 86400, simulation step should be small enough!'

UTsec = UTsec + dtsec
do while (UTsec >= 86400)
  UTsec = UTsec - 86400._wp
  day = day+1
  call day_wrap(year, month, day)
end do

ymd(1)=year; ymd(2)=month; ymd(3)=day;    !replace input array with new date

end subroutine dateinc


recursive pure subroutine day_wrap(year, month, day)
!! increment date if needed, according to day
!! that is, if day is beyond month, increment month and year if needed
integer, intent(inout) :: year, month, day

if (month < 1 .or. day < 1) error stop 'day_wrap: months and days must be positive'

!> wrap months
do while (month > 12)
  month = month - 12
  year = year + 1
end do

!> wrap days
do while (day > daysmonth(year, month))
  day = day - daysmonth(year, month)
  month = month + 1
  call day_wrap(year, month, day)
end do

end subroutine day_wrap


pure function date_filename(outdir, ymd, UTsec)
!! GENERATE A FILENAME stem OUT OF A GIVEN DATE/TIME
!! (does not include suffix like .h5)

character(*), intent(in) :: outdir
integer, intent(in) :: ymd(3)
class(*), intent(in) :: UTsec
character(:), allocatable :: date_filename

character(len=21) :: stem

stem = utsec2filestem(ymd, UTsec)

date_filename = outdir // '/' // stem

end function date_filename


pure character(21) function utsec2filestem(ymd, UTsec)
!! file stem is exactly 21 characters long, per Matt Z's de facto spec.
!! we keep microsecond dummy precision filenames to be legacy compatible
!! true filename resolution is 10 milliseconds due to real32 7 digits of precision vis 86400 second day.
integer, intent(in) :: ymd(3)
class(*), intent(in) :: UTsec
!! UTC second: real [0.0 .. 86400.0)

character(12) :: sec_str
integer :: year, month, day, seconds, millisec, frac

year = ymd(1)
month = ymd(2)
day = ymd(3)

select type(UTsec)
  type is (real(sp))
    !! round to nearest ten milliseconds
    millisec = nint(UTsec * 100) * 10
  type is (real(dp))
    !! round to nearest ten milliseconds
    millisec = nint(UTsec * 100) * 10
  type is (integer(int32))
    millisec = UTsec * 1000
  type is (integer(int64))
    millisec = int(UTsec) * 1000
  class default
    error stop "timeutils.f90:utsec2filestem unknown UTsec type"
end select

seconds = millisec / 1000 !< truncate fractional second
if (seconds < 0 .or. seconds > 86400) error stop 'timeutils.f90::utsec2filestem did NOT satisfy 0 <= seconds < 86400'
if (seconds == 86400) then
  !> FIXME: This corner case is from not using integers for microseconds
  ! write(stderr,*) 'utsec2filestem: FIXME: patching UTsec=86400 to next day midnight'
  day = day+1
  seconds = 0
  millisec = 0
  call day_wrap(year, month, day)
endif

frac = millisec*1000 - seconds * 1000000  !< microseconds

write(sec_str, '(I5.5, A1, I6.6)') seconds, '.', frac

write(utsec2filestem, '(i4,I2.2,I2.2,a13)') year, month, day, '_' // sec_str

end function utsec2filestem


pure subroutine find_lastdate(ymd0,UTsec0,ymdtarget,UTsectarget,cadence,ymd,UTsec)

!> Compute the last date before the target date based on a start date and date cadence.  The
!  file is assumed to exist and programmer needs to check for existence outside this
!  procedure.
! FIXME:  this will occasionally hang and may need to be re-examined at some point...

integer, dimension(3), intent(in) :: ymd0
real(wp), intent(in) :: UTsec0
integer, dimension(3), intent(in) :: ymdtarget
real(wp), intent(in) :: UTsectarget
real(wp), intent(in) :: cadence
integer, dimension(3), intent(out) :: ymd
real(wp), intent(out) :: UTsec

integer, dimension(3) :: ymdnext
real(wp) :: UTsecnext
logical :: flagend


ymd=ymd0
UTsec=UTsec0
ymdnext=ymd0
UTsecnext=UTsec0
flagend=ymdnext(1)>=ymdtarget(1) .and. ymdnext(2)>=ymdtarget(2) .and. ymdnext(3)>=ymdtarget(3) .and. UTsecnext>UTsectarget &
          .or. ymdnext(1)>ymdtarget(1) .or. ymdnext(2)>ymdtarget(2) .or. ymdnext(3)>ymdtarget(3) ! in case the first time step is the last before target
do while ( .not.(flagend) )
  ymd=ymdnext
  UTsec=UTsecnext
  call dateinc(cadence,ymdnext,UTsecnext)
  flagend=ymdnext(1)>=ymdtarget(1) .and. ymdnext(2)>=ymdtarget(2) .and. ymdnext(3)>=ymdtarget(3) .and. UTsecnext>UTsectarget &
            .or. ymdnext(1)>ymdtarget(1) .or. ymdnext(2)>ymdtarget(2) .or. ymdnext(3)>ymdtarget(3)
end do
! When the loops exits ymd,UTsec have the date of the last output file before the given target time OR the first output file in the case that the target date is before the begin date...

end subroutine find_lastdate


pure function find_time_elapsed(ymdstart,UTsecstart,ymdend,UTsecend,dt) result(telapsed)

! finds the amount of time that has elapsed between a given start and end date, using a given dt increment
! the resulting elapsed time will be the smallest multiple of dt that is >= true elapsed time

!! inputs
integer, dimension(3), intent(in) :: ymdstart,ymdend
real(wp), intent(in) :: UTsecstart,UTsecend
reaL(wp), intent(in) :: dt

integer, dimension(3) :: ymdnow
real(wp) :: UTsecnow

real(wp) :: telapsed  !! output

telapsed=0._wp; ymdnow=ymdstart; UTsecnow=UTsecstart;
do while (.not. (all(ymdend==ymdnow) .and. UTsecnow>UTsecend) )
  call dateinc(dt,ymdnow,UTsecnow)
  telapsed=telapsed+dt
end do
telapsed=telapsed-dt

end function find_time_elapsed


end module timeutils
