# -*- mode: sh; sh-shell: bash -*-
# vim: set ft=bash:
# shellcheck shell=bash

### Library for managing lockfiles

declare -gA LOCK_REC=()

function lock_wait ## <lockname> - Waits until we have the lock on a lockfile.
{
    ## '.lock' is automatically appended to the 'lockname'.
    ## These locks are recursive the same process can lock_wait on the same lockfile multiple
    ## times which must be paired with the same numbers of 'lock_remove' to unlock it.

    # shellcheck disable=SC2155
    local lockfile="$(realpath -s "$1.lock")"
    if (( LOCK_REC["$lockfile"] > 0 )); then
        (( LOCK_REC["$lockfile"]+=1 ))
        trace "recursive lock: $1 as $BASHPID"
        return 0
    fi

    # Create a temporary lockfile specific to this process
    local tmp_lockfile="${1}.$BASHPID.lock"

    # Put our PID in the temporary file
    echo -n $BASHPID > "$tmp_lockfile"

    # backoff with a binomial sequence up to 100ms
    declare -i backoff=1

    # Try to atomically acquire the lock
    while ! ln -s "$tmp_lockfile" "$lockfile" 2>/dev/null; do
        # Lock exists, check if it's stale
        if [[ -L "$lockfile" ]]; then
            # shellcheck disable=SC2155 # want to ignore errors here
            local lockfile_target=$(readlink "$lockfile")
            local lockpid="${lockfile_target%.lock}"
            lockpid="${lockpid##*.}"

            # Check if the process still exists
            if kill -0 "$lockpid" 2>/dev/null; then
                # Wait for the process to exit
                trace "wait: $lockpid to complete"
                wait "$lockpid" 2>/dev/null || sleep "${backoff:: -1}.${backoff: -1}"
                (( backoff < 100 )) && (( backoff+=1 ))
            else
                rm -f "$lockfile"
                continue
            fi
        fi
    done
    # remove stale tmp_lockfiles
    local file
    for file in "$1."*".lock"; do
        local lockpid="${file%.lock}"
        lockpid="${lockpid##*.}"
        [[ "$lockpid" != "$BASHPID" && "$(cat "$file")" = "$lockpid" ]] &&
            ! kill -0 "$lockpid" 2>/dev/null &&
            rm -f "$file"
    done

    LOCK_REC["$lockfile"]=1

    trace "locked: $1 as $BASHPID"
}

function lock_try_norec ## <lockname> - try to lock a lockfile non recursively.
{
    ## Will not wait, fails when we already have the lock
    # shellcheck disable=SC2155
    local lockfile="$(realpath -s "$1.lock")"
    if (( LOCK_REC["$lockfile"] > 0 )); then
        return 1
    fi

    # Create a temporary lockfile specific to this process
    local tmp_lockfile="${1}.$BASHPID.lock"

    # Put our PID in the temporary file
    echo $BASHPID > "$tmp_lockfile"

    # Try to atomically acquire the lock
    while ! ln -s "$tmp_lockfile" "$lockfile" 2>/dev/null; do
        # Lock exists, check if it's stale
        if [[ -L "$lockfile" ]]; then
            # shellcheck disable=SC2155 # want to ignore errors here
            local lockfile_target=$(readlink "$lockfile")
            local lockpid="${lockfile_target%.lock}"
            lockpid="${lockpid##*.}"

            # Check if the process still exists
            if kill -0 "$lockpid" 2>/dev/null; then
                return 1
            else
                rm -f "$lockfile"
                continue
            fi
        fi
    done
    LOCK_REC["$lockfile"]=1

    trace "locked: $1 as $BASHPID"
}

function lock_next # <old> <new> - hand over hand locking
{
    ## Locks 'new' and then removes the 'old' locck.
    lock_wait "$2"
    lock_remove "$1"
}

function lock_send ## <lock> <who> - Sends a 'lock' to another process with pid 'who'.
{
    ## Locks can be send atomically to another process. 'lock_send' registers the lock to be
    ## send to a process, the lock will be send at the final lock_remove in the sending
    ## process when all recursive locks are freed. Note that to obtain the 'who' pid the
    ## receiver has to be started first.
    # shellcheck disable=SC2155
    local lockfile="$(realpath -s "$1.lock")"
    (( LOCK_REC["$lockfile"] > 0 )) || die "lock $1 is not locked by $BASHPID"

    # shellcheck disable=SC2155
    local send_to=$(<"$lockfile")
    [[ "$send_to" != "$BASHPID" ]] && {
        error "lock $1 is already send to $send_to"
        return 1
    }

    echo -n "$2" > "$lockfile"
}

function lock_receive ## <lock> - Wait for lock send by another process.
{
    ## Blocks until the the sender removed all uses of 'lock',
    # shellcheck disable=SC2155
    local lockfile="$(realpath -s "$1.lock")"

    # Create a temporary lockfile specific to this process
    local tmp_lockfile="${1}.$BASHPID.lock"

    # Put our PID in the temporary file
    echo $BASHPID > "$tmp_lockfile"

    # backoff with a binomial sequence up to 100ms
    declare -i backoff=1

    while true; do
        if [[ "$lockfile" -ef "$tmp_lockfile" ]]; then
            trace "lock received: $1"
            return 0
        fi
        # TODO: if sender still exists, else die
        sleep "${backoff:: -1}.${backoff: -1}"
        (( backoff < 100 )) && (( backoff+=1 ))
    done
}

function lock_remove ## <lock> - Unlocks 'lock'.
{
    ## 'lock_remove' must be paired with the same numbers of earlier 'lock_wait'. Only the
    ## last 'lock_remove' will release the lockfile.
    # shellcheck disable=SC2155
    local lockfile="$(realpath -s "$1.lock")"

    (( LOCK_REC["$lockfile"]-=1 ))
    if (( LOCK_REC["$lockfile"] > 0 )); then
        trace "recursive unlock $1"
        return 0
    fi

    if [[ -L "$lockfile" ]]; then
        unset "LOCK_REC[$lockfile]"

        # shellcheck disable=SC2155 # want to ignore errors here
        local lockfile_target=$(readlink "$lockfile")
        local lockpid="${lockfile_target%.lock}"
        lockpid="${lockpid##*.}"

        if [[ "$lockpid" = "$BASHPID" ]]; then
            # shellcheck disable=SC2155
            local send_to=$(<"$lockfile_target")
            if [[ "$send_to" != "$BASHPID" ]]; then
                trace "send lock $1 to $send_to"

                local tmp_lockfile="${1}.$send_to.lock"
                [[ -f "$tmp_lockfile" ]] || die "lock receiver $tmp_lockfile does not exist"

                ln -sf "$tmp_lockfile" "$lockfile" 2>/dev/null
                rm -f "$lockfile_target"
            else
                trace "unlock $1"
                # Remove both the lock and the temporary file
                rm -f "$lockfile"
                rm -f "$lockfile_target"
            fi
            return 0
        else
            error "$lockfile is not ours (we are $BASHPID, owned by $lockpid)"
            return 1
        fi
    elif [[ -f "$lockfile" ]]; then
        error "$lockfile is not a symbolic link lockfile"
        return 1
    else
        error "$lockfile does not exist"
        return 1
    fi
}

