#!/usr/bin/env bash

set -u
set -o pipefail

PROGRAM="$(basename "$0")"
WORK_ROOT="$(pwd -P)"
REPO_URL="${CIVIDESK_CIVICRM_REPO_URL:-https://github.com/civicrm/civicrm-core}"
TMP_ROOT="/tmp"
VERSION=""
PATCH_NAME=""

usage() {
  cat <<EOF
Usage: $PROGRAM [OPTIONS]

Create patch files from local CRM/ and templates/ core overrides.

Options:
  -v, --version VERSION   CiviCRM version/branch to use.
                          Defaults to the latest patches-[version] directory.
  -n, --name NAME         Patch file name. Creates NAME.patch.
  -t, --tmp-dir DIR       Directory containing civicrm-core-VERSION.
                          Defaults to /tmp.
      --repo-url URL      CiviCRM core repository URL.
                          Defaults to $REPO_URL
  -h, --help              Show this help.
EOF
}

log() {
  printf '%s\n' "$*"
}

warn() {
  printf 'Warning: %s\n' "$*" >&2
}

die() {
  printf 'Error: %s\n' "$*" >&2
  exit 1
}

find_patch_dirs() {
  local pattern="$1"

  find "$WORK_ROOT" \
    \( -type d \( -name .git -o -name vendor \) \) -prune \
    -o -type d -name "$pattern" -print | sort
}

latest_patch_version() {
  local versions

  versions="$(find_patch_dirs 'patches-*' | sed 's#.*/patches-##')"
  [ -n "$versions" ] || return 1

  if command -v php >/dev/null 2>&1; then
    printf '%s\n' "$versions" | php -r '
      $versions = array_values(array_filter(array_map("trim", file("php://stdin"))));
      usort($versions, "version_compare");
      echo end($versions), PHP_EOL;
    '
  else
    printf '%s\n' "$versions" | sort | tail -n 1
  fi
}

find_override_roots() {
  find "$WORK_ROOT" \
    \( -type d \( -name .git -o -name vendor -o -name 'patches-*' \) \) -prune \
    -o -type d \( -name CRM -o -name templates \) -print |
    while IFS= read -r dir; do
      local root
      local parent_name

      root="$(dirname "$dir")"
      parent_name="$(basename "$root")"

      case "$parent_name" in
        CRM|templates)
          continue
          ;;
      esac

      printf '%s\n' "$root"
    done | sort -u
}

prepare_checkout() {
  local checkout_dir="$1"

  mkdir -p "$TMP_ROOT" || return 1

  if [ -d "$checkout_dir/.git" ]; then
    log "Updating $checkout_dir from $REPO_URL ($VERSION) ..."
    git -C "$checkout_dir" remote set-url origin "$REPO_URL" || return 1
    git -C "$checkout_dir" fetch --quiet --depth 1 origin "$VERSION" || \
      git -C "$checkout_dir" fetch --quiet origin "$VERSION" || return 1
    git -C "$checkout_dir" checkout --quiet -B "cividesk-patch-$VERSION" FETCH_HEAD || return 1
  else
    [ ! -e "$checkout_dir" ] || die "$checkout_dir exists but is not a git checkout"
    log "Cloning $REPO_URL ($VERSION) into $checkout_dir ..."
    git clone --quiet --depth 1 --branch "$VERSION" "$REPO_URL" "$checkout_dir" || return 1
  fi

  reset_checkout "$checkout_dir"
}

reset_checkout() {
  local checkout_dir="$1"

  git -C "$checkout_dir" reset --hard --quiet HEAD || return 1
  git -C "$checkout_dir" clean -fd --quiet || return 1
}

overlay_dir() {
  local source_dir="$1"
  local destination_dir="$2"

  [ -d "$source_dir" ] || return 0

  mkdir -p "$destination_dir" || return 1
  cp -Rp "$source_dir/." "$destination_dir/" || return 1
}

create_patch_for_root() {
  local checkout_dir="$1"
  local override_root="$2"
  local patch_dir="$override_root/patches-$VERSION"
  local patch_file="$patch_dir/$PATCH_NAME.patch"
  local tmp_patch

  log "Processing $override_root"
  reset_checkout "$checkout_dir" || return 1

  overlay_dir "$override_root/CRM" "$checkout_dir/CRM" || return 1
  overlay_dir "$override_root/templates" "$checkout_dir/templates" || return 1

  tmp_patch="$(mktemp "${TMPDIR:-/tmp}/cividesk-patch-create.XXXXXX")" || return 1
  git -C "$checkout_dir" diff --binary --no-ext-diff -- CRM templates > "$tmp_patch" || {
    rm -f "$tmp_patch"
    return 1
  }

  if [ ! -s "$tmp_patch" ]; then
    rm -f "$tmp_patch"
    log "  No core override changes found."
    return 0
  fi

  mkdir -p "$patch_dir" || {
    rm -f "$tmp_patch"
    return 1
  }

  mv "$tmp_patch" "$patch_file" || return 1
  log "  Created $patch_file"
}

parse_args() {
  while [ "$#" -gt 0 ]; do
    case "$1" in
      -v|--version)
        [ "$#" -ge 2 ] || die "$1 requires a value"
        VERSION="$2"
        shift 2
        ;;
      -n|--name)
        [ "$#" -ge 2 ] || die "$1 requires a value"
        PATCH_NAME="$2"
        shift 2
        ;;
      -t|--tmp-dir)
        [ "$#" -ge 2 ] || die "$1 requires a value"
        TMP_ROOT="$2"
        shift 2
        ;;
      --repo-url)
        [ "$#" -ge 2 ] || die "$1 requires a value"
        REPO_URL="$2"
        shift 2
        ;;
      -h|--help)
        usage
        exit 0
        ;;
      *)
        die "Unknown option: $1"
        ;;
    esac
  done
}

main() {
  local checkout_dir
  local override_roots
  local override_root

  parse_args "$@"

  command -v git >/dev/null 2>&1 || die "git is required"
  command -v sed >/dev/null 2>&1 || die "sed is required"

  [ -n "$PATCH_NAME" ] || die "--name is required"
  case "$PATCH_NAME" in
    */*|*.patch)
      die "--name must be a bare name without slashes or .patch"
      ;;
  esac

  if [ -z "$VERSION" ]; then
    VERSION="$(latest_patch_version)" || die "No patches-[version] directories found"
  fi

  override_roots="$(find_override_roots)"
  [ -n "$override_roots" ] || die "No CRM or templates override directories found under $WORK_ROOT"

  checkout_dir="$TMP_ROOT/civicrm-core-$VERSION"
  prepare_checkout "$checkout_dir" || die "Could not prepare $checkout_dir"

  while IFS= read -r override_root; do
    [ -n "$override_root" ] || continue
    create_patch_for_root "$checkout_dir" "$override_root" || die "Could not create patch for $override_root"
  done <<EOF
$override_roots
EOF
}

main "$@"
