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

### The library that implements rules, always loaded

declare -g   BAR_CHECK_LEVEL=0         # less alerting for check dependencies
declare -gx  BAR_KEEP_GOING="${BAR_KEEP_GOING:-1}"  ## Controls keep-going mode: 0 or 1 stops at first failure (default), numbers >= 2 allow that many failures before stopping, non-numeric values (yes/true/etc) continue until exhaustion. In keep-going mode, pure clauses continue executing after failures though failures are still propagated. Non-pure or conclusive clauses halt keep-going within the current rule.
declare -gAx BAR_RULES                 # discovered rules as name:"clauseidx.."
declare -gAx BAR_RULES_RESULT          # results "name":"hashes .." and "#hash":rc
declare -gA  BAR_RULE_FLAGS=(          # [r]everse [a]lways [m]eta [d]isjunct [m]meta [D]isabled [b]are
    [CLEANUP]="r"
    [clause_local]="am"
)
declare -gax BAR_CLAUSES=()            # clause bodies enumerated
declare -gax BAR_CLAUSE_DEPS=()        # dependencies for each clause
declare -ga  BAR_CLAUSE_FLAGS=()       # [c]onclusive [d]efault

function rule () ## [[--rule-flag].. <name>:] [--clause-flag].. [[!]dep[?] [arg]..].. [-- body..] - Defines a rule.
{
    ## See RULE DEFINITION SYNTAX in help for details
    trace "$*"

    local ruleflags=""
    while [[ "$#" -gt 0 ]]; do
        case "$1" in
        --always)
            ruleflags+="a"
            ;;
        --bare)
            ruleflags+="b"
            ;;
        --reverse)
            ruleflags+="r"
            ;;
        --disjunct)
            ruleflags+="d"
            ;;
        --meta)
            ruleflags+="m"
            ;;
        --disable)
            ruleflags+="D"
            ;;
        *)
            break;;
        esac
        shift
    done

    local name="$1"
    if [[ "$name" = *: ]]; then
        name="${name%:}"
        shift
    else
        name=""
    fi

    local clauseflags=""
    while [[ "$#" -gt 0 ]]; do
        case "$1" in
        --conclusive)
            clauseflags+="c"
            ;;
        --default)
            clauseflags+="d"
            ;;
        *)
            break;;
        esac
        shift
    done

    local deps=()
    local has_body=
    local deps_for_cmd=
    while [[ $# -ne 0 ]]; do
        if [[ "$1" = - ]]; then
            shift
            deps_for_cmd=true
            break
        fi
        if [[ "$1" = -- ]]; then
            shift
            has_body=true
            break
        fi
        deps+=("$1")
        shift
    done

    # name given but no deps, no body, make body default to the rule name
    local body=""
    if [[ "${#deps[*]}" = 0 && "$#" == 0 && -n "$name" ]]; then
        body="$name "'"${RULE_ARGS[@]}"'
    elif [[ -n "$deps_for_cmd" && "$#" == 0 && -n "$name" ]]; then
        rule_exists "$name" && die "rule '$name' already defined"
        body="$name "'"${RULE_ARGS[@]}"'
    elif [[ -z "$name" ]]; then
        # default to MAIN when no rule name is given
        name="MAIN"
    elif [[ -z "$deps_for_cmd" && -z "${BAR_RULES[$name]:-}" ]]  && command -v -- "$name" >/dev/null ; then
        note "defining rule '$name' for existing command"
    fi

    if [[ -v BAR_RULES_RESULT[$name] ]]; then
        die "Can not add clause '$name:${deps[*]:+ ${deps[*]}}' after rule got evaluated"
    fi

    local ifs="$IFS"
    IFS=$'\n'
    BAR_CLAUSE_DEPS+=("${deps[*]}")
    IFS="$ifs"

    if [[ "$has_body" = true && $# = 0 ]]; then
        BAR_CLAUSES+=("$(cat)")
    else
        BAR_CLAUSES+=("$body$*")
    fi

    BAR_CLAUSE_FLAGS+=("$clauseflags")
    BAR_RULE_FLAGS["$name"]="${BAR_RULE_FLAGS[$name]:-}$ruleflags"

    local newclause=" $((${#BAR_CLAUSES[@]} - 1))"

    # if this clause is --default then just append it to the end
    if [[ "${BAR_CLAUSE_FLAGS[$newclause]}" = *d* ]]; then
        BAR_RULES["$name"]+="$newclause"
    else # append it before any defaults
        local newclauses=""
        local clause
        for clause in ${BAR_RULES["$name"]:-}; do
            if [[ -n "$newclause" && "${BAR_CLAUSE_FLAGS[$clause]}" = *d* ]]; then
                newclauses+="$newclause"
                newclause=""
            fi
            newclauses+=" $clause"
        done
        newclauses+="$newclause"
        BAR_RULES["$name"]="$newclauses"
    fi
}

function newrule ## <name:> ... - Creates a new rule, deletes the existing one.
{
    ## Used to replace already existing rules/clauses.
    rule_delete "${1%:}"
    rule "$@"
}

function rule_delete ## <name> - Deletes a rule (and all it clauses).
{
    ## Can only be used when the rule was not evaluated yet.
    trace "$*"
    local name="$1"
    if [[ -z "$name" ]]; then
        die "rule_delete requires a rule name"
    fi

    if ! rule_exists "$name"; then
        error "rule '$name' does not exist"
        return 1
    fi

    if [[ -v BAR_RULES_RESULT[$name] ]]; then
        error "Can not delete rule '$name' after rule got evaluated"
        return 1
    fi

    unset "BAR_RULES[$name]"
    unset "BAR_RULE_FLAGS[$name]"
}

function rule_rename ## <oldname> <newname> - renames a rule
{
    ## Can only be used when the rule was not evaluated yet.
    trace "$*"
    local oldname="$1"
    local newname="$2"
    local rc

    if [[ -z "$oldname" || -z "$newname" ]]; then
        die "rule_rename requires two rule names"
    fi

    if ! rule_exists "$oldname"; then
        error "rule '$oldname' does not exist"
        return 1
    fi

    if [[ -v BAR_RULES_RESULT[$oldname] ]]; then
        error "Can not rename rule '$oldname' after it got evaluated"
        return 1
    fi

    BAR_RULES["$newname"]="${BAR_RULES[$oldname]}"
    unset "BAR_RULES[$oldname]"
    BAR_RULE_FLAGS["$newname"]="${BAR_RULE_FLAGS[$oldname]}"
    unset "BAR_RULE_FLAGS[$oldname]"
}

function clause_local ## [var[=value]..] - Makes scalar variables local to a clause.
{
    local var
    local value
    for var in "$@"; do
        value="${var#*=}"
        var="${var%%=*}"
        if [[ -v "$var" ]]; then
            is_scalar "$var" || die "only scalar variables can be clause_local"
            CLAUSE_LOCAL[$var]="${!var}"
        else
            # using some magic here
            CLAUSE_LOCAL[$var]="<<unset_clause_local_variable>>"
        fi
        if [[ -n "$value" ]]; then
            eval "$var='$value'"
        fi
    done
}

function clause_local_restore
{
    local key
    for key in "${!CLAUSE_LOCAL[@]}"; do
        declare -n var="$key"
        local value="${CLAUSE_LOCAL[$key]}"
        if [[ "${value}" = "<<unset_clause_local_variable>>" ]]; then
            unset "$key"
        else
            var="$value"
        fi
    done
    unset CLAUSE_LOCAL
}

function rule_eval ## <name> [args..] - Evaluates a rule
{
    ## Normally this is used by bar itself but it can be used from function and rule bodies
    ## to evaluate rules.
    trace "$*"
    # shellcheck disable=2155
    local rule_id="$(hash_args "$PWD $*")"
    local RULE_NAME="$1"
    shift

    [[ "${BAR_RULE_FLAGS[$RULE_NAME]:-}" == *D* ]] && {
        debug "skipping disabled rule $RULE_NAME"
        return 0
    }

    # shellcheck disable=2034
    local RULE_ARGS=("$@")
    rule_exists "$RULE_NAME" || rule_autoload "$RULE_NAME"

    [[ -v BAR_RULES_RESULT["#$rule_id"] ]] && {
        rc="${BAR_RULES_RESULT[#$rule_id]}"
        if [[ "$rc" == pending ]]; then
            die "recursive rule $RULE_NAME"
        else
            return "$rc"
        fi
    }

    local evaluate_always=
    BAR_RULES_RESULT["#$rule_id"]="pending"
    BAR_RULES_RESULT["$RULE_NAME"]+="$rule_id "
    [[ "${BAR_RULE_FLAGS[$RULE_NAME]:-}" == *a* ]] && evaluate_always=true

    local clauses="${BAR_RULES[$RULE_NAME]:-}"

    if [[ "${BAR_RULE_FLAGS[$RULE_NAME]:-}" = *r* ]]; then
        local rclauses=""
        for n in $clauses; do
            rclauses="$n $rclauses"
        done
        clauses=${rclauses# }
    fi

    # Keep-going state tracking
    local rule_failed=
    local rule_failure_rc=0
    
    # Parse BAR_KEEP_GOING: 0 or 1 = stop at first failure, >= 2 = countdown, non-numeric = unlimited
    if [[ "$BAR_KEEP_GOING" =~ ^[0-9]+$ ]]; then
        if (( BAR_KEEP_GOING <= 1 )); then
            BAR_KEEP_GOING=1  # Stop at first failure (default behavior)
        fi
        # else: use the number as countdown limit
    else
        # Non-numeric means unlimited
        BAR_KEEP_GOING=-1
    fi

    local clause
    for clause in $clauses; do
        [[ "${BAR_RULE_FLAGS[$RULE_NAME]}" != *m* ]] && {
            declare -A CLAUSE_LOCAL=()
        }
        local skip=
        local dep
        local deps=()
        [[ "${#BAR_CLAUSE_DEPS[$clause]}" -gt 0 ]] && readarray -d $'\n' -t deps <<<"${BAR_CLAUSE_DEPS[$clause]}"

        local dep
        for dep in "${deps[@]}"; do
            local depname="${dep%% *}"
            declare -a depargs
            read -r -a depargs <<<"${dep:${#depname}+1}"
            local check=
            local ifexists=

            if [[ "$depname" = *"??" ]]; then
                ifexists=true
                depname="${depname%??}"
            fi
            if [[ "$depname" = *"?" ]]; then
                check=true
                depname="${depname%?}"
            fi
            if [[ "$depname" = "!"* ]]; then
                check=negated
                depname="${depname:1}"
            fi
            if [[ "$depname" = *"~" ]]; then
                [[ -z "$check" ]] || die "~ can not be combined with ! or ?"
                check=unconditional
                depname="${depname%?}"
            fi
            [[ -n "$check" ]] && BAR_CHECK_LEVEL=$((BAR_CHECK_LEVEL+1))

            # Check if bare rule depends on non-bare rule
            if [[ "${BAR_RULE_FLAGS[$RULE_NAME]:-}" == *b* ]]; then
                rule_exists "$depname" || rule_autoload "$depname"
                if [[ "${BAR_RULE_FLAGS[$depname]:-}" != *b* ]]; then
                    die "bare rule '$RULE_NAME' cannot depend on non-bare rule '$depname'"
                fi
            fi

            if [[ -n "$ifexists" ]] && ! rule_exists "$depname"; then
                BAR_CHECK_LEVEL=$((BAR_CHECK_LEVEL+1))
                failure "$RULE_NAME: $dep"
                BAR_CHECK_LEVEL=$((BAR_CHECK_LEVEL-1))
                skip=true
                break
            else
                progress "$RULE_NAME: $dep"
                # shellcheck disable=2155
                local dep_id="$(hash_args "$PWD $depname ${depargs[*]}")"
                local dep_rc="${BAR_RULES_RESULT[$dep_id]:-}"
                if [[ "${BAR_RULE_FLAGS[$depname]-a}" = *a* || -z "$dep_rc" ]]; then
                    dep_rc=0
                    # shellcheck disable=2086
                    rule_eval "$depname" "${depargs[@]}" || dep_rc=$?
                fi

                if [[ "$check" = unconditional ]]; then
                    uncond_call "$dep_rc" "$RULE_NAME: $depname~ ${depargs[*]}"
                    [[ -n "$check" ]] && BAR_CHECK_LEVEL=$((BAR_CHECK_LEVEL-1))
                    continue
                elif [[ "$dep_rc" == 0 && "$check" = negated ]]; then
                    skip=true
                    failure "$RULE_NAME: $dep"
                    [[ -n "$check" ]] && BAR_CHECK_LEVEL=$((BAR_CHECK_LEVEL-1))
                    break
                elif [[ "$dep_rc" != 0 ]]; then
                    if [[ "$check" = true ]]; then
                        failure "$RULE_NAME: $dep"
                        skip=true
                        [[ -n "$check" ]] && BAR_CHECK_LEVEL=$((BAR_CHECK_LEVEL-1))
                        break
                    elif [[ "$check" = negated ]]; then
                        success "$RULE_NAME: $dep"
                        [[ -n "$check" ]] && BAR_CHECK_LEVEL=$((BAR_CHECK_LEVEL-1))
                        continue
                    fi
                    # conclusive fail because of dep failure
                    failure "$RULE_NAME: $dep"
                    [[ -n "$check" ]] && BAR_CHECK_LEVEL=$((BAR_CHECK_LEVEL-1))
                    if [[ -n "$evaluate_always" ]]; then
                        unset "BAR_RULES_RESULT[#$rule_id]"
                    else
                        BAR_RULES_RESULT["#$rule_id"]="$dep_rc"
                    fi
                    [[ "${BAR_RULE_FLAGS[$RULE_NAME]}" != *m* ]] && clause_local_restore
                    return "$dep_rc"
                else
                    success "$RULE_NAME: $dep"
                    [[ -n "$check" ]] && BAR_CHECK_LEVEL=$((BAR_CHECK_LEVEL-1))
                fi
           fi
        done

        rc=0
        if [[ "$skip" != true ]]; then
            # Clear any progress message before executing clause body
            tty_newline
            eval "${BAR_CLAUSES[$clause]}" || rc=$?

            if [[ "${BAR_RULE_FLAGS[$RULE_NAME]}" = *d* ]]; then
                # Disjunct mode: stop at first success
                [[ "$rc" == 0 ]] && break
            else
                # Conjunct mode
                if [[ "$rc" != 0 ]]; then
                    # Clause failed - check if we can keep going
                    local clause_flags="${BAR_CLAUSE_FLAGS[$clause]}"
                    
                    if [[ "$BAR_KEEP_GOING" != 1 && "$clause_flags" != *c* ]]; then
                        # Check if we've exceeded the failure limit
                        if [[ "$BAR_KEEP_GOING" -ge 0 ]]; then
                            ((BAR_KEEP_GOING--))
                            if [[ "$BAR_KEEP_GOING" -eq 0 ]]; then
                                # Exhausted keep-going limit
                                failure "$RULE_NAME"
                                if [[ -n "$evaluate_always" ]]; then
                                    unset "BAR_RULES_RESULT[#$rule_id]"
                                else
                                    BAR_RULES_RESULT["#$rule_id"]="$rc"
                                fi
                                [[ "${BAR_RULE_FLAGS[$RULE_NAME]}" != *m* ]] && clause_local_restore
                                return "$rc"
                            fi
                        fi
                        
                        # Keep-going: clause is not conclusive
                        # Remember failure and continue to next clause
                        failure "$RULE_NAME"
                        rule_failed=true
                        [[ "$rule_failure_rc" == 0 ]] && rule_failure_rc=$rc
                    else
                        # Stop immediately: clause is conclusive or keep-going disabled
                        if [[ -n "$evaluate_always" ]]; then
                            unset "BAR_RULES_RESULT[#$rule_id]"
                        else
                            BAR_RULES_RESULT["#$rule_id"]="$rc"
                        fi
                        [[ "${BAR_RULE_FLAGS[$RULE_NAME]}" != *m* ]] && clause_local_restore
                        return "$rc"
                    fi
                elif [[ "${BAR_CLAUSE_FLAGS[$clause]}" = *c* ]]; then
                    # Conclusive success: stop
                    if [[ -n "$evaluate_always" ]]; then
                        unset "BAR_RULES_RESULT[#$rule_id]"
                    else
                        BAR_RULES_RESULT["#$rule_id"]="$rc"
                    fi
                    [[ "${BAR_RULE_FLAGS[$RULE_NAME]}" != *m* ]] && clause_local_restore
                    return "$rc"
                fi
            fi
        fi
        [[ "${BAR_RULE_FLAGS[$RULE_NAME]}" != *m* ]] && clause_local_restore
    done

    # After all clauses: return with failure if keep-going encountered failures
    if [[ -n "$rule_failed" ]]; then
        if [[ -n "$evaluate_always" ]]; then
            unset "BAR_RULES_RESULT[#$rule_id]"
        else
            BAR_RULES_RESULT["#$rule_id"]="$rule_failure_rc"
        fi
        return "$rule_failure_rc"
    fi

    if [[ -n "$evaluate_always" ]]; then
        unset "BAR_RULES_RESULT[#$rule_id]"
    else
        BAR_RULES_RESULT["#$rule_id"]="$rc"
    fi
    return "$rc"
}

function rule_exists ## <name> - checks if a rule is defined
{
    [[ -v BAR_RULES[$1] ]]
}

function rule_list ## [pattern] - lists all loaded rules or rules matching pattern
{
    ## Because of automatic module loading this list is only a current view for diagnostics.
    local rule
    for rule in $(tr ' ' '\n' <<<"${!BAR_RULES[@]}" | sort); do
        # shellcheck disable=2053
        if [[ "$rule" = ${1:-*} ]]; then
            local clause
            for clause in ${BAR_RULES[$rule]}; do
                local dep
                local deps
                deps=""
                while read -r dep; do
                    [[ -n "$deps" ]] && deps+=" "
                    if [[ "$dep" = *" "* ]]; then
                        deps+="'$dep'"
                    else
                        deps+="$dep"
                    fi
                done <<<"${BAR_CLAUSE_DEPS[$clause]}"
                echo -n "$rule: $deps"
                if [[ -n "${BAR_CLAUSES[$clause]}" ]]; then
                    [[ -n "$deps" ]] && echo -n " "
                    echo -e "-- '${BAR_CLAUSES[$clause]}'"
                else
                    echo
                fi
            done
        fi
        echo
    done | less -SR
}

# Define --bare rules for read-only rule query functions
rule --bare rule_exists: -
rule --bare rule_list: -

# Define --bare rules for logging functions
rule --bare error: -
rule --bare warn: -
rule --bare note: -
rule --bare info: -
rule --bare debug: -
rule --bare trace: -

# Define --bare rules for std_lib query/predicate functions
rule --bare is_scalar: -
rule --bare called_as: -
rule --bare is_command_installed: -
rule --bare bar_now: -
rule --bare hash_args: -
