#!/bin/sh
# Plush installer.
#
#   curl --proto '=https' -sSf https://maximecb.github.io/plush/install | sh
#
# Anything after "-s --" is handed to plush once the install finishes, so
# installing and running an example in one go looks like:
#
#   curl --proto '=https' -sSf https://maximecb.github.io/plush/install | sh -s -- --run-example tremor
#
# Installs into ~/.plush, replacing any previous install, and adds
# ~/.plush/bin to PATH via your shell profile.
#
# Options:
#   --force              reinstall even if the same version is present
#
# Every other argument is forwarded to plush verbatim, e.g. --run-example
# NAME, --list-examples, or a path to a .psh file.
#
# Environment variables:
#   PLUSH_HOME      install directory (default: ~/.plush)
#   PLUSH_VERSION   release tag to install (default: latest)
#   PLUSH_NO_PATH   set to 1 to leave shell profiles untouched

set -eu

REPO="maximecb/plush"
PLUSH_HOME="${PLUSH_HOME:-$HOME/.plush}"
VERSION="${PLUSH_VERSION:-latest}"

FORCE=0

# "sh -s -- ..." already eats one "--", so drop a second one rather than
# passing it on to plush
if [ "${1:-}" = "--" ]; then
    shift
fi

# Options the installer handles itself are taken out here. Every other
# argument is pushed back onto "$@" to be forwarded to plush, so the installer
# never has to know which options plush accepts.
remaining=$#

while [ "$remaining" -gt 0 ]; do
    arg="$1"
    shift
    remaining=$((remaining - 1))

    case "$arg" in
        --force)
            FORCE=1
            ;;
        run-example)
            # Spelling the older installer accepted, kept working
            set -- "$@" --run-example
            ;;
        *)
            set -- "$@" "$arg"
            ;;
    esac
done

say() { printf 'plush: %s\n' "$1"; }
err() { printf 'plush: error: %s\n' "$1" >&2; exit 1; }

# Resolve the platform to a release asset name
detect_target() {
    os="$(uname -s)"
    arch="$(uname -m)"

    case "$os" in
        Linux)  os=linux ;;
        Darwin) os=macos ;;
        *) err "unsupported operating system: $os. Only macOS and Linux have builds so far." ;;
    esac

    case "$arch" in
        x86_64 | amd64)  arch=x86_64 ;;
        arm64 | aarch64) arch=aarch64 ;;
        *) err "unsupported architecture: $arch" ;;
    esac

    printf 'plush-%s-%s' "$arch" "$os"
}

# $1 = url, $2 = destination path
download() {
    if command -v curl >/dev/null 2>&1; then
        # --proto '=https' also applies to the redirect GitHub sends us to
        curl --proto '=https' -fsSL "$1" -o "$2"
    else
        wget --https-only -qO "$2" "$1"
    fi
}

# Newest release whose tag looks like v<number>, so we can tell what we would
# install before downloading it.
#
# releases/latest is deliberately not used: the VSCode extension workflow
# publishes vsix-* releases that carry no plush binaries, and any of those
# would otherwise be picked as "latest".
resolve_latest_version() {
    api="https://api.github.com/repos/$REPO/releases?per_page=30"

    if command -v curl >/dev/null 2>&1; then
        body="$(curl --proto '=https' -fsSL "$api" 2>/dev/null || true)"
    else
        body="$(wget --https-only -qO- "$api" 2>/dev/null || true)"
    fi

    # Releases come back newest first
    printf '%s\n' "$body" \
        | sed -n 's/.*"tag_name"[ ]*:[ ]*"\(v[0-9][^"]*\)".*/\1/p' \
        | head -1
}

# Prints the version of an already installed plush, if there is one
installed_version() {
    if [ -x "$PLUSH_HOME/bin/plush" ]; then
        plush_bin="$PLUSH_HOME/bin/plush"
    elif command -v plush >/dev/null 2>&1; then
        plush_bin="$(command -v plush)"
    else
        return 0
    fi

    # Output looks like "plush 0.3.0"
    "$plush_bin" --version 2>/dev/null | awk '/^plush /{print $2}' || true
}

verify_checksum() {
    asset="$1"
    dir="$2"

    if command -v sha256sum >/dev/null 2>&1; then
        sum="$(sha256sum "$dir/$asset" | cut -d' ' -f1)"
    elif command -v shasum >/dev/null 2>&1; then
        sum="$(shasum -a 256 "$dir/$asset" | cut -d' ' -f1)"
    else
        say "no sha256 tool found, skipping checksum verification"
        return 0
    fi

    expected="$(grep " $asset\$" "$dir/SHA256SUMS" | cut -d' ' -f1)"
    [ -n "$expected" ] || err "no checksum listed for $asset"
    [ "$sum" = "$expected" ] || err "checksum mismatch for $asset (expected $expected, got $sum)"
}

# Shell profile to append to, chosen from the user's login shell
profile_file() {
    case "$(basename "${SHELL:-/bin/sh}")" in
        zsh)
            printf '%s\n' "${ZDOTDIR:-$HOME}/.zshrc"
            ;;
        bash)
            # macOS reads .bash_profile for login shells, Linux .bashrc
            if [ "$(uname -s)" = Darwin ]; then
                printf '%s\n' "$HOME/.bash_profile"
            else
                printf '%s\n' "$HOME/.bashrc"
            fi
            ;;
        *)
            printf '%s\n' "$HOME/.profile"
            ;;
    esac
}

# Path to a file under PLUSH_HOME for humans to read, using ~ when it is
# under $HOME. $1 is the name of the file.
home_display() {
    case "$PLUSH_HOME" in
        "$HOME"/*) printf '~/%s/%s' "${PLUSH_HOME#"$HOME"/}" "$1" ;;
        *) printf '%s/%s' "$PLUSH_HOME" "$1" ;;
    esac
}

env_display() {
    home_display env
}

# Same path for writing into a shell profile, keeping $HOME symbolic so the
# line survives the home directory moving
env_profile_ref() {
    case "$PLUSH_HOME" in
        "$HOME"/*) printf '$HOME/%s/env' "${PLUSH_HOME#"$HOME"/}" ;;
        *) printf '%s/env' "$PLUSH_HOME" ;;
    esac
}

# A sourceable file holding everything plush needs in the environment. This
# installer runs as a child process and cannot touch the calling shell, so
# sourcing this is what makes plush work without opening a new terminal.
write_env_file() {
    cat > "$PLUSH_HOME/env" <<EOF
#!/bin/sh
# Puts plush in the current shell. Source it:
#
#     . "$PLUSH_HOME/env"

case ":\${PATH}:" in
    *":$PLUSH_HOME/bin:"*) ;;
    *) PATH="$PLUSH_HOME/bin:\$PATH" ;;
esac

export PATH
export PLUSH_EXAMPLES_DIR="$PLUSH_HOME/examples"
EOF
}

update_profile() {
    if [ "${PLUSH_NO_PATH:-0}" = 1 ]; then
        say "source $PLUSH_HOME/env from your shell profile to finish"
        return 0
    fi

    profile="$(profile_file)"
    ref="$(env_profile_ref)"

    # Already added by a previous install, whether as the $HOME-relative line
    # written below or one of the older absolute-path forms
    if [ -f "$profile" ] \
        && { grep -qF "$ref" "$profile" || grep -qF "$PLUSH_HOME" "$profile"; }; then
        return 0
    fi

    # printf does not expand $HOME, so the profile keeps it symbolic
    {
        printf '\n# Added by the plush installer\n'
        printf '[ -f "%s" ] && . "%s"\n' "$ref" "$ref"
    } >> "$profile"

    say "set up $profile to load plush in new shells"
}

# The VSCode extension ships inside the release archive. Releases made before
# that was the case have no such file, so this stays quiet for them.
say_vscode_hint() {
    [ -f "$PLUSH_HOME/install_vsix.sh" ] || return 0

    say ""
    say "To install the VSCode extension, run this, then restart VSCode:"
    say ""
    say "    $(home_display install_vsix.sh)"
}

install_plush() {
    target="$(detect_target)"
    asset="$target.tar.gz"

    # Always an explicit tag, never releases/latest
    base="https://github.com/$REPO/releases/download/$RELEASE_TAG"

    tmp="$(mktemp -d)"
    trap 'rm -rf "$tmp"' EXIT INT TERM

    say "downloading $target ($RELEASE_TAG)"
    download "$base/$asset" "$tmp/$asset" \
        || err "download failed. Does release $RELEASE_TAG have an asset named $asset?"

    if download "$base/SHA256SUMS" "$tmp/SHA256SUMS" 2>/dev/null; then
        verify_checksum "$asset" "$tmp"
    else
        say "SHA256SUMS not available, skipping checksum verification"
    fi

    tar -xzf "$tmp/$asset" -C "$tmp"
    [ -x "$tmp/$target/bin/plush" ] || err "release archive is missing bin/plush"

    # Check the binary runs before we touch the existing install, so a bad
    # download or a wrong-architecture build can't leave the user broken
    version="$("$tmp/$target/bin/plush" --version 2>/dev/null || true)"
    [ -n "$version" ] || err "the downloaded binary did not run. Wrong architecture?"

    if [ -d "$PLUSH_HOME" ]; then
        say "removing previous install at $PLUSH_HOME"
        rm -rf "$PLUSH_HOME"
    fi

    mkdir -p "$(dirname "$PLUSH_HOME")"
    mv "$tmp/$target" "$PLUSH_HOME"

    say "installed $version to $PLUSH_HOME"
}

main() {
    command -v tar >/dev/null 2>&1 || err "'tar' is required but was not found"
    command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 \
        || err "either curl or wget is required"

    # Guard against wiping something important if PLUSH_HOME is set oddly
    case "$PLUSH_HOME" in
        "" | "/" | "$HOME" | "$HOME/") err "refusing to install to '$PLUSH_HOME'" ;;
    esac

    # Skip the download entirely when the wanted version is already here
    have="$(installed_version)"

    if [ "$VERSION" = latest ]; then
        RELEASE_TAG="$(resolve_latest_version)"
        [ -n "$RELEASE_TAG" ] \
            || err "could not find a plush release. Pass PLUSH_VERSION=<tag> to pick one."
    else
        RELEASE_TAG="$VERSION"
    fi

    want="${RELEASE_TAG#v}"

    # The binary under PLUSH_HOME has to be the one we found, otherwise there
    # is nothing there to run the forwarded arguments with
    if [ "$FORCE" = 0 ] && [ -x "$PLUSH_HOME/bin/plush" ] \
        && [ -n "$have" ] && [ "$have" = "$want" ]; then
        say "plush $have is already installed, skipping download"
    else
        install_plush
    fi

    write_env_file
    update_profile

    if [ $# -gt 0 ]; then
        # Ahead of the exec, which never returns
        say_vscode_hint
        say ""
        say "running: plush $*"
        exec "$PLUSH_HOME/bin/plush" "$@"
    fi

    # An installer runs in a child process and cannot change the shell that
    # started it, so this is the one step that has to be done by hand
    say ""
    say "To use plush in this shell right now, run:"
    say ""
    say "    source $(env_display)"
    say ""
    say "New shells pick it up automatically. Then try:"
    say ""
    say "    plush --run-example tremor"
    say "    plush --run-example night_ride"
    say ""
    say "To list all available examples:"
    say ""
    say "    plush --list-examples"

    say_vscode_hint
    say ""
}

main "$@"
