#!/bin/bash

set -u

source "${IGTOP}/lib/common.sh"

add_dir() {
   local spec=$1 dir
   if dir=$(map_path "$spec") && [[ -d $dir ]]; then
      dirs+=("$dir")
   fi
}

PHASE="$1"
PLAN="$2"
shift 2


# The phase name is the only key runner works from - it is the prefix every
# hook and overlay is discovered by. A layer's own are found by walking the
# build plan and looking under that layer's <stem>.d. Hooks belonging to no
# layer live in these directories, scanned in this order and executed after
# all layers' own hooks, but before any layer's late hooks.
dirs=()
add_dir 'SRCROOT:hooks'          # source tree hooks
add_dir 'IGROOT:builtin/hooks'   # built-in hooks


# Source tree overlays, applied once at the given phase after every layer's
# own content
declare -A TREE_OVERLAYS=(
   [customize]="\
SRCROOT:rootfs-overlay"
)


# Extra positional args a phase hands to everything it runs.
phase_args() {
   local phase=$1

   PHASE_ARGS=()
   case $phase in
      postbuild)
         PHASE_ARGS+=("${IGconf_target_path:?"required for $phase"}")
         ;;
      preimage)
         PHASE_ARGS+=("${IGconf_target_path:?"required for $phase"}")
         PHASE_ARGS+=("${IGconf_image_outputdir:?"required for $phase"}")
         ;;
      postimage)
         PHASE_ARGS+=("${IGconf_image_outputdir:?"required for $phase"}")
         ;;
      sbom)
         PHASE_ARGS+=("${IGconf_target_path:?"required for $phase"}")
         PHASE_ARGS+=("${IGconf_target_dir:?"required for $phase"}")
         ;;
      deploy)
         PHASE_ARGS+=("${IGconf_deploy_dir:?"required for $phase"}")
         [[ -d $IGconf_deploy_dir ]] ||
            die "runner: $phase: $IGconf_deploy_dir does not exist"
         ;;
      *)
   esac
}


# Run one hook from its own directory. An env file, when non-empty and present,
# is supported. Variables named by $envref are exported to the hook alone.
runhook() {
   local envref=$1 envfile=$2 path=$3; shift 3 || true
   local -n _lenv=$envref
   local dir=$(dirname "$path")
   local name=$(basename "$path")

   if [[ ! -x $path ]]; then
      warn "runner: $dir [ignored, non-executable $name]"
      return 0
   fi

   msg "runner: $dir [run $name] [args $@]"
   if [[ -n $envfile && -f $envfile ]]; then
      runenv "$envfile" -C "$dir" "${_lenv[@]}" "./$name" "$@"
   else
      env -C "$dir" "${_lenv[@]}" "./$name" "$@"
   fi
   local rc=$?
   [[ $rc -eq 0 ]] || die "runner: $dir/$name ($rc)"
}


# rsync an overlay directory over a destination
apply_overlay() {
   local src=$1 dest=$2

   [[ -d $src && -n $dest && -d $dest ]] || return 0

   msg "runner: overlay [$src -> $dest]"
   rsync -a --chmod=go-w --exclude='.keep' --exclude='.empty' -- "$src/" "$dest/"
   local rc=$?
   [[ $rc -eq 0 ]] || die "runner: error applying overlay ($rc)"
}


# Run every <phase>* script in a directory, lexicographic order. Subdirectories
# are not traversed and the file extension is ignored.
run_dir_hooks() {
   local dir=$1 phase=$2 envfile=$3 envref=$4; shift 4 || true

   [[ -d $dir ]] || return 0
   compgen -G "${dir}/${phase}"* >/dev/null || return 0

   local script name stem
   for script in "${dir}/${phase}"*; do
      [[ -f $script ]] || continue   # subdirectories are not traversed
      name=$(basename "$script")
      stem="${name%%.*}"
      if [[ $stem =~ ^[a-zA-Z0-9-]+$ ]]; then
         runhook "$envref" "$envfile" "$script" "$@"
      else
         warn "runner: $dir [skipped $name]"
      fi
   done
}


# The environment every per-layer hook is given. Hooks run from their own
# directory, so hooks belonging to a layer must be told where the layer lives
# in order to reach anything it ships. A companion directory is optional, so
# it's only announced when the layer ships one.
layer_vars() {
   local -n _lvars=$1
   local layer=$2 version=$3 stempath=$4 resolved=$5 workdir=$6 uuid=$7
   local vmajor vminor vpatch

   IFS='.' read -r vmajor vminor vpatch <<< "$version"

   _lvars=(
      "LAYER_NAME=$layer"
      "LAYER_VERSION=$version"
      "LAYER_VERSION_MAJOR=$vmajor"
      "LAYER_VERSION_MINOR=$vminor"
      "LAYER_VERSION_PATCH=$vpatch"
      "LAYER_DIR=$(dirname "$stempath")"
      "LAYER_FILE=$resolved"
      "LAYER_WORKDIR=$workdir"
      "LAYER_UUID=$uuid"
   )
   if [[ -d ${stempath}.d ]]; then
      _lvars+=( "LAYER_DOTD=${stempath}.d" )
   fi

   # Private executables
   local dir dirs=()
   for dir in "${stempath}.d/self/${DEB_BUILD_GNU_TYPE:-}/bin" "${stempath}.d/self/host/bin"; do
      [[ -d $dir ]] && dirs+=("$dir") || true
   done
   if (( ${#dirs[@]} )); then
      local selfpath
      selfpath=$(IFS=:; echo "${dirs[*]}")
      _lvars+=( "PATH=${selfpath}:${PATH}" )
      msg "runner: $layer [self PATH $selfpath]"
   fi
}


# Per-layer phased execution
run_layer_step() {
   local layer=$1 version=$2 stempath=$3 resolved=$4 workdir=$5 uuid=$6 phase=$7; shift 7
   local dest=${1:-}
   local -a lvars

   # customize is the only phase runner does not apply an overlay for: those
   # are applied by the layer's synthesised pre-config so that a layer's
   # overlay lands ahead of its own customize-hooks
   [[ $phase == customize ]] || apply_overlay "${stempath}.d/${phase}.overlay" "$dest"

   layer_vars lvars "$layer" "$version" "$stempath" "$resolved" "$workdir" "$uuid"
   run_dir_hooks "${stempath}.d/hooks" "$phase" "${stempath}.d/env" lvars "$@"
}


# A layer's late hooks for a phase. Same env and args as normal hooks, but no overlay.
run_layer_late() {
   local layer=$1 version=$2 stempath=$3 resolved=$4 workdir=$5 uuid=$6 phase=$7; shift 7
   local -a lvars

   layer_vars lvars "$layer" "$version" "$stempath" "$resolved" "$workdir" "$uuid"
   run_dir_hooks "${stempath}.d/hooks/late" "$phase" "${stempath}.d/env" lvars "$@"
}


# Everything that happens in a phase, in order. Each step is skipped when the
# phase has nothing for it, so no phase needs special-casing. $1 is the
# directory the phase operates on, and so the overlay destination.
run_phase() {
   local phase=$1; shift || true
   local dest=${1:-}

   # Layers in build plan order. A layer's overlay is applied before that same
   # layer's hooks, so its hooks may rely on its own overlay content.
   foreach_layer_in_plan run_layer_step "$phase" "$@"

   # Source tree overlays, after every layer's own content
   local spec src
   for spec in ${TREE_OVERLAYS[$phase]:-}; do
      src=$(map_path "$spec") && apply_overlay "$src" "$dest"
   done

   # Hooks belonging to no layer. Could split this so that builtin hooks get
   # additional env settings
   local dir
   local -a novars=()
   for dir in "${dirs[@]}"; do
      run_dir_hooks "$dir" "$phase" "${dir}/env" novars "$@"
   done

   # Late pass: every layer's late hooks, in build plan order, once everything
   # above has completed for this phase
   foreach_layer_in_plan run_layer_late "$phase" "$@"
}


declare -a PHASE_ARGS
phase_args "$PHASE"
args=("${PHASE_ARGS[@]}" "$@")

msg "runner: in $PHASE"
run_phase "$PHASE" "${args[@]}"
msg "runner: out $PHASE"
