#!/bin/bash
#
# sdk-assitant is a helper script for simple scratchbox2 management tasks
# in MerSDK
#
# Copyright (C) 2014-2018 Jolla Ltd.
# Copyright (C) 2020 Open Mobile Platform LLC.
# Contact: Martin Kampas <martin.kampas@jolla.com>
# All rights reserved.
#
# You may use this file under the terms of BSD license as follows:
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#   * Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#   * Neither the name of the Jolla Ltd nor the
#     names of its contributors may be used to endorse or promote products
#     derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

set -o nounset

fail() {
    echo "$*" 1>&2
    echo "Try passing '--help' for more information" 1>&2
    exit 1
}

# dry run function
_() {
    [[ -n $OPT_DRY_RUN ]] && echo "$@" || "$@"
}

usage() {
    cat <<EOF
SDK assistant tool

Usage:
    $(basename $0) [tooling|target] create <name> <URL> [--tooling <tooling>]
                   [--no-snapshot]
                            : create a new sb2 tooling or target <name>,
                              using the image at <URL>. In case of a new
                              target, an appropriate tooling will be selected
                              automatically unless overridden with '--tooling'.
                              Automatic creation of the default build target
                              snapshot may be suppressed with the
                              '--no-snapshot' option.

    $(basename $0) [tooling|target] clone <name> <clone-name>
                            : create a new sb2 tooling or target <clone-name>,
                              using the <name> object as a template.

    $(basename $0) [tooling|target] update <name>
                            : update the given sb2 tooling or target.
                              When updating a target the tooling used
                              by the target will be updated first.

    $(basename $0) [tooling|target] register [--user <user>
                   [--password <password>]] {--all | <name>}
                            : register the single named or all sb2 toolings
                              and/or targets. User name and password will be
                              queried interactively if omitted.

    $(basename $0) [tooling|target] remove [--snapshots-of] <name>
                            : remove the given sb2 tooling or target. With
                              '--snaphosts-of' remove just snapshots of
                              a target.

    $(basename $0) [tooling|target] list [-s|--snapshots] [--slow]
                            : list available sb2 toolings and/or targets.
                              In the combined listing targets are grouped
                              by toolings they use. With '--snapshots' build
                              targets are listed together with their snapshots.
                              With '--slow' the state is checked more
                              thoroughly.

    $(basename $0) [tooling|target] package-search <name> [<pattern>...]
                            : search packages inside the sb2 tooling or target
                              identified by <name>, installed and available.

    $(basename $0) [tooling|target] package-install <name> <package-name>...
                            : install packages into the sb2 tooling or target
                              identified by <name>

    $(basename $0) [tooling|target] package-remove <name> <package-name>...
                            : remove packages from the sb2 tooling or target
                              identified by <name>

    $(basename $0) [tooling|target] maintain <name> [<command> [<arg>...]]
                            : execute arbitrary maintenance commands under
                              the sb2 tooling or target identified by <name>.
                              I no <command> is specified, open a shell.

Common options:
    -y | --non-interactive  : do not ask questions
    -z | --dry-run          : do nothing, just print out what would happen
    -h | --help             : show this help

EOF

    # exit with error if any argument is given
    if [[ $# -ne 0 ]]; then
        echo "sdk-assistant: $@"
        exit 1
    else
        exit 0
    fi
}

guess_type() (
    local url=$1

    shopt -s nocasematch
    case ${url##*/} in
        *target*) echo target;;
        *tooling*) echo tooling;;
        *) return 1;;
    esac
)

list() {
    local type=${1:-}
    local no_snapshots=$([[ ! $OPT_SNAPSHOTS ]] && echo 1)
    local check_snapshots$([[ $OPT_SNAPSHOTS && $OPT_SLOW ]] && echo 1)
    if [[ $type == tooling ]]; then
        sdk-manage tooling list
    elif [[ $type == target ]]; then
        sdk-manage target list ${no_snapshots:+--no-snapshots}
    else
        {
            sdk-manage tooling list |awk -v OFS=: '{print $1, "", $1}'
            sdk-manage target list --long ${no_snapshots:+--no-snapshots} \
                ${check_snapshots:+--check-snapshots} |awk -v OFS=: '
                $4 != "-" {
                    note = sub(/\*$/, "", $4) ? "(snapshot, outdated)" : "(snapshot)"
                    print $1, $4, $1 "  " note
                    next
                }
                $2 == "-" {
                    print $1, "<no-tooling>", $1
                    if (!no_tooling_printed) {
                        print "<no-tooling>", "", "<no-tooling>"
                        no_tooling_printed = 1
                    }
                    next
                }
                {
                    print $1, $2, $1
                }'
        } |column --separator : --output-separator : --tree 3 --tree-id 1 --tree-parent 2 \
            |cut -d : -f 3
    fi
}

have() {
    local type=$1
    local name=$2
    sdk-manage "$type" list |grep -q -F --line-regexp "$name"
}

OPT_ALL=
OPT_CREATE=
OPT_CLONE=
OPT_COMMAND=()
OPT_DRY_RUN=
OPT_LIST=
OPT_MAINTAIN=
OPT_NAME=
OPT_NO_SNAPSHOT=
OPT_PACKAGE_LIST=
OPT_PACKAGE_INSTALL=
OPT_PACKAGE_NAME_PATTERNS=
OPT_PACKAGE_NAMES=
OPT_PACKAGE_REMOVE=
OPT_PASSWORD=
OPT_REGISTER=
OPT_REMOVE=
OPT_SLOW=
OPT_SNAPSHOTS=
OPT_SNAPSHOTS_OF=
OPT_TOOLING_NAME=
OPT_TYPE=
OPT_UPDATE=
OPT_USER=
OPT_URL=
OPT_INTERACTIVE=1

positional_args=()
while [[ ${1:-} ]]; do
    case "$1" in
        -y | --non-interactive )
            OPT_INTERACTIVE=
            ;;
        -z | --dry-run )
            OPT_DRY_RUN=1
            ;;
        --slow )
            OPT_SLOW=1
            ;;
        --user )
            [[ $# -ge 2 && $2 ]] || fail "missing argument for $1"
            OPT_USER=$2
            shift
            ;;
        --password )
            [[ $# -ge 2 && $2 ]] || fail "missing argument for $1"
            OPT_PASSWORD=$2
            shift
            ;;
        --no-snapshot )
            OPT_NO_SNAPSHOT=1
            ;;
        -s | --snapshots )
            OPT_SNAPSHOTS=1
            ;;
        --snapshots-of )
            OPT_SNAPSHOTS_OF=1
            ;;
        --tooling )
            [[ $# -ge 2 && $2 ]] || fail "missing argument for $1"
            OPT_TOOLING_NAME=$2
            shift
            ;;
        --all )
            OPT_ALL=1
            ;;
        -h | --help )
            usage
            ;;
        -* )
            fail "unknown option: $1"
            ;;
        maintain )
            # Stop processing - this command accepts arbitrary arguments
            positional_args+=("$@")
            shift $#
            break
            ;;
        * )
            positional_args+=("$1")
            ;;
    esac
    shift
done

set -- ${positional_args[@]:+"${positional_args[@]}"}

case ${1:-} in
    tooling|target)
        OPT_TYPE=$1
        shift
        ;;
esac

[[ $# -gt 0 ]] || usage "Command expected" >&2

case $1 in
    create )
        OPT_CREATE=1
        [[ $# -ge 3 && $2 && $3 ]] || fail "create: name and URL required"
        OPT_NAME=$2
        OPT_URL=$3
        shift 3
        [[ $# -eq 0 ]] || fail "create: unexpected argument: '$1'"
        ;;
    clone )
        OPT_CLONE=1
        [[ $# -ge 3 && $2 && $3 ]] || fail "clone: original and clone names required"
        OPT_NAME=$2
        OPT_CLONE_NAME=$3
        shift 3
        [[ $# -eq 0 ]] || fail "clone: unexpected argument: '$1'"
        ;;
    update )
        OPT_UPDATE=1
        [[ $# -ge 2 && $2 ]] || fail "update: ${OPT_TYPE:+$OPT_TYPE }name required"
        OPT_NAME=$2
        shift 2
        [[ $# -eq 0 ]] || fail "update: unexpected argument: '$1'"
        ;;
    register )
        OPT_REGISTER=1
        if [[ $# -ge 2 ]]; then
            [[ $2 ]] || fail "register: name must not be empty"
            OPT_NAME=$2
            shift
        fi
        shift
        [[ $# -eq 0 ]] || fail "register: unexpected argument: '$1'"
        ;;
    remove )
        OPT_REMOVE=1
        [[ $# -ge 2 && $2 ]] || fail "remove: ${OPT_TYPE:+$OPT_TYPE }name required"
        OPT_NAME=$2
        shift 2
        [[ $# -eq 0 ]] || fail "remove: unexpected argument: '$1'"
        ;;
    list )
        OPT_LIST=1
        shift
        [[ $# -eq 0 ]] || fail "list: unexpected argument: '$1'"
        ;;
    package-list|package-search )
        [[ $1 == package-list ]] && echo "WARNING: The 'package-list' command is " \
            "deprecated, use 'package-search' instead" >&2
        OPT_PACKAGE_LIST=1
        [[ $# -ge 2 && $2 ]] || fail "$1: ${OPT_TYPE:+$OPT_TYPE }name required"
        OPT_NAME=$2
        OPT_PACKAGE_NAME_PATTERNS=("${@:3}")
        shift $#
        ;;
    package-install )
        OPT_PACKAGE_INSTALL=1
        [[ $# -ge 3 && $2 && $3 ]] || fail "package-install: ${OPT_TYPE:+$OPT_TYPE }name and package-name required"
        OPT_NAME=$2
        OPT_PACKAGE_NAMES=("${@:3}")
        shift $#
        ;;
    package-remove )
        OPT_PACKAGE_REMOVE=1
        [[ $# -ge 3 && $2 && $3 ]] || fail "package-install: ${OPT_TYPE:+$OPT_TYPE }name and package-name required"
        OPT_NAME=$2
        OPT_PACKAGE_NAMES=("${@:3}")
        shift $#
        ;;
    maintain )
        OPT_MAINTAIN=1
        [[ $# -ge 2 && $2 ]] || fail "maintain: ${OPT_TYPE:+$OPT_TYPE }name required"
        OPT_NAME=$2
        OPT_COMMAND=("${@:3}")
        shift $#
        ;;
    * )
        fail "$1: unrecognized command"
        ;;
esac

[[ $# -eq 0 ]] || fail "$1: unexpected argument"

if [[ $OPT_SLOW && ! $OPT_LIST ]]; then
    fail "The '--slow' option can only be used with the 'list' command"
fi

if [[ $OPT_USER && ! $OPT_REGISTER ]]; then
    fail "The '--user' option can only be used with the 'register' command"
fi

if [[ $OPT_PASSWORD && ! $OPT_REGISTER ]]; then
    fail "The '--password' option can only be used with the 'register' command"
fi

if [[ $OPT_ALL && ! $OPT_REGISTER ]]; then
    fail "The '--all' option can only be used with the 'register' command"
fi

if [[ $OPT_NO_SNAPSHOT && ! $OPT_CREATE ]]; then
    fail "The '--no-snapshot' option can only be used with the 'create' command"
fi

if [[ $OPT_SNAPSHOTS && ! $OPT_LIST ]]; then
    fail "The '--snapshots' option can only be used with the 'list' command"
fi

if [[ $OPT_SNAPSHOTS_OF && ! $OPT_REMOVE ]]; then
    fail "The '--snapshots-of' option can only be used with the 'remove' command"
fi

if [[ $EUID -eq 0 ]]; then
    fail "$(basename $0) must not be run as root."
fi

if [[ $OPT_LIST ]]; then
    if [[ $OPT_SNAPSHOTS && $OPT_TYPE == tooling ]]; then
        fail "The '--snapshots' option cannot be used with toolings"
    fi
    # list requested, just do it
    list "$OPT_TYPE"
    exit
fi

if [[ $OPT_CREATE ]]; then
    if [[ ! $OPT_NAME ]] || [[ ! $OPT_URL ]]; then
        fail "create: ${OPT_TYPE:+$OPT_TYPE }<name> and <URL> required"
    fi

    if [[ ! $OPT_TYPE ]]; then
        if ! OPT_TYPE=$(guess_type "$OPT_URL"); then
            fail "Failed to guess type of tarball. Please specify either 'tooling' or 'target' on command line."
        fi
    fi

    if have "$OPT_TYPE" "$OPT_NAME"; then
        fail "$OPT_NAME: $OPT_TYPE already exists"
    fi

    if [[ ! ($OPT_URL =~ ^https?://) && ! -f ${OPT_URL#file://} ]]; then
        fail "$OPT_URL: no such file"
    fi

    cat <<EOF
Creating $OPT_TYPE [$OPT_NAME]
Using tarball [$OPT_URL]
EOF
fi

if [[ $OPT_CLONE || $OPT_UPDATE || $OPT_REMOVE || ($OPT_REGISTER && $OPT_NAME) \
        || $OPT_PACKAGE_LIST || $OPT_PACKAGE_INSTALL || $OPT_PACKAGE_REMOVE \
        || $OPT_MAINTAIN ]]; then
    if [[ ! $OPT_TYPE ]]; then
        if have tooling "$OPT_NAME"; then
            OPT_TYPE=tooling
        fi
        if have target "$OPT_NAME"; then
            if [[ $OPT_TYPE ]]; then
                fail "$OPT_NAME: Ambiguous. Please specify either 'tooling' or 'target' on command line."
            fi
            OPT_TYPE=target
        fi
        if [[ ! $OPT_TYPE ]]; then
            fail "$OPT_NAME: No such tooling or target"
        fi
    elif ! have "$OPT_TYPE" "$OPT_NAME"; then
        fail "$OPT_NAME: No such $OPT_TYPE"
    fi
fi

if [[ $OPT_PACKAGE_LIST ]]; then
    # list packages requested, just do it
    sdk-manage "$OPT_TYPE" package-list "$OPT_NAME" --long \
        ${OPT_PACKAGE_NAME_PATTERNS[@]:+"${OPT_PACKAGE_NAME_PATTERNS[@]}"}
    exit
fi

if [[ $OPT_CLONE ]]; then
    if have "$OPT_TYPE" "$OPT_CLONE_NAME"; then
        fail "$OPT_CLONE_NAME: $OPT_TYPE already exists"
    fi

    cat <<EOF
Going to clone the $OPT_TYPE [$OPT_NAME] as [$OPT_CLONE_NAME]
EOF
fi

if [[ $OPT_UPDATE ]]; then
    if [[ $OPT_TYPE == target ]]; then
        OPT_TOOLING_NAME=$(sdk-manage target list --long \
            |awk -v target="$OPT_NAME" '($1 == target) { print $2 };')
        if [[ $OPT_TOOLING_NAME == - ]]; then
            echo "Going to update the target [$OPT_NAME]"
            OPT_TOOLING_NAME=
        else
            echo "Going to update the tooling [$OPT_TOOLING_NAME], then the target [$OPT_NAME]"
        fi
    else
        echo "Going to update the tooling [$OPT_NAME]"
    fi
fi

if [[ $OPT_REGISTER ]]; then
    if [[ $OPT_ALL && $OPT_NAME ]]; then
        fail "Cannot use both '--all' and a name"
    elif [[ $OPT_ALL ]]; then
        if [[ $OPT_TYPE ]]; then
            echo "Going to register all ${OPT_TYPE}s"
        else
            echo "Going to register all toolings and targets"
        fi
    elif [[ $OPT_NAME ]]; then
        echo "Going to register the $OPT_TYPE [$OPT_NAME]"
    else
        fail "Either use '--all' or pass a name"
    fi
    if [[ $OPT_PASSWORD && ! $OPT_USER ]]; then
        fail "Cannot use '--password' without '--user'"
    fi
    if [[ ! $OPT_PASSWORD ]]; then
        # The act of supplying a password interactively is considered a confirmation
        OPT_INTERACTIVE=
    fi
fi

if [[ $OPT_REMOVE ]]; then
    if [[ $OPT_SNAPSHOTS_OF ]]; then
        if [[ $OPT_TYPE != target ]]; then
            fail "The '--snapshots-of' option can only be used with targets"
        fi
        echo "Going to remove snapshots of the [$OPT_NAME] target"
    else
        echo "Going to remove the $OPT_TYPE [$OPT_NAME]"
    fi
fi

if [[ $OPT_PACKAGE_INSTALL ]]; then
    echo "Going to install package(s) [${OPT_PACKAGE_NAMES[*]}] into the $OPT_TYPE [$OPT_NAME]"
fi

if [[ $OPT_PACKAGE_REMOVE ]]; then
    echo "Going to remove package(s) [${OPT_PACKAGE_NAMES[*]}] from the $OPT_TYPE [$OPT_NAME]"
fi

if [[ $OPT_MAINTAIN && $OPT_INTERACTIVE ]]; then
    if [[ ${#OPT_COMMAND[@]} -gt 0 ]]; then
        echo "Going to execute maintenance command under $OPT_TYPE [$OPT_NAME]"
    else
        echo "Going to open maintenance shell for $OPT_TYPE [$OPT_NAME]"
    fi
fi

[[ $OPT_DRY_RUN ]] && echo "[only print out what would be done]"

# confirm
if [[ $OPT_INTERACTIVE ]]; then
    while true; do
    read -p "Do you want to continue? (y/n) " answer
    case $answer in
        [Yy]*)
        break ;;
        [Nn]*)
        echo "Ok, exiting"
        exit 0
        ;;
        *)
        echo "Please answer yes or no."
        ;;
    esac
    done
fi

if [[ $OPT_CREATE ]]; then
    if [[ $OPT_TOOLING_NAME ]]; then
        _ sdk-manage "$OPT_TYPE" install "$OPT_NAME" "$OPT_URL" \
            --tooling "$OPT_TOOLING_NAME" \
            ${OPT_NO_SNAPSHOT:+--no-snapshot}
    else
        _ sdk-manage "$OPT_TYPE" install "$OPT_NAME" "$OPT_URL" \
            ${OPT_NO_SNAPSHOT:+--no-snapshot}
    fi
    exit
fi

if [[ $OPT_CLONE ]]; then
    _ sdk-manage "$OPT_TYPE" clone "$OPT_NAME" "$OPT_CLONE_NAME"
    exit
fi

if [[ $OPT_UPDATE ]]; then
    if [[ $OPT_TYPE == target ]]; then
        echo "Updating the tooling '$OPT_TOOLING_NAME'..." >&2
        _ sdk-manage ${OPT_INTERACTIVE:+--interactive} tooling refresh "$OPT_TOOLING_NAME" \
            && _ sdk-manage ${OPT_INTERACTIVE:+--interactive} tooling update "$OPT_TOOLING_NAME"
        rc=$?
        if [[ $rc -ne 0 ]]; then
            echo "Target update canceled:" \
                "An error occurred while updating the tooling '$OPT_TOOLING_NAME'" >&2
            exit $rc
        fi

        echo >&2
        echo >&2
    fi
    echo "Updating the $OPT_TYPE '$OPT_NAME'..." >&2
    _ sdk-manage ${OPT_INTERACTIVE:+--interactive} "$OPT_TYPE" refresh "$OPT_NAME" || exit
    _ sdk-manage ${OPT_INTERACTIVE:+--interactive} "$OPT_TYPE" update "$OPT_NAME"
    exit
fi

if [[ $OPT_REGISTER ]]; then
    common_options=(${OPT_USER:+--user "$OPT_USER"} ${OPT_PASSWORD:+--password "$OPT_PASSWORD"} --force)
    if [[ ! $OPT_TYPE && $OPT_ALL ]]; then
        _ sdk-manage register-all "${common_options[@]}" --no-sdk || exit
        _ sdk-manage refresh-all --no-sdk
    else
        _ sdk-manage "$OPT_TYPE" register "${common_options[@]}" ${OPT_ALL:+--all} \
            ${OPT_NAME:+"$OPT_NAME"} || exit
        _ sdk-manage "$OPT_TYPE" refresh ${OPT_ALL:+--all} ${OPT_NAME:+"$OPT_NAME"}
    fi
    exit
fi

if [[ $OPT_REMOVE ]]; then
    with_force=$([[ $OPT_TYPE == target ]] && echo 1)
    _ sdk-manage "$OPT_TYPE" remove ${with_force:+--force} ${OPT_SNAPSHOTS_OF:+--snapshots-of} \
        "$OPT_NAME"
    exit
fi

if [[ $OPT_PACKAGE_INSTALL ]]; then
    _ sdk-manage ${OPT_INTERACTIVE:+--interactive} "$OPT_TYPE" package-install "$OPT_NAME" "${OPT_PACKAGE_NAMES[@]}"
    exit
fi

if [[ $OPT_PACKAGE_REMOVE ]]; then
    _ sdk-manage ${OPT_INTERACTIVE:+--interactive} "$OPT_TYPE" package-remove "$OPT_NAME" "${OPT_PACKAGE_NAMES[@]}"
    exit
fi

if [[ $OPT_MAINTAIN ]]; then
    _ sdk-manage "$OPT_TYPE" maintain "$OPT_NAME" ${OPT_COMMAND:+"${OPT_COMMAND[@]}"}
    exit
fi

# For Emacs:
# Local Variables:
# indent-tabs-mode:nil
# tab-width:4
# mode: sh
# End:
# For VIM:
# vim:set softtabstop=4 shiftwidth=4 tabstop=4 expandtab:
