# -*- mode: sh; sh-shell: bash -*-
# vim: set ft=bash:
# shellcheck shell=bash
# shellcheck disable=2016,2086

### Rust/cargo support

# prototype: "toolchain" = "extcomp cargo"
# prototype: "tool" = "ext cargo_tool_complete"
# prototype: "buildargs" = "extcomp cargo ${CARGO_TOOLCHAIN} build --workspace"
# prototype: "testargs" = "extcomp cargo ${CARGO_TOOLCHAIN} test --workspace"
# prototype: "docargs" = "extcomp cargo ${CARGO_TOOLCHAIN} doc --workspace"
# prototype: "miriargs" = "extcomp cargo +nightly miri test"

require run

declare -xg CARGO_TOOLCHAIN="${CARGO_TOOLCHAIN:-}" ## The rust toolchain that is used for most operations.

function is_cargo_tool_installed ## [+toolchain] <tool> [args..] - Checks if 'cargo <tool>' is installed
{
    ## When [args..] is not present then '--version' is used. Tools that do not handle
    ## '--version' need 'args..' to give some sensible result.
    declare -a toolpart
    # may be prefixed with a '+toolchain'
    if [[ "${1:0:1}" = "+" ]]; then
        toolpart=("$1" "$2")
        shift 2
    else
        toolpart=("$1")
        shift
    fi
    declare -a args
    if [[ "$#" -eq 0 ]]; then
        # when no args are given then default to '--version'
        args=("--version")
    else
        args=("$@")
    fi
    if cargo "${toolpart[@]}" "${args[@]}" &>/dev/null; then
        debug "cargo ${toolpart[*]} is installed"
        return 0
    else
        note "cargo ${toolpart[*]} is not installed"
        return 1
    fi
}

memofn is_cargo_tool_installed

function is_cargo_toolchain_available ## <+toolchain> - Check if a toolchain is available
{
    if [[ $# -lt 1 ]]; then
        error "missing toolchain; usage: is_cargo_toolchain_available <+toolchain>"
        return 2
    fi

    local toolchain="$1"

    if [[ "$toolchain" != +* ]]; then
        toolchain="+$toolchain"
    fi

    if ! command -v cargo >/dev/null; then
        note "cargo is not installed"
        return 1
    fi

    if cargo "$toolchain" --version &>/dev/null; then
        debug "toolchain $toolchain is installed"
        return 0
    else
        note "toolchain $toolchain is not installed"
        return 1
    fi
}

memofn is_cargo_toolchain_available

function cargo_tool_complete ## List available cargo tools/subcommands for completion
{
    ## This function provides completion for cargo tool names (subcommands).
    ## It lists both built-in cargo commands and installed extensions.
    cargo --list 2>/dev/null | awk '/^    [a-z]/{print $1}' | grep -v '^[[:space:]]*$'
}


function cargo_cache_artifacts
{
    if [[ -n "${TESTDIR_PREV:-}" ]] && lock_wait "$TESTDIR_PREV/.bar" ; then
        info "caching artifacts from $TESTDIR_PREV"
        if [[ -d "$TESTDIR_PREV/target" ]]; then
            cp -rluf "$TESTDIR_PREV/target" "./"
        else
            info "no $TESTDIR_PREV/target/ to copy from"
        fi
        # PLANNED:    export CARGO_NET_OFFLINE="true"
        lock_remove "$TESTDIR_PREV/.bar"
    else
        info "not populating /target cache"
    fi
}

## Runs all linters
rule cargo_lint: cargo_check cargo_clippy_strict cargo_fmt_check

function cargo_toolchain
{
    is_cargo_toolchain_available "$1" || return 1
    clause_local "CARGO_TOOLCHAIN='$1'"
}

## <+toolchain> - Clause local change of the toochain used by cargo.
rule --meta cargo_toolchain:

function cargo_fmt_check
{
    # shellcheck disable=2153
    run_test cargo $CARGO_TOOLCHAIN fmt --all --check
}

## Checks whenever the source code is well formatted.
## If available the '+nightly' toolchain is used.
rule cargo_fmt_check: --conclusive 'cargo_toolchain? +nightly' -
rule cargo_fmt_check: --conclusive -

## Builds unit tests.
rule cargo_build_unit_tests: \
     'cargo_test --lib --no-run --quiet' \
     'cargo_test --bins --no-run --quiet'
## Runs unit tests.
rule cargo_test_units: cargo_build_unit_tests \
     'cargo_test --lib --quiet' \
     'cargo_test --bins --quiet'

## Build integration tests
rule cargo_build_integration_tests: \
     'cargo_test --tests --no-run --quiet'

## Run integration tests. This includes the doctests.
rule cargo_test_integrations: cargo_build_integration_tests \
     'cargo_test --tests --quiet' \
     'cargo_test --doc --quiet'

function cargo_fmt
{
    # shellcheck disable=2153
    run_test cargo $CARGO_TOOLCHAIN fmt --all
}

## Run 'cargo fmt'. When available the '+nightly' toolchain is used.
## Note that this will modify the source in place.
rule cargo_fmt: --conclusive 'cargo_toolchain? +nightly' -
rule cargo_fmt: --conclusive -

function cargo_fix ## Run 'cargo fix'.
{
    ## Note that this will modify the source in place.
    # shellcheck disable=2153
    run_test cargo $CARGO_TOOLCHAIN fix --all --allow-dirty
}

function cargo_build ## [buildargs..] - Compile a local package and all of its dependencies
{
    run_test cargo $CARGO_TOOLCHAIN --color "${COLOR:-auto}" build --workspace "$@"
}

function cargo_test ## [testargs..] - Execute all unit and integration tests
{
    run_test cargo $CARGO_TOOLCHAIN --color "${COLOR:-auto}" test --workspace "$@"
}

function cargo_check ## Run 'cargo check'. Testing whenever the source can be compiled.
{
    run_test cargo $CARGO_TOOLCHAIN --color "${COLOR:-auto}" check --workspace
}

function cargo_clippy_errors ## Tests with 'cargo clippy' for errors only
{
    cargo $CARGO_TOOLCHAIN --color "${COLOR:-auto}" clippy 2>&1 | tee /dev/stderr | awk '/error.*: /{exit 1}'
}

function cargo_clippy_strict ## Test with 'cargo clippy' for warnings and errors
{
    cargo $CARGO_TOOLCHAIN --color "${COLOR:-auto}" clippy 2>&1 | tee /dev/stderr | awk '/(warning|error).*: /{exit 1}'
}

function cargo_doc ## [docargs..] - Build this package's and its dependencies' documentation
{
    export RUSTDOCFLAGS="-D rustdoc::broken_intra_doc_links"
    run_test cargo $CARGO_TOOLCHAIN --color "${COLOR:-auto}" doc --workspace "$@"
}

function cargo_update ## Run 'cargo update'
{
    run_test cargo $CARGO_TOOLCHAIN --color "${COLOR:-auto}" update --recursive
}

function cargo_has_unsafe_code ## Checks if the source use 'unsafe' code.
{
    ## This does only a coarse '-Funsafe-code' check, but does not depend on external tools.
    if cargo rustc -- --emit=metadata -Funsafe-code &>/dev/null; then
        debug "no unsafe used"
        return 1
    else
        debug "unsafe used"
        return 0
    fi
}

function cargo_miri ## [miriargs..] - Run tests under miri (memory sanitizer)
{
    run_test cargo +nightly miri test "$@"
}

## When there is unsafe code then run a test under miri supervision.
## Requires the '+nightly' toolchain.
rule cargo_miri: cargo_has_unsafe_code? 'cargo_toolchain +nightly' -

function cargo_check_msrv
{
    run_test --timeout 300 cargo msrv --bisect --ignore-lockfile verify
}

## Checks whenever the 'rust_version' in the manifest is sufficient.
## This check only happens when 'cargo-msrv' is installed, otherwise a warning is printed.
rule cargo_check_msrv: --conclusive 'is_cargo_tool_installed? msrv' -
rule cargo_check_msrv: --conclusive '!is_cargo_tool_installed msrv' -- warn "cargo msrv is not installed"

function cargo_mutants
{
    cargo mutants --colors "${COLOR:-auto}" -j $(( $( nproc || echo 8 ) / 4 )) -t 300 --baseline skip
}

## Run 'cargo mutants' when it is available.
## When 'cargo-mutants' is not installed a warning is printed.
rule cargo_mutants: --conclusive 'is_cargo_tool_installed? mutants' cargo_test_units cargo_test_integrations -
rule cargo_mutants: --conclusive '!is_cargo_tool_installed mutants' -- warn "cargo mutants is not installed"

function cargo_outdated
{
    cargo outdated --color "${COLOR:-auto}" --quiet --exit-code 1
}

## Run 'cargo outedated' when it is available.
## When 'cargo-outdated' is not installed a warning is printed.
rule cargo_outdated: --conclusive 'is_cargo_tool_installed? outdated' -
rule cargo_outdated: --conclusive '!is_cargo_tool_installed outdated' -- warn "cargo outdated is not installed"

function cargo_audit
{
    cargo audit --color "${COLOR:-auto}" -D warnings --stale --quiet
}

## Run 'cargo audit' when it is available.
## When 'cargo-audit' is not installed a warning is printed.
rule cargo_audit: --conclusive 'is_cargo_tool_installed? audit' -
rule cargo_audit: --conclusive '!is_cargo_tool_installed audit' -- warn "cargo audit is not installed"

function cargo_semver_checks
{
    run_test --timeout 300 cargo semver-checks
}

## Run cargo semver-checks to test if a semver for a release is sufficient
rule cargo_semver_checks: --conclusive 'is_cargo_tool_installed? semver-checks' -
rule cargo_semver_checks: --conclusive '!is_cargo_tool_installed semver-checks' -- warn "cargo semver-checks is not installed"

function cargo_bench ## Run 'cargo bench'
{
    cargo --color "${COLOR:-auto}" bench
}

function cargo_publish ## Run 'cargo publish'
{
    cargo --color "${COLOR:-auto}" publish
}

# Define --bare rules for cargo query/predicate functions
rule --bare is_cargo_tool_installed: -
rule --bare is_cargo_toolchain_available: -
rule --bare cargo_has_unsafe_code: -
