#!/bin/bash

function get_from_gh() {
  if [[ "${GH_TOKEN:-}" ]]; then
    # User has provided a Personal Access Token to mitigate rate-limiting issues
    if [[ -z "${oAuthScopes}" ]]; then
      oAuthScopes=$(curl -s -H "Authorization: token $GH_TOKEN" https://api.github.com/users/codertocat -I | grep x-oauth-scopes)
    fi
    if [[ ! "$oAuthScopes" =~ ^x-oauth-scopes:[[:space:]]*$ ]]; then
      # Don't use what you don't have to...
      logError "GH_TOKEN has permissions it doesn't need. Recreate or update this personal access token and disable ALL scopes."
      exit 1
    else
      curl -fsSL -H "Authorization: token $GH_TOKEN" "${@:2}" "$1"
    fi
  else
    curl -fsSL "${@:2}" "$1"
  fi
}

function applyResultsFile() {
  # grab SERVER and export it
  set -a
  # shellcheck disable=SC1090
  source "$1"
  set +a
}

function loadEnvFromFile() {
  local source=${1?Missing required source argument}
  local downloaded=

  if isURL "$source"; then
    mkdir -p /data/.tmp
    downloaded=$(mktemp -p /data/.tmp)
    log "Downloading env file from $source"
    if ! get -o "$downloaded" "$source"; then
      logError "Failed to download env file from $source"
      rm -f "$downloaded"
      return 1
    fi
    log "Loading env vars from $source"
    applyResultsFile "$downloaded"
    rm -f "$downloaded"
  elif [ -f "$source" ]; then
    log "Loading env vars from $source"
    applyResultsFile "$source"
  else
    logError "Env file not found: $source"
    return 1
  fi
}

function loadEnvFromArchive() {
  local source=${1?Missing required source argument}
  local entry=${2:-.env}
  local archive=
  local downloaded=
  local tmpdir
  local rc=0

  mkdir -p /data/.tmp

  if isURL "$source"; then
    downloaded=$(mktemp -p /data/.tmp)
    log "Downloading archive from $source"
    if ! get -o "$downloaded" "$source"; then
      logError "Failed to download archive from $source"
      rm -f "$downloaded"
      return 1
    fi
    archive=$downloaded
  elif [ -f "$source" ]; then
    archive=$source
  else
    logError "Archive not found: $source"
    return 1
  fi

  tmpdir=$(mktemp -d -p /data/.tmp)
  if extract "$archive" "$tmpdir" "$entry" && [ -f "$tmpdir/$entry" ]; then
    log "Loading env vars from '$entry' in $source"
    applyResultsFile "$tmpdir/$entry"
  else
    logError "Failed to load env entry '$entry' from $source"
    rc=1
  fi

  rm -rf "$tmpdir"
  [[ -n "$downloaded" ]] && rm -f "$downloaded"
  return $rc
}

function loadEnvFromGenericPack() {
  : "${GENERIC_PACKS:=${GENERIC_PACK:-}}"
  : "${GENERIC_PACKS_PREFIX:=}"
  : "${GENERIC_PACKS_SUFFIX:=}"

  if [[ -z "${GENERIC_PACKS}" ]]; then
    logWarning "LOAD_ENV_FROM_GENERIC_PACK is set but GENERIC_PACK(S) is empty"
    return 0
  fi

  mkdir -p /data/.tmp
  IFS=',' read -ra packs <<< "${GENERIC_PACKS}"
  local loaded=0
  local pack packEntry packFile tmpdir
  for packEntry in "${packs[@]}"; do
    pack="${GENERIC_PACKS_PREFIX}${packEntry}${GENERIC_PACKS_SUFFIX}"
    if isURL "$pack"; then
      mkdir -p /data/packs
      if ! packFile=$(get -o /data/packs --output-filename --skip-up-to-date "$pack"); then
        logError "Failed to download generic pack $pack"
        return 1
      fi
    else
      packFile=$pack
    fi

    if [[ ! -f "$packFile" ]]; then
      logError "Generic pack not found: $packFile"
      return 1
    fi

    tmpdir=$(mktemp -d -p /data/.tmp)
    # Packs without a .env are valid — silently skip; the unpack step still applies them.
    if extract "$packFile" "$tmpdir" .env 2>/dev/null && [ -f "$tmpdir/.env" ]; then
      log "Loading env vars from .env in $pack"
      applyResultsFile "$tmpdir/.env"
      loaded=$((loaded + 1))
    fi
    rm -rf "$tmpdir"
  done

  if (( loaded == 0 )); then
    logWarning "LOAD_ENV_FROM_GENERIC_PACK is set but no pack in GENERIC_PACK(S) contained a .env"
  fi
}

function join_by() {
  local d=$1
  shift
  echo -n "$1"
  shift
  printf "%s" "${@/#/$d}"
}

function get_major_version() {
  version=$1
  echo "$version" | cut -d. -f 1-2
}

function isPercentage() {
  local value=$1
  
  [[ $value =~ ^[0-9]+(\.[0-9]+)?\s*%$ ]]
}

function getPercentageValue() {
  local value=$1
  if [[ "$value" =~ ^([0-9]+(\.[0-9]+)?)\s*%$ ]]; then
    echo "${BASH_REMATCH[1]}"
    return 0
  else
    logError "Value '$value' is not a valid percentage."
    return 1
  fi
}

function isURL() {
  local value=$1

  [[ $value =~ ^(https?|ftp):// ]]
}

function isValidFileURL() {
  suffix=${1:?Missing required suffix arg}
  url=${2:?Missing required url arg}

  [[ "$url" =~ ^http.*://.*\.${suffix}(\?.*)?$ ]]
}

function resolveEffectiveUrl() {
  url="${1:?Missing required url argument}"
  if ! curl -Ls -o /dev/null -w "%{url_effective}" "$url"; then
    logError "Failed to resolve effective URL from $url"
    exit 2
  fi
}

function getFilenameFromUrl() {
  url="${1:?Missing required url argument}"
  strippedOfQuery="${url%\?*}"
  basename "$strippedOfQuery"
}

function isTrue() {
  case "${1,,}" in
  true | yes | on | 1)
    return 0
    ;;
  *)
    return 1
    ;;
  esac
}

function isFalse() {
  case "${1,,}" in
  false | no | off | 0)
    return 0
    ;;
  *)
    return 1
    ;;
  esac
}

function isDebugging() {
  isTrue "${DEBUG:-false}"
}

function handleDebugMode() {
  if isDebugging; then
    set -x
  fi
}

function debug() {
  if isDebugging; then
    log "DEBUG: $*"
  fi
}

function logn() {
  echo -n "[init] $*"
}

# Use as
# cmd | applyLogPrefix
function applyLogPrefix() {
  sed 's/^/[init] /'
}

function log() {
  local oldState
  # The  return  status  when listing options is zero if all optnames are enabled, non- zero otherwise.
  oldState=$(shopt -po xtrace || true)
  shopt -u -o xtrace
  ts=
  if isDebugging || isTrue "${LOG_TIMESTAMP:-false}"; then
    ts=" $(date --rfc-3339=seconds)"
  fi
  echo -e "[init]${ts} $*"
  eval "$oldState"
}

# Refer to https://unix.stackexchange.com/a/10065/102376
function isTerminal() {
  if test -t 1; then
    # see if it supports colors...
    ncolors=$(tput colors)
    test -n "$ncolors" && test "$ncolors" -ge 8
  else
    return 1
  fi
}

errorLogTag="[ERROR]"
warningLogTag="[WARN]"

if isTerminal; then
  normal="$(tput sgr0)"
  red="$(tput setaf 1)"
  yellow="$(tput setaf 3)"
  function getErrorColoredLogString() {
    echo "${red}$errorLogTag $* ${normal}"
  }
  function getWarningColoredLogString() {
    echo "${yellow}$warningLogTag $* ${normal}"
  }
else
  function getErrorColoredLogString() {
    echo "$errorLogTag $*"
  }
  function getWarningColoredLogString() {
    echo "$warningLogTag $*"
  }
fi

function error() {
  echo -e "$(getErrorColoredLogString "$*")"
}

function logError() {
  if isDebugging; then
    set +x
  fi
  log "$(getErrorColoredLogString "$*")"
  if isDebugging; then
    set -x
  fi
}

function warning() {
  if isDebugging; then
    set +x
  fi
  echo -e "$(getWarningColoredLogString "$*")"
  if isDebugging; then
    set -x
  fi
}

function logWarning() {
  log "$(getWarningColoredLogString "$*")"
}

function isNumeric() {
  [[ $1 =~ ^[0-9]+$ ]]
}

function isNumericElseSetToDefault() {
  local var_name="$1"
  local default_value="$2"
  
  if ! isNumeric ${!var_name} ; then
    eval "$var_name=$default_value"
    export "$var_name"
    logWarning "$var_name is not numeric, set to $default_value (seconds)"
  fi
}

function checkIfNotZeroElseSetToDefault() {
  local var_name="$1"
  local default_value="$2"
  if [ "${!var_name}" -eq "0" ] ; then
    eval "$var_name=$default_value"
    export "$var_name"
    logWarning "$var_name must not be 0, set to $default_value (seconds)"
  fi
}

function logAutopause() {
  echo "[Autopause loop] $*"
}

function logAutopauseAction() {
  echo "[$(date -Iseconds)] [Autopause] $*"
}

function logAutostop() {
  echo "[Autostop loop] $*"
}

function logAutostopAction() {
  echo "[$(date -Iseconds)] [Autostop] $*"
}

function logRcon() {
  echo "[Rcon loop] $*"
}

function normalizeMemSize() {
  local scale=1
  local mem=1
  case ${1,,} in
  *k)
    scale=1024
    ;;
  *m)
    scale=1048576
    ;;
  *g)
    scale=1073741824
    ;;
  *%)
    # Get system memory
    if [ -f "/sys/fs/cgroup/memory/memory.limit_in_bytes" ]; then
    mem=$( cat /sys/fs/cgroup/memory/memory.limit_in_bytes )
    elif [ -f "/sys/fs/cgroup/memory.max" ]; then
    mem=$( cat /sys/fs/cgroup/memory.max )
    fi
    # Scale is used to transform percentages into decimals (eg. 60 -> 0.6)
    scale=0.01
    ;;
  esac

  val=${1:0:-1}
  echo $((val * scale * mem))
}

function compare_version() {
  local left_version=$1
  local comparison=$2
  local right_version=$3

  if [[ -z "$left_version" ]]; then
    echo "Left version is required"
    return 1
  fi

  if [[ -z "$right_version" ]]; then
    echo "Right version is required"
    return 1
  fi

  # Handle version channels ('a', 'b', or numeric)
  if [[ $left_version == a* ]]; then
    left_version=${left_version:1}
    left_version_channel=1
  elif [[ $left_version == b* ]]; then
    left_version=${left_version:1}
    left_version_channel=2
  else 
    left_version_channel=3    
  fi
  if [[ $right_version == a* ]]; then
    right_version=${right_version:1}
    right_version_channel=1
  elif [[ $right_version == b* ]]; then
    right_version=${right_version:1}
    right_version_channel=2
  else 
    right_version_channel=3    
  fi

  if [[ $comparison == "lt" && $left_version_channel < $right_version_channel ]]; then
      return 0
  elif [[ $comparison == "lt" && $left_version_channel > $right_version_channel ]]; then
      return 1
  elif [[ $comparison == "gt" && $left_version_channel > $right_version_channel ]]; then
      return 0
  elif [[ $comparison == "gt" && $left_version_channel < $right_version_channel ]]; then
      return 1
  elif [[ $comparison == "le" && $left_version_channel < $right_version_channel ]]; then
      return 0
  elif [[ $comparison == "le" && $left_version_channel == $right_version_channel ]]; then
      return 0
  elif [[ $comparison == "ge" && $left_version_channel > $right_version_channel ]]; then
      return 0
  elif [[ $comparison == "ge" && $left_version_channel == $right_version_channel ]]; then
      return 0
  elif [[ $comparison == "eq" && $left_version_channel == $right_version_channel ]]; then
      return 0
  fi

# Compare the versions using sort -V
  local result=1

  case $comparison in
      "lt")
          if [[ $(echo -e "$left_version\n$right_version" | sort -V | head -n1) == "$left_version" && "$left_version" != "$right_version" ]]; then
              result=0
          fi
          ;;
      "le")
          if [[ $(echo -e "$left_version\n$right_version" | sort -V | head -n1) == "$left_version" ]]; then
              result=0
          fi
          ;;
      "eq")
          if [[ "$left_version" == "$right_version" ]]; then
              result=0
          fi
          ;;
      "ge")
          if [[ $(echo -e "$left_version\n$right_version" | sort -V | tail -n1) == "$left_version" ]]; then
              result=0
          fi
          ;;
      "gt")
          if [[ $(echo -e "$left_version\n$right_version" | sort -V | tail -n1) == "$left_version" && "$left_version" != "$right_version" ]]; then
              result=0
          fi
          ;;
      *)
          echo "Unsupported comparison operator: $comparison"
          return 1
          ;;
  esac

  return $result
}

function versionLessThan() {
  local oldState
  # The  return  status  when listing options is zero if all optnames are enabled, non- zero otherwise.
  oldState=$(shopt -po xtrace || true)
  shopt -u -o xtrace

  eval "$oldState"

  # Verify strict mode because it might be enabled
  compare_version "${VERSION}" "lt" "${1?}"
}

function writeEula() {
  if ! echo "# Generated via Docker
# $(date)
eula=${EULA,,}
" >/data/eula.txt; then
    logError "Unable to write eula to /data. Please make sure attached directory is writable by uid=${UID}"
    exit 2
  fi
}

function removeOldMods {
  if [ -d "$1" ]; then
    log "Removing old mods including='${REMOVE_OLD_MODS_INCLUDE}' excluding='${REMOVE_OLD_MODS_EXCLUDE}' up to depth=${REMOVE_OLD_MODS_DEPTH}"
    args=(
      --delete
      --type file
      --min-depth=1 --max-depth "${REMOVE_OLD_MODS_DEPTH}"
      --name "${REMOVE_OLD_MODS_INCLUDE}"
      --exclude-name "${REMOVE_OLD_MODS_EXCLUDE}"
    )
    if ! isDebugging; then
      args+=(--quiet)
    fi
    mc-image-helper find "${args[@]}" "$1"
  fi
}

function get() {
  mc-image-helper get "$@"
}

function get_silent() {
  local flags=(-s)
  if isTrue "${DEBUG_GET:-false}"; then
    flags+=("--debug")
  fi
  mc-image-helper "${flags[@]}" get "$@"
}

function isFamily() {
  for f in "${@}"; do
    if [[ ${FAMILY^^} == "${f^^}" ]]; then
      return 0
    fi
  done
  return 1
}

function isType() {
  for t in "${@}"; do
    # shellcheck disable=SC2153
    if [[ ${TYPE^^} == "${t^^}" ]]; then
      return 0
    fi
  done
  return 1
}

function extract() {
  src=${1?}
  destDir=${2?}
  shift 2
  # remaining args are paths within the archive to extract; if none, extract everything

  type=$(file -b --mime-type "${src}")
  if [[ "$type" == application/octet-stream ]]; then
      logWarning "Detected non-specific file type $type for $src"
      case "$src" in
        *.zip)
          log "Assuming zip from extension"
          type=application/zip
          ;;
        # otherwise fall through to
      esac
  fi

  case "${type}" in
  application/zip)
    unzip -o -q -d "${destDir}" "${src}" "$@"
    ;;
  application/x-tar | application/gzip | application/x-gzip | application/x-bzip2)
    tar -C "${destDir}" -xf "${src}" "$@"
    ;;
  application/zstd | application/x-zstd)
    tar -C "${destDir}" --use-compress-program=unzstd -xf "${src}" "$@"
    ;;
  *)
    logError "Unsupported archive type: $type"
    return 1
    ;;
  esac
}

function getDistro() {
  grep -E "^ID=" /etc/os-release | cut -d= -f2 | sed -e 's/"//g'
}

function checkSum() {
  local sum_file=${1?}

  # Get distro
  distro=$(getDistro)

  case "${distro}" in
  debian | ubuntu | ol)
    sha1sum -c "${sum_file}" --status 2>/dev/null && return 0
    ;;
  alpine)
    sha1sum -c "${sum_file}" -s 2>/dev/null && return 0
    ;;
  *)
    return 1
    ;;
  esac
}

function usesMods() {
  if isTrue "${USES_MODS:-}"; then
    return 0

  else
    case "$FAMILY" in
    FORGE | FABRIC | HYBRID | SPONGE)
      return 0
      ;;
    esac
    return 1

  fi
}

function usesPlugins() {
  if isTrue "${USES_PLUGINS:-}"; then
    return 0

  else
    case "$FAMILY" in
    SPIGOT | HYBRID)
      return 0
      ;;
    esac
    return 1

  fi
}

function resolveVersion() {
  givenVersion="$VERSION"
  # shellcheck disable=SC2153
  if ! VERSION=$(mc-image-helper resolve-minecraft-version "$VERSION"); then
    exit 2
  fi
  log "Resolved version given ${givenVersion} into ${VERSION}"
}

function resolveFamily() {
  case "$TYPE" in
  PAPER | SPIGOT | BUKKIT | CANYON | PUFFERFISH | PURPUR)
    FAMILY=SPIGOT
    ;;
  FORGE)
    FAMILY=FORGE
    ;;
  FABRIC | QUILT)
    FAMILY=FABRIC
    ;;
  esac
  export FAMILY
}

function ensureRemoveAllModsOff() {
  reason=${1?}

  if isTrue "${REMOVE_OLD_MODS:-false}"; then
    logWarning "Using REMOVE_OLD_MODS interferes with $reason -- it is now disabled"
    REMOVE_OLD_MODS=false
  fi
}

function buildDownloadList() {
  repoUrl=${1?}
  version=${2?}
  shift 2
  result=
  for c in "${@}"; do
    if [[ $result ]]; then
      result+=","
    fi
    result+="${repoUrl}/${version}/$c"
  done
  echo "$result"
}

function firstArrayElement {
  local -n a="$1"
  if (( ${#a[@]} )); then
    echo "${a[0]}"
  fi
}

function shiftArray {
  local -n a="$1"
  if (( ${#a[@]} )); then
    a=("${a[@]:1}")
  fi
}
