#! /bin/bash
#
# Script to start a JMRI 4.26.0 application.
#
# This script is used for both all POSIX operating systems including Linux
# and macOS / OS X application bundles.
#
# If you need to specify an option with spaces in it, escape the spaces with a
# leading backslash like "\ ".
#
# If you need to add any persistent Java options or persistent command line
# arguments, include them in the "default_options" statement in the file
# jmri.conf in the settings directory.
#
# The default location for the settings directory is:
# - macOS / OS X: ${HOME}/Library/Preferences/JMRI
# - all other: ${HOME}/.jmri
#
# The settings directory can be set to a non-standard location using the
# "--settingsdir=/path/to/my/settings" command line option (unlike all other
# options, this one cannot be set in the jmri.conf file, since it determines
# from where the jmri.conf file is read).
#
# If your serial ports are not in the default list, include the names in the
# command line argument --serial-ports separated by commas:
#    --serial-ports=locobuffer,cmri
#
# You can run separate instances of the program with their own preferences
# if you
# - Provide the name of a configuration file as a parameter
# or
# - Copy and rename this script.
#
# If you rename the script to, for example, JmriNew, it will use
# "JmriNewConfig.properties" as it's configuration file. Note that configuration
# files only determine which profile to use, and if the profile selector should
# be shown at application launch.
#
# If you are getting X11 warnings about meta keys, uncomment the next line
# xprop -root -remove _MOTIF_DEFAULT_BINDINGS
#
# For more information, please see
# http://jmri.org/help/en/html/doc/Technical/StartUpScripts.shtml

# prevent the use of unbound variables
set -u

# display valid arguments
function usage() {
    cat <<EOM
Usage: $( basename $0 ) [--help] [OPTIONS] [--] [ARGUMENTS]
  -c CONFIG, --config=CONFIG     Start JMRI with configuration CONFIG
  --cp:a=CLASSPATH               Append specified JARs to the classpath
                                 Multiple JARs are separated with colons (:)
  --cp:p=CLASSPATH               Prepend specified JARs to the classpath
                                 Multiple JARs are separated with colons (:)
  -d, --debug                    Add verbose output to this script
  -ea, -da, -enableassertions, -disableassertions Java assertions
  -DPROPERTY                     Set the Java System property PROPERTY
  -JOPTION                       Pass the option OPTION to the JVM
  -m MAIN, --main=MAIN           Use main() method in class MAIN to start JMRI
  -p PROFILE, --profile=PROFILE  Start JMRI with the profile PROFILE
  --serial-ports=SERIAL_PORTS    Use the serial ports in SERIAL_PORTS by name
                                 Multiple names are separated with commas (,)
  --settingsdir=SETTINGS_DIR     Use SETTINGS_DIR as the settings directory
  -T                             Running Jemmy tests, set options appropriately
  -t                             Not running Jemmy tests, use regular menu options
  --                             Do not process anything following as an option,
                                 even if it matches one of the above options
EOM
}

# parse passed in arguments from either default_options or command line
# except --settingsdir argument
function parse_args() {
    while [ $# -gt 0 ] ; do
        if [ -z "${1}" ] ; then
            shift
            continue
        fi
        # note: set DEBUG=yes in environment to see at this point
        [ -n "${DEBUG}" ] && echo "Parsing Arg: '${1}'" | tee -a ${launcher_log}
        case "${1}" in
            # append to classpath
            --cp:a) post_classpath="${post_classpath}:${2}" ; shift ;;
            --cp:a=*) post_classpath="${post_classpath}:${1#*=}" ;;
            # prepend to classpath
            --cp:p) pre_classpath="${pre_classpath}:${2}" ; shift ;;
            --cp:p=*) pre_classpath="${pre_classpath}:${1#*=}" ;;
            # use named configuration
            -c|--config) CONFIGNAME="${2}" ; shift ;;
            --config=*) CONFIGNAME="${1#*=}" ;;
            # Java system properties
            -D*) jmri_options="${jmri_options} ${1}" ;;
            # debugging
            -d|--debug) DEBUG="yes" ;;
            # help
            --help) usage ; exit 2 ;;
            # heap sizes
            -J-Xms*) jmri_xms="${1#-J}" ;;
            -J-Xmx*) jmri_xmx="${1#-J}" ;;
            # JVM arguments other than max memory
            -J*) jmri_options="${jmri_options} ${1#-J}" ;;
            # assertions
            -ea|-enableassertions) jmri_options="${jmri_options} ${1}" ;;
            -da|-disableassertions) jmri_options="${jmri_options} ${1}" ;;
            -ea:*|-enableassertions:*) jmri_options="${jmri_options} ${1}" ;;
            -da:*|-disableassertions:*) jmri_options="${jmri_options} ${1}" ;;
            # main class
            -m|--main) CLASSNAME="${2}" ; shift ;;
            --main=*) CLASSNAME="${1#*=}" ;;
            # JMRI configuration profile
            -p|--profile) jmri_options="${jmri_options} -Dorg.jmri.profile=${2}" ; shift ;;
            --profile=*) jmri_options="${jmri_options} -Dorg.jmri.profile=${1#*=}" ;;
            # serial ports
            --serial-ports) JMRI_SERIAL_PORTS="${2}" ; shift ;;
            --serial-ports=*) JMRI_SERIAL_PORTS="${1#*=}" ;;
            # ignore and do not pass the settingsdir argument
            --settingsdir) shift ;;
            --settingsdir=*) ;;
            # ignore and do not pass a ProcessSerialNumber argument (from the open command on macOS)
            -psn_*) ;;
            -T)  JEMMY="yes" ;;
            -t)  JEMMY="" ;;
            # everything after this is to be passed to JMRI
            --) ARGS="${ARGS} $@" ; break ;;
            # pass anything else on to JMRI
            *) ARGS="${ARGS} ${1}" ;;
        esac
        shift
    done
}

# Get the default heap size for JMRI in MB.
# Based on total memory size, this is:
# - 1/4 total memory size on systems with more than 4GB RAM with minimum of 2GB
# - 1/2 total memory size on systems with 1-4GB RAM with minimum of 768MB
# - 3/4 total memory size on systems with less than 1GB RAM
# - with an absolute minimum of 192MB
# - note that minimums are maximum allocation for immediately smaller RAM size
#   (i.e. as 2GB = 1/2 4GB, do not use 1.5GB on 6GB system (1.5GB = 1/4 6GB))
function heap_size() {
    # get Java heap size
    heap=$( "${JAVACMD}" -XX:+PrintFlagsFinal -version 2>/dev/null | grep MaxHeapSize | grep -v SoftMaxHeapSize | awk '{print $4}' )
    heap=$( expr ${heap} / 1048576 2>/dev/null ) # bytes to MB
    # Java heap defaults to 1/4 total memory size
    # if <= 768MB (1/4 of 3GB), set it ourselves
    if [ -z "${heap}" ] || [ ${heap} -le 768 ] ; then
        case "$( uname )" in
            Linux*)
                mem=$( cat /proc/meminfo | grep MemTotal | tr -d [:space:][:alpha:]: )
                mem=$( expr $mem / 1024 )
                ;;
            Darwin*)
                mem=$( /usr/sbin/sysctl hw.memsize | tr -d [:alpha:][:space:].: )
                mem=$( expr $mem / 1048576 )
                ;;
            *)
                ;;
        esac
        if [ -z "$mem" ] ; then
            mem=640
        fi
        if [ $mem -le 1024 ] ; then
            heap=$( expr $mem \* 3 / 4 )
        elif [ $mem -le 4096 ] ; then
            heap=$( expr $mem \* 1 / 2 )
            [ $heap -le 768 ] && heap=768 # 3/4 of 1024
        else
            heap=$( expr $mem \* 1 / 4 )
            [ $heap -le 2048 ] && heap=2048 # 1/2 of 4096
        fi
        if [ $heap -lt 192 ] ; then
            heap=192 # 3/4 of 256MB
        fi
    fi
    echo $heap
    return 0
}

# get the script's location as an absolute path
SCRIPTDIR=$(cd "$( dirname "${0}" )" && pwd)

# define the class to be invoked
DEFAULT_APP_NAME="JmriFaceless"
CLASSNAME="apps.JmriFaceless"

# define empty jmri_options
jmri_options=""

# define empty array of passed in options
declare -a all_options=()

# ensure JMRI environment options are always set
JMRI_OPTIONS=${JMRI_OPTIONS:-}
JMRI_SERIAL_PORTS=${JMRI_SERIAL_PORTS:-}

# set default config name
CONFIGNAME=""
CONFIGFILE=""

# Installation locations
JAVA_HOME=${JAVA_HOME:-}
JMRI_HOME=${JMRI_HOME:-}
BUNDLEDIR=""

# Set default arguments
ARGS=

# set DEBUG to any non-empty string to see debugging output
DEBUG=${DEBUG:-}

# set JEMMY to any non-empty string to configure for Jemmy testing
JEMMY=${JEMMY:-}

# set default classpaths additions
pre_classpath=""
post_classpath=""

# set the OS (can be overridden in environment for debugging and development)
# this value is used to find OS-specific libraries, so it gets normalized
# in following case statement
OS=${OS:-}
if [ -z "${OS}" ] ; then
  OS=$( uname -s )
fi
ARCH=${ARCH:-}

# set OS-specific settings
case "${OS}" in
    macosx|Darwin*)
        settingsdir="${HOME}/Library/Preferences/JMRI"
        OS="macosx"
        ;;
    Linux*)
        settingsdir="${HOME}/.jmri"
        OS="linux"
        ;;
    *)
        settingsdir="${HOME}/.jmri"
        ;;
esac

# get the settings directory if set on command line
found_settingsdir=""
for opt in "$@"; do
    if [ "${found_settingsdir}" = "yes" ]; then
        # --settingsdir /path/to/... part 2
        settingsdir="$opt"
        jmri_options="${jmri_options} -Djmri.prefsdir=${settingsdir}"
        break
    elif [ "$opt" = "--settingsdir" ]; then
        # --settingsdir /path/to/... part 1
        found_settingsdir="yes"
    elif [[ "$opt" =~ "--settingsdir=" ]]; then
        # --settingsdir=/path/to/...
        settingsdir="${opt#*=}"
        jmri_options="${jmri_options} -Djmri.prefsdir=${settingsdir}"
        break;
    fi
done

# log to $settingsdir/log/launcher.log for debugging purposes
launcher_log=${settingsdir}/log/launcher.log
mkdir -p ${settingsdir}/log
[ -f ${launcher_log} ] && rm -f ${launcher_log}

# process arguments, stored arguments first, so CLI arguments can override

# process default_options from the launcher configuration file
if [ -f "${settingsdir}/jmri.conf" ] ; then
    default_options=""
    source "${settingsdir}/jmri.conf"
    if [ -n "${default_options}" ] ; then
        IFS=' ' read -a all_options <<< "${default_options}"
    fi
fi

# process environment and command line arguments
if [ -n "${JMRI_OPTIONS}" ] ; then
    IFS=' ' read -a options <<< "${JMRI_OPTIONS}"
    for option in "${options[@]}" ; do
        all_options=("${all_options[@]-}" "${option}")
    done
fi
if [ $# -gt 0 ] ; then
    for option in "$@"; do
        all_options=("${all_options[@]-}" "${option}")
    done
fi
parse_args "${all_options[@]:-}"

# define JAVA_HOME if needed
if [ -z "${JAVA_HOME}" ] ; then
    #
    # macOS / OS X get special treatment
    # all others use which to find java
    # otherwise error out
    #
    if [ "$OS" = "macosx" ] ; then
        #
        # macOS / OS X has a bewildering array of possibilities these days
        #
        # /usr/libexec/java_home will find the correct version, if a JDK for the
        # required version or newer is installed.
        #
        # As 10.8 and newer do not ship with a JRE, this command will prompt
        # the user and download a JRE as needed.
        #
        # But if the user already has the Oracle Java 8 JRE installed, we should
        # use that instead of forcing a download of Apple's Java 6 (10.11 and
        # newer will always direct the user to Oracle for Java.)
        #
        # Look for the Oracle Java 8 JRE and use it if found
        # Otherwise, use java_home if it's present
        # Otherwise, prompt the user to install Java
        #
        # Oracle JRE location; note that Oracle only shipped JREs for Java 8, 9, and 10
        # and now only ships the JRE for Java 8.
        JAVA_HOME="/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home"
        if [ -x "${JAVA_HOME}/bin/java" ] ; then
            JAVACMD="${JAVA_HOME}/bin/java"
            # Test if java is present and version 1.8, 9, or 10
            java_version="$( "${JAVACMD}" -version 2>&1 | grep version )"
            if [[ ! "${java_version}" =~ \"1.8|\"9|\"10 ]] ; then
                JAVA_HOME=""
            fi
        else
            # Reset JAVA_HOME since there is no java executable in it
            JAVA_HOME=""
        fi
        # Find an installed JRE/JDK location
        for jversion in 1.8 9 10 11 12 13 14 15 16 17 ; do
            if [[ -z "${JAVA_HOME}" && -x /usr/libexec/java_home ]] ; then
                # Test for any JDKs
                JAVA_HOME=$( /usr/libexec/java_home --version ${jversion} --failfast 2>/dev/null )
                if [ -n "${JAVA_HOME}" ] ; then
                    JAVACMD="${JAVA_HOME}/bin/java"
                    break
                fi
            fi
        done
        if [ -z "${JAVA_HOME}" ] ; then
            # JAVA_HOME is still not defined, so prompt to install Java for OS X
            /usr/bin/osascript << EOT
            try
                display alert "To use JMRI you need to install a JRE." \
                    message "Click \"More Info...\" to visit the Java Runtime Environment download website." \
                    buttons {"More Info...", "OK"} \
                    default button "OK" \
                    cancel button "More Info..."
            on error number -128
                -- user pressed cancel button
                open location "https://www.java.com/en/download/mac_download.jsp"
            end try
EOT
            exit 1
        fi
    elif which java >/dev/null 2>&1 ; then
        JAVACMD=$( which java )
        JAVA_HOME="$( dirname ${JAVACMD} )/.."
    else
        echo "Please install Java 1.8 per your operating system vendor's instructions." | tee -a ${launcher_log}
        exit 1
    fi
else
    JAVACMD="${JAVA_HOME}/bin/java"
    if [ ! -x "${JAVACMD}" ] ; then
        echo "Unable to execute java using JAVA_HOME=\"${JAVA_HOME}\"." | tee -a ${launcher_log}
        exit 1
    fi
fi
# make JAVA_HOME available to spawned processes
export JAVA_HOME

# set if -J-Xmx=... is not in options or arguments
if [ -z "${jmri_xmx:-}" ] ; then
    jmri_xmx="-Xmx$( heap_size )m"
fi
# set if -J-Xms=... is not in options or arguments
if [ -z "${jmri_xms:-}" ] ; then
    # initial heap size = default for 6 GB RAM (default = 1/64 installed RAM)
    jmri_xms="-Xms96m"
fi

# permit Java 9 illegal access
if [[ $( "${JAVACMD}" -version 2>&1 | grep version ) =~ \"9 ]] ; then
    jmri_options="${jmri_options} --illegal-access=warn"
fi

# define JMRI_HOME if it is not defined
if [ -z "${JMRI_HOME}" ] ; then
    JMRI_HOME="${SCRIPTDIR}"
    if [ "$OS" = "macosx" ] ; then
        # on OS X, the .app bundle is assumed to reside in the default JMRI_HOME
        BUNDLEDIR=$(cd "${SCRIPTDIR}/../.." && pwd)
        if [ -f "${BUNDLEDIR}/Contents/MacOS/StartJMRI" ] ; then
            JMRI_HOME=$(cd "${BUNDLEDIR}/.." && pwd)
            DEBUG="yes" # default to on, so always available in Console.app
        else
            BUNDLEDIR=""
        fi
    fi
fi

cd "${JMRI_HOME}"
[ -n "${DEBUG}" ] && echo "PWD: '${PWD}'" | tee -a ${launcher_log}


# build library path
SYSLIBPATH=


LIBDIR="lib"

if [ -d "${LIBDIR}/$OS" ] ; then
    SYSLIBPATH="${LIBDIR}/$OS"
fi

# one or another of these commands should return a useful value, except that sometimes
# it is spelled funny (e,g, amd64, not x86_64).

if [ -z "$ARCH" ] ; then
    for cmd in "arch" "uname -i" "uname -p" "uname -m" ; do
        ARCH=$( $cmd 2>/dev/null )
        if [ -n "$ARCH" ] ; then
            # canonicalize the architecture names where possible
            # we currently have AMD64 / X86_64
            if [ "$ARCH" = "amd64" ] ; then
                ARCH="x86_64"
            fi

            # and all the flavors of ia32 (traditional x86)
            if [ "$ARCH" = "i686" -o "$ARCH" = "i586" -o "$ARCH" = "i486" ] ; then
                ARCH="i386"
            fi

            # Now deal with ARM architecture:
            #   armv5 contains v5 soft-float version
            #   armv6l contains v6 hard-float version
            #   armv7l contains v7 hard-float version
            #   aarch64 contains 64-bit v8 version

            if [ "${ARCH:0:3}" = "arm" ] ; then

                #determine arm version & hard vs. soft float using readelf
				if type "readelf" >& /dev/null; then
					ARCHVERSION=$( readelf -A /proc/self/exe | grep Tag_CPU_arch | cut -d : -f2 )
					if [ -z "$ARCHVERSION" -o "${ARCHVERSION:1:2}" = "v5" ] ; then
						ARCH="armv5"
					elif [ "${ARCHVERSION:1:2}" = "v6" ] ; then
						ARCH="armv6l"
					elif [ "${ARCHVERSION:1:2}" = "v7" ] ; then
						ARCH="armv7l"
					else
						ARCH="armv5"
					fi
				fi
            fi

            if [ "$ARCH" = "aarch64" -o "$ARCH" = "arm64" ] ; then
                ARCH="aarch64"
            fi

            if [ -d "${SYSLIBPATH}/$ARCH" ] ; then
                SYSLIBPATH="${SYSLIBPATH}/$ARCH:$SYSLIBPATH"

                # we're only interested in ONE of these values, so as soon as we find a supported
                # architecture directory, continue processing and start up the program
                break
            fi
        fi
    done
fi

# build classpath dynamically
CP=""
if [ -n "${pre_classpath}" ] ; then
    pre_classpath="${pre_classpath#:}"
    CP="${pre_classpath}:"
fi
CP="${CP}.:classes:target/classes:jmri.jar"
# add contents of lib
CP="${CP}:$( ls -m ${LIBDIR}/*.jar | tr -d ' \n' | tr ',' ':' )"

if [ -n "${post_classpath}" ] ; then
    post_classpath="${post_classpath#:}"
    CP="${CP}:${post_classpath}"
fi

# remove any "\ " escaped spaces, since these are needed for bash, but not java
CP="${CP//\\ / }"

[ -n "${DEBUG}" ] && echo "CLASSPATH: '${CP}'" | tee -a ${launcher_log}

[ -n "${DEBUG}" ] && echo "Java CMD: '${JAVACMD[@]}'" | tee -a ${launcher_log}

# configuration file name is 1st argument.
# If not provided, build config file name dynamically
if [ "$OS" = "macosx" -a -n "${BUNDLEDIR}" ] ; then
    # OS X can have spaces in the application name
    APPNAME=$( basename -s .app "${BUNDLEDIR}" )
else
    APPNAME=$( basename "$0" )
fi

[ -n "${DEBUG}" ] && echo "APPNAME: '${APPNAME}'" | tee -a ${launcher_log}

# Process a config file name if passed as an option or when the application is
# NOT the default one this script was built for and pass it as a Java property
# so that scripts expecting another argument as the first one get that instead
if [ "$APPNAME" != "$DEFAULT_APP_NAME" -o -n "${CONFIGNAME}" ] ; then
    if [ -z "${CONFIGNAME}" ] ; then
        CONFIGNAME=$( echo $APPNAME | tr -d '[:space:]' | tr -d '=' )
    fi
    CONFIGFILE="-Dorg.jmri.Apps.configFilename=${CONFIGNAME}Config.xml"
    [ -n "${DEBUG}" ] && echo "CONFIGFILE: '${CONFIGFILE}'" | tee -a ${launcher_log}
fi

# create the option string
#
# Add JVM and RMI options to user options, if any

if [ "$OS" = "macosx" ] ; then
    # since bash mangles the application name and icon in $OPTIONS,
    # these are not stored in $OPTIONS
    OS_OPTIONS=""
        # omit custom menu bar if running Jemmy tests
        if [ "$JEMMY" = "" ] ; then
        OS_OPTIONS="${OS_OPTIONS} -Dapple.laf.useScreenMenuBar=true"
        OS_OPTIONS="${OS_OPTIONS} -Dcom.apple.macos.useScreenMenuBar=true"
    fi
    OS_OPTIONS="${OS_OPTIONS} -Dfile.encoding=UTF-8"
    if [ -f "${BUNDLEDIR}/Contents/Resources/@ICON@.icns" ] ; then
        APPICON="${BUNDLEDIR}/Contents/Resources/@ICON@.icns"
    else
        APPICON=$( find "${BUNDLEDIR}" 2>/dev/null | grep -s -m 1 icns )
    fi
fi

OPTIONS="${jmri_options} -noverify"
OPTIONS="${OPTIONS} -Djava.security.policy=${LIBDIR}/security.policy"
OPTIONS="${OPTIONS} -Djava.rmi.server.codebase=file:target/classes/"
OPTIONS="${OPTIONS} -Djava.library.path=.:$SYSLIBPATH:${LIBDIR}"

# memory start and max limits
OPTIONS="${OPTIONS} ${OS_OPTIONS-} ${jmri_xms} ${jmri_xmx}"
[ -n "${DEBUG}" ] && echo "OPTIONS: '${OPTIONS}'" | tee -a ${launcher_log}

# handle ports in --settings argument or JMRI_SERIAL_PORTS environment variable
# but not if already in OPTIONS
ALTPORTS=
if [[ -n "${JMRI_SERIAL_PORTS}" && ! ${OPTIONS} =~ "-Dpurejavacomm.portnamepattern=" ]] ; then
    ALTPORTS="-Dpurejavacomm.portnamepattern=${JMRI_SERIAL_PORTS}"
fi

declare -a DOCK_OPTIONS
# Apple dock extensions to java get mangled by bash if included in ${OPTIONS}
if [ "$OS" = "macosx" ] ; then
    DOCK_OPTIONS=(-Xdock:name="${APPNAME}" -Xdock:icon="${APPICON}")
    [ -n "${DEBUG}" ] && echo "DOCK_OPTIONS: '${DOCK_OPTIONS[@]}'" | tee -a ${launcher_log}
fi


RESTART_CODE=100
EXIT_STATUS=${RESTART_CODE}
while [ "${EXIT_STATUS}" -eq "${RESTART_CODE}" ] ; do

    if [ "$OS" = "macosx" ] ; then
        "${JAVACMD}" "${DOCK_OPTIONS[@]}" ${OPTIONS} ${ALTPORTS} ${CONFIGFILE} -cp "${CP}" "${CLASSNAME}" ${ARGS}
    else
        "${JAVACMD}" ${OPTIONS} ${ALTPORTS} ${CONFIGFILE} -cp "${CP}" "${CLASSNAME}" ${ARGS}
    fi

    EXIT_STATUS=$?
    [ -n "${DEBUG}" ] && echo Exit Status: "${EXIT_STATUS}" | tee -a ${launcher_log}
done
if [ $EXIT_STATUS -eq 200 ] ; then
    sudo shutdown -h now
fi
if [ $EXIT_STATUS -eq 210 ] ; then
    sudo shutdown -r now
fi
exit $EXIT_STATUS
