# librunscript.sh is a library of shell script functions that can be used in
# EC-Earth run scripts.
#
# Usage: source ./librunscript.sh

# Function info writes information to standard out.
#
# Usage: info MESSAGE ...
#
function info()
{
    echo "*II* $@"
}

# Function error writes information to standard out and exits the script with
# error code 1.
#
# Usage: error MESSAGE ...
#
function error()
{
    echo "*EE* $@"
    exit 1
}

# Function has_config checks it's arguments for matches in the $config variable
# and returns true (0) or false (1) accordingly. Optionally, the first argument
# can be either "all" (all arguments must match) or "any" (at least one
# argument must match). If the first argument is neither "all" nor "any", the
# function behaves like "all" was given as the first argument.
#
# Usage: has_config [all|any] ARGS ...
#
# Syntax rules:
#
# The $config variable takes a list of names (typically software components),
# separated by white spaces:
#
# config="foo bar baz"  # Specifies three components: 'foo', 'bar', and 'baz'
#
# It is possible to add comma-separated lists of options to components. The
# list is separated from the component by a colon:
#
# config="foo bar:qux,fred baz:plugh"  # Adds the options 'qux' and 'fred' to
#                                      # component 'bar' as well as option
#                                      # 'plugh' to  component 'baz'
#
# When using the has_config function to check the $config variable, it is
# important to list every component-option pair separately. To check for both
# the 'qux' and 'fred' options of component 'bar' in the above example, use:
#
# has_config bar:qux bar:fred && echo "Got it!"
#
function has_config()
{
    # If called without arguments, return false
    (( $# )) || return 1

    # If $config unset or empty, return false
    [[ -z "${config:-}" ]] && return 1

    local __c
    local __m

    # If first argument is "any" then only one of the arguments needs to match
    # to return true. Return false otherwise
    if [ "$1" == "any" ]
    then
        shift
        for __c in "$@"
        do
            for __m in $config
            do
                [[ "$__m" =~ "${__c%:*}" ]] && [[ "$__m" =~ "${__c#*:}" ]] && return 0
            done
        done
        return 1
    fi

    # If first argument is "all", or neither "any" nor "all", all arguments
    # must match to return true. Return false otherwise.
    [[ "$1" == "all" ]] && shift

    local __f
    for __c in "$@"
    do
        __f=0
        for __m in $config
        do
            [[ "$__m" =~ "${__c%:*}" ]] && [[ "$__m" =~ "${__c#*:}" ]] && __f=1
        done
        (( __f )) || return 1
    done
    return 0
}

# Function leap days calculates the number of leap days (29th of Februrary) in
# a time intervall between two dates.
#
# Usage leap_days START_DATE END_DATE
function leap_days()
{
    local ld=0
    local frstYYYY=$(date -ud "$1" +%Y)
    local lastYYYY=$(date -ud "$2" +%Y)

    set +e

    # Check first year for leap day between start and end date
    $(date -ud "${frstYYYY}-02-29" > /dev/null 2>&1) \
    && (( $(date -ud "$1" +%s) < $(date -ud "${frstYYYY}-03-01" +%s) )) \
    && (( $(date -ud "$2" +%s) > $(date -ud "${frstYYYY}-02-28" +%s) )) \
    && (( ld++ ))

    # Check intermediate years for leap day
    for (( y=(( ${frstYYYY}+1 )); y<=(( ${lastYYYY}-1 )); y++ ))
    do
        $(date -ud "$y-02-29" > /dev/null 2>&1) && (( ld++ ))
    done

    # Check last year (if different from first year) for leap day between start
    # and end date
    (( $lastYYYY > $frstYYYY )) \
    && $(date -ud "${lastYYYY}-02-29" > /dev/null 2>&1) \
    && (( $(date -ud "$1" +%s) < $(date -ud "${lastYYYY}-03-01" +%s) )) \
    && (( $(date -ud "$2" +%s) > $(date -ud "${lastYYYY}-02-28" +%s) )) \
    && (( ld++ ))

    set -e

    echo "$ld"
}

# Helper function for absolute_date_noleap().
#
# Recursively 'fixes' a relative date into an absolute date referenced to
# the "noleap" calendar instead of Julian/Gregorian.
#
# Usage: __noleap_fixer base_date modifier
#

function __noleap_fixer ()
{
    local base_date="$1"
    local modifier="$2"
    # Evaluate the base and the modified date.
    local t0=$(date -u -d "${base_date}" +'%Y%m%d %H')
    local t1=$(date -u -d "${base_date} ${modifier}" +'%Y%m%d %H')
    local nleap
    local op
    # Compute the number of leap days between the base date and the modified
    # date.
    if [[ $(date -u -d "${t0}" +%Y%j) -lt $(date -u -d "${t1}" +%Y%j) ]]; then
        nleap=$(leap_days "${t0}" "${t1}")
    else
        nleap=$(leap_days "${t1}" "${t0}")
    fi
    if [[ ${nleap} -eq 0 ]]; then
        # Return the modified date if there are no leap days to account for.
        echo "${t1}"
    else
        # Obtain the modifier symbol (either + or -), this is used to determine
        # the direction to apply corrections in.
        if echo "${modifier}" | grep '+' 2>&1 > /dev/null; then
            op='+'
        else
            op='-'
        fi
        if [[ $(date -ud "${t1}" +%m%d) == 0229 ]]; then
            # If the modified day is the leap day we shift it by one day in the
            # appropriate direction and subtract 1 from the number of leap days
            # we need to account for.
            t1=$(date -ud "${t1} ${op} 1 day" +'%Y%m%d %H')
            nleap=$(( nleap - 1 ))
        fi
        if [[ ${nleap}  -eq 0 ]]; then
            # After correction there maybe no leap days left to account for, if
            # so we just return the corrected modified date.
            echo "${t1}"
        else
            # Otherwise we recurse, to apply our modification safely accounting
            # for leap days.
            __noleap_fixer "${t1}" "${op} ${nleap} days"
        fi
    fi
}

# Convert a relative date to an absolute date assuming the "noleap" calendar.
#
# A relative date is a base date with some modifier, such as
# "2012-01-01 + 2 months".
#
# Usage: absolute_date_noleap date_specifier.
function absolute_date_noleap ()
{
    local input_date="$1"
    # Split the input into a date part and a modifier part.
    local regex='([a-z|A-Z|0-9|-|+|,|:|\s]*)\s*([+|-]\s*[0-9]+\s*(second|minute|hour|day)s{0,1}).*'
    local base_date=$(echo "$1" | sed -r "s/${regex}/\1^\2/" | cut -d "^" -f 1)
    local modifier=$(echo "$1" | sed -r "s/${regex}/\1^\2/" | cut -d "^" -f 2)
    if [ -z "${modifier}" ] || [ "${modifier}" = "${input_date}" ] || \
            ! echo "${modifier}" | egrep -i -e 'second|minute|hour|day' 2>&1 >/dev/null; then
        # Either there was no modifier or it performs calendar independent
        # modifications, and thus we can safely use the normal `date` tool.
        date -uRd "${input_date}"
    else
        # Special care is needed to make sure the modifier is applied within
        # the noleap calendar.
        date -uRd "$(__noleap_fixer "${base_date}" "${modifier}")"
    fi
}

# Functions to compute computationa performance according to CPMIP metrics
#
# cpmip_sypd: Computes Simulated Years Per Day (SYPD)
#             Needs two arguments:
#             $1 - length of leg in seconds (use $leg_length_sec)
#             $2 - run time of leg in seconds (use $(( t2 - t1 )))
function cpmip_sypd ()
{
    bc <<< "scale=2 ; $1 / ( $2 * 365 )"
}

# cpmip_chpsy: Computes Core Hours Per Simulated Years (CHPSY)
#              Needs three arguments:
#              $1 - length of leg in seconds (use $leg_length_sec)
#              $2 - run time of leg in seconds (use $(( t2 - t1 )))
#              $3 - overall number of cores
function cpmip_chpsy ()
{
    bc <<< "scale=0 ; 365 * 24 * $2 * $3 / $1"
}
