#!/bin/sh

#
# Installation script for mobileraker companion.
#
# Based on OctoEverywhere for Klipper, courtesy of Quinn Damerell
# OctoEverywhere: https://octoeverywhere.com
#

set -e

#
# First things first, we need to detect what kind of OS we are running on. The script works by default with all
# Debian OSs, but some printers with embedded computers run strange embedded OSs, that have a lot of restrictions.
# These must stay in sync with update.sh and uninstall.sh!
#

# The K1 and K1 Max run an OS called Buildroot. We detect that by looking at the os-release file.
# Quick note about bash vars, to support all OSs, we use the most compilable var version. This means we use ints
# where 1 is true and 0 is false, and we use comparisons like this [ "$IS_K1_OS" -eq 1 ]
IS_K1_OS=0
if grep -Fqs "ID=buildroot" /etc/os-release; then
    IS_K1_OS=1
    # On the K1, we always want the path to be /usr/data
    # /usr/share has very limited space, so we don't want to use it.
    # This is also where the github script installs moonraker and everything.
    HOME="/usr/data"
fi

# Next, we try to detect if this OS is the Sonic Pad OS.
# The Sonic Pad runs openwrt. We detect that by looking at the os-release file.
IS_SONIC_PAD_OS=0
if grep -Fqs "sonic" /etc/openwrt_release; then
    IS_SONIC_PAD_OS=1
    # On the K1, we always want the path to be /usr/share, this is where the rest of the klipper stuff is.
    HOME="/usr/share"
fi

# Get the path where this script is executing
SCRIPT_DIR=$(readlink -f "$(dirname "$0")")
REPO_DIR=$(readlink -f "$SCRIPT_DIR/..")

# This is the root of where our py virtual env will be. Note that all instances share this same
# virtual environment. This how the rest of the system is, where all other services, even with multiple instances, share the same
# virtual environment. I probably wouldn't have done it like this, but we have to create this before we know what instance we are targeting, so it's fine.
ENV_DIR="${HOME}/mobileraker-env"

# Note that this is parsed by the update process (Of Moonraker) to find and update required system packages on update!
# On update THIS SCRIPT ISN'T RAN, only this line is parsed out and used to install / update system packages.
# For python packages, the `requirements.txt` package is used on update.
# This var name MUST BE `PKGLIST`!!
#
# The python requirements are for the installer and plugin
# The virtualenv is for our virtual package env we create
# The curl requirement is for some things in this bootstrap script.
PKGLIST="python3 python3-pip virtualenv curl"
# For the Creality OS, we only need to install these.
# We don't override the default name, since that's used by the Moonraker installer
# Note that we DON'T want to use the same name as above (not even in this comment) because some parsers might find it.
SONIC_PAD_DEP_LIST="python3 python3-pip"
CREALITY_DEP_LIST="python3 python3-pillow python3-pip"


#
# Console Write Helpers
#
c_default=$(printf "\033[39m")
c_green=$(printf "\033[92m")
c_yellow=$(printf "\033[93m")
c_magenta=$(printf "\033[35m")
c_red=$(printf "\033[91m")
c_cyan=$(printf "\033[96m")

log_header()
{
    printf "%s\n" "${c_magenta}$1${c_default}"
}

log_important()
{
    printf "%s\n" "${c_yellow}$1${c_default}"
}

log_error()
{
    log_blank
    printf "%s\n" "${c_red}$1${c_default}"
    log_blank
}

log_info()
{
    printf "%s\n" "${c_green}$1${c_default}"
}

log_blue()
{
    printf "%s\n" "${c_cyan}$1${c_default}"
}

log_blank()
{
    printf "\n"
}

#
# It's important for consistency that the repo root is in set $HOME for the K1 and Sonic Pad
# To enforce that, we will move the repo where it should be.
ensure_creality_os_right_repo_path()
{
    # TODO - re-enable this for the  || [ "$IS_K1_OS" -eq 1 ] after the github script updates.
    if [ "$IS_SONIC_PAD_OS" -eq 1 ] || [ "$IS_K1_OS" -eq 1 ]; then
        # Due to the K1 shell, we have to use grep rather than any bash string contains syntax.
        if echo "$REPO_DIR" | grep "$HOME" - > /dev/null; then
            return
        else
            log_info "Current path $REPO_DIR"
            log_error "For the Creality devices the mobileraker_companion repo must be cloned into $HOME/mobileraker_companion"
            log_important "Moving the repo and running the install again..."
            cd "$HOME" || exit 1
            # Send errors to null, if the folder already exists this will fail.
            git clone https://github.com/Clon1998/mobileraker_companion.git mobileraker_companion 2>/dev/null || true
            cd "$HOME/mobileraker_companion" || exit 1
            # Ensure state
            git reset --hard
            # TODO: I dont want to checkout main. Just keep the current branch.
            #git checkout main
            git pull
            # Log the current path after git pull        
            log_info "Current path $(pwd)"
            # Run the install, if it fails, still do the clean-up of this repo.
            if [ "$IS_K1_OS" -eq 1 ]; then
                sh "$HOME/mobileraker_companion/scripts/install.sh" "$@" || true
            else
                "$HOME/mobileraker_companion/scripts/install.sh" "$@" || true
            fi
            installExit=$?
            # Delete this folder.
            log_info "Cleaning up the old repo folder. The new repo is in $HOME/mobileraker_companion"
            rm -fr "$REPO_DIR"
            # Take the user back to the new install folder.
            cd "$HOME" || exit 1
            # Exit.
            exit "$installExit"
        fi
    fi
}

#
# Logic to create / update our virtual py env
#
ensure_py_venv()
{
    log_header "Checking Python Virtual Environment For Mobileraker Companion..."
    # If the service is already running, we can't recreate the virtual env so if it exists, don't try to create it.
    # Note that we check the bin folder exists in the path, since we mkdir the folder below but virtualenv might fail and leave it empty.
    ENV_BIN_PATH="$ENV_DIR/bin"
    if [ -d "$ENV_BIN_PATH" ]; then
        # This virtual env refresh fails on some devices when the service is already running, so skip it for now.
        # This only refreshes the virtual environment package anyways, so it's not super needed.
        #log_info "Virtual environment found, updating to the latest version of python."
        #python3 -m venv --upgrade "${ENV_DIR}"
        return 0
    fi

    log_info "No virtual environment found, creating one now."
    mkdir -p "${ENV_DIR}"
    if [ "$IS_K1_OS" -eq 1 ]; then
        # The K1 requires we setup the virtualenv like this.
        if [[ -f /opt/bin/python3 ]]; then
            virtualenv -p /opt/bin/python3 --system-site-packages "${ENV_DIR}"
        else
            python3 /usr/lib/python3.8/site-packages/virtualenv.py -p /usr/bin/python3 --system-site-packages "${ENV_DIR}"
        fi
    else
        # Everything else can use this more modern style command.
        virtualenv -p /usr/bin/python3 --system-site-packages "${ENV_DIR}"
    fi
}

#
# Logic to make sure all of our required system packages are installed.
#
install_or_update_system_dependencies()
{
    log_header "Checking required system packages are installed..."

    if [ "$IS_K1_OS" -eq 1 ]; then
        # The K1 by default doesn't have any package manager. In some cases
        # the user might install opkg via the 3rd party moonraker installer script.
        # But in general, PY will already be installed, so there's no need to try.
        # On the K1, the only we thing we ensure is that virtualenv is installed via pip.
        if [[ -f /opt/bin/opkg ]]; then
            /opt/bin/opkg update || true
            /opt/bin/opkg install ${CREALITY_DEP_LIST}
        fi
        pip3 install --trusted-host pypi.python.org --trusted-host pypi.org --trusted-host=files.pythonhosted.org --no-cache-dir virtualenv
    elif [ "$IS_SONIC_PAD_OS" -eq 1 ]; then
        # The sonic pad always has opkg installed, so we can make sure these packages are installed.
        opkg update || true
        opkg install ${SONIC_PAD_DEP_LIST}
        pip3 install virtualenv
    else
        # It seems a lot of printer control systems don't have the date and time set correctly, and then the fail
        # getting packages and other downstream things. We will will use our HTTP API to set the current UTC time.
        # Note that since cloudflare will auto force http -> https, we use https, but ignore cert errors, that could be
        # caused by an incorrect date.
        # Note some companion systems don't have curl installed, so this will fail.
        log_info "Ensuring the system date and time is correct..."
        
        log_important "You might be asked for your system password - this is required to install the required system packages."
        # Thanks to Quinn Damerell for allowing us to use the OctoEverywhere API for this.
        sudo date -s "$(curl --insecure 'https://octoeverywhere.com/api/util/date' 2>/dev/null)" || true

        # These we require to be installed in the OS.
        # Note we need to do this before we create our virtual environment
    
        log_info "Installing required system packages. This can take a few minutes..."
        sudo apt update 1>/dev/null 2>/dev/null || true
        sudo apt install --yes ${PKGLIST}

        # The PY lib Pillow depends on some system packages that change names depending on the OS.
        # The easiest way to do this was just to try to install them and ignore errors.
        # Most systems already have the packages installed, so this only fixes edge cases.
        # Notes on Pillow deps: https://pillow.readthedocs.io/en/latest/installation.html
        log_info "Ensuring zlib is install for Pillow, it's ok if this package install fails."
        sudo apt install --yes zlib1g-dev 2> /dev/null || true
        sudo apt install --yes zlib-devel 2> /dev/null || true
        sudo apt install --yes libjpeg62-turbo-dev 2> /dev/null || true
        sudo apt install --yes libjpeg8-dev 2> /dev/null || true
    fi
    log_blank
    log_info "System package install complete."
}

#
# Logic to install or update the virtual env and all of our required packages.
#
install_or_update_python_env()
{
    # Now, ensure the virtual environment is created.
    ensure_py_venv

    # Update pip if needed - we added a note because this takes a while on the sonic pad.
    log_info "Updating PIP if needed... (this can take a few seconds or so)"
    if [ "$IS_K1_OS" -eq 1 ]; then
        "${ENV_DIR}"/bin/python -m pip install --trusted-host pypi.python.org --trusted-host pypi.org --trusted-host=files.pythonhosted.org --no-cache-dir --upgrade pip
    else
        "${ENV_DIR}"/bin/python -m pip install --upgrade pip
    fi

    # Set the cache directory based on the OS
    CACHE_DIR="${ENV_DIR}/cache"

    # Finally, ensure our plugin requirements are installed and updated.
    log_info "Installing or updating required python libs..."
    if [ "$IS_K1_OS" -eq 1 ]; then
        TMPDIR="${CACHE_DIR}" "${ENV_DIR}"/bin/pip3 install --trusted-host pypi.python.org --trusted-host pypi.org --trusted-host=files.pythonhosted.org -q -r "${SCRIPT_DIR}"/mobileraker-requirements.txt
    else
        TMPDIR="${CACHE_DIR}" "${ENV_DIR}"/bin/pip3 install -q -r "${SCRIPT_DIR}"/mobileraker-requirements.txt
    fi
    log_info "Python libs installed."
}

log_blank
log_blank
log_blank
cat << EOF
MMMMMMMMMMMMMMMMMMMMMMWNX0xolldk0XWWMMMMMMMMMMMMMMMMMMMM
MMMMMMMMMMMMMMMMMMWNKkdl:;;:;,,',:ldk0XWMMMMMMMMMMMMMMMM
MMMMMMMMMMMMMWNX0xoc;,;:ldxxxxdoc:,'',:lxOKNWWMMMMMMMMMM
MMMMMMMMMWNKOxl:;,;codxkkdooddxxkkdol:;''';coxOXWMMMMMMM
MMMMMMWXOdl:,,:loxkkkdoloodxkOOkxxxxkkxdlc;''',:lkXWMMMM
MMMMMWk:,,,,,:x0OdoloddxxxddxkkOOOOkxddxkOOl''''',cOWMMM
MMMMMWOc,,,,,,:lddxkkxdollllodddxxkO000kkxo:'''''':OWMMM
MMMMWXOkxo:;,,,,,;loddooodxO0KK0Okkkkkdl;,'''',:odkOXWMM
MMMWXo;:ldxxdl:,,',,,:clx0NWWWWNKkoc;,''''';ldxxoc;,lKMM
MMMMNx:,,,;coxkxoc;,,,,,;cokkkdl;,''''',:odxdl:,''':kNMM
MMMWXK0xl:,,,,:lxkkdl:,,,,,,'''''''';ldxxoc,'''';lx0KXWM
MMNkc:ok00koc,,,,;cdkkxoc;,''''',codxdl:,'''';lxOkdc;l0W
MMXo,',,;lxO0kdc;,,,;:oxOkxlccldxxoc,'''',:lxkko:,''',xW
MMWKo:,,,,,;ldO0Odl;,,,,;ldkkxdl;,'''',:oxkxo:,''''';dKW
MMMMNKxl;,,,,,;cdO00xo:,,,,,,,''''',coxkdl;'''''';lkKWMM
MMMMMMWNKxl:,,,,,,:ok00koc;,'''';ldkkdc,'''''';lxKWMMMMM
MMMMMMMMMMNKxo:,,,,,,:ok00Odlcoxkxo:,'''''';lkKNMMMMMMMM
MMMMMMMMMMMMWNKkl:,,,,,,;lxOOkxl:,'''''';lkKWMMMMMMMMMMM
MMMMMMMMMMMMMMMWNKkl:,,,,,,;;,''''''';lkKWMMMMMMMMMMMMMM
MMMMMMMMMMMMMMMMMMWNKko:,,,,'''''';lkKWMMMMMMMMMMMMMMMMM
MMMMMMMMMMMMMMMMMMMMMWNKko:,'',;lkKWMMMMMMMMMMMMMMMMMMMM
MMMMMMMMMMMMMMMMMMMMMMMMMWKkddkKWMMMMMMMMMMMMMMMMMMMMMMM
EOF
log_blank
log_important "                  Mobileraker Companion"
log_blue      " Companion for Mobileraker, enabling push notification for Klipper using Moonraker."
log_info      " - Installer Script based on OctoEverywhere for Klipper, courtesy of Quinn Damerell"
log_blank
log_blank
log_important "Mobileraker works as a simple UI for Klipper on the phone."
log_important "The companion is required to enable reliable push notifications."
log_info      "  - Print State Notifications"
log_info      "  - Progress Notifications"
log_info      "  - Custom Notifications via M117 or custom Macro enabling layer based notifications or heat up notifications."
log_blank
log_blank

# These are helpful for debugging.
if [ "$IS_SONIC_PAD_OS" -eq 1 ]; then
    echo "Running in Sonic Pad OS mode"
fi
if [ "$IS_K1_OS" -eq 1 ]; then
    echo "Running in K1 and K1 Max OS mode"
fi

# Before anything, make sure this repo is cloned into the correct path on Creality OS devices.
# If this is Creality OS and the path is wrong, it will re-clone the repo, run the install again, and exit.
ensure_creality_os_right_repo_path


# Check if cmd line arg "-uninstall" is passed, if so, do not run the system package install.
if (echo "$@" | grep -q -- '-uninstall'); then
    log_important "Uninstalling Mobileraker Companion..."
else
    # Next, make sure our required system packages are installed.
    # These are required for other actions in this script, so it must be done first.
    install_or_update_system_dependencies
    # Now make sure the virtual env exists, is updated, and all of our currently required PY packages are updated.
    install_or_update_python_env
fi
# Before launching our PY script, set any vars it needs to know
# Pass all of the command line args, so they can be handled by the PY script.
# Note that USER can be empty string on some systems when running as root. This is fixed in the PY installer.
USERNAME=${USER}
USER_HOME=${HOME}
CMD_LINE_ARGS="$@"

# ToDo Adjust passed args to match the needs of mobileraker
PY_LAUNCH_JSON="{\"REPO_DIR\":\"${REPO_DIR}\",\"ENV_DIR\":\"${ENV_DIR}\",\"USERNAME\":\"${USERNAME}\",\"USER_HOME\":\"${USER_HOME}\",\"CMD_LINE_ARGS\":\"${CMD_LINE_ARGS}\"}"
log_info "Bootstrap done. Starting python installer..."

# Now launch into our py setup script, that does everything else required.
# Since we use a module for file includes, we need to set the path to the root of the module
# so python will find it.
export PYTHONPATH="${REPO_DIR}"

# We can't use pushd on Creality OS, so do this.
CURRENT_DIR=$(pwd)
cd "${REPO_DIR}" > /dev/null || exit 1

# Disable the timestamp on the next line, since this is part of a larger function.
# shellcheck disable=SC2154
if [ "$IS_SONIC_PAD_OS" -eq 1 ] || [ "$IS_K1_OS" -eq 1 ]; then
    # Creality OS only has a root user and we can't use sudo.
    "${ENV_DIR}/bin/python3" -B -m installer "$PY_LAUNCH_JSON"
else
    sudo "${ENV_DIR}/bin/python3" -B -m installer "$PY_LAUNCH_JSON"
fi
PY_EXIT_CODE=$?

cd "${CURRENT_DIR}" > /dev/null || exit 1

log_info "Python installer done."

# All done, let the user know what to do next.
log_blank
if [ $PY_EXIT_CODE -eq 0 ]; then
    log_important "Installation of Mobileraker Companion was successful."
else
    log_error "Installation of Mobileraker Companion failed!"
fi