!> author: Sam Hatfield, Fred Kucharski, Franco Molteni
!  date: 01/05/2019
!
!  Change history:
!
!  01/07/2021 Andres Perez Hortal: Group parameters into derived datatypes.!
!
!  Model control module

module model_control
    use types, only: p
    implicit none

    private
    public datetime_equal, initialize_control, advance_date
    public Datetime_t, ControlParams_t, Datetime_Ptr_t, ControlParams_Ptr_t

    !> For storing dates and times.
    type Datetime_t
        integer :: year
        integer :: month
        integer :: day
        integer :: hour
        integer :: minute
        logical :: allocated = .false.
    end type

    ! Container for a Datetime object. Used for the python interface.
    type Datetime_Ptr_t
        type(Datetime_t), pointer :: p => NULL()
    end type

    ! ============================
    ! Model time control variables
    ! ============================
    !> Structure used to store the model control parameter,
    !  like start datem current date, end date, etc.
    type ControlParams_t
        type(Datetime_t)     :: model_datetime !! The model's current datetime (continuously updated)
        type(Datetime_t)     :: start_datetime !! The start datetime
        type(Datetime_t)     :: end_datetime   !! The end datetime
        integer              :: imont1           !! The month used for computing seasonal forcing fields
        real(p)              :: tmonth           !! The fraction of the current month elapsed
        real(p)              :: tyear            !! The fraction of the current year elapsed

        integer              :: month_idx = 1     !! Simulation month (star month=1)
        integer              :: ndaycal(12, 2)   !! The model calendar
        
        integer :: diag_interval     !! Period (number of steps) for diagnostic print-out
    end type

    ! Container for a ControlParams object. Used for the python interface.
    type ControlParams_Ptr_t
        type(ControlParams_t), pointer :: p => NULL()
    end type

    ! Parameters
    integer, parameter :: ncal = 365     !! The number of days in a year

    !> The number of days in each month
    integer, parameter :: ncal365(12) = (/31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31/)

contains
    !> Checks whether two datetimes are equal.
    logical function datetime_equal(datetime1, datetime2)
        type(Datetime_t), intent(in) :: datetime1, datetime2

        if (datetime1%year == datetime2%year .and. &
            datetime1%month == datetime2%month .and. &
            datetime1%day == datetime2%day .and. &
            datetime1%hour == datetime2%hour .and. &
            datetime1%minute == datetime2%minute) then
            datetime_equal = .true.
        else
            datetime_equal = .false.
        end if
    end function

    !> Initializes control structure with the default parameters.
    subroutine initialize_control(control_params, &
                                  start_datetime, end_datetime, &
                                  diag_interval)
        type(ControlParams_t), intent(inout), target  :: control_params
        type(Datetime_t), intent(in)  :: start_datetime, end_datetime
        integer, intent(in) :: diag_interval

        integer, pointer   :: ndaycal(:, :)

        integer :: jm

        ! Some mappings to improve code redability
        ndaycal => control_params%ndaycal

        control_params%start_datetime = start_datetime
        control_params%end_datetime = end_datetime
        ! Current model datetime is start datetime
        control_params%model_datetime = control_params%start_datetime

        control_params%diag_interval = diag_interval

        ! Set calendar
        if (ncal == 365) then
            ndaycal(:, 1) = ncal365(:)
        else
            ndaycal(:, 1) = 30
        end if

        ndaycal(1, 2) = 0
        do jm = 2, 12
            ndaycal(jm, 2) = ndaycal(jm - 1, 1) + ndaycal(jm - 1, 2)
        end do

        control_params%month_idx = 1

        call update_forcing_params(control_params)

    end subroutine

    !> Updates the current datetime and related date variables.
    subroutine advance_date(control_params)
        use params, only: nsteps
        type(ControlParams_t), target, intent(inout)  :: control_params

        type(Datetime_t), pointer  :: model_datetime
        model_datetime => control_params%model_datetime

        ! Increment minute counter
        model_datetime%minute = model_datetime%minute + int(24*60/nsteps)

        ! Increment hour counter if necessary
        if (model_datetime%minute >= 60) then
            model_datetime%minute = mod(model_datetime%minute, 60)
            model_datetime%hour = model_datetime%hour + 1
        end if

        ! Increment day counter if necessary
        if (model_datetime%hour >= 24) then
            model_datetime%hour = mod(model_datetime%hour, 24)
            model_datetime%day = model_datetime%day + 1
        end if

        ! Increment month counter if necessary
        ! Leap year and February?
        if (mod(model_datetime%year, 4) == 0 .and. model_datetime%month == 2) then
            if (model_datetime%day > 29) then
                model_datetime%day = 1
                model_datetime%month = model_datetime%month + 1
                control_params%month_idx = control_params%month_idx + 1
            end if
        else
            if (model_datetime%day > control_params%ndaycal(model_datetime%month, 1)) then
                model_datetime%day = 1
                model_datetime%month = model_datetime%month + 1
                control_params%month_idx = control_params%month_idx + 1
            end if
        end if

        ! Increment year counter if necessary
        if (model_datetime%month > 12) then
            model_datetime%month = 1
            model_datetime%year = model_datetime%year + 1
        end if

        call update_forcing_params(control_params)

    end subroutine

    subroutine update_forcing_params(control_params)
        use params, only: iseasc
        type(ControlParams_t), target, intent(inout)  :: control_params

        type(Datetime_t), pointer  :: model_datetime
        integer, pointer   :: imont1
        real(p), pointer   :: tmonth
        real(p), pointer   :: tyear
        integer, pointer   :: ndaycal(:, :)

        ! Some mappings to improve code redability
        ndaycal => control_params%ndaycal
        tyear => control_params%tyear
        tmonth => control_params%tmonth
        imont1 => control_params%imont1
        model_datetime => control_params%model_datetime

        ! additional variables to define forcing terms and boundary cond.
        if (iseasc >= 1) then
            imont1 = model_datetime%month
            tmonth = (model_datetime%day - 0.5)/float(ndaycal(model_datetime%month, 1))
            tyear = (ndaycal(model_datetime%month, 2) + model_datetime%day - 0.5)/float(ncal)
        end if
    end subroutine

end module
