#!/bin/bash
set -e

# Copyright 2019 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Mounts the components of soong into a directory structure that Go tools
# and editors expect.


#####################################################################
# Print the message to stderr with the prefix ERROR and abort this
# script.
#####################################################################
function log_FATAL() {
  echo "ERROR:" "$*" >&2
  exit 1
}

#####################################################################
# Print the message to stderr with the prefix WARN
#####################################################################
function log_WARN() {
  echo "WARN:" "$*" >&2
}


#####################################################################
# Print the message with the prefix INFO.
#####################################################################
function log_INFO() {
  echo "INFO:" "$*"
}


#####################################################################
# Find the root project directory of this repo. This is done by
# finding the directory of where this script lives and then go up one
# directory to check the ".repo" directory exist. If not, keep going
# up until we find the ".repo" file or we reached to the filesystem
# root. Project root directory is printed to stdout.
#####################################################################
function root_dir() (
  local dir
  if ! dir="$("${readlink}" -e $(dirname "$0"))"; then
    log_FATAL "failed to read the script's current directory."
  fi

  dir=${dir}/../../..
  if ! dir="$("${readlink}" -e "${dir}")"; then
    log_FATAL "Cannot find the root project directory"
  fi

  echo "${dir}"
)


#####################################################################
# executes a shell command by printing out to the screen first and
# then evaluating the command.
#####################################################################
function execute() {
  echo "$@"
  eval "$@"
}


#####################################################################
# Returns the source directory of a passed in path from BIND_PATHS
# array.
#####################################################################
function bind_path_src_dir() (
  local -r bind_path="$1"
  echo "${bind_path/%|*/}"
)


#####################################################################
# Returns the destination directory of a passed in path from
# BIND_PATHS array.
#####################################################################
function bind_path_dst_dir() (
  local -r bind_path="$1"
  echo  "${bind_path/#*|}"
)


#####################################################################
# Executes the bindfs command in linux. Expects $1 to be src
# directory and $2 to be destination directory.
#####################################################################
function linux_bind_dir() (
  execute bindfs "$1" "$2"
)

#####################################################################
# Executes the fusermount -u command in linux. Expects $1 to be the
# destination directory.
#####################################################################
function linux_unbind_dir() (
  execute fusermount -u "$1"
)

#####################################################################
# Executes the bindfs command in darwin. Expects $1 to be src
# directory and $2 to be destination directory.
#####################################################################
function darwin_bind_dir() (
  execute bindfs -o allow_recursion -n "$1" "$2"
)


#####################################################################
# Execute the umount command in darwin to unbind a directory. Expects
# $1 to be the destination directory
#####################################################################
function darwin_unbind_dir() (
  execute umount -f "$1"
)


#####################################################################
# Bind all the paths that are specified in the BIND_PATHS array.
#####################################################################
function bind_all() (
  local src_dir
  local dst_dir

  for path in ${BIND_PATHS[@]}; do
    src_dir=$(bind_path_src_dir "${path}")

    dst_dir=$(bind_path_dst_dir "${path}")
    mkdir -p "${dst_dir}"

    "${bind_dir}" ${src_dir} "${dst_dir}"
  done

  echo
  log_INFO "Created GOPATH-compatible directory structure at ${OUTPUT_PATH}."
)


#####################################################################
# Unbind all the paths that are specified in the BIND_PATHS array.
#####################################################################
function unbind_all() (
  local dst_dir
  local exit_code=0

  # need to go into reverse since several parent directory may have been
  # first before the child one.
  for (( i=${#BIND_PATHS[@]}-1; i>=0; i-- )); do
    dst_dir=$(bind_path_dst_dir "${BIND_PATHS[$i]}")

    # continue to unmount even one of them fails
    if ! "${unbind_dir}" "${dst_dir}"; then
      log_WARN "Failed to umount ${dst_dir}."
      exit_code=1
    fi
  done

  if [[ ${exit_code} -ne 0 ]]; then
    exit ${exit_code}
  fi

  echo
  log_INFO "Unmounted the GOPATH-compatible directory structure at ${OUTPUT_PATH}."
)


#####################################################################
# Asks the user to create the GOPATH-compatible directory structure.
#####################################################################
function confirm() (
  while true; do
    echo "Will create GOPATH-compatible directory structure at ${OUTPUT_PATH}"
    echo -n "Ok [Y/n]?"
    read decision
    if [ "${decision}" == "y" -o "${decision}" == "Y" -o "${decision}" == "" ]; then
      return 0
    else
      if [ "${decision}" == "n" ]; then
        return 1
      else
        log_WARN "Invalid choice ${decision}; choose either 'y' or 'n'"
      fi
    fi
  done
)


#####################################################################
# Help function.
#####################################################################
function help() (
  cat <<EOF
Mounts the components of soong into a directory structure that Go tools
and editors expect.

  --help
    This help

  --bind
    Create the directory structure that Go tools and editors expect by
    binding the one to aosp build directory.

  --unbind
    Reverse operation of bind.

If no flags were specified, the --bind one is selected by default.
EOF
)


#####################################################################
# Parse the arguments passed in to this script.
#####################################################################
function parse_arguments() {
  while [[ -n "$1" ]]; do
    case "$1" in
          --bind)
            ACTION="bind"
            shift
            ;;
          --unbind)
            ACTION="unbind"
            shift
            ;;
          --help )
            help
            shift
            exit 0
            ;;
          *)
            log_WARN "Unknown option: $1"
            help
            exit 1
            ;;
    esac
  done

  if [[ -z "${ACTION}" ]]; then
    ACTION=bind
  fi
}


#####################################################################
# Verifies that a list of required binaries are installed in the
# host in order to run this script.
#####################################################################
function check_exec_existence() (
  function check() {
    if ! hash "$1" &>/dev/null; then
      log_FATAL "missing $1"
    fi
  }

  local bins
  case "${os_type}" in
    Darwin)
      bins=("bindfs" "greadlink")
      ;;
    Linux)
      bins=("bindfs" "fusermount")
      ;;
    *)
      log_FATAL "${os_type} is not a recognized system."
  esac

  for bin in "${bins[@]}"; do
    check "${bin}"
  done
)


function main() {
  parse_arguments "$@"

  check_exec_existence

  if [[ "${ACTION}" == "bind" ]]; then
    if confirm; then
      echo
      bind_all
    else
      echo "skipping due to user request"
      exit 1
    fi
  else
    echo
    unbind_all
  fi
}

readonly os_type="$(uname -s)"
case "${os_type}" in
  Darwin)
    bind_dir=darwin_bind_dir
    unbind_dir=darwin_unbind_dir
    readlink=greadlink
    ;;
  Linux)
    bind_dir=linux_bind_dir
    unbind_dir=linux_unbind_dir
    readlink=readlink
    ;;
    *)
    log_FATAL "${os_type} is not a recognized system."
esac
readonly bind_dir
readonly unbind_dir
readonly readlink


if ! ANDROID_PATH="$(root_dir)"; then
  log_FATAL "failed to find the root of the repo checkout"
fi
readonly ANDROID_PATH

#if GOPATH contains multiple paths, use the first one
if ! OUTPUT_PATH="$(echo ${GOPATH} | sed 's/\:.*//')"; then
  log_FATAL "failed to extract the first GOPATH environment variable"
fi
readonly OUTPUT_PATH
if [ -z "${OUTPUT_PATH}" ]; then
  log_FATAL "Could not determine the desired location at which to create a" \
            "Go-compatible workspace. Please update GOPATH to specify the" \
            "desired destination directory."
fi

# Below are the paths to bind from src to dst. The paths are separated by |
# where the left side is the source and the right side is destination.
readonly BIND_PATHS=(
  "${ANDROID_PATH}/build/blueprint|${OUTPUT_PATH}/src/github.com/google/blueprint"
  "${ANDROID_PATH}/build/soong|${OUTPUT_PATH}/src/android/soong"
  "${ANDROID_PATH}/art/build|${OUTPUT_PATH}/src/android/soong/art"
  "${ANDROID_PATH}/external/golang-protobuf|${OUTPUT_PATH}/src/github.com/golang/protobuf"
  "${ANDROID_PATH}/external/llvm/soong|${OUTPUT_PATH}/src/android/soong/llvm"
  "${ANDROID_PATH}/external/clang/soong|${OUTPUT_PATH}/src/android/soong/clang"
)

main "$@"