#!/bin/bash -e

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#                                                                         #
#  Copyright (C) 2018 Simon Stürz <simon.stuerz@guh.io>                   #
#                                                                         #
#  This file is part of imagebuilder.                                     #
#                                                                         #
#  Imagebuilder is free software: you can redistribute it and/or modif    #
#  it under the terms of the GNU General Public License as published by   #
#  the Free Software Foundation, version 2 of the License.                #
#                                                                         #
#  Imagebuilder is distributed in the hope that it will be useful,        #
#  but WITHOUT ANY WARRANTY; without even the implied warranty of         #
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the           #
#  GNU General Public License for more details.                           #
#                                                                         #
#  You should have received a copy of the GNU General Public License      #
#  along with imagebuilder. If not, see <http://www.gnu.org/licenses/>.   #
#                                                                         #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

VERSION=0.1.0
APPLICATION=$(basename $0)

#------------------------------------------------------------------------------------------
function usage() {
  cat <<EOF
Usage: ${APPLICATION} [OPTIONS] [COMMAND]

This tool should help building images for different plattforms inside an lxd container.
The tool can be run inside the build script directory and requires the name of the build script as parameter.

Copyright © 2018 Simon Stürz <simon.stuerz@guh.io>
Version: ${VERSION}

COMMANDS:
  shell                   Log into shell from builder container
  info                    Print information about the current build environment
  clean                   Run the clean command of the build script
  stop                    Stop the builder container
  start                   Start the builder container
  delete                  Delete the image builder container
  help                    Show this message
  create-image            Create the base image

OPTIONS:
  -s, --script [SCRIPT]   The build script for the image
  -n, --no-colors         Disable colorfull output
  -v, --version           Show the version of the tool
  -h, --help              Show this message

EOF
}

#------------------------------------------------------------------------------------------
# bash colors
BASH_GREEN="\e[1;32m"
BASH_ORANGE="\e[33m"
BASH_RED="\e[1;31m"
BASH_NORMAL="\e[0m"

printGreen() {
    if ${COLORS}; then
        echo -e "${BASH_GREEN}[+] Image builder: $1${BASH_NORMAL}"
    else
        echo -e "[+] Image builder: $1"
    fi
}

printOrange() {
    if ${COLORS}; then
        echo -e "${BASH_ORANGE}[-] Image builder: $1${BASH_NORMAL}"
    else
        echo -e "[-] Image builder: $1"
    fi
}

printRed() {
    if ${COLORS}; then
        echo -e "${BASH_RED}[!] Image builder: $1${BASH_NORMAL}"
    else
        echo -e "[!] Image builder: $1"
    fi
}

#------------------------------------------------------------------------------------------
waitContainerNetwork() {
    printGreen "Wait for container getting online"
    NETWORK_UP=0
    for i in `seq 1 20`; do
        if lxc info ${CONTAINERNAME} | grep -e "eth0.*inet\b" > /dev/null 2>&1 ; then
            NETWORK_UP=1
            printGreen "Container is online"
            break
        fi
        sleep 1
    done

    if [ $NETWORK_UP -ne 1 ] ; then
        printRed "Container is not connected to the Internet."
        exit 1
    fi
}

waitDpkgAvailable() {
    printGreen "Wait for container dpkg getting available..."
    DPKG_AVAILABLE=0

    for i in `seq 1 60`
    do
        #echo "${ERROR_COLOR} /var/lib/dpkg/lock exists... Waiting for it to disappear...${NC}"
        if execContainerRoot test ! -e /var/lib/dpkg/lock; then
            DPKG_AVAILABLE=1
            printGreen "The update manager is available now."
            break
        fi
        sleep 1
    done
    if [ $DPKG_AVAILABLE -ne 1 ] ; then
        #echo "${ERROR_COLOR} /var/lib/dpkg/lock still exists after one minute. Assuming it is stale. Deleting it...${NC}"
        execContainerRoot rm /var/lib/dpkg/lock
    fi
}



#------------------------------------------------------------------------------------------
containerRunning() {
    lxc list | grep ${CONTAINERNAME} | grep 'RUNNING' > /dev/null 2>&1
    return $?
}

#------------------------------------------------------------------------------------------
createContainer() {
    if lxc info ${CONTAINERNAME} > /dev/null 2>&1 ; then
        #printOrange "--> Container ${CONTAINERNAME} already exists."
        startContainer
        return 0
    fi

    # Get the xenial image
    printGreen "Create lxc container ${CONTAINERNAME} out of nymea:${IMAGENAME} image for image building"
    # FIXME: nymea:${IMAGENAME}
    lxc init nymea:${IMAGENAME} ${CONTAINERNAME}

    # Add loop devices
    lxc config device add ${CONTAINERNAME} loop0 unix-block path=/dev/loop0
    lxc config device add ${CONTAINERNAME} loop1 unix-block path=/dev/loop1
    lxc config device add ${CONTAINERNAME} loop2 unix-block path=/dev/loop2
    lxc config device add ${CONTAINERNAME} loop3 unix-block path=/dev/loop3
    lxc config device add ${CONTAINERNAME} loop4 unix-block path=/dev/loop4
    lxc config device add ${CONTAINERNAME} loop5 unix-block path=/dev/loop5
    lxc config device add ${CONTAINERNAME} loop6 unix-block path=/dev/loop6
    lxc config device add ${CONTAINERNAME} loop7 unix-block path=/dev/loop7
    lxc config device add ${CONTAINERNAME} loop-control unix-char path=/dev/loop-control

    # Give permissions to the container
    lxc config set ${CONTAINERNAME} raw.apparmor "mount,"
    lxc config set ${CONTAINERNAME} security.privileged true

    startContainer
    waitContainerNetwork

    # Set up user
    USERNAME_CONTAINER=`lxc exec ${CONTAINERNAME} grep ":$USERID:" /etc/passwd | cut -f 1 -d ":"`
    if [ ! -z "$USERNAME_CONTAINER" ]; then
        # Note remove the current container user only if the user id is the same
        printGreen "Removing user in container with the same user id: $USERNAME_CONTAINER"
        lxc exec --env GROUPID=$GROUPID --env GROUPNAME=$GROUPNAME ${CONTAINERNAME} -- userdel $USERNAME_CONTAINER || true
        lxc exec --env GROUPID=$GROUPID --env GROUPNAME=$GROUPNAME ${CONTAINERNAME} -- delgroup $USERNAME_CONTAINER || true
    fi

    printGreen "Create user $USERNAME in the container"
    lxc exec --env GROUPID=$GROUPID --env GROUPNAME=$GROUPNAME ${CONTAINERNAME} -- addgroup --gid $GROUPID $GROUPNAME
    lxc exec --env GROUPID=$GROUPID --env USERNAME=$USERNAME --env USERID=$USERID ${CONTAINERNAME} -- adduser --disabled-password \
             --gecos "" --uid $USERID --gid $GROUPID $USERNAME

    execContainerRoot adduser $USERNAME sudo
    execContainerRoot passwd --delete $USERNAME

    # Mount folders
    if ! lxc config device get ${CONTAINERNAME} current_dir_mount disk 2> /dev/null ; then
        printGreen "Mounting build directory ${BUILDDIR} into container."
        lxc config device add ${CONTAINERNAME} current_dir_mount disk source=$BUILDDIR path=$BUILDDIR_MOUNT
    else
        lxc config device set ${CONTAINERNAME} current_dir_mount source $BUILDDIR
    fi

}

#------------------------------------------------------------------------------------------
createBaseImage() {
    printGreen "Create base image for image builder:"

    CONTAINERNAME=${IMAGENAME}-base

    # Check if there is an image
    if lxc image info ${CONTAINERNAME} > /dev/null 2>&1; then
        printRed "There is already an image with the name ${CONTAINERNAME}. Please rename or delete the existing image."
        exit 1
    fi

    # Check if there is a container
    if lxc info ${CONTAINERNAME} > /dev/null 2>&1 ; then
        printRed "--> Container ${CONTAINERNAME} already exists. Please rename or delete the existing container."
        exit 1
    fi

    # Get the bionic image
    printGreen "Create base container ${CONTAINERNAME}"
    lxc init ubuntu:18.04 ${CONTAINERNAME}

    # Add loop devices
    lxc config device add ${CONTAINERNAME} loop0 unix-block path=/dev/loop0
    lxc config device add ${CONTAINERNAME} loop1 unix-block path=/dev/loop1
    lxc config device add ${CONTAINERNAME} loop2 unix-block path=/dev/loop2
    lxc config device add ${CONTAINERNAME} loop3 unix-block path=/dev/loop3
    lxc config device add ${CONTAINERNAME} loop4 unix-block path=/dev/loop4
    lxc config device add ${CONTAINERNAME} loop5 unix-block path=/dev/loop5
    lxc config device add ${CONTAINERNAME} loop6 unix-block path=/dev/loop6
    lxc config device add ${CONTAINERNAME} loop7 unix-block path=/dev/loop7
    lxc config device add ${CONTAINERNAME} loop-control unix-char path=/dev/loop-control

    # Give permissions to the container
    lxc config set ${CONTAINERNAME} raw.apparmor "mount,"
    lxc config set ${CONTAINERNAME} security.privileged true

    startContainer
    waitContainerNetwork
    waitDpkgAvailable

    # Install packages for image building
    execContainerRoot apt update
    execContainerRoot apt -y upgrade
    execContainerRoot apt install -y  debootstrap qemu-utils xz-utils git zip qemu-user-static \
                                      rsync debian-keyring debian-archive-keyring wget whois \
                                      debootstrap f2fs-tools qemu binfmt-support

    # Create bootstraps
    CACHE_DIR=/root/cache

    # Debian stretch
    printGreen "Bootstrap Debian stretch"
    BOOTSTRAP=${CACHE_DIR}/bootstrap/debian-stretch
    BOOTSTRAP_FLAG=${CACHE_DIR}/bootstrap/.debian-stretch
    execContainerRoot mkdir -pv ${BOOTSTRAP}
    execContainerRoot qemu-debootstrap --verbose --arch=armhf stretch $BOOTSTRAP http://http.debian.net/debian
    execContainerRoot touch $BOOTSTRAP_FLAG
    printGreen "Bootstrap debian stretch finished successfully."

    # Ubuntu Xenial 16.04
    printGreen "Bootstrap Ubuntu xenial 16.04"
    BOOTSTRAP=${CACHE_DIR}/bootstrap/ubuntu-xenial
    BOOTSTRAP_FLAG=${CACHE_DIR}/bootstrap/.ubuntu-xenial
    execContainerRoot mkdir -pv ${BOOTSTRAP}
    execContainerRoot qemu-debootstrap --verbose --arch=armhf xenial $BOOTSTRAP http://ports.ubuntu.com/
    execContainerRoot touch $BOOTSTRAP_FLAG
    printGreen "Bootstrap Ubuntu xenial 16.04 finished successfully."

    # Ubuntu Bionic Beaver 18.04
    printGreen "Bootstrap Ubuntu bionic 18.04"
    BOOTSTRAP=${CACHE_DIR}/bootstrap/ubuntu-bionic
    BOOTSTRAP_FLAG=${CACHE_DIR}/bootstrap/.ubuntu-bionic
    execContainerRoot mkdir -pv ${BOOTSTRAP}
    execContainerRoot qemu-debootstrap --verbose --arch=armhf bionic $BOOTSTRAP http://ports.ubuntu.com/
    execContainerRoot touch $BOOTSTRAP_FLAG
    printGreen "Bootstrap Ubuntu bionic 18.04 finished successfully."

    # Clean up container
    execContainerRoot apt -y autoremove
    execContainerRoot apt -y clean

    # Create the image out of this container
    stopContainer
    printGreen "Create image ${IMAGENAME} out of the container ${CONTAINERNAME}"
    lxc publish ${CONTAINERNAME} --alias ${IMAGENAME}
    deleteContainer
    lxc image list | grep ${IMAGENAME}
}


#------------------------------------------------------------------------------------------
deleteContainer() {
    printGreen "Delete container ${CONTAINERNAME}"
    lxc delete ${CONTAINERNAME} --force || true
}

#------------------------------------------------------------------------------------------
startContainer() {
    printGreen "Start container ${CONTAINERNAME}"
    if ! containerRunning; then
        lxc start ${CONTAINERNAME};
    else
        printOrange "--> Container ${CONTAINERNAME} already running"
    fi
}

#------------------------------------------------------------------------------------------
stopContainer() {
    printGreen "Stop container ${CONTAINERNAME}"
    lxc stop ${CONTAINERNAME} --force || true
}

#------------------------------------------------------------------------------------------
buildImage() {
    if ${COLORS}; then
        lxc exec ${CONTAINERNAME} -- su -c "cd ${BUILDDIR_MOUNT} && ./${BUILDSCRIPT}"
    else
        lxc exec ${CONTAINERNAME} -- su -c "cd ${BUILDDIR_MOUNT} && ./${BUILDSCRIPT} -n"
    fi
}

#------------------------------------------------------------------------------------------
enterShell() {
    printGreen "Enter shell in container ${CONTAINERNAME}"
    lxc exec ${CONTAINERNAME} bash
}

#------------------------------------------------------------------------------------------
runCleanCommand() {
    printGreen "Run clean command in container: ${BUILDSCRIPT} --clean"
    execContainer ./${BUILDSCRIPT} --clean
}

#------------------------------------------------------------------------------------------
execContainer() {
    command="$@"
    #printGreen "Execute command in container: $command"
    lxc exec ${CONTAINERNAME} -- su -c "cd ${BUILDDIR_MOUNT}; $command"
}

#------------------------------------------------------------------------------------------
execContainerRoot() {
    command="$@"
    #printGreen "Execute command in container: $command"
    lxc exec ${CONTAINERNAME} -- su -c "$command"
}

#------------------------------------------------------------------------------------------
verifyCommandCount() {
    # Check if there is already a command specified
    if [ "${COMMAND}" != "" ]; then
        printRed "Multiple commands passed. Please pass only one command to ${APPLICATION}."
        #usage
        exit 1
    fi
}

#------------------------------------------------------------------------------------------
verifyGitRepository() {
    # Verify we have a git repository
    if ! git status > /dev/null 2>&1 ; then
        printRed "Could not find any git repositor in this folder. Please run ${APPLICATION} in the source directory of the specified image script."
        exit 1
    fi
}

#------------------------------------------------------------------------------------------
loadConfiguration() {
    if [ -e ${CONFIG} ]; then
        . ${CONFIG}
    else
        # Save default configuration
        saveConfiguration
    fi
}

#------------------------------------------------------------------------------------------
saveConfiguration() {
    if [ ! -d ${CONFIGDIR} ]; then mkdir ${CONFIGDIR}; fi
    if [ -e ${CONFIG} ]; then rm ${CONFIG}; fi
    touch ${CONFIG}

    echo "# nymea image builder configuration" >> ${CONFIG}
    echo "COLORS=${COLORS}" >> ${CONFIG}
    echo "BUILDSCRIPT=${BUILDSCRIPT}" >> ${CONFIG}
}

#------------------------------------------------------------------------------------------
initEnv() {

    # Container user
    USERNAME=`id --user --name`
    GROUPNAME=$USERNAME
    USERID=`id --user`
    GROUPID=`id --group`

    # Directories
    BUILDDIR=$(pwd)
    USERDIR=/home/${USERNAME}

    # Container
    IMAGENAME=nymea-image-builder

    # Script options
    COLORS=true
    CONFIGDIR=${BUILDDIR}/.imagebuilder
    CONFIG=${CONFIGDIR}/config
}

#------------------------------------------------------------------------------------------
printInfo() {
    echo "Project name: ${PROJECTNAME}"
    echo "- Directories:"
    echo "    Build directory: ${BUILDDIR}"
    echo "    Build mount directory: ${BUILDDIR_MOUNT}"

    echo "- User information:"
    echo "    Username: ${USERNAME}"
    echo "    User group: ${GROUPNAME}"
    echo "    User id: ${USERID}"
    echo "    Group id: ${GROUPID}"
    echo "    User directory: ${USERDIR}"

    echo "- Container information:"
    echo "    Image: nymea:${IMAGENAME}"
    echo "    Container: ${CONTAINERNAME}"

    echo "- Configuration:"
    echo "    Config directory: ${CONFIGDIR}"
    echo "    Config file: ${CONFIG}"

    echo "- Build information:"
    echo "    Build script: ${BUILDSCRIPT}"
    echo "    Commit hash: ${COMMITHASH}"

}

#------------------------------------------------------------------------------------------
finish() {
    # Change the permissions of the build output
    if [ -z ${CONTAINERNAME} ]; then
        return 0
    fi

    if containerRunning; then
        execContainerRoot chown -R $USERNAME:$GROUPNAME ${BUILDDIR_MOUNT}
    fi
}

#------------------------------------------------------------------------------------------
# Main
#------------------------------------------------------------------------------------------

# Clean up if the script exit
trap finish EXIT

COMMAND=""

initEnv
loadConfiguration

# Parse command
while [ "$1" != "" ]; do
    case $1 in
        shell )
            verifyCommandCount && COMMAND="enterShell";;
        clean )
            verifyCommandCount && COMMAND="runCleanCommand";;
        start )
            verifyCommandCount && COMMAND="startContainer";;
        stop )
            verifyCommandCount && COMMAND="stopContainer";;
        delete )
            verifyCommandCount && COMMAND="deleteContainer";;
        info )
            verifyCommandCount && COMMAND="printInfo";;
        create-image )
            verifyCommandCount && COMMAND="create-image";;
        help )
            verifyCommandCount && COMMAND="help";;
        -s | --script )
            BUILDSCRIPT=$2
            shift;;
        -n | --no-colors )
            COLORS=false;;
        -h | --help )
            usage && exit 0;;
        -v | --version )
            echo ${VERSION} && exit 0;;
        * )
            usage && exit 1;;
    esac
    shift
done

# Note: we don't need any build script for creating the base image
if [ "${COMMAND}" == "create-image" ]; then createBaseImage; exit 0; fi

# Check if build script is specified
if [ -z "${BUILDSCRIPT}" ]; then
    printRed "No build script specified. Please specify the build script using the \"-s\" or \"--script\" parameter."
    exit 1
fi

# Check if the build script exists
if [ ! -f ${BUILDDIR}/${BUILDSCRIPT} ]; then
    printRed "Could not find ${BUILDDIR}/${BUILDSCRIPT}. Please specify the build script using the \"-s\" or \"--script\" parameter."
    exit 1
fi

verifyGitRepository
saveConfiguration

# Init
PROJECTNAME=$(basename ${BUILDSCRIPT})
CONTAINERNAME=builder-${PROJECTNAME}-${IMAGENAME}
BUILDDIR_MOUNT=${USERDIR}/${PROJECTNAME}
COMMITHASH=$(git rev-parse --short HEAD)

# Execut command
if [ "${COMMAND}" == "help" ]; then usage; exit 0; fi
if [ "${COMMAND}" == "enterShell" ]; then createContainer && enterShell; exit 0; fi
if [ "${COMMAND}" == "runCleanCommand" ]; then runCleanCommand; exit 0; fi
if [ "${COMMAND}" == "startContainer" ]; then startContainer; exit 0; fi
if [ "${COMMAND}" == "stopContainer" ]; then stopContainer; exit 0; fi
if [ "${COMMAND}" == "deleteContainer" ]; then deleteContainer; exit 0; fi
if [ "${COMMAND}" == "printInfo" ]; then printInfo; exit 0; fi

# Default build the container
printInfo
createContainer
buildImage

