#!/usr/bin/env bash
set -euo pipefail

this_dir=${BASH_SOURCE[0]%/*}

# shellcheck source=tools/lib/git.sh
. "${this_dir}"/lib/git.sh

# shellcheck source=tools/lib/cli.sh
. "${this_dir}"/lib/cli.sh


## CLI PARSING

default_steps=(pod pub pub-major)

usage() {
    cat <<EOF
usage: tools/upgrade [OPTION]... [STEP]...

Upgrade our dependencies.

By default, run the following upgrade steps:
  ${default_steps[*]}

Each step produces a Git commit if there were any changes.

The steps are:

  pod         Upgrade CocoaPods pods.

  flutter-local
              Upgrade Flutter and its supporting libraries.
              EXPERIMENTAL and not in the default list,
              because it takes your current locally-installed version
              rather than finding the latest.

  pub         Upgrade pub packages within the constraints expressed
              in pubspec.yaml, then upgrade pods to match.

  pub-major   Upgrade pub packages to latest, editing pubspec.yaml,
              then upgrade pods to match.  If there are any changes
              here, the resulting commit is only a draft and requires
              human editing and review.

Options:

  --no-pod    Skip running CocoaPods.  This is convenient for
              drafting a branch on Linux.
EOF
}

opt_pod=y
opt_steps=()
while (( $# )); do
    case "$1" in
        --no-pod)
            opt_pod=; shift;;
        pod|flutter-local|pub|pub-major)
            opt_steps+=("$1"); shift;;
        --help) usage; exit 0;;
        *) usage >&2; exit 2;;
    esac
done

if (( ! "${#opt_steps[@]}" )); then
    opt_steps=( "${default_steps[@]}" )
fi


## EXECUTION

rootdir=$(git rev-parse --show-toplevel)
cd "$rootdir"

check_have_cocoapods() {
    [ -z "${opt_pod}" ] && return;

    if ! type pod >/dev/null; then
        echo >&2 "No \`pod\` command found."
        echo >&2
        echo >&2 "This script requires CocoaPods, in order to keep"
        echo >&2 "the CocoaPods lockfiles in sync with our \`pubspec.lock\`."
        echo >&2 "Try running it in a development environment on macOS."
        return 1
    fi
}

# Memoized result of check_pub_get_clean.
#
# (Useful because `flutter pub get` takes a couple of seconds to run.)
pub_get_known_clean=

# Check there are no changes that would be snuck in by the
# locally-installed Flutter version.
# TODO automate upgrading Flutter, too
check_pub_get_clean() {
    if [ -n "${pub_get_known_clean}" ]; then
        return 0
    fi

    run_visibly flutter pub get
    if ! no_uncommitted_changes; then
        echo >&2 "There were changes caused by running \`flutter pub get\`:"
        echo >&2
        git_status_short
        echo >&2
        echo >&2 "Typically this means your local Flutter install is newer"
        echo >&2 "than the version reflected in our \`pubspec.lock\`."
        echo >&2 "Follow the \"Upgrading Flutter\" steps in our README,"
        echo >&2 "and then try \`tools/upgrade\` again."
        return 1
    fi

    pub_get_known_clean=1
}

just_pod_update() {
    [ -z "${opt_pod}" ] && return;
    check_have_cocoapods

    run_visibly pod update --project-directory=ios/
    run_visibly pod update --project-directory=macos/
}

upgrade_pod() {
    if [ -z "${opt_pod}" ]; then
        echo >&2
        echo >&2 "pod update: WARNING: skipping because --no-pod"
        return
    fi

    check_have_cocoapods
    check_no_uncommitted_or_untracked
    check_pub_get_clean

    just_pod_update
    if no_uncommitted_changes; then
        echo >&2 "pod update: No changes."
        return
    fi

    git commit -a -m "\
deps: Update CocoaPods pods (tools/upgrade pod)
"
}

upgrade_flutter_local() {
    check_no_uncommitted_or_untracked

    # No check_pub_get_clean.  This operates on a `flutter` you've
    # already locally upgraded, so if that happens to update any
    # supporting libraries then it's expected that those will show up
    # on `flutter pub get`.
    # check_pub_get_clean

    # TODO upgrade Flutter to latest, rather than what's lying around

    local flutter_commit
    flutter_commit=$(git --git-dir="$(flutter_tree)"/.git rev-parse HEAD)

    local flutter_version_output versions flutter_version dart_sdk_version
    flutter_version_output=$(run_visibly flutter --version)
    # shellcheck disable=SC2207 # output has controlled whitespace
    versions=( $(echo -n "${flutter_version_output}" | perl -0ne '
      # Sometimes the `flutter --version` output begins with lines like
      # "Resolving dependencies..."; so the pattern accommodates those.
      # And the pattern requires Dart 3.x, because we emit "<4.0.0" below.
      if (/^Flutter (\S+) .*\sDart \S+ \(build (3\.\S+)\)/ms) {
        print "$1 $2";
      }
    ') )
    if (( !"${#versions[@]}" )); then
        echo >&2 "error: 'flutter --version' output not recognized"
        printf >&2 "output was:\n-----\n%s\n-----" "${flutter_version_output}"
        return 1
    fi
    flutter_version="${versions[0]}"
    dart_sdk_version="${versions[1]}"

    yaml_fragment="\
  sdk: '>=${dart_sdk_version} <4.0.0'
  flutter: '>=${flutter_version}'  # ${flutter_commit}
" \
      perl -i -0pe 's/^  sdk: .*\n  flutter: .*\n/$ENV{yaml_fragment}/m' \
        pubspec.yaml

    if no_uncommitted_changes; then
        echo >&2 "flutter: No changes."
        return
    fi

    run_visibly flutter pub get

    local libraries_updated=
    if git diff pubspec.lock | perl -0ne '
            s/.*?\n(@@)/$1/s;  # cut `git diff` header
            if (/^[-+](?!  (dart|flutter):)/m) { exit 0; } else { exit 1; }
          '; then
        libraries_updated=y
    fi

    local more="

And update Flutter's supporting libraries to match."
    git commit -a -m "\
deps: Upgrade Flutter to ${flutter_version}${libraries_updated:+"${more}"}
"

    cat <<EOF

There was a Flutter upgrade${libraries_updated:+, and libraries were updated}.

The \`tools/upgrade\` script created a draft commit, but
it requires manual checking:

 * The update was to the Flutter version you currently
   have installed at the \`flutter\` command.
   Check that that is the latest Flutter from main,
   or otherwise the version you intend to upgrade to.
EOF
}

upgrade_pub() {
    check_have_cocoapods
    check_no_uncommitted_or_untracked
    check_pub_get_clean

    run_visibly flutter pub upgrade
    if no_uncommitted_changes; then
        echo >&2 "flutter pub upgrade: No changes."
        return
    fi

    just_pod_update

    # Some upgrades also cause various "generated_plugin_registrant"
    # or "generated_plugins" files to get updated: see commits
    # bf09824bd and b8b72723a.  From the latter, it sounds like those
    # changes are made automatically by `flutter pub upgrade`, though,
    # so no action needed here.

    # TODO rerun build_runner, at least if Drift was updated;
    #   cf commits db7932244, 9400c8561, and 5dbf1e635.
    #   If that does change anything, probably flag for closer human review.

    git commit -a -m "\
deps: Upgrade packages within constraints (tools/upgrade pub)
"

    if [ -z "${opt_pod}" ]; then
        echo >&2
        echo >&2 "flutter pub upgrade: WARNING: Podfile.lock files may be out of date because --no-pod"
    fi
}

upgrade_pub_major() {
    check_have_cocoapods
    check_no_uncommitted_or_untracked
    check_pub_get_clean

    run_visibly flutter pub upgrade --major-versions
    if no_uncommitted_changes; then
        echo >&2 "flutter pub upgrade --major-versions: No changes."
        return
    fi

    just_pod_update

    # TODO rerun build_runner; see upgrade_pub

    git commit -a -m "\
WIP deps: Upgrade packages to latest, namely TODO:(which packages?)

This is the result of \`tools/upgrade pub-major\`, and
TODO:(describe any other changes you had to make).

Changelogs:
  TODO:(link https://pub.dev/packages/PACKAGE_NAME/changelog)
"

    cat <<EOF

There were upgrades beyond the constraints we had in \`pubspec.yaml\`.
This typically means the package maintainers identified the changes
as potentially breaking.

The \`tools/upgrade\` script created a draft commit, but this type
of upgrade cannot be fully automated because it requires review.
To finish the upgrade:

 * Identify which packages were updated.
 * Locate their changelogs: https://pub.dev/packages/PACKAGE_NAME/changelog .
 * Review the changelogs and determine if any of the breaking changes
   look like they could affect us.
 * Test any relevant areas of the app, and make any changes needed
   for our code to go along with the updates.
 * Amend the commit, and fill in the TODO items in the commit message.

If several unrelated packages were upgraded, and any of them require
changes in our code, consider upgrading them in separate commits.
EOF

    if [ -z "${opt_pod}" ]; then
        echo >&2
        echo >&2 "flutter pub upgrade --major-versions: WARNING: Podfile.lock files may be out of date because --no-pod"
    fi
}

divider_line='================================================================'

for step in "${opt_steps[@]}"; do
    echo
    echo "${divider_line}"
    echo "======== tools/upgrade ${step}"
    case "${step}" in
        pod)       upgrade_pod ;;
        flutter-local) upgrade_flutter_local ;;
        pub)       upgrade_pub ;;
        pub-major) upgrade_pub_major ;;
        *)   echo >&2 "Internal error: unknown step ${step}" ;;
    esac
done
