#!/bin/bash
# In the current directory:
# * start a documentation repository or
# * update the existing documentation boilerplate.
# The initial setup asks a few questions (which type of document, which
# variant). Mere updates are usually completely automatic.
# Usage:
# $ $0                               # Set up/update a documentation repository
# $ $0 --help                        # This screen
# $ DOCKIT_REPO=https://github.com/org/repo  # Use non-default source repository
# $ DOCKIT_BRANCH=different_branch   # Use non-default source branch

# project name
self="Doc Kit"

# project version number
version=0.5

repo_base=${DOCKIT_REPO:-https://github.com/openSUSE/doc-kit}
repo="$repo_base/raw"
branch=${DOCKIT_BRANCH:-main}
indexfile="KITS"
fileextension=".MANIFEST"
config="doc-kit.conf"


charclass_variants="[-_a-zA-Z0-9]+"
charclass_files_strict="[-_.a-zA-Z0-9][-_/a-zA-Z0-9][-_./a-zA-Z0-9]+"
charclass_files_relaxed="[-_.a-zA-Z0-9][-_./a-zA-Z0-9]+"


out() {
  # $1 = message
  # $2 = error code (optional)
  errorcode=1
  [[ "$2" ]] && errorcode=$2
  >&2 echo -e "[err] $1"
  exit $errorcode
}

geturl() {
  # $1 = file name
  wget -qO - "${repo}/${branch}/$1"
}

saveurl() {
  # $1 = file name at origin
  # $2 = file name to save under
  echo "Saving $1 to $2..."
  if [[ $(echo "$2" | grep -o '/') ]]; then
    dir=$(dirname "$2")
    if [[ -e "$dir" ]] && [[ ! -d "$dir" ]]; then
      out "$dir exists already but is a file. $dir needs to be a directory."
    fi
    [[ -d "$dir" ]] || mkdir -p "$dir"
  fi
  wget -q -O "$2" "${repo}/${branch}/$1"
}

shortenattop() {
  # $1 - text to cut the first line off of
  echo -e "$1" | tail -$(($(echo "$1" | wc -l) - 1))
}

sanitizevariants() {
  # $1 - variants string
  echo -e "$@" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed -r 's/ $//'
}

subsetof() {
  # $1 original set (separated by spaces or newlines)
  # $2 potential subset (separated by spaces or newlines)
  # $3 allow empty subset: 0 - no (default), 1 - yes
  original=$(echo -e "$1" |  tr ' ' '\n' | sort -u)
  potential=$(echo -e "$2" | tr ' ' '\n' | sort -u)
  emptysubset=0
  [[ ! "$potential" ]] && emptysubset=1
  while [[ "$potential" ]]; do
    local line=$(echo "$potential" | head -1)
    [[ $(echo -e "$original" | grep "^$line$") ]] || break
    potential=$(shortenattop "$potential")
  done
  if [[ ! "$potential" ]] && [[ $3 -eq 1 ]]; then
    echo "is_subset"
  elif [[ ! "$potential" ]] && [[ $emptysubset -eq 0 ]]; then
    echo "is_subset"
  fi
}

decide() {
  # $1 = prompt
  # $2 = valid answers, separated by newlines
  # $3 = allow multiple choice: 0 - no (default), 1 - yes
  # $4 = allow empty answer: 0 - no (default), 1 - yes
  allowempty=0
  [[ "$4" -eq 1 ]] && allowempty=1
  decision="&INITIALNONSENSEVALUE;"
  while [[ ! $(subsetof "$2" "$decision" "$allowempty") ]]; do
    echo -n "$1"
    [[ $3 -eq 1 ]] && echo -n " (separate multiple values with spaces)"
    echo -n ": "
    read decision
    [[ ! $3 -eq 1 ]] && decision=$(echo "$decision" | sed -r 's/ /#@#/g')
    decision=$(echo "$decision" | sed -r 's/[^-_+.a-z0-9 ]//g' | tr ' ' '\n')
  done
}

checkmanifestcompatibility() {
  # $1 - KITS/*.MANIFEST file with compatibility header in first two lines,
  #      example of a compatibility header:
  #        v0.2
  #        --
  candidate=$(echo -e "$1" | head -2)
  # Let's check line length anyway, helps to exclude cases where we get a single
  # line only.
  if [[ $(echo -e "$candidate" | wc -l) -ne 2 ]] || \
    [[ $(echo -e "$candidate" | head -1 | sed -r -n '/^v[0-9]+(\.[0-9]+)+$/ !p') ]] || \
    [[ $(echo -e "$candidate" | tail -1) != '--' ]]; then
    out "Manifest header is invalid."
  fi
  headerversion=$(echo -e "$candidate" | head -1 | sed 's/^v//')
  if [[ $(echo -e "$version\n$headerversion" | sort -V | head -1) != "$headerversion" ]]; then
    out "Header version $headerversion is unsupported in $self $version."
  fi
}

preparemanifest() {
  # $1 manifest
  echo -e "$1" | tr '\t' ' ' | sed -r \
    -e 's/^ +//' \
    -e 's/ +$//' \
    -e 's/^#.*$//' \
    | sed -n '/./ p'
}

validatemanifest() {
  # $1 = manifest
  # $2 = success message: 0 - hide (default), 1 - show
  # $3 = error mode: 0 - continue on error, 1 - quit on error (default)
  if  [[ ! $(echo -e "$1" | sed -n '/./ p') ]]; then
    out "Manifest file empty."
  fi
  validatedmanifest=$(preparemanifest "$1" | sed -r \
    -e "s/^(initial|always)(\(${charclass_variants}( +${charclass_variants})*\))?: +(${charclass_files_relaxed} +-> +)?${charclass_files_strict}\$//" \
    | sed -n '/./ p')

  if [[ "$validatedmanifest" ]]; then
    echo -e "Manifest validation failed. Failing lines:\n"
    echo -e "$validatedmanifest"
    [[ ! $3 -eq 0 ]] && exit 1
  elif [[ "$2" -eq 1 ]]; then
    echo "Manifest validation successful."
  fi
}


if [[ $1 == '--help' ]] || [[ $1 == '-h' ]]; then
  sed -rn '/#!/{n; p; :loop n; p; /^[ \t]*$/q; b loop}' $0 | sed -r -e 's/^# ?//' -e "s/\\\$0/$(basename $0)/"
  exit
fi

if [[ $1 == '--version' ]] || [[ $1 == '-v' ]]; then
  echo "$(basename $0) $version"
fi

configtype=
configvariants=
manifest=
initial=0
if [[ -f "$config" ]]; then # FIXME: validate config
  configtype=$(sed -r -n 's/^ *type: *// p' "$config")
  configvariants=$(sanitizevariants $(sed -r -n 's/^ *variant: *// p' "$config"))
  manifest=$(geturl "${configtype}${fileextension}")
  checkmanifestcompatibility "$manifest"
  manifest=$(echo -e "$manifest" | tail -n +3)
else
  echo "No $config file found: Running initial setup."
  manifestindex=$(wget -qO - "${repo}/${branch}/${indexfile}")
  checkmanifestcompatibility "$manifestindex"
  manifestindex=$(echo -e "$manifestindex" | tail -n +3 | sort -u)
  echo "Available document types:"
  echo -e "$manifestindex" | tr '\n' ' ' | sed -r 's/ $/\n/'
  echo ""
  decide "Specify the desired document type" "$manifestindex" 0 0
  configtype="$decision"
  echo ""
  manifest=$(geturl "${configtype}${fileextension}")
  checkmanifestcompatibility "$manifest"
  manifest=$(echo -e "$manifest" | tail -n +3)
  validatemanifest "$manifest" || out "Manifest validation failed" 1
  availablevariants=$(preparemanifest "$manifest" | sed -r \
    -e 's/\)?:.+$//' \
    -e 's/^[^\(]+\(?//' \
    | tr ' ' '\n' | sed -n '/./ p' | sort -u)
  if [[ $availablevariants != '' ]]; then
    echo "Available variants:"
    echo "$availablevariants" | tr '\n' ' ' | sed -r 's/ $/\n/'
    echo ""
    decide "Specify the desired variant(s)" "$availablevariants" 1 1
    configvariants=$(sanitizevariants "$decision")
  else
    echo "This document type has no variants."
    configvariants=''
  fi
  echo -e "type: $configtype\nvariant: $configvariants" > "$config"
  echo "Wrote $config file into current directory. Make sure to commit."
  initial=1
fi

validatemanifest "$manifest" || out "Manifest validation failed" 1

cleanmanifest=$(preparemanifest "$manifest")

cachedconfig=$(cat "$config")

forcereplace=0
while [[ "$cleanmanifest" ]]; do
  line=$(echo -e "$cleanmanifest" | head -1)

  type=$(echo "$line" | grep -oP '^(initial|always)')
  variants=$(sanitizevariants $(echo "$line" | grep -oP '^[^:]+' | sed -r -e 's/^[a-z]+\(?//' -e 's/\)$//'))
  from=$(echo "$line" | sed -r 's/^[^:]+: +//' | grep -oP "^${charclass_files_relaxed}")
  to=$(echo "$line" | grep -oP "${charclass_files_relaxed}+\$")

  if [[ "$type" = "always" ]]; then
    if [[ $(subsetof "$configvariants" "$variants" 1 0) ]]; then
      # FIXME: Adding $to here is dangerous (though file names are checked at
      # some point to only contain $charclass_files_relaxed.)
      oldsha=$(echo -e "$cachedconfig" | sed -r -n 's^file: +([0-9a-f]+) +'"$to"'$^\1^ p')
      # If the file does not already exist (e.g. initial setup or new file),
      # sha1sum will expectedly write out an error message, hence 2>/dev/null
      downstreamsha=$(sha1sum "$to" 2>/dev/null | cut -d ' ' -f 1)
      if [[ "$oldsha" == "$downstreamsha" ]] || \
        [[ ! "$downstreamsha" ]] || [[ ! "$oldsha" ]] || \
        [[ $forcereplace -eq 1 ]]; then
        saveurl "$configtype/$from" "$to"
        newfiles+="\nfile: "$(sha1sum "$to")
      else
        echo "Someone ignored the warnings in the file $to and changed it downstream since the last update via $self."
        decide "Replace file [r] or skip file temporarily [s]" "r s" 0 0
        if [[ "$decision" == "r" ]]; then
          forcereplace=1
          continue
        else
          newfiles+="\nfile: $oldsha $to"
        fi
      fi
    fi
  elif [[ $initial = 1 ]] && [[ "$type" = "initial" ]]; then
    if [[ $(subsetof "$configvariants" "$variants" 1 0) ]]; then
      saveurl "$configtype/$from" "$to"
      newfiles+="\nfile: "$(sha1sum "$to")
    fi
  fi

  cleanmanifest=$(shortenattop "$cleanmanifest")
  forcereplace=0
done

{
  echo -e "$cachedconfig" | head -2
  echo -e "$newfiles"
} > "$config"
