#-------------------------------------------------------------------------------
# VirtualBoxManage
#-------------------------------------------------------------------------------

VBM=vbm
: ${VBM_LOG:=$LOG_DIR/vbm.log}

# vbm is a wrapper around the VirtualBox VBoxManage executable; it handles
# logging and conditional execution (set OSBASH= to prevent the actual call to
# VBoxManage, or WBATCH= to keep a call from being recorded for Windows batch
# files)
function vbm {
    ${WBATCH:-:} wbatch_log_vbm "$@"

    mkdir -p "$(dirname "$VBM_LOG")"

    if [[ -n "${OSBASH:-}" ]]; then
        echo "$@" >> "$VBM_LOG"
        local rc=0
        "$VBM_EXE" "$@" || rc=$?
        if [ $rc -ne 0 ]; then
            echo >&2 "FAILURE: VBoxManage: $@"
            return 1
        fi
    else
        echo "(not executed) $@" >> "$VBM_LOG"
    fi
}

function get_vb_version {
    local VERSION=""
    local RAW=$(WBATCH= $VBM --version)
    local re='([0-9]+\.[0-9]+\.[0-9]+).*'
    if [[ $RAW =~ $re ]]; then
        VERSION=${BASH_REMATCH[1]}
    fi
    echo "$VERSION"
}

#-------------------------------------------------------------------------------
# VM status
#-------------------------------------------------------------------------------

function vm_exists {
    local VM_NAME=$1
    return $(WBATCH= $VBM list vms | grep -q "\"$VM_NAME\"")
}

function vm_is_running {
    local VM_NAME=$1
    return $(WBATCH= $VBM showvminfo --machinereadable "$VM_NAME" | \
        grep -q 'VMState="running"')
}

function vm_wait_for_shutdown {
    local VM=$1

    ${WBATCH:-:} wbatch_wait_poweroff "$VM"
    # Return if we are just faking it for wbatch
    ${OSBASH:+:} return 0

    echo >&2 -n "Machine shutting down"
    until WBATCH= $VBM showvminfo --machinereadable "$VM" 2>/dev/null | \
            grep -q '="poweroff"'; do
        echo -n .
        sleep 1
    done
    echo >&2 -e "\nMachine powered off."
}

function vm_power_off {
    local VM_NAME=$1
    if vm_is_running "$VM_NAME"; then
        echo >&2 "Powering off VM \"$VM_NAME\""
        $VBM controlvm "$VM_NAME" poweroff
    fi
    # VirtualBox VM needs a break before taking new commands
    vbox_sleep 1
}

function vm_snapshot {
    local VM_NAME=$1
    local SHOT_NAME=$2

    # Blanks would fail in Windows batch files; space becomes underscore
    SHOT_NAME="${SHOT_NAME// /_}"

    $VBM snapshot "$VM_NAME" take "$SHOT_NAME"
    # VirtualBox VM needs a break before taking new commands
    vbox_sleep 1
}

#-------------------------------------------------------------------------------
# Host-only network functions
#-------------------------------------------------------------------------------

function hostonlyif_in_use {
    local NAME=$1
    return $(WBATCH= $VBM list -l runningvms | \
        grep -q "Host-only Interface '$NAME'")
}

function ip_to_hostonlyif {
    local IP=$1
    local prevline=""
    WBATCH= $VBM list hostonlyifs | grep -e "^Name:" -e "^IPAddress:" | \
    while read line; do
        if [[ "$line" == *$IP* ]]; then
            # match longest string that ends with a space
            echo ${prevline##Name:* }
            break
        fi
        prevline=$line
    done
}

function create_hostonlyif {
    local OUT=$(WBATCH= $VBM hostonlyif create 2> /dev/null | grep "^Interface")
    # OUT is something like "Interface 'vboxnet3' was successfully created"
    local re="Interface '(.*)' was successfully created"
    if [[ $OUT =~ $re ]]; then
        echo "${BASH_REMATCH[1]}"
    else
        echo >&2 "Host-only interface creation failed"
        return 1
    fi
}

function create_network {
    local IP=$1

    # XXX We need host-only interface names as identifiers for wbatch; by
    #     always executing VBoxManage calls to ip_to_hostonlyif and
    #     create_hostonlyif we avoid the need to invent fake interface names

    local NAME="$(OSBASH=exec_cmd ip_to_hostonlyif "$IP")"
    if [ -n "$NAME" ]; then
        if hostonlyif_in_use "$NAME"; then
            echo >&2 "Host-only interface $NAME ($IP) is in use. Using it, too."
        fi
    else
        echo >&2 "Creating host-only interface"
        NAME=$(OSBASH=exec_cmd  create_hostonlyif)
    fi

    echo >&2 "Configuring host-only network $IP ($NAME)"
    $VBM hostonlyif ipconfig "$NAME" \
        --ip "$IP" \
        --netmask 255.255.255.0 >/dev/null
    echo "$NAME"
}

#-------------------------------------------------------------------------------
# Disk functions
#-------------------------------------------------------------------------------

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Creating, registering and unregistering disk images with VirtualBox
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# DISK can be either a path or a disk UUID
function disk_registered {
    local DISK=$1
    return $(WBATCH= $VBM list hdds | grep -q "$DISK")
}

# DISK can be either a path or a disk UUID
function disk_unregister {
    local DISK=$1
    echo >&2 -e "Unregistering disk\n\t$DISK"
    $VBM closemedium disk "$DISK"
}

function create_vdi {
    local HDPATH=$1
    local SIZE=$2
    echo >&2 -e "Creating disk:\n\t$HDPATH"
    $VBM createhd --format VDI --filename "$HDPATH" --size "$SIZE"
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Attaching and detaching disks from VMs
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# DISK can be either a path or a disk UUID
function get_next_child_uuid {
    local DISK=$1
    local CHILD_UUID=""
    if disk_registered "$DISK"; then
        local LINE=$(WBATCH= $VBM showhdinfo "$DISK" | grep -e "^Child UUIDs:")
        CHILD_UUID=${LINE##Child UUIDs:* }
    fi
    echo -e "next_child_uuid $DISK:\n\t$LINE\n\t$CHILD_UUID" >> "$VBM_LOG"
    echo "$CHILD_UUID"
}

# DISK can be either a path or a disk UUID
function path_to_disk_uuid {
    local DISK=$1
    local UUID=""
    local LINE=$(WBATCH= $VBM showhdinfo "$DISK" | grep -e "^UUID:")
    local re='UUID:[ ]+([^ ]+)'
    if [[ $LINE =~ $re ]]; then
        UUID=${BASH_REMATCH[1]}
    fi
    echo -e "path_to_disk_uuid $DISK:\n\t$LINE\n\t$UUID" >> "$VBM_LOG"
    echo "$UUID"
}

# DISK can be either a path or a disk UUID
function disk_to_path {
    local DISK=$1
    local FPATH=""
    local LINE=$(WBATCH= $VBM showhdinfo "$DISK" | grep -e "^Location:")
    local re='Location:[ ]+([^ ]+)'
    if [[ $LINE =~ $re ]]; then
        FPATH=${BASH_REMATCH[1]}
    fi
    echo -e "disk_to_path $DISK:\n\t$LINE\n\t$FPATH" >> "$VBM_LOG"
    echo "$FPATH"
}

# DISK can be either a path or a disk UUID
function disk_to_vm {
    local DISK=$1
    local VM_NAME=""
    local LINE=$(WBATCH= $VBM showhdinfo "$DISK" | grep -e "^In use by VMs:")
    local re='In use by VMs:[ ]+([^ ]+) '
    if [[ $LINE =~ $re ]]; then
        VM_NAME=${BASH_REMATCH[1]}
    fi
    echo -e "disk_to_vm $DISK:\n\t$LINE\n\t$VM_NAME" >> "$VBM_LOG"
    echo "$VM_NAME"
}

function vm_get_disk_path {
    local VM_NAME=$1
    local LINE=$(WBATCH= $VBM showvminfo --machinereadable "$VM_NAME" | \
        grep '^"SATA-0-0"=.*vdi"$')
    local HDPATH=${LINE##\"SATA-0-0\"=\"}
    HDPATH=${HDPATH%\"}
    echo "$HDPATH"
}

function vm_detach_disk {
    local VM_NAME=$1
    echo >&2 "Detaching disk from VM \"$VM_NAME\""
    $VBM storageattach "$VM_NAME" \
        --storagectl SATA \
        --port 0 \
        --device 0 \
        --type hdd \
        --medium none
    # VirtualBox VM needs a break before taking new commands
    vbox_sleep 1
}

# DISK can be either a path or a disk UUID
function vm_attach_disk {
    local VM_NAME=$1
    local DISK=$2
    echo >&2 -e "Attaching to VM \"$VM_NAME\":\n\t$DISK"
    $VBM storageattach "$VM_NAME" \
        --storagectl SATA \
        --port 0 \
        --device 0 \
        --type hdd \
        --medium "$DISK"
}

# DISK can be either a path or a disk UUID
function vm_attach_disk_multi {
    local VM_NAME=$1
    local DISK=$2
    echo >&2 -e "Attaching to VM \"$VM_NAME\":\n\t$DISK"
    $VBM storageattach "$VM_NAME" \
        --storagectl SATA \
        --port 0 \
        --device 0 \
        --type hdd \
        --medium "$DISK" \
        --mtype multiattach
}

#-------------------------------------------------------------------------------
# VM create and configure
#-------------------------------------------------------------------------------

function vm_mem {
    local NAME="$1"
    local MEM="$2"
    $VBM modifyvm "$NAME" --memory "$MEM"
}

# Port forwarding from host to VM (binding to host's 127.0.0.1)
function vm_port {
    local NAME="$1"
    local DESC="$2"
    local HOSTPORT="$3"
    local GUESTPORT="$4"
    $VBM modifyvm "$NAME" --natpf1 "$DESC,tcp,127.0.0.1,$HOSTPORT,,$GUESTPORT"
}

function vm_nic_hostonly {
    local VM=$1
    # We start counting interfaces at 0, but VirtualBox starts NICs at 1
    local NIC=$(($2 + 1))
    local NETNAME=$3
    $VBM modifyvm "$VM" \
        "--nictype$NIC" "$NICTYPE" \
        "--nic$NIC" hostonly \
        "--hostonlyadapter$NIC" "$NETNAME" \
        "--nicpromisc$NIC" allow-all
}

function vm_nic_nat {
    local VM=$1
    # We start counting interfaces at 0, but VirtualBox starts NICs at 1
    local NIC=$(($2 + 1))
    $VBM modifyvm "$VM" "--nictype$NIC" "$NICTYPE" "--nic$NIC" nat
}

function vm_create {
    # NOTE: We assume that a VM with a matching name is ours.
    #       Remove and recreate just in case someone messed with it.
    local VM_NAME="$1"

    ${WBATCH:-:} wbatch_abort_if_vm_exists "$VM_NAME"

    # Don't write to wbatch scripts, and don't execute when we are faking it
    # it for wbatch
    WBATCH= ${OSBASH:-:} vm_delete "$VM_NAME"

    # XXX ostype is distro-specific; moving it to modifyvm disables networking

    # Note: The VirtualBox GUI may not notice group changes after VM creation
    #       until GUI is restarted. Moving a VM with group membership will
    #       fail in cases (lingering files from old VM) where creating a
    #       VM in that location succeeds.
    #
    # XXX temporary hack
    # --groups not supported in VirtualBox 4.1 (Mac OS X 10.5)
    echo >&2 "Creating VM \"$VM_NAME\""
    local VER=$(get_vb_version)
    if [[ $VER = 4.1*  ]]; then
        $VBM createvm \
            --name "$VM_NAME" \
            --register \
            --ostype Ubuntu_64 >/dev/null
    else
        $VBM createvm \
            --name "$VM_NAME" \
            --register \
            --ostype Ubuntu_64 \
            --groups "/$VM_GROUP"  >/dev/null
    fi

    $VBM modifyvm "$VM_NAME" --rtcuseutc on
    $VBM modifyvm "$VM_NAME" --biosbootmenu disabled
    $VBM modifyvm "$VM_NAME" --largepages on
    $VBM modifyvm "$VM_NAME" --boot1 disk

    # XXX temporary hack
    # --portcount not supported in VirtualBox 4.1 (Mac OS X 10.5)
    if [[ $VER == 4.1*  ]]; then
        $VBM storagectl "$VM_NAME" --name SATA --add sata
    else
        $VBM storagectl "$VM_NAME" --name SATA --add sata --portcount 1
    fi

    $VBM storagectl "$VM_NAME" --name IDE --add ide
    echo >&2 "Created VM \"$VM_NAME\""
}

#-------------------------------------------------------------------------------
# VM unregister, remove, delete
#-------------------------------------------------------------------------------

function vm_unregister_del {
    local VM_NAME=$1
    echo >&2 "Unregistering and deleting VM \"$VM_NAME\""
    $VBM unregistervm "$VM_NAME" --delete
}

function vm_delete {
    local VM_NAME=$1
    echo >&2 -n "Asked to delete VM \"$VM_NAME\" "
    if vm_exists "$VM_NAME"; then
        echo >&2 "(found)"
        vm_power_off "$VM_NAME"
        local HDPATH="$(vm_get_disk_path "$VM_NAME")"
        if [ -n "$HDPATH" ]; then
            echo >&2 -e "Disk attached: $HDPATH"
            vm_detach_disk "$VM_NAME"
            disk_unregister "$HDPATH"
            echo >&2 -e "Deleting: $HDPATH"
            rm -f "$HDPATH"
        fi
        vm_unregister_del "$VM_NAME"
    else
        echo >&2 "(not found)"
    fi
}

# Remove VMs using disk and its children disks
# DISK can be either a path or a disk UUID
function disk_delete_child_vms {
    local DISK=$1
    if ! disk_registered "$DISK"; then
        # VirtualBox doesn't know this disk; we are done
        echo >&2 -e "Disk not registered with VirtualBox:\n\t$DISK"
        return 0
    fi

    # XXX temporary hack
    # No Child UUIDs through showhdinfo in VirtualBox 4.1 (Mac OS X 10.5)
    local VER=$(get_vb_version)
    if [[ $VER == 4.1*  ]]; then
        local VM=""
        for VM in controller network compute base; do
            vm_delete "$VM"
        done
        return 0
    fi

    while [ : ]; do
        local CHILD_UUID=$(get_next_child_uuid "$DISK")
        if [ -n "$CHILD_UUID" ]; then
            local CHILD_DISK="$(disk_to_path "$CHILD_UUID")"
            echo >&2 -e "\nChild disk UUID: $CHILD_UUID\n\t$CHILD_DISK"

            local VM="$(disk_to_vm "$CHILD_UUID")"
            if [ -n "$VM" ]; then
                echo 2>&1 -e "\tstill attached to VM \"$VM\""
                vm_delete "$VM"
            else
                echo >&2 "Unregistering and deleting: $CHILD_UUID"
                disk_unregister "$CHILD_UUID"
                echo >&2 -e "\t$CHILD_DISK"
                rm -f "$CHILD_DISK"
            fi
        else
            break
        fi
    done
}

#-------------------------------------------------------------------------------
# VM shared folders
#-------------------------------------------------------------------------------

function vm_add_share_automount {
    local VM_NAME=$1
    local SHARE_DIR=$2
    local SHARE_NAME=$3
    $VBM sharedfolder add "$VM_NAME" \
        --name "$SHARE_NAME" \
        --hostpath "$SHARE_DIR" \
        --automount
}

function vm_add_share {
    local VM_NAME=$1
    local SHARE_DIR=$2
    local SHARE_NAME=$3
    $VBM sharedfolder add "$VM_NAME" \
        --name "$SHARE_NAME" \
        --hostpath "$SHARE_DIR"
}

function vm_rm_share {
    local VM_NAME=$1
    local SHARE_NAME=$2
    $VBM sharedfolder remove "$VM_NAME" --name "$SHARE_NAME"
}

#-------------------------------------------------------------------------------
# VirtualBox guest add-ons
#-------------------------------------------------------------------------------

function _download_guestadd-iso {
    # e.g. 4.1.32r92798 4.3.10_RPMFusionr93012 4.3.10_Debianr93012
    local ISO=VBoxGuestAdditions.iso
    local VER=$(get_vb_version)
    if [[ -n "$VER" ]]; then
        local URL="http://download.virtualbox.org/virtualbox/$VER/VBoxGuestAdditions_$VER.iso"
        download "$URL" "$ISO_DIR" $ISO
    fi
    GUESTADD_ISO="$ISO_DIR/$ISO"
}

function _get_guestadd-iso {
    local ISO=VBoxGuestAdditions.iso

    local ADD_ISO="$IMG_DIR/$ISO"
    if [ -f "$ADD_ISO" ]; then
        echo "$ADD_ISO"
        return 0
    fi

    ADD_ISO="/Applications/VirtualBox.app/Contents/MacOS/$ISO"
    if [ -f "$ADD_ISO" ]; then
        echo "$ADD_ISO"
        return 0
    fi

    echo >&2 "Searching filesystem for VBoxGuestAdditions. This may take a while..."
    ADD_ISO=$(find / -name "$ISO" 2>/dev/null) || true
    if [ -n "$ADD_ISO" ]; then
        echo "$ADD_ISO"
        return 0
    fi

    echo >&2 "Looking on the Internet"
    _download_guestadd-iso
    if [ -f "$ADD_ISO" ]; then
        echo "$ADD_ISO"
        return 0
    fi
}

function _vm_attach_guestadd-iso {
    local VM=$1
    local GUESTADD_ISO=$2
    local rc=0
    $VBM storageattach "$VM" --storagectl IDE --port 1 --device 0 --type dvddrive --medium "$GUESTADD_ISO" 2>/dev/null || rc=$?
    return $rc
}

function vm_attach_guestadd-iso {
    local VM=$1

    OSBASH= ${WBATCH:-:} _vm_attach_guestadd-iso "$VM" emptydrive
    OSBASH= ${WBATCH:-:} _vm_attach_guestadd-iso "$VM" additions
    # Return if we are just faking it for wbatch
    ${OSBASH:+:} return 0

    if [ -z "${GUESTADD_ISO-}" ]; then

        # No location provided, asking VirtualBox for one

        # An existing drive is needed to make additions shortcut work
        # (at least VirtualBox 4.3.12 and below)
        WBATCH= _vm_attach_guestadd-iso "$VM" emptydrive

        if WBATCH= _vm_attach_guestadd-iso "$VM" additions; then
            echo >&2 "Using VBoxGuestAdditions provided by VirtualBox"
            return 0
        fi
        # Neither user nor VirtualBox are helping, let's go guessing
        GUESTADD_ISO=$(_get_guestadd-iso)
        if [ -z "GUESTADD_ISO" ]; then
            # No ISO found
            return 2
        fi
    fi
    if WBATCH= _vm_attach_guestadd-iso "$VM" "$GUESTADD_ISO"; then
        echo >&2 "Attached $GUESTADD_ISO"
        return 0
    else
        echo >&2 "Failed to attach $GUESTADD_ISO"
        return 3
    fi
}

#-------------------------------------------------------------------------------
# Sleep
#-------------------------------------------------------------------------------

function vbox_sleep {
    SEC=$1

    # Don't sleep if we are just faking it for wbatch
    ${OSBASH:-:} sleep "$SEC"
    ${WBATCH:-:} wbatch_sleep "$SEC"
}

#-------------------------------------------------------------------------------
# Booting a VM and passing boot parameters
#-------------------------------------------------------------------------------

source "$OSBASH_LIB_DIR/scanlib"

function _vbox_push_scancode {
    local VM_NAME=$1
    shift
    # Split string (e.g. '01 81') into arguments (works also if we
    # get each hexbyte as a separate argument)
    # Not quoting $@ is intentional -- we want to split on blanks
    local SCANCODE=( $@ )
    $VBM controlvm "$VM_NAME" keyboardputscancode "${SCANCODE[@]}"
}

function vbox_kbd_escape_key {
    _vbox_push_scancode "$VM_NAME" "$(esc2scancode)"
}

function vbox_kbd_enter_key {
    _vbox_push_scancode "$VM_NAME" "$(enter2scancode)"
}

function vbox_kbd_string_input {
    local VM_NAME=$1
    local STR=$2

    # This loop is inefficient enough that we don't overrun the keyboard input
    # buffer when pushing scancodes to the VirtualBox.
    while IFS=  read -r -n1 char; do
        if [ -n "$char" ]; then
            SC=$(char2scancode "$char")
            if [ -n "$SC" ]; then
                _vbox_push_scancode "$VM_NAME" "$SC"
            else
                echo >&2 "not found: $char"
            fi
        fi
    done <<< "$STR"
}

function vbox_boot {
    local VM=$1

    echo >&2 "Starting VM \"$VM\""
    $VBM startvm "$VM"
}

#-------------------------------------------------------------------------------

# vim: set ai ts=4 sw=4 et ft=sh: