#!/bin/bash -e
# Copyright (c) 2018 Nicolas Mailhot <nim@fedoraproject.org>,
#                    Jan Chaloupka <jchaloup@redhat.com>
# This file is distributed under the terms of GNU GPL license version 3, or
# any later version.

usage() {
cat >&2 << EOF_USAGE
Usage: $0 <action> [ [-h] ]
                   [ [-i <go import path> ] ]
                   [ [-y] ] [ [-p <prefix>] [-g <go path>] ]
                   [ [-w] ] [ [-b <go build path>] ]
                   [ [-d <directory>] [-t <tree root>] [-r <regex>] ]
                   [ [-e <extension>] [-s <sourcedir>] [-o <file>] <file> ]
                   [ [-v <version>] ] [ [-a <attribute>] ]

<action>             should be one of: install, check, provides, requires

Most actions accept the same set of arguments, and will silently ignore those
that do not apply to a specific action. Unless specified otherwise, all
arguments are optional.

“install”-specific arguments:

-i <go import path>  is a mandatory argument
-c <hexsha>          add commit line into the .goipath
-e <extension>       add files ending in <extension> to the default
                     installation set,
                     can be specified several times
-s <sourcedir>       install files from the specified source directory, not
                     from the current directory
-o <file>            output file lists to file,
                     default value if not set: devel.file-list
<file>               add <file> to the default installation set,
                     can be specified several times

“check”-specific arguments:

-i <go import path>  is a mandatory argument
-y                   check the files installed in the system Go path, not the
                     files in the current work directory,
                     this option is usually used with -p and -g,
-w                   check the files in the current work directory, not the
                     files installed in the system Go path,
                     this option is usually used with -b,
                     this is the default check mode if neither -y nor -w are
                     specified

“provides”-specific arguments:

-v <version string>: tag the provides with <version string>
-a <attribute>:      an attribute to add to the provides, for example
                     -a "(commit=XXXX)"
                     -a "(branch=YYYY)"
                     -a "(tag=rx.y.z-alpha1)"
                     can be specified several times

Common arguments:

-i <go import path>  a Go import path of the target package,
                     can be specified several times,
                     mandatory for: install and check,
                     ignored by: provides and requires
-h                   print this help
-p <prefix>:         an optionnal prefix path such as %{buildroot}
-g <go path>:        the root of the Go source tree
                     default value if not set: /usr/share/gocode
-b <go build path>   sets the GO_BUILD_PATH work directory,
                     it will be created if not already existing,
                     default value if not set: \$PWD/_build
                     used by: install and check,
                     ignored by: provides and requires
-d <directory>       a directory that should be ignored during processing,
                     relative to the import path root,
                     non recursive,
                     can be specified several times
-t <tree root>       the root of a directory tree that should be ignored during
                     processing,
                     relative to the import path root,
                     recursive,
                     can be specified several times
-r <regex>           a regex matching elements that should be ignored during
                     processing,
                     relative to the import path root,
                     can be specified several times
EOF_USAGE
exit 1
}

action=''
version=''
prefix=''
checkin="workdir"
sourcedir="${PWD}"
GO_BUILD_PATH="${GO_BUILD_BATH:-${PWD}/_build}"
gopath=/usr/share/gocode
filelist='devel.file-list'
commit=''
goipathes=()
_last_goipath=''
declare -A attributes
declare -A golistflags

if [[ $# -eq 0 ]] ; then
  usage
else case $1 in
    install|check|provides|requires) action=$1 ;;
    *)                               usage     ;;
  esac
fi

shift

if ! options=$(getopt -n $0 -o hi:yp:g:wb:d:t:r:e:o:v:a:c: \
                      -l help,go-import-path: \
                      -l system-files,prefix:,go-path: \
                      -l workdir-files,go-build-path: \
                      -l ignore-directory: \
                      -l ignore-tree: \
                      -l ignore-regex: \
                      -l include-extension:,output: \
                      -l version:,attribute: \
                      -l commit: \
                      -- "$@")
then
    usage
fi

eval set -- "$options"

while [ $# -gt 0 ] ; do
  case $1 in
    -h|--help)                      usage                               ;;
    -i|--go-import-path)            goipathes+=( "$2" ); _last_goipath="${2}"          ; shift;;
    -c|--commit)                    commit="$2"                  ; shift;;
    -y|--system-files)              checkin="system"                    ;;
    -p|--prefix)                    prefix=$(realpath -sm "$2")  ; shift;;
    -g|--go-path)                   gopath="$2"                  ; shift;;
    -w|--workdir-files)             checkin="workdir"                   ;;
    -b|--go-build-path)             GO_BUILD_PATH="$2"           ; shift;;
    -d|--ignore-directory)          dir="${_last_goipath}/$2"; golistflags[$_last_goipath]="${golistflags[$_last_goipath]} -d ${dir%/.}" ; shift;;
    -t|--ignore-tree)               golistflags[$_last_goipath]="${golistflags[$_last_goipath]} -t ${_last_goipath}/$2" ; shift;;
    -r|--ignore-regex)              golistflags[$_last_goipath]="${golistflags[$_last_goipath]} -r $2" ; shift;;
    -e|--include-extension)         golistflags[$_last_goipath]="${golistflags[$_last_goipath]} -e $2" ; shift;;
    -s|--source-dir)                sourcedir="$2"               ; shift;;
    -o|--output)                    filelist="$2"                ; shift;;
    -v|--version)                   version="$2"                 ; shift;;
    -a|--attribute)                 IFS=')' read -r -a newattrs <<< "$2"
                                      for index in "${!newattrs[@]}" ; do
                                        newattrs[index]=${newattrs[index]#\(}
                                        attributes[${newattrs[index]%%=*}]=${newattrs[index]#*=}
                                    done                         ; shift;;
    (--)          shift; break ;;
    (-*)          usage ;;
    (*)           break ;;
  esac
  shift
done

installfile() {
local goipath="${1}"
local file="${2}"
for proot in "$(realpath -sm ${PWD})" \
             "${gopath}/src/${goipath}" \
             "${prefix}${gopath}/src/${goipath}"; do
  [[ ${file}/ == ${proot}/* ]] && file=".${file#${proot}}"
done
file="$(realpath -sm ${file})"
local workdir="$(realpath -sm .)"
if [[ ${file}/ == ${workdir}/* ]] ; then
  local dest="${prefix}${gopath}/src/${goipath}${file#${workdir}}"
else
  local dest="${prefix}${file}"
fi
local destdir="$(dirname ${dest})"
if [[ ! -d "${destdir}" ]] ; then
  installfile "${goipath}" "${destdir#${prefix}}"
fi
if [[ (! -e $file) || (-d "${file}" && ! -L "${file}") ]] ; then
  install -m 0755 -vd "${dest}"
  if [[ -d "${file}" && ! -L "${file}" ]] ; then
    find "${file}" -maxdepth 1 -iname '*.md' -type f | while read docfile ; do
      installfile "${goipath}" "${docfile}"
    done
  fi
  local fllprefix="%dir"
else
  if [[ -L "${file}" ]] ; then
    ln -vs   $(readlink "${file}") "${dest}"
    touch -h -r         "${file}"  "${dest}"
  else
    install -m 0644 -vp "${file}"  "${dest}"
  fi
  [[ "${file}" == *.md ]] && local fllprefix="%doc"
fi
echo "${fllprefix:+${fllprefix} }\"${dest#${prefix}}\"" >> "${filelist}"
}

listfiles() {
goipath="${1}"
echo $(GOPATH="${GO_BUILD_PATH}" golist --to-install \
                                        --package-path ${goipath} \
                                        ${golistflags[${goipath}]})
}

checks() {
goipath="${1}"
for dir in $(\
  GOPATH="${workroot}${GOPATH+:${GOPATH}}" \
  golist --provided --tests                \
         --package-path ${goipath}         \
         ${golistflags[$goipath]}          \
  ) ; do
  pushd "${workroot}/src/${dir}" >/dev/null
    echo "Testing: \"${workroot}/src/${dir}\""
    (set -x ; GOPATH="${GO_BUILD_PATH}:${GOPATH:+${GOPATH}:}${gopath}" \
              go test ${GO_TEST_FLAGS} \
                      -ldflags "${LDFLAGS:+${LDFLAGS} }-extldflags '${GO_TEST_EXT_LD_FLAGS}'")
  popd >/dev/null
done
}

fullprovides(){
  namespace=${1}
  prov=${2}
  for index in "${!deco[@]}" ; do
    echo "${namespace}(${prov})${deco[index]}${version:+ = ${version}}"
  done
}

provides() {
goipath="${1}"
fullprovides golang-ipath "${goipath}"
for prov in $(\
  GOPATH="${prefix}${gopath}"      \
  golist --provided                \
	 --package-path ${goipath} \
         ${golistflags[$goipath]}         \
  ); do
  fullprovides golang "${prov}"
done
}

requires() {
goipath="${1}"
for req in $(\
  GOPATH="${prefix}${gopath}"      \
  golist --imported                \
	 --package-path ${goipath} \
	 --skip-self               \
         ${golistflags[$goipath]}         \
  ); do
  echo "golang($req)"
done
}

# Action-specific preparation
case $action in
  install)        [[ ${#goipathes[@]} -eq 0 ]] && exit 0
                  otherfiles=( $* )
                  install -m 0755   -vd "${prefix}${gopath}/src"
                  for goipath in ${goipathes[@]} ; do
                    echo "Installing: ${goipath}"
                    if [[ ! -e  "${GO_BUILD_PATH}/src/${goipath}" ]] ; then
                      install -m 0755 -vd "$(dirname   ${GO_BUILD_PATH}/src/${goipath})"
                      ln -vfs "${sourcedir}"          "${GO_BUILD_PATH}/src/${goipath}"
                    fi
                    pushd "${GO_BUILD_PATH}/src/${goipath}" >/dev/null
                      # This needs to be the only place where the .goipath
                      # file is created and/or modified
                      ippgoipathfile=.$(echo "${goipath}" | sed "s/[/.]/-/g")-goipath
                      [[ ! -e ${ippgoipathfile} ]] && touch ${ippgoipathfile}
                      grep -x -- "---" ${ippgoipathfile} || echo "---" >> ${ippgoipathfile}
                      grep -x -- "metadata:" ${ippgoipathfile} || echo "metadata:" >> ${ippgoipathfile}
                      grep -x -- "  commit:.*" ${ippgoipathfile} || echo "  commit: ${commit}" >> ${ippgoipathfile}
                      cp ${ippgoipathfile} .goipath
                      golistfiles=$(listfiles "${goipath}")
                      for file in ${golistfiles[@]} ${otherfiles[@]} .goipath ; do
                        installfile "${goipath}" "${file}"
                      done
                      rm .goipath
                    popd >/dev/null
                  done
                  sort -u -o "${filelist}" "${filelist}" ;;
  check)          [[ ${#goipathes[@]} -eq 0 ]] && exit 0
                  [[ ${checkin} == "system" ]] && workroot="${prefix}${gopath}" \
                                               || workroot="${GO_BUILD_PATH}"
                  for goipath in ${goipathes[@]} ; do
                    echo "Testing: ${goipath}"
                    checks "${goipath}"
                  done ;;
  provides)       deco=( "" )
                  for key in "${!attributes[@]}"; do
                    [[ -n "${attributes[$key]}" ]] && deco+=( "($key=${attributes[$key]})" )
                  done
                  while read lockfile ; do
                    dir=$(dirname "${lockfile}")
                    provides "${dir#${prefix}${gopath}/src/}"
                  done ;;
  requires)       while read lockfile ; do
                    dir=$(dirname "${lockfile}")
                    requires "${dir#${prefix}${gopath}/src/}"
                  done ;;
esac
