#!/usr/bin/env bash

set -o errexit -o nounset -o pipefail

SEMVER_REGEX="^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$"

PROG=semver
PROG_VERSION=1.0.0

VERSION_FILE=vulndb/version.txt
DEFAULT_VERSION=0.1.0

USAGE="\
Usage:
  $PROG
  $PROG init [<version>]
  $PROG bump [(major|minor|patch|prerel <prerel>|build <build>) | --force <version>] [--pretend]
  $PROG compare <version> [<oldversion>]
  $PROG --help
  $PROG --version

Arguments:
  <version>  A version must match the following regex pattern:
             \"${SEMVER_REGEX}\".
             In english, the version must match X.Y.Z(-PRERELEASE)(+BUILD)
             where X, Y and Z are positive integers, PRERELEASE is an optionnal
             string composed of alphanumeric characters and hyphens and
             BUILD is also an optional string composed of alphanumeric
             characters and hyphens.

  <oldversion>  See <version> definition.

  <prerel>  String that must be composed of alphanumeric characters and hyphens.

  <build>   String that must be composed of alphanumeric characters and hyphens.

Options:
  -f, --force=<version>  Forces a bump of any version without checking if it
                         respects semver bumping rules.
  -p, --pretend          Do not overwrite the project's version file, only
                         output what the new version string would be.
  -v, --version          Print the version of this tool.
  -h, --help             Print this help message.

Commands:
  init     initialize this project's version.
  bump     this project's version by one of major, minor, patch, prerel, build
           or a forced potentialy conflicting version.
  compare  <version> to this project's version or to provided <oldversion>."


function warning {
  echo -e "$1" >&2
}

function error {
  echo -e "$1" >&2
  exit 1
}

function usage-help {
  error "$USAGE"
}

function usage-version {
  echo -e "${PROG}: $PROG_VERSION"
  exit 0
}

function validate-version {
  local version=$1
  if [[ "$version" =~ $SEMVER_REGEX ]]; then
    # if a second argument is passed, store the result in var named by $2
    if [ "$#" -eq "2" ]; then
      local major=${BASH_REMATCH[1]}
      local minor=${BASH_REMATCH[2]}
      local patch=${BASH_REMATCH[3]}
      local prere=${BASH_REMATCH[4]}
      local build=${BASH_REMATCH[5]}
      eval "$2=(\"$major\" \"$minor\" \"$patch\" \"$prere\" \"$build\")"
    else
      echo "$version"
    fi
  else
    error "version $version does not match the semver scheme 'X.Y.Z(-PRERELEASE)(+BUILD)'. See help for more information."
  fi
}

# this function will reverse-traverse folders until VERSION_FILE is found
# or '/' is reached.
function get-version {
  while [ -w . ]; do
    if [ -e $VERSION_FILE ]; then
      validate-version "$(cat $VERSION_FILE)"
      return 0
    fi

    pushd .. > /dev/null
  done

  error "Version file $VERSION_FILE not found, you may want to initialize this project with 'version init'"
}

function compare-version {
  validate-version "$1" V
  validate-version "$2" V_

  # MAJOR, MINOR and PATCH should compare numericaly
  for i in 0 1 2; do
    case $((${V[$i]} - ${V_[$i]})) in
      0) ;;
      -[0-9]*) echo -1; return 0;;
      [0-9]*) echo 1; return 0 ;;
    esac
  done

  # PREREL should compare with the ASCII order.
  if [[ "${V[3]}" > "${V_[3]}" ]]; then
    echo 1; return 0;
  elif [[ "${V[3]}" < "${V_[3]}" ]]; then
    echo -1; return 0;
  fi

  echo 0
}

function cli-print {
  get-version
  exit 0
}

function command-init {
  local version=""
  case $# in
    0) version="$DEFAULT_VERSION" ;;
    1) version=$(validate-version "$1") ;;
    2) usage-help;;
  esac

  if [ -e "$VERSION_FILE" ]; then
    error "version file $VERSION_FILE exists, cannot initialize project.  Either remove the current file or use 'version bump --force <newversion>'."
  fi

  echo "$version" | tee "$VERSION_FILE"
  exit 0
}

function command-bump {
  local new; local pretend=0; local version=$(get-version)
  validate-version $version split
  local major=${split[0]}
  local minor=${split[1]}
  local patch=${split[2]}
  local prere=${split[3]}
  local build=${split[4]}
  while [[ $# -gt 0 ]]; do
    case "$1" in
      major) new="$(($major + 1)).0.0"; shift ;;
      minor) new="${major}.$(($minor + 1)).0"; shift ;;
      patch) new="${major}.${minor}.$(($patch + 1))"; shift ;;
      prerel)
        if [[ $# -lt 2 ]]; then
          usage-help
        else
          new=$(validate-version "${major}.${minor}.${patch}-${2}")
          shift 2
        fi ;;
      build)
        if [[ $# -lt 2 ]]; then
          usage-help
        else
          new=$(validate-version "${major}.${minor}.${patch}${prere}+${2}")
          shift 2
        fi ;;
      --force|-f)
        if [[ $# -lt 2 ]]; then
          usage-help
        else
          new=$(validate-version "$2")
          shift 2
        fi ;;
      --pretend|-p) pretend=1; shift ;;
      "") break;;
      *) usage-help ;;
    esac
  done

  if [[ "$pretend" -eq 1 ]]; then
    echo $new
  else
    echo $new | tee $VERSION_FILE
  fi
  exit 0
}

function command-compare {
  local v; local v_; local version=$(get-version)

  case $# in
    0) usage-help ;;
    1) v=$(validate-version "$1"); v_=$version ;;
    2) v=$(validate-version "$1"); v_=$(validate-version "$2") ;;
  esac

  echo $(compare-version "$v" "$v_")
  exit 0
}

case $# in
  0) cli-print ;;
esac

case $1 in
  --help|-h) echo -e "$USAGE"; exit 0;;
  --version|-v) usage-version ;;
  init) shift; command-init $@;;
  bump) shift; command-bump $@;;
  compare) shift; command-compare $@;;
  *) echo "Unknown arguments: $@"; usage-help;;
esac
